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

Last change on this file since 507 was 2, checked in by Dmitry A. Kuminov, 17 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 }