source: trunk/tools/assistant/compat/helpdialog.cpp@ 507

Last change on this file since 507 was 2, checked in by Dmitry A. Kuminov, 16 years ago

Initially imported qt-all-opensource-src-4.5.1 from Trolltech.

File size: 41.6 KB
Line 
1/****************************************************************************
2**
3** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
4** Contact: Qt Software Information ([email protected])
5**
6** This file is part of the Qt Assistant of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial Usage
10** Licensees holding valid Qt Commercial licenses may use this file in
11** accordance with the Qt Commercial License Agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and Nokia.
14**
15** GNU Lesser General Public License Usage
16** Alternatively, this file may be used under the terms of the GNU Lesser
17** General Public License version 2.1 as published by the Free Software
18** Foundation and appearing in the file LICENSE.LGPL included in the
19** packaging of this file. Please review the following information to
20** ensure the GNU Lesser General Public License version 2.1 requirements
21** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
22**
23** In addition, as a special exception, Nokia gives you certain
24** additional rights. These rights are described in the Nokia Qt LGPL
25** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this
26** package.
27**
28** GNU General Public License Usage
29** Alternatively, this file may be used under the terms of the GNU
30** General Public License version 3.0 as published by the Free Software
31** Foundation and appearing in the file LICENSE.GPL included in the
32** packaging of this file. Please review the following information to
33** ensure the GNU General Public License version 3.0 requirements will be
34** met: http://www.gnu.org/copyleft/gpl.html.
35**
36** If you are unsure which license is appropriate for your use, please
37** contact the sales department at [email protected].
38** $QT_END_LICENSE$
39**
40****************************************************************************/
41
42#include "helpdialog.h"
43#include "helpwindow.h"
44#include "topicchooser.h"
45#include "docuparser.h"
46#include "mainwindow.h"
47#include "config.h"
48#include "tabbedbrowser.h"
49
50#include <QtGui>
51#include <QtDebug>
52#include <QtCore/QVarLengthArray>
53
54#include <stdlib.h>
55#include <limits.h>
56
57QT_BEGIN_NAMESPACE
58
59enum
60{
61 LinkRole = Qt::UserRole + 1000
62};
63
64static bool verifyDirectory(const QString &str)
65{
66 QFileInfo dirInfo(str);
67 if (!dirInfo.exists())
68 return QDir().mkdir(str);
69 if (!dirInfo.isDir()) {
70 qWarning("'%s' exists but is not a directory", str.toLatin1().constData());
71 return false;
72 }
73 return true;
74}
75
76struct IndexKeyword {
77 IndexKeyword(const QString &kw, const QString &l)
78 : keyword(kw), link(l) {}
79 IndexKeyword() : keyword(QString()), link(QString()) {}
80 bool operator<(const IndexKeyword &ik) const {
81 return keyword.toLower() < ik.keyword.toLower();
82 }
83 bool operator<=(const IndexKeyword &ik) const {
84 return keyword.toLower() <= ik.keyword.toLower();
85 }
86 bool operator>(const IndexKeyword &ik) const {
87 return keyword.toLower() > ik.keyword.toLower();
88 }
89 Q_DUMMY_COMPARISON_OPERATOR(IndexKeyword)
90 QString keyword;
91 QString link;
92};
93
94QDataStream &operator>>(QDataStream &s, IndexKeyword &ik)
95{
96 s >> ik.keyword;
97 s >> ik.link;
98 return s;
99}
100
101QDataStream &operator<<(QDataStream &s, const IndexKeyword &ik)
102{
103 s << ik.keyword;
104 s << ik.link;
105 return s;
106}
107
108QValidator::State SearchValidator::validate(QString &str, int &) const
109{
110 for (int i = 0; i < (int) str.length(); ++i) {
111 QChar c = str[i];
112 if (!c.isLetterOrNumber() && c != QLatin1Char('\'') && c != QLatin1Char('`')
113 && c != QLatin1Char('\"') && c != QLatin1Char(' ') && c != QLatin1Char('-') && c != QLatin1Char('_')
114 && c!= QLatin1Char('*'))
115 return QValidator::Invalid;
116 }
117 return QValidator::Acceptable;
118}
119
120class IndexListModel: public QStringListModel
121{
122public:
123 IndexListModel(QObject *parent = 0)
124 : QStringListModel(parent) {}
125
126 void clear() { contents.clear(); setStringList(QStringList()); }
127
128 QString description(int index) const { return stringList().at(index); }
129 QStringList links(int index) const { return contents.values(stringList().at(index)); }
130 void addLink(const QString &description, const QString &link) { contents.insert(description, link); }
131
132 void publish() { filter(QString(), QString()); }
133
134 QModelIndex filter(const QString &s, const QString &real);
135
136 virtual Qt::ItemFlags flags(const QModelIndex &index) const
137 { return QStringListModel::flags(index) & ~Qt::ItemIsEditable; }
138
139private:
140 QMultiMap<QString, QString> contents;
141};
142
143bool caseInsensitiveLessThan(const QString &as, const QString &bs)
144{
145 const QChar *a = as.unicode();
146 const QChar *b = bs.unicode();
147 if (a == 0)
148 return true;
149 if (b == 0)
150 return false;
151 if (a == b)
152 return false;
153 int l=qMin(as.length(),bs.length());
154 while (l-- && QChar::toLower(a->unicode()) == QChar::toLower(b->unicode()))
155 a++,b++;
156 if (l==-1)
157 return (as.length() < bs.length());
158 return QChar::toLower(a->unicode()) < QChar::toLower(b->unicode());
159}
160
161/**
162 * \a real is kinda a hack for the smart search, need a way to match a regexp to an item
163 * How would you say the best match for Q.*Wiget is QWidget?
164 */
165QModelIndex IndexListModel::filter(const QString &s, const QString &real)
166{
167 QStringList list;
168
169 int goodMatch = -1;
170 int perfectMatch = -1;
171 if (s.isEmpty())
172 perfectMatch = 0;
173
174 const QRegExp regExp(s, Qt::CaseInsensitive);
175 QMultiMap<QString, QString>::iterator it = contents.begin();
176 QString lastKey;
177 for (; it != contents.end(); ++it) {
178 if (it.key() == lastKey)
179 continue;
180 lastKey = it.key();
181 const QString key = it.key();
182 if (key.contains(regExp) || key.contains(s, Qt::CaseInsensitive)) {
183 list.append(key);
184 if (perfectMatch == -1 && (key.startsWith(real, Qt::CaseInsensitive))) {
185 if (goodMatch == -1)
186 goodMatch = list.count() - 1;
187 if (real.length() == key.length()){
188 perfectMatch = list.count() - 1;
189 }
190 } else if (perfectMatch > -1 && s == key) {
191 perfectMatch = list.count() - 1;
192 }
193 }
194 }
195
196 int bestMatch = perfectMatch;
197 if (bestMatch == -1)
198 bestMatch = goodMatch;
199 bestMatch = qMax(0, bestMatch);
200
201 // sort the new list
202 QString match;
203 if (bestMatch >= 0 && list.count() > bestMatch)
204 match = list[bestMatch];
205 qSort(list.begin(), list.end(), caseInsensitiveLessThan);
206 setStringList(list);
207 for (int i = 0; i < list.size(); ++i) {
208 if (list.at(i) == match){
209 bestMatch = i;
210 break;
211 }
212 }
213 return index(bestMatch, 0, QModelIndex());
214}
215
216HelpNavigationListItem::HelpNavigationListItem(QListWidget *ls, const QString &txt)
217 : QListWidgetItem(txt, ls)
218{
219}
220
221void HelpNavigationListItem::addLink(const QString &link)
222{
223 QString lnk = HelpDialog::removeAnchorFromLink(link);
224 if (linkList.filter(lnk, Qt::CaseInsensitive).count() > 0)
225 return;
226 linkList << link;
227}
228
229HelpDialog::HelpDialog(QWidget *parent, MainWindow *h)
230 : QWidget(parent), lwClosed(false), help(h)
231{
232 ui.setupUi(this);
233 ui.listContents->setUniformRowHeights(true);
234 ui.listContents->header()->setStretchLastSection(false);
235 ui.listContents->header()->setResizeMode(QHeaderView::ResizeToContents);
236 ui.listBookmarks->setUniformRowHeights(true);
237 ui.listBookmarks->header()->setStretchLastSection(false);
238 ui.listBookmarks->header()->setResizeMode(QHeaderView::ResizeToContents);
239
240 indexModel = new IndexListModel(this);
241 ui.listIndex->setModel(indexModel);
242 ui.listIndex->setLayoutMode(QListView::Batched);
243 ui.listBookmarks->setItemHidden(ui.listBookmarks->headerItem(), true);
244 ui.listContents->setItemHidden(ui.listContents->headerItem(), true);
245 ui.searchButton->setShortcut(QKeySequence(Qt::ALT|Qt::SHIFT|Qt::Key_S));
246}
247
248void HelpDialog::initialize()
249{
250 connect(ui.tabWidget, SIGNAL(currentChanged(int)), this, SLOT(currentTabChanged(int)));
251
252 connect(ui.listContents, SIGNAL(itemActivated(QTreeWidgetItem*,int)), this, SLOT(showTopic(QTreeWidgetItem*)));
253 connect(ui.listContents, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showTreeItemMenu(QPoint)));
254 ui.listContents->viewport()->installEventFilter(this);
255
256 connect(ui.editIndex, SIGNAL(returnPressed()), this, SLOT(showTopic()));
257 connect(ui.editIndex, SIGNAL(textEdited(QString)), this, SLOT(searchInIndex(QString)));
258
259 connect(ui.listIndex, SIGNAL(activated(QModelIndex)), this, SLOT(showTopic()));
260 connect(ui.listIndex, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showIndexItemMenu(QPoint)));
261
262 connect(ui.listBookmarks, SIGNAL(itemActivated(QTreeWidgetItem*,int)), this, SLOT(showTopic(QTreeWidgetItem*)));
263 connect(ui.listBookmarks, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showTreeItemMenu(QPoint)));
264
265 connect(ui.termsEdit, SIGNAL(textChanged(const QString&)), this, SLOT(updateSearchButton(const QString&)));
266
267 connect(ui.resultBox, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showListItemMenu(QPoint)));
268
269 cacheFilesPath = QDir::homePath() + QLatin1String("/.assistant"); //### Find a better location for the dbs
270
271 ui.editIndex->installEventFilter(this);
272
273 ui.framePrepare->hide();
274 connect(qApp, SIGNAL(lastWindowClosed()), SLOT(lastWinClosed()));
275
276 ui.termsEdit->setValidator(new SearchValidator(ui.termsEdit));
277
278 actionOpenCurrentTab = new QAction(this);
279 actionOpenCurrentTab->setText(tr("Open Link in Current Tab"));
280
281 actionOpenLinkInNewWindow = new QAction(this);
282 actionOpenLinkInNewWindow->setText(tr("Open Link in New Window"));
283
284 actionOpenLinkInNewTab = new QAction(this);
285 actionOpenLinkInNewTab->setText(tr("Open Link in New Tab"));
286
287 itemPopup = new QMenu(this);
288 itemPopup->addAction(actionOpenCurrentTab);
289 itemPopup->addAction(actionOpenLinkInNewWindow);
290 itemPopup->addAction(actionOpenLinkInNewTab);
291
292 ui.tabWidget->setElideMode(Qt::ElideNone);
293
294 contentList.clear();
295
296 initDoneMsgShown = false;
297 fullTextIndex = 0;
298 indexDone = false;
299 titleMapDone = false;
300 contentsInserted = false;
301 bookmarksInserted = false;
302 setupTitleMap();
303
304}
305
306void HelpDialog::processEvents()
307{
308 qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
309}
310
311
312void HelpDialog::lastWinClosed()
313{
314 lwClosed = true;
315}
316
317void HelpDialog::removeOldCacheFiles(bool onlyFulltextSearchIndex)
318{
319 if (!verifyDirectory(cacheFilesPath)) {
320 qWarning("Failed to created assistant directory");
321 return;
322 }
323 QString pname = QLatin1String(".") + Config::configuration()->profileName();
324
325 QStringList fileList;
326 fileList << QLatin1String("indexdb40.dict")
327 << QLatin1String("indexdb40.doc");
328
329 if (!onlyFulltextSearchIndex)
330 fileList << QLatin1String("indexdb40") << QLatin1String("contentdb40");
331
332 QStringList::iterator it = fileList.begin();
333 for (; it != fileList.end(); ++it) {
334 if (QFile::exists(cacheFilesPath + QDir::separator() + *it + pname)) {
335 QFile f(cacheFilesPath + QDir::separator() + *it + pname);
336 f.remove();
337 }
338 }
339}
340
341void HelpDialog::timerEvent(QTimerEvent *e)
342{
343 Q_UNUSED(e);
344 static int opacity = 255;
345 help->setWindowOpacity((opacity-=4)/255.0);
346 if (opacity<=0)
347 qApp->quit();
348}
349
350
351void HelpDialog::loadIndexFile()
352{
353 if (indexDone)
354 return;
355
356 setCursor(Qt::WaitCursor);
357 indexDone = true;
358 ui.labelPrepare->setText(tr("Prepare..."));
359 ui.framePrepare->show();
360 processEvents();
361
362 QProgressBar *bar = ui.progressPrepare;
363 bar->setMaximum(100);
364 bar->setValue(0);
365
366 keywordDocuments.clear();
367 QList<IndexKeyword> lst;
368 QFile indexFile(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.") +
369 Config::configuration()->profileName());
370 if (!indexFile.open(QFile::ReadOnly)) {
371 buildKeywordDB();
372 processEvents();
373 if (lwClosed)
374 return;
375 if (!indexFile.open(QFile::ReadOnly)) {
376 QMessageBox::warning(help, tr("Qt Assistant"), tr("Failed to load keyword index file\n"
377 "Assistant will not work!"));
378#if defined Q_WS_WIN || defined Q_WS_MACX
379 startTimer(50);
380#endif
381 return;
382 }
383 }
384
385 QDataStream ds(&indexFile);
386 quint32 fileAges;
387 ds >> fileAges;
388 if (fileAges != getFileAges()) {
389 indexFile.close();
390 buildKeywordDB();
391 if (!indexFile.open(QFile::ReadOnly)) {
392 QMessageBox::warning(help, tr("Qt Assistant"),
393 tr("Cannot open the index file %1").arg(QFileInfo(indexFile).absoluteFilePath()));
394 return;
395 }
396 ds.setDevice(&indexFile);
397 ds >> fileAges;
398 }
399 ds >> lst;
400 indexFile.close();
401
402 bar->setValue(bar->maximum());
403 processEvents();
404
405 for (int i=0; i<lst.count(); ++i) {
406 const IndexKeyword &idx = lst.at(i);
407 indexModel->addLink(idx.keyword, idx.link);
408
409 keywordDocuments << HelpDialog::removeAnchorFromLink(idx.link);
410 }
411
412 indexModel->publish();
413
414 ui.framePrepare->hide();
415 showInitDoneMessage();
416 setCursor(Qt::ArrowCursor);
417}
418
419quint32 HelpDialog::getFileAges()
420{
421 QStringList addDocuFiles = Config::configuration()->docFiles();
422 QStringList::const_iterator i = addDocuFiles.constBegin();
423
424 quint32 fileAges = 0;
425 for (; i != addDocuFiles.constEnd(); ++i) {
426 QFileInfo fi(*i);
427 if (fi.exists())
428 fileAges += fi.lastModified().toTime_t();
429 }
430
431 return fileAges;
432}
433
434void HelpDialog::buildKeywordDB()
435{
436 QStringList addDocuFiles = Config::configuration()->docFiles();
437 QStringList::iterator i = addDocuFiles.begin();
438
439 // Set up an indeterminate progress bar.
440 ui.labelPrepare->setText(tr("Prepare..."));
441 ui.progressPrepare->setMaximum(0);
442 ui.progressPrepare->setMinimum(0);
443 ui.progressPrepare->setValue(0);
444 processEvents();
445
446 QList<IndexKeyword> lst;
447 quint32 fileAges = 0;
448 for (i = addDocuFiles.begin(); i != addDocuFiles.end(); ++i) {
449 QFile file(*i);
450 if (!file.exists()) {
451 QMessageBox::warning(this, tr("Warning"),
452 tr("Documentation file %1 does not exist!\n"
453 "Skipping file.").arg(QFileInfo(file).absoluteFilePath()));
454 continue;
455 }
456 fileAges += QFileInfo(file).lastModified().toTime_t();
457 DocuParser *handler = DocuParser::createParser(*i);
458 bool ok = handler->parse(&file);
459 file.close();
460 if (!ok){
461 QString msg = QString::fromLatin1("In file %1:\n%2")
462 .arg(QFileInfo(file).absoluteFilePath())
463 .arg(handler->errorProtocol());
464 QMessageBox::critical(this, tr("Parse Error"), tr(msg.toUtf8()));
465 delete handler;
466 continue;
467 }
468
469 QList<IndexItem*> indLst = handler->getIndexItems();
470 int counter = 0;
471 foreach (IndexItem *indItem, indLst) {
472 QFileInfo fi(indItem->reference);
473 lst.append(IndexKeyword(indItem->keyword, indItem->reference));
474
475 if (++counter%100 == 0) {
476 if (ui.progressPrepare)
477 ui.progressPrepare->setValue(counter);
478 processEvents();
479 if (lwClosed) {
480 return;
481 }
482 }
483 }
484 delete handler;
485 }
486 if (!lst.isEmpty())
487 qSort(lst);
488
489 QFile indexout(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.")
490 + Config::configuration()->profileName());
491 if (verifyDirectory(cacheFilesPath) && indexout.open(QFile::WriteOnly)) {
492 QDataStream s(&indexout);
493 s << fileAges;
494 s << lst;
495 indexout.close();
496 }
497}
498
499void HelpDialog::setupTitleMap()
500{
501 if (titleMapDone)
502 return;
503
504 bool needRebuild = false;
505 if (Config::configuration()->profileName() == QLatin1String("default")) {
506 const QStringList docuFiles = Config::configuration()->docFiles();
507 for (QStringList::ConstIterator it = docuFiles.begin(); it != docuFiles.end(); ++it) {
508 if (!QFile::exists(*it)) {
509 Config::configuration()->saveProfile(Profile::createDefaultProfile());
510 Config::configuration()->loadDefaultProfile();
511 needRebuild = true;
512 break;
513 }
514 }
515 }
516
517 if (Config::configuration()->docRebuild() || needRebuild) {
518 removeOldCacheFiles();
519 Config::configuration()->setDocRebuild(false);
520 Config::configuration()->saveProfile(Config::configuration()->profile());
521 }
522 if (contentList.isEmpty())
523 getAllContents();
524
525 titleMapDone = true;
526 titleMap.clear();
527 for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) {
528 ContentList lst = (*it).second;
529 foreach (ContentItem item, lst) {
530 titleMap[item.reference] = item.title.trimmed();
531 }
532 }
533 processEvents();
534}
535
536void HelpDialog::getAllContents()
537{
538 QFile contentFile(cacheFilesPath + QDir::separator() + QLatin1String("contentdb40.")
539 + Config::configuration()->profileName());
540 contentList.clear();
541 if (!contentFile.open(QFile::ReadOnly)) {
542 buildContentDict();
543 return;
544 }
545
546 QDataStream ds(&contentFile);
547 quint32 fileAges;
548 ds >> fileAges;
549 if (fileAges != getFileAges()) {
550 contentFile.close();
551 removeOldCacheFiles(true);
552 buildContentDict();
553 return;
554 }
555 QString key;
556 QList<ContentItem> lst;
557 while (!ds.atEnd()) {
558 ds >> key;
559 ds >> lst;
560 contentList += qMakePair(key, QList<ContentItem>(lst));
561 }
562 contentFile.close();
563 processEvents();
564
565}
566
567void HelpDialog::buildContentDict()
568{
569 QStringList docuFiles = Config::configuration()->docFiles();
570
571 quint32 fileAges = 0;
572 for (QStringList::iterator it = docuFiles.begin(); it != docuFiles.end(); ++it) {
573 QFile file(*it);
574 if (!file.exists()) {
575 QMessageBox::warning(this, tr("Warning"),
576 tr("Documentation file %1 does not exist!\n"
577 "Skipping file.").arg(QFileInfo(file).absoluteFilePath()));
578 continue;
579 }
580 fileAges += QFileInfo(file).lastModified().toTime_t();
581 DocuParser *handler = DocuParser::createParser(*it);
582 if (!handler) {
583 QMessageBox::warning(this, tr("Warning"),
584 tr("Documentation file %1 is not compatible!\n"
585 "Skipping file.").arg(QFileInfo(file).absoluteFilePath()));
586 continue;
587 }
588 bool ok = handler->parse(&file);
589 file.close();
590 if (ok) {
591 contentList += qMakePair(*it, QList<ContentItem>(handler->getContentItems()));
592 delete handler;
593 } else {
594 QString msg = QString::fromLatin1("In file %1:\n%2")
595 .arg(QFileInfo(file).absoluteFilePath())
596 .arg(handler->errorProtocol());
597 QMessageBox::critical(this, tr("Parse Error"), tr(msg.toUtf8()));
598 continue;
599 }
600 }
601
602 QFile contentOut(cacheFilesPath + QDir::separator() + QLatin1String("contentdb40.")
603 + Config::configuration()->profileName());
604 if (contentOut.open(QFile::WriteOnly)) {
605 QDataStream s(&contentOut);
606 s << fileAges;
607 for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) {
608 s << *it;
609 }
610 contentOut.close();
611 }
612}
613
614void HelpDialog::currentTabChanged(int index)
615{
616 QString s = ui.tabWidget->widget(index)->objectName();
617 if (s == QLatin1String("indexPage"))
618 QTimer::singleShot(0, this, SLOT(loadIndexFile()));
619 else if (s == QLatin1String("bookmarkPage"))
620 insertBookmarks();
621 else if (s == QLatin1String("contentPage"))
622 QTimer::singleShot(0, this, SLOT(insertContents()));
623 else if (s == QLatin1String("searchPage"))
624 QTimer::singleShot(0, this, SLOT(setupFullTextIndex()));
625}
626
627void HelpDialog::showInitDoneMessage()
628{
629 if (initDoneMsgShown)
630 return;
631 initDoneMsgShown = true;
632 help->statusBar()->showMessage(tr("Done"), 3000);
633}
634
635void HelpDialog::showTopic(QTreeWidgetItem *item)
636{
637 if (item)
638 showTopic();
639}
640
641void HelpDialog::showTopic()
642{
643 QString tabName = ui.tabWidget->currentWidget()->objectName();
644
645 if (tabName == QLatin1String("indexPage"))
646 showIndexTopic();
647 else if (tabName == QLatin1String("bookmarkPage"))
648 showBookmarkTopic();
649 else if (tabName == QLatin1String("contentPage"))
650 showContentsTopic();
651}
652
653void HelpDialog::showIndexTopic()
654{
655 int row = ui.listIndex->currentIndex().row();
656 if (row == -1 || row >= indexModel->rowCount())
657 return;
658
659 QString description = indexModel->description(row);
660 QStringList links = indexModel->links(row);
661
662 bool blocked = ui.editIndex->blockSignals(true);
663 ui.editIndex->setText(description);
664 ui.editIndex->blockSignals(blocked);
665
666 if (links.count() == 1) {
667 emit showLink(links.first());
668 } else {
669 qSort(links);
670 QStringList::Iterator it = links.begin();
671 QStringList linkList;
672 QStringList linkNames;
673 for (; it != links.end(); ++it) {
674 linkList << *it;
675 linkNames << titleOfLink(*it);
676 }
677 QString link = TopicChooser::getLink(this, linkNames, linkList, description);
678 if (!link.isEmpty())
679 emit showLink(link);
680 }
681
682 ui.listIndex->setCurrentIndex(indexModel->index(indexModel->stringList().indexOf(description)));
683 ui.listIndex->scrollTo(ui.listIndex->currentIndex(), QAbstractItemView::PositionAtTop);
684}
685
686void HelpDialog::searchInIndex(const QString &searchString)
687{
688 QRegExp atoz(QLatin1String("[A-Z]"));
689 int matches = searchString.count(atoz);
690 if (matches > 0 && !searchString.contains(QLatin1String(".*")))
691 {
692 int start = 0;
693 QString newSearch;
694 for (; matches > 0; --matches) {
695 int match = searchString.indexOf(atoz, start+1);
696 if (match <= start)
697 continue;
698 newSearch += searchString.mid(start, match-start);
699 newSearch += QLatin1String(".*");
700 start = match;
701 }
702 newSearch += searchString.mid(start);
703 ui.listIndex->setCurrentIndex(indexModel->filter(newSearch, searchString));
704 }
705 else
706 ui.listIndex->setCurrentIndex(indexModel->filter(searchString, searchString));
707}
708
709QString HelpDialog::titleOfLink(const QString &link)
710{
711 QString s = HelpDialog::removeAnchorFromLink(link);
712 s = titleMap[s];
713 if (s.isEmpty())
714 return link;
715 return s;
716}
717
718bool HelpDialog::eventFilter(QObject * o, QEvent * e)
719{
720 if (o == ui.editIndex && e->type() == QEvent::KeyPress) {
721 switch (static_cast<QKeyEvent*>(e)->key()) {
722 case Qt::Key_Up:
723 case Qt::Key_Down:
724 case Qt::Key_PageDown:
725 case Qt::Key_PageUp:
726 QApplication::sendEvent(ui.listIndex, e);
727 break;
728
729 default:
730 break;
731 }
732 } else if (o == ui.listContents->viewport()) {
733 if (e->type() == QEvent::MouseButtonRelease) {
734 QMouseEvent *me = static_cast<QMouseEvent*>(e);
735 if (me->button() == Qt::LeftButton) {
736 QTreeWidgetItem *item = ui.listContents->itemAt(me->pos());
737 QRect vRect = ui.listContents->visualItemRect(item);
738
739 // only show topic if we clicked an item
740 if (item && vRect.contains(me->pos()))
741 showTopic(item);
742 }
743 }
744 }
745
746 return QWidget::eventFilter(o, e);
747}
748
749void HelpDialog::addBookmark()
750{
751 if (!bookmarksInserted)
752 insertBookmarks();
753 QString link = help->browsers()->currentBrowser()->source().toString();
754 QString title = help->browsers()->currentBrowser()->documentTitle();
755 if (title.isEmpty())
756 title = titleOfLink(link);
757
758 QTreeWidgetItem *i = new QTreeWidgetItem(ui.listBookmarks, 0);
759 i->setText(0, title);
760 i->setData(0, LinkRole, link);
761 ui.buttonRemove->setEnabled(true);
762 saveBookmarks();
763 help->updateBookmarkMenu();
764}
765
766void HelpDialog::on_buttonAdd_clicked()
767{
768 addBookmark();
769}
770
771void HelpDialog::on_buttonRemove_clicked()
772{
773 if (!ui.listBookmarks->currentItem())
774 return;
775
776 delete ui.listBookmarks->currentItem();
777 saveBookmarks();
778 if (ui.listBookmarks->topLevelItemCount() != 0) {
779 ui.listBookmarks->setCurrentItem(ui.listBookmarks->topLevelItem(0));
780 }
781 ui.buttonRemove->setEnabled(ui.listBookmarks->topLevelItemCount() > 0);
782 help->updateBookmarkMenu();
783}
784
785void HelpDialog::insertBookmarks()
786{
787 if (bookmarksInserted)
788 return;
789 bookmarksInserted = true;
790 ui.listBookmarks->clear();
791 QFile f(cacheFilesPath + QDir::separator() + QLatin1String("bookmarks.")
792 + Config::configuration()->profileName());
793 if (!f.open(QFile::ReadOnly))
794 return;
795 QTextStream ts(&f);
796 while (!ts.atEnd()) {
797 QTreeWidgetItem *i = new QTreeWidgetItem(ui.listBookmarks, 0);
798 i->setText(0, ts.readLine());
799 i->setData(0, LinkRole, ts.readLine());
800 }
801 ui.buttonRemove->setEnabled(ui.listBookmarks->topLevelItemCount() > 0);
802 help->updateBookmarkMenu();
803 showInitDoneMessage();
804}
805
806void HelpDialog::showBookmarkTopic()
807{
808 if (!ui.listBookmarks->currentItem())
809 return;
810
811 QTreeWidgetItem *i = (QTreeWidgetItem*)ui.listBookmarks->currentItem();
812 emit showLink(i->data(0, LinkRole).toString());
813}
814
815static void store(QTreeWidgetItem *i, QTextStream &ts)
816{
817 ts << i->text(0) << endl;
818 ts << i->data(0, LinkRole).toString() << endl;
819
820 for (int index = 0; index < i->childCount(); ++index)
821 store(i->child(index), ts);
822}
823
824static void store(QTreeWidget *tw, QTextStream &ts)
825{
826 for (int index = 0; index < tw->topLevelItemCount(); ++index)
827 store(tw->topLevelItem(index), ts);
828}
829
830void HelpDialog::saveBookmarks()
831{
832 QFile f(cacheFilesPath + QDir::separator() + QLatin1String("bookmarks.")
833 + Config::configuration()->profileName());
834 if (!f.open(QFile::WriteOnly))
835 return;
836
837 QTextStream ts(&f);
838 store(ui.listBookmarks, ts);
839 f.close();
840}
841
842void HelpDialog::insertContents()
843{
844#ifdef Q_WS_MAC
845 static const QLatin1String IconPath(":/trolltech/assistant/images/mac/book.png");
846#else
847 static const QLatin1String IconPath(":/trolltech/assistant/images/win/book.png");
848#endif
849 if (contentsInserted)
850 return;
851
852 if (contentList.isEmpty())
853 getAllContents();
854
855 contentsInserted = true;
856 ui.listContents->clear();
857 setCursor(Qt::WaitCursor);
858 if (!titleMapDone)
859 setupTitleMap();
860
861#if 0 // ### port me
862 ui.listContents->setSorting(-1);
863#endif
864
865 for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) {
866 QTreeWidgetItem *newEntry = 0;
867
868 QTreeWidgetItem *contentEntry = 0;
869 QStack<QTreeWidgetItem*> stack;
870 stack.clear();
871 int depth = 0;
872 bool root = false;
873
874 const int depthSize = 32;
875 QVarLengthArray<QTreeWidgetItem*, depthSize> lastItem(depthSize);
876
877 ContentList lst = (*it).second;
878 for (ContentList::ConstIterator it = lst.constBegin(); it != lst.constEnd(); ++it) {
879 ContentItem item = *it;
880 if (item.depth == 0) {
881 lastItem[0] = 0;
882 newEntry = new QTreeWidgetItem(ui.listContents, 0);
883 newEntry->setIcon(0, QIcon(IconPath));
884 newEntry->setText(0, item.title);
885 newEntry->setData(0, LinkRole, item.reference);
886 stack.push(newEntry);
887 depth = 1;
888 root = true;
889 }
890 else{
891 if ((item.depth > depth) && root) {
892 depth = item.depth;
893 stack.push(contentEntry);
894 }
895 if (item.depth == depth) {
896 if (lastItem.capacity() == depth)
897 lastItem.resize(depth + depthSize);
898 contentEntry = new QTreeWidgetItem(stack.top(), lastItem[ depth ]);
899 lastItem[ depth ] = contentEntry;
900 contentEntry->setText(0, item.title);
901 contentEntry->setData(0, LinkRole, item.reference);
902 }
903 else if (item.depth < depth) {
904 stack.pop();
905 depth--;
906 item = *(--it);
907 }
908 }
909 }
910 processEvents();
911 }
912 setCursor(Qt::ArrowCursor);
913 showInitDoneMessage();
914}
915
916void HelpDialog::showContentsTopic()
917{
918 QTreeWidgetItem *i = (QTreeWidgetItem*)ui.listContents->currentItem();
919 if (!i)
920 return;
921 emit showLink(i->data(0, LinkRole).toString());
922}
923
924QTreeWidgetItem * HelpDialog::locateLink(QTreeWidgetItem *item, const QString &link)
925{
926 QTreeWidgetItem *child = 0;
927#ifdef Q_OS_WIN
928 Qt::CaseSensitivity checkCase = Qt::CaseInsensitive;
929#else
930 Qt::CaseSensitivity checkCase = Qt::CaseSensitive;
931#endif
932 for (int i = 0, childCount = item->childCount(); i<childCount; i++) {
933 child = item->child(i);
934 ///check whether it is this item
935 if (link.startsWith(child->data(0, LinkRole).toString(), checkCase))
936 break;
937 //check if the link is a child of this item
938 else if (child->childCount()) {
939 child = locateLink(child, link);
940 if (child)
941 break;
942 }
943 child = 0;
944 }
945 return child;
946}
947
948void HelpDialog::locateContents(const QString &link)
949{
950 //ensure the TOC is filled
951 if (!contentsInserted)
952 insertContents();
953#ifdef Q_OS_WIN
954 Qt::CaseSensitivity checkCase = Qt::CaseInsensitive;
955#else
956 Qt::CaseSensitivity checkCase = Qt::CaseSensitive;
957#endif
958 QString findLink(link);
959 //Installations on a windows local drive will give the 'link' as <file:///C:/xxx>
960 //and the contents in the TOC will be <file:C:/xxx>.
961 //But on others the 'link' of format <file:///root/xxx>
962 //and the contents in the TOC will be <file:/root/xxx>.
963 if (findLink.contains(QLatin1String("file:///"))) {
964 if (findLink[9] == QLatin1Char(':')) //on windows drives
965 findLink.replace(0, 8, QLatin1String("file:"));
966 else
967 findLink.replace(0, 8, QLatin1String("file:/"));
968 }
969
970 bool topLevel = false;
971 QTreeWidgetItem *item = 0;
972 int totalItems = ui.listContents->topLevelItemCount();
973
974 for (int i = 0; i < totalItems; i++ ) {
975 // first see if we are one of the top level items
976 item = (QTreeWidgetItem*)ui.listContents->topLevelItem(i);
977 if (findLink.startsWith(item->data(0, LinkRole).toString(), checkCase)) {
978 topLevel = true;
979 break;
980 }
981 }
982
983 if (!topLevel) {
984 // now try to find it in the sublevel items
985 for (int n = 0; n < totalItems; ++n) {
986 item = (QTreeWidgetItem*)ui.listContents->topLevelItem(n);
987 item = locateLink(item, findLink);
988 if (item)
989 break;
990 }
991 }
992
993 //remove the old selection
994 QList<QTreeWidgetItem *> selected = ui.listContents->selectedItems();
995 foreach(QTreeWidgetItem *sel, selected)
996 ui.listContents->setItemSelected(sel, false);
997
998 //set the TOC item and show
999 ui.listContents->setCurrentItem(item);
1000 ui.listContents->setItemSelected(item, true);
1001 ui.listContents->scrollToItem(item);
1002}
1003
1004void HelpDialog::toggleContents()
1005{
1006 if (!isVisible() || ui.tabWidget->currentIndex() != 0) {
1007 ui.tabWidget->setCurrentIndex(0);
1008 parentWidget()->show();
1009 }
1010 else
1011 parentWidget()->hide();
1012}
1013
1014void HelpDialog::toggleIndex()
1015{
1016 if (!isVisible() || ui.tabWidget->currentIndex() != 1 || !ui.editIndex->hasFocus()) {
1017 ui.tabWidget->setCurrentIndex(1);
1018 parentWidget()->show();
1019 ui.editIndex->setFocus();
1020 }
1021 else
1022 parentWidget()->hide();
1023}
1024
1025void HelpDialog::toggleBookmarks()
1026{
1027 if (!isVisible() || ui.tabWidget->currentIndex() != 2) {
1028 ui.tabWidget->setCurrentIndex(2);
1029 parentWidget()->show();
1030 }
1031 else
1032 parentWidget()->hide();
1033}
1034
1035void HelpDialog::toggleSearch()
1036{
1037 if (!isVisible() || ui.tabWidget->currentIndex() != 3) {
1038 ui.tabWidget->setCurrentIndex(3);
1039 parentWidget()->show();
1040 }
1041 else
1042 parentWidget()->hide();
1043}
1044
1045void HelpDialog::setupFullTextIndex()
1046{
1047 if (fullTextIndex)
1048 return;
1049
1050 QString pname = Config::configuration()->profileName();
1051 fullTextIndex = new Index(QStringList(), QDir::homePath()); // ### Is this correct ?
1052 if (!verifyDirectory(cacheFilesPath)) {
1053 QMessageBox::warning(help, tr("Qt Assistant"),
1054 tr("Failed to save fulltext search index\n"
1055 "Assistant will not work!"));
1056 return;
1057 }
1058 fullTextIndex->setDictionaryFile(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.dict.") + pname);
1059 fullTextIndex->setDocListFile(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.doc.") + pname);
1060 processEvents();
1061
1062 connect(fullTextIndex, SIGNAL(indexingProgress(int)),
1063 this, SLOT(setIndexingProgress(int)));
1064 QFile f(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.dict.") + pname);
1065 if (!f.exists()) {
1066 QString doc;
1067 QSet<QString> documentSet;
1068 QMap<QString, QString>::ConstIterator it = titleMap.constBegin();
1069 for (; it != titleMap.constEnd(); ++it) {
1070 doc = HelpDialog::removeAnchorFromLink(it.key());
1071 if (!doc.isEmpty())
1072 documentSet.insert(doc);
1073 }
1074 loadIndexFile();
1075 for ( QStringList::Iterator it = keywordDocuments.begin(); it != keywordDocuments.end(); ++it ) {
1076 if (!(*it).isEmpty())
1077 documentSet.insert(*it);
1078 }
1079 fullTextIndex->setDocList( documentSet.toList() );
1080
1081 help->statusBar()->clearMessage();
1082 setCursor(Qt::WaitCursor);
1083 ui.labelPrepare->setText(tr("Indexing files..."));
1084 ui.progressPrepare->setMaximum(100);
1085 ui.progressPrepare->reset();
1086 ui.progressPrepare->show();
1087 ui.framePrepare->show();
1088 processEvents();
1089 if (fullTextIndex->makeIndex() == -1)
1090 return;
1091 fullTextIndex->writeDict();
1092 ui.progressPrepare->setValue(100);
1093 ui.framePrepare->hide();
1094 setCursor(Qt::ArrowCursor);
1095 showInitDoneMessage();
1096 } else {
1097 setCursor(Qt::WaitCursor);
1098 help->statusBar()->showMessage(tr("Reading dictionary..."));
1099 processEvents();
1100 fullTextIndex->readDict();
1101 help->statusBar()->showMessage(tr("Done"), 3000);
1102 setCursor(Qt::ArrowCursor);
1103 }
1104 keywordDocuments.clear();
1105}
1106
1107void HelpDialog::setIndexingProgress(int prog)
1108{
1109 ui.progressPrepare->setValue(prog);
1110 processEvents();
1111}
1112
1113void HelpDialog::startSearch()
1114{
1115 QString str = ui.termsEdit->text();
1116 str = str.simplified();
1117 str = str.replace(QLatin1String("\'"), QLatin1String("\""));
1118 str = str.replace(QLatin1String("`"), QLatin1String("\""));
1119 QString buf = str;
1120 str = str.replace(QLatin1String("-"), QLatin1String(" "));
1121 str = str.replace(QRegExp(QLatin1String("\\s[\\S]?\\s")), QLatin1String(" "));
1122 terms = str.split(QLatin1Char(' '));
1123 QStringList termSeq;
1124 QStringList seqWords;
1125 QStringList::iterator it = terms.begin();
1126 for (; it != terms.end(); ++it) {
1127 (*it) = (*it).simplified();
1128 (*it) = (*it).toLower();
1129 (*it) = (*it).replace(QLatin1String("\""), QLatin1String(""));
1130 }
1131 if (str.contains(QLatin1Char('\"'))) {
1132 if ((str.count(QLatin1Char('\"')))%2 == 0) {
1133 int beg = 0;
1134 int end = 0;
1135 QString s;
1136 beg = str.indexOf(QLatin1Char('\"'), beg);
1137 while (beg != -1) {
1138 beg++;
1139 end = str.indexOf(QLatin1Char('\"'), beg);
1140 s = str.mid(beg, end - beg);
1141 s = s.toLower();
1142 s = s.simplified();
1143 if (s.contains(QLatin1Char('*'))) {
1144 QMessageBox::warning(this, tr("Full Text Search"),
1145 tr("Using a wildcard within phrases is not allowed."));
1146 return;
1147 }
1148 seqWords += s.split(QLatin1Char(' '));
1149 termSeq << s;
1150 beg = str.indexOf(QLatin1Char('\"'), end + 1);
1151 }
1152 } else {
1153 QMessageBox::warning(this, tr("Full Text Search"),
1154 tr("The closing quotation mark is missing."));
1155 return;
1156 }
1157 }
1158 setCursor(Qt::WaitCursor);
1159 foundDocs.clear();
1160 foundDocs = fullTextIndex->query(terms, termSeq, seqWords);
1161 QString msg = tr("%n document(s) found.", "", foundDocs.count());
1162 help->statusBar()->showMessage(tr(msg.toUtf8()), 3000);
1163 ui.resultBox->clear();
1164 for (it = foundDocs.begin(); it != foundDocs.end(); ++it)
1165 ui.resultBox->addItem(fullTextIndex->getDocumentTitle(*it));
1166
1167 terms.clear();
1168 bool isPhrase = false;
1169 QString s;
1170 for (int i = 0; i < (int)buf.length(); ++i) {
1171 if (buf[i] == QLatin1Char('\"')) {
1172 isPhrase = !isPhrase;
1173 s = s.simplified();
1174 if (!s.isEmpty())
1175 terms << s;
1176 s = QLatin1String("");
1177 } else if (buf[i] == QLatin1Char(' ') && !isPhrase) {
1178 s = s.simplified();
1179 if (!s.isEmpty())
1180 terms << s;
1181 s = QLatin1String("");
1182 } else
1183 s += buf[i];
1184 }
1185 if (!s.isEmpty())
1186 terms << s;
1187
1188 setCursor(Qt::ArrowCursor);
1189}
1190
1191void HelpDialog::on_helpButton_clicked()
1192{
1193 emit showLink(MainWindow::urlifyFileName(
1194 Config::configuration()->assistantDocPath() +
1195 QLatin1String("/assistant-manual.html#full-text-searching")));
1196}
1197
1198void HelpDialog::on_resultBox_itemActivated(QListWidgetItem *item)
1199{
1200 showResultPage(item);
1201}
1202
1203void HelpDialog::showResultPage(QListWidgetItem *item)
1204{
1205 if (item)
1206 emit showSearchLink(foundDocs[ui.resultBox->row(item)], terms);
1207}
1208
1209void HelpDialog::showIndexItemMenu(const QPoint &pos)
1210{
1211 QListView *listView = qobject_cast<QListView*>(sender());
1212 if (!listView)
1213 return;
1214
1215 QModelIndex idx = listView->indexAt(pos);
1216 if (!idx.isValid())
1217 return;
1218
1219 QAction *action = itemPopup->exec(listView->viewport()->mapToGlobal(pos));
1220 if (action == actionOpenCurrentTab) {
1221 showTopic();
1222 } else if (action) {
1223 HelpWindow *hw = help->browsers()->currentBrowser();
1224 QString itemName = idx.data().toString();
1225 ui.editIndex->setText(itemName);
1226 QStringList links = indexModel->links(idx.row());
1227 if (links.count() == 1) {
1228 if (action == actionOpenLinkInNewWindow)
1229 hw->openLinkInNewWindow(links.first());
1230 else
1231 hw->openLinkInNewPage(links.first());
1232 } else {
1233 QStringList::Iterator it = links.begin();
1234 QStringList linkList;
1235 QStringList linkNames;
1236 for (; it != links.end(); ++it) {
1237 linkList << *it;
1238 linkNames << titleOfLink(*it);
1239 }
1240 QString link = TopicChooser::getLink(this, linkNames, linkList, itemName);
1241 if (!link.isEmpty()) {
1242 if (action == actionOpenLinkInNewWindow)
1243 hw->openLinkInNewWindow(link);
1244 else
1245 hw->openLinkInNewPage(link);
1246 }
1247 }
1248 }
1249}
1250
1251void HelpDialog::showListItemMenu(const QPoint &pos)
1252{
1253 QListWidget *listWidget = qobject_cast<QListWidget*>(sender());
1254 if (!listWidget)
1255 return;
1256 QListWidgetItem *item = listWidget->itemAt(pos);
1257 if (!item)
1258 return;
1259
1260 QAction *action = itemPopup->exec(listWidget->viewport()->mapToGlobal(pos));
1261 if (action == actionOpenCurrentTab) {
1262 showResultPage(item);
1263 } else if (action) {
1264 HelpWindow *hw = help->browsers()->currentBrowser();
1265 QString link = foundDocs[ui.resultBox->row(item)];
1266 if (action == actionOpenLinkInNewWindow)
1267 hw->openLinkInNewWindow(link);
1268 else
1269 hw->openLinkInNewPage(link);
1270 }
1271}
1272
1273void HelpDialog::showTreeItemMenu(const QPoint &pos)
1274{
1275 QTreeWidget *treeWidget = qobject_cast<QTreeWidget*>(sender());
1276
1277 if (!treeWidget)
1278 return;
1279
1280 QTreeWidgetItem *item = treeWidget->itemAt(pos);
1281
1282 if (!item)
1283 return;
1284
1285 QAction *action = itemPopup->exec(treeWidget->viewport()->mapToGlobal(pos));
1286 if (action == actionOpenCurrentTab) {
1287 if (ui.tabWidget->currentWidget()->objectName() == QLatin1String("contentPage"))
1288 showContentsTopic();
1289 else
1290 showBookmarkTopic();
1291 } else if (action) {
1292 QTreeWidgetItem *i = (QTreeWidgetItem*)item;
1293 if (action == actionOpenLinkInNewWindow)
1294 help->browsers()->currentBrowser()->openLinkInNewWindow(i->data(0, LinkRole).toString());
1295 else
1296 help->browsers()->currentBrowser()->openLinkInNewPage(i->data(0, LinkRole).toString());
1297 }
1298}
1299
1300void HelpDialog::on_termsEdit_returnPressed()
1301{
1302 startSearch();
1303}
1304
1305void HelpDialog::updateSearchButton(const QString &txt)
1306{
1307 ui.searchButton->setDisabled(txt.isEmpty());
1308}
1309
1310void HelpDialog::on_searchButton_clicked()
1311{
1312 startSearch();
1313}
1314
1315QString HelpDialog::removeAnchorFromLink(const QString &link)
1316{
1317 int i = link.length();
1318 int j = link.lastIndexOf(QLatin1Char('/'));
1319 int l = link.lastIndexOf(QDir::separator());
1320 if (l > j)
1321 j = l;
1322 if (j > -1) {
1323 QString fileName = link.mid(j+1);
1324 int k = fileName.lastIndexOf(QLatin1Char('#'));
1325 if (k > -1)
1326 i = j + k + 1;
1327 }
1328 return link.left(i);
1329}
1330
1331QT_END_NAMESPACE
Note: See TracBrowser for help on using the repository browser.