diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 96368ac570..3a3e590647 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -523,7 +523,7 @@ def create_defs():
defs['bookshelf_min_font_multiplier'] = 0.75
defs['bookshelf_max_font_multiplier'] = 1.3
defs['bookshelf_outline_width'] = 0
- defs['bookshelf_bold_font'] = True
+ defs['bookshelf_font'] = {'family': None, 'style': None}
defs['bookshelf_use_custom_colors'] = False
defs['bookshelf_custom_colors'] = {
'light': {}, 'dark': {},
diff --git a/src/calibre/gui2/library/bookshelf_view.py b/src/calibre/gui2/library/bookshelf_view.py
index 561d30dc85..435c64f378 100644
--- a/src/calibre/gui2/library/bookshelf_view.py
+++ b/src/calibre/gui2/library/bookshelf_view.py
@@ -27,6 +27,7 @@ from qt.core import (
QEasingCurve,
QEvent,
QFont,
+ QFontDatabase,
QFontInfo,
QFontMetricsF,
QIcon,
@@ -1482,7 +1483,10 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea):
self.theme: ColorTheme = None
self.palette_changed()
- self.base_font_size_pts = QFontInfo(self.font()).pointSizeF()
+ self.spine_font = self.default_spine_font = QFont(self.font())
+ self.spine_font.setBold(True)
+ self.divider_font = QFont(self.spine_font)
+ self.base_font_size_pts = QFontInfo(self.spine_font).pointSizeF()
self.outline_width = 0
self.min_line_height = self.base_font_size_pts * 1.2
@@ -1632,9 +1636,17 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea):
def refresh_settings(self):
'''Refresh the gui and render settings.'''
self.template_inited = False
+ s = gprefs['bookshelf_font']
+ if s and s.get('family'):
+ self.spine_font = QFontDatabase.font(s['family'], s['style'], int(self.base_font_size_pts))
+ self.spine_font.setPointSizeF(self.base_font_size_pts)
+ else:
+ self.spine_font = self.default_spine_font
+ self.get_sized_font.cache_clear()
+ self.get_text_metrics.cache_clear()
self.min_font_size = max(0.1, min(gprefs['bookshelf_min_font_multiplier'], 1)) * self.base_font_size_pts
self.max_font_size = max(1, min(gprefs['bookshelf_max_font_multiplier'], 3)) * self.base_font_size_pts
- _, fm, _ = self.get_sized_font(self.min_font_size, bold=gprefs['bookshelf_bold_font'])
+ _, fm, _ = self.get_sized_font(self.min_font_size)
self.outline_width = float(max(0, min(gprefs['bookshelf_outline_width'], 5)))
self.min_line_height = math.ceil(fm.height() + self.outline_width * 2)
self.calculate_shelf_geometry()
@@ -1973,25 +1985,24 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea):
painter.restore()
@lru_cache(maxsize=128)
- def get_sized_font(self, sz: float = 9, bold: bool = False) -> tuple[QFont, QFontMetricsF, QFontInfo]:
- font = QFont(self.font())
+ def get_sized_font(self, sz: float = 9, for_divider: bool = False) -> tuple[QFont, QFontMetricsF, QFontInfo]:
+ font = QFont(self.divider_font if for_divider else self.spine_font)
font.setPointSizeF(sz)
- font.setBold(bold)
return font, QFontMetricsF(font), QFontInfo(font)
@lru_cache(maxsize=4096)
def get_text_metrics(
- self, first_line: str, second_line: str = '', sz: QSize = QSize(), bold: bool = False, allow_wrap: bool = False,
- outline_width: float = 0,
+ self, first_line: str, second_line: str = '', sz: QSize = QSize(), allow_wrap: bool = False,
+ outline_width: float = 0, for_divider: bool = False,
) -> tuple[str, str, QFont, QFontMetricsF, bool]:
width, height = sz.width(), sz.height()
- font, fm, fi = self.get_sized_font(self.base_font_size_pts, bold)
+ font, fm, fi = self.get_sized_font(self.base_font_size_pts, for_divider=for_divider)
extra_height = outline_width * 2 # stroke width above and below
if allow_wrap and not second_line and first_line and fm.boundingRect(first_line).width() > width and height >= 2 * self.min_line_height:
# rather than reducing font size if there is available space, wrap to two lines
font2, fm2, fi2 = font, fm, fi
while math.ceil(2 * (fm2.height() + extra_height)) > height:
- font2, fm2, fi2 = self.get_sized_font(font2.pointSizeF() - 0.5, bold)
+ font2, fm2, fi2 = self.get_sized_font(font2.pointSizeF() - 0.5)
if fm2.boundingRect(first_line).width() >= width: # two line font size is larger than one line font size
font, fm, fi = font2, fm2, fi2
has_third_line = False
@@ -2015,7 +2026,7 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea):
# larger font sizes
if math.ceil(fm.height() + extra_height) < height:
while font.pointSizeF() < self.max_font_size:
- q, qm, qi = self.get_sized_font(font.pointSizeF() + 1, bold)
+ q, qm, qi = self.get_sized_font(font.pointSizeF() + 1)
if math.ceil(qm.height() + extra_height) < height:
font, fm = q, qm
else:
@@ -2025,14 +2036,14 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea):
nsz = font.pointSizeF()
if nsz < self.min_font_size and second_line:
return '', '', font, fm, False
- font, fm, fi = self.get_sized_font(nsz - 0.5, bold)
+ font, fm, fi = self.get_sized_font(nsz - 0.5)
# Now reduce the font size as much as needed to fit within width
text = first_line
if second_line and fm.boundingRect(first_line).width() < fm.boundingRect(second_line).width():
text = second_line
while fi.pointSizeF() > self.min_font_size and fm.boundingRect(text).width() > width:
- font, fm, fi = self.get_sized_font(font.pointSizeF() - 1, bold)
+ font, fm, fi = self.get_sized_font(font.pointSizeF() - 1)
if fi.pointSizeF() <= self.min_font_size:
first_line = fm.elidedText(first_line, Qt.TextElideMode.ElideRight, width)
if second_line:
@@ -2069,7 +2080,7 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea):
-self.TEXT_MARGIN if text_right else 0,
0,
)
- elided_text, _, font, _, _ = self.get_text_metrics(divider.group_name, '', text_rect.size(), bold=True)
+ elided_text, _, font, _, _ = self.get_text_metrics(divider.group_name, '', text_rect.size(), for_divider=True)
painter.save()
painter.setFont(font)
painter.setPen(self.theme.divider_text_color)
@@ -2200,13 +2211,12 @@ class BookshelfView(MomentumScrollMixin, QAbstractScrollArea):
nfl, nsl, font, fm, was_wrapped = self.get_text_metrics(
first_line, second_line, first_rect.transposed().size(), allow_wrap=True,
- bold=gprefs['bookshelf_bold_font'], outline_width=self.outline_width)
+ outline_width=self.outline_width)
if not nfl and not nsl: # two lines dont fit
second_line = ''
first_rect = QRect(rect.left(), first_rect.top(), rect.width(), first_rect.height())
nfl, nsl, font, fm, _ = self.get_text_metrics(
- first_line, second_line, first_rect.transposed().size(), bold=gprefs['bookshelf_bold_font'],
- outline_width=self.outline_width)
+ first_line, second_line, first_rect.transposed().size(), outline_width=self.outline_width)
elif was_wrapped:
first_rect, second_rect = calculate_rects(True)
first_line, second_line, = nfl, nsl
diff --git a/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.py b/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.py
index db7527b2ad..74cb18f241 100644
--- a/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.py
+++ b/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.py
@@ -9,7 +9,7 @@ import os
from contextlib import suppress
from functools import lru_cache, partial
-from qt.core import QApplication, QDialog, QDialogButtonBox, QIcon, QInputDialog, QLabel, Qt, QTabWidget, QTextBrowser, QTimer, QVBoxLayout, pyqtSignal
+from qt.core import QDialog, QDialogButtonBox, QIcon, QInputDialog, QLabel, Qt, QTabWidget, QTextBrowser, QTimer, QVBoxLayout, pyqtSignal
from calibre.gui2 import gprefs
from calibre.gui2.dialogs.confirm_delete import confirm
@@ -56,8 +56,29 @@ class BookshelfTab(QTabWidget, LazyConfigWidgetBase, Ui_Form):
recount_updated = pyqtSignal(object)
def __init__(self, parent=None):
+ self.current_font_choice = {}
super().__init__(parent)
+ def restore_defaults(self):
+ super().restore_defaults()
+ self.current_font_choice = gprefs.defaults['bookshelf_font'].copy()
+ self.update_font_display()
+ self.populate_custom_color_theme(use_defaults=True)
+
+ def update_font_display(self):
+ text = ''
+ s = self.current_font_choice
+ if s.get('family'):
+ text = s['family'] + ' - ' + (s.get('style') or '')
+ self.bookshelf_font_display.setText(text)
+
+ def initialize(self):
+ super().initialize()
+ s = gprefs['bookshelf_font'] or {}
+ self.current_font_choice = s.copy()
+ self.update_font_display()
+ self.populate_custom_color_theme()
+
def commit(self, *args):
import re
tp = self.opt_bookshelf_spine_size_template.text()
@@ -72,6 +93,7 @@ class BookshelfTab(QTabWidget, LazyConfigWidgetBase, Ui_Form):
for k,b in v.items():
d[k] = b.color
gprefs['bookshelf_custom_colors'] = newval
+ gprefs['bookshelf_font'] = self.current_font_choice.copy()
return super().commit(*args)
def genesis(self, gui):
@@ -87,7 +109,6 @@ class BookshelfTab(QTabWidget, LazyConfigWidgetBase, Ui_Form):
r('bookshelf_make_space_for_second_line', gprefs)
r('bookshelf_min_font_multiplier', gprefs)
r('bookshelf_max_font_multiplier', gprefs)
- r('bookshelf_bold_font', gprefs)
r('bookshelf_outline_width', gprefs)
r('bookshelf_divider_text_right', gprefs)
@@ -178,8 +199,18 @@ different calibre library you use.''').format('{size}', '{random}', '{pages}'))
l.setBuddy(b)
layout.insertRow(r, b, l)
b.color_changed.connect(self.changed_signal)
- QApplication.instance().palette_changed.connect(self.populate_custom_color_theme)
- self.populate_custom_color_theme()
+ self.change_font_button.clicked.connect(self.change_font)
+
+ def change_font(self):
+ from calibre.gui2.preferences.look_feel_tabs.font_selection_dialog import FontSelectionDialog
+ s = self.current_font_choice
+
+ d = FontSelectionDialog(family=s.get('family') or '', style=s.get('style') or '', parent=self)
+ if d.exec() == QDialog.DialogCode.Accepted:
+ family, style = d.selected_font()
+ self.current_font_choice = {'family': family, 'style': style}
+ self.update_font_display()
+ self.changed_signal.emit()
def lazy_initialize(self):
self.recount_timer.start()
@@ -273,13 +304,13 @@ def evaluate(book, context):
'current_selected_color': _('&The current and selected book highlight'),
}
- def populate_custom_color_theme(self):
+ def populate_custom_color_theme(self, use_defaults=False):
from calibre.gui2.library.bookshelf_view import ColorTheme
default = {
'light': ColorTheme.light_theme()._asdict(),
'dark': ColorTheme.dark_theme()._asdict(),
}
- configs = gprefs['bookshelf_custom_colors']
+ configs = (gprefs.defaults if use_defaults else gprefs)['bookshelf_custom_colors']
for theme in default:
for k in self.color_label_map():
b = self.color_buttons[theme][k]
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 44a61d698a..0d064008b1 100644
--- a/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.ui
+++ b/src/calibre/gui2/preferences/look_feel_tabs/bookshelf_view.ui
@@ -7,7 +7,7 @@
0
0
626
- 520
+ 521
@@ -377,6 +377,26 @@
+ -
+
+
+ &Font:
+
+
+ bookshelf_font_display
+
+
+
+ -
+
+
+ &Font size:
+
+
+ opt_bookshelf_min_font_multiplier
+
+
+
-
-
@@ -434,6 +454,38 @@
+ -
+
+
+ &Outline
+
+
+ opt_bookshelf_outline_width
+
+
+
+ -
+
+
+ <p>Set the thickness, in pixels, of the outline (border) around text on the spine. A value of zero means no outline. An outline helps the text standout against the spine, especially when the spine has a colorful cover.
+
+
+ none
+
+
+ px
+
+
+ 1
+
+
+ 3.000000000000000
+
+
+ 0.100000000000000
+
+
+
-
@@ -458,16 +510,6 @@
- -
-
-
- <p>Use a bold font to draw text on the book spine
-
-
- Use &bold font
-
-
-
-
@@ -483,47 +525,26 @@
- -
-
-
- &Font size:
-
-
- opt_bookshelf_min_font_multiplier
-
-
-
- -
-
-
- Text &outline:
-
-
- opt_bookshelf_outline_width
-
-
-
-
-
-
- <p>Set the thickness, in pixels, of the outline (border) around text on the spine. A value of zero means no outline. An outline helps the text standout against the spine, especially when the spine has a colorful cover.
-
-
- none
-
-
- px
-
-
- 1
-
-
- 3.000000000000000
-
-
- 0.100000000000000
-
-
+
+
-
+
+
+ true
+
+
+ Use default font
+
+
+
+ -
+
+
+ Change &font
+
+
+
+
diff --git a/src/calibre/gui2/preferences/look_feel_tabs/font_selection_dialog.py b/src/calibre/gui2/preferences/look_feel_tabs/font_selection_dialog.py
new file mode 100644
index 0000000000..c29ab74302
--- /dev/null
+++ b/src/calibre/gui2/preferences/look_feel_tabs/font_selection_dialog.py
@@ -0,0 +1,212 @@
+from qt.core import QDialog, QDialogButtonBox, QFontDatabase, QFontInfo, QHBoxLayout, QLabel, QListWidget, QScrollArea, Qt, QVBoxLayout, QWidget, pyqtSignal
+
+
+class FontSelectionDialog(QDialog):
+ fontSelected = pyqtSignal(str, str) # family, style
+
+ def __init__(self, family: str = '', style: str = '', min_size=8, medium_size=12, max_size=24, parent=None):
+ super().__init__(parent)
+ if family:
+ self.initial_family, self.initial_style = family, style
+ else:
+ font = self.font()
+ fi = QFontInfo(font)
+ self.initial_family = fi.family()
+ self.initial_style = fi.styleName()
+ self.min_size = min_size
+ self.medium_size = medium_size
+ self.max_size = max_size
+
+ self.setWindowTitle(_('Select font'))
+ self.setMinimumSize(600, 500)
+
+ self._setup_ui()
+ self._populate_families()
+
+ def _setup_ui(self):
+ main_layout = QVBoxLayout(self)
+
+ # Top section: Font families and styles side by side
+ lists_layout = QHBoxLayout()
+
+ # Font families list
+ families_layout = QVBoxLayout()
+ families_label = QLabel(_('&Family:'))
+ self.families_list = QListWidget()
+ families_label.setBuddy(self.families_list)
+ self.families_list.currentItemChanged.connect(self._on_family_changed)
+ families_layout.addWidget(families_label)
+ families_layout.addWidget(self.families_list)
+
+ # Styles list
+ styles_layout = QVBoxLayout()
+ styles_label = QLabel(_('&Style:'))
+ self.styles_list = QListWidget()
+ styles_label.setBuddy(self.styles_list)
+ self.styles_list.currentItemChanged.connect(self._on_style_changed)
+ styles_layout.addWidget(styles_label)
+ styles_layout.addWidget(self.styles_list)
+
+ lists_layout.addLayout(families_layout, 2)
+ lists_layout.addLayout(styles_layout, 1)
+
+ main_layout.addLayout(lists_layout, stretch=2)
+
+ # Preview area
+ preview_group = QWidget()
+ preview_layout = QVBoxLayout(preview_group)
+ preview_layout.setContentsMargins(0, 10, 0, 10)
+
+ # Scrollable preview area
+ scroll_area = QScrollArea()
+ scroll_area.setWidgetResizable(True)
+ scroll_area.setMinimumHeight(200)
+
+ preview_container = QWidget()
+ self.preview_layout = QVBoxLayout(preview_container)
+ self.preview_layout.setSpacing(10)
+ self.preview_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+
+ # Preview labels for different sizes
+ self.preview_small = QLabel('The quick brown fox jumps over the lazy dog')
+ self.preview_medium = QLabel(self.preview_small)
+ self.preview_large = QLabel(self.preview_small)
+ self.preview_layout.addWidget(self.preview_small)
+ self.preview_layout.addWidget(self.preview_medium)
+ self.preview_layout.addWidget(self.preview_large)
+
+ scroll_area.setWidget(preview_container)
+ preview_layout.addWidget(scroll_area)
+
+ main_layout.addWidget(preview_group, stretch=1)
+
+ # OK/Cancel buttons
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+
+ main_layout.addWidget(button_box)
+
+ def _populate_families(self):
+ '''Populate the families list with smoothly scalable fonts only'''
+ self.families_list.clear()
+
+ # Get all font families
+ all_families = QFontDatabase.families()
+
+ # Filter for smoothly scalable fonts
+ scalable_families = []
+ idx = i = 0
+ for family in all_families:
+ if QFontDatabase.isSmoothlyScalable(family):
+ scalable_families.append(family)
+ if family == self.initial_family:
+ idx = i
+ i += 1
+
+ scalable_families.sort()
+ self.families_list.addItems(scalable_families)
+
+ # Select the initial item if available
+ if self.families_list.count() > 0:
+ self.families_list.setCurrentRow(idx)
+ self._on_family_changed(self.families_list.currentItem(), None)
+
+ def _on_family_changed(self, current, previous):
+ '''When a family is selected, populate the styles list'''
+ if not current:
+ self.styles_list.clear()
+ return
+
+ family = current.text()
+ self.styles_list.clear()
+
+ # Get all styles for this family
+ styles = QFontDatabase.styles(family)
+ idx = 0
+ if family == self.initial_family and self.initial_style in styles:
+ idx = styles.index(self.initial_style)
+ self.styles_list.addItems(styles)
+
+ # Select first style if available
+ if self.styles_list.count() > 0:
+ self.styles_list.setCurrentRow(idx)
+ self._update_preview()
+
+ def _on_style_changed(self, current, previous):
+ '''Update the preview when style changes'''
+ self._update_preview()
+
+ def _update_preview(self):
+ '''Update the preview labels with the selected font'''
+ family_item = self.families_list.currentItem()
+ style_item = self.styles_list.currentItem()
+
+ if not family_item or not style_item:
+ return
+
+ family = family_item.text()
+ style = style_item.text()
+ systems = tuple(QFontDatabase.writingSystems(family))
+ text = ''
+ for s in systems:
+ if (t := QFontDatabase.writingSystemSample(s)):
+ text = t
+ break
+
+ def s(label, sz):
+ font = QFontDatabase.font(family, style, int(sz))
+ font.setPointSizeF(sz)
+ label.setFont(font)
+ label.setText('')
+ if label is self.preview_small:
+ prefix = _('Minimum size:')
+ elif label is self.preview_medium:
+ prefix = _('Base size:')
+ else:
+ prefix = _('Maximum size:')
+ label.setText(prefix + ' ' + text)
+ s(self.preview_small, self.min_size)
+ s(self.preview_medium, self.medium_size)
+ s(self.preview_large, self.max_size)
+
+ def selected_font(self):
+ '''Returns the selected font family and style as a tuple'''
+ family_item = self.families_list.currentItem()
+ style_item = self.styles_list. currentItem()
+
+ if family_item and style_item:
+ return family_item.text(), style_item.text()
+ return None, None
+
+ def get_font(self, size=None):
+ '''Returns a QFont object for the selected family and style'''
+ family, style = self.selected_font()
+ if family and style:
+ if size is None:
+ size = self.medium_size
+ return QFontDatabase.font(family, style, size)
+ return None
+
+ def accept(self):
+ '''Override accept to emit signal with selected font'''
+ family, style = self.selected_font()
+ if family and style:
+ self.fontSelected.emit(family, style)
+ super().accept()
+
+
+if __name__ == '__main__':
+ from calibre.gui2 import Application
+
+ app = Application()
+
+ def show_dialog():
+ dialog = FontSelectionDialog(min_size=10, medium_size=14, max_size=28)
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ family, style = dialog.selected_font()
+ selected_font = dialog.get_font(16)
+ print(f'Selected: {family} - {style}')
+ print(f'Font: {selected_font. family()} {selected_font.styleName()} {selected_font.pointSize()}pt')
+
+ show_dialog()