tools/assistant/compat/helpdialog.cpp
author Alex Gilkes <alex.gilkes@nokia.com>
Mon, 11 Jan 2010 14:00:40 +0000
changeset 0 1918ee327afb
child 3 41300fa6a67c
permissions -rw-r--r--
Revision: 200952

/****************************************************************************
**
** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the Qt Assistant of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** No Commercial Usage
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights.  These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**
**
**
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "helpdialog.h"
#include "helpwindow.h"
#include "topicchooser.h"
#include "docuparser.h"
#include "mainwindow.h"
#include "config.h"
#include "tabbedbrowser.h"

#include <QtGui>
#include <QtDebug>
#include <QtCore/QVarLengthArray>

#include <stdlib.h>
#include <limits.h>

QT_BEGIN_NAMESPACE

enum
{
    LinkRole = Qt::UserRole + 1000
};

static bool verifyDirectory(const QString &str)
{
    QFileInfo dirInfo(str);
    if (!dirInfo.exists())
        return QDir().mkdir(str);
    if (!dirInfo.isDir()) {
        qWarning("'%s' exists but is not a directory", str.toLatin1().constData());
        return false;
    }
    return true;
}

struct IndexKeyword {
    IndexKeyword(const QString &kw, const QString &l)
        : keyword(kw), link(l) {}
    IndexKeyword() : keyword(QString()), link(QString()) {}
    bool operator<(const IndexKeyword &ik) const {
        return keyword.toLower() < ik.keyword.toLower();
    }
    bool operator<=(const IndexKeyword &ik) const {
        return keyword.toLower() <= ik.keyword.toLower();
    }
    bool operator>(const IndexKeyword &ik) const {
        return keyword.toLower() > ik.keyword.toLower();
    }
    Q_DUMMY_COMPARISON_OPERATOR(IndexKeyword)
    QString keyword;
    QString link;
};

QDataStream &operator>>(QDataStream &s, IndexKeyword &ik)
{
    s >> ik.keyword;
    s >> ik.link;
    return s;
}

QDataStream &operator<<(QDataStream &s, const IndexKeyword &ik)
{
    s << ik.keyword;
    s << ik.link;
    return s;
}

QValidator::State SearchValidator::validate(QString &str, int &) const
{
    for (int i = 0; i < (int) str.length(); ++i) {
        QChar c = str[i];
        if (!c.isLetterOrNumber() && c != QLatin1Char('\'') && c != QLatin1Char('`')
            && c != QLatin1Char('\"') && c != QLatin1Char(' ') && c != QLatin1Char('-') && c != QLatin1Char('_')
            && c!= QLatin1Char('*'))
            return QValidator::Invalid;
    }
    return QValidator::Acceptable;
}

class IndexListModel: public QStringListModel
{
public:
    IndexListModel(QObject *parent = 0)
        : QStringListModel(parent) {}

    void clear() { contents.clear(); setStringList(QStringList()); }

    QString description(int index) const { return stringList().at(index); }
    QStringList links(int index) const { return contents.values(stringList().at(index)); }
    void addLink(const QString &description, const QString &link) { contents.insert(description, link); }

    void publish() { filter(QString(), QString()); }

    QModelIndex filter(const QString &s, const QString &real);

    virtual Qt::ItemFlags flags(const QModelIndex &index) const
    { return QStringListModel::flags(index) & ~Qt::ItemIsEditable; }

private:
    QMultiMap<QString, QString> contents;
};

bool caseInsensitiveLessThan(const QString &as, const QString &bs)
{
    const QChar *a = as.unicode();
    const QChar *b = bs.unicode();
    if (a == 0)
        return true;
    if (b == 0)
        return false;
    if (a == b)
        return false;
    int l=qMin(as.length(),bs.length());
    while (l-- && QChar::toLower(a->unicode()) == QChar::toLower(b->unicode()))
        a++,b++;
    if (l==-1)
        return (as.length() < bs.length());
    return QChar::toLower(a->unicode()) < QChar::toLower(b->unicode());
}

/**
 * \a real is kinda a hack for the smart search, need a way to match a regexp to an item
 * How would you say the best match for Q.*Wiget is QWidget?
 */
QModelIndex IndexListModel::filter(const QString &s, const QString &real)
{
    QStringList list;

    int goodMatch = -1;
    int perfectMatch = -1;
    if (s.isEmpty())
        perfectMatch = 0;

    const QRegExp regExp(s, Qt::CaseInsensitive);
    QMultiMap<QString, QString>::iterator it = contents.begin();
    QString lastKey;
    for (; it != contents.end(); ++it) {
        if (it.key() == lastKey)
            continue;
        lastKey = it.key();
        const QString key = it.key();
        if (key.contains(regExp) || key.contains(s, Qt::CaseInsensitive)) {
            list.append(key);
            if (perfectMatch == -1 && (key.startsWith(real, Qt::CaseInsensitive))) {
                if (goodMatch == -1)
                    goodMatch = list.count() - 1;
                if (real.length() == key.length()){
                    perfectMatch = list.count() - 1;
                }
            }  else if (perfectMatch > -1 && s == key) {
                perfectMatch = list.count() - 1;
            }
        }
    }

    int bestMatch = perfectMatch;
    if (bestMatch == -1)
        bestMatch = goodMatch;
    bestMatch = qMax(0, bestMatch);

    // sort the new list
    QString match;
    if (bestMatch >= 0 && list.count() > bestMatch)
        match = list[bestMatch];
    qSort(list.begin(), list.end(), caseInsensitiveLessThan);
    setStringList(list);
    for (int i = 0; i < list.size(); ++i) {
        if (list.at(i) == match){
            bestMatch = i;
            break;
        }
    }
    return index(bestMatch, 0, QModelIndex());
}

