mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on annotations browser action
This commit is contained in:
parent
ded52279fc
commit
5a0da12942
@ -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]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
31
src/calibre/gui2/actions/browse_annots.py
Normal file
31
src/calibre/gui2/actions/browse_annots.py
Normal 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()
|
@ -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__':
|
||||||
|
@ -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)
|
||||||
|
129
src/calibre/gui2/viewer/widgets.py
Normal file
129
src/calibre/gui2/viewer/widgets.py
Normal 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())
|
||||||
|
# }}}
|
Loading…
x
Reference in New Issue
Block a user