1/*
2 * This file is part of the KDE Help Center
3 *
4 * Copyright (C) 1999 Matthias Elter (me@kde.org)
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (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
14 * GNU 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; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 */
20
21#include "navigator.h"
22
23#include <QDir>
24#include <QFile>
25#include <QPixmap>
26
27#include <QLabel>
28#include <QtXml/QtXml>
29#include <QTextStream>
30#include <QRegExp>
31#include <QLayout>
32#include <QPushButton>
33#include <QFrame>
34#include <QHBoxLayout>
35#include <QBoxLayout>
36#include <QVBoxLayout>
37#include <QTreeWidgetItemIterator>
38
39#include <KAction>
40#include <KApplication>
41#include <KConfig>
42#include <KStandardDirs>
43#include <KGlobal>
44#include <KLocale>
45#include <KDebug>
46
47#include <KLineEdit>
48#include <KMessageBox>
49#include <KIconLoader>
50#include <KCharsets>
51#include <KDialog>
52#include <KDesktopFile>
53#include <KProtocolInfo>
54#include <KServiceGroup>
55#include <KServiceTypeTrader>
56#include <KCModuleInfo>
57#include <KCModule>
58
59#include <sys/types.h>
60#include <sys/stat.h>
61#include <unistd.h>
62
63#include "navigatoritem.h"
64#include "navigatorappitem.h"
65#include "searchwidget.h"
66#include "searchengine.h"
67#include "docmetainfo.h"
68#include "docentrytraverser.h"
69#include "glossary.h"
70#include "toc.h"
71#include "view.h"
72#include "infotree.h"
73#include "mainwindow.h"
74#include "plugintraverser.h"
75#include "scrollkeepertreebuilder.h"
76#include "kcmhelpcenter.h"
77#include "formatter.h"
78#include "history.h"
79#include "prefs.h"
80
81using namespace KHC;
82
83Navigator::Navigator( View *view, QWidget *parent, const char *name )
84 : QWidget( parent ), mIndexDialog( 0 ),
85 mView( view ), mSelected( false )
86{
87 setObjectName( name );
88
89 KConfigGroup config(KGlobal::config(), "General");
90 mShowMissingDocs = config.readEntry("ShowMissingDocs", false);
91
92 mSearchEngine = new SearchEngine( view );
93 connect( mSearchEngine, SIGNAL( searchFinished() ),
94 SLOT( slotSearchFinished() ) );
95
96 DocMetaInfo::self()->scanMetaInfo();
97
98 QBoxLayout *topLayout = new QVBoxLayout( this );
99
100 mSearchFrame = new QFrame( this );
101 topLayout->addWidget( mSearchFrame );
102
103 QBoxLayout *searchLayout = new QHBoxLayout( mSearchFrame );
104 searchLayout->setSpacing( KDialog::spacingHint() );
105 searchLayout->setMargin( 6 );
106
107 mSearchEdit = new KLineEdit( mSearchFrame );
108 mSearchEdit->setClearButtonShown(true);
109 searchLayout->addWidget( mSearchEdit );
110 connect( mSearchEdit, SIGNAL( returnPressed() ), SLOT( slotSearch() ) );
111 connect( mSearchEdit, SIGNAL( textChanged( const QString & ) ),
112 SLOT( checkSearchButton() ) );
113
114 mSearchButton = new QPushButton( i18n("&Search"), mSearchFrame );
115 searchLayout->addWidget( mSearchButton );
116 connect( mSearchButton, SIGNAL( clicked() ), SLOT( slotSearch() ) );
117
118 mTabWidget = new QTabWidget( this );
119 topLayout->addWidget( mTabWidget );
120
121 setupContentsTab();
122 setupGlossaryTab();
123 setupSearchTab();
124
125 insertPlugins();
126 hideSearch();
127/*
128 if ( !mSearchEngine->initSearchHandlers() ) {
129 hideSearch();
130 } else {
131 mSearchWidget->updateScopeList();
132 mSearchWidget->readConfig( KGlobal::config().data() );
133 }
134 */
135 connect( mTabWidget, SIGNAL( currentChanged( QWidget * ) ),
136 SLOT( slotTabChanged( QWidget * ) ) );
137}
138
139Navigator::~Navigator()
140{
141 delete mSearchEngine;
142}
143
144SearchEngine *Navigator::searchEngine() const
145{
146 return mSearchEngine;
147}
148
149Formatter *Navigator::formatter() const
150{
151 return mView->formatter();
152}
153
154bool Navigator::showMissingDocs() const
155{
156 return mShowMissingDocs;
157}
158
159void Navigator::setupContentsTab()
160{
161 mContentsTree = new QTreeWidget( mTabWidget );
162 mContentsTree->setFrameStyle( QFrame::NoFrame );
163 mContentsTree->setAllColumnsShowFocus(true);
164 mContentsTree->setRootIsDecorated(false);
165 mContentsTree->headerItem()->setHidden(true);
166
167 connect(mContentsTree, SIGNAL(itemActivated(QTreeWidgetItem*,int)),
168 SLOT(slotItemSelected(QTreeWidgetItem*)));
169
170 mTabWidget->addTab(mContentsTree, i18n("&Contents"));
171}
172
173void Navigator::setupSearchTab()
174{
175
176 mSearchWidget = new SearchWidget( mSearchEngine, mTabWidget );
177 connect( mSearchWidget, SIGNAL( searchResult( const QString & ) ),
178 SLOT( slotShowSearchResult( const QString & ) ) );
179 connect( mSearchWidget, SIGNAL( scopeCountChanged( int ) ),
180 SLOT( checkSearchButton() ) );
181 connect( mSearchWidget, SIGNAL( showIndexDialog() ),
182 SLOT( showIndexDialog() ) );
183
184 mTabWidget->addTab( mSearchWidget, i18n("Search Options"));
185
186}
187
188void Navigator::setupGlossaryTab()
189{
190 mGlossaryTree = new Glossary( mTabWidget );
191 connect( mGlossaryTree, SIGNAL( entrySelected( const GlossaryEntry & ) ),
192 this, SIGNAL( glossSelected( const GlossaryEntry & ) ) );
193 mTabWidget->addTab( mGlossaryTree, i18n( "G&lossary" ) );
194}
195
196void Navigator::insertPlugins()
197{
198 PluginTraverser t( this, mContentsTree );
199 DocMetaInfo::self()->traverseEntries( &t );
200}
201
202void Navigator::insertParentAppDocs( const QString &name, NavigatorItem *topItem )
203{
204 kDebug(1400) << "Requested plugin documents for ID " << name;
205
206 KServiceGroup::Ptr grp = KServiceGroup::childGroup( name );
207 if ( !grp )
208 return;
209
210 KServiceGroup::List entries = grp->entries();
211 KServiceGroup::List::ConstIterator it = entries.constBegin();
212 KServiceGroup::List::ConstIterator end = entries.constEnd();
213 for ( ; it != end; ++it ) {
214 QString desktopFile = ( *it )->entryPath();
215 if ( QDir::isRelativePath( desktopFile ) )
216 desktopFile = KStandardDirs::locate( "apps", desktopFile );
217 createItemFromDesktopFile( topItem, desktopFile );
218 }
219}
220
221void Navigator::insertKCMDocs( const QString &name, NavigatorItem *topItem, const QString &type )
222{
223 kDebug(1400) << "Requested KCM documents for ID" << name;
224 QString systemsettingskontrolconstraint = "[X-KDE-System-Settings-Parent-Category] != ''";
225 QString konquerorcontrolconstraint = "[X-KDE-PluginKeyword] == 'khtml_general'\
226 or [X-KDE-PluginKeyword] == 'performance'\
227 or [X-KDE-PluginKeyword] == 'bookmarks'";
228 QString filemanagercontrolconstraint = "[X-KDE-PluginKeyword] == 'behavior'\
229 or [X-KDE-PluginKeyword] == 'dolphinviewmodes'\
230 or [X-KDE-PluginKeyword] == 'dolphinnavigation'\
231 or [X-KDE-PluginKeyword] == 'dolphinservices'\
232 or [X-KDE-PluginKeyword] == 'dolphingeneral'\
233 or [X-KDE-PluginKeyword] == 'trash'";
234 QString browsercontrolconstraint = "[X-KDE-PluginKeyword] == 'khtml_behavior'\
235 or [X-KDE-PluginKeyword] == 'proxy'\
236 or [X-KDE-PluginKeyword] == 'khtml_appearance'\
237 or [X-KDE-PluginKeyword] == 'khtml_filter'\
238 or [X-KDE-PluginKeyword] == 'cache'\
239 or [X-KDE-PluginKeyword] == 'cookie'\
240 or [X-KDE-PluginKeyword] == 'useragent'\
241 or [X-KDE-PluginKeyword] == 'khtml_java_js'\
242 or [X-KDE-PluginKeyword] == 'khtml_plugins'";
243/* missing in browsercontrolconstraint
244History no X-KDE-PluginKeyword in kcmhistory.desktop
245*/
246 QString othercontrolconstraint = "[X-KDE-PluginKeyword] == 'cgi'";
247
248 KService::List list;
249
250 if ( type == QString("kcontrol") ) {
251 list = KServiceTypeTrader::self()->query( "KCModule", systemsettingskontrolconstraint );
252 } else if ( type == QString("konquerorcontrol") ) {
253 list = KServiceTypeTrader::self()->query( "KCModule", konquerorcontrolconstraint );
254 } else if ( type == QString("browsercontrol") ) {
255 list = KServiceTypeTrader::self()->query( "KCModule", browsercontrolconstraint );
256 } else if ( type == QString("filemanagercontrol") ) {
257 list = KServiceTypeTrader::self()->query( "KCModule", filemanagercontrolconstraint );
258 } else if ( type == QString("othercontrol") ) {
259 list = KServiceTypeTrader::self()->query( "KCModule", othercontrolconstraint );
260 } else if ( type == QString("kinfocenter") ) {
261 list = KServiceTypeTrader::self()->query( "KCModule", "[X-KDE-ParentApp] == 'kinfocenter'" );
262 }
263
264 for ( KService::List::const_iterator it = list.constBegin(); it != list.constEnd(); ++it )
265 {
266 KService::Ptr s = (*it);
267 KCModuleInfo m = KCModuleInfo(s);
268 QString desktopFile = KStandardDirs::locate( "services", m.fileName() );
269 createItemFromDesktopFile( topItem, desktopFile );
270 }
271 topItem->sortChildren( 0, Qt::AscendingOrder /* ascending */ );
272}
273
274void Navigator::insertIOSlaveDocs( const QString &name, NavigatorItem *topItem )
275{
276 kDebug(1400) << "Requested IOSlave documents for ID" << name;
277
278 QStringList list = KProtocolInfo::protocols();
279 list.sort();
280
281 NavigatorItem *prevItem = 0;
282 for ( QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it )
283 {
284 QString docPath = KProtocolInfo::docPath(*it);
285 if ( !docPath.isNull() )
286 {
287 // First parameter is ignored if second is an absolute path
288 KUrl url(KUrl("help:/"), docPath);
289 QString icon = KProtocolInfo::icon(*it);
290 if ( icon.isEmpty() ) icon = "text-plain";
291 DocEntry *entry = new DocEntry( *it, url.url(), icon );
292 NavigatorItem *item = new NavigatorItem( entry, topItem, prevItem );
293 prevItem = item;
294 item->setAutoDeleteDocEntry( true );
295 }
296 }
297}
298
299void Navigator::createItemFromDesktopFile( NavigatorItem *topItem,
300 const QString &file )
301{
302 KDesktopFile desktopFile( file );
303 QString docPath = desktopFile.readDocPath();
304 if ( !docPath.isNull() ) {
305 // First parameter is ignored if second is an absolute path
306 KUrl url(KUrl("help:/"), docPath);
307 QString icon = desktopFile.readIcon();
308 if ( icon.isEmpty() ) icon = "text-plain";
309 DocEntry *entry = new DocEntry( desktopFile.readName(), url.url(), icon );
310 NavigatorItem *item = new NavigatorItem( entry, topItem );
311 item->setAutoDeleteDocEntry( true );
312 }
313}
314
315void Navigator::insertInfoDocs( NavigatorItem *topItem )
316{
317 InfoTree *infoTree = new InfoTree( this );
318 infoTree->build( topItem );
319}
320
321NavigatorItem *Navigator::insertScrollKeeperDocs( NavigatorItem *topItem,
322 NavigatorItem *after )
323{
324 ScrollKeeperTreeBuilder *builder = new ScrollKeeperTreeBuilder( this );
325 return builder->build( topItem, after );
326}
327
328void Navigator::selectItem( const KUrl &url )
329{
330 kDebug() << "Navigator::selectItem(): " << url.url();
331
332 if ( url.url() == "khelpcenter:home" ) {
333 clearSelection();
334 return;
335 }
336
337 // help:/foo&anchor=bar gets redirected to help:/foo#bar
338 // Make sure that we match both the original URL as well as
339 // its counterpart.
340 KUrl alternativeURL = url;
341 if (url.hasRef())
342 {
343 alternativeURL.setQuery("anchor="+url.ref());
344 alternativeURL.setRef(QString());
345 }
346
347 // If the navigator already has the given URL selected, do nothing.
348 NavigatorItem *item;
349 item = static_cast<NavigatorItem *>( mContentsTree->currentItem() );
350 if ( item && mSelected ) {
351 KUrl currentURL ( item->entry()->url() );
352 if ( (currentURL == url) || (currentURL == alternativeURL) ) {
353 kDebug() << "URL already shown.";
354 return;
355 }
356 }
357
358 // First, populate the NavigatorAppItems if we don't want the home page
359 if ( url != homeURL() ) {
360 QTreeWidgetItemIterator it1( mContentsTree );
361 while( (*it1) )
362 {
363 NavigatorAppItem *appItem = dynamic_cast<NavigatorAppItem *>( (*it1) );
364 if ( appItem ) appItem->populate( true );
365 ++it1;
366 }
367 }
368
369 QTreeWidgetItemIterator it( mContentsTree );
370 while ( (*it) ) {
371 NavigatorItem *item = static_cast<NavigatorItem *>( (*it) );
372 KUrl itemUrl( item->entry()->url() );
373 if ( (itemUrl == url) || (itemUrl == alternativeURL) ) {
374 mContentsTree->setCurrentItem( item );
375 // If the current item was not selected and remained unchanged it
376 // needs to be explicitly selected
377 mContentsTree->setCurrentItem(item);
378 item->setExpanded( true );
379 break;
380 }
381 ++it;
382 }
383 if ( !(*it) ) {
384 clearSelection();
385 } else {
386 mSelected = true;
387 }
388}
389
390void Navigator::clearSelection()
391{
392 mContentsTree->clearSelection();
393 mSelected = false;
394}
395
396void Navigator::slotItemSelected( QTreeWidgetItem *currentItem )
397{
398 if ( !currentItem ) return;
399
400 mSelected = true;
401
402 NavigatorItem *item = static_cast<NavigatorItem *>( currentItem );
403
404 kDebug(1400) << item->entry()->name() << endl;
405
406 item->setExpanded( !item->isExpanded() );
407
408 KUrl url ( item->entry()->url() );
409
410
411
412 if ( url.protocol() == "khelpcenter" ) {
413 mView->closeUrl();
414 History::self().updateCurrentEntry( mView );
415 History::self().createEntry();
416 showOverview( item, url );
417 } else {
418
419 emit itemSelected( url.url() );
420 }
421
422 mLastUrl = url;
423}
424
425void Navigator::openInternalUrl( const KUrl &url )
426{
427 if ( url.url() == "khelpcenter:home" ) {
428 clearSelection();
429 showOverview( 0, url );
430 return;
431 }
432
433 selectItem( url );
434 if ( !mSelected ) return;
435
436 NavigatorItem *item =
437 static_cast<NavigatorItem *>( mContentsTree->currentItem() );
438
439 if ( item ) showOverview( item, url );
440}
441
442void Navigator::showOverview( NavigatorItem *item, const KUrl &url )
443{
444 mView->beginInternal( url );
445
446 QString fileName = KStandardDirs::locate( "data", "khelpcenter/index.html.in" );
447 if ( fileName.isEmpty() )
448 return;
449
450 QFile file( fileName );
451
452 if ( !file.open( QIODevice::ReadOnly ) )
453 return;
454
455 QTextStream stream( &file );
456 QString res = stream.readAll();
457
458 QString title,name,content;
459 uint childCount;
460
461 if ( item ) {
462 title = item->entry()->name();
463 name = item->entry()->name();
464
465 QString info = item->entry()->info();
466 if ( !info.isEmpty() ) content = QLatin1String("<p>") + info + QLatin1String("</p>\n");
467
468 childCount = item->childCount();
469 } else {
470 title = i18n("Start Page");
471 name = i18n("KDE Help Center");
472
473 childCount = mContentsTree->topLevelItemCount();
474 }
475
476 if ( childCount > 0 ) {
477 QTreeWidgetItem *child;
478 if ( item ) child = item;
479 else child = mContentsTree->invisibleRootItem();
480
481 mDirLevel = 0;
482
483 content += createChildrenList( child );
484 }
485 else
486 content += QLatin1String("<p></p>");
487
488 res = res.arg(title).arg(name).arg(content);
489
490 mView->write( res );
491
492 mView->end();
493}
494
495QString Navigator::createChildrenList( QTreeWidgetItem *child )
496{
497 ++mDirLevel;
498
499 QString t;
500
501 t += QLatin1String("<ul>\n");
502
503 int cc = child->childCount();
504 for (int i=0;i<cc;i++)
505 {
506 NavigatorItem *childItem = static_cast<NavigatorItem *>( child->child(i) );
507
508 DocEntry *e = childItem->entry();
509
510 t += QLatin1String("<li><a href=\"") + e->url() + QLatin1String("\">");
511 if ( e->isDirectory() ) t += QLatin1String("<b>");
512 t += e->name();
513 if ( e->isDirectory() ) t += QLatin1String("</b>");
514 t += QLatin1String("</a>");
515
516 if ( !e->info().isEmpty() ) {
517 t += QLatin1String("<br>") + e->info();
518 }
519
520 t += QLatin1String("</li>\n");
521
522 if ( childItem->childCount() > 0 && mDirLevel < 2 ) {
523 t += createChildrenList( childItem );
524 }
525
526 }
527
528 t += QLatin1String("</ul>\n");
529
530 --mDirLevel;
531
532 return t;
533}
534
535void Navigator::slotSearch()
536{
537
538 kDebug(1400) << "Navigator::slotSearch()";
539
540 if ( !checkSearchIndex() ) return;
541
542 if ( mSearchEngine->isRunning() ) return;
543
544 QString words = mSearchEdit->text();
545 QString method = mSearchWidget->method();
546 int pages = mSearchWidget->pages();
547 QString scope = mSearchWidget->scope();
548
549 kDebug(1400) << "Navigator::slotSearch() words: " << words;
550 kDebug(1400) << "Navigator::slotSearch() scope: " << scope;
551
552 if ( words.isEmpty() || scope.isEmpty() ) return;
553
554 // disable search Button during searches
555 mSearchButton->setEnabled(false);
556 QApplication::setOverrideCursor(Qt::WaitCursor);
557
558 if ( !mSearchEngine->search( words, method, pages, scope ) ) {
559 slotSearchFinished();
560 KMessageBox::sorry( this, i18n("Unable to run search program.") );
561 }
562
563}
564
565void Navigator::slotShowSearchResult( const QString &url )
566{
567 QString u = url;
568 u.replace( "%k", mSearchEdit->text() );
569
570 emit itemSelected( u );
571}
572
573void Navigator::slotSearchFinished()
574{
575 mSearchButton->setEnabled(true);
576 QApplication::restoreOverrideCursor();
577
578 kDebug( 1400 ) << "Search finished.";
579}
580
581void Navigator::checkSearchButton()
582{
583 mSearchButton->setEnabled( !mSearchEdit->text().isEmpty() &&
584 mSearchWidget->scopeCount() > 0 );
585 mTabWidget->setCurrentIndex( mTabWidget->indexOf( mSearchWidget ) );
586}
587
588
589void Navigator::hideSearch()
590{
591 mSearchFrame->hide();
592 mTabWidget->removeTab( mTabWidget->indexOf( mSearchWidget ) );
593}
594
595bool Navigator::checkSearchIndex()
596{
597 KConfigGroup cfg(KGlobal::config(), "Search" );
598 if ( cfg.readEntry( "IndexExists", false) ) return true;
599
600 if ( mIndexDialog && !mIndexDialog->isHidden() ) return true;
601
602 QString text = i18n( "A search index does not yet exist. Do you want "
603 "to create the index now?" );
604
605 int result = KMessageBox::questionYesNo( this, text, QString(),
606 KGuiItem(i18n("Create")),
607 KGuiItem(i18n("Do Not Create")),
608 QLatin1String("indexcreation") );
609 if ( result == KMessageBox::Yes ) {
610 showIndexDialog();
611 return false;
612 }
613
614 return true;
615}
616
617void Navigator::slotTabChanged( QWidget *wid )
618{
619 if ( wid == mSearchWidget ) checkSearchIndex();
620}
621
622void Navigator::slotSelectGlossEntry( const QString &id )
623{
624 mGlossaryTree->slotSelectGlossEntry( id );
625}
626
627KUrl Navigator::homeURL()
628{
629 if ( !mHomeUrl.isEmpty() ) return mHomeUrl;
630
631 KSharedConfig::Ptr cfg = KGlobal::config();
632 // We have to reparse the configuration here in order to get a
633 // language-specific StartUrl, e.g. "StartUrl[de]".
634 cfg->reparseConfiguration();
635 mHomeUrl = cfg->group("General").readPathEntry( "StartUrl", QLatin1String("khelpcenter:home") );
636 return mHomeUrl;
637}
638
639void Navigator::showIndexDialog()
640{
641 if ( !mIndexDialog ) {
642 mIndexDialog = new KCMHelpCenter( mSearchEngine, this );
643 connect( mIndexDialog, SIGNAL( searchIndexUpdated() ), mSearchWidget,
644 SLOT( updateScopeList() ) );
645 }
646 mIndexDialog->show();
647 mIndexDialog->raise();
648}
649
650void Navigator::readConfig()
651{
652 if ( Prefs::currentTab() == Prefs::Search ) {
653 mTabWidget->setCurrentIndex( mTabWidget->indexOf( mSearchWidget ) );
654 } else if ( Prefs::currentTab() == Prefs::Glossary ) {
655 mTabWidget->setCurrentIndex( mTabWidget->indexOf( mGlossaryTree ) );
656 } else {
657 mTabWidget->setCurrentIndex( mTabWidget->indexOf( mContentsTree ) );
658 }
659}
660
661void Navigator::writeConfig()
662{
663 if ( mTabWidget->currentWidget() == mSearchWidget ) {
664 Prefs::setCurrentTab( Prefs::Search );
665 } else if ( mTabWidget->currentWidget() == mGlossaryTree ) {
666 Prefs::setCurrentTab( Prefs::Glossary );
667 } else {
668 Prefs::setCurrentTab( Prefs::Content );
669 }
670}
671
672void Navigator::clearSearch()
673{
674 mSearchEdit->setText( QString() );
675}
676
677#include "navigator.moc"
678
679// vim:ts=2:sw=2:et
680