HelpNavigationListItem::HelpNavigationListItem(QListWidget *ls, const QString &txt)
    : QListWidgetItem(txt, ls)
{
}

void HelpNavigationListItem::addLink(const QString &link)
{
    QString lnk = HelpDialog::removeAnchorFromLink(link);
    if (linkList.filter(lnk, Qt::CaseInsensitive).count() > 0)
        return;
    linkList << link;
}

HelpDialog::HelpDialog(QWidget *parent, MainWindow *h)
    : QWidget(parent), lwClosed(false), help(h)
{
    ui.setupUi(this);
    ui.listContents->setUniformRowHeights(true);
    ui.listContents->header()->setStretchLastSection(false);
    ui.listContents->header()->setResizeMode(QHeaderView::ResizeToContents);
    ui.listBookmarks->setUniformRowHeights(true);
    ui.listBookmarks->header()->setStretchLastSection(false);
    ui.listBookmarks->header()->setResizeMode(QHeaderView::ResizeToContents);

    indexModel = new IndexListModel(this);
    ui.listIndex->setModel(indexModel);
    ui.listIndex->setLayoutMode(QListView::Batched);
    ui.listBookmarks->setItemHidden(ui.listBookmarks->headerItem(), true);
    ui.listContents->setItemHidden(ui.listContents->headerItem(), true);
    ui.searchButton->setShortcut(QKeySequence(Qt::ALT|Qt::SHIFT|Qt::Key_S));
}

void HelpDialog::initialize()
{
    connect(ui.tabWidget, SIGNAL(currentChanged(int)), this, SLOT(currentTabChanged(int)));

    connect(ui.listContents, SIGNAL(itemActivated(QTreeWidgetItem*,int)), this, SLOT(showTopic(QTreeWidgetItem*)));
    connect(ui.listContents, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showTreeItemMenu(QPoint)));
    ui.listContents->viewport()->installEventFilter(this);

    connect(ui.editIndex, SIGNAL(returnPressed()), this, SLOT(showTopic()));
    connect(ui.editIndex, SIGNAL(textEdited(QString)), this, SLOT(searchInIndex(QString)));

    connect(ui.listIndex, SIGNAL(activated(QModelIndex)), this, SLOT(showTopic()));
    connect(ui.listIndex, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showIndexItemMenu(QPoint)));

    connect(ui.listBookmarks, SIGNAL(itemActivated(QTreeWidgetItem*,int)), this, SLOT(showTopic(QTreeWidgetItem*)));
    connect(ui.listBookmarks, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showTreeItemMenu(QPoint)));

    connect(ui.termsEdit, SIGNAL(textChanged(const QString&)), this, SLOT(updateSearchButton(const QString&)));

    connect(ui.resultBox, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showListItemMenu(QPoint)));

    cacheFilesPath = QDir::homePath() + QLatin1String("/.assistant"); //### Find a better location for the dbs

    ui.editIndex->installEventFilter(this);

    ui.framePrepare->hide();
    connect(qApp, SIGNAL(lastWindowClosed()), SLOT(lastWinClosed()));

    ui.termsEdit->setValidator(new SearchValidator(ui.termsEdit));

    actionOpenCurrentTab = new QAction(this);
    actionOpenCurrentTab->setText(tr("Open Link in Current Tab"));

    actionOpenLinkInNewWindow = new QAction(this);
    actionOpenLinkInNewWindow->setText(tr("Open Link in New Window"));

    actionOpenLinkInNewTab = new QAction(this);
    actionOpenLinkInNewTab->setText(tr("Open Link in New Tab"));

    itemPopup = new QMenu(this);
    itemPopup->addAction(actionOpenCurrentTab);
    itemPopup->addAction(actionOpenLinkInNewWindow);
    itemPopup->addAction(actionOpenLinkInNewTab);

    ui.tabWidget->setElideMode(Qt::ElideNone);

    contentList.clear();

    initDoneMsgShown = false;
    fullTextIndex = 0;
    indexDone = false;
    titleMapDone = false;
    contentsInserted = false;
    bookmarksInserted = false;
    setupTitleMap();

}

void HelpDialog::processEvents()
{
    qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
}


void HelpDialog::lastWinClosed()
{
    lwClosed = true;
}

void HelpDialog::removeOldCacheFiles(bool onlyFulltextSearchIndex)
{
    if (!verifyDirectory(cacheFilesPath)) {
        qWarning("Failed to created assistant directory");
        return;
    }
    QString pname = QLatin1String(".") + Config::configuration()->profileName();

    QStringList fileList;
    fileList << QLatin1String("indexdb40.dict")
        << QLatin1String("indexdb40.doc");

    if (!onlyFulltextSearchIndex)
        fileList << QLatin1String("indexdb40") << QLatin1String("contentdb40");

    QStringList::iterator it = fileList.begin();
    for (; it != fileList.end(); ++it) {
      if (QFile::exists(cacheFilesPath + QDir::separator() + *it + pname)) {
            QFile f(cacheFilesPath + QDir::separator() + *it + pname);
            f.remove();
        }
    }
}

