From 3937e2bfea31daefb2a2f0cbc44c904ad1284579 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 16 Apr 2025 09:13:06 +0530 Subject: [PATCH] Cover grid: Allow configuring different backgrounds for light and dark mode in Preferences->Look & feel->Cover grid --- src/calibre/gui2/__init__.py | 27 ++- src/calibre/gui2/library/alternate_views.py | 7 +- src/calibre/gui2/preferences/__init__.py | 23 +- src/calibre/gui2/preferences/look_feel.py | 3 + .../preferences/look_feel_tabs/cover_grid.py | 202 ++++++++++++------ src/calibre/gui2/widgets2.py | 5 + 6 files changed, 192 insertions(+), 75 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 09f5bfdd9f..5d30fd3fbd 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -426,11 +426,11 @@ def create_defs(): defs['cover_grid_width'] = 0 defs['cover_grid_height'] = 0 defs['cover_grid_spacing'] = 0 - defs['cover_grid_color'] = (80, 80, 80) + defs['cover_grid_background'] = { + 'migrated': False, 'light': (80, 80, 80), 'dark': (45, 45, 45), 'light_texture': None, 'dark_texture': None} defs['cover_grid_cache_size_multiple'] = 5 defs['cover_grid_disk_cache_size'] = 2500 defs['cover_grid_show_title'] = False - defs['cover_grid_texture'] = None defs['cover_corner_radius'] = 0 defs['cover_corner_radius_unit'] = 'px' defs['show_vl_tabs'] = False @@ -1805,3 +1805,26 @@ def clip_border_radius(painter, rect): yield finally: painter.restore() + + +def resolve_grid_color(which='color', for_dark: bool | None = None, use_defaults: bool = False): + if use_defaults: + s = gprefs.defaults['cover_grid_background'] + else: + s = gprefs['cover_grid_background'] + if not s['migrated']: + s = s.copy() + s['migrated'] = True + legacy = gprefs.pop('cover_grid_color', None) + if legacy is not None and tuple(legacy) != (80, 80, 80): + s['light'] = s['dark'] = legacy + legacy = gprefs.pop('cover_grid_texture', None) + if legacy is not None: + s['light_texture'] = s['dark_texture'] = legacy + gprefs['cover_grid_background'] = s + if for_dark is None: + for_dark = QApplication.instance().is_dark_theme + key = 'dark' if for_dark else 'light' + if which == 'color': + return s[key] + return s[f'{key}_texture'] diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py index 24069c7e92..ea294b7583 100644 --- a/src/calibre/gui2/library/alternate_views.py +++ b/src/calibre/gui2/library/alternate_views.py @@ -59,7 +59,7 @@ from qt.core import ( from calibre import fit_image, human_readable, prepare_string_for_xml from calibre.constants import DEBUG, config_dir, islinux from calibre.ebooks.metadata import fmt_sidx, rating_to_stars -from calibre.gui2 import clip_border_radius, config, empty_index, gprefs, rating_font +from calibre.gui2 import clip_border_radius, config, empty_index, gprefs, rating_font, resolve_grid_color from calibre.gui2.dnd import path_from_qurl from calibre.gui2.gestures import GestureManager from calibre.gui2.library.caches import CoverCache, ThumbnailCache @@ -794,6 +794,7 @@ class GridView(QListView): self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.set_color() + QApplication.instance().palette_changed.connect(self.set_color) self.ignore_render_requests = Event() dpr = self.device_pixel_ratio # Up the version number if anything changes in how images are stored in @@ -880,8 +881,8 @@ class GridView(QListView): self.update(idx) def set_color(self): - r, g, b = gprefs['cover_grid_color'] - tex = gprefs['cover_grid_texture'] + r, g, b = resolve_grid_color() + tex = resolve_grid_color(which='texture') pal = self.palette() bgcol = QColor(r, g, b) pal.setColor(QPalette.ColorRole.Base, bgcol) diff --git a/src/calibre/gui2/preferences/__init__.py b/src/calibre/gui2/preferences/__init__.py index 3fd4d32b95..9308fd7166 100644 --- a/src/calibre/gui2/preferences/__init__.py +++ b/src/calibre/gui2/preferences/__init__.py @@ -365,18 +365,25 @@ class LazyConfigWidgetBase(ConfigWidgetBase): super().__init__(parent) self.lazy_init_called = False + def ensure_lazy_initialized(self): + if not self.lazy_init_called: + if hasattr(self, 'lazy_initialize'): + self.lazy_initialize() + self.lazy_init_called = True + def set_changed_signal(self, changed_signal): self.changed_signal.connect(changed_signal) + def restore_defaults(self): + self.ensure_lazy_initialized() + super().restore_defaults() + def showEvent(self, event): # called when the widget is actually displays. We can't do something like # lazy_genesis because Qt does "things" before showEvent() is called. In # particular, the register function doesn't work with combo boxes if # genesis isn't called before everything else. Why is a mystery. - if not self.lazy_init_called: - if hasattr(self, 'lazy_initialize'): - self.lazy_initialize() - self.lazy_init_called = True + self.ensure_lazy_initialized() super().showEvent(event) @@ -407,7 +414,7 @@ def init_gui(): def show_config_widget(category, name, gui=None, show_restart_msg=False, - parent=None, never_shutdown=False): + parent=None, never_shutdown=False, callback=None): ''' Show the preferences plugin identified by category and name @@ -457,6 +464,8 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False, w.initialize() w.do_on_child_tabs('initialize') d.restore_geometry(gprefs, conf_name) + if callback is not None: + callback(w) d.exec() d.save_geometry(gprefs, conf_name) rr = getattr(d, 'restart_required', False) @@ -521,8 +530,8 @@ class TableWidgetWithMoveByKeyPress(QTableWidget): # Testing {{{ -def test_widget(category, name, gui=None): - show_config_widget(category, name, gui=gui, show_restart_msg=True) +def test_widget(category, name, gui=None, callback=None): + show_config_widget(category, name, gui=gui, show_restart_msg=True, callback=callback) def test_all(): diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 4b93410235..bd8eb76fb5 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -36,6 +36,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def restore_defaults(self): ConfigWidgetBase.restore_defaults(self) + for w in self.tabWidget.all_widgets: + if hasattr(w, 'restore_defaults'): + w.restore_defaults() self.changed_signal.emit() def refresh_gui(self, gui): 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 ba5d7166f8..626a5f08de 100644 --- a/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.py +++ b/src/calibre/gui2/preferences/look_feel_tabs/cover_grid.py @@ -7,10 +7,26 @@ __docformat__ = 'restructuredtext en' from threading import Thread -from qt.core import QBrush, QColor, QColorDialog, QDialog, QPainter, QPixmap, QPushButton, QSize, QSizePolicy, Qt, QTabWidget, QWidget, pyqtSignal +from qt.core import ( + QBrush, + QColor, + QColorDialog, + QDialog, + QHBoxLayout, + QLabel, + QPainter, + QPixmap, + QPushButton, + QSize, + QSizePolicy, + Qt, + QTabWidget, + QWidget, + pyqtSignal, +) from calibre import human_readable -from calibre.gui2 import gprefs, open_local_file, question_dialog +from calibre.gui2 import gprefs, open_local_file, question_dialog, resolve_grid_color from calibre.gui2.library.alternate_views import CM_TO_INCH, auto_height from calibre.gui2.preferences import LazyConfigWidgetBase from calibre.gui2.preferences.look_feel_tabs.cover_grid_ui import Ui_Form @@ -21,35 +37,128 @@ from calibre.utils.icu import sort_key class Background(QWidget): + changed_signal = pyqtSignal() + def __init__(self, parent): QWidget.__init__(self, parent) - self.bcol = QColor(*gprefs['cover_grid_color']) - self.btex = gprefs['cover_grid_texture'] - self.update_brush() + self.l = QHBoxLayout(self) + self.l.setContentsMargins(0, 0, 0, 0) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.initialized = False + self.changed_signal.connect(self.update_brush) + + def load_from_gprefs(self, use_defaults=False): + self.bcol_dark = QColor(*resolve_grid_color(for_dark=True, use_defaults=use_defaults)) + self.bcol_light = QColor(*resolve_grid_color(for_dark=False, use_defaults=use_defaults)) + self.btex_dark = resolve_grid_color('texture', for_dark=True, use_defaults=use_defaults) + self.btex_light = resolve_grid_color('texture', for_dark=False, use_defaults=use_defaults) + self.update_brush() + + def lazy_initialize(self): + if self.initialized: + return + self.initialized = True + l = self.layout() + text = ( + '

{}
' + '{}
' + '{}

') + self.light_label = la = QLabel(text.format('black', _('Light'), _('Change color'), _('Change texture'))) + la.linkActivated.connect(self.light_link_activated) + l.addWidget(la) + self.dark_label = la = QLabel(text.format('white', _('Dark'), _('Change color'), _('Change texture'))) + la.linkActivated.connect(self.dark_link_activated) + l.addWidget(la) + self.load_from_gprefs() + + def change_color(self, light=False): + which = _('light') if light else _('dark') + col = QColorDialog.getColor(self.bcol_light if light else self.bcol_dark, + self, _('Choose {} background color for the Cover grid').format(which)) + + if col.isValid(): + if light: + self.bcol_light = col + else: + self.bcol_dark = col + btex = self.btex_light if light else self.btex_dark + if btex: + if question_dialog( + self, _('Remove background image?'), + _('There is currently a background image set, so the color' + ' you have chosen will not be visible. Remove the background image?')): + if light: + self.btex_light = None + else: + self.btex_dark = None + self.changed_signal.emit() + + def change_texture(self, light=False): + from calibre.gui2.preferences.texture_chooser import TextureChooser + btex = self.btex_light if light else self.btex_dark + d = TextureChooser(parent=self, initial=btex) + if d.exec() == QDialog.DialogCode.Accepted: + if light: + self.btex_light = d.texture + else: + self.btex_dark = d.texture + self.changed_signal.emit() + + def light_link_activated(self, url): + if 'texture' in url: + self.change_texture(light=True) + else: + self.change_color(light=True) + + def dark_link_activated(self, url): + if 'texture' in url: + self.change_texture(light=False) + else: + self.change_color(light=False) + + def commit(self): + s = gprefs['cover_grid_background'].copy() + s['light'] = tuple(self.bcol_light.getRgb())[:3] + s['dark'] = tuple(self.bcol_dark.getRgb())[:3] + s['light_texture'] = self.btex_light + s['dark_texture'] = self.btex_dark + gprefs['cover_grid_background'] = s + + def restore_defaults(self): + self.load_from_gprefs(use_defaults=True) def update_brush(self): - self.brush = QBrush(self.bcol) - if self.btex: - from calibre.gui2.preferences.texture_chooser import texture_path - path = texture_path(self.btex) + self.light_brush = QBrush(self.bcol_light) + self.dark_brush = QBrush(self.bcol_dark) + def dotex(path, brush): if path: - p = QPixmap(path) - try: - dpr = self.devicePixelRatioF() - except AttributeError: - dpr = self.devicePixelRatio() - p.setDevicePixelRatio(dpr) - self.brush.setTexture(p) + from calibre.gui2.preferences.texture_chooser import texture_path + path = texture_path(path) + if path: + p = QPixmap(path) + try: + dpr = self.devicePixelRatioF() + except AttributeError: + dpr = self.devicePixelRatio() + p.setDevicePixelRatio(dpr) + brush.setTexture(p) + dotex(self.btex_light, self.light_brush) + dotex(self.btex_dark, self.dark_brush) self.update() def sizeHint(self): return QSize(200, 120) def paintEvent(self, ev): + self.lazy_initialize() painter = QPainter(self) - painter.fillRect(ev.rect(), self.brush) + r = self.rect() + light = r.adjusted(0, 0, -r.width()//2, 0) + dark = r.adjusted(light.width(), 0, 0, 0) + painter.fillRect(light, self.light_brush) + painter.fillRect(dark, self.dark_brush) painter.end() + super().paintEvent(ev) class CoverGridTab(QTabWidget, LazyConfigWidgetBase, Ui_Form): @@ -87,18 +196,11 @@ class CoverGridTab(QTabWidget, LazyConfigWidgetBase, Ui_Form): l = self.cg_background_box.layout() self.cg_bg_widget = w = Background(self) + w.changed_signal.connect(self.changed_signal) l.addWidget(w, 0, 0, 3, 1) - self.cover_grid_color_button = b = QPushButton(_('Change &color'), self) - b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - l.addWidget(b, 0, 1) - b.clicked.connect(self.change_cover_grid_color) - self.cover_grid_texture_button = b = QPushButton(_('Change &background image'), self) - b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - l.addWidget(b, 1, 1) - b.clicked.connect(self.change_cover_grid_texture) self.cover_grid_default_appearance_button = b = QPushButton(_('Restore default &appearance'), self) b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - l.addWidget(b, 2, 1) + l.addWidget(b, 0, 1) b.clicked.connect(self.restore_cover_grid_appearance) self.cover_grid_empty_cache.clicked.connect(self.empty_cache) self.cover_grid_open_cache.clicked.connect(self.open_cg_cache) @@ -117,8 +219,7 @@ class CoverGridTab(QTabWidget, LazyConfigWidgetBase, Ui_Form): self.grid_rules.lazy_initialize() self.lazy_init_called = True self.blockSignals(False) - self.set_cg_color(gprefs['cover_grid_color']) - self.set_cg_texture(gprefs['cover_grid_texture']) + self.cg_bg_widget.lazy_initialize() self.update_aspect_ratio() def show_current_cache_usage(self): @@ -169,55 +270,30 @@ class CoverGridTab(QTabWidget, LazyConfigWidgetBase, Ui_Form): self.gui.grid_view.thumbnail_cache.empty() self.calc_cache_size() - def set_cg_color(self, val): - self.cg_bg_widget.bcol = QColor(*val) - self.cg_bg_widget.update_brush() - - def set_cg_texture(self, val): - self.cg_bg_widget.btex = val - self.cg_bg_widget.update_brush() - - def change_cover_grid_color(self): - col = QColorDialog.getColor(self.cg_bg_widget.bcol, - self.gui, _('Choose background color for the Cover grid')) - if col.isValid(): - col = tuple(col.getRgb())[:3] - self.set_cg_color(col) - self.changed_signal.emit() - if self.cg_bg_widget.btex: - if question_dialog( - self, _('Remove background image?'), - _('There is currently a background image set, so the color' - ' you have chosen will not be visible. Remove the background image?')): - self.set_cg_texture(None) - - def change_cover_grid_texture(self): - from calibre.gui2.preferences.texture_chooser import TextureChooser - d = TextureChooser(parent=self, initial=self.cg_bg_widget.btex) - if d.exec() == QDialog.DialogCode.Accepted: - self.set_cg_texture(d.texture) - self.changed_signal.emit() - def restore_cover_grid_appearance(self): - self.set_cg_color(gprefs.defaults['cover_grid_color']) - self.set_cg_texture(gprefs.defaults['cover_grid_texture']) + self.cg_bg_widget.restore_defaults() self.changed_signal.emit() def commit(self): with BusyCursor(): self.grid_rules.commit() - gprefs['cover_grid_color'] = tuple(self.cg_bg_widget.bcol.getRgb())[:3] - gprefs['cover_grid_texture'] = self.cg_bg_widget.btex + self.cg_bg_widget.commit() return LazyConfigWidgetBase.commit(self) def restore_defaults(self): LazyConfigWidgetBase.restore_defaults(self) self.grid_rules.restore_defaults() - self.set_cg_color(gprefs.defaults['cover_grid_color']) - self.set_cg_texture(gprefs.defaults['cover_grid_texture']) + self.cg_bg_widget.restore_defaults() self.changed_signal.emit() def refresh_gui(self, gui): gui.library_view.refresh_grid() gui.grid_view.refresh_settings() gui.update_auto_scroll_timeout() + + +if __name__ == '__main__': + from calibre.gui2 import Application + from calibre.gui2.preferences import test_widget + app = Application([]) + test_widget('Interface', 'Look & Feel', callback=lambda w: w.sections_view.setCurrentRow(1)) diff --git a/src/calibre/gui2/widgets2.py b/src/calibre/gui2/widgets2.py index 8841e6995a..448af3f97c 100644 --- a/src/calibre/gui2/widgets2.py +++ b/src/calibre/gui2/widgets2.py @@ -730,6 +730,11 @@ class ScrollingTabWidget(QTabWidget): sw.setStyleSheet(f'#{name} {{ background: transparent }}') return sw + @property + def all_widgets(self): + for i in range(self.count()): + yield self.widget(i).widget() + def indexOf(self, page): for i in range(self.count()): t = self.widget(i)