diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index ad3c6dc29c..c220ab2f87 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -564,8 +564,9 @@ class Cache: highlight_end=None, snippet_size=None, restrict_to_book_ids=None, + result_type=tuple, ): - return tuple(self.backend.fts_search( + return result_type(self.backend.fts_search( fts_engine_query, use_stemming=use_stemming, highlight_start=highlight_start, diff --git a/src/calibre/gui2/fts/search.py b/src/calibre/gui2/fts/search.py new file mode 100644 index 0000000000..58d25ea8e0 --- /dev/null +++ b/src/calibre/gui2/fts/search.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2022, Kovid Goyal + + +import os +import traceback +from qt.core import ( + QAbstractItemModel, QDialog, QDialogButtonBox, QModelIndex, Qt, QTreeView, + QVBoxLayout +) + +from calibre.gui2.fts.utils import get_db +from calibre.gui2.viewer.widgets import ResultsDelegate + +ROOT = QModelIndex() + + +class SearchDelegate(ResultsDelegate): + + def result_data(self, result): + if not isinstance(result, dict): + return None, None, None, None, None + full_text = result['text'] + parts = full_text.split('\x1d', 2) + before = after = '' + if len(parts) > 2: + before, text = parts[:2] + after = parts[2].replace('\x1d', '') + elif len(parts) == 2: + before, text = parts + else: + text = parts[0] + return False, before, text, after, False + + +class Results: + + _title = None + + def __init__(self, book_id): + self.book_id = book_id + self.search_results = [] + self.append = self.search_results.append + + def __len__(self): + return len(self.search_results) + + def __getitem__(self, x): + return self.search_results[x] + + @property + def title(self): + if self._title is None: + try: + self._title = get_db().field_for('title', self.book_id) + except Exception: + self._title = _('Unknown book') + return self._title + + +class ResultsModel(QAbstractItemModel): + + def __init__(self, parent=None): + super().__init__(parent) + self.results = [] + + def search(self, fts_engine_query, use_stemming=True, restrict_to_book_ids=None): + db = get_db() + + def construct(all_matches): + r = {} + sr = self.results = [] + + for x in all_matches: + book_id = x['book_id'] + results = r.get(book_id) + if results is None: + results = Results(book_id) + r[book_id] = results + sr.append(results) + results.append(x) + + self.beginResetModel() + db.fts_search( + fts_engine_query, use_stemming=use_stemming, highlight_start='\x1d', highlight_end='\x1d', snippet_size=64, + restrict_to_book_ids=restrict_to_book_ids, result_type=construct + ) + self.endResetModel() + + def index_to_entry(self, idx): + q = idx.internalId() + if q: + # search result + list_idx = q - 1 + try: + q = self.results[list_idx] + return q[idx.row()] + except Exception: + traceback.print_exc() + return None + else: + ans = idx.row() + if -1 < ans < len(self.results): + return self.results[ans] + + def index(self, row, column, parent=ROOT): + if parent.isValid(): + return self.createIndex(row, column, parent.row() + 1) + return self.createIndex(row, column, 0) + + def parent(self, index): + q = index.internalId() + if q: + return self.index(q - 1, 0) + return ROOT + + def rowCount(self, parent=ROOT): + if parent.isValid(): + x = self.index_to_entry(parent) + if isinstance(x, Results): + return len(x) + return 0 + return len(self.results) + + def columnCount(self, parent=ROOT): + return 1 + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + item = self.index_to_entry(index) + if item is None: + return + if isinstance(item, Results): + return self.data_for_book(item, role) + return self.data_for_match(item, role) + + def data_for_book(self, item, role): + if role == Qt.ItemDataRole.DisplayRole: + return item.title + if role == Qt.ItemDataRole.UserRole: + return item + + def data_for_match(self, item, role): + if role == Qt.ItemDataRole.UserRole: + return item + + +class ResultsView(QTreeView): + + def __init__(self, parent=None): + super().__init__(parent) + self.setHeaderHidden(True) + self.m = ResultsModel(self) + self.setModel(self.m) + self.delegate = SearchDelegate(self) + self.setItemDelegate(self.delegate) + + +if __name__ == '__main__': + from calibre.gui2 import Application + from calibre.library import db + app = Application([]) + d = QDialog() + l = QVBoxLayout(d) + bb = QDialogButtonBox(d) + bb.accepted.connect(d.accept), bb.rejected.connect(d.reject) + get_db.db = db(os.path.expanduser('~/test library')) + w = ResultsView(parent=d) + l.addWidget(w) + l.addWidget(bb) + w.model().search('asimov') + d.exec()