void HelpDialog::timerEvent(QTimerEvent *e)
{
    Q_UNUSED(e);
    static int opacity = 255;
    help->setWindowOpacity((opacity-=4)/255.0);
    if (opacity<=0)
        qApp->quit();
}


void HelpDialog::loadIndexFile()
{
    if (indexDone)
        return;

    setCursor(Qt::WaitCursor);
    indexDone = true;
    ui.labelPrepare->setText(tr("Prepare..."));
    ui.framePrepare->show();
    processEvents();

    QProgressBar *bar = ui.progressPrepare;
    bar->setMaximum(100);
    bar->setValue(0);

    keywordDocuments.clear();
    QList<IndexKeyword> lst;
    QFile indexFile(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.") +
                     Config::configuration()->profileName());
    if (!indexFile.open(QFile::ReadOnly)) {
        buildKeywordDB();
        processEvents();
        if (lwClosed)
            return;
        if (!indexFile.open(QFile::ReadOnly)) {
            QMessageBox::warning(help, tr("Qt Assistant"), tr("Failed to load keyword index file\n"
                                                              "Assistant will not work!"));
#if defined Q_WS_WIN || defined Q_WS_MACX
            startTimer(50);
#endif
            return;
        }
    }

    QDataStream ds(&indexFile);
    quint32 fileAges;
    ds >> fileAges;
    if (fileAges != getFileAges()) {
        indexFile.close();
        buildKeywordDB();
        if (!indexFile.open(QFile::ReadOnly)) {
            QMessageBox::warning(help, tr("Qt Assistant"),
                tr("Cannot open the index file %1").arg(QFileInfo(indexFile).absoluteFilePath()));
            return;
        }
        ds.setDevice(&indexFile);
        ds >> fileAges;
    }
    ds >> lst;
    indexFile.close();

    bar->setValue(bar->maximum());
    processEvents();

    for (int i=0; i<lst.count(); ++i) {
        const IndexKeyword &idx = lst.at(i);
        indexModel->addLink(idx.keyword, idx.link);

        keywordDocuments << HelpDialog::removeAnchorFromLink(idx.link);
    }

    indexModel->publish();

    ui.framePrepare->hide();
    showInitDoneMessage();
    setCursor(Qt::ArrowCursor);
}

quint32 HelpDialog::getFileAges()
{
    QStringList addDocuFiles = Config::configuration()->docFiles();
    QStringList::const_iterator i = addDocuFiles.constBegin();

    quint32 fileAges = 0;
    for (; i != addDocuFiles.constEnd(); ++i) {
        QFileInfo fi(*i);
        if (fi.exists())
            fileAges += fi.lastModified().toTime_t();
    }

    return fileAges;
}

void HelpDialog::buildKeywordDB()
{
    QStringList addDocuFiles = Config::configuration()->docFiles();
    QStringList::iterator i = addDocuFiles.begin();

    // Set up an indeterminate progress bar.
    ui.labelPrepare->setText(tr("Prepare..."));
    ui.progressPrepare->setMaximum(0);
    ui.progressPrepare->setMinimum(0);
    ui.progressPrepare->setValue(0);
    processEvents();

    QList<IndexKeyword> lst;
    quint32 fileAges = 0;
    for (i = addDocuFiles.begin(); i != addDocuFiles.end(); ++i) {
        QFile file(*i);
        if (!file.exists()) {
            QMessageBox::warning(this, tr("Warning"),
                tr("Documentation file %1 does not exist!\n"
                    "Skipping file.").arg(QFileInfo(file).absoluteFilePath()));
            continue;
        }
        fileAges += QFileInfo(file).lastModified().toTime_t();
        DocuParser *handler = DocuParser::createParser(*i);
        bool ok = handler->parse(&file);
        file.close();
        if (!ok){
            QString msg = QString::fromLatin1("In file %1:\n%2")
                          .arg(QFileInfo(file).absoluteFilePath())
                          .arg(handler->errorProtocol());
            QMessageBox::critical(this, tr("Parse Error"), tr(msg.toUtf8()));
            delete handler;
            continue;
        }

        QList<IndexItem*> indLst = handler->getIndexItems();
        int counter = 0;
        foreach (IndexItem *indItem, indLst) {
            QFileInfo fi(indItem->reference);
            lst.append(IndexKeyword(indItem->keyword, indItem->reference));

            if (++counter%100 == 0) {
                if (ui.progressPrepare)
                    ui.progressPrepare->setValue(counter);
                processEvents();
                if (lwClosed) {
                    return;
                }
            }
        }
        delete handler;
    }
    if (!lst.isEmpty())
        qSort(lst);

    QFile indexout(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.")
      + Config::configuration()->profileName());
    if (verifyDirectory(cacheFilesPath) && indexout.open(QFile::WriteOnly)) {
        QDataStream s(&indexout);
        s << fileAges;
        s << lst;
        indexout.close();
    }
}

