diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index 884d86fe08..62ccd2bf4e 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -642,6 +642,7 @@ class DB: defs['bookshelf_title_template'] = '{title}' defs['bookshelf_author_template'] = '' defs['bookshelf_spine_size_template'] = '{pages}' + defs['bookshelf_icon_rules'] = [] # Migrate the beta bookshelf_grouping_mode if self.prefs.get('bookshelf_grouping_mode', '') == 'none': diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index fea8eed2c6..594dd990d2 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -502,6 +502,7 @@ def create_defs(): defs['bookshelf_up_to_down'] = False defs['bookshelf_height'] = 119 defs['bookshelf_make_space_for_second_line'] = False + defs['bookshelf_emblem_position'] = 'auto' # Migrate beta bookshelf_thumbnail if isinstance(btv := gprefs.get('bookshelf_thumbnail'), bool): diff --git a/src/calibre/gui2/library/bookshelf_view.py b/src/calibre/gui2/library/bookshelf_view.py index 6c8fed5fc6..2d15d9cf0a 100644 --- a/src/calibre/gui2/library/bookshelf_view.py +++ b/src/calibre/gui2/library/bookshelf_view.py @@ -1323,6 +1323,8 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea): DIVIDER_GRADIENT_LINE_1.setAlphaF(0.0) # Transparent at top/bottom DIVIDER_GRADIENT_LINE_2.setAlphaF(0.75) # Visible in middle TEXT_MARGIN = 6 + EMBLEM_SIZE = 24 + EMBLEM_MARGIN = 2 def __init__(self, gui): super().__init__(gui) @@ -1454,6 +1456,20 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea): template, mi, TEMPLATE_ERROR, mi, column_name=column_name, template_cache=self.template_cache) return rslt or '' + def render_emblem(self, book_id: int) -> str: + if not (db := self.dbref()): + return '' + p = db.new_api.backend.prefs + if not (rules := p.get('bookshelf_icon_rules', p.defaults.get('bookshelf_icon_rules'))): + return '' + mi = db.get_proxy_metadata(book_id) + for (x,y,t) in rules: + rslt = mi.formatter.safe_format( + t, mi, TEMPLATE_ERROR, mi, column_name='bookshelf_emblem', template_cache=self.template_cache) + if rslt: + return rslt + return '' + def refresh_settings(self): '''Refresh the gui and render settings.''' self.template_inited = False @@ -1628,6 +1644,7 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea): def draw_emblems(self, painter: QPainter, item: ShelfItem, scroll_y: int) -> None: book_id = item.book_id above, below = [], [] + top, bottom = [], [] if m := self.model(): from calibre.gui2.ui import get_gui db = m.db @@ -1642,30 +1659,57 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea): self.on_device_icon = QIcon.ic('ok.png') which = above if below else below which.append(self.on_device_icon) + custom = self.render_emblem(book_id) + if custom: + match gprefs['bookshelf_emblem_position']: + case 'above': + which = above + case 'below': + which = below + case 'top': + which = top + case 'bottom': + which = bottom + case _: + which = above if below and not above else below + which.append(QIcon.ic(custom)) - def draw_horizontal(emblems: list[QIcon], above: bool = True) -> None: + def draw_horizontal(emblems: list[QIcon], position: str) -> None: if not emblems: return - gap = 2 + gap = self.EMBLEM_MARGIN max_width = (item.width - gap) // len(emblems) lc = self.layout_constraints - max_height = lc.shelf_gap if above else lc.shelf_height + match position: + case 'above': + max_height = lc.shelf_gap + case 'below': + max_height = lc.shelf_height + case 'top' | 'bottom': + max_height = self.EMBLEM_SIZE sz = min(max_width, max_height) width = sz if len(emblems) > 1: width += gap + sz x = max(0, (item.width - width) // 2) + item.start_x + lc.side_margin y = item.case_start_y - scroll_y - if above: - y += lc.shelf_gap + item.reduce_height_by - sz - else: - y += lc.spine_height + match position: + case 'above': + y += lc.shelf_gap + item.reduce_height_by - sz + case 'below': + y += lc.spine_height + case 'top': + y += lc.shelf_gap + item.reduce_height_by + self.EMBLEM_MARGIN + case 'bottom': + y += lc.spine_height - sz - self.EMBLEM_MARGIN for ic in emblems: p = ic.pixmap(sz, sz) painter.drawPixmap(QPoint(x, y), p) x += sz + gap - draw_horizontal(above) - draw_horizontal(below, False) + draw_horizontal(above, 'above') + draw_horizontal(below, 'below') + draw_horizontal(top, 'top') + draw_horizontal(bottom, 'bottom') def paintEvent(self, ev: QPaintEvent): '''Paint the bookshelf view.''' @@ -1888,6 +1932,7 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea): '''Draw vertically the title on the spine.''' first_line, second_line = self.first_line_renderer(book_id), self.second_line_renderer(book_id) margin = self.TEXT_MARGIN + second_rect = None if second_line: first_rect = QRect(rect.left(), rect.top() + margin, rect.width() // 2, rect.height() - 2*margin) second_rect = first_rect.translated(first_rect.width(), 0) @@ -1895,10 +1940,23 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea): first_rect, second_rect = second_rect, first_rect else: first_rect = QRect(rect.left(), rect.top() + margin, rect.width(), rect.height() - 2*margin) + + emblem_size = -margin + (self.EMBLEM_MARGIN * 2) + emblem_size += min(rect.width() - self.EMBLEM_MARGIN, self.EMBLEM_SIZE) + match gprefs['bookshelf_emblem_position']: + case 'top': + first_rect.adjust(0, emblem_size, 0, 0) + if second_rect: + second_rect.adjust(0, emblem_size, 0, 0) + case 'bottom': + first_rect.adjust(0, 0, 0, -emblem_size) + if second_rect: + second_rect.adjust(0, 0, 0, -emblem_size) + nfl, nsl, font = self.get_text_metrics(first_line, second_line, first_rect.transposed().size()) if not nfl and not nsl: # two lines dont fit second_line = '' - first_rect = QRect(rect.left(), rect.top() + margin, rect.width(), rect.height() - 2*margin) + first_rect = QRect(rect.left(), first_rect.top(), rect.width(), first_rect.height()) nfl, nsl, font = self.get_text_metrics(first_line, second_line, first_rect.transposed().size()) first_line, second_line, = nfl, nsl diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py index 1740fd475e..773d8d0d26 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -410,16 +410,24 @@ pref_name_map = { 'You can add emblems (small icons) that are displayed on the side of covers' ' in the Cover grid by creating "rules" that tell calibre what image to use.'), }, + 'bookshelf_icon_rules': { + 'kind': 'bookshelf_emblem', + 'name': _('Bookshelf emblem'), + 'label': _('Add the emblem:'), + 'text': _( + 'You can add emblem (small icon) that are displayed on the side of spines' + ' in the bookshelf by creating "rules" that tell calibre what image to use.'), + }, } -kind_icons = {'emblem', 'icon'} -kind_emblems = {'emblem'} +kind_icons = {'emblem', 'icon', 'bookshelf_emblem'} +kind_emblems = {'emblem', 'bookshelf_emblem'} kind_colors = {'color'} def get_template_dialog(parent, rule, field=None, kind=None): if parent.pref_name == 'column_color_rules': return TemplateDialog(parent, rule, mi=parent.mi, fm=parent.fm, color_field=field) - if parent.pref_name == 'cover_grid_icon_rules': + if parent.pref_name in {'cover_grid_icon_rules', 'bookshelf_icon_rules'}: return TemplateDialog(parent, rule, mi=parent.mi, fm=parent.fm, doing_emblem=True) return TemplateDialog(parent, rule, mi=parent.mi, fm=parent.fm, icon_field_key=field, icon_rule_kind=kind) @@ -960,7 +968,7 @@ class RulesModel(QAbstractListModel): # {{{
Advanced rule for column %(col)s:
%(rule)s''')%dict(col=col, rule=prepare_string_for_xml(rule)) - elif self.rule_kind in {'emblem'}: + elif self.rule_kind in kind_emblems: return _('''
Advanced rule:
%(rule)s@@ -981,6 +989,9 @@ class RulesModel(QAbstractListModel): # {{{ if kind == 'emblem': return _('
Add the emblem {0} to the cover if the following conditions are met:
' '\nAdd the emblem {0} to spine if the following conditions are met:
' + '\nSet the %(kind)s of %(col)s to %(color)s %(sample)s if the following conditions are met:
@@ -1056,6 +1067,16 @@ class EditRules(QWidget): # {{{ c.setVisible(False) c.stateChanged.connect(self.changed) + self.choices_label = cl = QLabel(self) + l.addWidget(cl, l.rowCount(), 0, 1, 1) + cl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + cl.setVisible(False) + self.choices = c = QComboBox(self) + l.addWidget(c, l.rowCount() - 1, 1, 1, 1) + c.setVisible(False) + c.currentIndexChanged.connect(self.changed) + cl.setBuddy(c) + self.l1 = l1 = QLabel('') l1.setWordWrap(True) l.addWidget(l1, l.rowCount(), 0, 1, 2) @@ -1148,6 +1169,7 @@ class EditRules(QWidget): # {{{ text = pref_name_map[pref_name]['text'] text += ' ' + _('Click the "Add rule" button below to get started.' 'You can change an existing rule by double clicking it.') + self.l1.setText('
'+ text) if pref_name == 'cover_grid_icon_rules': self.enabled.setVisible(True) self.enabled.setChecked(gprefs['show_emblems']) @@ -1158,7 +1180,25 @@ class EditRules(QWidget): # {{{ ' next to the covers shown in the Cover grid, controlled by the' ' metadata of the book.')) self.enabled_toggled() - self.l1.setText('
'+ text) + if pref_name == 'bookshelf_icon_rules': + self.choices_label.setVisible(True) + self.choices.setVisible(True) + self.choices_label.setText(_('&Position of the emblem:')) + self.choices.setToolTip(_( + '
"Automatic" will place the icon where the space is available above or below the spine, with below first.' + '
"Above" and "Below" will allway place the icon at the selected position of the spine.' + '
"Top" and "Bottom" will place the icon on the spine, reducing the space allowed to the text.'))
+ choice_map = (
+ (_('Automatic'), 'auto'),
+ (_('Above of the spine'), 'above'),
+ (_('Below of the spine'), 'below'),
+ (_('Top of the spine'), 'top'),
+ (_('Bottom of the spine'), 'bottom'),
+ )
+ for idx, (text, data) in enumerate(choice_map):
+ self.choices.addItem(text, data)
+ if data == gprefs['bookshelf_emblem_position']:
+ self.choices.setCurrentIndex(idx)
def enabled_toggled(self):
enabled = self.enabled.isChecked()
@@ -1308,6 +1348,9 @@ class EditRules(QWidget): # {{{
self.model.commit(prefs)
if self.pref_name == 'cover_grid_icon_rules':
gprefs['show_emblems'] = self.enabled.isChecked()
+ if self.pref_name == 'bookshelf_icon_rules':
+ idx = max(0, self.choices.currentIndex())
+ gprefs['bookshelf_emblem_position'] = self.choices.itemData(idx)
def export_rules(self):
path = choose_save_file(self, 'export-coloring-rules', _('Choose file to export to'),
diff --git a/src/calibre/gui2/preferences/look_feel_tabs/__init__.py b/src/calibre/gui2/preferences/look_feel_tabs/__init__.py
index 5b0b7a74dc..f64c4bbf85 100644
--- a/src/calibre/gui2/preferences/look_feel_tabs/__init__.py
+++ b/src/calibre/gui2/preferences/look_feel_tabs/__init__.py
@@ -269,6 +269,10 @@ class GridEmblemnRules(LazyEditRulesBase):
rule_set_name = 'cover_grid_icon_rules'
+class BookshelfEmblemRules(LazyEditRulesBase):
+ rule_set_name = 'bookshelf_icon_rules'
+
+
class BackgroundConfig(QGroupBox, LazyConfigWidgetBase):
changed_signal = pyqtSignal()
diff --git a/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.ui b/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.ui
index 05bd233ec5..baa194d38a 100644
--- a/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.ui
+++ b/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.ui
@@ -301,6 +301,11 @@
+