diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index ece157621b..73a6bbcf62 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -871,6 +871,12 @@ class ActionPolish(InterfaceActionBase): description = _('Fine tune your e-books') +class ActionBrowseAnnotations(InterfaceActionBase): + name = 'Browse Annotations' + actual_plugin = 'calibre.gui2.actions.browse_annots:BrowseAnnotationsAction' + description = _('Browse highlights and bookmarks from all books in the library') + + class ActionEditToC(InterfaceActionBase): name = 'Edit ToC' actual_plugin = 'calibre.gui2.actions.toc_edit:ToCEditAction' @@ -1095,7 +1101,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore, ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy, ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper, - ActionVirtualLibrary] + ActionVirtualLibrary, ActionBrowseAnnotations] # }}} diff --git a/src/calibre/gui2/actions/browse_annots.py b/src/calibre/gui2/actions/browse_annots.py new file mode 100644 index 0000000000..a0532fab42 --- /dev/null +++ b/src/calibre/gui2/actions/browse_annots.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal + +from __future__ import absolute_import, division, print_function, unicode_literals + + +from calibre.gui2.actions import InterfaceAction + + +class BrowseAnnotationsAction(InterfaceAction): + + name = 'Browse Annotations' + action_spec = (_('Browse annotations'), 'polish.png', + _('Browse highlights and bookmarks from all books in the library'), _('B')) + dont_add_to = frozenset(('context-menu-device',)) + action_type = 'current' + + def genesis(self): + self.qaction.triggered.connect(self.show_browser) + self._browser = None + + @property + def browser(self): + if self._browser is None: + from calibre.gui2.library.annotations import AnnotationsBrowser + self._browser = AnnotationsBrowser(self.gui) + return self._browser + + def show_browser(self): + self.browser.show_dialog() diff --git a/src/calibre/gui2/library/annotations.py b/src/calibre/gui2/library/annotations.py index dec0cc29c5..ab6dd8e250 100644 --- a/src/calibre/gui2/library/annotations.py +++ b/src/calibre/gui2/library/annotations.py @@ -16,7 +16,7 @@ from PyQt5.Qt import ( from calibre import prepare_string_for_xml from calibre.ebooks.metadata import authors_to_string, fmt_sidx from calibre.gui2 import Application, config, gprefs -from calibre.gui2.viewer.search import ResultsDelegate, SearchBox +from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox from calibre.gui2.widgets2 import Dialog @@ -437,6 +437,7 @@ class AnnotationsBrowser(Dialog): self.exec_() else: self.show() + self.raise_() if __name__ == '__main__': diff --git a/src/calibre/gui2/viewer/search.py b/src/calibre/gui2/viewer/search.py index 3cc2c0ea6f..9e0d72a582 100644 --- a/src/calibre/gui2/viewer/search.py +++ b/src/calibre/gui2/viewer/search.py @@ -3,22 +3,20 @@ # License: GPL v3 Copyright: 2020, Kovid Goyal import json -import re from collections import Counter, OrderedDict from threading import Thread import regex from PyQt5.Qt import ( - QAction, QCheckBox, QComboBox, QFont, QFontMetrics, QHBoxLayout, QIcon, QLabel, - QStyle, QStyledItemDelegate, Qt, QToolButton, QTreeWidget, QTreeWidgetItem, - QVBoxLayout, QWidget, pyqtSignal + QCheckBox, QComboBox, QFont, QHBoxLayout, QIcon, QLabel, Qt, QToolButton, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal ) from calibre.ebooks.conversion.search_replace import REGEX_FLAGS -from calibre.gui2 import QT_HIDDEN_CLEAR_ACTION, warning_dialog +from calibre.gui2 import warning_dialog from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.viewer.web_view import get_data, get_manifest, vprefs -from calibre.gui2.widgets2 import HistoryComboBox +from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox from polyglot.builtins import iteritems, map, unicode_type from polyglot.functools import lru_cache from polyglot.queue import Queue @@ -293,31 +291,6 @@ def search_in_name(name, search_query, ctx_size=50): yield before, match.group(), after, start -class SearchBox(HistoryComboBox): - - history_saved = pyqtSignal(object, object) - cleared = pyqtSignal() - - def __init__(self, parent=None): - HistoryComboBox.__init__(self, parent) - self.lineEdit().setPlaceholderText(_('Search')) - self.lineEdit().setClearButtonEnabled(True) - ac = self.lineEdit().findChild(QAction, QT_HIDDEN_CLEAR_ACTION) - if ac is not None: - ac.triggered.connect(self.cleared) - - def save_history(self): - ret = HistoryComboBox.save_history(self) - self.history_saved.emit(self.text(), self.history) - return ret - - def contextMenuEvent(self, event): - menu = self.lineEdit().createStandardContextMenu() - menu.addSeparator() - menu.addAction(_('Clear search history'), self.clear_history) - menu.exec_(event.globalPos()) - - class SearchInput(QWidget): # {{{ do_search = pyqtSignal(object) @@ -447,95 +420,6 @@ class SearchInput(QWidget): # {{{ # }}} -class ResultsDelegate(QStyledItemDelegate): # {{{ - - add_ellipsis = True - - def result_data(self, result): - if not isinstance(result, SearchResult): - return None, None, None, None - return result.is_hidden, result.before, result.text, result.text - - def paint(self, painter, option, index): - QStyledItemDelegate.paint(self, painter, option, index) - result = index.data(Qt.UserRole) - is_hidden, result_before, result_text, result_after = self.result_data(result) - if result_text is None: - return - painter.save() - try: - p = option.palette - c = p.HighlightedText if option.state & QStyle.State_Selected else p.Text - group = (p.Active if option.state & QStyle.State_Active else p.Inactive) - c = p.color(group, c) - painter.setPen(c) - font = option.font - emphasis_font = QFont(font) - emphasis_font.setBold(True) - flags = Qt.AlignTop | Qt.TextSingleLine | Qt.TextIncludeTrailingSpaces - rect = option.rect.adjusted(option.decorationSize.width() + 4 if is_hidden else 0, 0, 0, 0) - painter.setClipRect(rect) - before = re.sub(r'\s+', ' ', result_before) - before_width = 0 - if before: - before_width = painter.boundingRect(rect, flags, before).width() - after = re.sub(r'\s+', ' ', result_after.rstrip()) - after_width = 0 - if after: - after_width = painter.boundingRect(rect, flags, after).width() - ellipsis_width = painter.boundingRect(rect, flags, '...').width() - painter.setFont(emphasis_font) - text = re.sub(r'\s+', ' ', result_text) - match_width = painter.boundingRect(rect, flags, text).width() - if match_width >= rect.width() - 3 * ellipsis_width: - efm = QFontMetrics(emphasis_font) - text = efm.elidedText(text, Qt.ElideRight, rect.width()) - painter.drawText(rect, flags, text) - else: - self.draw_match( - painter, flags, before, text, after, rect, before_width, match_width, after_width, ellipsis_width, emphasis_font, font) - except Exception: - import traceback - traceback.print_exc() - painter.restore() - - def draw_match(self, painter, flags, before, text, after, rect, before_width, match_width, after_width, ellipsis_width, emphasis_font, normal_font): - extra_width = int(rect.width() - match_width) - if before_width < after_width: - left_width = min(extra_width // 2, before_width) - right_width = extra_width - left_width - else: - right_width = min(extra_width // 2, after_width) - left_width = min(before_width, extra_width - right_width) - x = rect.left() - nfm = QFontMetrics(normal_font) - if before_width and left_width: - r = rect.adjusted(0, 0, 0, 0) - r.setRight(x + left_width) - painter.setFont(normal_font) - ebefore = nfm.elidedText(before, Qt.ElideLeft, left_width) - if self.add_ellipsis and ebefore == before: - ebefore = '…' + before[1:] - r.setLeft(x) - x += painter.drawText(r, flags, ebefore).width() - painter.setFont(emphasis_font) - r = rect.adjusted(0, 0, 0, 0) - r.setLeft(x) - painter.drawText(r, flags, text).width() - x += match_width - if after_width and right_width: - painter.setFont(normal_font) - r = rect.adjusted(0, 0, 0, 0) - r.setLeft(x) - eafter = nfm.elidedText(after, Qt.ElideRight, right_width) - if self.add_ellipsis and eafter == after: - eafter = after[:-1] + '…' - painter.setFont(normal_font) - painter.drawText(r, flags, eafter) - -# }}} - - class Results(QTreeWidget): # {{{ show_search_result = pyqtSignal(object) diff --git a/src/calibre/gui2/viewer/widgets.py b/src/calibre/gui2/viewer/widgets.py new file mode 100644 index 0000000000..976cc246f4 --- /dev/null +++ b/src/calibre/gui2/viewer/widgets.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal + +from __future__ import absolute_import, division, print_function, unicode_literals + +import re + +from PyQt5.Qt import ( + QAction, QFont, QFontMetrics, QStyle, QStyledItemDelegate, Qt, pyqtSignal +) + +from calibre.gui2 import QT_HIDDEN_CLEAR_ACTION +from calibre.gui2.widgets2 import HistoryComboBox + + +class ResultsDelegate(QStyledItemDelegate): # {{{ + + add_ellipsis = True + + def result_data(self, result): + if not hasattr(result, 'is_hidden'): + return None, None, None, None + return result.is_hidden, result.before, result.text, result.text + + def paint(self, painter, option, index): + QStyledItemDelegate.paint(self, painter, option, index) + result = index.data(Qt.UserRole) + is_hidden, result_before, result_text, result_after = self.result_data(result) + if result_text is None: + return + painter.save() + try: + p = option.palette + c = p.HighlightedText if option.state & QStyle.State_Selected else p.Text + group = (p.Active if option.state & QStyle.State_Active else p.Inactive) + c = p.color(group, c) + painter.setPen(c) + font = option.font + emphasis_font = QFont(font) + emphasis_font.setBold(True) + flags = Qt.AlignTop | Qt.TextSingleLine | Qt.TextIncludeTrailingSpaces + rect = option.rect.adjusted(option.decorationSize.width() + 4 if is_hidden else 0, 0, 0, 0) + painter.setClipRect(rect) + before = re.sub(r'\s+', ' ', result_before) + before_width = 0 + if before: + before_width = painter.boundingRect(rect, flags, before).width() + after = re.sub(r'\s+', ' ', result_after.rstrip()) + after_width = 0 + if after: + after_width = painter.boundingRect(rect, flags, after).width() + ellipsis_width = painter.boundingRect(rect, flags, '...').width() + painter.setFont(emphasis_font) + text = re.sub(r'\s+', ' ', result_text) + match_width = painter.boundingRect(rect, flags, text).width() + if match_width >= rect.width() - 3 * ellipsis_width: + efm = QFontMetrics(emphasis_font) + text = efm.elidedText(text, Qt.ElideRight, rect.width()) + painter.drawText(rect, flags, text) + else: + self.draw_match( + painter, flags, before, text, after, rect, before_width, match_width, after_width, ellipsis_width, emphasis_font, font) + except Exception: + import traceback + traceback.print_exc() + painter.restore() + + def draw_match(self, painter, flags, before, text, after, rect, before_width, match_width, after_width, ellipsis_width, emphasis_font, normal_font): + extra_width = int(rect.width() - match_width) + if before_width < after_width: + left_width = min(extra_width // 2, before_width) + right_width = extra_width - left_width + else: + right_width = min(extra_width // 2, after_width) + left_width = min(before_width, extra_width - right_width) + x = rect.left() + nfm = QFontMetrics(normal_font) + if before_width and left_width: + r = rect.adjusted(0, 0, 0, 0) + r.setRight(x + left_width) + painter.setFont(normal_font) + ebefore = nfm.elidedText(before, Qt.ElideLeft, left_width) + if self.add_ellipsis and ebefore == before: + ebefore = '…' + before[1:] + r.setLeft(x) + x += painter.drawText(r, flags, ebefore).width() + painter.setFont(emphasis_font) + r = rect.adjusted(0, 0, 0, 0) + r.setLeft(x) + painter.drawText(r, flags, text).width() + x += match_width + if after_width and right_width: + painter.setFont(normal_font) + r = rect.adjusted(0, 0, 0, 0) + r.setLeft(x) + eafter = nfm.elidedText(after, Qt.ElideRight, right_width) + if self.add_ellipsis and eafter == after: + eafter = after[:-1] + '…' + painter.setFont(normal_font) + painter.drawText(r, flags, eafter) + +# }}} + + +class SearchBox(HistoryComboBox): # {{{ + + history_saved = pyqtSignal(object, object) + cleared = pyqtSignal() + + def __init__(self, parent=None): + HistoryComboBox.__init__(self, parent) + self.lineEdit().setPlaceholderText(_('Search')) + self.lineEdit().setClearButtonEnabled(True) + ac = self.lineEdit().findChild(QAction, QT_HIDDEN_CLEAR_ACTION) + if ac is not None: + ac.triggered.connect(self.cleared) + + def save_history(self): + ret = HistoryComboBox.save_history(self) + self.history_saved.emit(self.text(), self.history) + return ret + + def contextMenuEvent(self, event): + menu = self.lineEdit().createStandardContextMenu() + menu.addSeparator() + menu.addAction(_('Clear search history'), self.clear_history) + menu.exec_(event.globalPos()) +# }}}