void HelpDialog::setupTitleMap()
{
    if (titleMapDone)
        return;

    bool needRebuild = false;
    if (Config::configuration()->profileName() == QLatin1String("default")) {
        const QStringList docuFiles = Config::configuration()->docFiles();
        for (QStringList::ConstIterator it = docuFiles.begin(); it != docuFiles.end(); ++it) {
            if (!QFile::exists(*it)) {
                Config::configuration()->saveProfile(Profile::createDefaultProfile());
                Config::configuration()->loadDefaultProfile();
                needRebuild = true;
                break;
            }
        }
    }

    if (Config::configuration()->docRebuild() || needRebuild) {
        removeOldCacheFiles();
        Config::configuration()->setDocRebuild(false);
        Config::configuration()->saveProfile(Config::configuration()->profile());
    }
    if (contentList.isEmpty())
        getAllContents();

    titleMapDone = true;
    titleMap.clear();
    for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) {
        ContentList lst = (*it).second;
        foreach (ContentItem item, lst) {
            titleMap[item.reference] = item.title.trimmed();
        }
    }
    processEvents();
}

void HelpDialog::getAllContents()
{
    QFile contentFile(cacheFilesPath + QDir::separator() + QLatin1String("contentdb40.")
      + Config::configuration()->profileName());
    contentList.clear();
    if (!contentFile.open(QFile::ReadOnly)) {
        buildContentDict();
        return;
    }

    QDataStream ds(&contentFile);
    quint32 fileAges;
    ds >> fileAges;
    if (fileAges != getFileAges()) {
        contentFile.close();
        removeOldCacheFiles(true);
        buildContentDict();
        return;
    }
    QString key;
    QList<ContentItem> lst;
    while (!ds.atEnd()) {
        ds >> key;
        ds >> lst;
        contentList += qMakePair(key, QList<ContentItem>(lst));
    }
    contentFile.close();
    processEvents();

}

void HelpDialog::buildContentDict()
{
    QStringList docuFiles = Config::configuration()->docFiles();

    quint32 fileAges = 0;
    for (QStringList::iterator it = docuFiles.begin(); it != docuFiles.end(); ++it) {
        QFile file(*it);
        if (!file.exists()) {
            QMessageBox::warning(this, tr("Warning"),
            tr("Documentation file %1 does not exist!\n"
                "Skipping file.").arg(QFileInfo(file).absoluteFilePath()));
            continue;
        }
        fileAges += QFileInfo(file).lastModified().toTime_t();
        DocuParser *handler = DocuParser::createParser(*it);
        if (!handler) {
            QMessageBox::warning(this, tr("Warning"),
            tr("Documentation file %1 is not compatible!\n"
                "Skipping file.").arg(QFileInfo(file).absoluteFilePath()));
            continue;
        }
        bool ok = handler->parse(&file);
        file.close();
        if (ok) {
            contentList += qMakePair(*it, QList<ContentItem>(handler->getContentItems()));
            delete handler;
        } else {
            QString msg = QString::fromLatin1("In file %1:\n%2")
                          .arg(QFileInfo(file).absoluteFilePath())
                          .arg(handler->errorProtocol());
            QMessageBox::critical(this, tr("Parse Error"), tr(msg.toUtf8()));
            continue;
        }
    }

    QFile contentOut(cacheFilesPath + QDir::separator() + QLatin1String("contentdb40.")
      + Config::configuration()->profileName());
    if (contentOut.open(QFile::WriteOnly)) {
        QDataStream s(&contentOut);
        s << fileAges;
        for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) {
            s << *it;
        }
        contentOut.close();
    }
}

void HelpDialog::currentTabChanged(int index)
{
    QString s = ui.tabWidget->widget(index)->objectName();
    if (s == QLatin1String("indexPage"))
        QTimer::singleShot(0, this, SLOT(loadIndexFile()));
    else if (s == QLatin1String("bookmarkPage"))
        insertBookmarks();
    else if (s == QLatin1String("contentPage"))
        QTimer::singleShot(0, this, SLOT(insertContents()));
    else if (s == QLatin1String("searchPage"))
        QTimer::singleShot(0, this, SLOT(setupFullTextIndex()));
}

void HelpDialog::showInitDoneMessage()
{
    if (initDoneMsgShown)
        return;
    initDoneMsgShown = true;
    help->statusBar()->showMessage(tr("Done"), 3000);
}

void HelpDialog::showTopic(QTreeWidgetItem *item)
{
    if (item)
        showTopic();
}

void HelpDialog::showTopic()
{
    QString tabName = ui.tabWidget->currentWidget()->objectName();

    if (tabName == QLatin1String("indexPage"))
        showIndexTopic();
    else if (tabName == QLatin1String("bookmarkPage"))
        showBookmarkTopic();
    else if (tabName == QLatin1String("contentPage"))
        showContentsTopic();
}

void HelpDialog::showIndexTopic()
{
    int row = ui.listIndex->currentIndex().row();
    if (row == -1 || row >= indexModel->rowCount())
        return;

    QString description = indexModel->description(row);
    QStringList links = indexModel->links(row);

    bool blocked = ui.editIndex->blockSignals(true);
    ui.editIndex->setText(description);
    ui.editIndex->blockSignals(blocked);

    if (links.count() == 1) {
        emit showLink(links.first());
    } else {
        qSort(links);
        QStringList::Iterator it = links.begin();
        QStringList linkList;
        QStringList linkNames;
        for (; it != links.end(); ++it) {
            linkList << *it;
            linkNames << titleOfLink(*it);
        }
        QString link = TopicChooser::getLink(this, linkNames, linkList, description);
        if (!link.isEmpty())
            emit showLink(link);
    }

    ui.listIndex->setCurrentIndex(indexModel->index(indexModel->stringList().indexOf(description)));
    ui.listIndex->scrollTo(ui.listIndex->currentIndex(), QAbstractItemView::PositionAtTop);
}

