Start work on annotations browser action

This commit is contained in:
Kovid Goyal 2020-06-25 13:58:16 +05:30
parent ded52279fc
commit 5a0da12942
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 173 additions and 122 deletions

View File

@ -871,6 +871,12 @@ class ActionPolish(InterfaceActionBase):
description = _('Fine tune your e-books') 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): class ActionEditToC(InterfaceActionBase):
name = 'Edit ToC' name = 'Edit ToC'
actual_plugin = 'calibre.gui2.actions.toc_edit:ToCEditAction' actual_plugin = 'calibre.gui2.actions.toc_edit:ToCEditAction'
@ -1095,7 +1101,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore, ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore,
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy, ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper, ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper,
ActionVirtualLibrary] ActionVirtualLibrary, ActionBrowseAnnotations]
# }}} # }}}

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
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()

View File

@ -16,7 +16,7 @@ from PyQt5.Qt import (
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml
from calibre.ebooks.metadata import authors_to_string, fmt_sidx from calibre.ebooks.metadata import authors_to_string, fmt_sidx
from calibre.gui2 import Application, config, gprefs 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 from calibre.gui2.widgets2 import Dialog
@ -437,6 +437,7 @@ class AnnotationsBrowser(Dialog):
self.exec_() self.exec_()
else: else:
self.show() self.show()
self.raise_()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -3,22 +3,20 @@
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import json import json
import re
from collections import Counter, OrderedDict from collections import Counter, OrderedDict
from threading import Thread from threading import Thread
import regex import regex
from PyQt5.Qt import ( from PyQt5.Qt import (
QAction, QCheckBox, QComboBox, QFont, QFontMetrics, QHBoxLayout, QIcon, QLabel, QCheckBox, QComboBox, QFont, QHBoxLayout, QIcon, QLabel, Qt, QToolButton,
QStyle, QStyledItemDelegate, Qt, QToolButton, QTreeWidget, QTreeWidgetItem, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
QVBoxLayout, QWidget, pyqtSignal
) )
from calibre.ebooks.conversion.search_replace import REGEX_FLAGS 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.progress_indicator import ProgressIndicator
from calibre.gui2.viewer.web_view import get_data, get_manifest, vprefs 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.builtins import iteritems, map, unicode_type
from polyglot.functools import lru_cache from polyglot.functools import lru_cache
from polyglot.queue import Queue 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 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): # {{{ class SearchInput(QWidget): # {{{
do_search = pyqtSignal(object) 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): # {{{ class Results(QTreeWidget): # {{{
show_search_result = pyqtSignal(object) show_search_result = pyqtSignal(object)

View File

@ -0,0 +1,129 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
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())
# }}}