Merge branch 'bookshelf-emblem' of https://github.com/un-pogaz/calibre

This commit is contained in:
Kovid Goyal 2026-01-14 14:00:38 +05:30
commit 68c7650dee
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 132 additions and 15 deletions

View File

@ -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':

View File

@ -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):

View File

@ -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

View File

@ -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): # {{{
<p>Advanced rule for column <b>%(col)s</b>:
<pre>%(rule)s</pre>
''')%dict(col=col, rule=prepare_string_for_xml(rule))
elif self.rule_kind in {'emblem'}:
elif self.rule_kind in kind_emblems:
return _('''
<p>Advanced rule:
<pre>%(rule)s</pre>
@ -981,6 +989,9 @@ class RulesModel(QAbstractListModel): # {{{
if kind == 'emblem':
return _('<p>Add the emblem <b>{0}</b> to the cover if the following conditions are met:</p>'
'\n<ul>{1}</ul>').format(rule.color, ''.join(conditions))
if kind == 'bookshelf_emblem':
return _('<p>Add the emblem <b>{0}</b> to spine if the following conditions are met:</p>'
'\n<ul>{1}</ul>').format(rule.color, ''.join(conditions))
return _('''\
<p>Set the <b>%(kind)s</b> of <b>%(col)s</b> to <b>%(color)s</b> %(sample)s
if the following conditions are met:</p>
@ -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.'
'<p>You can <b>change an existing rule</b> by double clicking it.')
self.l1.setText('<p>'+ 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('<p>'+ 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(_(
'<p>"Automatic" will place the icon where the space is available above or below the spine, with below first.'
'<p>"Above" and "Below" will allway place the icon at the selected position of the spine.'
'<p>"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'),

View File

@ -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()

View File

@ -301,6 +301,11 @@
</item>
</layout>
</widget>
<widget class="BookshelfEmblemRules" name="emblem_rules">
<attribute name="title">
<string>&amp;Emblems</string>
</attribute>
</widget>
<widget class="CoverCacheConfig" name="config_cache">
<attribute name="title">
<string>&amp;Performance</string>
@ -313,6 +318,11 @@
<extends>QWidget</extends>
<header>calibre/gui2/preferences/look_feel_tabs.h</header>
</customwidget>
<customwidget>
<class>BookshelfEmblemRules</class>
<extends>ConfigWidgetBase</extends>
<header>calibre/gui2/preferences/look_feel_tabs.h</header>
</customwidget>
<customwidget>
<class>CoverCacheConfig</class>
<extends>ConfigWidgetBase</extends>