void HelpDialog::searchInIndex(const QString &searchString)
{
    QRegExp atoz(QLatin1String("[A-Z]"));
    int matches = searchString.count(atoz);
    if (matches > 0 && !searchString.contains(QLatin1String(".*")))
    {
        int start = 0;
        QString newSearch;
        for (; matches > 0; --matches) {
            int match = searchString.indexOf(atoz, start+1);
            if (match <= start)
                continue;
            newSearch += searchString.mid(start, match-start);
            newSearch += QLatin1String(".*");
            start = match;
        }
        newSearch += searchString.mid(start);
        ui.listIndex->setCurrentIndex(indexModel->filter(newSearch, searchString));
    }
    else
        ui.listIndex->setCurrentIndex(indexModel->filter(searchString, searchString));
}

QString HelpDialog::titleOfLink(const QString &link)
{
    QString s = HelpDialog::removeAnchorFromLink(link);
    s = titleMap[s];
    if (s.isEmpty())
        return link;
    return s;
}

bool HelpDialog::eventFilter(QObject * o, QEvent * e)
{
    if (o == ui.editIndex && e->type() == QEvent::KeyPress) {
        switch (static_cast<QKeyEvent*>(e)->key()) {
            case Qt::Key_Up:
            case Qt::Key_Down:
            case Qt::Key_PageDown:
            case Qt::Key_PageUp:
                QApplication::sendEvent(ui.listIndex, e);
                break;

            default:
                break;
        }
    } else if (o == ui.listContents->viewport()) {
        if (e->type() == QEvent::MouseButtonRelease) {
            QMouseEvent *me = static_cast<QMouseEvent*>(e);
            if (me->button() == Qt::LeftButton) {
                QTreeWidgetItem *item = ui.listContents->itemAt(me->pos());
                QRect vRect = ui.listContents->visualItemRect(item);

                // only show topic if we clicked an item
                if (item && vRect.contains(me->pos()))
                    showTopic(item);
            }
        }
    }

    return QWidget::eventFilter(o, e);
}

void HelpDialog::addBookmark()
{
    if (!bookmarksInserted)
        insertBookmarks();
    QString link = help->browsers()->currentBrowser()->source().toString();
    QString title = help->browsers()->currentBrowser()->documentTitle();
    if (title.isEmpty())
        title = titleOfLink(link);

    QTreeWidgetItem *i = new QTreeWidgetItem(ui.listBookmarks, 0);
    i->setText(0, title);
    i->setData(0, LinkRole, link);
    ui.buttonRemove->setEnabled(true);
    saveBookmarks();
    help->updateBookmarkMenu();
}

void HelpDialog::on_buttonAdd_clicked()
{
    addBookmark();
}

void HelpDialog::on_buttonRemove_clicked()
{
    if (!ui.listBookmarks->currentItem())
        return;

    delete ui.listBookmarks->currentItem();
    saveBookmarks();
    if (ui.listBookmarks->topLevelItemCount() != 0) {
        ui.listBookmarks->setCurrentItem(ui.listBookmarks->topLevelItem(0));
    }
    ui.buttonRemove->setEnabled(ui.listBookmarks->topLevelItemCount() > 0);
    help->updateBookmarkMenu();
}

void HelpDialog::insertBookmarks()
{
    if (bookmarksInserted)
        return;
    bookmarksInserted = true;
    ui.listBookmarks->clear();
    QFile f(cacheFilesPath + QDir::separator() + QLatin1String("bookmarks.")
      + Config::configuration()->profileName());
    if (!f.open(QFile::ReadOnly))
        return;
    QTextStream ts(&f);
    while (!ts.atEnd()) {
        QTreeWidgetItem *i = new QTreeWidgetItem(ui.listBookmarks, 0);
        i->setText(0, ts.readLine());
        i->setData(0, LinkRole, ts.readLine());
    }
    ui.buttonRemove->setEnabled(ui.listBookmarks->topLevelItemCount() > 0);
    help->updateBookmarkMenu();
    showInitDoneMessage();
}

void HelpDialog::showBookmarkTopic()
{
    if (!ui.listBookmarks->currentItem())
        return;

    QTreeWidgetItem *i = (QTreeWidgetItem*)ui.listBookmarks->currentItem();
    emit showLink(i->data(0, LinkRole).toString());
}

static void store(QTreeWidgetItem *i, QTextStream &ts)
{
    ts << i->text(0) << endl;
    ts << i->data(0, LinkRole).toString() << endl;

    for (int index = 0; index < i->childCount(); ++index)
        store(i->child(index), ts);
}

static void store(QTreeWidget *tw, QTextStream &ts)
{
    for (int index = 0; index < tw->topLevelItemCount(); ++index)
        store(tw->topLevelItem(index), ts);
}

void HelpDialog::saveBookmarks()
{
    QFile f(cacheFilesPath + QDir::separator() + QLatin1String("bookmarks.")
      + Config::configuration()->profileName());
    if (!f.open(QFile::WriteOnly))
        return;

    QTextStream ts(&f);
    store(ui.listBookmarks, ts);
    f.close();
}

