1/*
2 This file is part of KHelpcenter.
3
4 Copyright (C) 2002 Cornelius Schumacher <schumacher@kde.org>
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public
8 License as published by the Free Software Foundation; either
9 version 2 of the License, or (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; see the file COPYING. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*/
21
22#include "kcmhelpcenter.h"
23
24#include "htmlsearchconfig.h"
25#include "docmetainfo.h"
26#include "prefs.h"
27#include "searchhandler.h"
28#include "searchengine.h"
29
30#include "kcmhelpcenteradaptor.h"
31
32#include <KConfig>
33#include <KDebug>
34#include <KLocale>
35#include <KGlobal>
36#include <KAboutData>
37#include <KDialog>
38#include <KStandardDirs>
39#include <KProcess>
40#include <KApplication>
41#include <KTemporaryFile>
42#include <KUrlRequester>
43#include <KMessageBox>
44#include <KLineEdit>
45
46#include <QTreeWidget>
47#include <QtDBus/QtDBus>
48#include <QLabel>
49#include <QLayout>
50#include <QHeaderView>
51#include <QProgressBar>
52#include <QTextEdit>
53
54#include <unistd.h>
55#include <sys/types.h>
56
57using namespace KHC;
58
59IndexDirDialog::IndexDirDialog( QWidget *parent )
60 : KDialog( parent )
61{
62 setModal( true );
63 setCaption( i18n("Change Index Folder") );
64 setButtons( Ok | Cancel );
65
66 QFrame *topFrame = new QFrame( this );
67 setMainWidget( topFrame );
68
69 QBoxLayout *urlLayout = new QHBoxLayout( topFrame );
70
71 QLabel *label = new QLabel( i18n("Index folder:"), topFrame );
72 urlLayout->addWidget( label );
73
74 mIndexUrlRequester = new KUrlRequester( topFrame );
75 mIndexUrlRequester->setMode( KFile::Directory | KFile::ExistingOnly |
76 KFile::LocalOnly );
77 urlLayout->addWidget( mIndexUrlRequester );
78
79 mIndexUrlRequester->setUrl( Prefs::indexDirectory() );
80 connect(mIndexUrlRequester->lineEdit(),SIGNAL(textChanged ( const QString & )), this, SLOT(slotUrlChanged( const QString &)));
81 slotUrlChanged( mIndexUrlRequester->lineEdit()->text() );
82
83 connect( this, SIGNAL( okClicked() ), SLOT( slotOk() ) );
84}
85
86void IndexDirDialog::slotUrlChanged( const QString &_url )
87{
88 enableButtonOk( !_url.isEmpty() );
89}
90
91
92void IndexDirDialog::slotOk()
93{
94 Prefs::setIndexDirectory( mIndexUrlRequester->url().url() );
95 accept();
96}
97
98
99IndexProgressDialog::IndexProgressDialog( QWidget *parent )
100 : KDialog( parent ),
101 mFinished( true )
102{
103 setCaption( i18n("Build Search Indices") );
104
105 QVBoxLayout *topLayout = new QVBoxLayout( mainWidget() );
106 topLayout->setMargin( marginHint() );
107 topLayout->setSpacing( spacingHint() );
108
109 mLabel = new QLabel( mainWidget() );
110 mLabel->setAlignment( Qt::AlignHCenter );
111 topLayout->addWidget( mLabel );
112
113 mProgressBar = new QProgressBar( mainWidget() );
114 topLayout->addWidget( mProgressBar );
115
116 mLogLabel = new QLabel( i18n("Index creation log:"), mainWidget() );
117 topLayout->addWidget( mLogLabel );
118
119 mLogView = new QTextEdit( mainWidget() );
120 mLogView->setReadOnly( true );
121 mLogView->setWordWrapMode( QTextOption::NoWrap );
122 mLogView->setMinimumHeight( 200 );
123 topLayout->addWidget( mLogView );
124
125 setButtons( User1 | Close );
126 connect( this, SIGNAL( closeClicked() ), SLOT( slotEnd() ) );
127 connect( this, SIGNAL( user1Clicked() ), SLOT( toggleDetails() ) );
128
129 hideDetails();
130
131 setFinished( false );
132}
133
134IndexProgressDialog::~IndexProgressDialog()
135{
136 if ( !mLogView->isHidden() ) {
137 KConfigGroup cfg(KGlobal::config(), "indexprogressdialog");
138 cfg.writeEntry( "size", size() );
139 }
140}
141
142void IndexProgressDialog::setTotalSteps( int steps )
143{
144 mProgressBar->setRange( 0, steps );
145 mProgressBar->setValue( 0 );
146 setFinished( false );
147 mLogView->clear();
148}
149
150void IndexProgressDialog::advanceProgress()
151{
152 mProgressBar->setValue( mProgressBar->value() + 1 );
153}
154
155void IndexProgressDialog::setLabelText( const QString &text )
156{
157 mLabel->setText( text );
158}
159
160void IndexProgressDialog::setMinimumLabelWidth( int width )
161{
162 mLabel->setMinimumWidth( width );
163}
164
165void IndexProgressDialog::setFinished( bool finished )
166{
167 if ( finished == mFinished ) return;
168
169 mFinished = finished;
170
171 if ( mFinished ) {
172 setButtonText( Close, i18nc("Label for button to close search index progress dialog after successful completion", "Close") );
173 mLabel->setText( i18n("Index creation finished.") );
174 mProgressBar->setValue( mProgressBar->maximum() );
175 } else {
176 setButtonText( Close, i18nc("Label for stopping search index generation before completion", "Stop") );
177 }
178}
179
180void IndexProgressDialog::appendLog( const QString &text )
181{
182 mLogView->append( text );
183}
184
185void IndexProgressDialog::slotEnd()
186{
187 if ( mFinished ) {
188 emit closed();
189 accept();
190 } else {
191 emit cancelled();
192 reject();
193 }
194}
195
196void IndexProgressDialog::toggleDetails()
197{
198 KConfigGroup cfg(KGlobal::config(), "indexprogressdialog");
199 if ( mLogView->isHidden() ) {
200 mLogLabel->show();
201 mLogView->show();
202 setButtonText( User1, i18n("Details &lt;&lt;") );
203 QSize size = cfg.readEntry( "size", QSize() );
204 if ( !size.isEmpty() ) resize( size );
205 } else {
206 cfg.writeEntry( "size", size() );
207 hideDetails();
208 }
209}
210
211void IndexProgressDialog::hideDetails()
212{
213 mLogLabel->hide();
214 mLogView->hide();
215 setButtonText( User1, i18n("Details &gt;&gt;") );
216
217 // causes bug 166343
218 //layout()->activate();
219 adjustSize();
220}
221
222
223KCMHelpCenter::KCMHelpCenter( KHC::SearchEngine *engine, QWidget *parent,
224 const char *name)
225 : KDialog( parent ),
226 mEngine( engine ), mProgressDialog( 0 ), mCmdFile( 0 ),
227 mProcess( 0 ), mIsClosing( false ), mRunAsRoot( false )
228{
229 new KcmhelpcenterAdaptor(this);
230 QDBusConnection::sessionBus().registerObject(QLatin1String("/kcmhelpcenter"), this);
231 setObjectName( name );
232 setCaption( i18n("Build Search Index") );
233 setButtons( Ok | Cancel );
234
235 QWidget *widget = new QWidget( this );
236 setMainWidget( widget );
237
238 setupMainWidget( widget );
239
240 setButtonGuiItem( KDialog::Ok, KGuiItem(i18n("Build Index")) );
241
242 mConfig = KGlobal::config();
243
244 DocMetaInfo::self()->scanMetaInfo();
245
246 load();
247 const QString dbusInterface = "org.kde.khelpcenter.kcmhelpcenter";
248 QDBusConnection dbus = QDBusConnection::sessionBus();
249 bool success = dbus.connect(QString(), "/kcmhelpcenter", dbusInterface, "buildIndexProgress", this, SLOT(slotIndexProgress()));
250 if ( !success )
251 kError() << "connect D-Bus signal failed" << endl;
252 success = dbus.connect(QString(), "/kcmhelpcenter", dbusInterface, "buildIndexError", this, SLOT(slotIndexError(const QString&)));
253 if ( !success )
254 kError() << "connect D-Bus signal failed" << endl;
255 KConfigGroup id( mConfig, "IndexDialog" );
256 restoreDialogSize( id );
257}
258
259KCMHelpCenter::~KCMHelpCenter()
260{
261 KConfigGroup cg( KGlobal::config(), "IndexDialog" );
262 KDialog::saveDialogSize( cg );
263}
264
265void KCMHelpCenter::setupMainWidget( QWidget *parent )
266{
267 QVBoxLayout *topLayout = new QVBoxLayout( parent );
268 topLayout->setSpacing( KDialog::spacingHint() );
269
270 QString helpText =
271 i18n("To be able to search a document, a search\n"
272 "index needs to exist. The status column of the list below shows whether an index\n"
273 "for a document exists.\n") +
274 i18n("To create an index, check the box in the list and press the\n"
275 "\"Build Index\" button.\n");
276
277 QLabel *label = new QLabel( helpText, parent );
278 topLayout->addWidget( label );
279
280 mListView = new QTreeWidget( parent );
281 //mListView->setFullWidth( true );
282 mListView->setColumnCount(2);
283 mListView->setHeaderLabels( QStringList() << i18n("Search Scope") << i18n("Status") );
284 topLayout->addWidget( mListView );
285 // not just itemClicked, so that Key_Space also triggers it (#123954)
286 connect( mListView, SIGNAL(itemChanged(QTreeWidgetItem*,int)),
287 SLOT(checkSelection()) );
288
289 QBoxLayout *urlLayout = new QHBoxLayout();
290 topLayout->addLayout( urlLayout );
291
292 QLabel *urlLabel = new QLabel( i18n("Index folder:"), parent );
293 urlLayout->addWidget( urlLabel );
294
295 mIndexDirLabel = new QLabel( parent );
296 urlLayout->addWidget( mIndexDirLabel, 1 );
297
298 QPushButton *button = new QPushButton( i18n("Change..."), parent );
299 connect( button, SIGNAL( clicked() ), SLOT( showIndexDirDialog() ) );
300 urlLayout->addWidget( button );
301
302 QBoxLayout *buttonLayout = new QHBoxLayout();
303 topLayout->addLayout( buttonLayout );
304
305 buttonLayout->addStretch( 1 );
306
307 connect( this, SIGNAL( okClicked() ), SLOT( slotOk() ) );
308}
309
310void KCMHelpCenter::defaults()
311{
312}
313
314bool KCMHelpCenter::save()
315{
316 kDebug(1401) << "KCMHelpCenter::save()";
317
318 if ( !QFile::exists( Prefs::indexDirectory() ) ) {
319 KMessageBox::sorry( this,
320 i18n("<qt>The folder <b>%1</b> does not exist; unable to create index.</qt>",
321 Prefs::indexDirectory() ) );
322 return false;
323 } else {
324 return buildIndex();
325 }
326
327 return true;
328}
329
330void KCMHelpCenter::load()
331{
332 mIndexDirLabel->setText( Prefs::indexDirectory() );
333
334 mListView->clear();
335
336 const DocEntry::List &entries = DocMetaInfo::self()->docEntries();
337 DocEntry::List::ConstIterator it;
338 for( it = entries.begin(); it != entries.end(); ++it ) {
339// kDebug(1401) << "Entry: " << (*it)->name() << " Indexer: '"
340// << (*it)->indexer() << "'" << endl;
341 if ( mEngine->needsIndex( *it ) ) {
342 ScopeItem *item = new ScopeItem( mListView, *it );
343 item->setOn( (*it)->searchEnabled() );
344 }
345 }
346
347 mListView->header()->setResizeMode( QHeaderView::ResizeToContents );
348
349 updateStatus();
350}
351
352void KCMHelpCenter::updateStatus()
353{
354 QTreeWidgetItemIterator it( mListView );
355 while ( (*it) != 0 ) {
356 ScopeItem *item = static_cast<ScopeItem *>( (*it) );
357 QString status;
358 if ( item->entry()->indexExists( Prefs::indexDirectory() ) ) {
359 status = i18nc("Describes the status of a documentation index that is present", "OK");
360 item->setOn( false );
361 } else {
362 status = i18nc("Describes the status of a documentation index that is missing", "Missing");
363 }
364 item->setText( 1, status );
365
366 ++it;
367 }
368
369 checkSelection();
370}
371
372bool KCMHelpCenter::buildIndex()
373{
374 kDebug(1401) << "Build Index";
375
376 kDebug() << "IndexPath: '" << Prefs::indexDirectory() << "'";
377
378 if ( mProcess ) {
379 kError() << "Error: Index Process still running." << endl;
380 return false;
381 }
382
383 mIndexQueue.clear();
384
385 QFontMetrics fm( font() );
386 int maxWidth = 0;
387
388 mCmdFile = new KTemporaryFile;
389 if ( !mCmdFile->open() ) {
390 kError() << "Error opening command file." << endl;
391 deleteCmdFile();
392 return false;
393 }
394
395 QTextStream ts ( mCmdFile );
396 kDebug() << "Writing to file '" << mCmdFile->fileName() << "'";
397
398 bool hasError = false;
399
400 QTreeWidgetItemIterator it( mListView );
401 while ( (*it) != 0 ) {
402 ScopeItem *item = static_cast<ScopeItem *>( (*it) );
403 if ( item->isOn() ) {
404 DocEntry *entry = item->entry();
405
406 QString docText = i18nc(" Generic prefix label for error messages when creating documentation index, first arg is the document's identifier, second is the document's name", "Document '%1' (%2):\n",
407 entry->identifier() ,
408 entry->name() );
409 if ( entry->documentType().isEmpty() ) {
410 KMessageBox::sorry( this, docText +
411 i18n("No document type.") );
412 hasError = true;
413 } else {
414 QString error;
415 SearchHandler *handler = mEngine->handler( entry->documentType() );
416 if ( !handler ) {
417 KMessageBox::sorry( this, docText +
418 i18n("No search handler available for document type '%1'.",
419 entry->documentType() ) );
420 hasError = true;
421 } else if ( !handler->checkPaths( &error ) ) {
422 KMessageBox::sorry( this, docText + error );
423 hasError = true;
424 } else {
425 QString indexer = handler->indexCommand( entry->identifier() );
426 if ( indexer.isEmpty() ) {
427 KMessageBox::sorry( this, docText +
428 i18n("No indexing command specified for document type '%1'.",
429 entry->documentType() ) );
430 hasError = true;
431 } else {
432 indexer.replace( QLatin1String("%i" ), entry->identifier() );
433 indexer.replace( QLatin1String( "%d" ), Prefs::indexDirectory() );
434 indexer.replace( QLatin1String( "%p" ), entry->url() );
435 kDebug() << "INDEXER: " << indexer;
436 ts << indexer << endl;
437
438 int width = fm.width( entry->name() );
439 if ( width > maxWidth ) maxWidth = width;
440
441 mIndexQueue.append( entry );
442 }
443 }
444 }
445 }
446 ++it;
447 }
448
449 ts.flush();
450
451 if ( mIndexQueue.isEmpty() ) {
452 deleteCmdFile();
453 return !hasError;
454 }
455
456 mCurrentEntry = mIndexQueue.constBegin();
457 QString name = (*mCurrentEntry)->name();
458
459 if ( !mProgressDialog ) {
460 mProgressDialog = new IndexProgressDialog( parentWidget() );
461 connect( mProgressDialog, SIGNAL( cancelled() ),
462 SLOT( cancelBuildIndex() ) );
463 connect( mProgressDialog, SIGNAL( closed() ),
464 SLOT( slotProgressClosed() ) );
465 }
466 mProgressDialog->setLabelText( name );
467 mProgressDialog->setTotalSteps( mIndexQueue.count() );
468 mProgressDialog->setMinimumLabelWidth( maxWidth );
469 mProgressDialog->show();
470
471 startIndexProcess();
472
473 return true;
474}
475
476void KCMHelpCenter::startIndexProcess()
477{
478 kDebug() << "KCMHelpCenter::startIndexProcess()";
479
480 mProcess = new KProcess;
481#ifndef Q_WS_WIN
482 if ( mRunAsRoot ) {
483 QString kdesu = KStandardDirs::findExe("kdesu");
484 if(kdesu.isEmpty()) {
485 kError() << "Failed to run index process as root - could not find kdesu";
486 } else {
487 *mProcess << kdesu;
488 if(parentWidget()) {
489 *mProcess << "--attach" << QString::number(parentWidget()->window()->winId());
490 kDebug() << "Run as root, attaching kdesu to winid " << QString::number(parentWidget()->window()->winId());
491 }
492 *mProcess << "--";
493 }
494 }
495#endif
496
497 *mProcess << KStandardDirs::findExe("khc_indexbuilder");
498 *mProcess << mCmdFile->fileName();
499 *mProcess << Prefs::indexDirectory();
500
501 mProcess->setOutputChannelMode(KProcess::SeparateChannels);
502 connect( mProcess, SIGNAL( readyReadStandardError() ),
503 SLOT( slotReceivedStderr() ) );
504 connect( mProcess, SIGNAL( readyReadStandardOutput() ),
505 SLOT( slotReceivedStdout() ) );
506 connect( mProcess, SIGNAL( finished(int, QProcess::ExitStatus) ),
507 SLOT( slotIndexFinished(int, QProcess::ExitStatus) ) );
508
509 mProcess->start();
510 if (!mProcess->waitForStarted()) {
511 kError() << "KCMHelpcenter::startIndexProcess(): Failed to start process.";
512 deleteProcess();
513 deleteCmdFile();
514 }
515}
516
517void KCMHelpCenter::cancelBuildIndex()
518{
519 kDebug() << "cancelBuildIndex()";
520
521 deleteProcess();
522 deleteCmdFile();
523 mIndexQueue.clear();
524
525 if ( mIsClosing ) {
526 mIsClosing = false;
527 }
528}
529
530void KCMHelpCenter::slotIndexFinished(int exitCode, QProcess::ExitStatus exitStatus)
531{
532 kDebug() << "KCMHelpCenter::slotIndexFinished()";
533
534 if ( exitStatus == QProcess::NormalExit && exitCode == 2 ) {
535 if ( mRunAsRoot ) {
536 kError() << "Insufficient permissions." << endl;
537 } else {
538 kDebug() << "Insufficient permissions. Trying again as root.";
539 mRunAsRoot = true;
540 deleteProcess();
541 startIndexProcess();
542 return;
543 }
544 } else if ( exitStatus != QProcess::NormalExit || exitCode != 0 ) {
545 kDebug() << "KProcess reported an error.";
546 KMessageBox::error( this, i18n("Failed to build index.") );
547 } else {
548 mConfig->group( "Search" ).writeEntry( "IndexExists", true );
549 emit searchIndexUpdated();
550 }
551
552 deleteProcess();
553 deleteCmdFile();
554
555 if ( mProgressDialog ) {
556 mProgressDialog->setFinished( true );
557 }
558
559 mStdOut.clear();
560 mStdErr.clear();
561
562 if ( mIsClosing ) {
563 if ( !mProgressDialog || !mProgressDialog->isVisible() ) {
564 mIsClosing = false;
565 accept();
566 }
567 }
568}
569
570void KCMHelpCenter::deleteProcess()
571{
572 delete mProcess;
573 mProcess = 0;
574}
575
576void KCMHelpCenter::deleteCmdFile()
577{
578 delete mCmdFile;
579 mCmdFile = 0;
580}
581
582void KCMHelpCenter::slotIndexProgress()
583{
584 if( !mProcess )
585 return;
586
587 kDebug() << "KCMHelpCenter::slotIndexProgress()";
588
589 updateStatus();
590
591 advanceProgress();
592}
593
594void KCMHelpCenter::slotIndexError( const QString &str )
595{
596 if( !mProcess )
597 return;
598
599 kDebug() << "KCMHelpCenter::slotIndexError()";
600
601 KMessageBox::sorry( this, i18n("Error executing indexing build command:\n%1",
602 str ) );
603
604 if ( mProgressDialog ) {
605 mProgressDialog->appendLog( "<i>" + str + "</i>" );
606 }
607
608 advanceProgress();
609}
610
611void KCMHelpCenter::advanceProgress()
612{
613 if ( mProgressDialog && mProgressDialog->isVisible() ) {
614 mProgressDialog->advanceProgress();
615 mCurrentEntry++;
616 if ( mCurrentEntry != mIndexQueue.constEnd() ) {
617 QString name = (*mCurrentEntry)->name();
618 mProgressDialog->setLabelText( name );
619 }
620 }
621}
622
623void KCMHelpCenter::slotReceivedStdout()
624{
625 QByteArray text= mProcess->readAllStandardOutput();
626 int pos = text.lastIndexOf( '\n' );
627 if ( pos < 0 ) {
628 mStdOut.append( text );
629 } else {
630 if ( mProgressDialog ) {
631 mProgressDialog->appendLog( mStdOut + text.left( pos ) );
632 mStdOut = text.mid( pos + 1 );
633 }
634 }
635}
636
637void KCMHelpCenter::slotReceivedStderr( )
638{
639 QByteArray text = mProcess->readAllStandardError();
640 int pos = text.lastIndexOf( '\n' );
641 if ( pos < 0 ) {
642 mStdErr.append( text );
643 } else {
644 if ( mProgressDialog ) {
645 mProgressDialog->appendLog( QLatin1String("<i>") + mStdErr + text.left( pos ) +
646 QLatin1String("</i>"));
647 mStdErr = text.mid( pos + 1 );
648 }
649 }
650}
651
652void KCMHelpCenter::slotOk()
653{
654 if ( buildIndex() ) {
655 if ( !mProcess ) accept();
656 else mIsClosing = true;
657 }
658}
659
660void KCMHelpCenter::slotProgressClosed()
661{
662 kDebug() << "KCMHelpCenter::slotProgressClosed()";
663
664 if ( mIsClosing ) accept();
665}
666
667void KCMHelpCenter::showIndexDirDialog()
668{
669 IndexDirDialog dlg( this );
670 if ( dlg.exec() == QDialog::Accepted ) {
671 load();
672 }
673}
674
675void KCMHelpCenter::checkSelection()
676{
677 int count = 0;
678
679 QTreeWidgetItemIterator it( mListView );
680 while ( (*it) != 0 ) {
681 ScopeItem *item = static_cast<ScopeItem *>( (*it) );
682 if ( item->isOn() ) {
683 ++count;
684 }
685 ++it;
686 }
687
688 enableButtonOk( count != 0 );
689}
690
691#include "kcmhelpcenter.moc"
692
693// vim:ts=2:sw=2:et
694