diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 4787d8e49d..62eb9739a3 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -197,6 +197,8 @@ def _config(): # {{{ help='Search history for the main GUI') c.add_opt('viewer_search_history', default=[], help='Search history for the ebook viewer') + c.add_opt('viewer_toc_search_history', default=[], + help='Search history for the ToC in the ebook viewer') c.add_opt('lrf_viewer_search_history', default=[], help='Search history for the LRF viewer') c.add_opt('scheduler_search_history', default=[], diff --git a/src/calibre/gui2/viewer/config.py b/src/calibre/gui2/viewer/config.py index 7a071ef08f..b38b405f81 100644 --- a/src/calibre/gui2/viewer/config.py +++ b/src/calibre/gui2/viewer/config.py @@ -157,6 +157,7 @@ class ConfigDialog(QDialog, Ui_Dialog): def clear_search_history(self): from calibre.gui2 import config config['viewer_search_history'] = [] + config['viewer_toc_search_history'] = [] def save_theme(self): themename, ok = QInputDialog.getText(self, _('Theme name'), diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 0124b7dfdc..d636eff0c6 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -125,6 +125,7 @@ class EbookViewer(MainWindow): self.search.search.connect(self.find) self.search.focus_to_library.connect(lambda: self.view.setFocus(Qt.OtherFocusReason)) self.toc.pressed[QModelIndex].connect(self.toc_clicked) + self.toc.searched.connect(partial(self.toc_clicked, force=True)) self.reference.goto.connect(self.goto) self.bookmarks.edited.connect(self.bookmarks_edited) self.bookmarks.activated.connect(self.goto_bookmark) diff --git a/src/calibre/gui2/viewer/toc.py b/src/calibre/gui2/viewer/toc.py index c931553c8d..2da1dc63b9 100644 --- a/src/calibre/gui2/viewer/toc.py +++ b/src/calibre/gui2/viewer/toc.py @@ -8,13 +8,19 @@ __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' import re -from PyQt5.Qt import (QStandardItem, QStandardItemModel, Qt, QFont, - QTreeView) +from PyQt5.Qt import ( + QStandardItem, QStandardItemModel, Qt, QFont, QTreeView, QWidget, + QHBoxLayout, QToolButton, QIcon, QModelIndex, pyqtSignal) from calibre.ebooks.metadata.toc import TOC as MTOC +from calibre.gui2 import error_dialog +from calibre.gui2.search_box import SearchBox2 +from calibre.utils.icu import primary_contains class TOCView(QTreeView): + searched = pyqtSignal(object) + def __init__(self, *args): QTreeView.__init__(self, *args) self.setMinimumWidth(80) @@ -47,6 +53,35 @@ class TOCView(QTreeView): self.unsetCursor() return QTreeView.mouseMoveEvent(self, ev) +class TOCSearch(QWidget): + + def __init__(self, toc_view, parent=None): + QWidget.__init__(self, parent) + self.toc_view = toc_view + self.l = l = QHBoxLayout(self) + self.search = s = SearchBox2(self) + self.search.setMinimumContentsLength(15) + self.search.initialize('viewer_toc_search_history', help_text=_('Search Table of Contents')) + self.search.setToolTip(_('Search for text in the Table of Contents')) + s.search.connect(self.do_search) + self.go = b = QToolButton(self) + b.setIcon(QIcon(I('search.png'))) + b.clicked.connect(s.do_search) + b.setToolTip(_('Find next match')) + l.addWidget(s), l.addWidget(b) + + def do_search(self, text): + if not text or not text.strip(): + return + index = self.toc_view.model().search(text) + if index.isValid(): + self.toc_view.searched.emit(index) + else: + error_dialog(self.toc_view, _('No matches found'), _( + 'There are no Table of Contents entries matching: %s') % text, show=True) + self.search.search_done(True) + + class TOCItem(QStandardItem): def __init__(self, spine, toc, depth, all_items, parent=None): @@ -65,6 +100,7 @@ class TOCItem(QStandardItem): for t in toc: self.appendRow(TOCItem(spine, t, depth+1, all_items, parent=self)) self.setFlags(Qt.ItemIsEnabled) + self.is_current_search_result = False spos = 0 for i, si in enumerate(spine): if si == self.abspath: @@ -201,6 +237,14 @@ class TOCItem(QStandardItem): if changed: self.setFont(self.emphasis_font if is_being_viewed else self.normal_font) + def set_current_search_result(self, yes): + if yes and not self.is_current_search_result: + self.setText(self.text() + ' ◄') + self.is_current_search_result = True + elif not yes and self.is_current_search_result: + self.setText(self.text()[:-2]) + self.is_current_search_result = False + def __repr__(self): return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment) @@ -211,6 +255,7 @@ class TOC(QStandardItemModel): def __init__(self, spine, toc=None): QStandardItemModel.__init__(self) + self.current_query = {'text':'', 'index':-1, 'items':()} if toc is None: toc = MTOC() self.all_items = depth_first = [] @@ -269,5 +314,22 @@ class TOC(QStandardItemModel): if item is current_entry: found = True + def find_items(self, query): + for item in self.all_items: + if primary_contains(query, item.text()): + yield item - + def search(self, query): + cq = self.current_query + if cq['items'] and -1 < cq['index'] < len(cq['items']): + cq['items'][cq['index']].set_current_search_result(False) + if cq['text'] != query: + items = tuple(self.find_items(query)) + cq.update({'text':query, 'items':items, 'index':-1}) + if len(cq['items']) > 0: + cq['index'] = (cq['index'] + 1) % len(cq['items']) + item = cq['items'][cq['index']] + item.set_current_search_result(True) + index = self.indexFromItem(item) + return index + return QModelIndex() diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index a6dc0ae63a..2c82de61c4 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -13,14 +13,14 @@ from PyQt5.Qt import ( QIcon, QWidget, Qt, QGridLayout, QScrollBar, QToolBar, QAction, QToolButton, QMenu, QDoubleSpinBox, pyqtSignal, QLineEdit, QRegExpValidator, QRegExp, QPalette, QColor, QBrush, QPainter, - QDockWidget, QSize, QWebView, QLabel) + QDockWidget, QSize, QWebView, QLabel, QVBoxLayout) from calibre.gui2 import rating_font, workaround_broken_under_mouse from calibre.gui2.main_window import MainWindow from calibre.gui2.search_box import SearchBox2 from calibre.gui2.viewer.documentview import DocumentView from calibre.gui2.viewer.bookmarkmanager import BookmarkManager -from calibre.gui2.viewer.toc import TOCView +from calibre.gui2.viewer.toc import TOCView, TOCSearch class DoubleSpinBox(QDoubleSpinBox): # {{{ @@ -236,9 +236,13 @@ class Main(MainWindow): self.tool_bar2.addWidget(self.search) self.toc_dock = d = QDockWidget(_('Table of Contents'), self) - self.toc = TOCView(self) + self.toc_container = w = QWidget(self) + w.l = QVBoxLayout(w) + self.toc = TOCView(w) + self.toc_search = TOCSearch(self.toc, parent=w) + w.l.addWidget(self.toc), w.l.addWidget(self.toc_search), w.l.setContentsMargins(0, 0, 0, 0) d.setObjectName('toc-dock') - d.setWidget(self.toc) + d.setWidget(w) d.close() # starts out hidden self.addDockWidget(Qt.LeftDockWidgetArea, d) d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)