void HelpDialog::insertContents()
{
#ifdef Q_WS_MAC
    static const QLatin1String IconPath(":/trolltech/assistant/images/mac/book.png");
#else
    static const QLatin1String IconPath(":/trolltech/assistant/images/win/book.png");
#endif
    if (contentsInserted)
        return;

    if (contentList.isEmpty())
        getAllContents();

    contentsInserted = true;
    ui.listContents->clear();
    setCursor(Qt::WaitCursor);
    if (!titleMapDone)
        setupTitleMap();

#if 0 // ### port me
    ui.listContents->setSorting(-1);
#endif

    for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) {
        QTreeWidgetItem *newEntry = 0;

        QTreeWidgetItem *contentEntry = 0;
        QStack<QTreeWidgetItem*> stack;
        stack.clear();
        int depth = 0;
        bool root = false;

        const int depthSize = 32;
        QVarLengthArray<QTreeWidgetItem*, depthSize> lastItem(depthSize);

        ContentList lst = (*it).second;
        for (ContentList::ConstIterator it = lst.constBegin(); it != lst.constEnd(); ++it) {
            ContentItem item = *it;
            if (item.depth == 0) {
                lastItem[0] = 0;
                newEntry = new QTreeWidgetItem(ui.listContents, 0);
                newEntry->setIcon(0, QIcon(IconPath));
                newEntry->setText(0, item.title);
                newEntry->setData(0, LinkRole, item.reference);
                stack.push(newEntry);
                depth = 1;
                root = true;
            }
            else{
                if ((item.depth > depth) && root) {
                    depth = item.depth;
                    stack.push(contentEntry);
                }
                if (item.depth == depth) {
                    if (lastItem.capacity() == depth)
                        lastItem.resize(depth + depthSize);
                    contentEntry = new QTreeWidgetItem(stack.top(), lastItem[ depth ]);
                    lastItem[ depth ] = contentEntry;
                    contentEntry->setText(0, item.title);
                    contentEntry->setData(0, LinkRole, item.reference);
                }
                else if (item.depth < depth) {
                    stack.pop();
                    depth--;
                    item = *(--it);
                }
            }
        }
        processEvents();
    }
    setCursor(Qt::ArrowCursor);
    showInitDoneMessage();
}

void HelpDialog::showContentsTopic()
{
    QTreeWidgetItem *i = (QTreeWidgetItem*)ui.listContents->currentItem();
    if (!i)
        return;
    emit showLink(i->data(0, LinkRole).toString());
}

QTreeWidgetItem * HelpDialog::locateLink(QTreeWidgetItem *item, const QString &link)
{
    QTreeWidgetItem *child = 0;
#ifdef Q_OS_WIN
    Qt::CaseSensitivity checkCase = Qt::CaseInsensitive;
#else
    Qt::CaseSensitivity checkCase = Qt::CaseSensitive;
#endif
    for (int i = 0, childCount = item->childCount(); i<childCount; i++) {
        child = item->child(i);
        ///check whether it is this item
        if (link.startsWith(child->data(0, LinkRole).toString(), checkCase))
            break;
        //check if the link is a child of this item
        else if (child->childCount()) {
            child = locateLink(child, link);
            if (child)
                break;
        }
        child = 0;
    }
    return child;
}

void HelpDialog::locateContents(const QString &link)
{
    //ensure the TOC is filled
    if (!contentsInserted)
        insertContents();
#ifdef Q_OS_WIN
    Qt::CaseSensitivity checkCase = Qt::CaseInsensitive;
#else
    Qt::CaseSensitivity checkCase = Qt::CaseSensitive;
#endif
    QString findLink(link);
    //Installations on a windows local drive will give the 'link' as <file:///C:/xxx>
    //and the contents in the TOC will be <file:C:/xxx>.
    //But on others the 'link' of format <file:///root/xxx>
    //and the contents in the TOC will be <file:/root/xxx>.
    if (findLink.contains(QLatin1String("file:///"))) {
        if (findLink[9] == QLatin1Char(':')) //on windows drives
            findLink.replace(0, 8, QLatin1String("file:"));
        else
            findLink.replace(0, 8, QLatin1String("file:/"));
    }

    bool topLevel = false;
    QTreeWidgetItem *item = 0;
    int totalItems = ui.listContents->topLevelItemCount();

    for (int i = 0; i < totalItems; i++ ) {
        // first see if we are one of the top level items
        item = (QTreeWidgetItem*)ui.listContents->topLevelItem(i);
        if (findLink.startsWith(item->data(0, LinkRole).toString(), checkCase)) {
            topLevel = true;
            break;
        }
    }

    if (!topLevel) {
        // now try to find it in the sublevel items
        for (int n = 0; n < totalItems; ++n) {
            item = (QTreeWidgetItem*)ui.listContents->topLevelItem(n);
            item = locateLink(item, findLink);
            if (item)
                break;
        }
    }

    //remove the old selection
    QList<QTreeWidgetItem *> selected = ui.listContents->selectedItems();
    foreach(QTreeWidgetItem *sel, selected)
        ui.listContents->setItemSelected(sel, false);

    //set the TOC item and show
    ui.listContents->setCurrentItem(item);
    ui.listContents->setItemSelected(item, true);
    ui.listContents->scrollToItem(item);
}

void HelpDialog::toggleContents()
{
    if (!isVisible() || ui.tabWidget->currentIndex() != 0) {
        ui.tabWidget->setCurrentIndex(0);
        parentWidget()->show();
    }
    else
        parentWidget()->hide();
}

