From b47e0d512594f70644a53c32af5ad324a5e01409 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 4 Feb 2024 22:24:42 +0530 Subject: [PATCH] UI for customizing palettes Needs integration in to calibre preferences --- src/calibre/gui2/__init__.py | 4 + src/calibre/gui2/dialogs/palette.py | 191 ++++++++++++++++++++++++++++ src/calibre/gui2/palette.py | 83 ++++++++++-- 3 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 src/calibre/gui2/dialogs/palette.py diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index f18bd8fabf..2cc77969ab 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -427,6 +427,10 @@ def create_defs(): defs['show_notes_in_tag_browser'] = False defs['icons_on_right_in_tag_browser'] = True defs['cover_browser_narrow_view_position'] = 'automatic' + defs['dark_palette_name'] = '' + defs['light_palette_name'] = '' + defs['dark_palettes'] = {} + defs['light_palettes'] = {} def migrate_tweak(tweak_name, pref_name): # If the tweak has been changed then leave the tweak in the file so diff --git a/src/calibre/gui2/dialogs/palette.py b/src/calibre/gui2/dialogs/palette.py new file mode 100644 index 0000000000..3e0c6e0077 --- /dev/null +++ b/src/calibre/gui2/dialogs/palette.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2024, Kovid Goyal + + +from qt.core import ( + QCheckBox, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QPalette, QScrollArea, + QSize, QSizePolicy, QTabWidget, QVBoxLayout, QWidget, pyqtSignal, +) + +from calibre.gui2 import Application, gprefs +from calibre.gui2.palette import ( + default_dark_palette, default_light_palette, palette_colors, palette_from_dict, +) +from calibre.gui2.widgets2 import ColorButton, Dialog + + +class Color(QWidget): + + changed = pyqtSignal() + + def __init__(self, key: str, desc: str, parent: 'PaletteColors', palette: QPalette, default_palette: QPalette, mode_name: str, group=''): + super().__init__(parent) + self.key = key + self.setting_key = (key + '-' + group) if group else key + self.mode_name = mode_name + self.default_palette = default_palette + self.color_key = QPalette.ColorGroup.Disabled if group == 'disabled' else QPalette.ColorGroup.Active, getattr(QPalette.ColorRole, key) + self.initial_color = palette.color(*self.color_key) + self.l = l = QHBoxLayout(self) + self.button = b = ColorButton(self.initial_color.name(), self) + b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + b.color_changed.connect(self.color_changed) + l.addWidget(b) + + self.la = la = QLabel(desc) + la.setBuddy(b) + l.addWidget(la) + + def restore_defaults(self): + self.button.color = self.default_palette.color(*self.color_key) + + def color_changed(self): + self.changed.emit() + self.la.setStyleSheet('QLabel { font-style: italic }') + + @property + def value(self): + ans = self.button.color + if ans != self.default_palette.color(*self.color_key): + return ans + + +class PaletteColors(QWidget): + + def __init__(self, palette: QPalette, default_palette: QPalette, mode_name: str, parent=None): + super().__init__(parent) + self.link_colors = {} + self.mode_name = mode_name + self.foreground_colors = {} + self.background_colors = {} + self.default_palette = default_palette + + for key, desc in palette_colors().items(): + if 'Text' in key: + self.foreground_colors[key] = desc + elif 'Link' in key: + self.link_colors[key] = desc + else: + self.background_colors[key] = desc + + self.l = l = QVBoxLayout(self) + self.colors = [] + + def header(text): + ans = QLabel(text) + f = ans.font() + f.setBold(True) + ans.setFont(f) + return ans + + def c(x, desc): + w = Color(x, desc, self, palette, default_palette, mode_name) + l.addWidget(w) + self.colors.append(w) + + l.addWidget(header(_('Background colors'))) + for x, desc in self.background_colors.items(): + c(x, desc) + + l.addWidget(header(_('Foreground (text) colors'))) + for x, desc in self.foreground_colors.items(): + c(x, desc) + + l.addWidget(header(_('Foreground (text) colors when disabled'))) + for x, desc in self.foreground_colors.items(): + c(x, desc) + + l.addWidget(header(_('Link colors'))) + for x, desc in self.link_colors.items(): + c(x, desc) + + @property + def value(self): + ans = {} + for w in self.colors: + v = w.value + if v is not None: + ans[w.setting_key] = w.value + return ans + + def restore_defaults(self): + for w in self.colors: + w.restore_defaults() + + +class PaletteWidget(QWidget): + + def __init__(self, mode_name='light', parent=None): + super().__init__(parent) + self.mode_name = mode_name + self.mode_title = {'dark': _('dark'), 'light': _('light')}[mode_name] + self.l = l = QVBoxLayout(self) + self.la = la = QLabel(_('These colors will be used for the calibre interface when calibre is in "{}" mode').format(self.mode_title)) + l.addWidget(la) + la.setWordWrap(True) + self.use_custom = uc = QCheckBox(_('Use a &custom color scheme')) + uc.setChecked(bool(gprefs[f'{mode_name}_palette_name'])) + l.addWidget(uc) + uc.toggled.connect(self.use_custom_toggled) + + pdata = gprefs[f'{mode_name}_palettes'].get('__current__', {}) + default_palette = default_dark_palette() if mode_name == 'dark' else default_light_palette() + palette = palette_from_dict(pdata, default_palette) + self.sa = sa = QScrollArea(self) + l.addWidget(sa) + self.palette_colors = pc = PaletteColors(palette, default_palette, mode_name, self) + sa.setWidget(pc) + self.use_custom_toggled() + + def sizeHint(self): + return QSize(800, 600) + + def use_custom_toggled(self): + self.palette_colors.setEnabled(self.use_custom.isChecked()) + + def apply_settings(self): + val = self.palette_colors.value + v = gprefs[f'{self.mode_name}_palettes'] + v['__current__'] = val + gprefs[f'{self.mode_name}_palettes'] = v + gprefs[f'{self.mode_name}_palette_name'] = '__current__' if self.use_custom.isChecked() else '' + + def restore_defaults(self): + self.use_custom.setChecked(False) + self.palette_colors.restore_defaults() + + +class PaletteConfig(Dialog): + + def __init__(self, parent=None): + super().__init__(_('Customize the colors used by calibre'), 'customize-palette', parent=parent) + + def setup_ui(self): + self.l = l = QVBoxLayout(self) + self.tabs = tabs = QTabWidget(self) + l.addWidget(tabs) + self.light_tab = lt = PaletteWidget(parent=self) + tabs.addTab(lt, _('&Light mode colors')) + self.dark_tab = dt = PaletteWidget('dark', parent=self) + tabs.addTab(dt, _('&Dark mode colors')) + l.addWidget(self.bb) + b = self.bb.addButton(_('Restore &defaults'), QDialogButtonBox.ButtonRole.ActionRole) + b.clicked.connect(self.restore_defaults) + + def apply_settings(self): + with gprefs: + self.light_tab.apply_settings() + self.dark_tab.apply_settings() + + def restore_defaults(self): + self.light_tab.restore_defaults() + self.dark_tab.restore_defaults() + + +if __name__ == '__main__': + app = Application([]) + d = PaletteConfig() + if d.exec() == QDialog.DialogCode.Accepted: + d.apply_settings() + del d + del app diff --git a/src/calibre/gui2/palette.py b/src/calibre/gui2/palette.py index 7a9f279355..db0197d413 100644 --- a/src/calibre/gui2/palette.py +++ b/src/calibre/gui2/palette.py @@ -3,10 +3,11 @@ import os import sys -from contextlib import contextmanager +from contextlib import contextmanager, suppress +from functools import lru_cache from qt.core import ( QApplication, QByteArray, QColor, QDataStream, QIcon, QIODeviceBase, QObject, - QPalette, QProxyStyle, QStyle, Qt + QPalette, QProxyStyle, QStyle, Qt, ) from calibre.constants import DEBUG, dark_link_color, ismacos, iswindows @@ -64,7 +65,7 @@ QPalette.serialize_as_python = serialize_palette_as_python QPalette.unserialize_from_bytes = unserialize_palette -def dark_palette(): +def default_dark_palette(): p = QPalette() disabled_color = QColor(127,127,127) p.setColor(QPalette.ColorRole.Window, dark_color) @@ -75,22 +76,22 @@ def dark_palette(): p.setColor(QPalette.ColorRole.ToolTipBase, dark_color) p.setColor(QPalette.ColorRole.ToolTipText, dark_text_color) p.setColor(QPalette.ColorRole.Text, dark_text_color) - p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_color) p.setColor(QPalette.ColorRole.Button, dark_color) p.setColor(QPalette.ColorRole.ButtonText, dark_text_color) - p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_color) p.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) p.setColor(QPalette.ColorRole.Link, dark_link_color) p.setColor(QPalette.ColorRole.LinkVisited, Qt.GlobalColor.darkMagenta) - p.setColor(QPalette.ColorRole.Highlight, QColor(0x0b, 0x45, 0xc4)) p.setColor(QPalette.ColorRole.HighlightedText, dark_text_color) + + p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_color) p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, disabled_color) + p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_color) return p -def light_palette(): # {{{ +def default_light_palette(): p = QPalette() disabled_color = QColor(120,120,120) p.setColor(QPalette.ColorRole.Window, light_color) @@ -101,21 +102,81 @@ def light_palette(): # {{{ p.setColor(QPalette.ColorRole.ToolTipBase, light_color) p.setColor(QPalette.ColorRole.ToolTipText, light_text_color) p.setColor(QPalette.ColorRole.Text, light_text_color) - p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_color) p.setColor(QPalette.ColorRole.Button, light_color) p.setColor(QPalette.ColorRole.ButtonText, light_text_color) - p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_color) p.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) p.setColor(QPalette.ColorRole.Link, light_link_color) p.setColor(QPalette.ColorRole.LinkVisited, Qt.GlobalColor.magenta) - p.setColor(QPalette.ColorRole.Highlight, QColor(48, 140, 198)) p.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.white) + + p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_color) + p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_color) p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, disabled_color) return p -# }}} + +@lru_cache +def palette_colors(): + return { + 'WindowText': _('A general foreground color'), + 'Text': _('The foreground color for text input widgets'), + 'ButtonText': _('The foreground color for buttons'), + 'PlaceholderText': _('Placeholder text in text input widgets'), + 'ToolTipText': _('The foreground color for tool tips'), + 'BrightText': _('A "bright" text color'), + 'HighlightedText': _('The foreground color for highlighted items'), + + 'Window': _('A general background color'), + 'Base': _('The background color for text input widgets'), + 'Button': _('The background color for buttons'), + 'AlternateBase': _('The background color for alternate rows in tables and lists'), + 'ToolTipBase': _('The background color for tool tips'), + 'Highlight': _('The background color for highlighted items'), + + 'Link': _('The color for links'), + 'LinkVisited': _('The color for visited links'), + } + + +def palette_from_dict(data: dict[str, str], default_palette: QPalette) -> QPalette: + + def s(key, group=QPalette.ColorGroup.All): + role = getattr(QPalette.ColorRole, key) + grp = '' + if group == QPalette.ColorGroup.Disabled: + grp = 'disabled-' + c = QColor.fromString(data.get(grp + key, '')) + if c.isValid(): + p.setColor(group, role, c) + + p = QPalette() + for key in palette_colors(): + s(key) + for key in ('Text', 'ButtonText', 'HighlightedText'): + s(key, QPalette.ColorGroup.Disabled) + return p.resolve(default_palette) + + +def dark_palette(): + from calibre.gui2 import gprefs + ans = default_dark_palette() + if gprefs['dark_palette_name']: + pdata = gprefs['dark_palettes'].get(gprefs['dark_palette_name']) + with suppress(Exception): + return palette_from_dict(pdata, ans) + return ans + + +def light_palette(): + from calibre.gui2 import gprefs + ans = default_light_palette() + if gprefs['light_palette_name']: + pdata = gprefs['light_palettes'].get(gprefs['light_palette_name']) + with suppress(Exception): + return palette_from_dict(pdata, ans) + return ans standard_pixmaps = { # {{{