diff options
Diffstat (limited to 'src/assistant/help')
47 files changed, 11507 insertions, 0 deletions
diff --git a/src/assistant/help/help.pro b/src/assistant/help/help.pro new file mode 100644 index 000000000..2aa30992e --- /dev/null +++ b/src/assistant/help/help.pro @@ -0,0 +1,72 @@ +load(qt_module) + +TARGET = QtHelp +QPRO_PWD = $$PWD +QT += widgets sql network core-private clucene clucene-private + +CONFIG += module +MODULE_PRI += ../../../modules/qt_help.pri + +DEFINES += QHELP_LIB QT_CLUCENE_SUPPORT + +load(qt_module_config) + +HEADERS += qthelpversion.h + +DEFINES -= QT_ASCII_CAST_WARNINGS + +qclucene = QtCLucene$${QT_LIBINFIX} +if(!debug_and_release|build_pass):CONFIG(debug, debug|release) { + mac:qclucene = $${qclucene}_debug + win32:qclucene = $${qclucene}d +} +linux-lsb-g++:LIBS_PRIVATE += --lsb-shared-libs=$$qclucene +unix|win32-g++*:QMAKE_PKGCONFIG_REQUIRES += QtCore QtNetwork QtSql + +LIBS_PRIVATE += -l$$qclucene +RESOURCES += helpsystem.qrc +SOURCES += qhelpenginecore.cpp \ + qhelpengine.cpp \ + qhelpdbreader.cpp \ + qhelpcontentwidget.cpp \ + qhelpindexwidget.cpp \ + qhelpgenerator.cpp \ + qhelpdatainterface.cpp \ + qhelpprojectdata.cpp \ + qhelpcollectionhandler.cpp \ + qhelpsearchengine.cpp \ + qhelpsearchquerywidget.cpp \ + qhelpsearchresultwidget.cpp \ + qhelpsearchindex_default.cpp \ + qhelpsearchindexwriter_default.cpp \ + qhelpsearchindexreader_default.cpp \ + qhelpsearchindexreader.cpp \ + qclucenefieldnames.cpp \ + qhelp_global.cpp + +# access to clucene +SOURCES += qhelpsearchindexwriter_clucene.cpp \ + qhelpsearchindexreader_clucene.cpp +HEADERS += qhelpenginecore.h \ + qhelpengine.h \ + qhelpengine_p.h \ + qhelp_global.h \ + qhelpdbreader_p.h \ + qhelpcontentwidget.h \ + qhelpindexwidget.h \ + qhelpgenerator_p.h \ + qhelpdatainterface_p.h \ + qhelpprojectdata_p.h \ + qhelpcollectionhandler_p.h \ + qhelpsearchengine.h \ + qhelpsearchquerywidget.h \ + qhelpsearchresultwidget.h \ + qhelpsearchindex_default_p.h \ + qhelpsearchindexwriter_default_p.h \ + qhelpsearchindexreader_default_p.h \ + qhelpsearchindexreader_p.h \ + qclucenefieldnames_p.h + +# access to clucene +HEADERS += qhelpsearchindexwriter_clucene_p.h \ + qhelpsearchindexreader_clucene_p.h diff --git a/src/assistant/help/helpsystem.qrc b/src/assistant/help/helpsystem.qrc new file mode 100644 index 000000000..10efc6df1 --- /dev/null +++ b/src/assistant/help/helpsystem.qrc @@ -0,0 +1,8 @@ +<RCC> + <qresource prefix="/trolltech/assistant" > + <file>images/1leftarrow.png</file> + <file>images/1rightarrow.png</file> + <file>images/3leftarrow.png</file> + <file>images/3rightarrow.png</file> + </qresource> +</RCC> diff --git a/src/assistant/help/images/1leftarrow.png b/src/assistant/help/images/1leftarrow.png Binary files differnew file mode 100644 index 000000000..bd1a5a249 --- /dev/null +++ b/src/assistant/help/images/1leftarrow.png diff --git a/src/assistant/help/images/1rightarrow.png b/src/assistant/help/images/1rightarrow.png Binary files differnew file mode 100644 index 000000000..0c0c44ae6 --- /dev/null +++ b/src/assistant/help/images/1rightarrow.png diff --git a/src/assistant/help/images/3leftarrow.png b/src/assistant/help/images/3leftarrow.png Binary files differnew file mode 100644 index 000000000..8d38b0f57 --- /dev/null +++ b/src/assistant/help/images/3leftarrow.png diff --git a/src/assistant/help/images/3rightarrow.png b/src/assistant/help/images/3rightarrow.png Binary files differnew file mode 100644 index 000000000..c2faf501c --- /dev/null +++ b/src/assistant/help/images/3rightarrow.png diff --git a/src/assistant/help/qclucenefieldnames.cpp b/src/assistant/help/qclucenefieldnames.cpp new file mode 100644 index 000000000..31ef415af --- /dev/null +++ b/src/assistant/help/qclucenefieldnames.cpp @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qclucenefieldnames_p.h" + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace clucene { +const QString AttributeField(QLatin1String("attribute")); +const QString ContentField(QLatin1String("content")); +const QString NamespaceField(QLatin1String("namespace")); +const QString PathField(QLatin1String("path")); +const QString TitleField(QLatin1String("title")); +const QString TitleTokenizedField(QLatin1String("titleTokenized")); +} // namespace clucene +} // namespace fulltextsearch + +QT_END_NAMESPACE diff --git a/src/assistant/help/qclucenefieldnames_p.h b/src/assistant/help/qclucenefieldnames_p.h new file mode 100644 index 000000000..733e27121 --- /dev/null +++ b/src/assistant/help/qclucenefieldnames_p.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QCLUCENEFIELDNAMES_P_H +#define QCLUCENEFIELDNAMES_P_H + +#include <QtCore/QtGlobal> +#include <QtCore/QString> + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace clucene { + extern const QString AttributeField; + extern const QString ContentField; + extern const QString NamespaceField; + extern const QString PathField; + extern const QString TitleField; + extern const QString TitleTokenizedField; +} // namespace clucene +} // namespace fulltextsearch + +QT_END_NAMESPACE + +#endif // QCLUCENEFIELDNAMES_P_H diff --git a/src/assistant/help/qhelp_global.cpp b/src/assistant/help/qhelp_global.cpp new file mode 100644 index 000000000..2c7ca6214 --- /dev/null +++ b/src/assistant/help/qhelp_global.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 <QtCore/QCoreApplication> +#include <QtCore/QRegExp> +#include <QtCore/QMutexLocker> +#include <QtGui/QTextDocument> + +#include "qhelp_global.h" + +QString QHelpGlobal::uniquifyConnectionName(const QString &name, void *pointer) +{ + static int counter = 0; + static QMutex mutex; + + QMutexLocker locker(&mutex); + if (++counter > 1000) + counter = 0; + + return QString::fromLatin1("%1-%2-%3"). + arg(name).arg(quintptr(pointer)).arg(counter); +} + +QString QHelpGlobal::documentTitle(const QString &content) +{ + QString title = QCoreApplication::translate("QHelp", "Untitled"); + if (!content.isEmpty()) { + int start = content.indexOf(QLatin1String("<title>"), 0, Qt::CaseInsensitive) + 7; + int end = content.indexOf(QLatin1String("</title>"), 0, Qt::CaseInsensitive); + if ((end - start) > 0) { + title = content.mid(start, end - start); + if (Qt::mightBeRichText(title) || title.contains(QLatin1Char('&'))) { + QTextDocument doc; + doc.setHtml(title); + title = doc.toPlainText(); + } + } + } + return title; +} + +QString QHelpGlobal::codecFromData(const QByteArray &data) +{ + QString codec = codecFromXmlData(data); + if (codec.isEmpty()) + codec = codecFromHtmlData(data); + return codec.isEmpty() ? QLatin1String("utf-8") : codec; +} + +QString QHelpGlobal::codecFromHtmlData(const QByteArray &data) +{ + QString head = QString::fromUtf8(data.constData(), qMin(1000, data.size())); + int start = head.indexOf(QLatin1String("<meta"), 0, Qt::CaseInsensitive); + if (start > 0) { + QRegExp r(QLatin1String("charset=([^\"\\s]+)")); + while (start != -1) { + const int end = head.indexOf(QLatin1Char('>'), start) + 1; + if (end <= start) + break; + const QString &meta = head.mid(start, end - start).toLower(); + if (r.indexIn(meta) != -1) + return r.cap(1); + start = head.indexOf(QLatin1String("<meta"), end, + Qt::CaseInsensitive); + } + } + return QString(); +} + +QString QHelpGlobal::codecFromXmlData(const QByteArray &data) +{ + QString head = QString::fromUtf8(data.constData(), qMin(1000, data.size())); + const QRegExp encodingExp(QLatin1String("^\\s*<\\?xml version=" + "\"\\d\\.\\d\" encoding=\"([^\"]+)\"\\?>.*")); + return encodingExp.exactMatch(head) ? encodingExp.cap(1) : QString(); +} diff --git a/src/assistant/help/qhelp_global.h b/src/assistant/help/qhelp_global.h new file mode 100644 index 000000000..182298fc2 --- /dev/null +++ b/src/assistant/help/qhelp_global.h @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELP_GLOBAL_H +#define QHELP_GLOBAL_H + +#include <QtCore/qglobal.h> +#include <QtCore/QString> +#include <QtCore/QObject> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +#if !defined(QT_SHARED) && !defined(QT_DLL) +# define QHELP_EXPORT +#elif defined(QHELP_LIB) +# define QHELP_EXPORT Q_DECL_EXPORT +#else +# define QHELP_EXPORT Q_DECL_IMPORT +#endif + +class QHelpGlobal { +public: + static QString uniquifyConnectionName(const QString &name, void *pointer); + static QString documentTitle(const QString &content); + static QString codecFromData(const QByteArray &data); + +private: + static QString codecFromHtmlData(const QByteArray &data); + static QString codecFromXmlData(const QByteArray &data); +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QHELP_GLOBAL_H diff --git a/src/assistant/help/qhelpcollectionhandler.cpp b/src/assistant/help/qhelpcollectionhandler.cpp new file mode 100644 index 000000000..169d37e05 --- /dev/null +++ b/src/assistant/help/qhelpcollectionhandler.cpp @@ -0,0 +1,603 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpcollectionhandler_p.h" +#include "qhelp_global.h" +#include "qhelpdbreader_p.h" + +#include <QtCore/QFile> +#include <QtCore/QDir> +#include <QtCore/QFileInfo> +#include <QtCore/QDebug> + +#include <QtSql/QSqlError> +#include <QtSql/QSqlDriver> + +QT_BEGIN_NAMESPACE + +QHelpCollectionHandler::QHelpCollectionHandler(const QString &collectionFile, QObject *parent) + : QObject(parent) + , m_dbOpened(false) + , m_collectionFile(collectionFile) + , m_connectionName(QString()) +{ + QFileInfo fi(m_collectionFile); + if (!fi.isAbsolute()) + m_collectionFile = fi.absoluteFilePath(); + m_query.clear(); +} + +QHelpCollectionHandler::~QHelpCollectionHandler() +{ + m_query.clear(); + if (m_dbOpened) + QSqlDatabase::removeDatabase(m_connectionName); +} + +bool QHelpCollectionHandler::isDBOpened() +{ + if (m_dbOpened) + return true; + emit error(tr("The collection file '%1' is not set up yet!"). + arg(m_collectionFile)); + return false; +} + +QString QHelpCollectionHandler::collectionFile() const +{ + return m_collectionFile; +} + +bool QHelpCollectionHandler::openCollectionFile() +{ + if (m_dbOpened) + return m_dbOpened; + + m_connectionName = QHelpGlobal::uniquifyConnectionName( + QLatin1String("QHelpCollectionHandler"), this); + bool openingOk = true; + { + QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), + m_connectionName); + if (db.driver() + && db.driver()->lastError().type() == QSqlError::ConnectionError) { + emit error(tr("Cannot load sqlite database driver!")); + return false; + } + + db.setDatabaseName(collectionFile()); + openingOk = db.open(); + if (openingOk) + m_query = QSqlQuery(db); + } + if (!openingOk) { + QSqlDatabase::removeDatabase(m_connectionName); + emit error(tr("Cannot open collection file: %1").arg(collectionFile())); + return false; + } + + m_query.exec(QLatin1String("PRAGMA synchronous=OFF")); + m_query.exec(QLatin1String("PRAGMA cache_size=3000")); + + m_query.exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'" + "AND Name=\'NamespaceTable\'")); + m_query.next(); + if (m_query.value(0).toInt() < 1) { + if (!createTables(&m_query)) { + emit error(tr("Cannot create tables in file %1!").arg(collectionFile())); + return false; + } + } + + m_dbOpened = true; + return m_dbOpened; +} + +bool QHelpCollectionHandler::copyCollectionFile(const QString &fileName) +{ + if (!m_dbOpened) + return false; + + QFileInfo fi(fileName); + if (fi.exists()) { + emit error(tr("The collection file '%1' already exists!"). + arg(fileName)); + return false; + } + + if (!fi.absoluteDir().exists() && !QDir().mkpath(fi.absolutePath())) { + emit error(tr("Cannot create directory: %1").arg(fi.absolutePath())); + return false; + } + + QString colFile = fi.absoluteFilePath(); + QString connectionName = QHelpGlobal::uniquifyConnectionName( + QLatin1String("QHelpCollectionHandlerCopy"), this); + QSqlQuery *copyQuery = 0; + bool openingOk = true; + { + QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), connectionName); + db.setDatabaseName(colFile); + openingOk = db.open(); + if (openingOk) + copyQuery = new QSqlQuery(db); + } + + if (!openingOk) { + emit error(tr("Cannot open collection file: %1").arg(colFile)); + return false; + } + + copyQuery->exec(QLatin1String("PRAGMA synchronous=OFF")); + copyQuery->exec(QLatin1String("PRAGMA cache_size=3000")); + + if (!createTables(copyQuery)) { + emit error(tr("Cannot copy collection file: %1").arg(colFile)); + return false; + } + + QString oldBaseDir = QFileInfo(collectionFile()).absolutePath(); + QString oldFilePath; + QFileInfo newColFi(colFile); + m_query.exec(QLatin1String("SELECT Name, FilePath FROM NamespaceTable")); + while (m_query.next()) { + copyQuery->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?, ?)")); + copyQuery->bindValue(0, m_query.value(0).toString()); + oldFilePath = m_query.value(1).toString(); + if (!QDir::isAbsolutePath(oldFilePath)) + oldFilePath = oldBaseDir + QDir::separator() + oldFilePath; + copyQuery->bindValue(1, newColFi.absoluteDir().relativeFilePath(oldFilePath)); + copyQuery->exec(); + } + + m_query.exec(QLatin1String("SELECT NamespaceId, Name FROM FolderTable")); + while (m_query.next()) { + copyQuery->prepare(QLatin1String("INSERT INTO FolderTable VALUES(NULL, ?, ?)")); + copyQuery->bindValue(0, m_query.value(0).toString()); + copyQuery->bindValue(1, m_query.value(1).toString()); + copyQuery->exec(); + } + + m_query.exec(QLatin1String("SELECT Name FROM FilterAttributeTable")); + while (m_query.next()) { + copyQuery->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); + copyQuery->bindValue(0, m_query.value(0).toString()); + copyQuery->exec(); + } + + m_query.exec(QLatin1String("SELECT Name FROM FilterNameTable")); + while (m_query.next()) { + copyQuery->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)")); + copyQuery->bindValue(0, m_query.value(0).toString()); + copyQuery->exec(); + } + + m_query.exec(QLatin1String("SELECT NameId, FilterAttributeId FROM FilterTable")); + while (m_query.next()) { + copyQuery->prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)")); + copyQuery->bindValue(0, m_query.value(0).toInt()); + copyQuery->bindValue(1, m_query.value(1).toInt()); + copyQuery->exec(); + } + + m_query.exec(QLatin1String("SELECT Key, Value FROM SettingsTable")); + while (m_query.next()) { + if (m_query.value(0).toString() == QLatin1String("CluceneSearchNamespaces")) + continue; + copyQuery->prepare(QLatin1String("INSERT INTO SettingsTable VALUES(?, ?)")); + copyQuery->bindValue(0, m_query.value(0).toString()); + copyQuery->bindValue(1, m_query.value(1)); + copyQuery->exec(); + } + + copyQuery->clear(); + delete copyQuery; + QSqlDatabase::removeDatabase(connectionName); + return true; +} + +bool QHelpCollectionHandler::createTables(QSqlQuery *query) +{ + QStringList tables; + tables << QLatin1String("CREATE TABLE NamespaceTable (" + "Id INTEGER PRIMARY KEY, " + "Name TEXT, " + "FilePath TEXT )") + << QLatin1String("CREATE TABLE FolderTable (" + "Id INTEGER PRIMARY KEY, " + "NamespaceId INTEGER, " + "Name TEXT )") + << QLatin1String("CREATE TABLE FilterAttributeTable (" + "Id INTEGER PRIMARY KEY, " + "Name TEXT )") + << QLatin1String("CREATE TABLE FilterNameTable (" + "Id INTEGER PRIMARY KEY, " + "Name TEXT )") + << QLatin1String("CREATE TABLE FilterTable (" + "NameId INTEGER, " + "FilterAttributeId INTEGER )") + << QLatin1String("CREATE TABLE SettingsTable (" + "Key TEXT PRIMARY KEY, " + "Value BLOB )"); + + foreach (const QString &q, tables) { + if (!query->exec(q)) + return false; + } + return true; +} + +QStringList QHelpCollectionHandler::customFilters() const +{ + QStringList list; + if (m_dbOpened) { + m_query.exec(QLatin1String("SELECT Name FROM FilterNameTable")); + while (m_query.next()) + list.append(m_query.value(0).toString()); + } + return list; +} + +bool QHelpCollectionHandler::removeCustomFilter(const QString &filterName) +{ + if (!isDBOpened() || filterName.isEmpty()) + return false; + + int filterNameId = -1; + m_query.prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?")); + m_query.bindValue(0, filterName); + m_query.exec(); + if (m_query.next()) + filterNameId = m_query.value(0).toInt(); + + if (filterNameId < 0) { + emit error(tr("Unknown filter '%1'!").arg(filterName)); + return false; + } + + m_query.prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?")); + m_query.bindValue(0, filterNameId); + m_query.exec(); + + m_query.prepare(QLatin1String("DELETE FROM FilterNameTable WHERE Id=?")); + m_query.bindValue(0, filterNameId); + m_query.exec(); + + return true; +} + +bool QHelpCollectionHandler::addCustomFilter(const QString &filterName, + const QStringList &attributes) +{ + if (!isDBOpened() || filterName.isEmpty()) + return false; + + int nameId = -1; + m_query.prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?")); + m_query.bindValue(0, filterName); + m_query.exec(); + if (m_query.next()) + nameId = m_query.value(0).toInt(); + + m_query.exec(QLatin1String("SELECT Id, Name FROM FilterAttributeTable")); + QStringList idsToInsert = attributes; + QMap<QString, int> attributeMap; + while (m_query.next()) { + attributeMap.insert(m_query.value(1).toString(), + m_query.value(0).toInt()); + if (idsToInsert.contains(m_query.value(1).toString())) + idsToInsert.removeAll(m_query.value(1).toString()); + } + + foreach (const QString &id, idsToInsert) { + m_query.prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); + m_query.bindValue(0, id); + m_query.exec(); + attributeMap.insert(id, m_query.lastInsertId().toInt()); + } + + if (nameId < 0) { + m_query.prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)")); + m_query.bindValue(0, filterName); + if (m_query.exec()) + nameId = m_query.lastInsertId().toInt(); + } + + if (nameId < 0) { + emit error(tr("Cannot register filter %1!").arg(filterName)); + return false; + } + + m_query.prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?")); + m_query.bindValue(0, nameId); + m_query.exec(); + + foreach (const QString &att, attributes) { + m_query.prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)")); + m_query.bindValue(0, nameId); + m_query.bindValue(1, attributeMap[att]); + if (!m_query.exec()) + return false; + } + return true; +} + +QHelpCollectionHandler::DocInfoList QHelpCollectionHandler::registeredDocumentations() const +{ + DocInfoList list; + if (m_dbOpened) { + m_query.exec(QLatin1String("SELECT a.Name, a.FilePath, b.Name " + "FROM NamespaceTable a, FolderTable b WHERE a.Id=b.NamespaceId")); + + while (m_query.next()) { + DocInfo info; + info.fileName = m_query.value(1).toString(); + info.folderName = m_query.value(2).toString(); + info.namespaceName = m_query.value(0).toString(); + list.append(info); + } + } + return list; +} + +bool QHelpCollectionHandler::registerDocumentation(const QString &fileName) +{ + if (!isDBOpened()) + return false; + + QHelpDBReader reader(fileName, QHelpGlobal::uniquifyConnectionName( + QLatin1String("QHelpCollectionHandler"), this), 0); + if (!reader.init()) { + emit error(tr("Cannot open documentation file %1!").arg(fileName)); + return false; + } + + QString ns = reader.namespaceName(); + if (ns.isEmpty()) { + emit error(tr("Invalid documentation file '%1'!").arg(fileName)); + return false; + } + + int nsId = registerNamespace(ns, fileName); + if (nsId < 1) + return false; + + if (!registerVirtualFolder(reader.virtualFolder(), nsId)) + return false; + + addFilterAttributes(reader.filterAttributes()); + foreach (const QString &filterName, reader.customFilters()) + addCustomFilter(filterName, reader.filterAttributes(filterName)); + + optimizeDatabase(fileName); + + return true; +} + +bool QHelpCollectionHandler::unregisterDocumentation(const QString &namespaceName) +{ + if (!isDBOpened()) + return false; + + m_query.prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?")); + m_query.bindValue(0, namespaceName); + m_query.exec(); + + int nsId = -1; + if (m_query.next()) + nsId = m_query.value(0).toInt(); + + if (nsId < 0) { + emit error(tr("The namespace %1 was not registered!").arg(namespaceName)); + return false; + } + + m_query.prepare(QLatin1String("DELETE FROM NamespaceTable WHERE Id=?")); + m_query.bindValue(0, nsId); + m_query.exec(); + + m_query.prepare(QLatin1String("DELETE FROM FolderTable WHERE NamespaceId=?")); + m_query.bindValue(0, nsId); + return m_query.exec(); +} + +bool QHelpCollectionHandler::removeCustomValue(const QString &key) +{ + if (!isDBOpened()) + return false; + + m_query.prepare(QLatin1String("DELETE FROM SettingsTable WHERE Key=?")); + m_query.bindValue(0, key); + return m_query.exec(); +} + +QVariant QHelpCollectionHandler::customValue(const QString &key, + const QVariant &defaultValue) const +{ + QVariant value = defaultValue; + if (m_dbOpened) { + m_query.prepare(QLatin1String("SELECT COUNT(Key) FROM SettingsTable WHERE Key=?")); + m_query.bindValue(0, key); + if (!m_query.exec() || !m_query.next() || !m_query.value(0).toInt()) { + m_query.clear(); + return defaultValue; + } + + m_query.clear(); + m_query.prepare(QLatin1String("SELECT Value FROM SettingsTable WHERE Key=?")); + m_query.bindValue(0, key); + if (m_query.exec() && m_query.next()) + value = m_query.value(0); + m_query.clear(); + } + return value; +} + +bool QHelpCollectionHandler::setCustomValue(const QString &key, + const QVariant &value) +{ + if (!isDBOpened()) + return false; + + m_query.prepare(QLatin1String("SELECT Value FROM SettingsTable WHERE Key=?")); + m_query.bindValue(0, key); + m_query.exec(); + if (m_query.next()) { + m_query.prepare(QLatin1String("UPDATE SettingsTable SET Value=? where Key=?")); + m_query.bindValue(0, value); + m_query.bindValue(1, key); + } + else { + m_query.prepare(QLatin1String("INSERT INTO SettingsTable VALUES(?, ?)")); + m_query.bindValue(0, key); + m_query.bindValue(1, value); + } + return m_query.exec(); +} + +bool QHelpCollectionHandler::addFilterAttributes(const QStringList &attributes) +{ + if (!isDBOpened()) + return false; + + m_query.exec(QLatin1String("SELECT Name FROM FilterAttributeTable")); + QSet<QString> atts; + while (m_query.next()) + atts.insert(m_query.value(0).toString()); + + foreach (const QString &s, attributes) { + if (!atts.contains(s)) { + m_query.prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); + m_query.bindValue(0, s); + m_query.exec(); + } + } + return true; +} + +QStringList QHelpCollectionHandler::filterAttributes() const +{ + QStringList list; + if (m_dbOpened) { + m_query.exec(QLatin1String("SELECT Name FROM FilterAttributeTable")); + while (m_query.next()) + list.append(m_query.value(0).toString()); + } + return list; +} + +QStringList QHelpCollectionHandler::filterAttributes(const QString &filterName) const +{ + QStringList list; + if (m_dbOpened) { + m_query.prepare(QLatin1String("SELECT a.Name FROM FilterAttributeTable a, " + "FilterTable b, FilterNameTable c WHERE a.Id=b.FilterAttributeId " + "AND b.NameId=c.Id AND c.Name=?")); + m_query.bindValue(0, filterName); + m_query.exec(); + while (m_query.next()) + list.append(m_query.value(0).toString()); + } + return list; +} + +int QHelpCollectionHandler::registerNamespace(const QString &nspace, const QString &fileName) +{ + m_query.prepare(QLatin1String("SELECT COUNT(Id) FROM NamespaceTable WHERE Name=?")); + m_query.bindValue(0, nspace); + m_query.exec(); + while (m_query.next()) { + if (m_query.value(0).toInt() > 0) { + emit error(tr("Namespace %1 already exists!").arg(nspace)); + return -1; + } + } + + QFileInfo fi(m_collectionFile); + m_query.prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?, ?)")); + m_query.bindValue(0, nspace); + m_query.bindValue(1, fi.absoluteDir().relativeFilePath(fileName)); + int namespaceId = -1; + if (m_query.exec()) + namespaceId = m_query.lastInsertId().toInt(); + if (namespaceId < 1) { + emit error(tr("Cannot register namespace '%1'!").arg(nspace)); + return -1; + } + return namespaceId; +} + +bool QHelpCollectionHandler::registerVirtualFolder(const QString &folderName, int namespaceId) +{ + m_query.prepare(QLatin1String("INSERT INTO FolderTable VALUES(NULL, ?, ?)")); + m_query.bindValue(0, namespaceId); + m_query.bindValue(1, folderName); + return m_query.exec(); +} + +void QHelpCollectionHandler::optimizeDatabase(const QString &fileName) +{ + if (!QFile::exists(fileName)) + return; + + { // according to removeDatabase() documentation + QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), QLatin1String("optimize")); + db.setDatabaseName(fileName); + if (!db.open()) { + QSqlDatabase::removeDatabase(QLatin1String("optimize")); + emit error(tr("Cannot open database '%1' to optimize!").arg(fileName)); + return; + } + + QSqlQuery query(db); + db.exec(QLatin1String("PRAGMA synchronous=OFF")); + db.exec(QLatin1String("PRAGMA cache_size=3000")); + db.exec(QLatin1String("CREATE INDEX IF NOT EXISTS NameIndex ON IndexTable(Name)")); + db.exec(QLatin1String("CREATE INDEX IF NOT EXISTS FileNameIndex ON FileNameTable(Name)")); + db.exec(QLatin1String("CREATE INDEX IF NOT EXISTS FileIdIndex ON FileNameTable(FileId)")); + + db.close(); + } + + QSqlDatabase::removeDatabase(QLatin1String("optimize")); +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpcollectionhandler_p.h b/src/assistant/help/qhelpcollectionhandler_p.h new file mode 100644 index 000000000..a97af8fa9 --- /dev/null +++ b/src/assistant/help/qhelpcollectionhandler_p.h @@ -0,0 +1,124 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPCOLLECTIONHANDLER_H +#define QHELPCOLLECTIONHANDLER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QList> +#include <QtCore/QString> +#include <QtCore/QObject> +#include <QtCore/QVariant> +#include <QtCore/QStringList> + +#include <QtSql/QSqlQuery> + +QT_BEGIN_NAMESPACE + +class QHelpCollectionHandler : public QObject +{ + Q_OBJECT + +public: + struct DocInfo + { + QString fileName; + QString folderName; + QString namespaceName; + }; + typedef QList<DocInfo> DocInfoList; + + explicit QHelpCollectionHandler(const QString &collectionFile, + QObject *parent = 0); + ~QHelpCollectionHandler(); + + QString collectionFile() const; + + bool openCollectionFile(); + bool copyCollectionFile(const QString &fileName); + + QStringList customFilters() const; + bool removeCustomFilter(const QString &filterName); + bool addCustomFilter(const QString &filterName, + const QStringList &attributes); + + DocInfoList registeredDocumentations() const; + bool registerDocumentation(const QString &fileName); + bool unregisterDocumentation(const QString &namespaceName); + + bool removeCustomValue(const QString &key); + QVariant customValue(const QString &key, const QVariant &defaultValue) const; + bool setCustomValue(const QString &key, const QVariant &value); + + bool addFilterAttributes(const QStringList &attributes); + QStringList filterAttributes() const; + QStringList filterAttributes(const QString &filterName) const; + + int registerNamespace(const QString &nspace, const QString &fileName); + bool registerVirtualFolder(const QString &folderName, int namespaceId); + void optimizeDatabase(const QString &fileName); + +signals: + void error(const QString &msg); + +private: + bool isDBOpened(); + bool createTables(QSqlQuery *query); + + bool m_dbOpened; + QString m_collectionFile; + QString m_connectionName; + mutable QSqlQuery m_query; +}; + +QT_END_NAMESPACE + +#endif //QHELPCOLLECTIONHANDLER_H diff --git a/src/assistant/help/qhelpcontentwidget.cpp b/src/assistant/help/qhelpcontentwidget.cpp new file mode 100644 index 000000000..7e3cb93d1 --- /dev/null +++ b/src/assistant/help/qhelpcontentwidget.cpp @@ -0,0 +1,586 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpcontentwidget.h" +#include "qhelpenginecore.h" +#include "qhelpengine_p.h" +#include "qhelpdbreader_p.h" + +#include <QtCore/QDir> +#include <QtCore/QStack> +#include <QtCore/QThread> +#include <QtCore/QMutex> +#include <QtWidgets/QHeaderView> + +QT_BEGIN_NAMESPACE + +class QHelpContentItemPrivate +{ +public: + QHelpContentItemPrivate(const QString &t, const QString &l, + QHelpDBReader *r, QHelpContentItem *p) + { + parent = p; + title = t; + link = l; + helpDBReader = r; + } + + QList<QHelpContentItem*> childItems; + QHelpContentItem *parent; + QString title; + QString link; + QHelpDBReader *helpDBReader; +}; + +class QHelpContentProvider : public QThread +{ +public: + QHelpContentProvider(QHelpEnginePrivate *helpEngine); + ~QHelpContentProvider(); + void collectContents(const QString &customFilterName); + void stopCollecting(); + QHelpContentItem *rootItem(); + int nextChildCount() const; + +private: + void run(); + + QHelpEnginePrivate *m_helpEngine; + QHelpContentItem *m_rootItem; + QStringList m_filterAttributes; + QQueue<QHelpContentItem*> m_rootItems; + QMutex m_mutex; + bool m_abort; +}; + +class QHelpContentModelPrivate +{ +public: + QHelpContentItem *rootItem; + QHelpContentProvider *qhelpContentProvider; +}; + + + +/*! + \class QHelpContentItem + \inmodule QtHelp + \brief The QHelpContentItem class provides an item for use with QHelpContentModel. + \since 4.4 +*/ + +QHelpContentItem::QHelpContentItem(const QString &name, const QString &link, + QHelpDBReader *reader, QHelpContentItem *parent) +{ + d = new QHelpContentItemPrivate(name, link, reader, parent); +} + +/*! + Destroys the help content item. +*/ +QHelpContentItem::~QHelpContentItem() +{ + qDeleteAll(d->childItems); + delete d; +} + +void QHelpContentItem::appendChild(QHelpContentItem *item) +{ + d->childItems.append(item); +} + +/*! + Returns the child of the content item in the give \a row. + + \sa parent() +*/ +QHelpContentItem *QHelpContentItem::child(int row) const +{ + if (row >= childCount()) + return 0; + return d->childItems.value(row); +} + +/*! + Returns the number of child items. +*/ +int QHelpContentItem::childCount() const +{ + return d->childItems.count(); +} + +/*! + Returns the row of this item from its parents view. +*/ +int QHelpContentItem::row() const +{ + if (d->parent) + return d->parent->d->childItems.indexOf(const_cast<QHelpContentItem*>(this)); + return 0; +} + +/*! + Returns the title of the content item. +*/ +QString QHelpContentItem::title() const +{ + return d->title; +} + +/*! + Returns the URL of this content item. +*/ +QUrl QHelpContentItem::url() const +{ + return d->helpDBReader->urlOfPath(d->link); +} + +/*! + Returns the parent content item. +*/ +QHelpContentItem *QHelpContentItem::parent() const +{ + return d->parent; +} + +/*! + Returns the position of a given \a child. +*/ +int QHelpContentItem::childPosition(QHelpContentItem *child) const +{ + return d->childItems.indexOf(child); +} + + + +QHelpContentProvider::QHelpContentProvider(QHelpEnginePrivate *helpEngine) + : QThread(helpEngine) +{ + m_helpEngine = helpEngine; + m_rootItem = 0; + m_abort = false; +} + +QHelpContentProvider::~QHelpContentProvider() +{ + stopCollecting(); +} + +void QHelpContentProvider::collectContents(const QString &customFilterName) +{ + m_mutex.lock(); + m_filterAttributes = m_helpEngine->q->filterAttributes(customFilterName); + m_mutex.unlock(); + if (!isRunning()) { + start(LowPriority); + } else { + stopCollecting(); + start(LowPriority); + } +} + +void QHelpContentProvider::stopCollecting() +{ + if (!isRunning()) + return; + m_mutex.lock(); + m_abort = true; + m_mutex.unlock(); + wait(); +} + +QHelpContentItem *QHelpContentProvider::rootItem() +{ + QMutexLocker locker(&m_mutex); + return m_rootItems.dequeue(); +} + +int QHelpContentProvider::nextChildCount() const +{ + return m_rootItems.head()->childCount(); +} + +void QHelpContentProvider::run() +{ + QString title; + QString link; + int depth = 0; + QHelpContentItem *item = 0; + + m_mutex.lock(); + m_rootItem = new QHelpContentItem(QString(), QString(), 0); + m_rootItems.enqueue(m_rootItem); + QStringList atts = m_filterAttributes; + const QStringList fileNames = m_helpEngine->orderedFileNameList; + m_mutex.unlock(); + + foreach (const QString &dbFileName, fileNames) { + m_mutex.lock(); + if (m_abort) { + m_abort = false; + m_mutex.unlock(); + break; + } + m_mutex.unlock(); + QHelpDBReader reader(dbFileName, + QHelpGlobal::uniquifyConnectionName(dbFileName + + QLatin1String("FromQHelpContentProvider"), + QThread::currentThread()), 0); + if (!reader.init()) + continue; + foreach (const QByteArray& ba, reader.contentsForFilter(atts)) { + if (ba.size() < 1) + continue; + + int _depth = 0; + bool _root = false; + QStack<QHelpContentItem*> stack; + + QDataStream s(ba); + for (;;) { + s >> depth; + s >> link; + s >> title; + if (title.isEmpty()) + break; +CHECK_DEPTH: + if (depth == 0) { + m_mutex.lock(); + item = new QHelpContentItem(title, link, + m_helpEngine->fileNameReaderMap.value(dbFileName), m_rootItem); + m_rootItem->appendChild(item); + m_mutex.unlock(); + stack.push(item); + _depth = 1; + _root = true; + } else { + if (depth > _depth && _root) { + _depth = depth; + stack.push(item); + } + if (depth == _depth) { + item = new QHelpContentItem(title, link, + m_helpEngine->fileNameReaderMap.value(dbFileName), stack.top()); + stack.top()->appendChild(item); + } else if (depth < _depth) { + stack.pop(); + --_depth; + goto CHECK_DEPTH; + } + } + } + } + } + m_mutex.lock(); + m_abort = false; + m_mutex.unlock(); +} + + + +/*! + \class QHelpContentModel + \inmodule QtHelp + \brief The QHelpContentModel class provides a model that supplies content to views. + \since 4.4 +*/ + +/*! + \fn void QHelpContentModel::contentsCreationStarted() + + This signal is emitted when the creation of the contents has + started. The current contents are invalid from this point on + until the signal contentsCreated() is emitted. + + \sa isCreatingContents() +*/ + +/*! + \fn void QHelpContentModel::contentsCreated() + + This signal is emitted when the contents have been created. +*/ + +QHelpContentModel::QHelpContentModel(QHelpEnginePrivate *helpEngine) + : QAbstractItemModel(helpEngine) +{ + d = new QHelpContentModelPrivate(); + d->rootItem = 0; + d->qhelpContentProvider = new QHelpContentProvider(helpEngine); + + connect(d->qhelpContentProvider, SIGNAL(finished()), + this, SLOT(insertContents()), Qt::QueuedConnection); + connect(helpEngine->q, SIGNAL(setupStarted()), this, SLOT(invalidateContents())); +} + +/*! + Destroys the help content model. +*/ +QHelpContentModel::~QHelpContentModel() +{ + delete d->rootItem; + delete d; +} + +void QHelpContentModel::invalidateContents(bool onShutDown) +{ + if (onShutDown) + disconnect(this, SLOT(insertContents())); + d->qhelpContentProvider->stopCollecting(); + if (d->rootItem) { + delete d->rootItem; + d->rootItem = 0; + } + if (!onShutDown) + reset(); +} + +/*! + Creates new contents by querying the help system + for contents specified for the \a customFilterName. +*/ +void QHelpContentModel::createContents(const QString &customFilterName) +{ + d->qhelpContentProvider->collectContents(customFilterName); + emit contentsCreationStarted(); +} + +void QHelpContentModel::insertContents() +{ + int count; + if (d->rootItem) { + count = d->rootItem->childCount() - 1; + beginRemoveRows(QModelIndex(), 0, count > 0 ? count : 0); + delete d->rootItem; + d->rootItem = 0; + endRemoveRows(); + } + + count = d->qhelpContentProvider->nextChildCount() - 1; + beginInsertRows(QModelIndex(), 0, count > 0 ? count : 0); + d->rootItem = d->qhelpContentProvider->rootItem(); + endInsertRows(); + reset(); + emit contentsCreated(); +} + +/*! + Returns true if the contents are currently rebuilt, otherwise + false. +*/ +bool QHelpContentModel::isCreatingContents() const +{ + return d->qhelpContentProvider->isRunning(); +} + +/*! + Returns the help content item at the model index position + \a index. +*/ +QHelpContentItem *QHelpContentModel::contentItemAt(const QModelIndex &index) const +{ + if (index.isValid()) + return static_cast<QHelpContentItem*>(index.internalPointer()); + else + return d->rootItem; +} + +/*! + Returns the index of the item in the model specified by + the given \a row, \a column and \a parent index. +*/ +QModelIndex QHelpContentModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!d->rootItem) + return QModelIndex(); + + QHelpContentItem *parentItem = contentItemAt(parent); + QHelpContentItem *item = parentItem->child(row); + if (!item) + return QModelIndex(); + return createIndex(row, column, item); +} + +/*! + Returns the parent of the model item with the given + \a index, or QModelIndex() if it has no parent. +*/ +QModelIndex QHelpContentModel::parent(const QModelIndex &index) const +{ + QHelpContentItem *item = contentItemAt(index); + if (!item) + return QModelIndex(); + + QHelpContentItem *parentItem = static_cast<QHelpContentItem*>(item->parent()); + if (!parentItem) + return QModelIndex(); + + QHelpContentItem *grandparentItem = static_cast<QHelpContentItem*>(parentItem->parent()); + if (!grandparentItem) + return QModelIndex(); + + int row = grandparentItem->childPosition(parentItem); + return createIndex(row, index.column(), parentItem); +} + +/*! + Returns the number of rows under the given \a parent. +*/ +int QHelpContentModel::rowCount(const QModelIndex &parent) const +{ + QHelpContentItem *parentItem = contentItemAt(parent); + if (!parentItem) + return 0; + return parentItem->childCount(); +} + +/*! + Returns the number of columns under the given \a parent. Currently returns always 1. +*/ +int QHelpContentModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return 1; +} + +/*! + Returns the data stored under the given \a role for + the item referred to by the \a index. +*/ +QVariant QHelpContentModel::data(const QModelIndex &index, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + QHelpContentItem *item = contentItemAt(index); + if (!item) + return QVariant(); + return item->title(); +} + + + +/*! + \class QHelpContentWidget + \inmodule QtHelp + \brief The QHelpContentWidget class provides a tree view for displaying help content model items. + \since 4.4 +*/ + +/*! + \fn void QHelpContentWidget::linkActivated(const QUrl &link) + + This signal is emitted when a content item is activated and + its associated \a link should be shown. +*/ + +QHelpContentWidget::QHelpContentWidget() + : QTreeView(0) +{ + header()->hide(); + setUniformRowHeights(true); + connect(this, SIGNAL(activated(QModelIndex)), + this, SLOT(showLink(QModelIndex))); +} + +/*! + Returns the index of the content item with the \a link. + An invalid index is returned if no such an item exists. +*/ +QModelIndex QHelpContentWidget::indexOf(const QUrl &link) +{ + QHelpContentModel *contentModel = + qobject_cast<QHelpContentModel*>(model()); + if (!contentModel || link.scheme() != QLatin1String("qthelp")) + return QModelIndex(); + + m_syncIndex = QModelIndex(); + for (int i=0; i<contentModel->rowCount(); ++i) { + QHelpContentItem *itm = + contentModel->contentItemAt(contentModel->index(i, 0)); + if (itm && itm->url().host() == link.host()) { + QString path = link.path(); + if (path.startsWith(QLatin1Char('/'))) + path = path.mid(1); + if (searchContentItem(contentModel, contentModel->index(i, 0), path)) { + return m_syncIndex; + } + } + } + return QModelIndex(); +} + +bool QHelpContentWidget::searchContentItem(QHelpContentModel *model, + const QModelIndex &parent, const QString &path) +{ + QHelpContentItem *parentItem = model->contentItemAt(parent); + if (!parentItem) + return false; + + if (QDir::cleanPath(parentItem->url().path()) == path) { + m_syncIndex = parent; + return true; + } + + for (int i=0; i<parentItem->childCount(); ++i) { + if (searchContentItem(model, model->index(i, 0, parent), path)) + return true; + } + return false; +} + +void QHelpContentWidget::showLink(const QModelIndex &index) +{ + QHelpContentModel *contentModel = qobject_cast<QHelpContentModel*>(model()); + if (!contentModel) + return; + + QHelpContentItem *item = contentModel->contentItemAt(index); + if (!item) + return; + QUrl url = item->url(); + if (url.isValid()) + emit linkActivated(url); +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpcontentwidget.h b/src/assistant/help/qhelpcontentwidget.h new file mode 100644 index 000000000..ab7d80a43 --- /dev/null +++ b/src/assistant/help/qhelpcontentwidget.h @@ -0,0 +1,146 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPCONTENTWIDGET_H +#define QHELPCONTENTWIDGET_H + +#include <QtHelp/qhelp_global.h> + +#include <QtCore/QQueue> +#include <QtCore/QString> +#include <QtWidgets/QTreeView> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +class QHelpEnginePrivate; +class QHelpDBReader; +class QHelpContentItemPrivate; +class QHelpContentModelPrivate; +class QHelpEngine; +class QHelpContentProvider; + +class QHELP_EXPORT QHelpContentItem +{ +public: + ~QHelpContentItem(); + + QHelpContentItem *child(int row) const; + int childCount() const; + QString title() const; + QUrl url() const; + int row() const; + QHelpContentItem *parent() const; + int childPosition(QHelpContentItem *child) const; + +private: + QHelpContentItem(const QString &name, const QString &link, + QHelpDBReader *reader, QHelpContentItem *parent = 0); + void appendChild(QHelpContentItem *child); + + QHelpContentItemPrivate *d; + friend class QHelpContentProvider; +}; + +class QHELP_EXPORT QHelpContentModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + ~QHelpContentModel(); + + void createContents(const QString &customFilterName); + QHelpContentItem *contentItemAt(const QModelIndex &index) const; + + QVariant data(const QModelIndex &index, int role) const; + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + bool isCreatingContents() const; + +Q_SIGNALS: + void contentsCreationStarted(); + void contentsCreated(); + +private Q_SLOTS: + void insertContents(); + void invalidateContents(bool onShutDown = false); + +private: + QHelpContentModel(QHelpEnginePrivate *helpEngine); + QHelpContentModelPrivate *d; + friend class QHelpEnginePrivate; +}; + +class QHELP_EXPORT QHelpContentWidget : public QTreeView +{ + Q_OBJECT + +public: + QModelIndex indexOf(const QUrl &link); + +Q_SIGNALS: + void linkActivated(const QUrl &link); + +private Q_SLOTS: + void showLink(const QModelIndex &index); + +private: + bool searchContentItem(QHelpContentModel *model, + const QModelIndex &parent, const QString &path); + QModelIndex m_syncIndex; + +private: + QHelpContentWidget(); + friend class QHelpEngine; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif + diff --git a/src/assistant/help/qhelpdatainterface.cpp b/src/assistant/help/qhelpdatainterface.cpp new file mode 100644 index 000000000..d3f07c756 --- /dev/null +++ b/src/assistant/help/qhelpdatainterface.cpp @@ -0,0 +1,273 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpdatainterface_p.h" + +QT_BEGIN_NAMESPACE + +/*! + \internal + \class QHelpDataContentItem + \since 4.4 + \brief The QHelpDataContentItem class provides an item which represents + a topic or section of the contents. + + Every item holds several pieces of information, most notably the title + which can later be displayed in a contents overview. The reference is used + to store a relative file link to the corresponding section in the + documentation. +*/ + +/*! + Constructs a new content item with \a parent as parent item. + The constucted item has the title \a title and links to the + location specified by \a reference. +*/ +QHelpDataContentItem::QHelpDataContentItem(QHelpDataContentItem *parent, + const QString &title, const QString &reference) + : m_title(title), m_reference(reference) +{ + if (parent) + parent->m_children.append(this); +} + +/*! + Destructs the item and its children. +*/ +QHelpDataContentItem::~QHelpDataContentItem() +{ + qDeleteAll(m_children); +} + +/*! + Returns the title of the item. +*/ +QString QHelpDataContentItem::title() const +{ + return m_title; +} + +/*! + Returns the file reference of the item. +*/ +QString QHelpDataContentItem::reference() const +{ + return m_reference; +} + +/*! + Returns a list of all its child items. +*/ +QList<QHelpDataContentItem*> QHelpDataContentItem::children() const +{ + return m_children; +} + +bool QHelpDataIndexItem::operator==(const QHelpDataIndexItem & other) const +{ + return (other.name == name) + && (other.reference == reference); +} + + + +/*! + \internal + \class QHelpDataFilterSection + \since 4.4 +*/ + +/*! + Constructs a help data filter section. +*/ +QHelpDataFilterSection::QHelpDataFilterSection() +{ + d = new QHelpDataFilterSectionData(); +} + +/*! + Adds the filter attribute \a filter to the filter attributes of + this section. +*/ +void QHelpDataFilterSection::addFilterAttribute(const QString &filter) +{ + d->filterAttributes.append(filter); +} + +/*! + Returns a list of all filter attributes defined for this section. +*/ +QStringList QHelpDataFilterSection::filterAttributes() const +{ + return d->filterAttributes; +} + +/*! + Adds the index item \a index to the list of indices. +*/ +void QHelpDataFilterSection::addIndex(const QHelpDataIndexItem &index) +{ + d->indices.append(index); +} + +/*! + Sets the filter sections list of indices to \a indices. +*/ +void QHelpDataFilterSection::setIndices(const QList<QHelpDataIndexItem> &indices) +{ + d->indices = indices; +} + +/*! + Returns the list of indices. +*/ +QList<QHelpDataIndexItem> QHelpDataFilterSection::indices() const +{ + return d->indices; +} + +/*! + Adds the top level content item \a content to the filter section. +*/ +void QHelpDataFilterSection::addContent(QHelpDataContentItem *content) +{ + d->contents.append(content); +} + +/*! + Sets the list of top level content items of the filter section to + \a contents. +*/ +void QHelpDataFilterSection::setContents(const QList<QHelpDataContentItem*> &contents) +{ + qDeleteAll(d->contents); + d->contents = contents; +} + +/*! + Returns a list of top level content items. +*/ +QList<QHelpDataContentItem*> QHelpDataFilterSection::contents() const +{ + return d->contents; +} + +/*! + Adds the file \a file to the filter section. +*/ +void QHelpDataFilterSection::addFile(const QString &file) +{ + d->files.append(file); +} + +/*! + Set the list of files to \a files. +*/ +void QHelpDataFilterSection::setFiles(const QStringList &files) +{ + d->files = files; +} + +/*! + Returns the list of files. +*/ +QStringList QHelpDataFilterSection::files() const +{ + return d->files; +} + +/*! + \internal + \class QHelpDataInterface + \since 4.4 +*/ + +/*! + \fn QHelpDataInterface::QHelpDataInterface() + + Constructs a new help data interface. +*/ + +/*! + \fn QHelpDataInterface::~QHelpDataInterface() + + Destroys the help data interface. +*/ + +/*! + \fn QString QHelpDataInterface::namespaceName() const = 0 + + Returns the namespace name of the help data set. +*/ + +/*! + \fn QString QHelpDataInterface::virtualFolder() const = 0 + + Returns the virtual folder of the help data set. +*/ + +/*! + \fn QList<QHelpDataCustomFilter> QHelpDataInterface::customFilters () const = 0 + + Returns a list of custom filters. Defining custom filters is optional. +*/ + +/*! + \fn QList<QHelpDataFilterSection> QHelpDataInterface::filterSections() const = 0 + + Returns a list of filter sections. +*/ + +/*! + \fn QMap<QString, QVariant> QHelpDataInterface::metaData() const = 0 + + Returns a map of meta data. A meta data item can hold almost any data + and is identified by its name. +*/ + +/*! + \fn QString QHelpDataInterface::rootPath() const = 0 + + Returns the root file path of the documentation data. All referenced file + path or links of content items are relative to this path. +*/ + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpdatainterface_p.h b/src/assistant/help/qhelpdatainterface_p.h new file mode 100644 index 000000000..886d68650 --- /dev/null +++ b/src/assistant/help/qhelpdatainterface_p.h @@ -0,0 +1,155 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPDATAINTERFACE_H +#define QHELPDATAINTERFACE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include "qhelp_global.h" + +#include <QtCore/QStringList> +#include <QtCore/QSharedData> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QHELP_EXPORT QHelpDataContentItem +{ +public: + QHelpDataContentItem(QHelpDataContentItem *parent, const QString &title, + const QString &reference); + ~QHelpDataContentItem(); + + QString title() const; + QString reference() const; + QList<QHelpDataContentItem*> children() const; + +private: + QString m_title; + QString m_reference; + QList<QHelpDataContentItem*> m_children; +}; + +struct QHELP_EXPORT QHelpDataIndexItem { + QHelpDataIndexItem() {} + QHelpDataIndexItem(const QString &n, const QString &id, const QString &r) + : name(n), identifier(id), reference(r) {} + + QString name; + QString identifier; + QString reference; + + bool operator==(const QHelpDataIndexItem & other) const; +}; + +class QHelpDataFilterSectionData : public QSharedData +{ +public: + ~QHelpDataFilterSectionData() + { + qDeleteAll(contents); + } + + QStringList filterAttributes; + QList<QHelpDataIndexItem> indices; + QList<QHelpDataContentItem*> contents; + QStringList files; +}; + +class QHELP_EXPORT QHelpDataFilterSection +{ +public: + QHelpDataFilterSection(); + + void addFilterAttribute(const QString &filter); + QStringList filterAttributes() const; + + void addIndex(const QHelpDataIndexItem &index); + void setIndices(const QList<QHelpDataIndexItem> &indices); + QList<QHelpDataIndexItem> indices() const; + + void addContent(QHelpDataContentItem *content); + void setContents(const QList<QHelpDataContentItem*> &contents); + QList<QHelpDataContentItem*> contents() const; + + void addFile(const QString &file); + void setFiles(const QStringList &files); + QStringList files() const; + +private: + QSharedDataPointer<QHelpDataFilterSectionData> d; +}; + +struct QHELP_EXPORT QHelpDataCustomFilter { + QStringList filterAttributes; + QString name; +}; + +class QHELP_EXPORT QHelpDataInterface +{ +public: + QHelpDataInterface() {} + virtual ~QHelpDataInterface() {} + + virtual QString namespaceName() const = 0; + virtual QString virtualFolder() const = 0; + virtual QList<QHelpDataCustomFilter> customFilters() const = 0; + virtual QList<QHelpDataFilterSection> filterSections() const = 0; + virtual QMap<QString, QVariant> metaData() const = 0; + virtual QString rootPath() const = 0; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QHELPDATAINTERFACE_H diff --git a/src/assistant/help/qhelpdbreader.cpp b/src/assistant/help/qhelpdbreader.cpp new file mode 100644 index 000000000..c4735f8b8 --- /dev/null +++ b/src/assistant/help/qhelpdbreader.cpp @@ -0,0 +1,583 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpdbreader_p.h" +#include "qhelp_global.h" + +#include <QtCore/QVariant> +#include <QtCore/QFile> +#include <QtSql/QSqlError> +#include <QtSql/QSqlQuery> + +QT_BEGIN_NAMESPACE + +QHelpDBReader::QHelpDBReader(const QString &dbName) + : QObject(0) +{ + initObject(dbName, + QHelpGlobal::uniquifyConnectionName(QLatin1String("QHelpDBReader"), + this)); +} + +QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId, + QObject *parent) + : QObject(parent) +{ + initObject(dbName, uniqueId); +} + +void QHelpDBReader::initObject(const QString &dbName, const QString &uniqueId) +{ + m_dbName = dbName; + m_uniqueId = uniqueId; + m_initDone = false; + m_query = 0; + m_useAttributesCache = false; +} + +QHelpDBReader::~QHelpDBReader() +{ + if (m_initDone) { + delete m_query; + QSqlDatabase::removeDatabase(m_uniqueId); + } +} + +bool QHelpDBReader::init() +{ + if (m_initDone) + return true; + + if (!QFile::exists(m_dbName)) + return false; + + QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), m_uniqueId); + db.setDatabaseName(m_dbName); + if (!db.open()) { + /*: The placeholders are: %1 - The name of the database which cannot be opened + %2 - The unique id for the connection + %3 - The actual error string */ + m_error = tr("Cannot open database '%1' '%2': %3").arg(m_dbName, m_uniqueId, db.lastError().text()); + QSqlDatabase::removeDatabase(m_uniqueId); + return false; + } + + m_initDone = true; + m_query = new QSqlQuery(db); + + return true; +} + +QString QHelpDBReader::databaseName() const +{ + return m_dbName; +} + +QString QHelpDBReader::errorMessage() const +{ + return m_error; +} + +QString QHelpDBReader::namespaceName() const +{ + if (!m_namespace.isEmpty()) + return m_namespace; + if (m_query) { + m_query->exec(QLatin1String("SELECT Name FROM NamespaceTable")); + if (m_query->next()) + m_namespace = m_query->value(0).toString(); + } + return m_namespace; +} + +QString QHelpDBReader::virtualFolder() const +{ + if (m_query) { + m_query->exec(QLatin1String("SELECT Name FROM FolderTable WHERE Id=1")); + if (m_query->next()) + return m_query->value(0).toString(); + } + return QString(); +} + +QList<QStringList> QHelpDBReader::filterAttributeSets() const +{ + QList<QStringList> result; + if (m_query) { + m_query->exec(QLatin1String("SELECT a.Id, b.Name FROM FileAttributeSetTable a, " + "FilterAttributeTable b WHERE a.FilterAttributeId=b.Id ORDER BY a.Id")); + int oldId = -1; + while (m_query->next()) { + int id = m_query->value(0).toInt(); + if (id != oldId) { + result.append(QStringList()); + oldId = id; + } + result.last().append(m_query->value(1).toString()); + } + } + return result; +} + +bool QHelpDBReader::fileExists(const QString &virtualFolder, + const QString &filePath, + const QStringList &filterAttributes) const +{ + if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query) + return false; + +//SELECT COUNT(a.Name) FROM FileNameTable a, FolderTable b, FileFilterTable c, FilterAttributeTable d WHERE a.FolderId=b.Id AND b.Name='qtdoc' AND a.Name='qstring.html' AND a.FileId=c.FileId AND c.FilterAttributeId=d.Id AND d.Name='qtrefdoc' + + QString query; + namespaceName(); + if (filterAttributes.isEmpty()) { + query = QString(QLatin1String("SELECT COUNT(a.Name) FROM FileNameTable a, FolderTable b " + "WHERE a.FolderId=b.Id AND b.Name=\'%1\' AND a.Name=\'%2\'")).arg(quote(virtualFolder)).arg(quote(filePath)); + } else { + query = QString(QLatin1String("SELECT COUNT(a.Name) FROM FileNameTable a, FolderTable b, " + "FileFilterTable c, FilterAttributeTable d WHERE a.FolderId=b.Id " + "AND b.Name=\'%1\' AND a.Name=\'%2\' AND a.FileId=c.FileId AND " + "c.FilterAttributeId=d.Id AND d.Name=\'%3\'")) + .arg(quote(virtualFolder)).arg(quote(filePath)) + .arg(quote(filterAttributes.first())); + for (int i=1; i<filterAttributes.count(); ++i) { + query.append(QString(QLatin1String(" INTERSECT SELECT COUNT(a.Name) FROM FileNameTable a, " + "FolderTable b, FileFilterTable c, FilterAttributeTable d WHERE a.FolderId=b.Id " + "AND b.Name=\'%1\' AND a.Name=\'%2\' AND a.FileId=c.FileId AND " + "c.FilterAttributeId=d.Id AND d.Name=\'%3\'")) + .arg(quote(virtualFolder)).arg(quote(filePath)) + .arg(quote(filterAttributes.at(i)))); + } + } + m_query->exec(query); + if (m_query->next() && m_query->isValid() && m_query->value(0).toInt()) + return true; + return false; +} + +QByteArray QHelpDBReader::fileData(const QString &virtualFolder, + const QString &filePath) const +{ + QByteArray ba; + if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query) + return ba; + + namespaceName(); + m_query->prepare(QLatin1String("SELECT a.Data FROM FileDataTable a, FileNameTable b, FolderTable c, " + "NamespaceTable d WHERE a.Id=b.FileId AND (b.Name=? OR b.Name=?) AND b.FolderId=c.Id " + "AND c.Name=? AND c.NamespaceId=d.Id AND d.Name=?")); + m_query->bindValue(0, filePath); + m_query->bindValue(1, QString(QLatin1String("./") + filePath)); + m_query->bindValue(2, virtualFolder); + m_query->bindValue(3, m_namespace); + m_query->exec(); + if (m_query->next() && m_query->isValid()) + ba = qUncompress(m_query->value(0).toByteArray()); + return ba; +} + +QStringList QHelpDBReader::customFilters() const +{ + QStringList lst; + if (m_query) { + m_query->exec(QLatin1String("SELECT Name FROM FilterNameTable")); + while (m_query->next()) + lst.append(m_query->value(0).toString()); + } + return lst; +} + +QStringList QHelpDBReader::filterAttributes(const QString &filterName) const +{ + QStringList lst; + if (m_query) { + if (filterName.isEmpty()) { + m_query->prepare(QLatin1String("SELECT Name FROM FilterAttributeTable")); + } else { + m_query->prepare(QLatin1String("SELECT a.Name FROM FilterAttributeTable a, " + "FilterTable b, FilterNameTable c WHERE c.Name=? " + "AND c.Id=b.NameId AND b.FilterAttributeId=a.Id")); + m_query->bindValue(0, filterName); + } + m_query->exec(); + while (m_query->next()) + lst.append(m_query->value(0).toString()); + } + return lst; +} + +QStringList QHelpDBReader::indicesForFilter(const QStringList &filterAttributes) const +{ + QStringList indices; + if (!m_query) + return indices; + + //SELECT DISTINCT a.Name FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c WHERE a.Id=b.IndexId AND b.FilterAttributeId=c.Id AND c.Name in ('4.2.3', 'qt') + + QString query; + if (filterAttributes.isEmpty()) { + query = QLatin1String("SELECT DISTINCT Name FROM IndexTable"); + } else { + query = QString(QLatin1String("SELECT DISTINCT a.Name FROM IndexTable a, " + "IndexFilterTable b, FilterAttributeTable c WHERE a.Id=b.IndexId " + "AND b.FilterAttributeId=c.Id AND c.Name='%1'")).arg(quote(filterAttributes.first())); + for (int i=1; i<filterAttributes.count(); ++i) { + query.append(QString(QLatin1String(" INTERSECT SELECT DISTINCT a.Name FROM IndexTable a, " + "IndexFilterTable b, FilterAttributeTable c WHERE a.Id=b.IndexId " + "AND b.FilterAttributeId=c.Id AND c.Name='%1'")) + .arg(quote(filterAttributes.at(i)))); + } + } + + m_query->exec(query); + while (m_query->next()) { + if (!m_query->value(0).toString().isEmpty()) + indices.append(m_query->value(0).toString()); + } + return indices; +} + +void QHelpDBReader::linksForKeyword(const QString &keyword, const QStringList &filterAttributes, + QMap<QString, QUrl> &linkMap) const +{ + if (!m_query) + return; + + QString query; + if (filterAttributes.isEmpty()) { + query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor " + "FROM IndexTable a, FileNameTable d, " + "FolderTable e, NamespaceTable f WHERE " + "a.FileId=d.FileId AND d.FolderId=e.Id AND a.NamespaceId=f.Id " + "AND a.Name='%1'")).arg(quote(keyword)); + } else if (m_useAttributesCache) { + query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor, a.Id " + "FROM IndexTable a, " + "FileNameTable d, FolderTable e, NamespaceTable f WHERE " + "a.FileId=d.FileId AND d.FolderId=e.Id " + "AND a.NamespaceId=f.Id AND a.Name='%1'")) + .arg(quote(keyword)); + m_query->exec(query); + while (m_query->next()) { + if (m_indicesCache.contains(m_query->value(5).toInt())) { + linkMap.insertMulti(m_query->value(0).toString(), buildQUrl(m_query->value(1).toString(), + m_query->value(2).toString(), m_query->value(3).toString(), + m_query->value(4).toString())); + } + } + return; + } else { + query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor " + "FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c, " + "FileNameTable d, FolderTable e, NamespaceTable f " + "WHERE a.FileId=d.FileId AND d.FolderId=e.Id " + "AND a.NamespaceId=f.Id AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id " + "AND a.Name='%1' AND c.Name='%2'")).arg(quote(keyword)) + .arg(quote(filterAttributes.first())); + for (int i=1; i<filterAttributes.count(); ++i) { + query.append(QString(QLatin1String(" INTERSECT SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor " + "FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c, " + "FileNameTable d, FolderTable e, NamespaceTable f " + "WHERE a.FileId=d.FileId AND d.FolderId=e.Id " + "AND a.NamespaceId=f.Id AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id " + "AND a.Name='%1' AND c.Name='%2'")).arg(quote(keyword)) + .arg(quote(filterAttributes.at(i)))); + } + } + + QString title; + m_query->exec(query); + while (m_query->next()) { + title = m_query->value(0).toString(); + if (title.isEmpty()) // generate a title + corresponding path + title = keyword + QLatin1String(" : ") + m_query->value(3).toString(); + linkMap.insertMulti(title, buildQUrl(m_query->value(1).toString(), + m_query->value(2).toString(), m_query->value(3).toString(), + m_query->value(4).toString())); + } +} + +void QHelpDBReader::linksForIdentifier(const QString &id, + const QStringList &filterAttributes, + QMap<QString, QUrl> &linkMap) const +{ + if (!m_query) + return; + + QString query; + if (filterAttributes.isEmpty()) { + query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor " + "FROM IndexTable a, FileNameTable d, FolderTable e, " + "NamespaceTable f WHERE a.FileId=d.FileId AND " + "d.FolderId=e.Id AND a.NamespaceId=f.Id AND a.Identifier='%1'")) + .arg(quote(id)); + } else if (m_useAttributesCache) { + query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor, a.Id " + "FROM IndexTable a," + "FileNameTable d, FolderTable e, NamespaceTable f WHERE " + "a.FileId=d.FileId AND d.FolderId=e.Id " + "AND a.NamespaceId=f.Id AND a.Identifier='%1'")) + .arg(quote(id)); + m_query->exec(query); + while (m_query->next()) { + if (m_indicesCache.contains(m_query->value(5).toInt())) { + linkMap.insertMulti(m_query->value(0).toString(), buildQUrl(m_query->value(1).toString(), + m_query->value(2).toString(), m_query->value(3).toString(), + m_query->value(4).toString())); + } + } + return; + } else { + query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor " + "FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c, " + "FileNameTable d, FolderTable e, NamespaceTable f " + "WHERE a.FileId=d.FileId AND d.FolderId=e.Id " + "AND a.NamespaceId=f.Id AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id " + "AND a.Identifier='%1' AND c.Name='%2'")).arg(quote(id)) + .arg(quote(filterAttributes.first())); + for (int i=0; i<filterAttributes.count(); ++i) { + query.append(QString(QLatin1String(" INTERSECT SELECT d.Title, f.Name, e.Name, " + "d.Name, a.Anchor FROM IndexTable a, IndexFilterTable b, " + "FilterAttributeTable c, FileNameTable d, " + "FolderTable e, NamespaceTable f WHERE " + "a.FileId=d.FileId AND d.FolderId=e.Id AND a.NamespaceId=f.Id " + "AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id AND " + "a.Identifier='%1' AND c.Name='%2'")).arg(quote(id)) + .arg(quote(filterAttributes.at(i)))); + } + } + + m_query->exec(query); + while (m_query->next()) { + linkMap.insertMulti(m_query->value(0).toString(), buildQUrl(m_query->value(1).toString(), + m_query->value(2).toString(), m_query->value(3).toString(), + m_query->value(4).toString())); + } +} + +QUrl QHelpDBReader::buildQUrl(const QString &ns, const QString &folder, + const QString &relFileName, const QString &anchor) const +{ + QUrl url; + url.setScheme(QLatin1String("qthelp")); + url.setAuthority(ns); + url.setPath(folder + QLatin1Char('/') + relFileName); + url.setFragment(anchor); + return url; +} + +QList<QByteArray> QHelpDBReader::contentsForFilter(const QStringList &filterAttributes) const +{ + QList<QByteArray> contents; + if (!m_query) + return contents; + + //SELECT DISTINCT a.Data FROM ContentsTable a, ContentsFilterTable b, FilterAttributeTable c WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id AND c.Name='qt' INTERSECT SELECT DISTINCT a.Data FROM ContentsTable a, ContentsFilterTable b, FilterAttributeTable c WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id AND c.Name='3.3.8'; + + QString query; + if (filterAttributes.isEmpty()) { + query = QLatin1String("SELECT Data from ContentsTable"); + } else { + query = QString(QLatin1String("SELECT a.Data FROM ContentsTable a, " + "ContentsFilterTable b, FilterAttributeTable c " + "WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id " + "AND c.Name='%1'")).arg(quote(filterAttributes.first())); + for (int i=1; i<filterAttributes.count(); ++i) { + query.append(QString(QLatin1String(" INTERSECT SELECT a.Data FROM ContentsTable a, " + "ContentsFilterTable b, FilterAttributeTable c " + "WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id " + "AND c.Name='%1'")).arg(quote(filterAttributes.at(i)))); + } + } + + m_query->exec(query); + while (m_query->next()) { + contents.append(m_query->value(0).toByteArray()); + } + return contents; +} + +QUrl QHelpDBReader::urlOfPath(const QString &relativePath) const +{ + QUrl url; + if (!m_query) + return url; + + m_query->exec(QLatin1String("SELECT a.Name, b.Name FROM NamespaceTable a, " + "FolderTable b WHERE a.id=b.NamespaceId and a.Id=1")); + if (m_query->next()) { + QString rp = relativePath; + QString anchor; + int i = rp.indexOf(QLatin1Char('#')); + if (i > -1) { + rp = relativePath.left(i); + anchor = relativePath.mid(i+1); + } + url = buildQUrl(m_query->value(0).toString(), + m_query->value(1).toString(), rp, anchor); + } + return url; +} + +QStringList QHelpDBReader::files(const QStringList &filterAttributes, + const QString &extensionFilter) const +{ + QStringList lst; + if (!m_query) + return lst; + + QString query; + QString extension; + if (!extensionFilter.isEmpty()) + extension = QString(QLatin1String("AND b.Name like \'%.%1\'")).arg(extensionFilter); + + if (filterAttributes.isEmpty()) { + query = QString(QLatin1String("SELECT a.Name, b.Name FROM FolderTable a, " + "FileNameTable b WHERE b.FolderId=a.Id %1")) + .arg(extension); + } else { + query = QString(QLatin1String("SELECT a.Name, b.Name FROM FolderTable a, " + "FileNameTable b, FileFilterTable c, FilterAttributeTable d " + "WHERE b.FolderId=a.Id AND b.FileId=c.FileId " + "AND c.FilterAttributeId=d.Id AND d.Name=\'%1\' %2")) + .arg(quote(filterAttributes.first())).arg(extension); + for (int i=1; i<filterAttributes.count(); ++i) { + query.append(QString(QLatin1String(" INTERSECT SELECT a.Name, b.Name FROM " + "FolderTable a, FileNameTable b, FileFilterTable c, " + "FilterAttributeTable d WHERE b.FolderId=a.Id AND " + "b.FileId=c.FileId AND c.FilterAttributeId=d.Id AND " + "d.Name=\'%1\' %2")).arg(quote(filterAttributes.at(i))) + .arg(extension)); + } + } + m_query->exec(query); + while (m_query->next()) { + lst.append(m_query->value(0).toString() + QLatin1Char('/') + + m_query->value(1).toString()); + } + + return lst; +} + +QVariant QHelpDBReader::metaData(const QString &name) const +{ + QVariant v; + if (!m_query) + return v; + + m_query->prepare(QLatin1String("SELECT COUNT(Value), Value FROM MetaDataTable " + "WHERE Name=?")); + m_query->bindValue(0, name); + if (m_query->exec() && m_query->next() + && m_query->value(0).toInt() == 1) + v = m_query->value(1); + return v; +} + +QString QHelpDBReader::mergeList(const QStringList &list) const +{ + QString str; + foreach (const QString &s, list) + str.append(QLatin1Char('\'') + quote(s) + QLatin1String("\', ")); + if (str.endsWith(QLatin1String(", "))) + str = str.left(str.length()-2); + return str; +} + +QString QHelpDBReader::quote(const QString &string) const +{ + QString s = string; + s.replace(QLatin1Char('\''), QLatin1String("\'\'")); + return s; +} + +QSet<int> QHelpDBReader::indexIds(const QStringList &attributes) const +{ + QSet<int> ids; + + if (attributes.isEmpty()) + return ids; + + QString query = QString(QLatin1String("SELECT a.IndexId FROM IndexFilterTable a, " + "FilterAttributeTable b WHERE a.FilterAttributeId=b.Id " + "AND b.Name='%1'")).arg(attributes.first()); + for (int i=0; i<attributes.count(); ++i) { + query.append(QString(QLatin1String(" INTERSECT SELECT a.IndexId FROM " + "IndexFilterTable a, FilterAttributeTable b WHERE " + "a.FilterAttributeId=b.Id AND b.Name='%1'")) + .arg(attributes.at(i))); + } + + if (!m_query->exec(query)) + return ids; + + while (m_query->next()) + ids.insert(m_query->value(0).toInt()); + + return ids; +} + +bool QHelpDBReader::createAttributesCache(const QStringList &attributes, + const QSet<int> &indexIds) +{ + m_useAttributesCache = false; + + if (attributes.count() < 2) { + m_viewAttributes.clear(); + return true; + } + + bool needUpdate = !m_viewAttributes.count(); + + foreach (const QString &s, attributes) + m_viewAttributes.remove(s); + + if (m_viewAttributes.count() || needUpdate) { + m_viewAttributes.clear(); + m_indicesCache = indexIds; + } + foreach (const QString &s, attributes) + m_viewAttributes.insert(s); + m_useAttributesCache = true; + return true; +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpdbreader_p.h b/src/assistant/help/qhelpdbreader_p.h new file mode 100644 index 000000000..5fdf2e90b --- /dev/null +++ b/src/assistant/help/qhelpdbreader_p.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPDBREADER_H +#define QHELPDBREADER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QObject> +#include <QtCore/QStringList> +#include <QtCore/QUrl> +#include <QtCore/QByteArray> +#include <QtCore/QSet> + +QT_BEGIN_NAMESPACE + +class QSqlQuery; + +class QHelpDBReader : public QObject +{ + Q_OBJECT + +public: + QHelpDBReader(const QString &dbName); + QHelpDBReader(const QString &dbName, const QString &uniqueId, + QObject *parent); + ~QHelpDBReader(); + + bool init(); + + QString errorMessage() const; + + QString databaseName() const; + QString namespaceName() const; + QString virtualFolder() const; + QList<QStringList> filterAttributeSets() const; + QStringList files(const QStringList &filterAttributes, + const QString &extensionFilter = QString()) const; + bool fileExists(const QString &virtualFolder, const QString &filePath, + const QStringList &filterAttributes = QStringList()) const; + QByteArray fileData(const QString &virtualFolder, + const QString &filePath) const; + + QStringList customFilters() const; + QStringList filterAttributes(const QString &filterName = QString()) const; + QStringList indicesForFilter(const QStringList &filterAttributes) const; + void linksForKeyword(const QString &keyword, const QStringList &filterAttributes, + QMap<QString, QUrl> &linkMap) const; + + void linksForIdentifier(const QString &id, const QStringList &filterAttributes, + QMap<QString, QUrl> &linkMap) const; + + QList<QByteArray> contentsForFilter(const QStringList &filterAttributes) const; + QUrl urlOfPath(const QString &relativePath) const; + + QSet<int> indexIds(const QStringList &attributes) const; + bool createAttributesCache(const QStringList &attributes, + const QSet<int> &indexIds); + QVariant metaData(const QString &name) const; + +private: + void initObject(const QString &dbName, const QString &uniqueId); + QUrl buildQUrl(const QString &ns, const QString &folder, + const QString &relFileName, const QString &anchor) const; + QString mergeList(const QStringList &list) const; + QString quote(const QString &string) const; + + bool m_initDone; + QString m_dbName; + QString m_uniqueId; + QString m_error; + QSqlQuery *m_query; + mutable QString m_namespace; + QSet<QString> m_viewAttributes; + bool m_useAttributesCache; + QSet<int> m_indicesCache; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/assistant/help/qhelpengine.cpp b/src/assistant/help/qhelpengine.cpp new file mode 100644 index 000000000..007cc84a9 --- /dev/null +++ b/src/assistant/help/qhelpengine.cpp @@ -0,0 +1,213 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpengine.h" +#include "qhelpengine_p.h" +#include "qhelpdbreader_p.h" +#include "qhelpcontentwidget.h" +#include "qhelpindexwidget.h" +#include "qhelpsearchengine.h" +#include "qhelpcollectionhandler_p.h" + +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QLibrary> +#include <QtCore/QPluginLoader> +#include <QtWidgets/QApplication> +#include <QtSql/QSqlQuery> + +QT_BEGIN_NAMESPACE + +QHelpEnginePrivate::QHelpEnginePrivate() + : QHelpEngineCorePrivate() + , contentModel(0) + , contentWidget(0) + , indexModel(0) + , indexWidget(0) + , searchEngine(0) +{ +} + +QHelpEnginePrivate::~QHelpEnginePrivate() +{ +} + +void QHelpEnginePrivate::init(const QString &collectionFile, + QHelpEngineCore *helpEngineCore) +{ + QHelpEngineCorePrivate::init(collectionFile, helpEngineCore); + + if (!contentModel) + contentModel = new QHelpContentModel(this); + if (!indexModel) + indexModel = new QHelpIndexModel(this); + + connect(helpEngineCore, SIGNAL(setupFinished()), this, + SLOT(applyCurrentFilter())); + connect(helpEngineCore, SIGNAL(currentFilterChanged(QString)), this, + SLOT(applyCurrentFilter())); +} + +void QHelpEnginePrivate::applyCurrentFilter() +{ + if (!error.isEmpty()) + return; + contentModel->createContents(currentFilter); + indexModel->createIndex(currentFilter); +} + +void QHelpEnginePrivate::setContentsWidgetBusy() +{ + contentWidget->setCursor(Qt::WaitCursor); +} + +void QHelpEnginePrivate::unsetContentsWidgetBusy() +{ + contentWidget->unsetCursor(); +} + +void QHelpEnginePrivate::setIndexWidgetBusy() +{ + indexWidget->setCursor(Qt::WaitCursor); +} + +void QHelpEnginePrivate::unsetIndexWidgetBusy() +{ + indexWidget->unsetCursor(); +} + +void QHelpEnginePrivate::stopDataCollection() +{ + contentModel->invalidateContents(true); + indexModel->invalidateIndex(true); +} + + + +/*! + \class QHelpEngine + \since 4.4 + \inmodule QtHelp + \brief The QHelpEngine class provides access to contents and + indices of the help engine. + + +*/ + +/*! + Constructs a new help engine with the given \a parent. The help + engine uses the information stored in the \a collectionFile for + providing help. If the collection file does not already exist, + it will be created. +*/ +QHelpEngine::QHelpEngine(const QString &collectionFile, QObject *parent) + : QHelpEngineCore(d = new QHelpEnginePrivate(), parent) +{ + d->init(collectionFile, this); +} + +/*! + Destroys the help engine object. +*/ +QHelpEngine::~QHelpEngine() +{ + d->stopDataCollection(); +} + +/*! + Returns the content model. +*/ +QHelpContentModel *QHelpEngine::contentModel() const +{ + return d->contentModel; +} + +/*! + Returns the index model. +*/ +QHelpIndexModel *QHelpEngine::indexModel() const +{ + return d->indexModel; +} + +/*! + Returns the content widget. +*/ +QHelpContentWidget *QHelpEngine::contentWidget() +{ + if (!d->contentWidget) { + d->contentWidget = new QHelpContentWidget(); + d->contentWidget->setModel(d->contentModel); + connect(d->contentModel, SIGNAL(contentsCreationStarted()), + d, SLOT(setContentsWidgetBusy())); + connect(d->contentModel, SIGNAL(contentsCreated()), + d, SLOT(unsetContentsWidgetBusy())); + } + return d->contentWidget; +} + +/*! + Returns the index widget. +*/ +QHelpIndexWidget *QHelpEngine::indexWidget() +{ + if (!d->indexWidget) { + d->indexWidget = new QHelpIndexWidget(); + d->indexWidget->setModel(d->indexModel); + connect(d->indexModel, SIGNAL(indexCreationStarted()), + d, SLOT(setIndexWidgetBusy())); + connect(d->indexModel, SIGNAL(indexCreated()), + d, SLOT(unsetIndexWidgetBusy())); + } + return d->indexWidget; +} + +/*! + Returns the default search engine. +*/ +QHelpSearchEngine* QHelpEngine::searchEngine() +{ + if (!d->searchEngine) + d->searchEngine = new QHelpSearchEngine(this, this); + return d->searchEngine; +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpengine.h b/src/assistant/help/qhelpengine.h new file mode 100644 index 000000000..cc0bca299 --- /dev/null +++ b/src/assistant/help/qhelpengine.h @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPENGINE_H +#define QHELPENGINE_H + +#include <QtHelp/qhelpenginecore.h> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +class QHelpContentModel; +class QHelpContentWidget; +class QHelpIndexModel; +class QHelpIndexWidget; +class QHelpEnginePrivate; +class QHelpSearchEngine; + +class QHELP_EXPORT QHelpEngine : public QHelpEngineCore +{ + Q_OBJECT + +public: + explicit QHelpEngine(const QString &collectionFile, QObject *parent = 0); + ~QHelpEngine(); + + QHelpContentModel *contentModel() const; + QHelpIndexModel *indexModel() const; + + QHelpContentWidget *contentWidget(); + QHelpIndexWidget *indexWidget(); + + QHelpSearchEngine *searchEngine(); + +private: + QHelpEnginePrivate *d; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif diff --git a/src/assistant/help/qhelpengine_p.h b/src/assistant/help/qhelpengine_p.h new file mode 100644 index 000000000..5a7c30184 --- /dev/null +++ b/src/assistant/help/qhelpengine_p.h @@ -0,0 +1,144 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPENGINE_P_H +#define QHELPENGINE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QMap> +#include <QtCore/QStringList> +#include <QtCore/QObject> + +QT_BEGIN_NAMESPACE + +class QSqlQuery; + +class QHelpEngineCore; +class QHelpDBReader; +class QHelpContentModel; +class QHelpContentWidget; +class QHelpIndexModel; +class QHelpIndexWidget; +class QHelpSearchEngine; +class QHelpCollectionHandler; + +class QHelpEngineCorePrivate : public QObject +{ + Q_OBJECT + +public: + QHelpEngineCorePrivate(); + virtual ~QHelpEngineCorePrivate(); + + virtual void init(const QString &collectionFile, + QHelpEngineCore *helpEngineCore); + + void clearMaps(); + bool setup(); + + QMap<QString, QHelpDBReader*> readerMap; + QMap<QString, QHelpDBReader*> fileNameReaderMap; + QMultiMap<QString, QHelpDBReader*> virtualFolderMap; + QStringList orderedFileNameList; + + QHelpCollectionHandler *collectionHandler; + QString currentFilter; + QString error; + bool needsSetup; + bool autoSaveFilter; + +protected: + QHelpEngineCore *q; + +private slots: + void errorReceived(const QString &msg); +}; + + +class QHelpEnginePrivate : public QHelpEngineCorePrivate +{ + Q_OBJECT + +public: + QHelpEnginePrivate(); + ~QHelpEnginePrivate(); + + void init(const QString &collectionFile, + QHelpEngineCore *helpEngineCore); + + QHelpContentModel *contentModel; + QHelpContentWidget *contentWidget; + + QHelpIndexModel *indexModel; + QHelpIndexWidget *indexWidget; + + QHelpSearchEngine *searchEngine; + + void stopDataCollection(); + + friend class QHelpContentProvider; + friend class QHelpContentModel; + friend class QHelpIndexProvider; + friend class QHelpIndexModel; + +public slots: + void setContentsWidgetBusy(); + void unsetContentsWidgetBusy(); + void setIndexWidgetBusy(); + void unsetIndexWidgetBusy(); + +private slots: + void applyCurrentFilter(); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/assistant/help/qhelpenginecore.cpp b/src/assistant/help/qhelpenginecore.cpp new file mode 100644 index 000000000..178e11ee6 --- /dev/null +++ b/src/assistant/help/qhelpenginecore.cpp @@ -0,0 +1,737 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpenginecore.h" +#include "qhelpengine_p.h" +#include "qhelpdbreader_p.h" +#include "qhelpcollectionhandler_p.h" + +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QLibrary> +#include <QtCore/QPluginLoader> +#include <QtCore/QFileInfo> +#include <QtCore/QThread> +#include <QtWidgets/QApplication> +#include <QtSql/QSqlQuery> + +QT_BEGIN_NAMESPACE + +QHelpEngineCorePrivate::QHelpEngineCorePrivate() +{ + QHelpGlobal::uniquifyConnectionName(QString(), this); + autoSaveFilter = true; +} + +void QHelpEngineCorePrivate::init(const QString &collectionFile, + QHelpEngineCore *helpEngineCore) +{ + q = helpEngineCore; + collectionHandler = new QHelpCollectionHandler(collectionFile, helpEngineCore); + connect(collectionHandler, SIGNAL(error(QString)), + this, SLOT(errorReceived(QString))); + needsSetup = true; +} + +QHelpEngineCorePrivate::~QHelpEngineCorePrivate() +{ + delete collectionHandler; + clearMaps(); +} + +void QHelpEngineCorePrivate::clearMaps() +{ + QMap<QString, QHelpDBReader*>::iterator it = readerMap.begin(); + while (it != readerMap.end()) { + delete it.value(); + ++it; + } + readerMap.clear(); + fileNameReaderMap.clear(); + virtualFolderMap.clear(); + orderedFileNameList.clear(); +} + +bool QHelpEngineCorePrivate::setup() +{ + error.clear(); + if (!needsSetup) + return true; + + needsSetup = false; + emit q->setupStarted(); + clearMaps(); + + if (!collectionHandler->openCollectionFile()) { + emit q->setupFinished(); + return false; + } + + const QHelpCollectionHandler::DocInfoList docList = + collectionHandler->registeredDocumentations(); + QFileInfo fi(collectionHandler->collectionFile()); + QString absFileName; + foreach(const QHelpCollectionHandler::DocInfo &info, docList) { + if (QDir::isAbsolutePath(info.fileName)) { + absFileName = info.fileName; + } else { + absFileName = QFileInfo(fi.absolutePath() + QDir::separator() + info.fileName) + .absoluteFilePath(); + } + QHelpDBReader *reader = new QHelpDBReader(absFileName, + QHelpGlobal::uniquifyConnectionName(info.fileName, this), this); + if (!reader->init()) { + emit q->warning(QHelpEngineCore::tr("Cannot open documentation file %1: %2!") + .arg(absFileName, reader->errorMessage())); + continue; + } + + readerMap.insert(info.namespaceName, reader); + fileNameReaderMap.insert(absFileName, reader); + virtualFolderMap.insert(info.folderName, reader); + orderedFileNameList.append(absFileName); + } + q->currentFilter(); + emit q->setupFinished(); + return true; +} + +void QHelpEngineCorePrivate::errorReceived(const QString &msg) +{ + error = msg; +} + + + +/*! + \class QHelpEngineCore + \since 4.4 + \inmodule QtHelp + \brief The QHelpEngineCore class provides the core functionality + of the help system. + + Before the help engine can be used, it must be initialized by + calling setupData(). At the beginning of the setup process the + signal setupStarted() is emitted. From this point on until + the signal setupFinished() is emitted, is the help data in an + undefined meaning unusable state. + + The core help engine can be used to perform different tasks. + By calling linksForIdentifier() the engine returns + urls specifying the file locations inside the help system. The + actual file data can then be retrived by calling fileData(). In + contrast to all other functions in this class, linksForIdentifier() + depends on the currently set custom filter. Depending on the filter, + the function may return different hits. + + Every help engine can contain any number of custom filters. A custom + filter is defined by a name and set of filter attributes and can be + added to the help engine by calling addCustomFilter(). Analogous, + it is removed by calling removeCustomFilter(). customFilters() returns + all defined filters. + + The help engine also offers the possibility to set and read values + in a persistant way comparable to ini files or Windows registry + entries. For more information see setValue() or value(). + + This class does not offer any GUI components or functionality for + indices or contents. If you need one of those use QHelpEngine + instead. + + When creating a custom help viewer the viewer can be + configured by writing a custom collection file which could contain various + keywords to be used to configure the help engine. These keywords and values + and their meaning can be found in the help information for + \l{assistant-custom-help-viewer.html#creating-a-custom-help-collection-file} + {creating a custom help collection file} for Assistant. +*/ + +/*! + \fn void QHelpEngineCore::setupStarted() + + This signal is emitted when setup is started. +*/ + +/*! + \fn void QHelpEngineCore::setupFinished() + + This signal is emitted when the setup is complete. +*/ + +/*! + \fn void QHelpEngineCore::currentFilterChanged(const QString &newFilter) + + This signal is emitted when the current filter is changed to + \a newFilter. +*/ + +/*! + \fn void QHelpEngineCore::warning(const QString &msg) + + This signal is emitted when a non critical error occurs. + The warning message is stored in \a msg. +*/ + +/*! + Constructs a new core help engine with a \a parent. The help engine + uses the information stored in the \a collectionFile to provide help. + If the collection file does not exist yet, it'll be created. +*/ +QHelpEngineCore::QHelpEngineCore(const QString &collectionFile, QObject *parent) + : QObject(parent) +{ + d = new QHelpEngineCorePrivate(); + d->init(collectionFile, this); +} + +/*! + \internal +*/ +QHelpEngineCore::QHelpEngineCore(QHelpEngineCorePrivate *helpEngineCorePrivate, + QObject *parent) + : QObject(parent) +{ + d = helpEngineCorePrivate; +} + +/*! + Destructs the help engine. +*/ +QHelpEngineCore::~QHelpEngineCore() +{ + delete d; +} + +/*! + \property QHelpEngineCore::collectionFile + \brief the absolute file name of the collection file currently used. + \since 4.5 + + Setting this property leaves the help engine in an invalid state. It is + important to invoke setupData() or any getter function in order to setup + the help engine again. +*/ +QString QHelpEngineCore::collectionFile() const +{ + return d->collectionHandler->collectionFile(); +} + +void QHelpEngineCore::setCollectionFile(const QString &fileName) +{ + if (fileName == collectionFile()) + return; + + if (d->collectionHandler) { + delete d->collectionHandler; + d->collectionHandler = 0; + d->clearMaps(); + } + d->init(fileName, this); + d->needsSetup = true; +} + +/*! + Sets up the help engine by processing the information found + in the collection file and returns true if successful; otherwise + returns false. + + By calling the function, the help + engine is forced to initialize itself immediately. Most of + the times, this function does not have to be called + explicitly because getter functions which depend on a correctly + set up help engine do that themselves. + + \note \c{qsqlite4.dll} needs to be deployed with the application as the + help system uses the sqlite driver when loading help collections. +*/ +bool QHelpEngineCore::setupData() +{ + d->needsSetup = true; + return d->setup(); +} + +/*! + Creates the file \a fileName and copies all contents from + the current collection file into the newly created file, + and returns true if successful; otherwise returns false. + + The copying process makes sure that file references to Qt + Collection files (\c{.qch}) files are updated accordingly. +*/ +bool QHelpEngineCore::copyCollectionFile(const QString &fileName) +{ + if (!d->setup()) + return false; + return d->collectionHandler->copyCollectionFile(fileName); +} + +/*! + Returns the namespace name defined for the Qt compressed help file (.qch) + specified by its \a documentationFileName. If the file is not valid, an + empty string is returned. + + \sa documentationFileName() +*/ +QString QHelpEngineCore::namespaceName(const QString &documentationFileName) +{ + QHelpDBReader reader(documentationFileName, + QHelpGlobal::uniquifyConnectionName(QLatin1String("GetNamespaceName"), + QThread::currentThread()), 0); + if (reader.init()) + return reader.namespaceName(); + return QString(); +} + +/*! + Registers the Qt compressed help file (.qch) contained in the file + \a documentationFileName. One compressed help file, uniquely + identified by its namespace can only be registered once. + True is returned if the registration was successful, otherwise + false. + + \sa unregisterDocumentation(), error() +*/ +bool QHelpEngineCore::registerDocumentation(const QString &documentationFileName) +{ + d->error.clear(); + d->needsSetup = true; + return d->collectionHandler->registerDocumentation(documentationFileName); +} + +/*! + Unregisters the Qt compressed help file (.qch) identified by its + \a namespaceName from the help collection. Returns true + on success, otherwise false. + + \sa registerDocumentation(), error() +*/ +bool QHelpEngineCore::unregisterDocumentation(const QString &namespaceName) +{ + d->error.clear(); + d->needsSetup = true; + return d->collectionHandler->unregisterDocumentation(namespaceName); +} + +/*! + Returns the absolute file name of the Qt compressed help file (.qch) + identified by the \a namespaceName. If there is no Qt compressed help file + with the specified namespace registered, an empty string is returned. + + \sa namespaceName() +*/ +QString QHelpEngineCore::documentationFileName(const QString &namespaceName) +{ + if (d->setup()) { + const QHelpCollectionHandler::DocInfoList docList = + d->collectionHandler->registeredDocumentations(); + foreach(const QHelpCollectionHandler::DocInfo &info, docList) { + if (info.namespaceName == namespaceName) { + if (QDir::isAbsolutePath(info.fileName)) + return QDir::cleanPath(info.fileName); + + QFileInfo fi(d->collectionHandler->collectionFile()); + fi.setFile(fi.absolutePath() + QDir::separator() + info.fileName); + return QDir::cleanPath(fi.absoluteFilePath()); + } + } + } + return QString(); +} + +/*! + Returns a list of all registered Qt compressed help files of the current collection file. + The returned names are the namespaces of the registered Qt compressed help files (.qch). +*/ +QStringList QHelpEngineCore::registeredDocumentations() const +{ + QStringList list; + if (!d->setup()) + return list; + const QHelpCollectionHandler::DocInfoList docList = d->collectionHandler->registeredDocumentations(); + foreach(const QHelpCollectionHandler::DocInfo &info, docList) { + list.append(info.namespaceName); + } + return list; +} + +/*! + Returns a list of custom filters. + + \sa addCustomFilter(), removeCustomFilter() +*/ +QStringList QHelpEngineCore::customFilters() const +{ + if (!d->setup()) + return QStringList(); + return d->collectionHandler->customFilters(); +} + +/*! + Adds the new custom filter \a filterName. The filter attributes + are specified by \a attributes. If the filter already exists, + its attribute set is replaced. The function returns true if + the operation succeeded, otherwise it returns false. + + \sa customFilters(), removeCustomFilter() +*/ +bool QHelpEngineCore::addCustomFilter(const QString &filterName, + const QStringList &attributes) +{ + d->error.clear(); + d->needsSetup = true; + return d->collectionHandler->addCustomFilter(filterName, + attributes); +} + +/*! + Returns true if the filter \a filterName was removed successfully, + otherwise false. + + \sa addCustomFilter(), customFilters() +*/ +bool QHelpEngineCore::removeCustomFilter(const QString &filterName) +{ + d->error.clear(); + d->needsSetup = true; + return d->collectionHandler->removeCustomFilter(filterName); +} + +/*! + Returns a list of all defined filter attributes. +*/ +QStringList QHelpEngineCore::filterAttributes() const +{ + if (!d->setup()) + return QStringList(); + return d->collectionHandler->filterAttributes(); +} + +/*! + Returns a list of filter attributes used by the custom + filter \a filterName. +*/ +QStringList QHelpEngineCore::filterAttributes(const QString &filterName) const +{ + if (!d->setup()) + return QStringList(); + return d->collectionHandler->filterAttributes(filterName); +} + +/*! + \property QHelpEngineCore::currentFilter + \brief the name of the custom filter currently applied. + \since 4.5 + + Setting this property will save the new custom filter permanently in the + help collection file. To set a custom filter without saving it + permanently, disable the auto save filter mode. + + \sa autoSaveFilter() +*/ +QString QHelpEngineCore::currentFilter() const +{ + if (!d->setup()) + return QString(); + + if (d->currentFilter.isEmpty()) { + QString filter = + d->collectionHandler->customValue(QLatin1String("CurrentFilter"), + QString()).toString(); + if (!filter.isEmpty() + && d->collectionHandler->customFilters().contains(filter)) + d->currentFilter = filter; + } + return d->currentFilter; +} + +void QHelpEngineCore::setCurrentFilter(const QString &filterName) +{ + if (!d->setup() || filterName == d->currentFilter) + return; + d->currentFilter = filterName; + if (d->autoSaveFilter) { + d->collectionHandler->setCustomValue(QLatin1String("CurrentFilter"), + d->currentFilter); + } + emit currentFilterChanged(d->currentFilter); +} + +/*! + Returns a list of filter attributes for the different filter sections + defined in the Qt compressed help file with the given namespace + \a namespaceName. +*/ +QList<QStringList> QHelpEngineCore::filterAttributeSets(const QString &namespaceName) const +{ + if (d->setup()) { + QHelpDBReader *reader = d->readerMap.value(namespaceName); + if (reader) + return reader->filterAttributeSets(); + } + return QList<QStringList>(); +} + +/*! + Returns a list of files contained in the Qt compressed help file \a + namespaceName. The files can be filtered by \a filterAttributes as + well as by their extension \a extensionFilter (e.g. 'html'). +*/ +QList<QUrl> QHelpEngineCore::files(const QString namespaceName, + const QStringList &filterAttributes, + const QString &extensionFilter) +{ + QList<QUrl> res; + if (!d->setup()) + return res; + QHelpDBReader *reader = d->readerMap.value(namespaceName); + if (!reader) { + d->error = tr("The specified namespace does not exist!"); + return res; + } + + QUrl url; + url.setScheme(QLatin1String("qthelp")); + url.setAuthority(namespaceName); + + const QStringList files = reader->files(filterAttributes, extensionFilter); + foreach (const QString &file, files) { + url.setPath(QLatin1String("/") + file); + res.append(url); + } + return res; +} + +/*! + Returns an invalid URL if the file \a url cannot be found. + If the file exists, either the same url is returned or a + different url if the file is located in a different namespace + which is merged via a common virtual folder. +*/ +QUrl QHelpEngineCore::findFile(const QUrl &url) const +{ + QUrl res; + if (!d->setup() || !url.isValid() || url.toString().count(QLatin1Char('/')) < 4 + || url.scheme() != QLatin1String("qthelp")) + return res; + + QString ns = url.authority(); + QString filePath = QDir::cleanPath(url.path()); + if (filePath.startsWith(QLatin1Char('/'))) + filePath = filePath.mid(1); + QString virtualFolder = filePath.mid(0, filePath.indexOf(QLatin1Char('/'), 1)); + filePath = filePath.mid(virtualFolder.length()+1); + + QHelpDBReader *defaultReader = 0; + if (d->readerMap.contains(ns)) { + defaultReader = d->readerMap.value(ns); + if (defaultReader->fileExists(virtualFolder, filePath)) + return url; + } + + QStringList filterAtts = filterAttributes(currentFilter()); + foreach (QHelpDBReader *reader, d->virtualFolderMap.values(virtualFolder)) { + if (reader == defaultReader) + continue; + if (reader->fileExists(virtualFolder, filePath, filterAtts)) { + res = url; + res.setAuthority(reader->namespaceName()); + return res; + } + } + + foreach (QHelpDBReader *reader, d->virtualFolderMap.values(virtualFolder)) { + if (reader == defaultReader) + continue; + if (reader->fileExists(virtualFolder, filePath)) { + res = url; + res.setAuthority(reader->namespaceName()); + break; + } + } + + return res; +} + +/*! + Returns the data of the file specified by \a url. If the + file does not exist, an empty QByteArray is returned. + + \sa findFile() +*/ +QByteArray QHelpEngineCore::fileData(const QUrl &url) const +{ + if (!d->setup() || !url.isValid() || url.toString().count(QLatin1Char('/')) < 4 + || url.scheme() != QLatin1String("qthelp")) + return QByteArray(); + + QString ns = url.authority(); + QString filePath = QDir::cleanPath(url.path()); + if (filePath.startsWith(QLatin1Char('/'))) + filePath = filePath.mid(1); + QString virtualFolder = filePath.mid(0, filePath.indexOf(QLatin1Char('/'), 1)); + filePath = filePath.mid(virtualFolder.length()+1); + + QByteArray ba; + QHelpDBReader *defaultReader = 0; + if (d->readerMap.contains(ns)) { + defaultReader = d->readerMap.value(ns); + ba = defaultReader->fileData(virtualFolder, filePath); + } + + if (ba.isEmpty()) { + foreach (QHelpDBReader *reader, d->virtualFolderMap.values(virtualFolder)) { + if (reader == defaultReader) + continue; + ba = reader->fileData(virtualFolder, filePath); + if (!ba.isEmpty()) + return ba; + } + } + return ba; +} + +/*! + Returns a map of hits found for the \a id. A hit contains the + title of the document and the url where the keyword is located. + The result depends on the current filter, meaning only the keywords + registered for the current filter will be returned. +*/ +QMap<QString, QUrl> QHelpEngineCore::linksForIdentifier(const QString &id) const +{ + QMap<QString, QUrl> linkMap; + if (!d->setup()) + return linkMap; + + QStringList atts = filterAttributes(d->currentFilter); + foreach (QHelpDBReader *reader, d->readerMap) + reader->linksForIdentifier(id, atts, linkMap); + + return linkMap; +} + +/*! + Removes the \a key from the settings section in the + collection file. Returns true if the value was removed + successfully, otherwise false. + + \sa customValue(), setCustomValue() +*/ +bool QHelpEngineCore::removeCustomValue(const QString &key) +{ + d->error.clear(); + return d->collectionHandler->removeCustomValue(key); +} + +/*! + Returns the value assigned to the \a key. If the requested + key does not exist, the specified \a defaultValue is + returned. + + \sa setCustomValue(), removeCustomValue() +*/ +QVariant QHelpEngineCore::customValue(const QString &key, const QVariant &defaultValue) const +{ + if (!d->setup()) + return QVariant(); + return d->collectionHandler->customValue(key, defaultValue); +} + +/*! + Save the \a value under the \a key. If the key already exist, + the value will be overwritten. Returns true if the value was + saved successfully, otherwise false. + + \sa customValue(), removeCustomValue() +*/ +bool QHelpEngineCore::setCustomValue(const QString &key, const QVariant &value) +{ + d->error.clear(); + return d->collectionHandler->setCustomValue(key, value); +} + +/*! + Returns the meta data for the Qt compressed help file \a + documentationFileName. If there is no data available for + \a name, an invalid QVariant() is returned. The meta + data is defined when creating the Qt compressed help file and + cannot be modified later. Common meta data includes e.g. + the author of the documentation. +*/ +QVariant QHelpEngineCore::metaData(const QString &documentationFileName, + const QString &name) +{ + QHelpDBReader reader(documentationFileName, QLatin1String("GetMetaData"), 0); + + if (reader.init()) + return reader.metaData(name); + return QVariant(); +} + +/*! + Returns a description of the last error that occurred. +*/ +QString QHelpEngineCore::error() const +{ + return d->error; +} + +/*! + \property QHelpEngineCore::autoSaveFilter + \brief whether QHelpEngineCore is in auto save filter mode or not. + \since 4.5 + + If QHelpEngineCore is in auto save filter mode, the current filter is + automatically saved when it is changed by the setCurrentFilter() + function. The filter is saved persistently in the help collection file. + + By default, this mode is on. +*/ +void QHelpEngineCore::setAutoSaveFilter(bool save) +{ + d->autoSaveFilter = save; +} + +bool QHelpEngineCore::autoSaveFilter() const +{ + return d->autoSaveFilter; +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpenginecore.h b/src/assistant/help/qhelpenginecore.h new file mode 100644 index 000000000..fbfb8a0e8 --- /dev/null +++ b/src/assistant/help/qhelpenginecore.h @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPENGINECORE_H +#define QHELPENGINECORE_H + +#include <QtHelp/qhelp_global.h> + +#include <QtCore/QUrl> +#include <QtCore/QMap> +#include <QtCore/QObject> +#include <QtCore/QVariant> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +class QHelpEngineCorePrivate; + +class QHELP_EXPORT QHelpEngineCore : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool autoSaveFilter READ autoSaveFilter WRITE setAutoSaveFilter) + Q_PROPERTY(QString collectionFile READ collectionFile WRITE setCollectionFile) + Q_PROPERTY(QString currentFilter READ currentFilter WRITE setCurrentFilter) + +public: + explicit QHelpEngineCore(const QString &collectionFile, QObject *parent = 0); + virtual ~QHelpEngineCore(); + + bool setupData(); + + QString collectionFile() const; + void setCollectionFile(const QString &fileName); + + bool copyCollectionFile(const QString &fileName); + + static QString namespaceName(const QString &documentationFileName); + bool registerDocumentation(const QString &documentationFileName); + bool unregisterDocumentation(const QString &namespaceName); + QString documentationFileName(const QString &namespaceName); + + QStringList customFilters() const; + bool removeCustomFilter(const QString &filterName); + bool addCustomFilter(const QString &filterName, + const QStringList &attributes); + + QStringList filterAttributes() const; + QStringList filterAttributes(const QString &filterName) const; + + QString currentFilter() const; + void setCurrentFilter(const QString &filterName); + + QStringList registeredDocumentations() const; + QList<QStringList> filterAttributeSets(const QString &namespaceName) const; + QList<QUrl> files(const QString namespaceName, + const QStringList &filterAttributes, + const QString &extensionFilter = QString()); + QUrl findFile(const QUrl &url) const; + QByteArray fileData(const QUrl &url) const; + + QMap<QString, QUrl> linksForIdentifier(const QString &id) const; + + bool removeCustomValue(const QString &key); + QVariant customValue(const QString &key, + const QVariant &defaultValue = QVariant()) const; + bool setCustomValue(const QString &key, const QVariant &value); + + static QVariant metaData(const QString &documentationFileName, + const QString &name); + + QString error() const; + + void setAutoSaveFilter(bool save); + bool autoSaveFilter() const; + +Q_SIGNALS: + void setupStarted(); + void setupFinished(); + void currentFilterChanged(const QString &newFilter); + void warning(const QString &msg); + +protected: + QHelpEngineCore(QHelpEngineCorePrivate *helpEngineCorePrivate, + QObject *parent); + +private: + QHelpEngineCorePrivate *d; + friend class QHelpEngineCorePrivate; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QHELPENGINECORE_H diff --git a/src/assistant/help/qhelpgenerator.cpp b/src/assistant/help/qhelpgenerator.cpp new file mode 100644 index 000000000..ce9c8562d --- /dev/null +++ b/src/assistant/help/qhelpgenerator.cpp @@ -0,0 +1,909 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpgenerator_p.h" +#include "qhelpdatainterface_p.h" + +#include <math.h> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QDir> +#include <QtCore/QDebug> +#include <QtCore/QSet> +#include <QtCore/QVariant> +#include <QtCore/QDateTime> +#include <QtCore/QTextCodec> +#include <QtSql/QSqlQuery> + +QT_BEGIN_NAMESPACE + +class QHelpGeneratorPrivate +{ +public: + QHelpGeneratorPrivate(); + ~QHelpGeneratorPrivate(); + + QString error; + QSqlQuery *query; + + int namespaceId; + int virtualFolderId; + + QMap<QString, int> fileMap; + QMap<int, QSet<int> > fileFilterMap; + + double progress; + double oldProgress; + double contentStep; + double fileStep; + double indexStep; +}; + +QHelpGeneratorPrivate::QHelpGeneratorPrivate() +{ + query = 0; + namespaceId = -1; + virtualFolderId = -1; +} + +QHelpGeneratorPrivate::~QHelpGeneratorPrivate() +{ +} + + + +/*! + \internal + \class QHelpGenerator + \since 4.4 + \brief The QHelpGenerator class generates a new + Qt compressed help file (.qch). + + The help generator takes a help data structure as + input for generating a new Qt compressed help files. Since + the generation may takes some time, the generator emits + various signals to inform about its current state. +*/ + +/*! + \fn void QHelpGenerator::statusChanged(const QString &msg) + + This signal is emitted when the generation status changes. + The status is basically a specific task like inserting + files or building up the keyword index. The parameter + \a msg contains the detailed status description. +*/ + +/*! + \fn void QHelpGenerator::progressChanged(double progress) + + This signal is emitted when the progress changes. The + \a progress ranges from 0 to 100. +*/ + +/*! + \fn void QHelpGenerator::warning(const QString &msg) + + This signal is emitted when a non critical error occurs, + e.g. when a referenced file cannot be found. \a msg + contains the exact warning message. +*/ + +/*! + Constructs a new help generator with the give \a parent. +*/ +QHelpGenerator::QHelpGenerator(QObject *parent) + : QObject(parent) +{ + d = new QHelpGeneratorPrivate; +} + +/*! + Destructs the help generator. +*/ +QHelpGenerator::~QHelpGenerator() +{ + delete d; +} + +/*! + Takes the \a helpData and generates a new documentation + set from it. The Qt compressed help file is written to \a + outputFileName. Returns true on success, otherwise false. +*/ +bool QHelpGenerator::generate(QHelpDataInterface *helpData, + const QString &outputFileName) +{ + emit progressChanged(0); + d->error.clear(); + if (!helpData || helpData->namespaceName().isEmpty()) { + d->error = tr("Invalid help data!"); + return false; + } + + QString outFileName = outputFileName; + if (outFileName.isEmpty()) { + d->error = tr("No output file name specified!"); + return false; + } + + QFileInfo fi(outFileName); + if (fi.exists()) { + if (!fi.dir().remove(fi.fileName())) { + d->error = tr("The file %1 cannot be overwritten!").arg(outFileName); + return false; + } + } + + setupProgress(helpData); + + emit statusChanged(tr("Building up file structure...")); + bool openingOk = true; + { + QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), QLatin1String("builder")); + db.setDatabaseName(outFileName); + openingOk = db.open(); + if (openingOk) + d->query = new QSqlQuery(db); + } + + if (!openingOk) { + d->error = tr("Cannot open data base file %1!").arg(outFileName); + cleanupDB(); + return false; + } + + d->query->exec(QLatin1String("PRAGMA synchronous=OFF")); + d->query->exec(QLatin1String("PRAGMA cache_size=3000")); + + addProgress(1.0); + createTables(); + insertFileNotFoundFile(); + insertMetaData(helpData->metaData()); + + if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) { + d->error = tr("Cannot register namespace %1!").arg(helpData->namespaceName()); + cleanupDB(); + return false; + } + addProgress(1.0); + + emit statusChanged(tr("Insert custom filters...")); + foreach (const QHelpDataCustomFilter &f, helpData->customFilters()) { + if (!registerCustomFilter(f.name, f.filterAttributes, true)) { + cleanupDB(); + return false; + } + } + addProgress(1.0); + + int i = 1; + QList<QHelpDataFilterSection>::const_iterator it = helpData->filterSections().constBegin(); + while (it != helpData->filterSections().constEnd()) { + emit statusChanged(tr("Insert help data for filter section (%1 of %2)...") + .arg(i++).arg(helpData->filterSections().count())); + insertFilterAttributes((*it).filterAttributes()); + QByteArray ba; + QDataStream s(&ba, QIODevice::WriteOnly); + foreach (QHelpDataContentItem *itm, (*it).contents()) + writeTree(s, itm, 0); + if (!insertFiles((*it).files(), helpData->rootPath(), (*it).filterAttributes()) + || !insertContents(ba, (*it).filterAttributes()) + || !insertKeywords((*it).indices(), (*it).filterAttributes())) { + cleanupDB(); + return false; + } + ++it; + } + + cleanupDB(); + emit progressChanged(100); + emit statusChanged(tr("Documentation successfully generated.")); + return true; +} + +void QHelpGenerator::setupProgress(QHelpDataInterface *helpData) +{ + d->progress = 0; + d->oldProgress = 0; + + int numberOfFiles = 0; + int numberOfIndices = 0; + QList<QHelpDataFilterSection>::const_iterator it = helpData->filterSections().constBegin(); + while (it != helpData->filterSections().constEnd()) { + numberOfFiles += (*it).files().count(); + numberOfIndices += (*it).indices().count(); + ++it; + } + // init 2% + // filters 1% + // contents 10% + // files 60% + // indices 27% + d->contentStep = 10.0/(double)helpData->customFilters().count(); + d->fileStep = 60.0/(double)numberOfFiles; + d->indexStep = 27.0/(double)numberOfIndices; +} + +void QHelpGenerator::addProgress(double step) +{ + d->progress += step; + if ((d->progress-d->oldProgress) >= 1.0 && d->progress <= 100.0) { + d->oldProgress = d->progress; + emit progressChanged(ceil(d->progress)); + } +} + +void QHelpGenerator::cleanupDB() +{ + if (d->query) { + d->query->clear(); + delete d->query; + d->query = 0; + } + QSqlDatabase::removeDatabase(QLatin1String("builder")); +} + +void QHelpGenerator::writeTree(QDataStream &s, QHelpDataContentItem *item, int depth) +{ + QString fReference = QDir::cleanPath(item->reference()); + if (fReference.startsWith(QLatin1String("./"))) + fReference = fReference.mid(2); + + s << depth; + s << fReference; + s << item->title(); + foreach (QHelpDataContentItem *i, item->children()) + writeTree(s, i, depth+1); +} + +/*! + Returns the last error message. +*/ +QString QHelpGenerator::error() const +{ + return d->error; +} + +bool QHelpGenerator::createTables() +{ + if (!d->query) + return false; + + d->query->exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'" + "AND Name=\'NamespaceTable\'")); + d->query->next(); + if (d->query->value(0).toInt() > 0) { + d->error = tr("Some tables already exist!"); + return false; + } + + QStringList tables; + tables << QLatin1String("CREATE TABLE NamespaceTable (" + "Id INTEGER PRIMARY KEY," + "Name TEXT )") + << QLatin1String("CREATE TABLE FilterAttributeTable (" + "Id INTEGER PRIMARY KEY, " + "Name TEXT )") + << QLatin1String("CREATE TABLE FilterNameTable (" + "Id INTEGER PRIMARY KEY, " + "Name TEXT )") + << QLatin1String("CREATE TABLE FilterTable (" + "NameId INTEGER, " + "FilterAttributeId INTEGER )") + << QLatin1String("CREATE TABLE IndexTable (" + "Id INTEGER PRIMARY KEY, " + "Name TEXT, " + "Identifier TEXT, " + "NamespaceId INTEGER, " + "FileId INTEGER, " + "Anchor TEXT )") + << QLatin1String("CREATE TABLE IndexItemTable (" + "Id INTEGER, " + "IndexId INTEGER )") + << QLatin1String("CREATE TABLE IndexFilterTable (" + "FilterAttributeId INTEGER, " + "IndexId INTEGER )") + << QLatin1String("CREATE TABLE ContentsTable (" + "Id INTEGER PRIMARY KEY, " + "NamespaceId INTEGER, " + "Data BLOB )") + << QLatin1String("CREATE TABLE ContentsFilterTable (" + "FilterAttributeId INTEGER, " + "ContentsId INTEGER )") + << QLatin1String("CREATE TABLE FileAttributeSetTable (" + "Id INTEGER, " + "FilterAttributeId INTEGER )") + << QLatin1String("CREATE TABLE FileDataTable (" + "Id INTEGER PRIMARY KEY, " + "Data BLOB )") + << QLatin1String("CREATE TABLE FileFilterTable (" + "FilterAttributeId INTEGER, " + "FileId INTEGER )") + << QLatin1String("CREATE TABLE FileNameTable (" + "FolderId INTEGER, " + "Name TEXT, " + "FileId INTEGER, " + "Title TEXT )") + << QLatin1String("CREATE TABLE FolderTable(" + "Id INTEGER PRIMARY KEY, " + "Name Text, " + "NamespaceID INTEGER )") + << QLatin1String("CREATE TABLE MetaDataTable(" + "Name Text, " + "Value BLOB )"); + + foreach (const QString &q, tables) { + if (!d->query->exec(q)) { + d->error = tr("Cannot create tables!"); + return false; + } + } + + d->query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')")); + + d->query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES('CreationDate', ?)")); + d->query->bindValue(0, QDateTime::currentDateTime().toString(Qt::ISODate)); + d->query->exec(); + + return true; +} + +bool QHelpGenerator::insertFileNotFoundFile() +{ + if (!d->query) + return false; + + d->query->exec(QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'")); + if (d->query->next() && d->query->isValid()) + return true; + + d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)")); + d->query->bindValue(0, QByteArray()); + if (!d->query->exec()) + return false; + + int fileId = d->query->lastInsertId().toInt(); + d->query->prepare(QLatin1String("INSERT INTO FileNameTable (FolderId, Name, FileId, Title) " + " VALUES (0, '', ?, '')")); + d->query->bindValue(0, fileId); + if (fileId > -1 && d->query->exec()) { + d->fileMap.insert(QString(), fileId); + return true; + } + return false; +} + +bool QHelpGenerator::registerVirtualFolder(const QString &folderName, const QString &ns) +{ + if (!d->query || folderName.isEmpty() || ns.isEmpty()) + return false; + + d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); + d->query->bindValue(0, folderName); + d->query->exec(); + d->query->next(); + if (d->query->isValid() && d->query->value(0).toInt() > 0) + return true; + + d->namespaceId = -1; + d->query->prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?")); + d->query->bindValue(0, ns); + d->query->exec(); + while (d->query->next()) { + d->namespaceId = d->query->value(0).toInt(); + break; + } + + if (d->namespaceId < 0) { + d->query->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)")); + d->query->bindValue(0, ns); + if (d->query->exec()) + d->namespaceId = d->query->lastInsertId().toInt(); + } + + if (d->namespaceId > 0) { + d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); + d->query->bindValue(0, folderName); + d->query->exec(); + while (d->query->next()) + d->virtualFolderId = d->query->value(0).toInt(); + + if (d->virtualFolderId > 0) + return true; + + d->query->prepare(QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) " + "VALUES (?, ?)")); + d->query->bindValue(0, d->namespaceId); + d->query->bindValue(1, folderName); + if (d->query->exec()) { + d->virtualFolderId = d->query->lastInsertId().toInt(); + return d->virtualFolderId > 0; + } + } + d->error = tr("Cannot register virtual folder!"); + return false; +} + +bool QHelpGenerator::insertFiles(const QStringList &files, const QString &rootPath, + const QStringList &filterAttributes) +{ + if (!d->query) + return false; + + emit statusChanged(tr("Insert files...")); + QList<int> filterAtts; + foreach (const QString &filterAtt, filterAttributes) { + d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable " + "WHERE Name=?")); + d->query->bindValue(0, filterAtt); + d->query->exec(); + if (d->query->next()) + filterAtts.append(d->query->value(0).toInt()); + } + + int filterSetId = -1; + d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable")); + if (d->query->next()) + filterSetId = d->query->value(0).toInt(); + if (filterSetId < 0) + return false; + ++filterSetId; + foreach (const int &attId, filterAtts) { + d->query->prepare(QLatin1String("INSERT INTO FileAttributeSetTable " + "VALUES(?, ?)")); + d->query->bindValue(0, filterSetId); + d->query->bindValue(1, attId); + d->query->exec(); + } + + int tableFileId = 1; + d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); + if (d->query->next()) + tableFileId = d->query->value(0).toInt() + 1; + + QString title; + QString charSet; + FileNameTableData fileNameData; + QList<QByteArray> fileDataList; + QMap<int, QSet<int> > tmpFileFilterMap; + QList<FileNameTableData> fileNameDataList; + + int i = 0; + foreach (const QString &file, files) { + const QString fileName = QDir::cleanPath(file); + if (fileName.startsWith(QLatin1String("../"))) { + emit warning(tr("The referenced file %1 must be inside or within a " + "subdirectory of (%2). Skipping it.").arg(fileName).arg(rootPath)); + continue; + } + + QFile fi(rootPath + QDir::separator() + fileName); + if (!fi.exists()) { + emit warning(tr("The file %1 does not exist! Skipping it.") + .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName))); + continue; + } + + if (!fi.open(QIODevice::ReadOnly)) { + emit warning(tr("Cannot open file %1! Skipping it.") + .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName))); + continue; + } + + QByteArray data = fi.readAll(); + if (fileName.endsWith(QLatin1String(".html")) + || fileName.endsWith(QLatin1String(".htm"))) { + charSet = QHelpGlobal::codecFromData(data); + QTextStream stream(&data); + stream.setCodec(QTextCodec::codecForName(charSet.toLatin1().constData())); + title = QHelpGlobal::documentTitle(stream.readAll()); + } else { + title = fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1); + } + + int fileId = -1; + QMap<QString, int>::Iterator fileMapIt = d->fileMap.find(fileName); + if (fileMapIt == d->fileMap.end()) { + fileDataList.append(qCompress(data)); + + fileNameData.name = fileName; + fileNameData.fileId = tableFileId; + fileNameData.title = title; + fileNameDataList.append(fileNameData); + + d->fileMap.insert(fileName, tableFileId); + d->fileFilterMap.insert(tableFileId, filterAtts.toSet()); + tmpFileFilterMap.insert(tableFileId, filterAtts.toSet()); + + ++tableFileId; + } else { + fileId = fileMapIt.value(); + QSet<int> &fileFilterSet = d->fileFilterMap[fileId]; + QSet<int> &tmpFileFilterSet = tmpFileFilterMap[fileId]; + foreach (const int &filter, filterAtts) { + if (!fileFilterSet.contains(filter) + && !tmpFileFilterSet.contains(filter)) { + fileFilterSet.insert(filter); + tmpFileFilterSet.insert(filter); + } + } + } + } + + if (!tmpFileFilterMap.isEmpty()) { + d->query->exec(QLatin1String("BEGIN")); + QMap<int, QSet<int> >::const_iterator it = tmpFileFilterMap.constBegin(); + while (it != tmpFileFilterMap.constEnd()) { + QSet<int>::const_iterator si = it.value().constBegin(); + while (si != it.value().constEnd()) { + d->query->prepare(QLatin1String("INSERT INTO FileFilterTable " + "VALUES(?, ?)")); + d->query->bindValue(0, *si); + d->query->bindValue(1, it.key()); + d->query->exec(); + ++si; + } + ++it; + } + + QList<QByteArray>::const_iterator fileIt = fileDataList.constBegin(); + while (fileIt != fileDataList.constEnd()) { + d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES " + "(Null, ?)")); + d->query->bindValue(0, *fileIt); + d->query->exec(); + ++fileIt; + if (++i%20 == 0) + addProgress(d->fileStep*20.0); + } + + QList<FileNameTableData>::const_iterator fileNameIt = + fileNameDataList.constBegin(); + while (fileNameIt != fileNameDataList.constEnd()) { + d->query->prepare(QLatin1String("INSERT INTO FileNameTable " + "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)")); + d->query->bindValue(0, 1); + d->query->bindValue(1, (*fileNameIt).name); + d->query->bindValue(2, (*fileNameIt).fileId); + d->query->bindValue(3, (*fileNameIt).title); + d->query->exec(); + ++fileNameIt; + } + d->query->exec(QLatin1String("COMMIT")); + } + + d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); + if (d->query->next() + && d->query->value(0).toInt() == tableFileId-1) { + addProgress(d->fileStep*(i%20)); + return true; + } + return false; +} + +bool QHelpGenerator::registerCustomFilter(const QString &filterName, + const QStringList &filterAttribs, bool forceUpdate) +{ + if (!d->query) + return false; + + d->query->exec(QLatin1String("SELECT Id, Name FROM FilterAttributeTable")); + QStringList idsToInsert = filterAttribs; + QMap<QString, int> attributeMap; + while (d->query->next()) { + attributeMap.insert(d->query->value(1).toString(), + d->query->value(0).toInt()); + idsToInsert.removeAll(d->query->value(1).toString()); + } + + foreach (const QString &id, idsToInsert) { + d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); + d->query->bindValue(0, id); + d->query->exec(); + attributeMap.insert(id, d->query->lastInsertId().toInt()); + } + + int nameId = -1; + d->query->prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?")); + d->query->bindValue(0, filterName); + d->query->exec(); + while (d->query->next()) { + nameId = d->query->value(0).toInt(); + break; + } + + if (nameId < 0) { + d->query->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)")); + d->query->bindValue(0, filterName); + if (d->query->exec()) + nameId = d->query->lastInsertId().toInt(); + } else if (!forceUpdate) { + d->error = tr("The filter %1 is already registered!").arg(filterName); + return false; + } + + if (nameId < 0) { + d->error = tr("Cannot register filter %1!").arg(filterName); + return false; + } + + d->query->prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?")); + d->query->bindValue(0, nameId); + d->query->exec(); + + foreach (const QString &att, filterAttribs) { + d->query->prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)")); + d->query->bindValue(0, nameId); + d->query->bindValue(1, attributeMap[att]); + if (!d->query->exec()) + return false; + } + return true; +} + +bool QHelpGenerator::insertKeywords(const QList<QHelpDataIndexItem> &keywords, + const QStringList &filterAttributes) +{ + if (!d->query) + return false; + + emit statusChanged(tr("Insert indices...")); + int indexId = 1; + d->query->exec(QLatin1String("SELECT MAX(Id) FROM IndexTable")); + if (d->query->next()) + indexId = d->query->value(0).toInt() + 1; + + QList<int> filterAtts; + foreach (const QString &filterAtt, filterAttributes) { + d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable WHERE Name=?")); + d->query->bindValue(0, filterAtt); + d->query->exec(); + if (d->query->next()) + filterAtts.append(d->query->value(0).toInt()); + } + + int pos = -1; + QString fileName; + QString anchor; + QString fName; + int fileId = 1; + QList<int> indexFilterTable; + + int i = 0; + d->query->exec(QLatin1String("BEGIN")); + QSet<QString> indices; + foreach (const QHelpDataIndexItem &itm, keywords) { + // Identical ids make no sense and just confuse the Assistant user, + // so we ignore all repetitions. + if (indices.contains(itm.identifier)) + continue; + + // Still empty ids should be ignored, as otherwise we will include only + // the first keyword with an empty id. + if (!itm.identifier.isEmpty()) + indices.insert(itm.identifier); + + pos = itm.reference.indexOf(QLatin1Char('#')); + fileName = itm.reference.left(pos); + if (pos > -1) + anchor = itm.reference.mid(pos+1); + else + anchor.clear(); + + fName = QDir::cleanPath(fileName); + if (fName.startsWith(QLatin1String("./"))) + fName = fName.mid(2); + + QMap<QString, int>::ConstIterator it = d->fileMap.find(fName); + if (it != d->fileMap.end()) + fileId = it.value(); + else + fileId = 1; + + d->query->prepare(QLatin1String("INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) " + "VALUES(?, ?, ?, ?, ?)")); + d->query->bindValue(0, itm.name); + d->query->bindValue(1, itm.identifier); + d->query->bindValue(2, d->namespaceId); + d->query->bindValue(3, fileId); + d->query->bindValue(4, anchor); + d->query->exec(); + + indexFilterTable.append(indexId++); + if (++i%100 == 0) + addProgress(d->indexStep*100.0); + } + d->query->exec(QLatin1String("COMMIT")); + + d->query->exec(QLatin1String("BEGIN")); + foreach (int idx, indexFilterTable) { + foreach (int a, filterAtts) { + d->query->prepare(QLatin1String("INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) " + "VALUES(?, ?)")); + d->query->bindValue(0, a); + d->query->bindValue(1, idx); + d->query->exec(); + } + } + d->query->exec(QLatin1String("COMMIT")); + + d->query->exec(QLatin1String("SELECT COUNT(Id) FROM IndexTable")); + if (d->query->next() && d->query->value(0).toInt() >= indices.count()) + return true; + return false; +} + +bool QHelpGenerator::insertContents(const QByteArray &ba, + const QStringList &filterAttributes) +{ + if (!d->query) + return false; + + emit statusChanged(tr("Insert contents...")); + d->query->prepare(QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) " + "VALUES(?, ?)")); + d->query->bindValue(0, d->namespaceId); + d->query->bindValue(1, ba); + d->query->exec(); + int contentId = d->query->lastInsertId().toInt(); + if (contentId < 1) { + d->error = tr("Cannot insert contents!"); + return false; + } + + // associate the filter attributes + foreach (const QString &filterAtt, filterAttributes) { + d->query->prepare(QLatin1String("INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) " + "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?")); + d->query->bindValue(0, contentId); + d->query->bindValue(1, filterAtt); + d->query->exec(); + if (!d->query->isActive()) { + d->error = tr("Cannot register contents!"); + return false; + } + } + addProgress(d->contentStep); + return true; +} + +bool QHelpGenerator::insertFilterAttributes(const QStringList &attributes) +{ + if (!d->query) + return false; + + d->query->exec(QLatin1String("SELECT Name FROM FilterAttributeTable")); + QSet<QString> atts; + while (d->query->next()) + atts.insert(d->query->value(0).toString()); + + foreach (const QString &s, attributes) { + if (!atts.contains(s)) { + d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); + d->query->bindValue(0, s); + d->query->exec(); + } + } + return true; +} + +bool QHelpGenerator::insertMetaData(const QMap<QString, QVariant> &metaData) +{ + if (!d->query) + return false; + + QMap<QString, QVariant>::const_iterator it = metaData.constBegin(); + while (it != metaData.constEnd()) { + d->query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES(?, ?)")); + d->query->bindValue(0, it.key()); + d->query->bindValue(1, it.value()); + d->query->exec(); + ++it; + } + return true; +} + +bool QHelpGenerator::checkLinks(const QHelpDataInterface &helpData) +{ + /* + * Step 1: Gather the canoncal file paths of all files in the project. + * We use a set, because there will be a lot of look-ups. + */ + QSet<QString> files; + foreach (const QHelpDataFilterSection &filterSection, helpData.filterSections()) { + foreach (const QString &file, filterSection.files()) { + QFileInfo fileInfo(helpData.rootPath() + QDir::separator() + file); + const QString &canonicalFileName = fileInfo.canonicalFilePath(); + if (!fileInfo.exists()) + emit warning(tr("File '%1' does not exist.").arg(file)); + else + files.insert(canonicalFileName); + } + } + + /* + * Step 2: Check the hypertext and image references of all HTML files. + * Note that we don't parse the files, but simply grep for the + * respective HTML elements. Therefore. contents that are e.g. + * commented out can cause false warning. + */ + bool allLinksOk = true; + foreach (const QString &fileName, files) { + if (!fileName.endsWith(QLatin1String("html")) + && !fileName.endsWith(QLatin1String("htm"))) + continue; + QFile htmlFile(fileName); + if (!htmlFile.open(QIODevice::ReadOnly)) { + emit warning(tr("File '%1' cannot be opened.").arg(fileName)); + continue; + } + const QRegExp linkPattern(QLatin1String("<(?:a href|img src)=\"?([^#\">]+)[#\">]")); + QTextStream stream(&htmlFile); + const QString codec = QHelpGlobal::codecFromData(htmlFile.read(1000)); + stream.setCodec(QTextCodec::codecForName(codec.toLatin1().constData())); + const QString &content = stream.readAll(); + QStringList invalidLinks; + for (int pos = linkPattern.indexIn(content); pos != -1; + pos = linkPattern.indexIn(content, pos + 1)) { + const QString& linkedFileName = linkPattern.cap(1); + if (linkedFileName.contains(QLatin1String("://"))) + continue; + const QString curDir = QFileInfo(fileName).dir().path(); + const QString &canonicalLinkedFileName = + QFileInfo(curDir + QDir::separator() + linkedFileName).canonicalFilePath(); + if (!files.contains(canonicalLinkedFileName) + && !invalidLinks.contains(canonicalLinkedFileName)) { + emit warning(tr("File '%1' contains an invalid link to file '%2'"). + arg(fileName).arg(linkedFileName)); + allLinksOk = false; + invalidLinks.append(canonicalLinkedFileName); + } + } + } + + if (!allLinksOk) + d->error = tr("Invalid links in HTML files."); + return allLinksOk; +} + +QT_END_NAMESPACE + diff --git a/src/assistant/help/qhelpgenerator_p.h b/src/assistant/help/qhelpgenerator_p.h new file mode 100644 index 000000000..ad32c05de --- /dev/null +++ b/src/assistant/help/qhelpgenerator_p.h @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPGENERATOR_H +#define QHELPGENERATOR_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include "qhelp_global.h" +#include "qhelpdatainterface_p.h" + +#include <QtCore/QObject> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QHelpGeneratorPrivate; + +class QHELP_EXPORT QHelpGenerator : public QObject +{ + Q_OBJECT + +public: + QHelpGenerator(QObject *parent = 0); + ~QHelpGenerator(); + + bool generate(QHelpDataInterface *helpData, + const QString &outputFileName); + bool checkLinks(const QHelpDataInterface &helpData); + QString error() const; + +Q_SIGNALS: + void statusChanged(const QString &msg); + void progressChanged(double progress); + void warning(const QString &msg); + +private: + struct FileNameTableData + { + QString name; + int fileId; + QString title; + }; + + void writeTree(QDataStream &s, QHelpDataContentItem *item, int depth); + bool createTables(); + bool insertFileNotFoundFile(); + bool registerCustomFilter(const QString &filterName, + const QStringList &filterAttribs, bool forceUpdate = false); + bool registerVirtualFolder(const QString &folderName, const QString &ns); + bool insertFilterAttributes(const QStringList &attributes); + bool insertKeywords(const QList<QHelpDataIndexItem> &keywords, + const QStringList &filterAttributes); + bool insertFiles(const QStringList &files, const QString &rootPath, + const QStringList &filterAttributes); + bool insertContents(const QByteArray &ba, + const QStringList &filterAttributes); + bool insertMetaData(const QMap<QString, QVariant> &metaData); + void cleanupDB(); + void setupProgress(QHelpDataInterface *helpData); + void addProgress(double step); + + QHelpGeneratorPrivate *d; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif diff --git a/src/assistant/help/qhelpindexwidget.cpp b/src/assistant/help/qhelpindexwidget.cpp new file mode 100644 index 000000000..1b34ca0aa --- /dev/null +++ b/src/assistant/help/qhelpindexwidget.cpp @@ -0,0 +1,444 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpindexwidget.h" +#include "qhelpenginecore.h" +#include "qhelpengine_p.h" +#include "qhelpdbreader_p.h" + +#include <QtCore/QThread> +#include <QtCore/QMutex> +#include <QtWidgets/QListView> +#include <QtWidgets/QHeaderView> + +QT_BEGIN_NAMESPACE + +class QHelpIndexProvider : public QThread +{ +public: + QHelpIndexProvider(QHelpEnginePrivate *helpEngine); + ~QHelpIndexProvider(); + void collectIndices(const QString &customFilterName); + void stopCollecting(); + QStringList indices() const; + QList<QHelpDBReader*> activeReaders() const; + QSet<int> indexIds(QHelpDBReader *reader) const; + +private: + void run(); + + QHelpEnginePrivate *m_helpEngine; + QStringList m_indices; + QList<QHelpDBReader*> m_activeReaders; + QMap<QHelpDBReader*, QSet<int> > m_indexIds; + QStringList m_filterAttributes; + mutable QMutex m_mutex; + bool m_abort; +}; + +class QHelpIndexModelPrivate +{ +public: + QHelpIndexModelPrivate(QHelpEnginePrivate *hE) + { + helpEngine = hE; + indexProvider = new QHelpIndexProvider(helpEngine); + insertedRows = 0; + } + + QHelpEnginePrivate *helpEngine; + QHelpIndexProvider *indexProvider; + QStringList indices; + int insertedRows; + QString currentFilter; + QList<QHelpDBReader*> activeReaders; +}; + +static bool caseInsensitiveLessThan(const QString &as, const QString &bs) +{ + return QString::compare(as, bs, Qt::CaseInsensitive) < 0; +} + +QHelpIndexProvider::QHelpIndexProvider(QHelpEnginePrivate *helpEngine) + : QThread(helpEngine) +{ + m_helpEngine = helpEngine; + m_abort = false; +} + +QHelpIndexProvider::~QHelpIndexProvider() +{ + stopCollecting(); +} + +void QHelpIndexProvider::collectIndices(const QString &customFilterName) +{ + m_mutex.lock(); + m_filterAttributes = m_helpEngine->q->filterAttributes(customFilterName); + m_mutex.unlock(); + if (!isRunning()) { + start(LowPriority); + } else { + stopCollecting(); + start(LowPriority); + } +} + +void QHelpIndexProvider::stopCollecting() +{ + if (!isRunning()) + return; + m_mutex.lock(); + m_abort = true; + m_mutex.unlock(); + wait(); + m_abort = false; +} + +QStringList QHelpIndexProvider::indices() const +{ + QMutexLocker lck(&m_mutex); + return m_indices; +} + +QList<QHelpDBReader*> QHelpIndexProvider::activeReaders() const +{ + QMutexLocker lck(&m_mutex); + return m_activeReaders; +} + +QSet<int> QHelpIndexProvider::indexIds(QHelpDBReader *reader) const +{ + QMutexLocker lck(&m_mutex); + if (m_indexIds.contains(reader)) + return m_indexIds.value(reader); + return QSet<int>(); +} + +void QHelpIndexProvider::run() +{ + m_mutex.lock(); + QStringList atts = m_filterAttributes; + m_indices.clear(); + m_activeReaders.clear(); + QSet<QString> indicesSet; + m_mutex.unlock(); + + foreach (const QString &dbFileName, m_helpEngine->fileNameReaderMap.keys()) { + m_mutex.lock(); + if (m_abort) { + m_mutex.unlock(); + return; + } + m_mutex.unlock(); + QHelpDBReader reader(dbFileName, + QHelpGlobal::uniquifyConnectionName(dbFileName + + QLatin1String("FromIndexProvider"), + QThread::currentThread()), 0); + if (!reader.init()) + continue; + QStringList lst = reader.indicesForFilter(atts); + if (!lst.isEmpty()) { + m_mutex.lock(); + foreach (const QString &s, lst) + indicesSet.insert(s); + if (m_abort) { + m_mutex.unlock(); + return; + } + QHelpDBReader *orgReader = m_helpEngine->fileNameReaderMap.value(dbFileName); + m_indexIds.insert(orgReader, reader.indexIds(atts)); + m_activeReaders.append(orgReader); + m_mutex.unlock(); + } + } + m_mutex.lock(); + m_indices = indicesSet.values(); + qSort(m_indices.begin(), m_indices.end(), caseInsensitiveLessThan); + m_mutex.unlock(); +} + + + +/*! + \class QHelpIndexModel + \since 4.4 + \inmodule QtHelp + \brief The QHelpIndexModel class provides a model that + supplies index keywords to views. + + +*/ + +/*! + \fn void QHelpIndexModel::indexCreationStarted() + + This signal is emitted when the creation of a new index + has started. The current index is invalid from this + point on until the signal indexCreated() is emitted. + + \sa isCreatingIndex() +*/ + +/*! + \fn void QHelpIndexModel::indexCreated() + + This signal is emitted when the index has been created. +*/ + +QHelpIndexModel::QHelpIndexModel(QHelpEnginePrivate *helpEngine) + : QStringListModel(helpEngine) +{ + d = new QHelpIndexModelPrivate(helpEngine); + + connect(d->indexProvider, SIGNAL(finished()), this, SLOT(insertIndices())); + connect(helpEngine->q, SIGNAL(setupStarted()), this, SLOT(invalidateIndex())); +} + +QHelpIndexModel::~QHelpIndexModel() +{ + delete d; +} + +void QHelpIndexModel::invalidateIndex(bool onShutDown) +{ + if (onShutDown) + disconnect(this, SLOT(insertIndices())); + d->indexProvider->stopCollecting(); + d->indices.clear(); + if (!onShutDown) + filter(QString()); +} + +/*! + Creates a new index by querying the help system for + keywords for the specified \a customFilterName. +*/ +void QHelpIndexModel::createIndex(const QString &customFilterName) +{ + d->currentFilter = customFilterName; + d->indexProvider->collectIndices(customFilterName); + emit indexCreationStarted(); +} + +void QHelpIndexModel::insertIndices() +{ + d->indices = d->indexProvider->indices(); + d->activeReaders = d->indexProvider->activeReaders(); + QStringList attributes = d->helpEngine->q->filterAttributes(d->currentFilter); + if (attributes.count() > 1) { + foreach (QHelpDBReader *r, d->activeReaders) + r->createAttributesCache(attributes, d->indexProvider->indexIds(r)); + } + filter(QString()); + emit indexCreated(); +} + +/*! + Returns true if the index is currently built up, otherwise + false. +*/ +bool QHelpIndexModel::isCreatingIndex() const +{ + return d->indexProvider->isRunning(); +} + +/*! + Returns all hits found for the \a keyword. A hit consists of + the URL and the document title. +*/ +QMap<QString, QUrl> QHelpIndexModel::linksForKeyword(const QString &keyword) const +{ + QMap<QString, QUrl> linkMap; + QStringList filterAttributes = d->helpEngine->q->filterAttributes(d->currentFilter); + foreach (QHelpDBReader *reader, d->activeReaders) + reader->linksForKeyword(keyword, filterAttributes, linkMap); + return linkMap; +} + +/*! + Filters the indices and returns the model index of the best + matching keyword. In a first step, only the keywords containing + \a filter are kept in the model's index list. Analogously, if + \a wildcard is not empty, only the keywords matched are left + in the index list. In a second step, the best match is + determined and its index model returned. When specifying a + wildcard expression, the \a filter string is used to + search for the best match. +*/ +QModelIndex QHelpIndexModel::filter(const QString &filter, const QString &wildcard) +{ + if (filter.isEmpty()) { + setStringList(d->indices); + return index(-1, 0, QModelIndex()); + } + + QStringList lst; + int goodMatch = -1; + int perfectMatch = -1; + + if (!wildcard.isEmpty()) { + QRegExp regExp(wildcard, Qt::CaseInsensitive); + regExp.setPatternSyntax(QRegExp::Wildcard); + foreach (const QString &index, d->indices) { + if (index.contains(regExp)) { + lst.append(index); + if (perfectMatch == -1 && index.startsWith(filter, Qt::CaseInsensitive)) { + if (goodMatch == -1) + goodMatch = lst.count()-1; + if (filter.length() == index.length()){ + perfectMatch = lst.count()-1; + } + } else if (perfectMatch > -1 && index == filter) { + perfectMatch = lst.count()-1; + } + } + } + } else { + foreach (const QString &index, d->indices) { + if (index.contains(filter, Qt::CaseInsensitive)) { + lst.append(index); + if (perfectMatch == -1 && index.startsWith(filter, Qt::CaseInsensitive)) { + if (goodMatch == -1) + goodMatch = lst.count()-1; + if (filter.length() == index.length()){ + perfectMatch = lst.count()-1; + } + } else if (perfectMatch > -1 && index == filter) { + perfectMatch = lst.count()-1; + } + } + } + + } + + if (perfectMatch == -1) + perfectMatch = qMax(0, goodMatch); + + setStringList(lst); + return index(perfectMatch, 0, QModelIndex()); +} + + + +/*! + \class QHelpIndexWidget + \inmodule QtHelp + \since 4.4 + \brief The QHelpIndexWidget class provides a list view + displaying the QHelpIndexModel. +*/ + +/*! + \fn void QHelpIndexWidget::linkActivated(const QUrl &link, + const QString &keyword) + + This signal is emitted when an item is activated and its + associated \a link should be shown. To know where the link + belongs to, the \a keyword is given as a second paremeter. +*/ + +/*! + \fn void QHelpIndexWidget::linksActivated(const QMap<QString, QUrl> &links, + const QString &keyword) + + This signal is emitted when the item representing the \a keyword + is activated and the item has more than one link associated. + The \a links consist of the document title and their URL. +*/ + +QHelpIndexWidget::QHelpIndexWidget() + : QListView(0) +{ + setEditTriggers(QAbstractItemView::NoEditTriggers); + setUniformItemSizes(true); + connect(this, SIGNAL(activated(QModelIndex)), + this, SLOT(showLink(QModelIndex))); +} + +void QHelpIndexWidget::showLink(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + QHelpIndexModel *indexModel = qobject_cast<QHelpIndexModel*>(model()); + if (!indexModel) + return; + QVariant v = indexModel->data(index, Qt::DisplayRole); + QString name; + if (v.isValid()) + name = v.toString(); + + QMap<QString, QUrl> links = indexModel->linksForKeyword(name); + if (links.count() == 1) { + emit linkActivated(links.constBegin().value(), name); + } else if (links.count() > 1) { + emit linksActivated(links, name); + } +} + +/*! + Activates the current item which will result eventually in + the emitting of a linkActivated() or linksActivated() + signal. +*/ +void QHelpIndexWidget::activateCurrentItem() +{ + showLink(currentIndex()); +} + +/*! + Filters the indices according to \a filter or \a wildcard. + The item with the best match is set as current item. + + \sa QHelpIndexModel::filter() +*/ +void QHelpIndexWidget::filterIndices(const QString &filter, const QString &wildcard) +{ + QHelpIndexModel *indexModel = qobject_cast<QHelpIndexModel*>(model()); + if (!indexModel) + return; + QModelIndex idx = indexModel->filter(filter, wildcard); + if (idx.isValid()) + setCurrentIndex(idx); +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpindexwidget.h b/src/assistant/help/qhelpindexwidget.h new file mode 100644 index 000000000..41e66f647 --- /dev/null +++ b/src/assistant/help/qhelpindexwidget.h @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPINDEXWIDGET_H +#define QHELPINDEXWIDGET_H + +#include <QtHelp/qhelp_global.h> + +#include <QtCore/QUrl> +#include <QtWidgets/QStringListModel> +#include <QtWidgets/QListView> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +class QHelpEnginePrivate; +class QHelpIndexModelPrivate; + +class QHELP_EXPORT QHelpIndexModel : public QStringListModel +{ + Q_OBJECT + +public: + void createIndex(const QString &customFilterName); + QModelIndex filter(const QString &filter, + const QString &wildcard = QString()); + + QMap<QString, QUrl> linksForKeyword(const QString &keyword) const; + bool isCreatingIndex() const; + +Q_SIGNALS: + void indexCreationStarted(); + void indexCreated(); + +private Q_SLOTS: + void insertIndices(); + void invalidateIndex(bool onShutDown = false); + +private: + QHelpIndexModel(QHelpEnginePrivate *helpEngine); + ~QHelpIndexModel(); + + QHelpIndexModelPrivate *d; + friend class QHelpEnginePrivate; +}; + +class QHELP_EXPORT QHelpIndexWidget : public QListView +{ + Q_OBJECT + +Q_SIGNALS: + void linkActivated(const QUrl &link, const QString &keyword); + void linksActivated(const QMap<QString, QUrl> &links, + const QString &keyword); + +public Q_SLOTS: + void filterIndices(const QString &filter, + const QString &wildcard = QString()); + void activateCurrentItem(); + +private Q_SLOTS: + void showLink(const QModelIndex &index); + +private: + QHelpIndexWidget(); + friend class QHelpEngine; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif diff --git a/src/assistant/help/qhelpprojectdata.cpp b/src/assistant/help/qhelpprojectdata.cpp new file mode 100644 index 000000000..ce0ddd720 --- /dev/null +++ b/src/assistant/help/qhelpprojectdata.cpp @@ -0,0 +1,450 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpprojectdata_p.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/QDir> +#include <QtCore/QFileInfo> +#include <QtCore/QStack> +#include <QtCore/QMap> +#include <QtCore/QRegExp> +#include <QtCore/QUrl> +#include <QtCore/QVariant> +#include <QtCore/QXmlStreamReader> + +QT_BEGIN_NAMESPACE + +class QHelpProjectDataPrivate : public QXmlStreamReader +{ +public: + void readData(const QByteArray &contents); + + QString virtualFolder; + QString namespaceName; + QString rootPath; + + QStringList fileList; + QList<QHelpDataCustomFilter> customFilterList; + QList<QHelpDataFilterSection> filterSectionList; + QMap<QString, QVariant> metaData; + + QString errorMsg; + +private: + void readProject(); + void readCustomFilter(); + void readFilterSection(); + void readTOC(); + void readKeywords(); + void readFiles(); + void raiseUnknownTokenError(); + void addMatchingFiles(const QString &pattern); + bool hasValidSyntax(const QString &nameSpace, const QString &vFolder) const; + + QMap<QString, QStringList> dirEntriesCache; +}; + +void QHelpProjectDataPrivate::raiseUnknownTokenError() +{ + raiseError(QCoreApplication::translate("QHelpProject", "Unknown token.")); +} + +void QHelpProjectDataPrivate::readData(const QByteArray &contents) +{ + addData(contents); + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("QtHelpProject") + && attributes().value(QLatin1String("version")) == QLatin1String("1.0")) + readProject(); + else + raiseError(QCoreApplication::translate("QHelpProject", + "Unknown token. Expected \"QtHelpProject\"!")); + } + } + + if (hasError()) { + raiseError(QCoreApplication::translate("QHelpProject", + "Error in line %1: %2").arg(lineNumber()) + .arg(errorString())); + } +} + +void QHelpProjectDataPrivate::readProject() +{ + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("virtualFolder")) { + virtualFolder = readElementText(); + if (!hasValidSyntax(QLatin1String("test"), virtualFolder)) + raiseError(QCoreApplication::translate("QHelpProject", + "Virtual folder has invalid syntax.")); + } else if (name() == QLatin1String("namespace")) { + namespaceName = readElementText(); + if (!hasValidSyntax(namespaceName, QLatin1String("test"))) + raiseError(QCoreApplication::translate("QHelpProject", + "Namespace has invalid syntax.")); + } else if (name() == QLatin1String("customFilter")) { + readCustomFilter(); + } else if (name() == QLatin1String("filterSection")) { + readFilterSection(); + } else if (name() == QLatin1String("metaData")) { + QString n = attributes().value(QLatin1String("name")).toString(); + if (!metaData.contains(n)) + metaData[n] + = attributes().value(QLatin1String("value")).toString(); + else + metaData.insert(n, attributes(). + value(QLatin1String("value")).toString()); + } else { + raiseUnknownTokenError(); + } + } else if (isEndElement() && name() == QLatin1String("QtHelpProject")) { + if (namespaceName.isEmpty()) + raiseError(QCoreApplication::translate("QHelpProject", + "Missing namespace in QtHelpProject.")); + else if (virtualFolder.isEmpty()) + raiseError(QCoreApplication::translate("QHelpProject", + "Missing virtual folder in QtHelpProject")); + break; + } + } +} + +void QHelpProjectDataPrivate::readCustomFilter() +{ + QHelpDataCustomFilter filter; + filter.name = attributes().value(QLatin1String("name")).toString(); + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("filterAttribute")) + filter.filterAttributes.append(readElementText()); + else + raiseUnknownTokenError(); + } else if (isEndElement() && name() == QLatin1String("customFilter")) { + break; + } + } + customFilterList.append(filter); +} + +void QHelpProjectDataPrivate::readFilterSection() +{ + filterSectionList.append(QHelpDataFilterSection()); + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("filterAttribute")) + filterSectionList.last().addFilterAttribute(readElementText()); + else if (name() == QLatin1String("toc")) + readTOC(); + else if (name() == QLatin1String("keywords")) + readKeywords(); + else if (name() == QLatin1String("files")) + readFiles(); + else + raiseUnknownTokenError(); + } else if (isEndElement() && name() == QLatin1String("filterSection")) { + break; + } + } +} + +void QHelpProjectDataPrivate::readTOC() +{ + QStack<QHelpDataContentItem*> contentStack; + QHelpDataContentItem *itm = 0; + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("section")) { + QString title = attributes().value(QLatin1String("title")).toString(); + QString ref = attributes().value(QLatin1String("ref")).toString(); + if (contentStack.isEmpty()) { + itm = new QHelpDataContentItem(0, title, ref); + filterSectionList.last().addContent(itm); + } else { + itm = new QHelpDataContentItem(contentStack.top(), title, ref); + } + contentStack.push(itm); + } else { + raiseUnknownTokenError(); + } + } else if (isEndElement()) { + if (name() == QLatin1String("section")) { + contentStack.pop(); + continue; + } else if (name() == QLatin1String("toc") && contentStack.isEmpty()) { + break; + } else { + raiseUnknownTokenError(); + } + } + } +} + +void QHelpProjectDataPrivate::readKeywords() +{ + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("keyword")) { + if (attributes().value(QLatin1String("ref")).toString().isEmpty() + || (attributes().value(QLatin1String("name")).toString().isEmpty() + && attributes().value(QLatin1String("id")).toString().isEmpty())) + raiseError(QCoreApplication::translate("QHelpProject", + "Missing attribute in keyword at line %1.") + .arg(lineNumber())); + filterSectionList.last() + .addIndex(QHelpDataIndexItem(attributes(). + value(QLatin1String("name")).toString(), + attributes().value(QLatin1String("id")).toString(), + attributes().value(QLatin1String("ref")).toString())); + } else { + raiseUnknownTokenError(); + } + } else if (isEndElement()) { + if (name() == QLatin1String("keyword")) + continue; + else if (name() == QLatin1String("keywords")) + break; + else + raiseUnknownTokenError(); + } + } +} + +void QHelpProjectDataPrivate::readFiles() +{ + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("file")) + addMatchingFiles(readElementText()); + else + raiseUnknownTokenError(); + } else if (isEndElement()) { + if (name() == QLatin1String("file")) + continue; + else if (name() == QLatin1String("files")) + break; + else + raiseUnknownTokenError(); + } + } +} + +// Expand file pattern and add matches into list. If the pattern does not match +// any files, insert the pattern itself so the QHelpGenerator will emit a +// meaningful warning later. +void QHelpProjectDataPrivate::addMatchingFiles(const QString &pattern) +{ + // The pattern matching is expensive, so we skip it if no + // wildcard symbols occur in the string. + if (!pattern.contains('?') && !pattern.contains('*') + && !pattern.contains('[') && !pattern.contains(']')) { + filterSectionList.last().addFile(pattern); + return; + } + + QFileInfo fileInfo(rootPath + '/' + pattern); + const QDir &dir = fileInfo.dir(); + const QString &path = dir.canonicalPath(); + + // QDir::entryList() is expensive, so we cache the results. + QMap<QString, QStringList>::ConstIterator it = dirEntriesCache.find(path); + const QStringList &entries = it != dirEntriesCache.constEnd() ? + it.value() : dir.entryList(QDir::Files); + if (it == dirEntriesCache.constEnd()) + dirEntriesCache.insert(path, entries); + + bool matchFound = false; +#ifdef Q_OS_WIN + Qt::CaseSensitivity cs = Qt::CaseInsensitive; +#else + Qt::CaseSensitivity cs = Qt::CaseSensitive; +#endif + QRegExp regExp(fileInfo.fileName(), cs, QRegExp::Wildcard); + foreach (const QString &file, entries) { + if (regExp.exactMatch(file)) { + matchFound = true; + filterSectionList.last(). + addFile(QFileInfo(pattern).dir().path() + '/' + file); + } + } + if (!matchFound) + filterSectionList.last().addFile(pattern); +} + +bool QHelpProjectDataPrivate::hasValidSyntax(const QString &nameSpace, + const QString &vFolder) const +{ + const QLatin1Char slash('/'); + if (nameSpace.contains(slash) || vFolder.contains(slash)) + return false; + QUrl url; + const QLatin1String scheme("qthelp"); + url.setScheme(scheme); + const QString canonicalNamespace = nameSpace.toLower(); + url.setHost(canonicalNamespace); + url.setPath(vFolder); + + const QString expectedUrl(scheme + QLatin1String("://") + + canonicalNamespace + slash + vFolder); + return url.isValid() && url.toString() == expectedUrl; +} + +/*! + \internal + \class QHelpProjectData + \since 4.4 + \brief The QHelpProjectData class stores all information found + in a Qt help project file. + + The structure is filled with data by calling readData(). The + specified file has to have the Qt help project file format in + order to be read successfully. Possible reading errors can be + retrieved by calling errorMessage(). +*/ + +/*! + Constructs a Qt help project data structure. +*/ +QHelpProjectData::QHelpProjectData() +{ + d = new QHelpProjectDataPrivate; +} + +/*! + Destroys the help project data. +*/ +QHelpProjectData::~QHelpProjectData() +{ + delete d; +} + +/*! + Reads the file \a fileName and stores the help data. The file has to + have the Qt help project file format. Returns true if the file + was successfully read, otherwise false. + + \sa errorMessage() +*/ +bool QHelpProjectData::readData(const QString &fileName) +{ + d->rootPath = QFileInfo(fileName).absolutePath(); + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + d->errorMsg = QCoreApplication::translate("QHelpProject", + "The input file %1 could not be opened!").arg(fileName); + return false; + } + + d->readData(file.readAll()); + return !d->hasError(); +} + +/*! + Returns an error message if the reading of the Qt help project + file failed. Otherwise, an empty QString is returned. + + \sa readData() +*/ +QString QHelpProjectData::errorMessage() const +{ + if (d->hasError()) + return d->errorString(); + return d->errorMsg; +} + +/*! + \internal +*/ +QString QHelpProjectData::namespaceName() const +{ + return d->namespaceName; +} + +/*! + \internal +*/ +QString QHelpProjectData::virtualFolder() const +{ + return d->virtualFolder; +} + +/*! + \internal +*/ +QList<QHelpDataCustomFilter> QHelpProjectData::customFilters() const +{ + return d->customFilterList; +} + +/*! + \internal +*/ +QList<QHelpDataFilterSection> QHelpProjectData::filterSections() const +{ + return d->filterSectionList; +} + +/*! + \internal +*/ +QMap<QString, QVariant> QHelpProjectData::metaData() const +{ + return d->metaData; +} + +/*! + \internal +*/ +QString QHelpProjectData::rootPath() const +{ + return d->rootPath; +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpprojectdata_p.h b/src/assistant/help/qhelpprojectdata_p.h new file mode 100644 index 000000000..64ae32505 --- /dev/null +++ b/src/assistant/help/qhelpprojectdata_p.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPPROJECTDATA_H +#define QHELPPROJECTDATA_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include "qhelp_global.h" +#include "qhelpdatainterface_p.h" + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QHelpProjectDataPrivate; + +class QHELP_EXPORT QHelpProjectData : public QHelpDataInterface +{ +public: + QHelpProjectData(); + ~QHelpProjectData(); + + bool readData(const QString &fileName); + QString errorMessage() const; + + QString namespaceName() const; + QString virtualFolder() const; + QList<QHelpDataCustomFilter> customFilters() const; + QList<QHelpDataFilterSection> filterSections() const; + QMap<QString, QVariant> metaData() const; + QString rootPath() const; + +private: + QHelpProjectDataPrivate *d; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif diff --git a/src/assistant/help/qhelpsearchengine.cpp b/src/assistant/help/qhelpsearchengine.cpp new file mode 100644 index 000000000..f8f8acf44 --- /dev/null +++ b/src/assistant/help/qhelpsearchengine.cpp @@ -0,0 +1,450 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpenginecore.h" +#include "qhelpsearchengine.h" +#include "qhelpsearchquerywidget.h" +#include "qhelpsearchresultwidget.h" + +#include "qhelpsearchindexreader_p.h" +#if defined(QT_CLUCENE_SUPPORT) +# include "qhelpsearchindexreader_clucene_p.h" +# include "qhelpsearchindexwriter_clucene_p.h" +#else +# include "qhelpsearchindexreader_default_p.h" +# include "qhelpsearchindexwriter_default_p.h" +#endif + +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QVariant> +#include <QtCore/QThread> +#include <QtCore/QPointer> + +QT_BEGIN_NAMESPACE + +#if defined(QT_CLUCENE_SUPPORT) + using namespace fulltextsearch::clucene; +#else + using namespace fulltextsearch::std; +#endif + +class QHelpSearchEnginePrivate : public QObject +{ + Q_OBJECT + +signals: + void indexingStarted(); + void indexingFinished(); + + void searchingStarted(); + void searchingFinished(int hits); + +private: + QHelpSearchEnginePrivate(QHelpEngineCore *helpEngine) + : queryWidget(0) + , resultWidget(0) + , helpEngine(helpEngine) + { + indexReader = 0; + indexWriter = 0; + } + + ~QHelpSearchEnginePrivate() + { + delete indexReader; + delete indexWriter; + } + + int hitCount() const + { + int count = 0; + if (indexReader) + count = indexReader->hitCount(); + + return count; + } + + QList<QHelpSearchEngine::SearchHit> hits(int start, int end) const + { + return indexReader ? + indexReader->hits(start, end) : + QList<QHelpSearchEngine::SearchHit>(); + } + + void updateIndex(bool reindex = false) + { + if (helpEngine.isNull()) + return; + + if (!QFile::exists(QFileInfo(helpEngine->collectionFile()).path())) + return; + + if (!indexWriter) { + indexWriter = new QHelpSearchIndexWriter(); + + connect(indexWriter, SIGNAL(indexingStarted()), this, SIGNAL(indexingStarted())); + connect(indexWriter, SIGNAL(indexingFinished()), this, SIGNAL(indexingFinished())); + connect(indexWriter, SIGNAL(indexingFinished()), this, SLOT(optimizeIndex())); + } + + indexWriter->cancelIndexing(); + indexWriter->updateIndex(helpEngine->collectionFile(), + indexFilesFolder(), reindex); + } + + void cancelIndexing() + { + if (indexWriter) + indexWriter->cancelIndexing(); + } + + void search(const QList<QHelpSearchQuery> &queryList) + { + if (helpEngine.isNull()) + return; + + if (!QFile::exists(QFileInfo(helpEngine->collectionFile()).path())) + return; + + if (!indexReader) { +#if defined(QT_CLUCENE_SUPPORT) + indexReader = new QHelpSearchIndexReaderClucene(); +#else + indexReader = new QHelpSearchIndexReaderDefault(); +#endif // QT_CLUCENE_SUPPORT + connect(indexReader, SIGNAL(searchingStarted()), this, SIGNAL(searchingStarted())); + connect(indexReader, SIGNAL(searchingFinished(int)), this, SIGNAL(searchingFinished(int))); + } + + m_queryList = queryList; + indexReader->cancelSearching(); + indexReader->search(helpEngine->collectionFile(), indexFilesFolder(), queryList); + } + + void cancelSearching() + { + if (indexReader) + indexReader->cancelSearching(); + } + + QString indexFilesFolder() const + { + QString indexFilesFolder = QLatin1String(".fulltextsearch"); + if (helpEngine && !helpEngine->collectionFile().isEmpty()) { + QFileInfo fi(helpEngine->collectionFile()); + indexFilesFolder = fi.absolutePath() + QDir::separator() + + QLatin1Char('.') + + fi.fileName().left(fi.fileName().lastIndexOf(QLatin1String(".qhc"))); + } + return indexFilesFolder; + } + +private slots: + void optimizeIndex() + { +#if defined(QT_CLUCENE_SUPPORT) + if (indexWriter && !helpEngine.isNull()) { + indexWriter->optimizeIndex(); + } +#endif + } + +private: + friend class QHelpSearchEngine; + + QHelpSearchQueryWidget *queryWidget; + QHelpSearchResultWidget *resultWidget; + + fulltextsearch::QHelpSearchIndexReader *indexReader; + QHelpSearchIndexWriter *indexWriter; + + QPointer<QHelpEngineCore> helpEngine; + + QList<QHelpSearchQuery> m_queryList; +}; + +#include "qhelpsearchengine.moc" + + +/*! + \class QHelpSearchQuery + \since 4.4 + \inmodule QtHelp + \brief The QHelpSearchQuery class contains the field name and the associated + search term + + The QHelpSearchQuery class contains the field name and the associated search + term. Depending on the field the search term might get split up into separate + terms to be parsed differently by the search engine. + + \sa QHelpSearchQueryWidget +*/ + +/*! + \fn QHelpSearchQuery::QHelpSearchQuery() + + Constructs a new empty QHelpSearchQuery. +*/ + +/*! + \fn QHelpSearchQuery::QHelpSearchQuery(FieldName field, const QStringList &wordList) + + Constructs a new QHelpSearchQuery and initializes it with the given \a field and \a wordList. +*/ + +/*! + \enum QHelpSearchQuery::FieldName + This enum type specifies the field names that are handled by the search engine. + + \value DEFAULT the default field provided by the search widget, several terms should be + split and stored in the word list except search terms enclosed in quotes. + \value FUZZY a field only provided in use with clucene. Terms should be split in separate + words and passed to the search engine. + \value WITHOUT a field only provided in use with clucene. Terms should be split in separate + words and passed to the search engine. + \value PHRASE a field only provided in use with clucene. Terms should not be split in separate + words. + \value ALL a field only provided in use with clucene. Terms should be split in separate + words and passed to the search engine + \value ATLEAST a field only provided in use with clucene. Terms should be split in separate + words and passed to the search engine +*/ + +/*! + \class QHelpSearchEngine + \since 4.4 + \inmodule QtHelp + \brief The QHelpSearchEngine class provides access to widgets reusable + to integrate fulltext search as well as to index and search documentation. + + Before the search engine can be used, one has to instantiate at least a + QHelpEngineCore object that needs to be passed to the search engines constructor. + This is required as the search engine needs to be connected to the help + engines setupFinished() signal to know when it can start to index documentation. + + After starting the indexing process the signal indexingStarted() is emitted and + on the end of the indexing process the indexingFinished() is emitted. To stop + the indexing one can call cancelIndexing(). + + While the indexing process has finished, the search engine can now be used to search + thru its index for a given term. To do this one may use the possibility of creating the + QHelpSearchQuery list by self or reuse the QHelpSearchQueryWidget which has the inbuild + functionality to set up a proper search queries list that get's passed to the search engines + search() function. + + After the list of querys has been passed to the search engine, the signal searchingStarted() + is emitted and after the search has finished the searchingFinished() signal is emitted. The + search process can be stopped by calling cancelSearching(). + + If the search succeeds, the searchingFinished() will be called with the search hits count, + which can be reused to fetch the search hits from the search engine. Calling the hits() + function with the range of hits you would like to get will return a list of the requested + SearchHits. They basically constist at the moment of a pair of strings where the values + of that pair are the documentation file path and the page title. + + To display the given hits use the QHelpSearchResultWidget or build up your own one if you need + more advanced functionality. Note that the QHelpSearchResultWidget can not be instantiated + directly, you must retrieve the widget from the search engine in use as all connections will be + established for you by the widget itself. +*/ + +/*! + \fn void QHelpSearchEngine::indexingStarted() + + This signal is emitted when indexing process is started. +*/ + +/*! + \fn void QHelpSearchEngine::indexingFinished() + + This signal is emitted when the indexing process is complete. +*/ + +/*! + \fn void QHelpSearchEngine::searchingStarted() + + This signal is emitted when the search process is started. +*/ + +/*! + \fn void QHelpSearchEngine::searchingFinished(int hits) + + This signal is emitted when the search process is complete. + The hit count is stored in \a hits. +*/ + +/*! + Constructs a new search engine with the given \a parent. The search engine + uses the given \a helpEngine to access the documentation that needs to be indexed. + The QHelpEngine's setupFinished() signal is automatically connected to the + QHelpSearchEngine's indexing function, so that new documentation will be indexed + after the signal is emitted. +*/ +QHelpSearchEngine::QHelpSearchEngine(QHelpEngineCore *helpEngine, QObject *parent) + : QObject(parent) +{ + d = new QHelpSearchEnginePrivate(helpEngine); + + connect(helpEngine, SIGNAL(setupFinished()), this, SLOT(indexDocumentation())); + + connect(d, SIGNAL(indexingStarted()), this, SIGNAL(indexingStarted())); + connect(d, SIGNAL(indexingFinished()), this, SIGNAL(indexingFinished())); + connect(d, SIGNAL(searchingStarted()), this, SIGNAL(searchingStarted())); + connect(d, SIGNAL(searchingFinished(int)), this, SIGNAL(searchingFinished(int))); +} + +/*! + Destructs the search engine. +*/ +QHelpSearchEngine::~QHelpSearchEngine() +{ + delete d; +} + +/*! + Returns a widget to use as input widget. Depending on your search engine + configuration you will get a different widget with more or less subwidgets. +*/ +QHelpSearchQueryWidget* QHelpSearchEngine::queryWidget() +{ + if (!d->queryWidget) + d->queryWidget = new QHelpSearchQueryWidget(); + + return d->queryWidget; +} + +/*! + Returns a widget that can hold and display the search results. +*/ +QHelpSearchResultWidget* QHelpSearchEngine::resultWidget() +{ + if (!d->resultWidget) + d->resultWidget = new QHelpSearchResultWidget(this); + + return d->resultWidget; +} + +/*! + \obsolete + Returns the amount of hits the search engine found. + \sa hitCount() +*/ +int QHelpSearchEngine::hitsCount() const +{ + return d->hitCount(); +} + +/*! + \since 4.6 + Returns the amount of hits the search engine found. +*/ +int QHelpSearchEngine::hitCount() const +{ + return d->hitCount(); +} + +/*! + \typedef QHelpSearchEngine::SearchHit + + Typedef for QPair<QString, QString>. + The values of that pair are the documentation file path and the page title. + + \sa hits() +*/ + +/*! + Returns a list of search hits within the range of \a start \a end. +*/ +QList<QHelpSearchEngine::SearchHit> QHelpSearchEngine::hits(int start, int end) const +{ + return d->hits(start, end); +} + +/*! + Returns the list of queries last searched for. + \since 4.5 +*/ +QList<QHelpSearchQuery> QHelpSearchEngine::query() const +{ + return d->m_queryList; +} + +/*! + Forces the search engine to reindex all documentation files. +*/ +void QHelpSearchEngine::reindexDocumentation() +{ + d->updateIndex(true); +} + +/*! + Stops the indexing process. +*/ +void QHelpSearchEngine::cancelIndexing() +{ + d->cancelIndexing(); +} + +/*! + Stops the search process. +*/ +void QHelpSearchEngine::cancelSearching() +{ + d->cancelSearching(); +} + +/*! + Starts the search process using the given list of queries \a queryList + build by the search field name and the values to search for. +*/ +void QHelpSearchEngine::search(const QList<QHelpSearchQuery> &queryList) +{ + d->search(queryList); +} + +void QHelpSearchEngine::indexDocumentation() +{ + d->updateIndex(); +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchengine.h b/src/assistant/help/qhelpsearchengine.h new file mode 100644 index 000000000..baceb7057 --- /dev/null +++ b/src/assistant/help/qhelpsearchengine.h @@ -0,0 +1,125 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHENGINE_H +#define QHELPSEARCHENGINE_H + +#include <QtHelp/qhelp_global.h> + +#include <QtCore/QMap> +#include <QtCore/QUrl> +#include <QtCore/QObject> +#include <QtCore/QString> +#include <QtCore/QStringList> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +class QHelpEngineCore; +class QHelpSearchQueryWidget; +class QHelpSearchResultWidget; +class QHelpSearchEnginePrivate; + +class QHELP_EXPORT QHelpSearchQuery +{ +public: + enum FieldName { DEFAULT = 0, FUZZY, WITHOUT, PHRASE, ALL, ATLEAST }; + + QHelpSearchQuery() + : fieldName(DEFAULT) { wordList.clear(); } + QHelpSearchQuery(FieldName field, const QStringList &wordList) + : fieldName(field), wordList(wordList) {} + + FieldName fieldName; + QStringList wordList; +}; + +class QHELP_EXPORT QHelpSearchEngine : public QObject +{ + Q_OBJECT + +public: + explicit QHelpSearchEngine(QHelpEngineCore *helpEngine, + QObject *parent = 0); + ~QHelpSearchEngine(); + + QHelpSearchQueryWidget* queryWidget(); + QHelpSearchResultWidget* resultWidget(); + +#ifdef QT_DEPRECATED + QT_DEPRECATED int hitsCount() const; +#endif + int hitCount() const; + + typedef QPair<QString, QString> SearchHit; + QList<SearchHit> hits(int start, int end) const; + + QList<QHelpSearchQuery> query() const; + +public Q_SLOTS: + void reindexDocumentation(); + void cancelIndexing(); + + void search(const QList<QHelpSearchQuery> &queryList); + void cancelSearching(); + +Q_SIGNALS: + void indexingStarted(); + void indexingFinished(); + + void searchingStarted(); + void searchingFinished(int hits); + +private Q_SLOTS: + void indexDocumentation(); + +private: + QHelpSearchEnginePrivate *d; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QHELPSEARCHENGINE_H diff --git a/src/assistant/help/qhelpsearchindex_default.cpp b/src/assistant/help/qhelpsearchindex_default.cpp new file mode 100644 index 000000000..9974f618d --- /dev/null +++ b/src/assistant/help/qhelpsearchindex_default.cpp @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpsearchindex_default_p.h" + +QT_BEGIN_NAMESPACE + +QDataStream &operator>>(QDataStream &s, Document &l) +{ + s >> l.docNumber; + s >> l.frequency; + return s; +} + +QDataStream &operator<<(QDataStream &s, const Document &l) +{ + s << qint16(l.docNumber); + s << qint16(l.frequency); + return s; +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchindex_default_p.h b/src/assistant/help/qhelpsearchindex_default_p.h new file mode 100644 index 000000000..868609960 --- /dev/null +++ b/src/assistant/help/qhelpsearchindex_default_p.h @@ -0,0 +1,149 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHINDEXDEFAULT_H +#define QHELPSEARCHINDEXDEFAULT_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QString> +#include <QtCore/QVector> +#include <QtCore/QDataStream> + +QT_BEGIN_NAMESPACE + +namespace QtHelpInternal { + +struct Document { + Document(qint16 d, qint16 f) + : docNumber(d), frequency(f) {} + + Document() + : docNumber(-1), frequency(0) {} + + bool operator==(const Document &doc) const { + return docNumber == doc.docNumber; + } + bool operator<(const Document &doc) const { + return frequency > doc.frequency; + } + bool operator<=(const Document &doc) const { + return frequency >= doc.frequency; + } + bool operator>(const Document &doc) const { + return frequency < doc.frequency; + } + + qint16 docNumber; + qint16 frequency; +}; + +struct DocumentInfo : public Document { + DocumentInfo() + : Document(-1, 0), documentTitle(QString()), documentUrl(QString()) {} + + DocumentInfo(qint16 d, qint16 f, const QString &title, const QString &url) + : Document(d, f), documentTitle(title), documentUrl(url) {} + + DocumentInfo(const Document &document, const QString &title, const QString &url) + : Document(document.docNumber, document.frequency), documentTitle(title), documentUrl(url) {} + + QString documentTitle; + QString documentUrl; +}; + +struct Entry { + Entry(qint16 d) { documents.append(Document(d, 1)); } + Entry(QVector<Document> l) : documents(l) {} + + QVector<Document> documents; +}; + +struct PosEntry { + PosEntry(int p) { positions.append(p); } + QList<uint> positions; +}; + +struct Term { + Term() : frequency(-1) {} + Term(const QString &t, int f, QVector<Document> l) : term(t), frequency(f), documents(l) {} + QString term; + int frequency; + QVector<Document>documents; + bool operator<(const Term &i2) const { return frequency < i2.frequency; } +}; + +struct TermInfo { + TermInfo() : frequency(-1) {} + TermInfo(const QString &t, int f, QVector<DocumentInfo> l) + : term(t), frequency(f), documents(l) {} + + bool operator<(const TermInfo &i2) const { return frequency < i2.frequency; } + + QString term; + int frequency; + QVector<DocumentInfo>documents; +}; + +} // namespace QtHelpInternal + +using QtHelpInternal::Document; +using QtHelpInternal::DocumentInfo; +using QtHelpInternal::Entry; +using QtHelpInternal::PosEntry; +using QtHelpInternal::Term; +using QtHelpInternal::TermInfo; + +QDataStream &operator>>(QDataStream &s, Document &l); +QDataStream &operator<<(QDataStream &s, const Document &l); + +QT_END_NAMESPACE + +#endif // QHELPSEARCHINDEXDEFAULT_H diff --git a/src/assistant/help/qhelpsearchindexreader.cpp b/src/assistant/help/qhelpsearchindexreader.cpp new file mode 100644 index 000000000..1ee356d2f --- /dev/null +++ b/src/assistant/help/qhelpsearchindexreader.cpp @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpsearchindexreader_p.h" + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { + +QHelpSearchIndexReader::QHelpSearchIndexReader() + : QThread() + , m_cancel(false) +{ + // nothing todo +} + +QHelpSearchIndexReader::~QHelpSearchIndexReader() +{ + mutex.lock(); + this->m_cancel = true; + mutex.unlock(); + + wait(); +} + +void QHelpSearchIndexReader::cancelSearching() +{ + mutex.lock(); + this->m_cancel = true; + mutex.unlock(); +} + +void QHelpSearchIndexReader::search(const QString &collectionFile, const QString &indexFilesFolder, + const QList<QHelpSearchQuery> &queryList) +{ + wait(); + + this->hitList.clear(); + this->m_cancel = false; + this->m_query = queryList; + this->m_collectionFile = collectionFile; + this->m_indexFilesFolder = indexFilesFolder; + + start(QThread::NormalPriority); +} + +int QHelpSearchIndexReader::hitCount() const +{ + QMutexLocker lock(&mutex); + return hitList.count(); +} + +QList<QHelpSearchEngine::SearchHit> QHelpSearchIndexReader::hits(int start, + int end) const +{ + QList<QHelpSearchEngine::SearchHit> hits; + QMutexLocker lock(&mutex); + for (int i = start; i < end && i < hitList.count(); ++i) + hits.append(hitList.at(i)); + return hits; +} + + +} // namespace fulltextsearch + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchindexreader_clucene.cpp b/src/assistant/help/qhelpsearchindexreader_clucene.cpp new file mode 100644 index 000000000..b67898f8a --- /dev/null +++ b/src/assistant/help/qhelpsearchindexreader_clucene.cpp @@ -0,0 +1,481 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "private/qindexreader_p.h" +#include "private/qqueryparser_p.h" +#include "private/qsearchable_p.h" +#include "qclucenefieldnames_p.h" +#include "qhelpenginecore.h" + +#include "qhelpsearchindexreader_clucene_p.h" + +#include <QtCore/QDir> +#include <QtCore/QSet> +#include <QtCore/QString> +#include <QtCore/QFileInfo> +#include <QtCore/QSharedPointer> +#include <QtCore/QStringList> +#include <QtCore/QTextStream> +#include <QtCore/QMutexLocker> + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace clucene { + +QHelpSearchIndexReaderClucene::QHelpSearchIndexReaderClucene() + : QHelpSearchIndexReader() +{ + // nothing todo +} + +QHelpSearchIndexReaderClucene::~QHelpSearchIndexReaderClucene() +{ +} + + +void QHelpSearchIndexReaderClucene::run() +{ + mutex.lock(); + + if (m_cancel) { + mutex.unlock(); + return; + } + + const QString collectionFile(this->m_collectionFile); + const QList<QHelpSearchQuery> &queryList = this->m_query; + const QString indexPath(m_indexFilesFolder); + + mutex.unlock(); + + QHelpEngineCore engine(collectionFile, 0); + if (!engine.setupData()) + return; + + QFileInfo fInfo(indexPath); + if (fInfo.exists() && !fInfo.isWritable()) { + qWarning("Full Text Search, could not read index (missing permissions)."); + return; + } + + if(QCLuceneIndexReader::indexExists(indexPath)) { + mutex.lock(); + if (m_cancel) { + mutex.unlock(); + return; + } + mutex.unlock(); + + emit searchingStarted(); + +#if !defined(QT_NO_EXCEPTIONS) + try { +#endif + QCLuceneBooleanQuery booleanQueryTitle; + QCLuceneBooleanQuery booleanQueryContent; + QCLuceneStandardAnalyzer analyzer; + const QStringList& attribList = + engine.filterAttributes(engine.currentFilter()); + bool titleQueryIsValid = buildQuery(queryList, TitleTokenizedField, + attribList, booleanQueryTitle, analyzer); + bool contentQueryIsValid = buildQuery(queryList, ContentField, + attribList, booleanQueryContent, analyzer); + if (!titleQueryIsValid && !contentQueryIsValid) { + emit searchingFinished(0); + return; + } + + QCLuceneIndexSearcher indexSearcher(indexPath); + + // QCLuceneHits object must be allocated on the heap, because + // there is no default constructor. + QSharedPointer<QCLuceneHits> titleHits; + QSharedPointer<QCLuceneHits> contentHits; + if (titleQueryIsValid) { + titleHits = QSharedPointer<QCLuceneHits>(new QCLuceneHits( + indexSearcher.search(booleanQueryTitle))); + } + if (contentQueryIsValid) { + contentHits = QSharedPointer<QCLuceneHits>(new QCLuceneHits( + indexSearcher.search(booleanQueryContent))); + } + bool boost = true; + if ((titleHits.isNull() || titleHits->length() == 0) + && (contentHits.isNull() || contentHits->length() == 0)) { + booleanQueryTitle = QCLuceneBooleanQuery(); + booleanQueryContent = QCLuceneBooleanQuery(); + titleQueryIsValid = + buildTryHarderQuery(queryList, TitleTokenizedField, + attribList, booleanQueryTitle, analyzer); + contentQueryIsValid = + buildTryHarderQuery(queryList, ContentField, attribList, + booleanQueryContent, analyzer); + if (!titleQueryIsValid && !contentQueryIsValid) { + emit searchingFinished(0); + return; + } + if (titleQueryIsValid) { + titleHits = QSharedPointer<QCLuceneHits>(new QCLuceneHits( + indexSearcher.search(booleanQueryTitle))); + } + if (contentQueryIsValid) { + contentHits = QSharedPointer<QCLuceneHits>(new QCLuceneHits( + indexSearcher.search(booleanQueryContent))); + } + boost = false; + } + QList<QSharedPointer<QCLuceneHits> > cluceneHitsList; + if (!titleHits.isNull()) + cluceneHitsList.append(titleHits); + if (!contentHits.isNull()) + cluceneHitsList.append(contentHits); + + QSet<QString> pathSet; + QCLuceneDocument document; + const QStringList namespaceList = engine.registeredDocumentations(); + + foreach (const QSharedPointer<QCLuceneHits> &hits, cluceneHitsList) { + for (qint32 i = 0; i < hits->length(); i++) { + document = hits->document(i); + const QString path = document.get(PathField); + if (!pathSet.contains(path) && namespaceList.contains( + document.get(NamespaceField), Qt::CaseInsensitive)) { + pathSet.insert(path); + hitList.append(qMakePair(path, document.get(TitleField))); + } + document.clear(); + + mutex.lock(); + if (m_cancel) { + mutex.unlock(); + emit searchingFinished(0); + return; + } + mutex.unlock(); + } + } + + indexSearcher.close(); + const int count = hitList.count(); + if ((count > 0) && boost) + boostSearchHits(engine, hitList, queryList); + emit searchingFinished(hitList.count()); + +#if !defined(QT_NO_EXCEPTIONS) + } catch(...) { + mutex.lock(); + hitList.clear(); + mutex.unlock(); + emit searchingFinished(0); + } +#endif + } +} + +bool QHelpSearchIndexReaderClucene::buildQuery( + const QList<QHelpSearchQuery> &queries, const QString &fieldName, + const QStringList &filterAttributes, QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer) +{ + bool queryIsValid = false; + foreach (const QHelpSearchQuery &query, queries) { + if (fieldName != ContentField && isNegativeQuery(query)) { + queryIsValid = false; + break; + } + switch (query.fieldName) { + case QHelpSearchQuery::FUZZY: + if (addFuzzyQuery(query, fieldName, booleanQuery, analyzer)) + queryIsValid = true; + break; + case QHelpSearchQuery::WITHOUT: + if (fieldName != ContentField) + return false; + if (addWithoutQuery(query, fieldName, booleanQuery)) + queryIsValid = true; + break; + case QHelpSearchQuery::PHRASE: + if (addPhraseQuery(query, fieldName, booleanQuery)) + queryIsValid = true; + break; + case QHelpSearchQuery::ALL: + if (addAllQuery(query, fieldName, booleanQuery)) + queryIsValid = true; + break; + case QHelpSearchQuery::DEFAULT: + if (addDefaultQuery(query, fieldName, true, booleanQuery, analyzer)) + queryIsValid = true; + break; + case QHelpSearchQuery::ATLEAST: + if (addAtLeastQuery(query, fieldName, booleanQuery, analyzer)) + queryIsValid = true; + break; + default: + Q_ASSERT(!"Invalid field name"); + } + } + + if (queryIsValid && !filterAttributes.isEmpty()) { + queryIsValid = + addAttributesQuery(filterAttributes, booleanQuery, analyzer); + } + + return queryIsValid; +} + +bool QHelpSearchIndexReaderClucene::buildTryHarderQuery( + const QList<QHelpSearchQuery> &queries, const QString &fieldName, + const QStringList &filterAttributes, QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer) +{ + if (queries.isEmpty()) + return false; + const QHelpSearchQuery &query = queries.front(); + if (query.fieldName != QHelpSearchQuery::DEFAULT) + return false; + if (isNegativeQuery(query)) + return false; + if (!addDefaultQuery(query, fieldName, false, booleanQuery, analyzer)) + return false; + if (filterAttributes.isEmpty()) + return true; + return addAttributesQuery(filterAttributes, booleanQuery, analyzer); +} + +bool QHelpSearchIndexReaderClucene::isNegativeQuery(const QHelpSearchQuery &query) const +{ + const QString &search = query.wordList.join(" "); + return search.contains('!') || search.contains('-') + || search.contains(QLatin1String(" NOT ")); +} + +bool QHelpSearchIndexReaderClucene::addFuzzyQuery(const QHelpSearchQuery &query, + const QString &fieldName, QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer) +{ + bool queryIsValid = false; + const QLatin1String fuzzy("~"); + foreach (const QString &term, query.wordList) { + if (!term.isEmpty()) { + QCLuceneQuery *lQuery = + QCLuceneQueryParser::parse(term + fuzzy, fieldName, analyzer); + if (lQuery != 0) { + booleanQuery.add(lQuery, true, false, false); + queryIsValid = true; + } + } + } + return queryIsValid; +} + +bool QHelpSearchIndexReaderClucene::addWithoutQuery(const QHelpSearchQuery &query, + const QString &fieldName, QCLuceneBooleanQuery &booleanQuery) +{ + bool queryIsValid = false; + const QStringList &stopWords = QCLuceneStopAnalyzer().englishStopWords(); + foreach (const QString &term, query.wordList) { + if (stopWords.contains(term, Qt::CaseInsensitive)) + continue; + QCLuceneQuery *lQuery = new QCLuceneTermQuery(QCLuceneTerm( + fieldName, term.toLower())); + booleanQuery.add(lQuery, true, false, true); + queryIsValid = true; + } + return queryIsValid; +} + +bool QHelpSearchIndexReaderClucene::addPhraseQuery(const QHelpSearchQuery &query, + const QString &fieldName, QCLuceneBooleanQuery &booleanQuery) +{ + bool queryIsValid = false; + const QString &term = query.wordList.at(0).toLower(); + if (term.contains(QLatin1Char(' '))) { + const QStringList termList = term.split(QLatin1String(" ")); + QCLucenePhraseQuery *q = new QCLucenePhraseQuery(); + const QStringList stopWords = QCLuceneStopAnalyzer().englishStopWords(); + foreach (const QString &term, termList) { + if (!stopWords.contains(term, Qt::CaseInsensitive)) + q->addTerm(QCLuceneTerm(fieldName, term.toLower())); + } + if (!q->getTerms().isEmpty()) { + booleanQuery.add(q, true, true, false); + queryIsValid = true; + } + } else { + QCLuceneQuery *lQuery = new QCLuceneTermQuery(QCLuceneTerm( + fieldName, term.toLower())); + booleanQuery.add(lQuery, true, true, false); + queryIsValid = true; + } + return queryIsValid; +} + +bool QHelpSearchIndexReaderClucene::addAllQuery(const QHelpSearchQuery &query, + const QString &fieldName, QCLuceneBooleanQuery &booleanQuery) +{ + bool queryIsValid = false; + const QStringList &stopWords = QCLuceneStopAnalyzer().englishStopWords(); + foreach (const QString &term, query.wordList) { + if (stopWords.contains(term, Qt::CaseInsensitive)) + continue; + QCLuceneQuery *lQuery = new QCLuceneTermQuery(QCLuceneTerm( + fieldName, term.toLower())); + booleanQuery.add(lQuery, true, true, false); + queryIsValid = true; + } + return queryIsValid; +} + +bool QHelpSearchIndexReaderClucene::addDefaultQuery(const QHelpSearchQuery &query, + const QString &fieldName, bool allTermsRequired, + QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer) +{ + bool queryIsValid = false; + foreach (const QString &term, query.wordList) { + QCLuceneQuery *lQuery = + QCLuceneQueryParser::parse(term.toLower(), fieldName, analyzer); + if (lQuery) { + booleanQuery.add(lQuery, true, allTermsRequired, false); + queryIsValid = true; + } + } + return queryIsValid; +} + +bool QHelpSearchIndexReaderClucene::addAtLeastQuery( + const QHelpSearchQuery &query, const QString &fieldName, + QCLuceneBooleanQuery &booleanQuery, QCLuceneAnalyzer &analyzer) +{ + bool queryIsValid = false; + foreach (const QString &term, query.wordList) { + if (!term.isEmpty()) { + QCLuceneQuery *lQuery = + QCLuceneQueryParser::parse(term, fieldName, analyzer); + if (lQuery) { + booleanQuery.add(lQuery, true, false, false); + queryIsValid = true; + } + } + } + return queryIsValid; +} + +bool QHelpSearchIndexReaderClucene::addAttributesQuery( + const QStringList &filterAttributes, QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer) +{ + QCLuceneQuery* lQuery = QCLuceneQueryParser::parse(QLatin1String("+") + + filterAttributes.join(QLatin1String(" +")), AttributeField, analyzer); + if (!lQuery) + return false; + booleanQuery.add(lQuery, true, true, false); + return true; +} + +void QHelpSearchIndexReaderClucene::boostSearchHits(const QHelpEngineCore &engine, + QList<QHelpSearchEngine::SearchHit> &hitList, const QList<QHelpSearchQuery> &queryList) +{ + foreach (const QHelpSearchQuery &query, queryList) { + if (query.fieldName != QHelpSearchQuery::DEFAULT) + continue; + + QString joinedQuery = query.wordList.join(QLatin1String(" ")); + + QCLuceneStandardAnalyzer analyzer; + QCLuceneQuery *parsedQuery = QCLuceneQueryParser::parse( + joinedQuery, ContentField, analyzer); + + if (parsedQuery) { + joinedQuery = parsedQuery->toString(); + delete parsedQuery; + } + + const QString contentString(ContentField + QLatin1String(":")); + int length = contentString.length(); + int index = joinedQuery.indexOf(contentString); + + QString term; + int nextIndex = 0; + QStringList searchTerms; + while (index != -1) { + nextIndex = joinedQuery.indexOf(contentString, index + 1); + term = joinedQuery.mid(index + length, nextIndex - (length + index)).simplified(); + if (term.startsWith(QLatin1String("\"")) + && term.endsWith(QLatin1String("\""))) { + searchTerms.append(term.remove(QLatin1String("\""))); + } else { + searchTerms += term.split(QLatin1Char(' ')); + } + index = nextIndex; + } + searchTerms.removeDuplicates(); + + int count = qMin(75, hitList.count()); + QMap<int, QHelpSearchEngine::SearchHit> hitMap; + for (int i = 0; i < count; ++i) { + const QHelpSearchEngine::SearchHit &hit = hitList.at(i); + QString data = QString::fromUtf8(engine.fileData(hit.first)); + + int counter = 0; + foreach (const QString &term, searchTerms) + counter += data.count(term, Qt::CaseInsensitive); + hitMap.insertMulti(counter, hit); + } + + QList<QHelpSearchEngine::SearchHit> boostedList; + QMap<int, QHelpSearchEngine::SearchHit>::const_iterator it = hitMap.constEnd(); + do { + --it; + boostedList.append(it.value()); + } while (it != hitMap.constBegin()); + boostedList += hitList.mid(count, hitList.count()); + mutex.lock(); + hitList = boostedList; + mutex.unlock(); + } +} + +} // namespace clucene +} // namespace fulltextsearch + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchindexreader_clucene_p.h b/src/assistant/help/qhelpsearchindexreader_clucene_p.h new file mode 100644 index 000000000..1e3d199ed --- /dev/null +++ b/src/assistant/help/qhelpsearchindexreader_clucene_p.h @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHINDEXREADERCLUCENE_H +#define QHELPSEARCHINDEXREADERCLUCENE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QList> +#include <QtCore/QString> +#include <QtCore/QStringList> + +#include "private/qanalyzer_p.h" +#include "private/qquery_p.h" +#include "qhelpsearchindexreader_p.h" + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace clucene { + +class QHelpSearchIndexReaderClucene : public QHelpSearchIndexReader +{ + Q_OBJECT + +public: + QHelpSearchIndexReaderClucene(); + ~QHelpSearchIndexReaderClucene(); + +private: + void run(); + void boostSearchHits(const QHelpEngineCore &engine, QList<QHelpSearchEngine::SearchHit> &hitList, + const QList<QHelpSearchQuery> &queryList); + bool buildQuery(const QList<QHelpSearchQuery> &queries, + const QString &fieldName, + const QStringList &filterAttributes, + QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer); + bool buildTryHarderQuery(const QList<QHelpSearchQuery> &queries, + const QString &fieldName, + const QStringList &filterAttributes, + QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer); + bool addFuzzyQuery(const QHelpSearchQuery &query, const QString &fieldName, + QCLuceneBooleanQuery &booleanQuery, QCLuceneAnalyzer &analyzer); + bool addWithoutQuery(const QHelpSearchQuery &query, const QString &fieldName, + QCLuceneBooleanQuery &booleanQuery); + bool addPhraseQuery(const QHelpSearchQuery &query, const QString &fieldName, + QCLuceneBooleanQuery &booleanQuery); + bool addAllQuery(const QHelpSearchQuery &query, const QString &fieldName, + QCLuceneBooleanQuery &booleanQuery); + bool addDefaultQuery(const QHelpSearchQuery &query, const QString &fieldName, + bool allTermsRequired, QCLuceneBooleanQuery &booleanQuery, + QCLuceneAnalyzer &analyzer); + bool addAtLeastQuery(const QHelpSearchQuery &query, const QString &fieldName, + QCLuceneBooleanQuery &booleanQuery, QCLuceneAnalyzer &analyzer); + bool addAttributesQuery(const QStringList &filterAttributes, + QCLuceneBooleanQuery &booleanQuery, QCLuceneAnalyzer &analyzer); + bool isNegativeQuery(const QHelpSearchQuery &query) const; +}; + +} // namespace clucene +} // namespace fulltextsearch + +QT_END_NAMESPACE + +#endif // QHELPSEARCHINDEXREADERCLUCENE_H diff --git a/src/assistant/help/qhelpsearchindexreader_default.cpp b/src/assistant/help/qhelpsearchindexreader_default.cpp new file mode 100644 index 000000000..e49fdfcbd --- /dev/null +++ b/src/assistant/help/qhelpsearchindexreader_default.cpp @@ -0,0 +1,612 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpenginecore.h" +#include "qhelpsearchindexreader_default_p.h" + +#include <QtCore/QDir> +#include <QtCore/QUrl> +#include <QtCore/QFile> +#include <QtCore/QVariant> +#include <QtCore/QFileInfo> +#include <QtCore/QDataStream> +#include <QtCore/QTextStream> + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace std { + +namespace { + QStringList split( const QString &str ) + { + QStringList lst; + int j = 0; + int i = str.indexOf(QLatin1Char('*'), j ); + + if (str.startsWith(QLatin1String("*"))) + lst << QLatin1String("*"); + + while ( i != -1 ) { + if ( i > j && i <= (int)str.length() ) { + lst << str.mid( j, i - j ); + lst << QLatin1String("*"); + } + j = i + 1; + i = str.indexOf(QLatin1Char('*'), j ); + } + + int l = str.length() - 1; + if ( str.mid( j, l - j + 1 ).length() > 0 ) + lst << str.mid( j, l - j + 1 ); + + return lst; + } +} + + +Reader::Reader() + : indexPath(QString()) + , indexFile(QString()) + , documentFile(QString()) +{ + termList.clear(); + indexTable.clear(); + searchIndexTable.clear(); +} + +Reader::~Reader() +{ + reset(); + searchIndexTable.clear(); +} + +bool Reader::readIndex() +{ + if (indexTable.contains(indexFile)) + return true; + + QFile idxFile(indexFile); + if (!idxFile.open(QFile::ReadOnly)) + return false; + + QString key; + int numOfDocs; + EntryTable entryTable; + QVector<Document> docs; + QDataStream dictStream(&idxFile); + while (!dictStream.atEnd()) { + dictStream >> key; + dictStream >> numOfDocs; + docs.resize(numOfDocs); + dictStream >> docs; + entryTable.insert(key, new Entry(docs)); + } + idxFile.close(); + + if (entryTable.isEmpty()) + return false; + + QFile docFile(documentFile); + if (!docFile.open(QFile::ReadOnly)) + return false; + + QString title, url; + DocumentList documentList; + QDataStream docStream(&docFile); + while (!docStream.atEnd()) { + docStream >> title; + docStream >> url; + documentList.append(QStringList(title) << url); + } + docFile.close(); + + if (documentList.isEmpty()) { + cleanupIndex(entryTable); + return false; + } + + indexTable.insert(indexFile, Index(entryTable, documentList)); + return true; +} + +bool Reader::initCheck() const +{ + return !searchIndexTable.isEmpty(); +} + +void Reader::setIndexPath(const QString &path) +{ + indexPath = path; +} + +void Reader::filterFilesForAttributes(const QStringList &attributes) +{ + searchIndexTable.clear(); + for(IndexTable::ConstIterator it = indexTable.begin(); it != indexTable.end(); ++it) { + const QString fileName = it.key(); + bool containsAll = true; + QStringList split = fileName.split(QLatin1String("@")); + foreach (const QString &attribute, attributes) { + if (!split.contains(attribute, Qt::CaseInsensitive)) { + containsAll = false; + break; + } + } + + if (containsAll) + searchIndexTable.insert(fileName, it.value()); + } +} + +void Reader::setIndexFile(const QString &namespaceName, const QString &attributes) +{ + QString extension = namespaceName + QLatin1String("@") + attributes; + indexFile = indexPath + QLatin1String("/indexdb40.") + extension; + documentFile = indexPath + QLatin1String("/indexdoc40.") + extension; +} + +bool Reader::splitSearchTerm(const QString &searchTerm, QStringList *terms, + QStringList *termSeq, QStringList *seqWords) +{ + QString term = searchTerm; + + term = term.simplified(); + term = term.replace(QLatin1String("\'"), QLatin1String("\"")); + term = term.replace(QLatin1String("`"), QLatin1String("\"")); + term = term.replace(QLatin1String("-"), QLatin1String(" ")); + term = term.replace(QRegExp(QLatin1String("\\s[\\S]?\\s")), QLatin1String(" ")); + + *terms = term.split(QLatin1Char(' ')); + QStringList::iterator it = terms->begin(); + for (; it != terms->end(); ++it) { + (*it) = (*it).simplified(); + (*it) = (*it).toLower(); + (*it) = (*it).replace(QLatin1String("\""), QLatin1String("")); + } + + if (term.contains(QLatin1Char('\"'))) { + if ((term.count(QLatin1Char('\"')))%2 == 0) { + int beg = 0; + int end = 0; + QString s; + beg = term.indexOf(QLatin1Char('\"'), beg); + while (beg != -1) { + beg++; + end = term.indexOf(QLatin1Char('\"'), beg); + s = term.mid(beg, end - beg); + s = s.toLower(); + s = s.simplified(); + if (s.contains(QLatin1Char('*'))) { + qWarning("Full Text Search, using a wildcard within phrases is not allowed."); + return false; + } + *seqWords += s.split(QLatin1Char(' ')); + *termSeq << s; + beg = term.indexOf(QLatin1Char('\"'), end + 1); + } + } else { + qWarning("Full Text Search, the closing quotation mark is missing."); + return false; + } + } + + return true; +} + +void Reader::searchInIndex(const QStringList &terms) +{ + foreach (const QString &term, terms) { + QVector<Document> documents; + + for(IndexTable::ConstIterator it = searchIndexTable.begin(); + it != searchIndexTable.end(); ++it) { + EntryTable entryTable = it.value().first; + DocumentList documentList = it.value().second; + + if (term.contains(QLatin1Char('*'))) + documents = setupDummyTerm(getWildcardTerms(term, entryTable), entryTable); + else if (entryTable.value(term)) + documents = entryTable.value(term)->documents; + else + continue; + + if (!documents.isEmpty()) { + DocumentInfo info; + QString title, url; + QVector<DocumentInfo> documentsInfo; + foreach(const Document &doc, documents) { + info.docNumber = doc.docNumber; + info.frequency = doc.frequency; + info.documentUrl = documentList.at(doc.docNumber).at(1); + info.documentTitle = documentList.at(doc.docNumber).at(0); + documentsInfo.append(info); + } + + bool found = false; + for(QList<TermInfo>::Iterator tit = termList.begin(); + tit != termList.end(); ++tit) { + TermInfo *t = &(*tit); + if(t->term == term) { + t->documents += documentsInfo; + t->frequency += documentsInfo.count(); + found = true; break; + } + } + if (!found) + termList.append(TermInfo(term, documentsInfo.count(), documentsInfo)); + } + } + } + qSort(termList); +} + +QVector<DocumentInfo> Reader::hits() +{ + QVector<DocumentInfo> documents; + if (!termList.count()) + return documents; + + documents = termList.takeFirst().documents; + for(QList<TermInfo>::Iterator it = termList.begin(); it != termList.end(); ++it) { + TermInfo *t = &(*it); + QVector<DocumentInfo> docs = t->documents; + for(QVector<DocumentInfo>::Iterator minDoc_it = documents.begin(); + minDoc_it != documents.end(); ) { + bool found = false; + for (QVector<DocumentInfo>::ConstIterator doc_it = docs.constBegin(); + doc_it != docs.constEnd(); ++doc_it ) { + if ( (*minDoc_it).docNumber == (*doc_it).docNumber ) { + (*minDoc_it).frequency += (*doc_it).frequency; + found = true; + break; + } + } + if (!found) + minDoc_it = documents.erase(minDoc_it); + else + ++minDoc_it; + } + } + + qSort(documents); + return documents; +} + +bool Reader::searchForPattern(const QStringList &patterns, const QStringList &words, + const QByteArray &data) +{ + if (data.isEmpty()) + return false; + + for(QHash<QString, PosEntry*>::ConstIterator mit = + miniIndex.begin(); mit != miniIndex.end(); ++mit) { + delete mit.value(); + } + miniIndex.clear(); + + wordNum = 3; + QStringList::ConstIterator cIt = words.begin(); + for ( ; cIt != words.end(); ++cIt ) + miniIndex.insert(*cIt, new PosEntry(0)); + + QTextStream s(data); + QString text = s.readAll(); + bool valid = true; + const QChar *buf = text.unicode(); + QChar str[64]; + QChar c = buf[0]; + int j = 0; + int i = 0; + while ( j < text.length() ) { + if ( c == QLatin1Char('<') || c == QLatin1Char('&') ) { + valid = false; + if ( i > 1 ) + buildMiniIndex( QString(str,i) ); + i = 0; + c = buf[++j]; + continue; + } + if ( ( c == QLatin1Char('>') || c == QLatin1Char(';') ) && !valid ) { + valid = true; + c = buf[++j]; + continue; + } + if ( !valid ) { + c = buf[++j]; + continue; + } + if ( ( c.isLetterOrNumber() || c == QLatin1Char('_') ) && i < 63 ) { + str[i] = c.toLower(); + ++i; + } else { + if ( i > 1 ) + buildMiniIndex( QString(str,i) ); + i = 0; + } + c = buf[++j]; + } + if ( i > 1 ) + buildMiniIndex( QString(str,i) ); + + QStringList::ConstIterator patIt = patterns.begin(); + QStringList wordLst; + QList<uint> a, b; + QList<uint>::iterator aIt; + for ( ; patIt != patterns.end(); ++patIt ) { + wordLst = (*patIt).split(QLatin1Char(' ')); + a = miniIndex[ wordLst[0] ]->positions; + for ( int j = 1; j < (int)wordLst.count(); ++j ) { + b = miniIndex[ wordLst[j] ]->positions; + aIt = a.begin(); + while ( aIt != a.end() ) { + if ( b.contains( *aIt + 1 )) { + (*aIt)++; + ++aIt; + } else { + aIt = a.erase( aIt ); + } + } + } + } + if ( a.count() ) + return true; + return false; +} + +QVector<Document> Reader::setupDummyTerm(const QStringList &terms, + const EntryTable &entryTable) +{ + QList<Term> termList; + for (QStringList::ConstIterator it = terms.begin(); it != terms.end(); ++it) { + if (entryTable.value(*it)) { + Entry *e = entryTable.value(*it); + termList.append(Term(*it, e->documents.count(), e->documents ) ); + } + } + QVector<Document> maxList(0); + if ( !termList.count() ) + return maxList; + qSort(termList); + + maxList = termList.takeLast().documents; + for(QList<Term>::Iterator it = termList.begin(); it != termList.end(); ++it) { + Term *t = &(*it); + QVector<Document> docs = t->documents; + for (QVector<Document>::iterator docIt = docs.begin(); docIt != docs.end(); ++docIt ) { + if ( maxList.indexOf( *docIt ) == -1 ) + maxList.append( *docIt ); + } + } + return maxList; +} + +QStringList Reader::getWildcardTerms(const QString &term, + const EntryTable &entryTable) +{ + QStringList lst; + QStringList terms = split(term); + QStringList::Iterator iter; + + for(EntryTable::ConstIterator it = entryTable.begin(); + it != entryTable.end(); ++it) { + int index = 0; + bool found = false; + QString text( it.key() ); + for ( iter = terms.begin(); iter != terms.end(); ++iter ) { + if ( *iter == QLatin1String("*") ) { + found = true; + continue; + } + if ( iter == terms.begin() && (*iter)[0] != text[0] ) { + found = false; + break; + } + index = text.indexOf( *iter, index ); + if ( *iter == terms.last() && index != (int)text.length()-1 ) { + index = text.lastIndexOf( *iter ); + if ( index != (int)text.length() - (int)(*iter).length() ) { + found = false; + break; + } + } + if ( index != -1 ) { + found = true; + index += (*iter).length(); + continue; + } else { + found = false; + break; + } + } + if (found) + lst << text; + } + + return lst; +} + +void Reader::buildMiniIndex(const QString &string) +{ + if (miniIndex[string]) + miniIndex[string]->positions.append(wordNum); + ++wordNum; +} + +void Reader::reset() +{ + for(IndexTable::Iterator it = indexTable.begin(); + it != indexTable.end(); ++it) { + cleanupIndex(it.value().first); + it.value().second.clear(); + } +} + +void Reader::cleanupIndex(EntryTable &entryTable) +{ + for(EntryTable::ConstIterator it = + entryTable.begin(); it != entryTable.end(); ++it) { + delete it.value(); + } + + entryTable.clear(); +} + + +QHelpSearchIndexReaderDefault::QHelpSearchIndexReaderDefault() + : QHelpSearchIndexReader() +{ + // nothing todo +} + +QHelpSearchIndexReaderDefault::~QHelpSearchIndexReaderDefault() +{ +} + +void QHelpSearchIndexReaderDefault::run() +{ + mutex.lock(); + + if (m_cancel) { + mutex.unlock(); + return; + } + + const QList<QHelpSearchQuery> &queryList = this->m_query; + const QLatin1String key("DefaultSearchNamespaces"); + const QString collectionFile(this->m_collectionFile); + const QString indexPath = m_indexFilesFolder; + + mutex.unlock(); + + QString queryTerm; + foreach (const QHelpSearchQuery &query, queryList) { + if (query.fieldName == QHelpSearchQuery::DEFAULT) { + queryTerm = query.wordList.at(0); + break; + } + } + + if (queryTerm.isEmpty()) + return; + + QHelpEngineCore engine(collectionFile, 0); + if (!engine.setupData()) + return; + + const QStringList registeredDocs = engine.registeredDocumentations(); + const QStringList indexedNamespaces = engine.customValue(key).toString(). + split(QLatin1String("|"), QString::SkipEmptyParts); + + emit searchingStarted(); + + // setup the reader + m_reader.setIndexPath(indexPath); + foreach(const QString &namespaceName, registeredDocs) { + mutex.lock(); + if (m_cancel) { + mutex.unlock(); + searchingFinished(0); // TODO: check this ??? + return; + } + mutex.unlock(); + + const QList<QStringList> attributeSets = + engine.filterAttributeSets(namespaceName); + + foreach (const QStringList &attributes, attributeSets) { + // read all index files + m_reader.setIndexFile(namespaceName, attributes.join(QLatin1String("@"))); + if (!m_reader.readIndex()) { + qWarning("Full Text Search, could not read file for namespace: %s.", + namespaceName.toUtf8().constData()); + } + } + } + + // get the current filter attributes and minimize the index files table + m_reader.filterFilesForAttributes(engine.filterAttributes(engine.currentFilter())); + + hitList.clear(); + QStringList terms, termSeq, seqWords; + if (m_reader.initCheck() && // check if we could read anything + m_reader.splitSearchTerm(queryTerm, &terms, &termSeq, &seqWords) ) { + + // search for term(s) + m_reader.searchInIndex(terms); // TODO: should this be interruptible as well ??? + + QVector<DocumentInfo> hits = m_reader.hits(); + if (!hits.isEmpty()) { + if (termSeq.isEmpty()) { + foreach (const DocumentInfo &docInfo, hits) { + mutex.lock(); + if (m_cancel) { + mutex.unlock(); + searchingFinished(0); // TODO: check this, speed issue while locking??? + return; + } + mutex.unlock(); + hitList.append(qMakePair(docInfo.documentTitle, docInfo.documentUrl)); + } + } else { + foreach (const DocumentInfo &docInfo, hits) { + mutex.lock(); + if (m_cancel) { + mutex.unlock(); + searchingFinished(0); // TODO: check this, speed issue while locking??? + return; + } + mutex.unlock(); + + if (m_reader.searchForPattern(termSeq, seqWords, engine.fileData(docInfo.documentUrl))) // TODO: should this be interruptible as well ??? + hitList.append(qMakePair(docInfo.documentTitle, docInfo.documentUrl)); + } + } + } + } + + emit searchingFinished(hitList.count()); +} + +} // namespace std +} // namespace fulltextsearch + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchindexreader_default_p.h b/src/assistant/help/qhelpsearchindexreader_default_p.h new file mode 100644 index 000000000..30dfe1ed6 --- /dev/null +++ b/src/assistant/help/qhelpsearchindexreader_default_p.h @@ -0,0 +1,131 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHINDEXREADERDEFAULT_H +#define QHELPSEARCHINDEXREADERDEFAULT_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include "qhelpsearchindex_default_p.h" +#include "qhelpsearchindexreader_p.h" + +#include <QtCore/QHash> +#include <QtCore/QPair> + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace std { + +class Reader +{ + typedef QList<QStringList> DocumentList; + typedef QHash<QString, Entry*> EntryTable; + typedef QPair<EntryTable, DocumentList> Index; + typedef QHash<QString, Index> IndexTable; + +public: + Reader(); + ~Reader(); + + bool readIndex(); + bool initCheck() const; + void setIndexPath(const QString &path); + void filterFilesForAttributes(const QStringList &attributes); + void setIndexFile(const QString &namespaceName, const QString &attributes); + bool splitSearchTerm(const QString &searchTerm, QStringList *terms, + QStringList *termSeq, QStringList *seqWords); + + void searchInIndex(const QStringList &terms); + QVector<DocumentInfo> hits(); + bool searchForPattern(const QStringList &patterns, + const QStringList &words, const QByteArray &data); + +private: + QVector<Document> setupDummyTerm(const QStringList &terms, const EntryTable &entryTable); + QStringList getWildcardTerms(const QString &term, const EntryTable &entryTable); + void buildMiniIndex(const QString &string); + void reset(); + void cleanupIndex(EntryTable &entryTable); + +private: + uint wordNum; + QString indexPath; + QString indexFile; + QString documentFile; + + IndexTable indexTable; + QList<TermInfo> termList; + IndexTable searchIndexTable; + QHash<QString, PosEntry*> miniIndex; +}; + + +class QHelpSearchIndexReaderDefault : public QHelpSearchIndexReader +{ + Q_OBJECT + +public: + QHelpSearchIndexReaderDefault(); + ~QHelpSearchIndexReaderDefault(); + +private: + void run(); + +private: + Reader m_reader; +}; + +} // namespace std +} // namespace fulltextsearch + +QT_END_NAMESPACE + +#endif // QHELPSEARCHINDEXREADERDEFAULT_H diff --git a/src/assistant/help/qhelpsearchindexreader_p.h b/src/assistant/help/qhelpsearchindexreader_p.h new file mode 100644 index 000000000..0d7518a6e --- /dev/null +++ b/src/assistant/help/qhelpsearchindexreader_p.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHINDEXREADER_H +#define QHELPSEARCHINDEXREADER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include "qhelpsearchengine.h" + +#include <QtCore/QList> +#include <QtCore/QMutex> +#include <QtCore/QObject> +#include <QtCore/QString> +#include <QtCore/QThread> +#include <QtCore/QWaitCondition> + +QT_BEGIN_NAMESPACE + +class QHelpEngineCore; + +namespace fulltextsearch { + +class QHelpSearchIndexReader : public QThread +{ + Q_OBJECT + +public: + QHelpSearchIndexReader(); + ~QHelpSearchIndexReader(); + + void cancelSearching(); + void search(const QString &collectionFile, + const QString &indexFilesFolder, + const QList<QHelpSearchQuery> &queryList); + int hitCount() const; + QList<QHelpSearchEngine::SearchHit> hits(int start, int end) const; + +signals: + void searchingStarted(); + void searchingFinished(int hits); + +protected: + mutable QMutex mutex; + QList<QHelpSearchEngine::SearchHit> hitList; + bool m_cancel; + QString m_collectionFile; + QList<QHelpSearchQuery> m_query; + QString m_indexFilesFolder; + +private: + virtual void run()=0; +}; + +} // namespace fulltextsearch + +QT_END_NAMESPACE + +#endif // QHELPSEARCHINDEXREADER_H diff --git a/src/assistant/help/qhelpsearchindexwriter_clucene.cpp b/src/assistant/help/qhelpsearchindexwriter_clucene.cpp new file mode 100644 index 000000000..287151338 --- /dev/null +++ b/src/assistant/help/qhelpsearchindexwriter_clucene.cpp @@ -0,0 +1,898 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qclucenefieldnames_p.h" +#include "qhelpenginecore.h" +#include "qhelp_global.h" +#include "private/qhits_p.h" +#include "private/qquery_p.h" +#include "private/qanalyzer_p.h" +#include "private/qdocument_p.h" +#include "private/qsearchable_p.h" +#include "private/qindexreader_p.h" +#include "private/qindexwriter_p.h" +#include "qhelpsearchindexwriter_clucene_p.h" + +#include <QtCore/QDir> +#include <QtCore/QString> +#include <QtCore/QFileInfo> +#include <QtCore/QTextCodec> +#include <QtCore/QTextStream> + +#include <QtNetwork/QLocalSocket> +#include <QtNetwork/QLocalServer> + +#include "private/qfunctions_p.h" + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace clucene { + +// taken from qtexthtmlparser +static const struct QTextHtmlEntity +{ + const char *name; + quint16 code; +} entities[] = { + { "AElig", 0x00c6 }, + { "AMP", 38 }, + { "Aacute", 0x00c1 }, + { "Acirc", 0x00c2 }, + { "Agrave", 0x00c0 }, + { "Alpha", 0x0391 }, + { "Aring", 0x00c5 }, + { "Atilde", 0x00c3 }, + { "Auml", 0x00c4 }, + { "Beta", 0x0392 }, + { "Ccedil", 0x00c7 }, + { "Chi", 0x03a7 }, + { "Dagger", 0x2021 }, + { "Delta", 0x0394 }, + { "ETH", 0x00d0 }, + { "Eacute", 0x00c9 }, + { "Ecirc", 0x00ca }, + { "Egrave", 0x00c8 }, + { "Epsilon", 0x0395 }, + { "Eta", 0x0397 }, + { "Euml", 0x00cb }, + { "GT", 62 }, + { "Gamma", 0x0393 }, + { "Iacute", 0x00cd }, + { "Icirc", 0x00ce }, + { "Igrave", 0x00cc }, + { "Iota", 0x0399 }, + { "Iuml", 0x00cf }, + { "Kappa", 0x039a }, + { "LT", 60 }, + { "Lambda", 0x039b }, + { "Mu", 0x039c }, + { "Ntilde", 0x00d1 }, + { "Nu", 0x039d }, + { "OElig", 0x0152 }, + { "Oacute", 0x00d3 }, + { "Ocirc", 0x00d4 }, + { "Ograve", 0x00d2 }, + { "Omega", 0x03a9 }, + { "Omicron", 0x039f }, + { "Oslash", 0x00d8 }, + { "Otilde", 0x00d5 }, + { "Ouml", 0x00d6 }, + { "Phi", 0x03a6 }, + { "Pi", 0x03a0 }, + { "Prime", 0x2033 }, + { "Psi", 0x03a8 }, + { "QUOT", 34 }, + { "Rho", 0x03a1 }, + { "Scaron", 0x0160 }, + { "Sigma", 0x03a3 }, + { "THORN", 0x00de }, + { "Tau", 0x03a4 }, + { "Theta", 0x0398 }, + { "Uacute", 0x00da }, + { "Ucirc", 0x00db }, + { "Ugrave", 0x00d9 }, + { "Upsilon", 0x03a5 }, + { "Uuml", 0x00dc }, + { "Xi", 0x039e }, + { "Yacute", 0x00dd }, + { "Yuml", 0x0178 }, + { "Zeta", 0x0396 }, + { "aacute", 0x00e1 }, + { "acirc", 0x00e2 }, + { "acute", 0x00b4 }, + { "aelig", 0x00e6 }, + { "agrave", 0x00e0 }, + { "alefsym", 0x2135 }, + { "alpha", 0x03b1 }, + { "amp", 38 }, + { "and", 0x22a5 }, + { "ang", 0x2220 }, + { "apos", 0x0027 }, + { "aring", 0x00e5 }, + { "asymp", 0x2248 }, + { "atilde", 0x00e3 }, + { "auml", 0x00e4 }, + { "bdquo", 0x201e }, + { "beta", 0x03b2 }, + { "brvbar", 0x00a6 }, + { "bull", 0x2022 }, + { "cap", 0x2229 }, + { "ccedil", 0x00e7 }, + { "cedil", 0x00b8 }, + { "cent", 0x00a2 }, + { "chi", 0x03c7 }, + { "circ", 0x02c6 }, + { "clubs", 0x2663 }, + { "cong", 0x2245 }, + { "copy", 0x00a9 }, + { "crarr", 0x21b5 }, + { "cup", 0x222a }, + { "curren", 0x00a4 }, + { "dArr", 0x21d3 }, + { "dagger", 0x2020 }, + { "darr", 0x2193 }, + { "deg", 0x00b0 }, + { "delta", 0x03b4 }, + { "diams", 0x2666 }, + { "divide", 0x00f7 }, + { "eacute", 0x00e9 }, + { "ecirc", 0x00ea }, + { "egrave", 0x00e8 }, + { "empty", 0x2205 }, + { "emsp", 0x2003 }, + { "ensp", 0x2002 }, + { "epsilon", 0x03b5 }, + { "equiv", 0x2261 }, + { "eta", 0x03b7 }, + { "eth", 0x00f0 }, + { "euml", 0x00eb }, + { "euro", 0x20ac }, + { "exist", 0x2203 }, + { "fnof", 0x0192 }, + { "forall", 0x2200 }, + { "frac12", 0x00bd }, + { "frac14", 0x00bc }, + { "frac34", 0x00be }, + { "frasl", 0x2044 }, + { "gamma", 0x03b3 }, + { "ge", 0x2265 }, + { "gt", 62 }, + { "hArr", 0x21d4 }, + { "harr", 0x2194 }, + { "hearts", 0x2665 }, + { "hellip", 0x2026 }, + { "iacute", 0x00ed }, + { "icirc", 0x00ee }, + { "iexcl", 0x00a1 }, + { "igrave", 0x00ec }, + { "image", 0x2111 }, + { "infin", 0x221e }, + { "int", 0x222b }, + { "iota", 0x03b9 }, + { "iquest", 0x00bf }, + { "isin", 0x2208 }, + { "iuml", 0x00ef }, + { "kappa", 0x03ba }, + { "lArr", 0x21d0 }, + { "lambda", 0x03bb }, + { "lang", 0x2329 }, + { "laquo", 0x00ab }, + { "larr", 0x2190 }, + { "lceil", 0x2308 }, + { "ldquo", 0x201c }, + { "le", 0x2264 }, + { "lfloor", 0x230a }, + { "lowast", 0x2217 }, + { "loz", 0x25ca }, + { "lrm", 0x200e }, + { "lsaquo", 0x2039 }, + { "lsquo", 0x2018 }, + { "lt", 60 }, + { "macr", 0x00af }, + { "mdash", 0x2014 }, + { "micro", 0x00b5 }, + { "middot", 0x00b7 }, + { "minus", 0x2212 }, + { "mu", 0x03bc }, + { "nabla", 0x2207 }, + { "nbsp", 0x00a0 }, + { "ndash", 0x2013 }, + { "ne", 0x2260 }, + { "ni", 0x220b }, + { "not", 0x00ac }, + { "notin", 0x2209 }, + { "nsub", 0x2284 }, + { "ntilde", 0x00f1 }, + { "nu", 0x03bd }, + { "oacute", 0x00f3 }, + { "ocirc", 0x00f4 }, + { "oelig", 0x0153 }, + { "ograve", 0x00f2 }, + { "oline", 0x203e }, + { "omega", 0x03c9 }, + { "omicron", 0x03bf }, + { "oplus", 0x2295 }, + { "or", 0x22a6 }, + { "ordf", 0x00aa }, + { "ordm", 0x00ba }, + { "oslash", 0x00f8 }, + { "otilde", 0x00f5 }, + { "otimes", 0x2297 }, + { "ouml", 0x00f6 }, + { "para", 0x00b6 }, + { "part", 0x2202 }, + { "percnt", 0x0025 }, + { "permil", 0x2030 }, + { "perp", 0x22a5 }, + { "phi", 0x03c6 }, + { "pi", 0x03c0 }, + { "piv", 0x03d6 }, + { "plusmn", 0x00b1 }, + { "pound", 0x00a3 }, + { "prime", 0x2032 }, + { "prod", 0x220f }, + { "prop", 0x221d }, + { "psi", 0x03c8 }, + { "quot", 34 }, + { "rArr", 0x21d2 }, + { "radic", 0x221a }, + { "rang", 0x232a }, + { "raquo", 0x00bb }, + { "rarr", 0x2192 }, + { "rceil", 0x2309 }, + { "rdquo", 0x201d }, + { "real", 0x211c }, + { "reg", 0x00ae }, + { "rfloor", 0x230b }, + { "rho", 0x03c1 }, + { "rlm", 0x200f }, + { "rsaquo", 0x203a }, + { "rsquo", 0x2019 }, + { "sbquo", 0x201a }, + { "scaron", 0x0161 }, + { "sdot", 0x22c5 }, + { "sect", 0x00a7 }, + { "shy", 0x00ad }, + { "sigma", 0x03c3 }, + { "sigmaf", 0x03c2 }, + { "sim", 0x223c }, + { "spades", 0x2660 }, + { "sub", 0x2282 }, + { "sube", 0x2286 }, + { "sum", 0x2211 }, + { "sup", 0x2283 }, + { "sup1", 0x00b9 }, + { "sup2", 0x00b2 }, + { "sup3", 0x00b3 }, + { "supe", 0x2287 }, + { "szlig", 0x00df }, + { "tau", 0x03c4 }, + { "there4", 0x2234 }, + { "theta", 0x03b8 }, + { "thetasym", 0x03d1 }, + { "thinsp", 0x2009 }, + { "thorn", 0x00fe }, + { "tilde", 0x02dc }, + { "times", 0x00d7 }, + { "trade", 0x2122 }, + { "uArr", 0x21d1 }, + { "uacute", 0x00fa }, + { "uarr", 0x2191 }, + { "ucirc", 0x00fb }, + { "ugrave", 0x00f9 }, + { "uml", 0x00a8 }, + { "upsih", 0x03d2 }, + { "upsilon", 0x03c5 }, + { "uuml", 0x00fc }, + { "weierp", 0x2118 }, + { "xi", 0x03be }, + { "yacute", 0x00fd }, + { "yen", 0x00a5 }, + { "yuml", 0x00ff }, + { "zeta", 0x03b6 }, + { "zwj", 0x200d }, + { "zwnj", 0x200c } +}; + +Q_STATIC_GLOBAL_OPERATOR bool operator<(const QString &entityStr, const QTextHtmlEntity &entity) +{ + return entityStr < QLatin1String(entity.name); +} + +Q_STATIC_GLOBAL_OPERATOR bool operator<(const QTextHtmlEntity &entity, const QString &entityStr) +{ + return QLatin1String(entity.name) < entityStr; +} + +static QChar resolveEntity(const QString &entity) +{ + const QTextHtmlEntity *start = &entities[0]; + const QTextHtmlEntity *end = &entities[(sizeof(entities) / sizeof(entities[0]))]; + const QTextHtmlEntity *e = qBinaryFind(start, end, entity); + if (e == end) + return QChar(); + return e->code; +} + +static const uint latin1Extended[0xA0 - 0x80] = { + 0x20ac, // 0x80 + 0x0081, // 0x81 direct mapping + 0x201a, // 0x82 + 0x0192, // 0x83 + 0x201e, // 0x84 + 0x2026, // 0x85 + 0x2020, // 0x86 + 0x2021, // 0x87 + 0x02C6, // 0x88 + 0x2030, // 0x89 + 0x0160, // 0x8A + 0x2039, // 0x8B + 0x0152, // 0x8C + 0x008D, // 0x8D direct mapping + 0x017D, // 0x8E + 0x008F, // 0x8F directmapping + 0x0090, // 0x90 directmapping + 0x2018, // 0x91 + 0x2019, // 0x92 + 0x201C, // 0x93 + 0X201D, // 0x94 + 0x2022, // 0x95 + 0x2013, // 0x96 + 0x2014, // 0x97 + 0x02DC, // 0x98 + 0x2122, // 0x99 + 0x0161, // 0x9A + 0x203A, // 0x9B + 0x0153, // 0x9C + 0x009D, // 0x9D direct mapping + 0x017E, // 0x9E + 0x0178 // 0x9F +}; +// end taken from qtexthtmlparser + +class DocumentHelper +{ +public: + DocumentHelper(const QString &fileName, const QByteArray &data) + : fileName(fileName) , data(readData(data)) {} + ~DocumentHelper() {} + + bool addFieldsToDocument(QCLuceneDocument *document, + const QString &namespaceName, const QString &attributes = QString()) + { + if (!document) + return false; + + if(!data.isEmpty()) { + QString parsedData = parseData(); + QString parsedTitle = QHelpGlobal::documentTitle(data); + + if(!parsedData.isEmpty()) { + document->add(new QCLuceneField(ContentField, + parsedData,QCLuceneField::INDEX_TOKENIZED)); + document->add(new QCLuceneField(PathField, fileName, + QCLuceneField::STORE_YES | QCLuceneField::INDEX_UNTOKENIZED)); + document->add(new QCLuceneField(TitleField, parsedTitle, + QCLuceneField::STORE_YES | QCLuceneField::INDEX_UNTOKENIZED)); + document->add(new QCLuceneField(TitleTokenizedField, parsedTitle, + QCLuceneField::STORE_YES | QCLuceneField::INDEX_TOKENIZED)); + document->add(new QCLuceneField(NamespaceField, namespaceName, + QCLuceneField::STORE_YES | QCLuceneField::INDEX_UNTOKENIZED)); + document->add(new QCLuceneField(AttributeField, attributes, + QCLuceneField::STORE_YES | QCLuceneField::INDEX_TOKENIZED)); + return true; + } + } + + return false; + } + +private: + QString readData(const QByteArray &data) + { + QTextStream textStream(data); + const QByteArray &codec = QHelpGlobal::codecFromData(data).toLatin1(); + textStream.setCodec(QTextCodec::codecForName(codec.constData())); + + QString stream = textStream.readAll(); + if (stream.isNull() || stream.isEmpty()) + return QString(); + + return stream; + } + + QString parseData() const + { + const int length = data.length(); + const QChar *buf = data.unicode(); + + QString parsedContent; + parsedContent.reserve(length); + + bool valid = true; + int j = 0, count = 0; + + QChar c; + while (j < length) { + c = buf[j++]; + if (c == QLatin1Char('<') || c == QLatin1Char('&')) { + if (count > 1 && c != QLatin1Char('&')) + parsedContent.append(QLatin1Char(' ')); + else if (c == QLatin1Char('&')) { + // Note: this will modify the counter j, in case we sucessful parsed the entity + // we will have modified the counter to stay 1 before the closing ';', so + // the following if condition will be met with if (c == QLatin1Char(';')) + parsedContent.append(parseEntity(length, buf, j)); + } + + count = 0; + valid = false; + continue; + } + if ((c == QLatin1Char('>') || c == QLatin1Char(';')) && !valid) { + valid = true; + continue; + } + if (!valid) + continue; + + if (c.isLetterOrNumber() || c.isPrint()) { + ++count; + parsedContent.append(c.toLower()); + } else { + if (count > 1) + parsedContent.append(QLatin1Char(' ')); + count = 0; + } + } + + return parsedContent; + } + + // taken from qtexthtmlparser + // parses an entity after "&", and returns it + QString parseEntity(int len, const QChar *buf, int &pos) const + { + int recover = pos; + QString entity; + while (pos < len) { + QChar c = buf[pos++]; + if (c.isSpace() || pos - recover > 9) { + goto error; + } + if (c == QLatin1Char(';')) { + pos--; + break; + } + entity += c; + } + { + QChar resolved = resolveEntity(entity); + if (!resolved.isNull()) + return QString(resolved); + } + if (entity.length() > 1 && entity.at(0) == QLatin1Char('#')) { + entity.remove(0, 1); // removing leading # + + int base = 10; + bool ok = false; + + if (entity.at(0).toLower() == QLatin1Char('x')) { // hex entity? + entity.remove(0, 1); + base = 16; + } + + uint uc = entity.toUInt(&ok, base); + if (ok) { + if (uc >= 0x80 && uc < 0x80 + (sizeof(latin1Extended) / sizeof(latin1Extended[0]))) + uc = latin1Extended[uc - 0x80]; // windows latin 1 extended + QString str; + if (uc > 0xffff) { + // surrogate pair + uc -= 0x10000; + ushort high = uc/0x400 + 0xd800; + ushort low = uc%0x400 + 0xdc00; + str.append(QChar(high)); + str.append(QChar(low)); + } else { + str.append(QChar(uc)); + } + return str; + } + } + error: + pos = recover; + return QLatin1String(" "); + } + // end taken from qtexthtmlparser + +private: + QString fileName; + QString data; +}; + + +QHelpSearchIndexWriter::QHelpSearchIndexWriter() + : QThread(0) + , m_cancel(false) +{ + // nothing todo +} + +QHelpSearchIndexWriter::~QHelpSearchIndexWriter() +{ + mutex.lock(); + this->m_cancel = true; + waitCondition.wakeOne(); + mutex.unlock(); + + wait(); +} + +void QHelpSearchIndexWriter::cancelIndexing() +{ + mutex.lock(); + this->m_cancel = true; + mutex.unlock(); +} + +void QHelpSearchIndexWriter::updateIndex(const QString &collectionFile, + const QString &indexFilesFolder, bool reindex) +{ + wait(); + mutex.lock(); + this->m_cancel = false; + this->m_reindex = reindex; + this->m_collectionFile = collectionFile; + this->m_indexFilesFolder = indexFilesFolder; + mutex.unlock(); + + start(QThread::LowestPriority); +} + +void QHelpSearchIndexWriter::optimizeIndex() +{ +#if !defined(QT_NO_EXCEPTIONS) + try { +#endif + if (QCLuceneIndexReader::indexExists(m_indexFilesFolder)) { + if (QCLuceneIndexReader::isLocked(m_indexFilesFolder)) + return; + + QCLuceneStandardAnalyzer analyzer; + QCLuceneIndexWriter writer(m_indexFilesFolder, analyzer, false); + writer.optimize(); + writer.close(); + } +#if !defined(QT_NO_EXCEPTIONS) + } catch (...) { + qWarning("Full Text Search, could not optimize index."); + return; + } +#endif +} + +void QHelpSearchIndexWriter::run() +{ +#if !defined(QT_NO_EXCEPTIONS) + try { +#endif + QMutexLocker mutexLocker(&mutex); + + if (m_cancel) + return; + + const bool reindex = this->m_reindex; + const QString collectionFile(this->m_collectionFile); + + mutexLocker.unlock(); + + QHelpEngineCore engine(collectionFile, 0); + if (!engine.setupData()) + return; + + const QLatin1String key("CluceneIndexedNamespaces"); + if (reindex) + engine.setCustomValue(key, QLatin1String("")); + + QMap<QString, QDateTime> indexMap; + const QLatin1String oldKey("CluceneSearchNamespaces"); + if (!engine.customValue(oldKey, QString()).isNull()) { + // old style qhc file < 4.4.2, need to convert... + const QStringList indexedNamespaces + = engine.customValue(oldKey).toString() + .split(QLatin1String("|"), QString::SkipEmptyParts); + foreach (const QString &nameSpace, indexedNamespaces) + indexMap.insert(nameSpace, QDateTime()); + engine.removeCustomValue(oldKey); + } else { + QDataStream dataStream(engine.customValue(key).toByteArray()); + dataStream >> indexMap; + } + + QString indexPath = m_indexFilesFolder; + + QFileInfo fInfo(indexPath); + if (fInfo.exists() && !fInfo.isWritable()) { + qWarning("Full Text Search, could not create index (missing permissions for '%s').", + qPrintable(indexPath)); + return; + } + + emit indexingStarted(); + + QCLuceneIndexWriter *writer = 0; + QCLuceneStandardAnalyzer analyzer; + const QStringList registeredDocs = engine.registeredDocumentations(); + + QLocalSocket localSocket; + localSocket.connectToServer(QString(QLatin1String("QtAssistant%1")) + .arg(QLatin1String(QT_VERSION_STR))); + + QLocalServer localServer; + bool otherInstancesRunning = true; + if (!localSocket.waitForConnected()) { + otherInstancesRunning = false; + localServer.listen(QString(QLatin1String("QtAssistant%1")) + .arg(QLatin1String(QT_VERSION_STR))); + } + + // check if it's locked, and if the other instance is running + if (!otherInstancesRunning && QCLuceneIndexReader::isLocked(indexPath)) + QCLuceneIndexReader::unlock(indexPath); + + if (QCLuceneIndexReader::isLocked(indexPath)) { + // poll unless indexing finished to fake progress + while (QCLuceneIndexReader::isLocked(indexPath)) { + mutexLocker.relock(); + if (m_cancel) + break; + mutexLocker.unlock(); + this->sleep(1); + } + emit indexingFinished(); + return; + } + + if (QCLuceneIndexReader::indexExists(indexPath) && !reindex) { + foreach(const QString &namespaceName, registeredDocs) { + mutexLocker.relock(); + if (m_cancel) { + emit indexingFinished(); + return; + } + mutexLocker.unlock(); + + if (!indexMap.contains(namespaceName)) { + // make sure we remove some partly indexed stuff + removeDocuments(indexPath, namespaceName); + } else { + QString path = engine.documentationFileName(namespaceName); + if (indexMap.value(namespaceName) + < QFileInfo(path).lastModified()) { + // make sure we remove some outdated indexed stuff + indexMap.remove(namespaceName); + removeDocuments(indexPath, namespaceName); + } + + if (indexMap.contains(namespaceName)) { + // make sure we really have content indexed for namespace + QCLuceneTermQuery query(QCLuceneTerm(NamespaceField, namespaceName)); + QCLuceneIndexSearcher indexSearcher(indexPath); + QCLuceneHits hits = indexSearcher.search(query); + if (hits.length() <= 0) + indexMap.remove(namespaceName); + } + } + } + writer = new QCLuceneIndexWriter(indexPath, analyzer, false); + } else { + indexMap.clear(); + writer = new QCLuceneIndexWriter(indexPath, analyzer, true); + } + + writer->setMergeFactor(100); + writer->setMinMergeDocs(1000); + writer->setMaxFieldLength(QCLuceneIndexWriter::DEFAULT_MAX_FIELD_LENGTH); + + QStringList namespaces; + foreach(const QString &namespaceName, registeredDocs) { + mutexLocker.relock(); + if (m_cancel) { + closeIndexWriter(writer); + emit indexingFinished(); + return; + } + mutexLocker.unlock(); + + namespaces.append(namespaceName); + if (indexMap.contains(namespaceName)) + continue; + + const QList<QStringList> attributeSets = + engine.filterAttributeSets(namespaceName); + + if (attributeSets.isEmpty()) { + const QList<QUrl> docFiles = indexableFiles(&engine, namespaceName, + QStringList()); + if (!addDocuments(docFiles, engine, QStringList(), namespaceName, + writer, analyzer)) + break; + } else { + bool bail = false; + foreach (const QStringList &attributes, attributeSets) { + const QList<QUrl> docFiles = indexableFiles(&engine, + namespaceName, attributes); + if (!addDocuments(docFiles, engine, attributes, namespaceName, + writer, analyzer)) { + bail = true; + break; + } + } + if (bail) + break; + } + + mutexLocker.relock(); + if (!m_cancel) { + QString path(engine.documentationFileName(namespaceName)); + indexMap.insert(namespaceName, QFileInfo(path).lastModified()); + writeIndexMap(engine, indexMap); + } + mutexLocker.unlock(); + } + + closeIndexWriter(writer); + + mutexLocker.relock(); + if (!m_cancel) { + mutexLocker.unlock(); + + QStringList indexedNamespaces = indexMap.keys(); + foreach(const QString &namespaceName, indexedNamespaces) { + mutexLocker.relock(); + if (m_cancel) + break; + mutexLocker.unlock(); + + if (!namespaces.contains(namespaceName)) { + indexMap.remove(namespaceName); + writeIndexMap(engine, indexMap); + removeDocuments(indexPath, namespaceName); + } + } + } + +#if !defined(QT_NO_EXCEPTIONS) + } catch (...) { + qWarning("%s: Failed because of CLucene exception.", Q_FUNC_INFO); + } +#endif + + emit indexingFinished(); +} + +bool QHelpSearchIndexWriter::addDocuments(const QList<QUrl> docFiles, + const QHelpEngineCore &engine, const QStringList &attributes, + const QString &namespaceName, QCLuceneIndexWriter *writer, + QCLuceneAnalyzer &analyzer) +{ + QMutexLocker locker(&mutex); + const QString attrList = attributes.join(QLatin1String(" ")); + + locker.unlock(); + foreach(const QUrl &url, docFiles) { + QCLuceneDocument document; + DocumentHelper helper(url.toString(), engine.fileData(url)); + if (helper.addFieldsToDocument(&document, namespaceName, attrList)) { +#if !defined(QT_NO_EXCEPTIONS) + try { +#endif + writer->addDocument(document, analyzer); +#if !defined(QT_NO_EXCEPTIONS) + } catch (...) { + qWarning("Full Text Search, could not properly add documents."); + return false; + } +#endif + } + locker.relock(); + if (m_cancel) + return false; + locker.unlock(); + } + return true; +} + +void QHelpSearchIndexWriter::removeDocuments(const QString &indexPath, + const QString &namespaceName) +{ + if (namespaceName.isEmpty() || QCLuceneIndexReader::isLocked(indexPath)) + return; + + QCLuceneIndexReader reader = QCLuceneIndexReader::open(indexPath); + reader.deleteDocuments(QCLuceneTerm(NamespaceField, namespaceName)); + + reader.close(); +} + +bool QHelpSearchIndexWriter::writeIndexMap(QHelpEngineCore &engine, + const QMap<QString, QDateTime> &indexMap) +{ + QByteArray bArray; + + QDataStream data(&bArray, QIODevice::ReadWrite); + data << indexMap; + + return engine.setCustomValue(QLatin1String("CluceneIndexedNamespaces"), + bArray); +} + +QList<QUrl> QHelpSearchIndexWriter::indexableFiles(QHelpEngineCore *helpEngine, + const QString &namespaceName, const QStringList &attributes) const +{ + QList<QUrl> docFiles = helpEngine->files(namespaceName, attributes, + QLatin1String("html")); + docFiles += helpEngine->files(namespaceName, attributes, QLatin1String("htm")); + docFiles += helpEngine->files(namespaceName, attributes, QLatin1String("txt")); + + return docFiles; +} + +void QHelpSearchIndexWriter::closeIndexWriter(QCLuceneIndexWriter *writer) +{ +#if !defined(QT_NO_EXCEPTIONS) + try { +#endif + writer->close(); + delete writer; +#if !defined(QT_NO_EXCEPTIONS) + } catch (...) { + qWarning("Full Text Search, could not properly close index writer."); + } +#endif +} + +} // namespace clucene +} // namespace fulltextsearch + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchindexwriter_clucene_p.h b/src/assistant/help/qhelpsearchindexwriter_clucene_p.h new file mode 100644 index 000000000..7c9a91468 --- /dev/null +++ b/src/assistant/help/qhelpsearchindexwriter_clucene_p.h @@ -0,0 +1,124 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHINDEXWRITERCLUCENE_H +#define QHELPSEARCHINDEXWRITERCLUCENE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include "qhelpenginecore.h" +#include "private/qanalyzer_p.h" + +#include <QtCore/QUrl> +#include <QtCore/QThread> +#include <QtCore/QMutex> +#include <QtCore/QObject> +#include <QtCore/QString> +#include <QtCore/QDateTime> +#include <QtCore/QStringList> +#include <QtCore/QWaitCondition> + +QT_BEGIN_NAMESPACE + +class QCLuceneIndexWriter; + +namespace fulltextsearch { +namespace clucene { + +class QHelpSearchIndexWriter : public QThread +{ + Q_OBJECT + +public: + QHelpSearchIndexWriter(); + ~QHelpSearchIndexWriter(); + + void cancelIndexing(); + void updateIndex(const QString &collectionFile, + const QString &indexFilesFolder, bool reindex); + void optimizeIndex(); + +signals: + void indexingStarted(); + void indexingFinished(); + +private: + void run(); + + bool addDocuments(const QList<QUrl> docFiles, const QHelpEngineCore &engine, + const QStringList &attributes, const QString &namespaceName, + QCLuceneIndexWriter *writer, QCLuceneAnalyzer &analyzer); + void removeDocuments(const QString &indexPath, const QString &namespaceName); + + bool writeIndexMap(QHelpEngineCore& engine, + const QMap<QString, QDateTime>& indexMap); + + QList<QUrl> indexableFiles(QHelpEngineCore *helpEngine, + const QString &namespaceName, const QStringList &attributes) const; + + void closeIndexWriter(QCLuceneIndexWriter *writer); + +private: + QMutex mutex; + QWaitCondition waitCondition; + + bool m_cancel; + bool m_reindex; + QString m_collectionFile; + QString m_indexFilesFolder; +}; + +} // namespace clucene +} // namespace fulltextsearch + + +QT_END_NAMESPACE + +#endif // QHELPSEARCHINDEXWRITERCLUCENE_H diff --git a/src/assistant/help/qhelpsearchindexwriter_default.cpp b/src/assistant/help/qhelpsearchindexwriter_default.cpp new file mode 100644 index 000000000..db7f6ba0d --- /dev/null +++ b/src/assistant/help/qhelpsearchindexwriter_default.cpp @@ -0,0 +1,384 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpsearchindexwriter_default_p.h" +#include "qhelp_global.h" +#include "qhelpenginecore.h" + +#include <QtCore/QDir> +#include <QtCore/QSet> +#include <QtCore/QUrl> +#include <QtCore/QFile> +#include <QtCore/QRegExp> +#include <QtCore/QVariant> +#include <QtCore/QFileInfo> +#include <QtCore/QTextCodec> +#include <QtCore/QTextStream> + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace std { + +Writer::Writer(const QString &path) + : indexPath(path) + , indexFile(QString()) + , documentFile(QString()) +{ + // nothing todo +} + +Writer::~Writer() +{ + reset(); +} + +void Writer::reset() +{ + for(QHash<QString, Entry*>::ConstIterator it = + index.begin(); it != index.end(); ++it) { + delete it.value(); + } + + index.clear(); + documentList.clear(); +} + +bool Writer::writeIndex() const +{ + bool status; + QFile idxFile(indexFile); + if (!(status = idxFile.open(QFile::WriteOnly))) + return status; + + QDataStream indexStream(&idxFile); + for(QHash<QString, Entry*>::ConstIterator it = + index.begin(); it != index.end(); ++it) { + indexStream << it.key(); + indexStream << it.value()->documents.count(); + indexStream << it.value()->documents; + } + idxFile.close(); + + QFile docFile(documentFile); + if (!(status = docFile.open(QFile::WriteOnly))) + return status; + + QDataStream docStream(&docFile); + foreach(const QStringList &list, documentList) { + docStream << list.at(0); + docStream << list.at(1); + } + docFile.close(); + + return status; +} + +void Writer::removeIndex() const +{ + QFile idxFile(indexFile); + if (idxFile.exists()) + idxFile.remove(); + + QFile docFile(documentFile); + if (docFile.exists()) + docFile.remove(); +} + +void Writer::setIndexFile(const QString &namespaceName, const QString &attributes) +{ + QString extension = namespaceName + QLatin1String("@") + attributes; + indexFile = indexPath + QLatin1String("/indexdb40.") + extension; + documentFile = indexPath + QLatin1String("/indexdoc40.") + extension; +} + +void Writer::insertInIndex(const QString &string, int docNum) +{ + if (string == QLatin1String("amp") || string == QLatin1String("nbsp")) + return; + + Entry *entry = 0; + if (index.count()) + entry = index[string]; + + if (entry) { + if (entry->documents.last().docNumber != docNum) + entry->documents.append(Document(docNum, 1)); + else + entry->documents.last().frequency++; + } else { + index.insert(string, new Entry(docNum)); + } +} + +void Writer::insertInDocumentList(const QString &title, const QString &url) +{ + documentList.append(QStringList(title) << url); +} + + +QHelpSearchIndexWriter::QHelpSearchIndexWriter() + : QThread() + , m_cancel(false) +{ + // nothing todo +} + +QHelpSearchIndexWriter::~QHelpSearchIndexWriter() +{ + mutex.lock(); + this->m_cancel = true; + waitCondition.wakeOne(); + mutex.unlock(); + + wait(); +} + +void QHelpSearchIndexWriter::cancelIndexing() +{ + mutex.lock(); + this->m_cancel = true; + mutex.unlock(); +} + +void QHelpSearchIndexWriter::updateIndex(const QString &collectionFile, + const QString &indexFilesFolder, + bool reindex) +{ + wait(); + QMutexLocker lock(&mutex); + + this->m_cancel = false; + this->m_reindex = reindex; + this->m_collectionFile = collectionFile; + this->m_indexFilesFolder = indexFilesFolder; + + start(QThread::LowestPriority); +} + +void QHelpSearchIndexWriter::run() +{ + mutex.lock(); + + if (m_cancel) { + mutex.unlock(); + return; + } + + const bool reindex(this->m_reindex); + const QLatin1String key("DefaultSearchNamespaces"); + const QString collectionFile(this->m_collectionFile); + const QString indexPath = m_indexFilesFolder; + + mutex.unlock(); + + QHelpEngineCore engine(collectionFile, 0); + if (!engine.setupData()) + return; + + if (reindex) + engine.setCustomValue(key, QLatin1String("")); + + const QStringList registeredDocs = engine.registeredDocumentations(); + const QStringList indexedNamespaces = engine.customValue(key).toString(). + split(QLatin1String("|"), QString::SkipEmptyParts); + + emit indexingStarted(); + + QStringList namespaces; + Writer writer(indexPath); + foreach(const QString &namespaceName, registeredDocs) { + mutex.lock(); + if (m_cancel) { + mutex.unlock(); + return; + } + mutex.unlock(); + + // if indexed, continue + namespaces.append(namespaceName); + if (indexedNamespaces.contains(namespaceName)) + continue; + + const QList<QStringList> attributeSets = + engine.filterAttributeSets(namespaceName); + + foreach (const QStringList &attributes, attributeSets) { + // cleanup maybe old or unfinished files + writer.setIndexFile(namespaceName, attributes.join(QLatin1String("@"))); + writer.removeIndex(); + + QSet<QString> documentsSet; + const QList<QUrl> docFiles = engine.files(namespaceName, attributes); + foreach(QUrl url, docFiles) { + if (m_cancel) + return; + + // get rid of duplicated files + if (url.hasFragment()) + url.setFragment(QString()); + + QString s = url.toString(); + if (s.endsWith(QLatin1String(".html")) + || s.endsWith(QLatin1String(".htm")) + || s.endsWith(QLatin1String(".txt"))) + documentsSet.insert(s); + } + + int docNum = 0; + const QStringList documentsList(documentsSet.toList()); + foreach(const QString &url, documentsList) { + if (m_cancel) + return; + + QByteArray data(engine.fileData(url)); + if (data.isEmpty()) + continue; + + QTextStream s(data); + QString en = QHelpGlobal::codecFromData(data); + s.setCodec(QTextCodec::codecForName(en.toLatin1().constData())); + + QString text = s.readAll(); + if (text.isNull()) + continue; + + QString title = QHelpGlobal::documentTitle(text); + + int j = 0; + int i = 0; + bool valid = true; + const QChar *buf = text.unicode(); + QChar str[64]; + QChar c = buf[0]; + + while ( j < text.length() ) { + if (m_cancel) + return; + + if ( c == QLatin1Char('<') || c == QLatin1Char('&') ) { + valid = false; + if ( i > 1 ) + writer.insertInIndex(QString(str,i), docNum); + i = 0; + c = buf[++j]; + continue; + } + if ( ( c == QLatin1Char('>') || c == QLatin1Char(';') ) && !valid ) { + valid = true; + c = buf[++j]; + continue; + } + if ( !valid ) { + c = buf[++j]; + continue; + } + if ( ( c.isLetterOrNumber() || c == QLatin1Char('_') ) && i < 63 ) { + str[i] = c.toLower(); + ++i; + } else { + if ( i > 1 ) + writer.insertInIndex(QString(str,i), docNum); + i = 0; + } + c = buf[++j]; + } + if ( i > 1 ) + writer.insertInIndex(QString(str,i), docNum); + + docNum++; + writer.insertInDocumentList(title, url); + } + + if (writer.writeIndex()) { + engine.setCustomValue(key, addNamespace( + engine.customValue(key).toString(), namespaceName)); + } + + writer.reset(); + } + } + + QStringListIterator qsli(indexedNamespaces); + while (qsli.hasNext()) { + const QString namespaceName = qsli.next(); + if (namespaces.contains(namespaceName)) + continue; + + const QList<QStringList> attributeSets = + engine.filterAttributeSets(namespaceName); + + foreach (const QStringList &attributes, attributeSets) { + writer.setIndexFile(namespaceName, attributes.join(QLatin1String("@"))); + writer.removeIndex(); + } + + engine.setCustomValue(key, removeNamespace( + engine.customValue(key).toString(), namespaceName)); + } + + emit indexingFinished(); +} + +QString QHelpSearchIndexWriter::addNamespace(const QString namespaces, + const QString &namespaceName) +{ + QString value = namespaces; + if (!value.contains(namespaceName)) + value.append(namespaceName).append(QLatin1String("|")); + + return value; +} + +QString QHelpSearchIndexWriter::removeNamespace(const QString namespaces, + const QString &namespaceName) +{ + QString value = namespaces; + if (value.contains(namespaceName)) + value.remove(namespaceName + QLatin1String("|")); + + return value; +} + +} // namespace std +} // namespace fulltextsearch + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchindexwriter_default_p.h b/src/assistant/help/qhelpsearchindexwriter_default_p.h new file mode 100644 index 000000000..d510fbc9d --- /dev/null +++ b/src/assistant/help/qhelpsearchindexwriter_default_p.h @@ -0,0 +1,130 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHINDEXWRITERDEFAULT_H +#define QHELPSEARCHINDEXWRITERDEFAULT_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the help generator tools. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include "qhelpsearchindex_default_p.h" + +#include <QtCore/QHash> +#include <QtCore/QMutex> +#include <QtCore/QObject> +#include <QtCore/QString> +#include <QtCore/QThread> +#include <QtCore/QStringList> +#include <QtCore/QWaitCondition> + +QT_BEGIN_NAMESPACE + +namespace fulltextsearch { +namespace std { + +class Writer +{ +public: + Writer(const QString &path); + ~Writer(); + + void reset(); + bool writeIndex() const; + void removeIndex() const; + void setIndexFile(const QString &namespaceName, const QString &attributes); + void insertInIndex(const QString &string, int docNum); + void insertInDocumentList(const QString &title, const QString &url); + +private: + QString indexPath; + QString indexFile; + QString documentFile; + + QHash<QString, Entry*> index; + QList<QStringList> documentList; +}; + + +class QHelpSearchIndexWriter : public QThread +{ + Q_OBJECT + +public: + QHelpSearchIndexWriter(); + ~QHelpSearchIndexWriter(); + + void cancelIndexing(); + void updateIndex(const QString &collectionFile, + const QString &indexFilesFolder, bool reindex); + +signals: + void indexingStarted(); + void indexingFinished(); + +private: + void run(); + QString addNamespace(const QString namespaces, const QString &namespaceName); + QString removeNamespace(const QString namespaces, const QString &namespaceName); + +private: + QMutex mutex; + QWaitCondition waitCondition; + + bool m_cancel; + bool m_reindex; + QString m_collectionFile; + QString m_indexFilesFolder; +}; + +} // namespace std +} // namespace fulltextsearch + +QT_END_NAMESPACE + +#endif // QHELPSEARCHINDEXWRITERDEFAULT_H diff --git a/src/assistant/help/qhelpsearchquerywidget.cpp b/src/assistant/help/qhelpsearchquerywidget.cpp new file mode 100644 index 000000000..55974ada5 --- /dev/null +++ b/src/assistant/help/qhelpsearchquerywidget.cpp @@ -0,0 +1,587 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpsearchquerywidget.h" + +#include <QtCore/QAbstractListModel> +#include <QtCore/QObject> +#include <QtCore/QStringList> +#include <QtCore/QtGlobal> + +#include <QtWidgets/QCompleter> +#include <QtWidgets/QLabel> +#include <QtWidgets/QLayout> +#include <QtWidgets/QLineEdit> +#include <QtGui/QFocusEvent> +#include <QtWidgets/QPushButton> +#include <QtWidgets/QToolButton> + +QT_BEGIN_NAMESPACE + +class QHelpSearchQueryWidgetPrivate : public QObject +{ + Q_OBJECT + +private: + struct QueryHistory { + explicit QueryHistory() : curQuery(-1) {} + QList<QList<QHelpSearchQuery> > queries; + int curQuery; + }; + + class CompleterModel : public QAbstractListModel + { + public: + explicit CompleterModel(QObject *parent) + : QAbstractListModel(parent) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return parent.isValid() ? 0 : termList.size(); + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + if (!index.isValid() || index.row() >= termList.count()|| + (role != Qt::EditRole && role != Qt::DisplayRole)) + return QVariant(); + return termList.at(index.row()); + } + + void addTerm(const QString &term) + { + if (!termList.contains(term)) { + termList.append(term); + reset(); + } + } + + private: + QStringList termList; + }; + + QHelpSearchQueryWidgetPrivate() + : QObject() + , simpleSearch(true) + , searchCompleter(new CompleterModel(this), this) + { + searchButton = 0; + advancedSearchWidget = 0; + showHideAdvancedSearchButton = 0; + defaultQuery = 0; + exactQuery = 0; + similarQuery = 0; + withoutQuery = 0; + allQuery = 0; + atLeastQuery = 0; + } + + ~QHelpSearchQueryWidgetPrivate() + { + // nothing todo + } + + void retranslate() + { + simpleSearchLabel->setText(QHelpSearchQueryWidget::tr("Search for:")); + prevQueryButton->setToolTip(QHelpSearchQueryWidget::tr("Previous search")); + nextQueryButton->setToolTip(QHelpSearchQueryWidget::tr("Next search")); + searchButton->setText(QHelpSearchQueryWidget::tr("Search")); +#ifdef QT_CLUCENE_SUPPORT + advancedSearchLabel->setText(QHelpSearchQueryWidget::tr("Advanced search")); + similarLabel->setText(QHelpSearchQueryWidget::tr("words <B>similar</B> to:")); + withoutLabel->setText(QHelpSearchQueryWidget::tr("<B>without</B> the words:")); + exactLabel->setText(QHelpSearchQueryWidget::tr("with <B>exact phrase</B>:")); + allLabel->setText(QHelpSearchQueryWidget::tr("with <B>all</B> of the words:")); + atLeastLabel->setText(QHelpSearchQueryWidget::tr("with <B>at least one</B> of the words:")); +#endif + } + + QStringList buildTermList(const QString query) + { + bool s = false; + QString phrase; + QStringList wordList; + QString searchTerm = query; + + for (int i = 0; i < searchTerm.length(); ++i) { + if (searchTerm[i] == QLatin1Char('\"') && !s) { + s = true; + phrase = searchTerm[i]; + continue; + } + if (searchTerm[i] != QLatin1Char('\"') && s) + phrase += searchTerm[i]; + if (searchTerm[i] == QLatin1Char('\"') && s) { + s = false; + phrase += searchTerm[i]; + wordList.append(phrase); + searchTerm.remove(phrase); + } + } + if (s) + searchTerm.replace(phrase, phrase.mid(1)); + + const QRegExp exp(QLatin1String("\\s+")); + wordList += searchTerm.split(exp, QString::SkipEmptyParts); + return wordList; + } + + void saveQuery(const QList<QHelpSearchQuery> &query, QueryHistory &queryHist) + { + // We only add the query to the list if it is different from the last one. + bool insert = false; + if (queryHist.queries.empty()) + insert = true; + else { + const QList<QHelpSearchQuery> &lastQuery = queryHist.queries.last(); + if (lastQuery.size() != query.size()) { + insert = true; + } else { + for (int i = 0; i < query.size(); ++i) { + if (query.at(i).fieldName != lastQuery.at(i).fieldName + || query.at(i).wordList != lastQuery.at(i).wordList) { + insert = true; + break; + } + } + } + } + if (insert) { + queryHist.queries.append(query); + foreach (const QHelpSearchQuery &queryPart, query) { + static_cast<CompleterModel *>(searchCompleter.model())-> + addTerm(queryPart.wordList.join(" ")); + } + } + } + + void nextOrPrevQuery(int maxOrMinIndex, int addend, QToolButton *thisButton, + QToolButton *otherButton) + { + QueryHistory *queryHist; + QList<QLineEdit *> lineEdits; + if (simpleSearch) { + queryHist = &simpleQueries; + lineEdits << defaultQuery; + } else { + queryHist = &complexQueries; + lineEdits << allQuery << atLeastQuery << similarQuery + << withoutQuery << exactQuery; + } + foreach (QLineEdit *lineEdit, lineEdits) + lineEdit->clear(); + + // Otherwise, the respective button would be disabled. + Q_ASSERT(queryHist->curQuery != maxOrMinIndex); + + queryHist->curQuery += addend; + const QList<QHelpSearchQuery> &query = + queryHist->queries.at(queryHist->curQuery); + foreach (const QHelpSearchQuery &queryPart, query) { + if (QLineEdit *lineEdit = lineEditFor(queryPart.fieldName)) + lineEdit->setText(queryPart.wordList.join(" ")); + } + + if (queryHist->curQuery == maxOrMinIndex) + thisButton->setEnabled(false); + otherButton->setEnabled(true); + } + + QLineEdit* lineEditFor(const QHelpSearchQuery::FieldName &fieldName) const + { + switch (fieldName) { + case QHelpSearchQuery::DEFAULT: + return defaultQuery; + case QHelpSearchQuery::ALL: + return allQuery; + case QHelpSearchQuery::ATLEAST: + return atLeastQuery; + case QHelpSearchQuery::FUZZY: + return similarQuery; + case QHelpSearchQuery::WITHOUT: + return withoutQuery; + case QHelpSearchQuery::PHRASE: + return exactQuery; + default: + Q_ASSERT(0); + } + return 0; + } + + void enableOrDisableToolButtons() + { + const QueryHistory &queryHist = simpleSearch ? simpleQueries + : complexQueries; + prevQueryButton->setEnabled(queryHist.curQuery > 0); + nextQueryButton->setEnabled(queryHist.curQuery + < queryHist.queries.size() - 1); + } + +private slots: + void showHideAdvancedSearch() + { + if (simpleSearch) { + advancedSearchWidget->show(); + showHideAdvancedSearchButton->setText((QLatin1String("-"))); + } else { + advancedSearchWidget->hide(); + showHideAdvancedSearchButton->setText((QLatin1String("+"))); + } + + simpleSearch = !simpleSearch; + defaultQuery->setEnabled(simpleSearch); + enableOrDisableToolButtons(); + } + + void searchRequested() + { + QList<QHelpSearchQuery> queryList; +#if !defined(QT_CLUCENE_SUPPORT) + queryList.append(QHelpSearchQuery(QHelpSearchQuery::DEFAULT, + QStringList(defaultQuery->text()))); + +#else + if (defaultQuery->isEnabled()) { + queryList.append(QHelpSearchQuery(QHelpSearchQuery::DEFAULT, + buildTermList(defaultQuery->text()))); + } else { + const QRegExp exp(QLatin1String("\\s+")); + QStringList lst = similarQuery->text().split(exp, + QString::SkipEmptyParts); + if (!lst.isEmpty()) { + QStringList fuzzy; + foreach (const QString &term, lst) + fuzzy += buildTermList(term); + queryList.append(QHelpSearchQuery(QHelpSearchQuery::FUZZY, + fuzzy)); + } + + lst = withoutQuery->text().split(exp, QString::SkipEmptyParts); + if (!lst.isEmpty()) { + QStringList without; + foreach (const QString &term, lst) + without.append(term); + queryList.append(QHelpSearchQuery(QHelpSearchQuery::WITHOUT, + without)); + } + + if (!exactQuery->text().isEmpty()) { + QString phrase = exactQuery->text().remove(QLatin1Char('\"')); + phrase = phrase.simplified(); + queryList.append(QHelpSearchQuery(QHelpSearchQuery::PHRASE, + QStringList(phrase))); + } + + lst = allQuery->text().split(exp, QString::SkipEmptyParts); + if (!lst.isEmpty()) { + QStringList all; + foreach (const QString &term, lst) + all.append(term); + queryList.append(QHelpSearchQuery(QHelpSearchQuery::ALL, all)); + } + + lst = atLeastQuery->text().split(exp, QString::SkipEmptyParts); + if (!lst.isEmpty()) { + QStringList atLeast; + foreach (const QString &term, lst) + atLeast += buildTermList(term); + queryList.append(QHelpSearchQuery(QHelpSearchQuery::ATLEAST, + atLeast)); + } + } +#endif + QueryHistory &queryHist = simpleSearch ? simpleQueries : complexQueries; + saveQuery(queryList, queryHist); + queryHist.curQuery = queryHist.queries.size() - 1; + if (queryHist.curQuery > 0) + prevQueryButton->setEnabled(true); + nextQueryButton->setEnabled(false); + } + + void nextQuery() + { + nextOrPrevQuery((simpleSearch ? simpleQueries + : complexQueries).queries.size() - 1, 1, nextQueryButton, + prevQueryButton); + } + + void prevQuery() + { + nextOrPrevQuery(0, -1, prevQueryButton, nextQueryButton); + } + +private: + friend class QHelpSearchQueryWidget; + + bool simpleSearch; + QLabel *simpleSearchLabel; + QLabel *advancedSearchLabel; + QLabel *similarLabel; + QLabel *withoutLabel; + QLabel *exactLabel; + QLabel *allLabel; + QLabel *atLeastLabel; + QPushButton *searchButton; + QWidget* advancedSearchWidget; + QToolButton *showHideAdvancedSearchButton; + QLineEdit *defaultQuery; + QLineEdit *exactQuery; + QLineEdit *similarQuery; + QLineEdit *withoutQuery; + QLineEdit *allQuery; + QLineEdit *atLeastQuery; + QToolButton *nextQueryButton; + QToolButton *prevQueryButton; + QueryHistory simpleQueries; + QueryHistory complexQueries; + QCompleter searchCompleter; +}; + +#include "qhelpsearchquerywidget.moc" + + +/*! + \class QHelpSearchQueryWidget + \since 4.4 + \inmodule QtHelp + \brief The QHelpSearchQueryWidget class provides a simple line edit or + an advanced widget to enable the user to input a search term in a + standardized input mask. +*/ + +/*! + \fn void QHelpSearchQueryWidget::search() + + This signal is emitted when a the user has the search button invoked. + After reciving the signal you can ask the QHelpSearchQueryWidget for the + build list of QHelpSearchQuery's that you may pass to the QHelpSearchEngine's + search() function. +*/ + +/*! + Constructs a new search query widget with the given \a parent. +*/ +QHelpSearchQueryWidget::QHelpSearchQueryWidget(QWidget *parent) + : QWidget(parent) +{ + d = new QHelpSearchQueryWidgetPrivate(); + + QVBoxLayout *vLayout = new QVBoxLayout(this); + vLayout->setMargin(0); + + QHBoxLayout* hBoxLayout = new QHBoxLayout(); + d->simpleSearchLabel = new QLabel(this); + d->defaultQuery = new QLineEdit(this); + d->defaultQuery->setCompleter(&d->searchCompleter); + d->prevQueryButton = new QToolButton(this); + d->prevQueryButton->setArrowType(Qt::LeftArrow); + d->prevQueryButton->setEnabled(false); + d->nextQueryButton = new QToolButton(this); + d->nextQueryButton->setArrowType(Qt::RightArrow); + d->nextQueryButton->setEnabled(false); + d->searchButton = new QPushButton(this); + hBoxLayout->addWidget(d->simpleSearchLabel); + hBoxLayout->addWidget(d->defaultQuery); + hBoxLayout->addWidget(d->prevQueryButton); + hBoxLayout->addWidget(d->nextQueryButton); + hBoxLayout->addWidget(d->searchButton); + + vLayout->addLayout(hBoxLayout); + + connect(d->prevQueryButton, SIGNAL(clicked()), d, SLOT(prevQuery())); + connect(d->nextQueryButton, SIGNAL(clicked()), d, SLOT(nextQuery())); + connect(d->searchButton, SIGNAL(clicked()), this, SIGNAL(search())); + connect(d->defaultQuery, SIGNAL(returnPressed()), this, SIGNAL(search())); + +#if defined(QT_CLUCENE_SUPPORT) + hBoxLayout = new QHBoxLayout(); + d->showHideAdvancedSearchButton = new QToolButton(this); + d->showHideAdvancedSearchButton->setText(QLatin1String("+")); + d->showHideAdvancedSearchButton->setMinimumSize(25, 20); + + d->advancedSearchLabel = new QLabel(this); + QSizePolicy sizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + sizePolicy.setHeightForWidth(d->advancedSearchLabel->sizePolicy().hasHeightForWidth()); + d->advancedSearchLabel->setSizePolicy(sizePolicy); + + QFrame* hLine = new QFrame(this); + hLine->setFrameStyle(QFrame::HLine); + hBoxLayout->addWidget(d->showHideAdvancedSearchButton); + hBoxLayout->addWidget(d->advancedSearchLabel); + hBoxLayout->addWidget(hLine); + + vLayout->addLayout(hBoxLayout); + + // setup advanced search layout + d->advancedSearchWidget = new QWidget(this); + QGridLayout *gLayout = new QGridLayout(d->advancedSearchWidget); + gLayout->setMargin(0); + + d->similarLabel = new QLabel(this); + gLayout->addWidget(d->similarLabel, 0, 0); + d->similarQuery = new QLineEdit(this); + d->similarQuery->setCompleter(&d->searchCompleter); + gLayout->addWidget(d->similarQuery, 0, 1); + + d->withoutLabel = new QLabel(this); + gLayout->addWidget(d->withoutLabel, 1, 0); + d->withoutQuery = new QLineEdit(this); + d->withoutQuery->setCompleter(&d->searchCompleter); + gLayout->addWidget(d->withoutQuery, 1, 1); + + d->exactLabel = new QLabel(this); + gLayout->addWidget(d->exactLabel, 2, 0); + d->exactQuery = new QLineEdit(this); + d->exactQuery->setCompleter(&d->searchCompleter); + gLayout->addWidget(d->exactQuery, 2, 1); + + d->allLabel = new QLabel(this); + gLayout->addWidget(d->allLabel, 3, 0); + d->allQuery = new QLineEdit(this); + d->allQuery->setCompleter(&d->searchCompleter); + gLayout->addWidget(d->allQuery, 3, 1); + + d->atLeastLabel = new QLabel(this); + gLayout->addWidget(d->atLeastLabel, 4, 0); + d->atLeastQuery = new QLineEdit(this); + d->atLeastQuery->setCompleter(&d->searchCompleter); + gLayout->addWidget(d->atLeastQuery, 4, 1); + + vLayout->addWidget(d->advancedSearchWidget); + d->advancedSearchWidget->hide(); + + d->retranslate(); + + connect(d->exactQuery, SIGNAL(returnPressed()), this, SIGNAL(search())); + connect(d->similarQuery, SIGNAL(returnPressed()), this, SIGNAL(search())); + connect(d->withoutQuery, SIGNAL(returnPressed()), this, SIGNAL(search())); + connect(d->allQuery, SIGNAL(returnPressed()), this, SIGNAL(search())); + connect(d->atLeastQuery, SIGNAL(returnPressed()), this, SIGNAL(search())); + connect(d->showHideAdvancedSearchButton, SIGNAL(clicked()), + d, SLOT(showHideAdvancedSearch())); +#endif + connect(this, SIGNAL(search()), d, SLOT(searchRequested())); +} + +/*! + Destroys the search query widget. +*/ +QHelpSearchQueryWidget::~QHelpSearchQueryWidget() +{ + delete d; +} + +/*! + Expands the search query widget so that the extended search fields are shown. +*/ +void QHelpSearchQueryWidget::expandExtendedSearch() +{ + if (d->simpleSearch) + d->showHideAdvancedSearch(); +} + +/*! + Collapses the search query widget so that only the default search field is + shown. +*/ +void QHelpSearchQueryWidget::collapseExtendedSearch() +{ + if (!d->simpleSearch) + d->showHideAdvancedSearch(); +} + +/*! + Returns a list of queries to use in combination with the search engines + search(QList<QHelpSearchQuery> &queryList) function. +*/ +QList<QHelpSearchQuery> QHelpSearchQueryWidget::query() const +{ + const QHelpSearchQueryWidgetPrivate::QueryHistory &queryHist = + d->simpleSearch ? d->simpleQueries : d->complexQueries; + return queryHist.queries.isEmpty() ? + QList<QHelpSearchQuery>() : queryHist.queries.last(); +} + +/*! + Sets the QHelpSearchQueryWidget input fields to the values specified by + \a queryList search field name. Please note that one has to call the search + engine's search(QList<QHelpSearchQuery> &queryList) function to perform the + actual search. +*/ +void QHelpSearchQueryWidget::setQuery(const QList<QHelpSearchQuery> &queryList) +{ + QList<QLineEdit *> lineEdits; + lineEdits << d->defaultQuery << d->allQuery << d->atLeastQuery + << d->similarQuery << d->withoutQuery << d->exactQuery; + foreach (QLineEdit *lineEdit, lineEdits) + lineEdit->clear(); + + const QLatin1String space(" "); + foreach (const QHelpSearchQuery &q, queryList) { + if (QLineEdit *lineEdit = d->lineEditFor(q.fieldName)) + lineEdit->setText(lineEdit->text() + q.wordList.join(space) + space); + } + d->searchRequested(); +} + +/*! + \reimp +*/ +void QHelpSearchQueryWidget::focusInEvent(QFocusEvent *focusEvent) +{ + if (focusEvent->reason() != Qt::MouseFocusReason) { + d->defaultQuery->selectAll(); + d->defaultQuery->setFocus(); + } +} + +/*! \reimp +*/ +void QHelpSearchQueryWidget::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::LanguageChange) + d->retranslate(); + else + QWidget::changeEvent(event); +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchquerywidget.h b/src/assistant/help/qhelpsearchquerywidget.h new file mode 100644 index 000000000..e438df0b5 --- /dev/null +++ b/src/assistant/help/qhelpsearchquerywidget.h @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHQUERYWIDGET_H +#define QHELPSEARCHQUERYWIDGET_H + +#include <QtHelp/qhelp_global.h> +#include <QtHelp/qhelpsearchengine.h> + +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> + +#include <QtWidgets/QWidget> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +class QFocusEvent; +class QHelpSearchQueryWidgetPrivate; + +class QHELP_EXPORT QHelpSearchQueryWidget : public QWidget +{ + Q_OBJECT + +public: + QHelpSearchQueryWidget(QWidget *parent = 0); + ~QHelpSearchQueryWidget(); + + void expandExtendedSearch(); + void collapseExtendedSearch(); + + QList<QHelpSearchQuery> query() const; + void setQuery(const QList<QHelpSearchQuery> &queryList); + +Q_SIGNALS: + void search(); + +private: + virtual void focusInEvent(QFocusEvent *focusEvent); + virtual void changeEvent(QEvent *event); + +private: + QHelpSearchQueryWidgetPrivate *d; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QHELPSEARCHQUERYWIDGET_H diff --git a/src/assistant/help/qhelpsearchresultwidget.cpp b/src/assistant/help/qhelpsearchresultwidget.cpp new file mode 100644 index 000000000..964185b5e --- /dev/null +++ b/src/assistant/help/qhelpsearchresultwidget.cpp @@ -0,0 +1,447 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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 "qhelpsearchresultwidget.h" + +#include <QtCore/QList> +#include <QtCore/QString> +#include <QtCore/QPointer> +#include <QtCore/QStringList> + +#include <QtWidgets/QLabel> +#include <QtWidgets/QLayout> +#include <QtGui/QMouseEvent> +#include <QtWidgets/QHeaderView> +#include <QtWidgets/QSpacerItem> +#include <QtWidgets/QToolButton> +#include <QtWidgets/QTreeWidget> +#include <QtWidgets/QTextBrowser> +#include <QtWidgets/QTreeWidgetItem> + +QT_BEGIN_NAMESPACE + +class QDefaultResultWidget : public QTreeWidget +{ + Q_OBJECT + +public: + QDefaultResultWidget(QWidget *parent = 0) + : QTreeWidget(parent) + { + header()->hide(); + connect(this, SIGNAL(itemActivated(QTreeWidgetItem*,int)), + this, SLOT(itemActivated(QTreeWidgetItem*,int))); + } + + void showResultPage(const QList<QHelpSearchEngine::SearchHit> hits) + { + foreach (const QHelpSearchEngine::SearchHit &hit, hits) + new QTreeWidgetItem(this, QStringList(hit.first) << hit.second); + } + +signals: + void requestShowLink(const QUrl &url); + +private slots: + void itemActivated(QTreeWidgetItem *item, int /* column */) + { + if (item) { + QString data = item->data(1, Qt::DisplayRole).toString(); + emit requestShowLink(data); + } + } +}; + + +class QCLuceneResultWidget : public QTextBrowser +{ + Q_OBJECT + +public: + QCLuceneResultWidget(QWidget *parent = 0) + : QTextBrowser(parent) + { + connect(this, SIGNAL(anchorClicked(QUrl)), + this, SIGNAL(requestShowLink(QUrl))); + setContextMenuPolicy(Qt::NoContextMenu); + } + + void showResultPage(const QList<QHelpSearchEngine::SearchHit> hits, bool isIndexing) + { + QString htmlFile = QString(QLatin1String("<html><head><title>%1</title></head><body>")) + .arg(tr("Search Results")); + + int count = hits.count(); + if (count != 0) { + if (isIndexing) + htmlFile += QString(QLatin1String("<div style=\"text-align:left; font-weight:bold; color:red\">" + "%1 <span style=\"font-weight:normal; color:black\">" + "%2</span></div></div><br>")).arg(tr("Note:")) + .arg(tr("The search results may not be complete since the " + "documentation is still being indexed!")); + + foreach (const QHelpSearchEngine::SearchHit &hit, hits) { + htmlFile += QString(QLatin1String("<div style=\"text-align:left; font-weight:bold\"" + "><a href=\"%1\">%2</a><div style=\"color:green; font-weight:normal;" + " margin:5px\">%1</div></div><p></p>")) + .arg(hit.first).arg(hit.second); + } + } else { + htmlFile += QLatin1String("<div align=\"center\"><br><br><h2>") + + tr("Your search did not match any documents.") + + QLatin1String("</h2><div>"); + if (isIndexing) + htmlFile += QLatin1String("<div align=\"center\"><h3>") + + tr("(The reason for this might be that the documentation " + "is still being indexed.)") + + QLatin1String("</h3><div>"); + } + + htmlFile += QLatin1String("</body></html>"); + + setHtml(htmlFile); + } + +signals: + void requestShowLink(const QUrl &url); + +private slots: + void setSource(const QUrl & /* name */) {} +}; + + +class QHelpSearchResultWidgetPrivate : public QObject +{ + Q_OBJECT + +private slots: + void setResults(int hitsCount) + { + if (!searchEngine.isNull()) { +#if defined(QT_CLUCENE_SUPPORT) + showFirstResultPage(); + updateNextButtonState(((hitsCount > 20) ? true : false)); +#else + resultTreeWidget->clear(); + resultTreeWidget->showResultPage(searchEngine->hits(0, hitsCount)); +#endif + } + } + + void showNextResultPage() + { + if (!searchEngine.isNull() + && resultLastToShow < searchEngine->hitCount()) { + resultLastToShow += 20; + resultFirstToShow += 20; + + resultTextBrowser->showResultPage(searchEngine->hits(resultFirstToShow, + resultLastToShow), isIndexing); + if (resultLastToShow >= searchEngine->hitCount()) + updateNextButtonState(false); + } + updateHitRange(); + } + + void showLastResultPage() + { + if (!searchEngine.isNull()) { + resultLastToShow = searchEngine->hitCount(); + resultFirstToShow = resultLastToShow - (resultLastToShow % 20); + + if (resultFirstToShow == resultLastToShow) + resultFirstToShow -= 20; + + resultTextBrowser->showResultPage(searchEngine->hits(resultFirstToShow, + resultLastToShow), isIndexing); + updateNextButtonState(false); + } + updateHitRange(); + } + + void showFirstResultPage() + { + if (!searchEngine.isNull()) { + resultLastToShow = 20; + resultFirstToShow = 0; + + resultTextBrowser->showResultPage(searchEngine->hits(resultFirstToShow, + resultLastToShow), isIndexing); + updatePrevButtonState(false); + } + updateHitRange(); + } + + void showPreviousResultPage() + { + if (!searchEngine.isNull()) { + int count = resultLastToShow % 20; + if (count == 0 || resultLastToShow != searchEngine->hitCount()) + count = 20; + + resultLastToShow -= count; + resultFirstToShow = resultLastToShow -20; + + resultTextBrowser->showResultPage(searchEngine->hits(resultFirstToShow, + resultLastToShow), isIndexing); + if (resultFirstToShow == 0) + updatePrevButtonState(false); + } + updateHitRange(); + } + + void updatePrevButtonState(bool state = true) + { + firstResultPage->setEnabled(state); + previousResultPage->setEnabled(state); + } + + void updateNextButtonState(bool state = true) + { + nextResultPage->setEnabled(state); + lastResultPage->setEnabled(state); + } + + void indexingStarted() + { + isIndexing = true; + } + + void indexingFinished() + { + isIndexing = false; + } + +private: + QHelpSearchResultWidgetPrivate(QHelpSearchEngine *engine) + : QObject() + , searchEngine(engine) + , isIndexing(false) + { + resultTreeWidget = 0; + resultTextBrowser = 0; + + resultLastToShow = 20; + resultFirstToShow = 0; + + firstResultPage = 0; + previousResultPage = 0; + hitsLabel = 0; + nextResultPage = 0; + lastResultPage = 0; + + connect(searchEngine, SIGNAL(indexingStarted()), + this, SLOT(indexingStarted())); + connect(searchEngine, SIGNAL(indexingFinished()), + this, SLOT(indexingFinished())); + } + + ~QHelpSearchResultWidgetPrivate() + { + delete searchEngine; + } + + QToolButton* setupToolButton(const QString &iconPath) + { + QToolButton *button = new QToolButton(); + button->setEnabled(false); + button->setAutoRaise(true); + button->setIcon(QIcon(iconPath)); + button->setIconSize(QSize(12, 12)); + button->setMaximumSize(QSize(16, 16)); + + return button; + } + + void updateHitRange() + { + int last = 0; + int first = 0; + int count = 0; + + if (!searchEngine.isNull()) { + count = searchEngine->hitCount(); + if (count > 0) { + first = resultFirstToShow +1; + last = resultLastToShow > count ? count : resultLastToShow; + } + } + hitsLabel->setText(QHelpSearchResultWidget::tr("%1 - %2 of %n Hits", 0, count).arg(first).arg(last)); + } + +private: + friend class QHelpSearchResultWidget; + + QPointer<QHelpSearchEngine> searchEngine; + + QDefaultResultWidget *resultTreeWidget; + QCLuceneResultWidget *resultTextBrowser; + + int resultLastToShow; + int resultFirstToShow; + bool isIndexing; + + QToolButton *firstResultPage; + QToolButton *previousResultPage; + QLabel *hitsLabel; + QToolButton *nextResultPage; + QToolButton *lastResultPage; +}; + +#include "qhelpsearchresultwidget.moc" + + +/*! + \class QHelpSearchResultWidget + \since 4.4 + \inmodule QtHelp + \brief The QHelpSearchResultWidget class provides either a tree + widget or a text browser depending on the used search engine to display + the hits found by the search. +*/ + +/*! + \fn void QHelpSearchResultWidget::requestShowLink(const QUrl &link) + + This signal is emitted when a item is activated and its associated + \a link should be shown. +*/ + +QHelpSearchResultWidget::QHelpSearchResultWidget(QHelpSearchEngine *engine) + : QWidget(0) + , d(new QHelpSearchResultWidgetPrivate(engine)) +{ + QVBoxLayout *vLayout = new QVBoxLayout(this); + vLayout->setMargin(0); + vLayout->setSpacing(0); + +#if defined(QT_CLUCENE_SUPPORT) + QHBoxLayout *hBoxLayout = new QHBoxLayout(); +#ifndef Q_OS_MAC + hBoxLayout->setMargin(0); + hBoxLayout->setSpacing(0); +#endif + hBoxLayout->addWidget(d->firstResultPage = d->setupToolButton( + QString::fromUtf8(":/trolltech/assistant/images/3leftarrow.png"))); + + hBoxLayout->addWidget(d->previousResultPage = d->setupToolButton( + QString::fromUtf8(":/trolltech/assistant/images/1leftarrow.png"))); + + d->hitsLabel = new QLabel(tr("0 - 0 of 0 Hits"), this); + d->hitsLabel->setEnabled(false); + hBoxLayout->addWidget(d->hitsLabel); + d->hitsLabel->setAlignment(Qt::AlignCenter); + d->hitsLabel->setMinimumSize(QSize(150, d->hitsLabel->height())); + + hBoxLayout->addWidget(d->nextResultPage = d->setupToolButton( + QString::fromUtf8(":/trolltech/assistant/images/1rightarrow.png"))); + + hBoxLayout->addWidget(d->lastResultPage = d->setupToolButton( + QString::fromUtf8(":/trolltech/assistant/images/3rightarrow.png"))); + + QSpacerItem *spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + hBoxLayout->addItem(spacer); + + vLayout->addLayout(hBoxLayout); + + d->resultTextBrowser = new QCLuceneResultWidget(this); + vLayout->addWidget(d->resultTextBrowser); + + connect(d->resultTextBrowser, SIGNAL(requestShowLink(QUrl)), this, + SIGNAL(requestShowLink(QUrl))); + + connect(d->nextResultPage, SIGNAL(clicked()), d, SLOT(showNextResultPage())); + connect(d->lastResultPage, SIGNAL(clicked()), d, SLOT(showLastResultPage())); + connect(d->firstResultPage, SIGNAL(clicked()), d, SLOT(showFirstResultPage())); + connect(d->previousResultPage, SIGNAL(clicked()), d, SLOT(showPreviousResultPage())); + + connect(d->firstResultPage, SIGNAL(clicked()), d, SLOT(updateNextButtonState())); + connect(d->previousResultPage, SIGNAL(clicked()), d, SLOT(updateNextButtonState())); + connect(d->nextResultPage, SIGNAL(clicked()), d, SLOT(updatePrevButtonState())); + connect(d->lastResultPage, SIGNAL(clicked()), d, SLOT(updatePrevButtonState())); + +#else + d->resultTreeWidget = new QDefaultResultWidget(this); + vLayout->addWidget(d->resultTreeWidget); + connect(d->resultTreeWidget, SIGNAL(requestShowLink(QUrl)), this, + SIGNAL(requestShowLink(QUrl))); +#endif + + connect(engine, SIGNAL(searchingFinished(int)), d, SLOT(setResults(int))); +} + +/*! \reimp +*/ +void QHelpSearchResultWidget::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::LanguageChange) + d->setResults(d->searchEngine->hitCount()); +} + +/*! + Destroys the search result widget. +*/ +QHelpSearchResultWidget::~QHelpSearchResultWidget() +{ + delete d; +} + +/*! + Returns a reference of the URL that the item at \a point owns, or an + empty URL if no item exists at that point. +*/ +QUrl QHelpSearchResultWidget::linkAt(const QPoint &point) +{ + QUrl url; +#if defined(QT_CLUCENE_SUPPORT) + if (d->resultTextBrowser) + url = d->resultTextBrowser->anchorAt(point); +#else + if (d->resultTreeWidget) { + QTreeWidgetItem *item = d->resultTreeWidget->itemAt(point); + if (item) + url = item->data(1, Qt::DisplayRole).toString(); + } +#endif + return url; +} + +QT_END_NAMESPACE diff --git a/src/assistant/help/qhelpsearchresultwidget.h b/src/assistant/help/qhelpsearchresultwidget.h new file mode 100644 index 000000000..f9300e979 --- /dev/null +++ b/src/assistant/help/qhelpsearchresultwidget.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2011 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$ +** +****************************************************************************/ + +#ifndef QHELPSEARCHRESULTWIDGET_H +#define QHELPSEARCHRESULTWIDGET_H + +#include <QtHelp/qhelpsearchengine.h> +#include <QtHelp/qhelp_global.h> + +#include <QtCore/QUrl> +#include <QtCore/QPoint> +#include <QtCore/QObject> + +#include <QtWidgets/QWidget> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Help) + +class QHelpSearchResultWidgetPrivate; + +class QHELP_EXPORT QHelpSearchResultWidget : public QWidget +{ + Q_OBJECT + +public: + ~QHelpSearchResultWidget(); + QUrl linkAt(const QPoint &point); + +Q_SIGNALS: + void requestShowLink(const QUrl &url); + +private: + friend class QHelpSearchEngine; + + QHelpSearchResultWidgetPrivate *d; + QHelpSearchResultWidget(QHelpSearchEngine *engine); + virtual void changeEvent(QEvent *event); +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QHELPSEARCHRESULTWIDGET_H |