void HelpDialog::toggleIndex()
{
    if (!isVisible() || ui.tabWidget->currentIndex() != 1 || !ui.editIndex->hasFocus()) {
        ui.tabWidget->setCurrentIndex(1);
        parentWidget()->show();
        ui.editIndex->setFocus();
    }
    else
        parentWidget()->hide();
}

void HelpDialog::toggleBookmarks()
{
    if (!isVisible() || ui.tabWidget->currentIndex() != 2) {
        ui.tabWidget->setCurrentIndex(2);
        parentWidget()->show();
    }
    else
        parentWidget()->hide();
}

void HelpDialog::toggleSearch()
{
    if (!isVisible() || ui.tabWidget->currentIndex() != 3) {
        ui.tabWidget->setCurrentIndex(3);
        parentWidget()->show();
    }
    else
        parentWidget()->hide();
}

void HelpDialog::setupFullTextIndex()
{
    if (fullTextIndex)
        return;

    QString pname = Config::configuration()->profileName();
    fullTextIndex = new Index(QStringList(), QDir::homePath()); // ### Is this correct ?
    if (!verifyDirectory(cacheFilesPath)) {
        QMessageBox::warning(help, tr("Qt Assistant"),
                             tr("Failed to save fulltext search index\n"
                                "Assistant will not work!"));
        return;
    }
    fullTextIndex->setDictionaryFile(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.dict.") + pname);
    fullTextIndex->setDocListFile(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.doc.") + pname);
    processEvents();

    connect(fullTextIndex, SIGNAL(indexingProgress(int)),
             this, SLOT(setIndexingProgress(int)));
    QFile f(cacheFilesPath + QDir::separator() + QLatin1String("indexdb40.dict.") + pname);
    if (!f.exists()) {
        QString doc;
        QSet<QString> documentSet;
        QMap<QString, QString>::ConstIterator it = titleMap.constBegin();
        for (; it != titleMap.constEnd(); ++it) {
            doc = HelpDialog::removeAnchorFromLink(it.key());
            if (!doc.isEmpty())
                documentSet.insert(doc);
        }
        loadIndexFile();
        for ( QStringList::Iterator it = keywordDocuments.begin(); it != keywordDocuments.end(); ++it ) {
            if (!(*it).isEmpty())
                documentSet.insert(*it);
        }
        fullTextIndex->setDocList( documentSet.toList() );

        help->statusBar()->clearMessage();
        setCursor(Qt::WaitCursor);
        ui.labelPrepare->setText(tr("Indexing files..."));
        ui.progressPrepare->setMaximum(100);
        ui.progressPrepare->reset();
        ui.progressPrepare->show();
        ui.framePrepare->show();
        processEvents();
        if (fullTextIndex->makeIndex() == -1)
            return;
        fullTextIndex->writeDict();
        ui.progressPrepare->setValue(100);
        ui.framePrepare->hide();
        setCursor(Qt::ArrowCursor);
        showInitDoneMessage();
    } else {
        setCursor(Qt::WaitCursor);
        help->statusBar()->showMessage(tr("Reading dictionary..."));
        processEvents();
        fullTextIndex->readDict();
        help->statusBar()->showMessage(tr("Done"), 3000);
        setCursor(Qt::ArrowCursor);
    }
    keywordDocuments.clear();
}

void HelpDialog::setIndexingProgress(int prog)
{
    ui.progressPrepare->setValue(prog);
    processEvents();
}

void HelpDialog::startSearch()
{
    QString str = ui.termsEdit->text();
    str = str.simplified();
    str = str.replace(QLatin1String("\'"), QLatin1String("\""));
    str = str.replace(QLatin1String("`"), QLatin1String("\""));
    QString buf = str;
    str = str.replace(QLatin1String("-"), QLatin1String(" "));
    str = str.replace(QRegExp(QLatin1String("\\s[\\S]?\\s")), QLatin1String(" "));
    terms = str.split(QLatin1Char(' '));
    QStringList termSeq;
    QStringList seqWords;
    QStringList::iterator it = terms.begin();
    for (; it != terms.end(); ++it) {
        (*it) = (*it).simplified();
        (*it) = (*it).toLower();
        (*it) = (*it).replace(QLatin1String("\""), QLatin1String(""));
    }
    if (str.contains(QLatin1Char('\"'))) {
        if ((str.count(QLatin1Char('\"')))%2 == 0) {
            int beg = 0;
            int end = 0;
            QString s;
            beg = str.indexOf(QLatin1Char('\"'), beg);
            while (beg != -1) {
                beg++;
                end = str.indexOf(QLatin1Char('\"'), beg);
                s = str.mid(beg, end - beg);
                s = s.toLower();
                s = s.simplified();
                if (s.contains(QLatin1Char('*'))) {
                    QMessageBox::warning(this, tr("Full Text Search"),
                        tr("Using a wildcard within phrases is not allowed."));
                    return;
                }
                seqWords += s.split(QLatin1Char(' '));
                termSeq << s;
                beg = str.indexOf(QLatin1Char('\"'), end + 1);
            }
        } else {
            QMessageBox::warning(this, tr("Full Text Search"),
                tr("The closing quotation mark is missing."));
            return;
        }
    }
    setCursor(Qt::WaitCursor);
    foundDocs.clear();
    foundDocs = fullTextIndex->query(terms, termSeq, seqWords);
    QString msg = tr("%n document(s) found.", "", foundDocs.count());
    help->statusBar()->showMessage(tr(msg.toUtf8()), 3000);
    ui.resultBox->clear();
    for (it = foundDocs.begin(); it != foundDocs.end(); ++it)
        ui.resultBox->addItem(fullTextIndex->getDocumentTitle(*it));

    terms.clear();
    bool isPhrase = false;
    QString s;
    for (int i = 0; i < (int)buf.length(); ++i) {
        if (buf[i] == QLatin1Char('\"')) {
            isPhrase = !isPhrase;
            s = s.simplified();
            if (!s.isEmpty())
                terms << s;
            s = QLatin1String("");
        } else if (buf[i] == QLatin1Char(' ') && !isPhrase) {
            s = s.simplified();
            if (!s.isEmpty())
                terms << s;
            s = QLatin1String("");
        } else
            s += buf[i];
    }
    if (!s.isEmpty())
        terms << s;

    setCursor(Qt::ArrowCursor);
}

void HelpDialog::on_helpButton_clicked()
{
    emit showLink(MainWindow::urlifyFileName(
                  Config::configuration()->assistantDocPath() +
                  QLatin1String("/assistant-manual.html#full-text-searching")));
}

void HelpDialog::on_resultBox_itemActivated(QListWidgetItem *item)
{
    showResultPage(item);
}

void HelpDialog::showResultPage(QListWidgetItem *item)
{
    if (item)
        emit showSearchLink(foundDocs[ui.resultBox->row(item)], terms);
}

void HelpDialog::showIndexItemMenu(const QPoint &pos)
{
    QListView *listView = qobject_cast<QListView*>(sender());
    if (!listView)
        return;

    QModelIndex idx = listView->indexAt(pos);
    if (!idx.isValid())
        return;

    QAction *action = itemPopup->exec(listView->viewport()->mapToGlobal(pos));
    if (action == actionOpenCurrentTab) {
        showTopic();
    } else if (action) {
        HelpWindow *hw = help->browsers()->currentBrowser();
        QString itemName = idx.data().toString();
        ui.editIndex->setText(itemName);
        QStringList links = indexModel->links(idx.row());
        if (links.count() == 1) {
            if (action == actionOpenLinkInNewWindow)
                hw->openLinkInNewWindow(links.first());
            else
                hw->openLinkInNewPage(links.first());
        } else {
            QStringList::Iterator it = links.begin();
            QStringList linkList;
            QStringList linkNames;
            for (; it != links.end(); ++it) {
                linkList << *it;
                linkNames << titleOfLink(*it);
            }
            QString link = TopicChooser::getLink(this, linkNames, linkList, itemName);
            if (!link.isEmpty()) {
                if (action == actionOpenLinkInNewWindow)
                    hw->openLinkInNewWindow(link);
                else
                    hw->openLinkInNewPage(link);
            }
        }
    }
}

void HelpDialog::showListItemMenu(const QPoint &pos)
{
    QListWidget *listWidget = qobject_cast<QListWidget*>(sender());
    if (!listWidget)
        return;
    QListWidgetItem *item = listWidget->itemAt(pos);
    if (!item)
        return;

    QAction *action = itemPopup->exec(listWidget->viewport()->mapToGlobal(pos));
    if (action == actionOpenCurrentTab) {
        showResultPage(item);
    } else if (action) {
        HelpWindow *hw = help->browsers()->currentBrowser();
        QString link = foundDocs[ui.resultBox->row(item)];
        if (action == actionOpenLinkInNewWindow)
            hw->openLinkInNewWindow(link);
        else
            hw->openLinkInNewPage(link);
    }
}

void HelpDialog::showTreeItemMenu(const QPoint &pos)
{
    QTreeWidget *treeWidget = qobject_cast<QTreeWidget*>(sender());

    if (!treeWidget)
        return;

    QTreeWidgetItem *item = treeWidget->itemAt(pos);

    if (!item)
        return;

    QAction *action = itemPopup->exec(treeWidget->viewport()->mapToGlobal(pos));
    if (action == actionOpenCurrentTab) {
        if (ui.tabWidget->currentWidget()->objectName() == QLatin1String("contentPage"))
            showContentsTopic();
        else
            showBookmarkTopic();
    } else if (action) {
        QTreeWidgetItem *i = (QTreeWidgetItem*)item;
        if (action == actionOpenLinkInNewWindow)
            help->browsers()->currentBrowser()->openLinkInNewWindow(i->data(0, LinkRole).toString());
        else
            help->browsers()->currentBrowser()->openLinkInNewPage(i->data(0, LinkRole).toString());
    }
}

void HelpDialog::on_termsEdit_returnPressed()
{
    startSearch();
}

void HelpDialog::updateSearchButton(const QString &txt)
{
    ui.searchButton->setDisabled(txt.isEmpty());
}

void HelpDialog::on_searchButton_clicked()
{
    startSearch();
}

QString HelpDialog::removeAnchorFromLink(const QString &link)
{
    int i = link.length();
   int j = link.lastIndexOf(QLatin1Char('/'));
    int l = link.lastIndexOf(QDir::separator());
    if (l > j)
        j = l;
   if (j > -1) {
      QString fileName = link.mid(j+1);
      int k = fileName.lastIndexOf(QLatin1Char('#'));
      if (k > -1)
         i = j + k + 1;
   }
   return link.left(i);
}

QT_END_NAMESPACE