Search improvements

- highlighting (bold font) of searched phrase
 - search in substrings if startswith lookup returned less than 40 results
 - escape special SQL characters
This commit is contained in:
Jerzy Kozera
2013-02-02 11:19:09 +00:00
parent 723bbc5cae
commit aecbc58995
10 changed files with 186 additions and 30 deletions
+3 -3
View File
@@ -11,9 +11,9 @@
Zeal is a simple documentation browser inspired by [Dash](http://kapeli.com/dash/).
![Screenshot](http://i.imgur.com/6EcSIvD.png)
![Screenshot](http://i.imgur.com/SiLvpz8.png)
[More screenshots (imgur)](http://imgur.com/a/VrYQx)
[More screenshots (imgur)](http://imgur.com/a/EFmzc)
## How to use
@@ -33,7 +33,7 @@ Currently Zeal requires Qt 5.0. To compile it, run `qmake` and `make` in the `ze
## Windows binary
A 64-bit Windows binary with all dependencies is available to download from Dropbox - [zeal.zip](https://www.dropbox.com/s/cs2r0eych4om2k2/zeal.zip) (24M).
A 64-bit Windows binary with all dependencies is available to download from Dropbox - [zeal.zip](https://www.dropbox.com/s/rbx3bxqif45v0nn/zeal.zip) (24M).
## TODO
+6
View File
@@ -4,6 +4,7 @@
#include "zealsearchmodel.h"
#include "zealnativeeventfilter.h"
#include "zealdocsetsregistry.h"
#include "zealsearchitemdelegate.h"
#include <QDebug>
#include <QAbstractEventDispatcher>
@@ -17,7 +18,11 @@
#ifdef WIN32
#include <windows.h>
#else
#if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0)
#include <QtGui/5.1.0/QtGui/qpa/qplatformnativeinterface.h>
#else
#include <QtGui/5.0.0/QtGui/qpa/qplatformnativeinterface.h>
#endif
#include <xcb/xcb.h>
#include <xcb/xcb_keysyms.h>
#include <X11/keysym.h>
@@ -117,6 +122,7 @@ MainWindow::MainWindow(QWidget *parent) :
ui->lineEdit->setFocus();
ui->treeView->setModel(&zealList);
ui->treeView->setColumnHidden(1, true);
ui->treeView->setItemDelegate(new ZealSearchItemDelegate(ui->treeView, ui->lineEdit, ui->treeView));
connect(ui->treeView, &QTreeView::activated, [&](const QModelIndex& index) {
QStringList url_l = index.sibling(index.row(), 1).data().toString().split('#');
QUrl url = QUrl::fromLocalFile(url_l[0]);
+6 -2
View File
@@ -20,7 +20,9 @@ SOURCES += main.cpp\
zealsearchedit.cpp \
zealnativeeventfilter.cpp \
lineedit.cpp \
searchablewebview.cpp
searchablewebview.cpp \
zealsearchitemdelegate.cpp \
zealsearchitemstyle.cpp
HEADERS += mainwindow.h \
zeallistmodel.h \
@@ -30,7 +32,9 @@ HEADERS += mainwindow.h \
zealsearchedit.h \
zealnativeeventfilter.h \
lineedit.h \
searchablewebview.h
searchablewebview.h \
zealsearchitemdelegate.h \
zealsearchitemstyle.h
FORMS += mainwindow.ui
+47 -25
View File
@@ -1,5 +1,6 @@
#include <QThread>
#include <QVariant>
#include <QDebug>
#include <QtSql/QSqlQuery>
#include "zealdocsetsregistry.h"
@@ -48,68 +49,89 @@ void ZealDocsetsRegistry::runQuery(const QString& query)
QMetaObject::invokeMethod(this, "_runQuery", Qt::QueuedConnection, Q_ARG(QString, query), Q_ARG(int, lastQuery));
}
void ZealDocsetsRegistry::_runQuery(const QString& query, int queryNum)
void ZealDocsetsRegistry::_runQuery(const QString& query_, int queryNum)
{
if(queryNum != lastQuery) return; // some other queries pending - ignore this one
QList<ZealSearchResult> results;
QString query = query_;
query.replace("\\", "\\\\");
query.replace("_", "\\_");
query.replace("%", "\\%");
query.replace("'", "''");
for(auto name : names()) {
QString qstr;
QSqlQuery q;
bool found = false;
QList<QList<QVariant> > found;
bool withSubStrings = false;
while(!found) {
while(found.size() < 40) {
auto curQuery = query;
QString notQuery; // don't return the same result twice
QString parentQuery;
if(withSubStrings) {
// if nothing found starting with query, search all substrings
// if less than 40 found starting with query, search all substrings
curQuery = "%"+query;
if(types[name] == ZDASH) {
notQuery = QString(" and not (ztokenname like '%1%' escape '\\' or ztokenname like '%.%1%' escape '\\') ").arg(query);
} else {
notQuery = QString(" and not t.name like '%1%' escape '\\' ").arg(query);
if(types[name] == ZEAL) {
parentQuery = QString(" or t2.name like '%1%' escape '\\' ").arg(query);
}
}
}
int cols = 3;
if(types[name] == ZEAL) {
qstr = QString("select name, parent, path from things where name "
"like '%1%' order by lower(name) asc, path asc limit 40").arg(curQuery);
qstr = QString("select t.name, t2.name, t.path from things t left join things t2 on t2.id=t.parent where "
"(t.name like '%1%' escape '\\' %3) %2 order by lower(t.name) asc, t.path asc limit 40").arg(curQuery, notQuery, parentQuery);
} else if(types[name] == DASH) {
qstr = QString("select name, null, path from searchIndex where name "
"like '%1%' order by lower(name) asc, path asc limit 40").arg(curQuery);
qstr = QString("select t.name, null, t.path from searchIndex t where t.name "
"like '%1%' escape '\\' %2 order by lower(t.name) asc, t.path asc limit 40").arg(curQuery, notQuery);
} else if(types[name] == ZDASH) {
cols = 4;
qstr = QString("select ztokenname, null, zpath, zanchor from ztoken "
"join ztokenmetainformation on ztoken.zmetainformation = ztokenmetainformation.z_pk "
"join zfilepath on ztokenmetainformation.zfile = zfilepath.z_pk where ztokenname "
"join zfilepath on ztokenmetainformation.zfile = zfilepath.z_pk where (ztokenname "
// %.%1% for long Django docset values like django.utils.http
// (Might be not appropriate for other docsets, but I don't have any on hand to test)
"like '%1%' or ztokenname like '%.%1%' order by lower(ztokenname) asc, zpath asc, zanchor asc limit 40").arg(curQuery);
"like '%1%' escape '\\' or ztokenname like '%.%1%' escape '\\' ) %2 order by lower(ztokenname) asc, zpath asc, "
"zanchor asc limit 40").arg(curQuery, notQuery);
}
q = db(name).exec(qstr);
if(q.next()) { found = true; }
else {
if(withSubStrings) break;
withSubStrings = true; // try again searching for substrings
while(q.next()) {
QList<QVariant> values;
for(int i = 0; i < cols; ++i) {
values.append(q.value(i));
}
found.append(values);
}
if(withSubStrings) break;
withSubStrings = true; // try again searching for substrings
}
if(!found) continue;
do {
for(auto &row : found) {
QString parentName;
if(!q.value(1).isNull()) {
auto qp = db(name).exec(QString("select name from things where id = %1").arg(q.value(1).toInt()));
qp.next();
parentName = qp.value(0).toString();
if(!row[1].isNull()) {
parentName = row[1].toString();
}
auto path = q.value(2).toString();
auto path = row[2].toString();
// FIXME: refactoring to use common code in ZealListModel and ZealDocsetsRegistry
// TODO: parent name, splitting by '.', as in ZealDocsetsRegistry
if(types[name] == DASH || types[name] == ZDASH) {
path = QDir(QDir(QDir("Contents").filePath("Resources")).filePath("Documents")).filePath(path);
}
if(types[name] == ZDASH) {
path += "#" + q.value(3).toString();
path += "#" + row[3].toString();
}
auto itemName = q.value(0).toString();
if(itemName.indexOf('.') != -1 && itemName.indexOf('.') != 0 && q.value(1).isNull()) {
auto itemName = row[0].toString();
if(itemName.indexOf('.') != -1 && itemName.indexOf('.') != 0 && row[1].isNull()) {
auto splitted = itemName.split(".");
itemName = splitted.at(splitted.size()-1);
parentName = splitted.at(splitted.size()-2);
}
results.append(ZealSearchResult(itemName, parentName, path, name, query));
} while (q.next());
}
}
qSort(results);
if(queryNum != lastQuery) return; // some other queries pending - ignore this one
+1
View File
@@ -79,6 +79,7 @@ const QPair<QString, QString> ZealListModel::getItem(const QString& path, int in
item.first = q.value(0).toString();
auto filePath = q.value(1).toString();
// FIXME: refactoring to use common code in ZealListModel and ZealDocsetsRegistry
// TODO: parent name, splitting by '.', as in ZealDocsetsRegistry
if(docsets->type(docsetName) == DASH || docsets->type(docsetName) == ZDASH) {
filePath = QDir(QDir(QDir("Contents").filePath("Resources")).filePath("Documents")).filePath(filePath);
}
+4
View File
@@ -7,7 +7,11 @@
#include <xcb/xcb_keysyms.h>
#include <X11/keysym.h>
#include <QGuiApplication>
#if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0)
#include <QtGui/5.1.0/QtGui/qpa/qplatformnativeinterface.h>
#else
#include <QtGui/5.0.0/QtGui/qpa/qplatformnativeinterface.h>
#endif
#include <QDebug>
// http://svn.tribler.org/vlc/trunk/modules/control/globalhotkeys/xcb.c
+72
View File
@@ -0,0 +1,72 @@
#include "zealsearchitemdelegate.h"
#include "zealsearchitemstyle.h"
#include <QPainter>
#include <QFontMetrics>
#include <QDebug>
#include <QApplication>
ZealSearchItemDelegate::ZealSearchItemDelegate(QObject *parent, QLineEdit* lineEdit_, QWidget* view_) :
QStyledItemDelegate(parent), lineEdit(lineEdit_), view(view_)
{
}
void ZealSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option_, const QModelIndex &index) const
{
painter->save();
QStyleOptionViewItem option(option_);
#if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0) || defined(WIN32)
// overriding subElementRect doesn't work with Qt 5.0.0, but is required to display
// selected item frame correctly in Windows (Windows binary distribution contains patched Qt
// - for patch see https://codereview.qt-project.org/#change,46559)
option.text = index.data().toString();
option.features |= QStyleOptionViewItem::HasDisplay;
#endif
if(!index.data(Qt::DecorationRole).isNull()) {
option.features |= QStyleOptionViewItem::HasDecoration;
option.icon = index.data(Qt::DecorationRole).value<QIcon>();
}
ZealSearchItemStyle style;
style.drawControl(QStyle::CE_ItemViewItem, &option, painter, view);
if(option.state & QStyle::State_Selected) {
#ifdef WIN32
option.palette.setColor(QPalette::All, QPalette::HighlightedText, option.palette.color(QPalette::Active, QPalette::Text));
#endif
painter->setPen(QPen(option.palette.highlightedText(), 1));
}
auto rect = static_cast<QApplication*>(QApplication::instance())->style()->subElementRect(QStyle::SE_ItemViewItemText, &option, view);
const int margin = style.pixelMetric(QStyle::PM_FocusFrameHMargin, 0, view);
rect.adjust(margin, 0, 2, 0); // +2px for bold text
QFontMetrics metrics(painter->font());
QFont bold(painter->font());;
bold.setBold(true);
QFontMetrics metricsBold(bold);
auto elided = metrics.elidedText(index.data().toString(), option.textElideMode, rect.width());
QString highlight;
if(lineEdit) {
highlight = lineEdit->text();
}
int from = 0;
while(from < elided.size()) {
int until = elided.toLower().indexOf(highlight.toLower(), from);
if(!highlight.size()) until = -1;
if(until == -1) {
painter->drawText(rect, elided.mid(from));
from = elided.size();
} else {
painter->drawText(rect, elided.mid(from, until-from));
rect.setLeft(rect.left() + metrics.width(elided.mid(from, until-from)));
QFont old(painter->font());
painter->setFont(bold);
painter->drawText(rect, elided.mid(until, highlight.size()));
painter->setFont(old);
rect.setLeft(rect.left() + metricsBold.width(elided.mid(until, highlight.size())));
from = until + highlight.size();
}
}
painter->restore();
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef ZEALSEARCHITEMDELEGATE_H
#define ZEALSEARCHITEMDELEGATE_H
#include <QStyledItemDelegate>
#include <QLineEdit>
class ZealSearchItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit ZealSearchItemDelegate(QObject *parent = 0, QLineEdit* lineEdit_ = nullptr, QWidget* view_ = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
signals:
public slots:
private:
QLineEdit *lineEdit;
QWidget *view;
};
#endif // ZEALSEARCHITEMDELEGATE_H
+12
View File
@@ -0,0 +1,12 @@
#include "zealsearchitemstyle.h"
#include <QDebug>
QRect ZealSearchItemStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const
{
if(element == QStyle::SE_ItemViewItemText) {
// do not draw text - delegate does it
return QRect();
} else {
return QProxyStyle::subElementRect(element, option, widget);
}
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef ZEALSEARCHITEMSTYLE_H
#define ZEALSEARCHITEMSTYLE_H
#include <QProxyStyle>
class ZealSearchItemStyle : public QProxyStyle
{
public:
QRect subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const;
};
#endif // ZEALSEARCHITEMSTYLE_H