From deeb08929cb933db02ab93ccf509705a50f6237a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:20:17 +0000 Subject: [PATCH] Cover grid: Option to draw emblems on top of cover in Preferences->Look & feel->Cover grid->Emblems Fixes #3019 (Cover grid: replace show_emblems/draw_emblems_on_cover with emblem_style enum; add on-cover emblem rendering) --- src/calibre/gui2/__init__.py | 11 ++++- src/calibre/gui2/library/alternate_views.py | 47 +++++++++++++++++-- .../preferences/look_feel_tabs/cover_grid.py | 4 ++ .../preferences/look_feel_tabs/cover_grid.ui | 37 ++++++++++++++- 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index b5747d75eb..33e75107eb 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -451,9 +451,9 @@ def create_defs(): defs['cb_preserve_aspect_ratio'] = False defs['cb_double_click_to_activate'] = False defs['gpm_template_editor_font_size'] = 10 - defs['show_emblems'] = False defs['emblem_size'] = 32 defs['emblem_position'] = 'left' + defs['emblem_style'] = 'none' defs['metadata_diff_mark_rejected'] = False defs['tag_browser_show_counts'] = True defs['tag_browser_show_tooltips'] = True @@ -551,6 +551,15 @@ def create_defs(): migrate_tweak('metadata_edit_single_cc_label_length', 'edit_metadata_single_cc_label_length') migrate_tweak('metadata_single_use_2_cols_for_custom_fields', 'edit_metadata_single_use_2_cols_for_custom_fields') + # Migrate show_emblems and draw_emblems_on_cover to emblem_style + show_emblems = gprefs.pop('show_emblems', None) + draw_emblems_on_cover = gprefs.pop('draw_emblems_on_cover', None) + if show_emblems is not None or draw_emblems_on_cover is not None: + if show_emblems: + gprefs['emblem_style'] = 'emboss' if draw_emblems_on_cover else 'gutter' + else: + gprefs['emblem_style'] = 'none' + create_defs() del create_defs diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py index a677417dcb..0a04c96491 100644 --- a/src/calibre/gui2/library/alternate_views.py +++ b/src/calibre/gui2/library/alternate_views.py @@ -617,10 +617,10 @@ class CoverDelegate(QStyledItemDelegate): height = self.original_height = gprefs['cover_grid_height'] self.original_show_title = show_title = gprefs['cover_grid_show_title'] self.original_flush_bottom = self.flush_bottom = gprefs['cover_grid_text_flush_bottom'] - self.original_show_emblems = gprefs['show_emblems'] + self.original_emblem_style = gprefs['emblem_style'] self.orginal_emblem_size = gprefs['emblem_size'] self.orginal_emblem_position = gprefs['emblem_position'] - self.emblem_size = gprefs['emblem_size'] if self.original_show_emblems else 0 + self.emblem_size = gprefs['emblem_size'] if self.original_emblem_style != 'none' else 0 try: self.gutter_position = getattr(self, self.orginal_emblem_position.upper()) except Exception: @@ -644,7 +644,7 @@ class CoverDelegate(QStyledItemDelegate): sz = f.pointSize() * self.parent().logicalDpiY() / 72.0 self.title_height = int(max(25, sz + 10)) self.item_size = self.cover_size + QSize(2 * self.MARGIN, (2 * self.MARGIN) + self.title_height) - if self.emblem_size > 0: + if self.emblem_size > 0 and self.original_emblem_style == 'gutter': extra = self.emblem_size + self.MARGIN self.item_size += QSize(extra, 0) if self.gutter_position in (self.LEFT, self.RIGHT) else QSize(0, extra) self.calculate_spacing() @@ -732,10 +732,11 @@ class CoverDelegate(QStyledItemDelegate): painter.save() right_adjust = 0 + emblem_rect = None try: rect = option.rect rect.adjust(self.MARGIN, self.MARGIN, -self.MARGIN, -self.MARGIN) - if self.emblem_size > 0: + if self.emblem_size > 0 and self.original_emblem_style == 'gutter': self.paint_emblems(painter, rect, emblems) orect = QRect(rect) trect = QRect(rect) @@ -749,6 +750,7 @@ class CoverDelegate(QStyledItemDelegate): painter.drawText(rect, Qt.AlignmentFlag.AlignCenter|Qt.TextFlag.TextWordWrap, f'{title}\n\n{authors}') if self.title_height != 0: self.paint_title(painter, trect, db, book_id) + emblem_rect = QRect(rect) else: if self.animating is not None and self.animating.row() == index.row(): cover = cover.scaled(cover.size() * self._animated_size) @@ -764,6 +766,10 @@ class CoverDelegate(QStyledItemDelegate): if self.flush_bottom: trect.setTop(rect.bottom() + 5) self.paint_title(painter, trect, db, book_id, align_top=self.flush_bottom) + emblem_rect = QRect(rect) + if self.original_emblem_style == 'emboss' and emblems: + self.paint_emblems_on_cover(painter, emblem_rect, emblems) + return if self.emblem_size > 0: # We don't draw embossed emblems as the ondevice/marked emblems are drawn in the gutter return @@ -831,6 +837,37 @@ class CoverDelegate(QStyledItemDelegate): finally: painter.restore() + def paint_emblems_on_cover(self, painter, rect, emblems): + esz = self.emblem_size + if not esz: + return + margin = self.MARGIN + r = gprefs['cover_corner_radius'] + if r > 0: + if gprefs['cover_corner_radius_unit'] == '%': + corner_inset = int(r / 100 * min(rect.width(), rect.height())) + else: + corner_inset = int(r) + else: + corner_inset = 0 + available_height = rect.height() - corner_inset + max_per_edge = max(1, available_height // (esz + margin)) + with painter: + painter.setClipRect(rect) + for i, emblem in enumerate(emblems): + if i < max_per_edge: + x = rect.left() + margin + y = rect.top() + corner_inset + i * (esz + margin) + else: + j = i - max_per_edge + if j >= max_per_edge: + break + x = rect.right() - esz - margin + y = rect.top() + corner_inset + j * (esz + margin) + ew = int(emblem.width() / emblem.devicePixelRatio()) + eh = int(emblem.height() / emblem.devicePixelRatio()) + painter.drawPixmap(QRect(x, y, ew, eh), emblem) + def paint_embossed_emblem(self, pixmap, painter, orect, right_adjust, left=True): drect = QRect(orect) pw = int(pixmap.width() / pixmap.devicePixelRatio()) @@ -1030,7 +1067,7 @@ class GridView(MomentumScrollMixin, QListView): ) if (size_changed or gprefs[ 'cover_grid_show_title'] != self.delegate.original_show_title or gprefs[ - 'show_emblems'] != self.delegate.original_show_emblems or gprefs[ + 'emblem_style'] != self.delegate.original_emblem_style or gprefs[ 'emblem_size'] != self.delegate.orginal_emblem_size or gprefs[ 'emblem_position'] != self.delegate.orginal_emblem_position or gprefs[ 'cover_grid_text_flush_bottom'] != self.delegate.original_flush_bottom): diff --git a/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.py b/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.py index 58c535a1cc..57bb558bfc 100644 --- a/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.py +++ b/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.py @@ -38,6 +38,10 @@ class CoverGridTab(QTabWidget, LazyConfigWidgetBase, Ui_cover_grid_tab): r('emblem_size', gprefs) r('emblem_position', gprefs, choices=[ (_('Left'), 'left'), (_('Top'), 'top'), (_('Right'), 'right'), (_('Bottom'), 'bottom')]) + r('emblem_style', gprefs, choices=[ + (_('Do not show emblems'), 'none'), + (_('Show in a gutter next to the cover'), 'gutter'), + (_('Draw on the cover'), 'emboss')]) fm = db.field_metadata choices = sorted((('{} ({})'.format(fm[k]['name'], k), k) for k in fm.displayable_field_keys() if fm[k]['name']), diff --git a/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.ui b/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.ui index ac02724368..5081f2c141 100644 --- a/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.ui +++ b/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.ui @@ -288,10 +288,45 @@ A value of zero means calculate automatically. - + &Emblems + + + + + + + Emblem &style: + + + opt_emblem_style + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + +