| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /lib/calibre/calibre/gui2/ |
Upload File : |
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
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,
)
from calibre.constants import DEBUG, dark_link_color, ismacos, iswindows
dark_link_color = QColor(dark_link_color)
dark_color = QColor(45,45,45)
dark_text_color = QColor('#ddd')
light_color = QColor(0xf0, 0xf0, 0xf0)
light_text_color = QColor(0,0,0)
light_link_color = QColor(0, 0, 255)
class UseCalibreIcons(QProxyStyle):
def standardIcon(self, standard_pixmap, option=None, widget=None):
ic = QApplication.instance().get_qt_standard_icon(standard_pixmap)
if ic.isNull():
return super().standardIcon(standard_pixmap, option, widget)
return ic
def palette_is_dark(self):
col = self.color(QPalette.ColorRole.Window)
return max(col.getRgb()[:3]) < 115
def serialize_palette(self):
ba = QByteArray()
ds = QDataStream(ba, QIODeviceBase.OpenModeFlag.WriteOnly)
ds << self
return bytes(ba)
def unserialize_palette(self, data: bytes):
QDataStream(QByteArray(data)) >> self
def serialize_palette_as_python(self):
lines = []
for group in QPalette.ColorGroup:
if group in (QPalette.ColorGroup.All, QPalette.ColorGroup.NColorGroups):
continue
for role in QPalette.ColorRole:
if role == QPalette.ColorRole.NColorRoles:
continue
c = self.color(group, role)
lines.append(
f'self.setColor(QPalette.ColorGroup.{group.name}, QPalette.ColorRole.{role.name}, QColor({c.red()}, {c.green()}, {c.blue()}, {c.alpha()}))')
return '\n'.join(lines)
QPalette.is_dark_theme = palette_is_dark
QPalette.serialize_as_bytes = serialize_palette
QPalette.serialize_as_python = serialize_palette_as_python
QPalette.unserialize_from_bytes = unserialize_palette
def default_dark_palette():
p = QPalette()
disabled_color = QColor(127,127,127)
p.setColor(QPalette.ColorRole.Window, dark_color)
p.setColor(QPalette.ColorRole.WindowText, dark_text_color)
p.setColor(QPalette.ColorRole.PlaceholderText, disabled_color)
p.setColor(QPalette.ColorRole.Base, QColor(18,18,18))
p.setColor(QPalette.ColorRole.AlternateBase, dark_color)
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.ColorRole.Button, dark_color)
p.setColor(QPalette.ColorRole.ButtonText, dark_text_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 default_light_palette():
p = QPalette()
disabled_color = QColor(120,120,120)
p.setColor(QPalette.ColorRole.Window, light_color)
p.setColor(QPalette.ColorRole.WindowText, light_text_color)
p.setColor(QPalette.ColorRole.PlaceholderText, disabled_color)
p.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.white)
p.setColor(QPalette.ColorRole.AlternateBase, QColor(245, 245, 245))
p.setColor(QPalette.ColorRole.ToolTipBase, QColor(0xff, 0xff, 0xdc))
p.setColor(QPalette.ColorRole.ToolTipText, light_text_color)
p.setColor(QPalette.ColorRole.Text, light_text_color)
p.setColor(QPalette.ColorRole.Button, light_color)
p.setColor(QPalette.ColorRole.ButtonText, light_text_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 = { # {{{
QStyle.StandardPixmap.SP_DialogYesButton: 'ok.png',
QStyle.StandardPixmap.SP_DialogNoButton: 'window-close.png',
QStyle.StandardPixmap.SP_DialogCloseButton: 'close.png',
QStyle.StandardPixmap.SP_DialogOkButton: 'ok.png',
QStyle.StandardPixmap.SP_DialogCancelButton: 'window-close.png',
QStyle.StandardPixmap.SP_DialogHelpButton: 'help.png',
QStyle.StandardPixmap.SP_DialogOpenButton: 'document_open.png',
QStyle.StandardPixmap.SP_DialogSaveButton: 'save.png',
QStyle.StandardPixmap.SP_DialogApplyButton: 'ok.png',
QStyle.StandardPixmap.SP_DialogDiscardButton: 'trash.png',
QStyle.StandardPixmap.SP_MessageBoxInformation: 'dialog_information.png',
QStyle.StandardPixmap.SP_MessageBoxWarning: 'dialog_warning.png',
QStyle.StandardPixmap.SP_MessageBoxCritical: 'dialog_error.png',
QStyle.StandardPixmap.SP_MessageBoxQuestion: 'dialog_question.png',
QStyle.StandardPixmap.SP_BrowserReload: 'view-refresh.png',
QStyle.StandardPixmap.SP_LineEditClearButton: 'clear_left.png',
QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton: 'v-ellipsis.png',
QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton: 'h-ellipsis.png',
QStyle.StandardPixmap.SP_FileDialogBack: 'back.png',
QStyle.StandardPixmap.SP_ArrowRight: 'forward.png',
QStyle.StandardPixmap.SP_ArrowLeft: 'back.png',
QStyle.StandardPixmap.SP_ArrowBack: 'back.png',
QStyle.StandardPixmap.SP_ArrowForward: 'forward.png',
QStyle.StandardPixmap.SP_ArrowUp: 'arrow-up.png',
QStyle.StandardPixmap.SP_ArrowDown: 'arrow-down.png',
QStyle.StandardPixmap.SP_FileDialogToParent: 'arrow-up.png',
QStyle.StandardPixmap.SP_FileDialogNewFolder: 'tb_folder.png',
QStyle.StandardPixmap.SP_FileDialogListView: 'format-list-unordered.png',
QStyle.StandardPixmap.SP_FileDialogDetailedView: 'format-list-ordered.png',
} # }}}
class PaletteManager(QObject):
color_palette: str
using_calibre_style: bool
is_dark_theme: bool
def __init__(self, force_calibre_style, headless):
from calibre.gui2 import gprefs
super().__init__()
self.color_palette = gprefs['color_palette']
ui_style = gprefs['ui_style']
self.is_dark_theme = False
self.ignore_palette_changes = False
if force_calibre_style:
self.using_calibre_style = True
else:
if iswindows or ismacos:
self.using_calibre_style = ui_style != 'system'
else:
self.using_calibre_style = os.environ.get('CALIBRE_USE_SYSTEM_THEME', '0') == '0'
args = []
self.args_to_qt = tuple(args)
if ismacos and not headless:
from calibre_extensions.cocoa import set_appearance
set_appearance(self.color_palette)
def initialize(self):
app = QApplication.instance()
self.setParent(app)
if not self.using_calibre_style and app.style().objectName() == 'fusion':
# Since Qt is using the fusion style anyway, specialize it
self.using_calibre_style = True
@property
def use_dark_palette(self):
app = QApplication.instance()
system_is_dark = False
return self.color_palette == 'dark' or (self.color_palette == 'system' and system_is_dark)
def setup_styles(self):
if self.using_calibre_style:
app = QApplication.instance()
self.set_dark_mode_palette() if self.use_dark_palette else self.set_light_mode_palette()
QApplication.instance().setAttribute(Qt.ApplicationAttribute.AA_SetPalette, True)
if DEBUG:
print('Using calibre Qt style:', self.using_calibre_style, file=sys.stderr)
if self.using_calibre_style:
self.load_calibre_style()
else:
app = QApplication.instance()
self.native_proxy_style = UseCalibreIcons(app.style())
app.setStyle(self.native_proxy_style)
self.on_palette_change()
def get_qt_standard_icon(self, standard_pixmap):
from qt.core import QStyle
sp = QStyle.StandardPixmap(standard_pixmap)
val = standard_pixmaps.get(sp)
if val is None:
return QIcon()
return QIcon.ic(val)
def load_calibre_style(self):
transient_scroller = 0
if ismacos:
from calibre_extensions.cocoa import transient_scroller
transient_scroller = transient_scroller()
app = QApplication.instance()
from calibre_extensions.progress_indicator import CalibreStyle
self.calibre_style = style = CalibreStyle(transient_scroller)
app.setStyle(style)
def on_palette_change(self):
app = QApplication.instance()
app.cached_qimage.cache_clear()
app.cached_qpixmap.cache_clear()
self.is_dark_theme = app.palette().is_dark_theme()
QIcon.ic.set_theme()
app.setProperty('is_dark_theme', self.is_dark_theme)
if self.using_calibre_style:
ss = 'QTabBar::tab:selected { font-style: italic }\n\n'
if self.is_dark_theme:
ss += 'QMenu { border: 1px solid palette(shadow); }'
ss += '''
QTabBar::tab:selected {
background-color: palette(base);
border: 1px solid gray;
padding: 2px 8px;
margin-left: -4px;
margin-right: -4px;
}
QTabBar::tab:top:selected {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-width: 0;
}
QTabBar::tab:bottom:selected {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-width: 0;
}
QTabBar::tab:first:selected {
margin-left: 0; /* the first selected tab has nothing to overlap with on the left */
}
QTabBar::tab:last:selected {
margin-right: 0; /* the last selected tab has nothing to overlap with on the right */
}
QTabBar::tab:only-one {
margin: 0; /* if there is only one tab, we don't want overlapping margins */
}
'''
app.setStyleSheet(ss)
app.palette_changed.emit()
def set_dark_mode_palette(self):
self.set_palette(dark_palette())
def set_light_mode_palette(self):
self.set_palette(light_palette())
def color_scheme_changed(self, new_color_scheme):
if DEBUG:
print('System Color Scheme changed to:', new_color_scheme, file=sys.stderr)
if self.color_palette != 'system' or not self.using_calibre_style:
return
if new_color_scheme == Qt.ColorScheme.Dark:
self.set_dark_mode_palette()
elif new_color_scheme == Qt.ColorScheme.Light:
self.set_light_mode_palette()
elif new_color_scheme == Qt.ColorScheme.Unknown:
self.set_light_mode_palette()
self.on_palette_change()
@contextmanager
def changing_palette(self):
orig = self.ignore_palette_changes
self.ignore_palette_changes = True
try:
yield
finally:
self.ignore_palette_changes = orig
def set_palette(self, pal):
with self.changing_palette():
QApplication.instance().setPalette(pal)
def on_qt_palette_change(self):
if self.ignore_palette_changes:
if DEBUG:
print('ApplicationPaletteChange event ignored', file=sys.stderr)
else:
if DEBUG:
print('ApplicationPaletteChange event received', file=sys.stderr)
if self.using_calibre_style:
pal = dark_palette() if self.use_dark_palette else light_palette()
if QApplication.instance().palette().color(QPalette.ColorRole.Window) != pal.color(QPalette.ColorRole.Window):
if DEBUG:
print('Detected a spontaneous palette change by Qt, reverting it', file=sys.stderr)
self.set_palette(pal)
self.on_palette_change()
def refresh_palette(self):
from calibre.gui2 import gprefs
self.color_palette = gprefs['color_palette']
if ismacos:
from calibre_extensions.cocoa import set_appearance
set_appearance(self.color_palette)
system_is_dark = False
is_dark = self.color_palette == 'dark' or (self.color_palette == 'system' and system_is_dark)
pal = dark_palette() if is_dark else light_palette()
self.set_palette(pal)
self.on_palette_change()
def tree_view_hover_style(self):
g1, g2 = '#e7effd', '#cbdaf1'
border_size = '1px'
if self.is_dark_theme:
c = QApplication.instance().palette().color(QPalette.ColorRole.Highlight)
c = c.lighter(180)
g1 = g2 = c.name()
border_size = '0px'
return f'''
QTreeView::item:hover {{
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 {g1}, stop: 1 {g2});
border: {border_size} solid #bfcde4;
border-radius: 6px;
}}
'''