mirror of
https://github.com/zealdocs/zeal.git
synced 2026-08-29 08:34:50 +08:00
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:
@@ -11,9 +11,9 @@
|
||||
|
||||
Zeal is a simple documentation browser inspired by [Dash](http://kapeli.com/dash/).
|
||||
|
||||

|
||||

|
||||
|
||||
[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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user