E-book viewer: Highlight panel: Allow filtering the shown highlights by type of highlight. Fixes #2057929 [ebook viewer ER: for highlight panel, add ability to filter by highlight type (color)](https://bugs.launchpad.net/calibre/+bug/2057929)

This commit is contained in:
Kovid Goyal 2024-03-30 15:20:49 +05:30
parent 3fbb55d184
commit e85af248a6
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C

View File

@ -11,6 +11,7 @@ from qt.core import (
QAbstractItemView,
QColor,
QDialog,
QDialogButtonBox,
QFont,
QHBoxLayout,
QIcon,
@ -18,6 +19,9 @@ from qt.core import (
QItemSelectionModel,
QKeySequence,
QLabel,
QListView,
QListWidget,
QListWidgetItem,
QMenu,
QPainter,
QPainterPath,
@ -25,11 +29,13 @@ from qt.core import (
QPixmap,
QPushButton,
QRect,
QSize,
QSizePolicy,
QStyle,
Qt,
QTextCursor,
QTextEdit,
QToolButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
@ -76,8 +82,12 @@ def wavy_path(width, height, y_origin):
return path
def compute_style_key(style):
return tuple((k, style[k]) for k in sorted(style))
def decoration_for_style(palette, style, icon_size, device_pixel_ratio, is_dark):
style_key = (is_dark, icon_size, device_pixel_ratio, tuple((k, style[k]) for k in sorted(style)))
style_key = (is_dark, icon_size, device_pixel_ratio, compute_style_key(style))
sentinel = object()
ans = decoration_cache.get(style_key, sentinel)
if ans is not sentinel:
@ -143,6 +153,75 @@ def decoration_for_style(palette, style, icon_size, device_pixel_ratio, is_dark)
return ans
class SwatchList(QListWidget):
def __init__(self, all_styles, selected_styles, parent=None):
super().__init__(parent)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setViewMode(QListView.ViewMode.IconMode)
icon_size = parent.style().pixelMetric(QStyle.PixelMetric.PM_IconViewIconSize, None, self)
self.setIconSize(QSize(icon_size, icon_size))
self.setSpacing(20)
dpr = self.devicePixelRatioF()
is_dark = is_dark_theme()
flags = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren
self.itemClicked.connect(self.toggle_item)
for s in all_styles:
key = compute_style_key(s)
dec = decoration_for_style(self.palette(), s, icon_size, dpr, is_dark)
i = QListWidgetItem(self)
i.setFlags(flags)
i.setData(Qt.ItemDataRole.UserRole, s)
i.setData(Qt.ItemDataRole.UserRole + 1, key)
if dec:
i.setData(Qt.ItemDataRole.DecorationRole, dec)
i.setCheckState(Qt.CheckState.Checked if selected_styles and key in selected_styles else Qt.CheckState.Unchecked)
def toggle_item(self, item):
item.setCheckState(Qt.CheckState.Unchecked if item.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked)
@property
def selected_styles(self):
for i in range(self.count()):
item = self.item(i)
if item.checkState() == Qt.CheckState.Checked:
yield item.data(Qt.ItemDataRole.UserRole + 1)
def select_all(self):
for i in range(self.count()):
self.item(i).setCheckState(Qt.CheckState.Checked)
def select_none(self):
for i in range(self.count()):
self.item(i).setCheckState(Qt.CheckState.Unchecked)
class FilterDialog(Dialog):
def __init__(self, all_styles, show_only_styles, parent=None):
self.all_styles, self.show_only_styles = all_styles, show_only_styles
super().__init__(_('Filter shown highlights'), 'filter-highlights', parent=parent)
def sizeHint(self):
return QSize(500, 400)
def setup_ui(self):
self.setWindowIcon(QIcon.ic('filter.png'))
self.l = l = QVBoxLayout(self)
la = QLabel(_('Choose what kinds of highlights will be displayed below. If none are selected, no filtering is performed.'))
la.setWordWrap(True)
l.addWidget(la)
self.swatches = s = SwatchList(self.all_styles, self.show_only_styles, self)
l.addWidget(s)
l.addWidget(self.bb)
self.bb.addButton(_('Select &all'), QDialogButtonBox.ButtonRole.ActionRole).clicked.connect(s.select_all)
self.bb.addButton(_('Select &none'), QDialogButtonBox.ButtonRole.ActionRole).clicked.connect(s.select_none)
@property
def selected_styles(self):
return frozenset(self.swatches.selected_styles)
class Export(ExportBase):
prefs = vprefs
pref_name = 'highlight_export_format'
@ -184,6 +263,7 @@ class Highlights(QTreeWidget):
QTreeWidget.__init__(self, parent)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
self.styles_to_show = frozenset()
self.default_decoration = QIcon.ic('blank.png')
self.setHeaderHidden(True)
self.num_of_items = 0
@ -310,6 +390,7 @@ class Highlights(QTreeWidget):
item.setData(0, Qt.ItemDataRole.DecorationRole, dec)
self.uuid_map[h['uuid']] = secnum, itemnum
self.num_of_items += 1
self.apply_filters()
def sorted_highlights(self, highlights):
def_idx = 999999999999999
@ -405,6 +486,37 @@ class Highlights(QTreeWidget):
for item in self.selectedItems():
yield item.data(0, highlight_role)
@property
def all_highlight_styles(self):
seen = set()
for h in self.all_highlights:
s = h.get('style')
if h.get('removed') or h.get('type') != 'highlight' or not s:
continue
k = compute_style_key(s)
if k not in seen:
yield s
seen.add(k)
def apply_filters(self):
q = self.styles_to_show
for item in self.iteritems():
h = item.data(0, highlight_role)
hidden = False
if q:
skey = compute_style_key(h.get('style', {}))
hidden = skey not in q
item.setHidden(hidden)
root = self.invisibleRootItem()
for i in range(root.childCount()):
sec = root.child(i)
for k in range(sec.childCount()):
if not sec.child(k).isHidden():
sec.setHidden(False)
break
else:
sec.setHidden(True)
def keyPressEvent(self, ev):
if ev.matches(QKeySequence.StandardKey.Delete):
self.delete_requested.emit()
@ -488,9 +600,13 @@ class HighlightsPanel(QWidget):
si.do_search.connect(self.search_requested)
l.addWidget(si)
h = QHBoxLayout()
la = QLabel(_('Double click to jump to an entry'))
la.setWordWrap(True)
l.addWidget(la)
self.filter_button = b = QToolButton(self)
b.setIcon(QIcon.ic('filter.png')), b.setToolTip(_('Show only highlights of a specific types'))
b.clicked.connect(self.change_active_filter)
h.addWidget(la), h.addStretch(10), h.addWidget(b)
l.addLayout(h)
self.highlights = h = Highlights(self)
l.addWidget(h)
@ -523,6 +639,12 @@ class HighlightsPanel(QWidget):
nd.setVisible(False)
l.addLayout(h)
def change_active_filter(self):
d = FilterDialog(self.highlights.all_highlight_styles, self.highlights.styles_to_show, self)
if d.exec() == QDialog.DialogCode.Accepted:
self.highlights.styles_to_show = d.selected_styles
self.highlights.apply_filters()
def notes_edited(self, text):
h = self.highlights.current_highlight
if h is not None: