Merge branch 'split-look_feel' of https://github.com/un-pogaz/calibre

This commit is contained in:
Kovid Goyal 2025-02-02 08:38:01 +05:30
commit 2a8f7de119
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
23 changed files with 2289 additions and 2214 deletions

View File

@ -5,621 +5,23 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from collections import defaultdict from qt.core import QKeySequence, QListWidgetItem, Qt
from functools import partial
from threading import Thread
from qt.core import ( from calibre.gui2.preferences import ConfigWidgetBase, test_widget
QApplication,
QBrush,
QColor,
QColorDialog,
QComboBox,
QDialog,
QDialogButtonBox,
QFont,
QFontDialog,
QFontInfo,
QFormLayout,
QHeaderView,
QIcon,
QKeySequence,
QLabel,
QLineEdit,
QListWidgetItem,
QPainter,
QPixmap,
QPushButton,
QSize,
QSizePolicy,
Qt,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
pyqtSignal,
)
from calibre import human_readable
from calibre.constants import ismacos, iswindows
from calibre.ebooks.metadata.book.render import DEFAULT_AUTHOR_LINK
from calibre.ebooks.metadata.sources.prefs import msprefs
from calibre.gui2 import config, default_author_link, error_dialog, gprefs, icon_resource_manager, open_local_file, qt_app, question_dialog
from calibre.gui2.custom_column_widgets import get_field_list as em_get_field_list
from calibre.gui2.dialogs.quickview import get_qv_field_list
from calibre.gui2.library.alternate_views import CM_TO_INCH, auto_height
from calibre.gui2.preferences import ConfigWidgetBase, Setting, set_help_tips, test_widget
from calibre.gui2.preferences.coloring import EditRules
from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, export_layout, import_layout, move_field_down, move_field_up, reset_layout
from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2.widgets import BusyCursor
from calibre.gui2.widgets2 import Dialog
from calibre.startup import connect_lambda
from calibre.utils.config import prefs
from calibre.utils.icu import sort_key
from calibre.utils.localization import available_translations, get_lang, get_language
from calibre.utils.resources import get_path as P
from calibre.utils.resources import set_data
from polyglot.builtins import iteritems
class DefaultAuthorLink(QWidget): # {{{
changed_signal = pyqtSignal()
def __init__(self, parent):
QWidget.__init__(self, parent)
l = QVBoxLayout(parent)
l.addWidget(self)
l.setContentsMargins(0, 0, 0, 0)
l = QFormLayout(self)
l.setContentsMargins(0, 0, 0, 0)
l.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
self.choices = c = QComboBox()
c.setMinimumContentsLength(30)
for text, data in [
(_('Search for the author on Goodreads'), 'search-goodreads'),
(_('Search for the author on Amazon'), 'search-amzn'),
(_('Search for the author in your calibre library'), 'search-calibre'),
(_('Search for the author on Wikipedia'), 'search-wikipedia'),
(_('Search for the author on Google Books'), 'search-google'),
(_('Search for the book on Goodreads'), 'search-goodreads-book'),
(_('Search for the book on Amazon'), 'search-amzn-book'),
(_('Search for the book on Google Books'), 'search-google-book'),
(_('Use a custom search URL'), 'url'),
]:
c.addItem(text, data)
l.addRow(_('Clicking on &author names should:'), c)
self.custom_url = u = QLineEdit(self)
u.setToolTip(_(
'Enter the URL to search. It should contain the string {0}'
'\nwhich will be replaced by the author name. For example,'
'\n{1}').format('{author}', 'https://en.wikipedia.org/w/index.php?search={author}'))
u.textChanged.connect(self.changed_signal)
u.setPlaceholderText(_('Enter the URL'))
c.currentIndexChanged.connect(self.current_changed)
l.addRow(u)
self.current_changed()
c.currentIndexChanged.connect(self.changed_signal)
@property
def value(self):
k = self.choices.currentData()
if k == 'url':
return self.custom_url.text()
return k if k != DEFAULT_AUTHOR_LINK else None
@value.setter
def value(self, val):
i = self.choices.findData(val)
if i < 0:
i = self.choices.findData('url')
self.custom_url.setText(val)
self.choices.setCurrentIndex(i)
def current_changed(self):
k = self.choices.currentData()
self.custom_url.setVisible(k == 'url')
# }}}
# IdLinksEditor {{{
class IdLinksRuleEdit(Dialog):
def __init__(self, key='', name='', template='', parent=None):
title = _('Edit rule') if key else _('Create a new rule')
Dialog.__init__(self, title=title, name='id-links-rule-editor', parent=parent)
self.key.setText(key), self.nw.setText(name), self.template.setText(template or 'https://example.com/{id}')
if self.size().height() < self.sizeHint().height():
self.resize(self.sizeHint())
@property
def rule(self):
return self.key.text().lower(), self.nw.text(), self.template.text()
def setup_ui(self):
self.l = l = QFormLayout(self)
l.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
l.addRow(QLabel(_(
'The key of the identifier, for example, in isbn:XXX, the key is "isbn"')))
self.key = k = QLineEdit(self)
l.addRow(_('&Key:'), k)
l.addRow(QLabel(_(
'The name that will appear in the Book details panel')))
self.nw = n = QLineEdit(self)
l.addRow(_('&Name:'), n)
la = QLabel(_(
'The template used to create the link.'
' The placeholder {0} in the template will be replaced'
' with the actual identifier value. Use {1} to avoid the value'
' being quoted.').format('{id}', '{id_unquoted}'))
la.setWordWrap(True)
l.addRow(la)
self.template = t = QLineEdit(self)
l.addRow(_('&Template:'), t)
t.selectAll()
t.setFocus(Qt.FocusReason.OtherFocusReason)
l.addWidget(self.bb)
def accept(self):
r = self.rule
for i, which in enumerate([_('Key'), _('Name'), _('Template')]):
if not r[i]:
return error_dialog(self, _('Value needed'), _(
'The %s field cannot be empty') % which, show=True)
Dialog.accept(self)
class IdLinksEditor(Dialog):
def __init__(self, parent=None):
Dialog.__init__(self, title=_('Create rules for identifiers'), name='id-links-rules-editor', parent=parent)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.la = la = QLabel(_(
'Create rules to convert identifiers into links.'))
la.setWordWrap(True)
l.addWidget(la)
items = []
for k, lx in iteritems(msprefs['id_link_rules']):
for n, t in lx:
items.append((k, n, t))
items.sort(key=lambda x: sort_key(x[1]))
self.table = t = QTableWidget(len(items), 3, self)
t.setHorizontalHeaderLabels([_('Key'), _('Name'), _('Template')])
for r, (key, val, template) in enumerate(items):
t.setItem(r, 0, QTableWidgetItem(key))
t.setItem(r, 1, QTableWidgetItem(val))
t.setItem(r, 2, QTableWidgetItem(template))
l.addWidget(t)
t.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.cb = b = QPushButton(QIcon.ic('plus.png'), _('&Add rule'), self)
connect_lambda(b.clicked, self, lambda self: self.edit_rule())
self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
self.rb = b = QPushButton(QIcon.ic('minus.png'), _('&Remove rule'), self)
connect_lambda(b.clicked, self, lambda self: self.remove_rule())
self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
self.eb = b = QPushButton(QIcon.ic('modified.png'), _('&Edit rule'), self)
connect_lambda(b.clicked, self, lambda self: self.edit_rule(self.table.currentRow()))
self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
l.addWidget(self.bb)
def sizeHint(self):
return QSize(700, 550)
def accept(self):
rules = defaultdict(list)
for r in range(self.table.rowCount()):
def item(c):
return self.table.item(r, c).text()
rules[item(0)].append([item(1), item(2)])
msprefs['id_link_rules'] = dict(rules)
Dialog.accept(self)
def edit_rule(self, r=-1):
key = name = template = ''
if r > -1:
key, name, template = (self.table.item(r, c).text() for c in range(3))
d = IdLinksRuleEdit(key, name, template, self)
if d.exec() == QDialog.DialogCode.Accepted:
if r < 0:
self.table.setRowCount(self.table.rowCount() + 1)
r = self.table.rowCount() - 1
rule = d.rule
for c in range(3):
self.table.setItem(r, c, QTableWidgetItem(rule[c]))
self.table.scrollToItem(self.table.item(r, 0))
def remove_rule(self):
r = self.table.currentRow()
if r > -1:
self.table.removeRow(r)
# }}}
class EMDisplayedFields(DisplayedFields): # {{{
def __init__(self, db, parent=None):
DisplayedFields.__init__(self, db, parent)
def initialize(self, use_defaults=False, pref_data_override=None):
self.beginResetModel()
self.fields = [[x[0], x[1]] for x in
em_get_field_list(self.db, use_defaults=use_defaults, pref_data_override=pref_data_override)]
self.endResetModel()
self.changed = True
def commit(self):
if self.changed:
self.db.new_api.set_pref('edit_metadata_custom_columns_to_display', self.fields)
# }}}
class QVDisplayedFields(DisplayedFields): # {{{
def __init__(self, db, parent=None):
DisplayedFields.__init__(self, db, parent)
def initialize(self, use_defaults=False):
self.beginResetModel()
self.fields = [[x[0], x[1]] for x in
get_qv_field_list(self.db.field_metadata, use_defaults=use_defaults)]
self.endResetModel()
self.changed = True
def commit(self):
if self.changed:
self.db.new_api.set_pref('qv_display_fields', self.fields)
# }}}
class TBDisplayedFields(DisplayedFields): # {{{
# The code in this class depends on the fact that the tag browser is
# initialized before this class is instantiated.
def __init__(self, db, parent=None, category_icons=None):
DisplayedFields.__init__(self, db, parent, category_icons=category_icons)
from calibre.gui2.ui import get_gui
self.gui = get_gui()
def initialize(self, use_defaults=False, pref_data_override=None):
tv = self.gui.tags_view
cat_ord = tv.model().get_ordered_categories(use_defaults=use_defaults,
pref_data_override=pref_data_override)
if use_defaults:
hc = []
self.changed = True
elif pref_data_override:
hc = [k for k,v in pref_data_override if not v]
self.changed = True
else:
hc = tv.hidden_categories
self.beginResetModel()
self.fields = [[x, x not in hc] for x in cat_ord]
self.endResetModel()
def commit(self):
if self.changed:
self.db.prefs.set('tag_browser_hidden_categories', [k for k,v in self.fields if not v])
self.db.prefs.set('tag_browser_category_order', [k for k,v in self.fields])
# }}}
class TBPartitionedFields(DisplayedFields): # {{{
# The code in this class depends on the fact that the tag browser is
# initialized before this class is instantiated.
def __init__(self, db, parent=None, category_icons=None):
DisplayedFields.__init__(self, db, parent, category_icons=category_icons)
from calibre.gui2.ui import get_gui
self.gui = get_gui()
def filter_user_categories(self, tv):
cats = tv.model().categories
answer = {}
filtered = set()
for key,name in cats.items():
if key.startswith('@'):
key = key.partition('.')[0]
name = key[1:]
if key not in filtered:
answer[key] = name
filtered.add(key)
return answer
def initialize(self, use_defaults=False, pref_data_override=None):
tv = self.gui.tags_view
cats = self.filter_user_categories(tv)
ans = []
if use_defaults:
ans = [[k, True] for k in cats.keys()]
self.changed = True
elif pref_data_override:
po = dict(pref_data_override)
ans = [[k, po.get(k, True)] for k in cats.keys()]
self.changed = True
else:
# Check if setting not migrated yet
cats_to_partition = frozenset(self.db.prefs.get('tag_browser_dont_collapse', gprefs.get('tag_browser_dont_collapse')) or ())
for key in cats:
ans.append([key, key not in cats_to_partition])
self.beginResetModel()
self.fields = ans
self.endResetModel()
def commit(self):
if self.changed:
# Migrate to a per-library setting
self.db.prefs.set('tag_browser_dont_collapse', [k for k,v in self.fields if not v])
# }}}
class BDVerticalCats(DisplayedFields): # {{{
def __init__(self, db, parent=None, category_icons=None):
DisplayedFields.__init__(self, db, parent, category_icons=category_icons)
from calibre.gui2.ui import get_gui
self.gui = get_gui()
def initialize(self, use_defaults=False, pref_data_override=None):
fm = self.db.field_metadata
cats = [k for k in fm if fm[k]['name'] and fm[k]['is_multiple'] and not k.startswith('#')]
cats.append('path')
cats.extend([k for k in fm if fm[k]['name'] and fm[k]['is_multiple'] and k.startswith('#')])
ans = []
if use_defaults:
ans = [[k, False] for k in cats]
self.changed = True
elif pref_data_override:
ph = dict(pref_data_override)
ans = [[k, ph.get(k, False)] for k in cats]
self.changed = True
else:
vertical_cats = self.db.prefs.get('book_details_vertical_categories') or ()
for key in cats:
ans.append([key, key in vertical_cats])
self.beginResetModel()
self.fields = ans
self.endResetModel()
def commit(self):
if self.changed:
self.db.prefs.set('book_details_vertical_categories', [k for k,v in self.fields if v])
# }}}
class Background(QWidget): # {{{
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.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
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)
if path:
p = QPixmap(path)
try:
dpr = self.devicePixelRatioF()
except AttributeError:
dpr = self.devicePixelRatio()
p.setDevicePixelRatio(dpr)
self.brush.setTexture(p)
self.update()
def sizeHint(self):
return QSize(200, 120)
def paintEvent(self, ev):
painter = QPainter(self)
painter.fillRect(ev.rect(), self.brush)
painter.end()
# }}}
class LanguageSetting(Setting):
def commit(self):
val = self.get_gui_val()
oldval = self.get_config_val()
if val != oldval:
gprefs.set('last_used_language', oldval)
return super().commit()
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
size_calculated = pyqtSignal(object)
def genesis(self, gui): def genesis(self, gui):
self.gui = gui self.gui = gui
self.ui_style_available = True
if not ismacos and not iswindows:
self.label_widget_style.setVisible(False)
self.opt_ui_style.setVisible(False)
self.ui_style_available = False
db = gui.library_view.model().db
r = self.register
try:
self.icon_theme_title = icon_resource_manager.user_theme_title
except Exception:
self.icon_theme_title = _('Default icons')
self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
self.commit_icon_theme = None
self.icon_theme_button.clicked.connect(self.choose_icon_theme)
self.default_author_link = DefaultAuthorLink(self.default_author_link_container)
self.default_author_link.changed_signal.connect(self.changed_signal)
r('ui_style', gprefs, restart_required=True, choices=[(_('System default'), 'system'), (_('calibre style'), 'calibre')])
r('book_list_tooltips', gprefs)
r('dnd_merge', gprefs)
r('wrap_toolbar_text', gprefs, restart_required=True)
r('show_layout_buttons', gprefs)
r('show_sb_all_actions_button', gprefs)
# r('show_sb_preference_button', gprefs)
r('row_numbers_in_book_list', gprefs)
r('bd_show_cover', gprefs)
r('bd_overlay_cover_size', gprefs)
r('cover_corner_radius', gprefs)
r('cover_corner_radius_unit', gprefs, choices=[(_('Pixels'), 'px'), (_('Percentage'), '%')])
r('cover_grid_width', gprefs)
r('cover_grid_height', gprefs)
r('cover_grid_cache_size_multiple', gprefs)
r('cover_grid_disk_cache_size', gprefs)
r('cover_grid_spacing', gprefs)
r('cover_grid_show_title', gprefs)
r('emblem_size', gprefs)
r('emblem_position', gprefs, choices=[
(_('Left'), 'left'), (_('Top'), 'top'), (_('Right'), 'right'), (_('Bottom'), 'bottom')])
r('book_list_extra_row_spacing', gprefs)
r('booklist_grid', gprefs)
r('book_details_comments_heading_pos', gprefs, choices=[
(_('Never'), 'hide'), (_('Above text'), 'above'), (_('Beside text'), 'side')])
r('book_details_note_link_icon_width', gprefs)
self.id_links_button.clicked.connect(self.edit_id_link_rules)
def get_esc_lang(l):
if l == 'en':
return 'English'
return get_language(l)
lang = get_lang()
if lang is None or lang not in available_translations():
lang = 'en'
items = [(l, get_esc_lang(l)) for l in available_translations()
if l != lang]
if lang != 'en':
items.append(('en', get_esc_lang('en')))
items.sort(key=lambda x: x[1].lower())
choices = [(y, x) for x, y in items]
# Default language is the autodetected one
choices = [(get_language(lang), lang)] + choices
lul = gprefs.get('last_used_language')
if lul and (lul in available_translations() or lul in ('en', 'eng')):
choices.insert(1, ((get_language(lul), lul)))
r('language', prefs, choices=choices, restart_required=True, setting=LanguageSetting)
r('disable_animations', config)
r('systray_icon', config, restart_required=True)
r('show_splash_screen', gprefs)
r('disable_tray_notification', config)
r('use_roman_numerals_for_series_number', config)
choices = [(_('Off'), 'off'), (_('Small'), 'small'),
(_('Medium-small'), 'mid-small'), (_('Medium'), 'medium'), (_('Large'), 'large')]
r('toolbar_icon_size', gprefs, choices=choices)
choices = [(_('If there is enough room'), 'auto'), (_('Always'), 'always'),
(_('Never'), 'never')]
r('toolbar_text', gprefs, choices=choices)
fm = db.field_metadata
choices = sorted(((fm[k]['name'], k) for k in fm.displayable_field_keys() if fm[k]['name']),
key=lambda x:sort_key(x[0]))
r('field_under_covers_in_grid', db.prefs, choices=choices)
choices = [(_('Default'), 'default'), (_('Compact metadata'), 'alt1'),
(_('All on 1 tab'), 'alt2')]
r('edit_metadata_single_layout', gprefs,
choices=[(_('Default'), 'default'), (_('Compact metadata'), 'alt1'),
(_('All on 1 tab'), 'alt2')])
r('edit_metadata_ignore_display_order', db.prefs)
r('edit_metadata_elision_point', gprefs,
choices=[(_('Left'), 'left'), (_('Middle'), 'middle'),
(_('Right'), 'right')])
r('edit_metadata_elide_labels', gprefs)
r('edit_metadata_single_use_2_cols_for_custom_fields', gprefs)
r('edit_metadata_bulk_cc_label_length', gprefs)
r('edit_metadata_single_cc_label_length', gprefs)
r('edit_metadata_templates_only_F2_on_booklist', gprefs)
self.current_font = self.initial_font = None
self.change_font_button.clicked.connect(self.change_font)
self.display_model = DisplayedFields(self.gui.current_db, self.field_display_order)
self.display_model.dataChanged.connect(self.changed_signal)
self.field_display_order.setModel(self.display_model)
mu = partial(move_field_up, self.field_display_order, self.display_model)
md = partial(move_field_down, self.field_display_order, self.display_model)
self.df_up_button.clicked.connect(mu)
self.df_down_button.clicked.connect(md)
self.field_display_order.set_movement_functions(mu, md)
self.em_display_model = EMDisplayedFields(self.gui.current_db, self.em_display_order)
self.em_display_model.dataChanged.connect(self.changed_signal)
self.em_display_order.setModel(self.em_display_model)
mu = partial(move_field_up, self.em_display_order, self.em_display_model)
md = partial(move_field_down, self.em_display_order, self.em_display_model)
self.em_display_order.set_movement_functions(mu, md)
self.em_up_button.clicked.connect(mu)
self.em_down_button.clicked.connect(md)
self.em_export_layout_button.clicked.connect(partial(export_layout, self, model=self.em_display_model))
self.em_import_layout_button.clicked.connect(partial(import_layout, self, model=self.em_display_model))
self.em_reset_layout_button.clicked.connect(partial(reset_layout, model=self.em_display_model))
self.bd_vertical_cats_model = BDVerticalCats(self.gui.current_db, self.tb_hierarchy_tab.tb_hierarchical_cats)
self.bd_vertical_cats_model.dataChanged.connect(self.changed_signal)
self.bd_vertical_cats.setModel(self.bd_vertical_cats_model)
self.edit_rules = EditRules(self.tabWidget)
self.edit_rules.changed.connect(self.changed_signal)
self.tabWidget.addTab(self.edit_rules, QIcon.ic('format-fill-color.png'), _('Column &coloring'))
self.icon_rules = EditRules(self.tabWidget)
self.icon_rules.changed.connect(self.changed_signal)
self.tabWidget.addTab(self.icon_rules, QIcon.ic('icon_choose.png'), _('Column &icons'))
self.grid_rules = EditRules(self.emblems_tab)
self.grid_rules.changed.connect(self.changed_signal)
self.emblems_tab.setLayout(QVBoxLayout())
self.emblems_tab.layout().addWidget(self.grid_rules)
self.tabWidget.setCurrentIndex(0) self.tabWidget.setCurrentIndex(0)
self.tabWidget.tabBar().setVisible(False) self.tabWidget.tabBar().setVisible(False)
keys = [QKeySequence('F11', QKeySequence.SequenceFormat.PortableText), QKeySequence( keys = [QKeySequence('F11', QKeySequence.SequenceFormat.PortableText), QKeySequence(
'Ctrl+Shift+F', QKeySequence.SequenceFormat.PortableText)] 'Ctrl+Shift+F', QKeySequence.SequenceFormat.PortableText)]
keys = [str(x.toString(QKeySequence.SequenceFormat.NativeText)) for x in keys] keys = [str(x.toString(QKeySequence.SequenceFormat.NativeText)) for x in keys]
self.size_calculated.connect(self.update_cg_cache_size, type=Qt.ConnectionType.QueuedConnection)
self.tabWidget.currentChanged.connect(self.tab_changed)
l = self.cg_background_box.layout()
self.cg_bg_widget = w = Background(self)
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)
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)
connect_lambda(self.cover_grid_smaller_cover.clicked, self, lambda self: self.resize_cover(True))
connect_lambda(self.cover_grid_larger_cover.clicked, self, lambda self: self.resize_cover(False))
self.cover_grid_reset_size.clicked.connect(self.cg_reset_size)
self.opt_cover_grid_disk_cache_size.setMinimum(self.gui.grid_view.thumbnail_cache.min_disk_cache)
self.opt_cover_grid_disk_cache_size.setMaximum(self.gui.grid_view.thumbnail_cache.min_disk_cache * 100)
self.opt_cover_grid_width.valueChanged.connect(self.update_aspect_ratio)
self.opt_cover_grid_height.valueChanged.connect(self.update_aspect_ratio)
self.opt_book_details_css.textChanged.connect(self.changed_signal)
from calibre.gui2.tweak_book.editor.text import get_highlighter, get_theme
self.css_highlighter = get_highlighter('css')()
self.css_highlighter.apply_theme(get_theme(None))
self.css_highlighter.set_document(self.opt_book_details_css.document())
for i in range(self.tabWidget.count()): for i in range(self.tabWidget.count()):
self.sections_view.addItem(QListWidgetItem(self.tabWidget.tabIcon(i), self.tabWidget.tabText(i).replace('&', ''))) self.sections_view.addItem(QListWidgetItem(self.tabWidget.tabIcon(i), self.tabWidget.tabText(i).replace('&', '')))
self.sections_view.setCurrentRow(self.tabWidget.currentIndex()) self.sections_view.setCurrentRow(self.tabWidget.currentIndex())
@ -628,256 +30,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.sections_view.setSpacing(4) self.sections_view.setSpacing(4)
self.sections_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.sections_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.tabWidget.currentWidget().setFocus(Qt.FocusReason.OtherFocusReason) self.tabWidget.currentWidget().setFocus(Qt.FocusReason.OtherFocusReason)
self.opt_ui_style.currentIndexChanged.connect(self.update_color_palette_state)
self.opt_gui_layout.addItem(_('Wide'), 'wide')
self.opt_gui_layout.addItem(_('Narrow'), 'narrow')
self.opt_gui_layout.currentIndexChanged.connect(self.changed_signal)
set_help_tips(self.opt_gui_layout, config.help('gui_layout'))
self.button_adjust_colors.clicked.connect(self.adjust_colors)
def adjust_colors(self):
from calibre.gui2.dialogs.palette import PaletteConfig
d = PaletteConfig(self)
if d.exec() == QDialog.DialogCode.Accepted:
d.apply_settings()
self.changed_signal.emit()
def initial_tab_changed(self): def initial_tab_changed(self):
self.sections_view.setCurrentRow(self.tabWidget.currentIndex()) self.sections_view.setCurrentRow(self.tabWidget.currentIndex())
def update_color_palette_state(self):
if self.ui_style_available:
enabled = self.opt_ui_style.currentData() == 'calibre'
self.button_adjust_colors.setEnabled(enabled)
def choose_icon_theme(self):
from calibre.gui2.icon_theme import ChooseTheme
d = ChooseTheme(self)
if d.exec() == QDialog.DialogCode.Accepted:
self.commit_icon_theme = d.commit_changes
self.icon_theme_title = d.new_theme_title or _('Default icons')
self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
self.changed_signal.emit()
def edit_id_link_rules(self):
if IdLinksEditor(self).exec() == QDialog.DialogCode.Accepted:
self.changed_signal.emit()
@property
def current_cover_size(self):
cval = self.opt_cover_grid_height.value()
wval = self.opt_cover_grid_width.value()
if cval < 0.1:
dpi = self.opt_cover_grid_height.logicalDpiY()
cval = auto_height(self.opt_cover_grid_height) / dpi / CM_TO_INCH
if wval < 0.1:
wval = 0.75 * cval
return wval, cval
def update_aspect_ratio(self, *args):
width, height = self.current_cover_size
ar = width / height
self.cover_grid_aspect_ratio.setText(_('Current aspect ratio (width/height): %.2g') % ar)
def resize_cover(self, smaller):
wval, cval = self.current_cover_size
ar = wval / cval
delta = 0.2 * (-1 if smaller else 1)
cval += delta
cval = max(0, cval)
self.opt_cover_grid_height.setValue(cval)
self.opt_cover_grid_width.setValue(cval * ar)
def cg_reset_size(self):
self.opt_cover_grid_width.setValue(0)
self.opt_cover_grid_height.setValue(0)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.default_author_link.value = default_author_link()
font = gprefs['font']
if font is not None:
font = list(font)
font.append(gprefs.get('font_stretch', QFont.Stretch.Unstretched))
self.current_font = self.initial_font = font
self.update_font_display()
self.display_model.initialize()
self.em_display_model.initialize()
self.bd_vertical_cats_model.initialize()
db = self.gui.current_db
mi = []
try:
rows = self.gui.current_view().selectionModel().selectedRows()
for row in rows:
if row.isValid():
mi.append(db.new_api.get_proxy_metadata(db.data.index_to_id(row.row())))
except:
pass
self.edit_rules.initialize(db.field_metadata, db.prefs, mi, 'column_color_rules')
self.icon_rules.initialize(db.field_metadata, db.prefs, mi, 'column_icon_rules')
self.grid_rules.initialize(db.field_metadata, db.prefs, mi, 'cover_grid_icon_rules')
self.set_cg_color(gprefs['cover_grid_color'])
self.set_cg_texture(gprefs['cover_grid_texture'])
self.update_aspect_ratio()
self.opt_book_details_css.blockSignals(True)
self.opt_book_details_css.setPlainText(P('templates/book_details.css', data=True).decode('utf-8'))
self.opt_book_details_css.blockSignals(False)
self.update_color_palette_state()
self.opt_gui_layout.setCurrentIndex(0 if self.gui.layout_container.is_wide else 1)
def open_cg_cache(self):
open_local_file(self.gui.grid_view.thumbnail_cache.location)
def update_cg_cache_size(self, size):
self.cover_grid_current_disk_cache.setText(
_('Current space used: %s') % human_readable(size))
def tab_changed(self, index):
if self.tabWidget.currentWidget() is self.cover_grid_tab:
self.show_current_cache_usage()
def show_current_cache_usage(self):
t = Thread(target=self.calc_cache_size)
t.daemon = True
t.start()
def calc_cache_size(self):
self.size_calculated.emit(self.gui.grid_view.thumbnail_cache.current_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 empty_cache(self):
self.gui.grid_view.thumbnail_cache.empty()
self.calc_cache_size()
def restore_defaults(self): def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self) ConfigWidgetBase.restore_defaults(self)
self.default_author_link.value = DEFAULT_AUTHOR_LINK
ofont = self.current_font
self.current_font = None
if ofont is not None:
self.changed_signal.emit()
self.update_font_display()
self.display_model.restore_defaults()
self.em_display_model.restore_defaults()
self.bd_vertical_cats_model.restore_defaults()
gprefs.set('tb_search_order', gprefs.defaults['tb_search_order'])
self.edit_rules.clear()
self.icon_rules.clear()
self.grid_rules.clear()
self.changed_signal.emit() self.changed_signal.emit()
self.set_cg_color(gprefs.defaults['cover_grid_color'])
self.set_cg_texture(gprefs.defaults['cover_grid_texture'])
self.opt_book_details_css.setPlainText(P('templates/book_details.css', allow_user_override=False, data=True).decode('utf-8'))
self.opt_gui_layout.setCurrentIndex(0)
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.changed_signal.emit()
def build_font_obj(self):
font_info = qt_app.original_font if self.current_font is None else self.current_font
font = QFont(*(font_info[:4]))
font.setStretch(font_info[4])
return font
def update_font_display(self):
font = self.build_font_obj()
fi = QFontInfo(font)
name = str(fi.family())
self.font_display.setFont(font)
self.font_display.setText(name + f' [{fi.pointSize()}pt]')
def change_font(self, *args):
fd = QFontDialog(self.build_font_obj(), self)
if fd.exec() == QDialog.DialogCode.Accepted:
font = fd.selectedFont()
fi = QFontInfo(font)
self.current_font = [str(fi.family()), fi.pointSize(),
fi.weight(), fi.italic(), font.stretch()]
self.update_font_display()
self.changed_signal.emit()
def commit(self, *args):
with BusyCursor():
rr = ConfigWidgetBase.commit(self, *args)
if self.current_font != self.initial_font:
gprefs['font'] = (self.current_font[:4] if self.current_font else
None)
gprefs['font_stretch'] = (self.current_font[4] if self.current_font
is not None else QFont.Stretch.Unstretched)
QApplication.setFont(self.font_display.font())
rr = True
self.display_model.commit()
self.em_display_model.commit()
self.bd_vertical_cats_model.commit()
self.edit_rules.commit(self.gui.current_db.prefs)
self.icon_rules.commit(self.gui.current_db.prefs)
self.grid_rules.commit(self.gui.current_db.prefs)
gprefs['cover_grid_color'] = tuple(self.cg_bg_widget.bcol.getRgb())[:3]
gprefs['cover_grid_texture'] = self.cg_bg_widget.btex
if self.commit_icon_theme is not None:
self.commit_icon_theme()
gprefs['default_author_link'] = self.default_author_link.value
bcss = self.opt_book_details_css.toPlainText().encode('utf-8')
defcss = P('templates/book_details.css', data=True, allow_user_override=False)
if defcss == bcss:
bcss = None
set_data('templates/book_details.css', bcss)
self.gui.layout_container.change_layout(self.gui, self.opt_gui_layout.currentIndex() == 0)
return rr
def refresh_gui(self, gui): def refresh_gui(self, gui):
gui.book_details.book_info.refresh_css()
gui.place_layout_buttons()
m = gui.library_view.model() m = gui.library_view.model()
m.update_db_prefs_cache() m.update_db_prefs_cache()
m.beginResetModel(), m.endResetModel() m.beginResetModel(), m.endResetModel()
self.update_font_display()
gui.tags_view.set_look_and_feel()
gui.tags_view.reread_collapse_parameters()
gui.tags_view.model().reset_tag_browser() gui.tags_view.model().reset_tag_browser()
gui.library_view.refresh_book_details(force=True)
gui.library_view.refresh_grid()
gui.library_view.refresh_composite_edit()
gui.library_view.set_row_header_visibility()
for view in 'library memory card_a card_b'.split():
getattr(gui, view + '_view').set_row_header_visibility()
gui.library_view.refresh_row_sizing()
gui.grid_view.refresh_settings()
gui.update_auto_scroll_timeout()
gui.sb_all_gui_actions_button.setVisible(gprefs['show_sb_all_actions_button'])
# gui.sb_preferences_button.setVisible(gprefs['show_sb_preference_button'])
if __name__ == '__main__': if __name__ == '__main__':

File diff suppressed because it is too large Load Diff

View File

@ -7,13 +7,79 @@ __docformat__ = 'restructuredtext en'
import json import json
from qt.core import QAbstractListModel, QIcon, QItemSelectionModel, Qt from qt.core import QAbstractListModel, QComboBox, QFormLayout, QIcon, QItemSelectionModel, QLineEdit, Qt, QVBoxLayout, QWidget, pyqtSignal
from calibre.ebooks.metadata.book.render import DEFAULT_AUTHOR_LINK
from calibre.gui2 import choose_files, choose_save_file, error_dialog from calibre.gui2 import choose_files, choose_save_file, error_dialog
from calibre.gui2.book_details import get_field_list from calibre.gui2.book_details import get_field_list
from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences.coloring import EditRules
from calibre.gui2.ui import get_gui
class DisplayedFields(QAbstractListModel): # {{{ class DefaultAuthorLink(QWidget):
changed_signal = pyqtSignal()
def __init__(self, parent):
QWidget.__init__(self, parent)
l = QVBoxLayout()
l.addWidget(self)
l.setContentsMargins(0, 0, 0, 0)
l = QFormLayout(self)
l.setContentsMargins(0, 0, 0, 0)
l.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
self.choices = c = QComboBox()
c.setMinimumContentsLength(30)
for text, data in [
(_('Search for the author on Goodreads'), 'search-goodreads'),
(_('Search for the author on Amazon'), 'search-amzn'),
(_('Search for the author in your calibre library'), 'search-calibre'),
(_('Search for the author on Wikipedia'), 'search-wikipedia'),
(_('Search for the author on Google Books'), 'search-google'),
(_('Search for the book on Goodreads'), 'search-goodreads-book'),
(_('Search for the book on Amazon'), 'search-amzn-book'),
(_('Search for the book on Google Books'), 'search-google-book'),
(_('Use a custom search URL'), 'url'),
]:
c.addItem(text, data)
l.addRow(_('Clicking on &author names should:'), c)
self.custom_url = u = QLineEdit(self)
u.setToolTip(_(
'Enter the URL to search. It should contain the string {0}'
'\nwhich will be replaced by the author name. For example,'
'\n{1}').format('{author}', 'https://en.wikipedia.org/w/index.php?search={author}'))
u.textChanged.connect(self.changed_signal)
u.setPlaceholderText(_('Enter the URL'))
c.currentIndexChanged.connect(self.current_changed)
l.addRow(u)
self.current_changed()
c.currentIndexChanged.connect(self.changed_signal)
@property
def value(self):
k = self.choices.currentData()
if k == 'url':
return self.custom_url.text()
return k if k != DEFAULT_AUTHOR_LINK else None
@value.setter
def value(self, val):
i = self.choices.findData(val)
if i < 0:
i = self.choices.findData('url')
self.custom_url.setText(val)
self.choices.setCurrentIndex(i)
def current_changed(self):
k = self.choices.currentData()
self.custom_url.setVisible(k == 'url')
def restore_defaults(self):
self.value = DEFAULT_AUTHOR_LINK
class DisplayedFields(QAbstractListModel):
def __init__(self, db, parent=None, pref_name=None, category_icons=None): def __init__(self, db, parent=None, pref_name=None, category_icons=None):
self.pref_name = pref_name or 'book_display_fields' self.pref_name = pref_name or 'book_display_fields'
@ -103,6 +169,47 @@ class DisplayedFields(QAbstractListModel): # {{{
return idx return idx
class LazyEditRulesBase(LazyConfigWidgetBase):
rule_set_name = None
def __init__(self, parent=None):
super().__init__(parent)
self.rules_editor = EditRules(parent)
self.setLayout(self.rules_editor.layout())
def genesis(self, gui):
self.gui = gui
self.rules_editor.changed.connect(self.changed_signal)
def lazy_initialize(self):
if not self.rule_set_name:
raise NotImplementedError('You must define the attribut "rule_set_name" in LazyEditRulesBase subclasses')
self.load_rule_set(self.rule_set_name)
def load_rule_set(self, name):
db = self.gui.current_db
mi = selected_rows_metadatas()
self.rules_editor.initialize(db.field_metadata, db.prefs, mi, name)
def commit(self):
self.rules_editor.commit(self, self.gui.current_db.prefs)
return LazyConfigWidgetBase.commit(self)
def restore_defaults(self):
LazyConfigWidgetBase.restore_defaults(self)
self.rules_editor.clear(self)
self.changed_signal.emit()
class ColumnColorRules(LazyEditRulesBase):
rule_set_name = 'column_color_rules'
class ColumnIconRules(LazyEditRulesBase):
rule_set_name = 'column_icon_rules'
def export_layout(in_widget, model=None): def export_layout(in_widget, model=None):
filename = choose_save_file(in_widget, 'look_feel_prefs_import_export_field_list', filename = choose_save_file(in_widget, 'look_feel_prefs_import_export_field_list',
_('Save column list to file'), _('Save column list to file'),
@ -155,4 +262,15 @@ def move_field_down(widget, model):
sm.select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect) sm.select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)
widget.setCurrentIndex(idx) widget.setCurrentIndex(idx)
# }}}
def selected_rows_metadatas():
rslt = []
try:
db = get_gui().current_db
rows = get_gui().current_view().selectionModel().selectedRows()
for row in rows:
if row.isValid():
rslt.append(db.new_api.get_proxy_metadata(db.data.index_to_id(row.row())))
except:
pass
return rslt

View File

@ -0,0 +1,256 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2025, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from collections import defaultdict
from functools import partial
from qt.core import (
QDialog,
QDialogButtonBox,
QFormLayout,
QHeaderView,
QIcon,
QLabel,
QLineEdit,
QPushButton,
QSize,
Qt,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
)
from calibre.ebooks.metadata.sources.prefs import msprefs
from calibre.gui2 import config, default_author_link, error_dialog, gprefs
from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, move_field_down, move_field_up
from calibre.gui2.preferences.look_feel_tabs.book_details_ui import Ui_Form
from calibre.gui2.widgets import BusyCursor
from calibre.gui2.widgets2 import Dialog
from calibre.startup import connect_lambda
from calibre.utils.icu import sort_key
from calibre.utils.resources import set_data
from polyglot.builtins import iteritems
class IdLinksRuleEdit(Dialog):
def __init__(self, key='', name='', template='', parent=None):
title = _('Edit rule') if key else _('Create a new rule')
Dialog.__init__(self, title=title, name='id-links-rule-editor', parent=parent)
self.key.setText(key), self.nw.setText(name), self.template.setText(template or 'https://example.com/{id}')
if self.size().height() < self.sizeHint().height():
self.resize(self.sizeHint())
@property
def rule(self):
return self.key.text().lower(), self.nw.text(), self.template.text()
def setup_ui(self):
self.l = l = QFormLayout(self)
l.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
l.addRow(QLabel(_(
'The key of the identifier, for example, in isbn:XXX, the key is "isbn"')))
self.key = k = QLineEdit(self)
l.addRow(_('&Key:'), k)
l.addRow(QLabel(_(
'The name that will appear in the Book details panel')))
self.nw = n = QLineEdit(self)
l.addRow(_('&Name:'), n)
la = QLabel(_(
'The template used to create the link.'
' The placeholder {0} in the template will be replaced'
' with the actual identifier value. Use {1} to avoid the value'
' being quoted.').format('{id}', '{id_unquoted}'))
la.setWordWrap(True)
l.addRow(la)
self.template = t = QLineEdit(self)
l.addRow(_('&Template:'), t)
t.selectAll()
t.setFocus(Qt.FocusReason.OtherFocusReason)
l.addWidget(self.bb)
def accept(self):
r = self.rule
for i, which in enumerate([_('Key'), _('Name'), _('Template')]):
if not r[i]:
return error_dialog(self, _('Value needed'), _(
'The %s field cannot be empty') % which, show=True)
Dialog.accept(self)
class IdLinksEditor(Dialog):
def __init__(self, parent=None):
Dialog.__init__(self, title=_('Create rules for identifiers'), name='id-links-rules-editor', parent=parent)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.la = la = QLabel(_(
'Create rules to convert identifiers into links.'))
la.setWordWrap(True)
l.addWidget(la)
items = []
for k, lx in iteritems(msprefs['id_link_rules']):
for n, t in lx:
items.append((k, n, t))
items.sort(key=lambda x: sort_key(x[1]))
self.table = t = QTableWidget(len(items), 3, self)
t.setHorizontalHeaderLabels([_('Key'), _('Name'), _('Template')])
for r, (key, val, template) in enumerate(items):
t.setItem(r, 0, QTableWidgetItem(key))
t.setItem(r, 1, QTableWidgetItem(val))
t.setItem(r, 2, QTableWidgetItem(template))
l.addWidget(t)
t.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.cb = b = QPushButton(QIcon.ic('plus.png'), _('&Add rule'), self)
connect_lambda(b.clicked, self, lambda self: self.edit_rule())
self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
self.rb = b = QPushButton(QIcon.ic('minus.png'), _('&Remove rule'), self)
connect_lambda(b.clicked, self, lambda self: self.remove_rule())
self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
self.eb = b = QPushButton(QIcon.ic('modified.png'), _('&Edit rule'), self)
connect_lambda(b.clicked, self, lambda self: self.edit_rule(self.table.currentRow()))
self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
l.addWidget(self.bb)
def sizeHint(self):
return QSize(700, 550)
def accept(self):
rules = defaultdict(list)
for r in range(self.table.rowCount()):
def item(c):
return self.table.item(r, c).text()
rules[item(0)].append([item(1), item(2)])
msprefs['id_link_rules'] = dict(rules)
Dialog.accept(self)
def edit_rule(self, r=-1):
key = name = template = ''
if r > -1:
key, name, template = (self.table.item(r, c).text() for c in range(3))
d = IdLinksRuleEdit(key, name, template, self)
if d.exec() == QDialog.DialogCode.Accepted:
if r < 0:
self.table.setRowCount(self.table.rowCount() + 1)
r = self.table.rowCount() - 1
rule = d.rule
for c in range(3):
self.table.setItem(r, c, QTableWidgetItem(rule[c]))
self.table.scrollToItem(self.table.item(r, 0))
def remove_rule(self):
r = self.table.currentRow()
if r > -1:
self.table.removeRow(r)
class BDVerticalCats(DisplayedFields):
def __init__(self, db, parent=None, category_icons=None):
DisplayedFields.__init__(self, db, parent, category_icons=category_icons)
from calibre.gui2.ui import get_gui
self.gui = get_gui()
def initialize(self, use_defaults=False, pref_data_override=None):
fm = self.db.field_metadata
cats = [k for k in fm if fm[k]['name'] and fm[k]['is_multiple'] and not k.startswith('#')]
cats.append('path')
cats.extend([k for k in fm if fm[k]['name'] and fm[k]['is_multiple'] and k.startswith('#')])
ans = []
if use_defaults:
ans = [[k, False] for k in cats]
self.changed = True
elif pref_data_override:
ph = dict(pref_data_override)
ans = [[k, ph.get(k, False)] for k in cats]
self.changed = True
else:
vertical_cats = self.db.prefs.get('book_details_vertical_categories') or ()
for key in cats:
ans.append([key, key in vertical_cats])
self.beginResetModel()
self.fields = ans
self.endResetModel()
def commit(self):
if self.changed:
self.db.prefs.set('book_details_vertical_categories', [k for k,v in self.fields if v])
class BookDetailsTab(LazyConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
r = self.register
self.default_author_link.changed_signal.connect(self.changed_signal)
r('bd_show_cover', gprefs)
r('bd_overlay_cover_size', gprefs)
r('book_details_comments_heading_pos', gprefs, choices=[
(_('Never'), 'hide'), (_('Above text'), 'above'), (_('Beside text'), 'side')])
r('book_details_note_link_icon_width', gprefs)
self.id_links_button.clicked.connect(self.edit_id_link_rules)
r('use_roman_numerals_for_series_number', config)
self.bd_vertical_cats_model = BDVerticalCats(self.gui.current_db, parent=self)
self.bd_vertical_cats_model.dataChanged.connect(self.changed_signal)
self.bd_vertical_cats.setModel(self.bd_vertical_cats_model)
self.display_model = DisplayedFields(self.gui.current_db, self.field_display_order)
self.display_model.dataChanged.connect(self.changed_signal)
self.field_display_order.setModel(self.display_model)
mu = partial(move_field_up, self.field_display_order, self.display_model)
md = partial(move_field_down, self.field_display_order, self.display_model)
self.df_up_button.clicked.connect(mu)
self.df_down_button.clicked.connect(md)
self.field_display_order.set_movement_functions(mu, md)
self.opt_book_details_css.textChanged.connect(self.changed_signal)
from calibre.gui2.tweak_book.editor.text import get_highlighter, get_theme
self.css_highlighter = get_highlighter('css')()
self.css_highlighter.apply_theme(get_theme(None))
self.css_highlighter.set_document(self.opt_book_details_css.document())
def lazy_initialize(self):
self.blockSignals(True)
self.default_author_link.value = default_author_link()
self.display_model.initialize()
self.bd_vertical_cats_model.initialize()
self.opt_book_details_css.setPlainText(P('templates/book_details.css', data=True).decode('utf-8'))
self.blockSignals(False)
def edit_id_link_rules(self):
if IdLinksEditor(self).exec() == QDialog.DialogCode.Accepted:
self.changed_signal.emit()
def commit(self):
with BusyCursor():
self.display_model.commit()
self.bd_vertical_cats_model.commit()
gprefs['default_author_link'] = self.default_author_link.value
bcss = self.opt_book_details_css.toPlainText().encode('utf-8')
defcss = P('templates/book_details.css', data=True, allow_user_override=False)
if defcss == bcss:
bcss = None
set_data('templates/book_details.css', bcss)
return LazyConfigWidgetBase.commit(self)
def restore_defaults(self):
LazyConfigWidgetBase.restore_defaults(self)
self.default_author_link.restore_defaults()
self.display_model.restore_defaults()
self.bd_vertical_cats_model.restore_defaults()
self.opt_book_details_css.setPlainText(P('templates/book_details.css', allow_user_override=False, data=True).decode('utf-8'))
self.changed_signal.emit()
def refresh_gui(self, gui):
gui.book_details.book_info.refresh_css()
gui.library_view.refresh_book_details(force=True)
gui.library_view.refresh_composite_edit()

View File

@ -0,0 +1,258 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="book_details_tab">
<layout class="QGridLayout" name="gridLayout_6">
<item row="6" column="0">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Text styling</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QPlainTextEdit" name="opt_book_details_css"/>
</item>
</layout>
</widget>
</item>
<item row="5" column="0" colspan="3">
<widget class="QPushButton" name="id_links_button">
<property name="text">
<string>Create rules to convert &amp;identifiers into links</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="DefaultAuthorLink" name="default_author_link" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QGroupBox" name="categories_on_separate_lines_group_box">
<property name="toolTip">
<string>&lt;p&gt;Check the box if you want the category's values displayed on separate lines instead of separated by commas&lt;/p&gt;</string>
</property>
<property name="title">
<string>Categories on separate lines</string>
</property>
<layout class="QVBoxLayout" name="vblayout_3">
<item>
<widget class="QListView" name="bd_vertical_cats">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="6" column="1">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select displayed metadata</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="1">
<widget class="QToolButton" name="df_down_button">
<property name="toolTip">
<string>Move down. Keyboard shortcut: Ctrl-Down arrow</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QToolButton" name="df_up_button">
<property name="toolTip">
<string>Move up. Keyboard shortcut: Ctrl-Up arrow</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" rowspan="3">
<widget class="ListViewWithMoveByKeyPress" name="field_display_order">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;p&gt;Note: &lt;b&gt;comments&lt;/b&gt;-like columns will always
be displayed at the end unless their &quot;Heading position&quot; is
&quot;Show heading to the side&quot;.&lt;/p&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QHBoxLayout">
<item>
<widget class="QCheckBox" name="opt_bd_show_cover">
<property name="text">
<string>Show &amp;cover</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="opt_bd_overlay_cover_size">
<property name="toolTip">
<string>Show the size of the book's cover in pixels</string>
</property>
<property name="text">
<string>Show cover &amp;size</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
<property name="text">
<string>Use &amp;Roman numerals for series</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_bd1">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_25">
<property name="text">
<string>Show comments &amp;heading:</string>
</property>
<property name="buddy">
<cstring>opt_book_details_comments_heading_pos</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_book_details_comments_heading_pos">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_bd13">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_1022">
<property name="text">
<string>Link and note icon si&amp;ze:</string>
</property>
<property name="buddy">
<cstring>opt_book_details_note_link_icon_width</cstring>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="opt_book_details_note_link_icon_width">
<property name="toolTip">
<string>Increase or decrease the size of the links and notes icons by this number. Larger
than one increases the icon size while smaller than one decreases it.</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.5</double>
</property>
<property name="maximum">
<double>4.0</double>
</property>
<property name="singleStep">
<double>0.1</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_bd13">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ListViewWithMoveByKeyPress</class>
<extends>QListView</extends>
<header>calibre/gui2/preferences.h</header>
</customwidget>
<customwidget>
<class>DefaultAuthorLink</class>
<extends>QWidget</extends>
<header>calibre/gui2/preferences/look_feel_tabs.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
</ui>

View File

@ -0,0 +1,228 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2025, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from threading import Thread
from qt.core import QBrush, QColor, QColorDialog, QDialog, QPainter, QPixmap, QPushButton, QSize, QSizePolicy, Qt, QTabWidget, QVBoxLayout, QWidget, pyqtSignal
from calibre import human_readable
from calibre.gui2 import gprefs, open_local_file, question_dialog
from calibre.gui2.library.alternate_views import CM_TO_INCH, auto_height
from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences.coloring import EditRules
from calibre.gui2.preferences.look_feel_tabs import selected_rows_metadatas
from calibre.gui2.preferences.look_feel_tabs.cover_grid_ui import Ui_Form
from calibre.gui2.widgets import BusyCursor
from calibre.startup import connect_lambda
from calibre.utils.icu import sort_key
class Background(QWidget):
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.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
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)
if path:
p = QPixmap(path)
try:
dpr = self.devicePixelRatioF()
except AttributeError:
dpr = self.devicePixelRatio()
p.setDevicePixelRatio(dpr)
self.brush.setTexture(p)
self.update()
def sizeHint(self):
return QSize(200, 120)
def paintEvent(self, ev):
painter = QPainter(self)
painter.fillRect(ev.rect(), self.brush)
painter.end()
class CoverGridTab(QTabWidget, LazyConfigWidgetBase, Ui_Form):
changed_signal = pyqtSignal()
restart_now = pyqtSignal()
size_calculated = pyqtSignal(object)
def __init__(self, parent=None):
super().__init__(parent)
def genesis(self, gui):
self.gui = gui
db = self.gui.library_view.model().db
r = self.register
r('cover_grid_width', gprefs)
r('cover_grid_height', gprefs)
r('cover_grid_cache_size_multiple', gprefs)
r('cover_grid_disk_cache_size', gprefs)
r('cover_grid_spacing', gprefs)
r('cover_grid_show_title', gprefs)
r('emblem_size', gprefs)
r('emblem_position', gprefs, choices=[
(_('Left'), 'left'), (_('Top'), 'top'), (_('Right'), 'right'), (_('Bottom'), 'bottom')])
fm = db.field_metadata
choices = sorted(((fm[k]['name'], k) for k in fm.displayable_field_keys() if fm[k]['name']),
key=lambda x:sort_key(x[0]))
r('field_under_covers_in_grid', db.prefs, choices=choices)
self.grid_rules = EditRules(self.emblems_tab)
self.grid_rules.changed.connect(self.changed_signal)
self.emblems_tab.setLayout(QVBoxLayout())
self.emblems_tab.layout().addWidget(self.grid_rules)
self.size_calculated.connect(self.update_cg_cache_size, type=Qt.ConnectionType.QueuedConnection)
l = self.cg_background_box.layout()
self.cg_bg_widget = w = Background(self)
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)
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)
connect_lambda(self.cover_grid_smaller_cover.clicked, self, lambda self: self.resize_cover(True))
connect_lambda(self.cover_grid_larger_cover.clicked, self, lambda self: self.resize_cover(False))
self.cover_grid_reset_size.clicked.connect(self.cg_reset_size)
self.opt_cover_grid_disk_cache_size.setMinimum(self.gui.grid_view.thumbnail_cache.min_disk_cache)
self.opt_cover_grid_disk_cache_size.setMaximum(self.gui.grid_view.thumbnail_cache.min_disk_cache * 100)
self.opt_cover_grid_width.valueChanged.connect(self.update_aspect_ratio)
self.opt_cover_grid_height.valueChanged.connect(self.update_aspect_ratio)
def lazy_initialize(self):
self.show_current_cache_usage()
db = self.gui.current_db
self.blockSignals(True)
self.grid_rules.initialize(db.field_metadata, db.prefs, selected_rows_metadatas(), 'cover_grid_icon_rules')
self.blockSignals(False)
self.set_cg_color(gprefs['cover_grid_color'])
self.set_cg_texture(gprefs['cover_grid_texture'])
self.update_aspect_ratio()
def show_current_cache_usage(self):
t = Thread(target=self.calc_cache_size)
t.daemon = True
t.start()
def calc_cache_size(self):
self.size_calculated.emit(self.gui.grid_view.thumbnail_cache.current_size)
@property
def current_cover_size(self):
cval = self.opt_cover_grid_height.value()
wval = self.opt_cover_grid_width.value()
if cval < 0.1:
dpi = self.opt_cover_grid_height.logicalDpiY()
cval = auto_height(self.opt_cover_grid_height) / dpi / CM_TO_INCH
if wval < 0.1:
wval = 0.75 * cval
return wval, cval
def update_aspect_ratio(self):
width, height = self.current_cover_size
ar = width / height
self.cover_grid_aspect_ratio.setText(_('Current aspect ratio (width/height): %.2g') % ar)
def resize_cover(self, smaller):
wval, cval = self.current_cover_size
ar = wval / cval
delta = 0.2 * (-1 if smaller else 1)
cval += delta
cval = max(0, cval)
self.opt_cover_grid_height.setValue(cval)
self.opt_cover_grid_width.setValue(cval * ar)
def cg_reset_size(self):
self.opt_cover_grid_width.setValue(0)
self.opt_cover_grid_height.setValue(0)
def open_cg_cache(self):
open_local_file(self.gui.grid_view.thumbnail_cache.location)
def update_cg_cache_size(self, size):
self.cover_grid_current_disk_cache.setText(
_('Current space used: %s') % human_readable(size))
def empty_cache(self):
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.changed_signal.emit()
def commit(self):
with BusyCursor():
self.grid_rules.commit(self.gui.current_db.prefs)
gprefs['cover_grid_color'] = tuple(self.cg_bg_widget.bcol.getRgb())[:3]
gprefs['cover_grid_texture'] = self.cg_bg_widget.btex
return LazyConfigWidgetBase.commit(self)
def restore_defaults(self):
LazyConfigWidgetBase.restore_defaults(self)
self.grid_rules.clear()
self.set_cg_color(gprefs.defaults['cover_grid_color'])
self.set_cg_texture(gprefs.defaults['cover_grid_texture'])
self.changed_signal.emit()
def refresh_gui(self, gui):
gui.library_view.refresh_grid()
gui.grid_view.refresh_settings()
gui.update_auto_scroll_timeout()

View File

@ -0,0 +1,436 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QTabWidget" name="cover_grid_tab">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_5">
<attribute name="title">
<string>&amp;Layout</string>
</attribute>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_19">
<property name="text">
<string>Control the Cover grid view. You can enable this view by clicking the &quot;Layout&quot; button in the bottom right corner of the main calibre window.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Field to show &amp;under the covers:</string>
</property>
<property name="buddy">
<cstring>opt_field_under_covers_in_grid</cstring>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="opt_field_under_covers_in_grid"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Spac&amp;ing between covers:</string>
</property>
<property name="buddy">
<cstring>opt_cover_grid_spacing</cstring>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QDoubleSpinBox" name="opt_cover_grid_spacing">
<property name="toolTip">
<string>The spacing between covers. A value of zero means calculate automatically based on cover size.</string>
</property>
<property name="specialValueText">
<string>Automatic</string>
</property>
<property name="suffix">
<string> cm</string>
</property>
<property name="decimals">
<number>2</number>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="cg_background_box">
<property name="title">
<string>Background for the Cover grid</string>
</property>
<layout class="QGridLayout" name="gridLayout_5"/>
</widget>
</item>
<item row="10" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Cover size</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="2">
<widget class="QPushButton" name="cover_grid_larger_cover">
<property name="toolTip">
<string>Make the covers larger, maintaining current aspect ratio.</string>
</property>
<property name="text">
<string>&amp;Larger covers</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/plus.png</normaloff>:/images/plus.png</iconset>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="cover_grid_smaller_cover">
<property name="toolTip">
<string>Make the covers smaller, maintaining current aspect ratio.</string>
</property>
<property name="text">
<string>&amp;Smaller covers</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Cover &amp;height: </string>
</property>
<property name="buddy">
<cstring>opt_cover_grid_height</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Cover &amp;width: </string>
</property>
<property name="buddy">
<cstring>opt_cover_grid_width</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="opt_cover_grid_height">
<property name="toolTip">
<string>The height of displayed covers.
Aue of zero means calculate automatically.</string>
</property>
<property name="specialValueText">
<string>Automatic</string>
</property>
<property name="suffix">
<string> cm</string>
</property>
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="3" rowspan="2">
<widget class="QPushButton" name="cover_grid_reset_size">
<property name="toolTip">
<string>Reset size to automatic</string>
</property>
<property name="text">
<string>&amp;Reset size</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="5">
<widget class="QLabel" name="cover_grid_aspect_ratio">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="4">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="opt_cover_grid_width">
<property name="toolTip">
<string>The width of displayed covers.
Aue of zero means calculate automatically.</string>
</property>
<property name="specialValueText">
<string>Automatic</string>
</property>
<property name="suffix">
<string> cm</string>
</property>
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
<item row="0" column="0" colspan="5">
<widget class="QLabel" name="cover_size_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>By default, calibre chooses a cover size based on your computer's screen size. You can change the cover size here:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="11" column="1">
<widget class="QSpinBox" name="opt_emblem_size">
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>16</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Size of the emblems (if any) shown &amp;next to the covers: </string>
</property>
<property name="buddy">
<cstring>opt_emblem_size</cstring>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_cover_grid_show_title">
<property name="text">
<string>Show a &amp;field (such as title) under the covers</string>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>&amp;Location of the emblems shown next to the covers:</string>
</property>
<property name="buddy">
<cstring>opt_emblem_position</cstring>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QComboBox" name="opt_emblem_position"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="emblems_tab">
<attribute name="title">
<string>&amp;Emblems</string>
</attribute>
</widget>
<widget class="QWidget" name="tab_6">
<attribute name="title">
<string>&amp;Performance</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Caching of covers for improved performance</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="5">
<widget class="QLabel" name="label_13">
<property name="text">
<string>There are two kinds of caches that calibre uses to improve performance when rendering covers in the grid view. A disk cache that is kept on your hard disk and stores the cover thumbnails and an in memory cache used to ensure flicker free rendering of covers. For best results, keep the memory cache small and the disk cache large, unless you have a lot of extra RAM in your computer and don't mind it being used by the memory cache.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="opt_cover_grid_disk_cache_size">
<property name="specialValueText">
<string>Disable</string>
</property>
<property name="suffix">
<string> MB</string>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="cover_grid_empty_cache">
<property name="text">
<string>&amp;Empty disk cache</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="cover_grid_open_cache">
<property name="text">
<string>&amp;Open cache folder</string>
</property>
</widget>
</item>
<item row="4" column="2" colspan="3">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>310</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="3">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Number of screenfulls of covers to cache in &amp;memory (keep this small):</string>
</property>
<property name="buddy">
<cstring>opt_cover_grid_cache_size_multiple</cstring>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="label_18">
<property name="text">
<string>Maximum amount of &amp;disk space to use for caching thumbnails: </string>
</property>
<property name="buddy">
<cstring>opt_cover_grid_disk_cache_size</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="opt_cover_grid_cache_size_multiple">
<property name="toolTip">
<string>The maximum number of screenfulls of thumbnails to keep in memory. Increasing this will make rendering faster, at the cost of more memory usage. Note that regardless of this setting, a minimum of one hundred thumbnails are always kept in memory, to ensure flicker free rendering.</string>
</property>
<property name="minimum">
<number>2</number>
</property>
<property name="maximum">
<number>100</number>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="cover_grid_current_disk_cache">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>opt_cover_grid_show_title</sender>
<signal>toggled(bool)</signal>
<receiver>opt_field_under_covers_in_grid</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>86</x>
<y>49</y>
</hint>
<hint type="destinationlabel">
<x>101</x>
<y>51</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -10,7 +10,7 @@ from qt.core import QKeySequence
from calibre.gui2 import config, gprefs from calibre.gui2 import config, gprefs
from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.preferences import ConfigWidgetBase, LazyConfigWidgetBase, set_help_tips from calibre.gui2.preferences import LazyConfigWidgetBase, set_help_tips
from calibre.gui2.preferences.look_feel_tabs.cover_view_ui import Ui_Form from calibre.gui2.preferences.look_feel_tabs.cover_view_ui import Ui_Form
@ -41,8 +41,6 @@ class CoverView(LazyConfigWidgetBase, Ui_Form):
self.fs_help_msg.setText(self.fs_help_msg.text()%( self.fs_help_msg.setText(self.fs_help_msg.text()%(
QKeySequence(QKeySequence.StandardKey.FullScreen).toString(QKeySequence.SequenceFormat.NativeText))) QKeySequence(QKeySequence.StandardKey.FullScreen).toString(QKeySequence.SequenceFormat.NativeText)))
def lazy_initialize(self):
ConfigWidgetBase.initialize(self)
set_help_tips(self.opt_cover_browser_narrow_view_position, _( set_help_tips(self.opt_cover_browser_narrow_view_position, _(
'This option controls the position of the cover browser when using the Narrow user ' 'This option controls the position of the cover browser when using the Narrow user '
'interface layout. "Automatic" will place the cover browser on top or on the right ' 'interface layout. "Automatic" will place the cover browser on top or on the right '
@ -64,12 +62,6 @@ class CoverView(LazyConfigWidgetBase, Ui_Form):
if t.exec(): if t.exec():
self.opt_cover_browser_title_template.setText(t.rule[1]) self.opt_cover_browser_title_template.setText(t.rule[1])
def commit(self):
return ConfigWidgetBase.commit(self)
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
def refresh_gui(self, gui): def refresh_gui(self, gui):
gui.cover_flow.setShowReflections(gprefs['cover_browser_reflections']) gui.cover_flow.setShowReflections(gprefs['cover_browser_reflections'])
gui.cover_flow.setPreserveAspectRatio(gprefs['cb_preserve_aspect_ratio']) gui.cover_flow.setPreserveAspectRatio(gprefs['cb_preserve_aspect_ratio'])

View File

@ -2,13 +2,6 @@
<ui version="4.0"> <ui version="4.0">
<class>Form</class> <class>Form</class>
<widget class="QWidget" name="cover_browser_tab"> <widget class="QWidget" name="cover_browser_tab">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/cover_flow.png</normaloff>:/images/cover_flow.png</iconset>
</attribute>
<attribute name="title">
<string>Cover &amp;browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_cover_browser"> <layout class="QGridLayout" name="gridLayout_cover_browser">
<property name="leftMargin"> <property name="leftMargin">
<number>0</number> <number>0</number>

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2025, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from functools import partial
from calibre.gui2 import gprefs
from calibre.gui2.custom_column_widgets import get_field_list as em_get_field_list
from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, export_layout, import_layout, move_field_down, move_field_up, reset_layout
from calibre.gui2.preferences.look_feel_tabs.edit_metadata_ui import Ui_Form
from calibre.gui2.widgets import BusyCursor
class EMDisplayedFields(DisplayedFields):
def __init__(self, db, parent=None):
DisplayedFields.__init__(self, db, parent)
def initialize(self, use_defaults=False, pref_data_override=None):
self.beginResetModel()
self.fields = [[x[0], x[1]] for x in
em_get_field_list(self.db, use_defaults=use_defaults, pref_data_override=pref_data_override)]
self.endResetModel()
self.changed = True
def commit(self):
if self.changed:
self.db.new_api.set_pref('edit_metadata_custom_columns_to_display', self.fields)
class EditMetadataTab(LazyConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
db = self.gui.library_view.model().db
r = self.register
r('edit_metadata_single_layout', gprefs,
choices=[(_('Default'), 'default'), (_('Compact metadata'), 'alt1'),
(_('All on 1 tab'), 'alt2')])
r('edit_metadata_ignore_display_order', db.prefs)
r('edit_metadata_elision_point', gprefs,
choices=[(_('Left'), 'left'), (_('Middle'), 'middle'),
(_('Right'), 'right')])
r('edit_metadata_elide_labels', gprefs)
r('edit_metadata_single_use_2_cols_for_custom_fields', gprefs)
r('edit_metadata_bulk_cc_label_length', gprefs)
r('edit_metadata_single_cc_label_length', gprefs)
r('edit_metadata_templates_only_F2_on_booklist', gprefs)
self.em_display_model = EMDisplayedFields(self.gui.current_db, self.em_display_order)
self.em_display_model.dataChanged.connect(self.changed_signal)
self.em_display_order.setModel(self.em_display_model)
mu = partial(move_field_up, self.em_display_order, self.em_display_model)
md = partial(move_field_down, self.em_display_order, self.em_display_model)
self.em_display_order.set_movement_functions(mu, md)
self.em_up_button.clicked.connect(mu)
self.em_down_button.clicked.connect(md)
self.em_export_layout_button.clicked.connect(partial(export_layout, self, model=self.em_display_model))
self.em_import_layout_button.clicked.connect(partial(import_layout, self, model=self.em_display_model))
self.em_reset_layout_button.clicked.connect(partial(reset_layout, model=self.em_display_model))
def lazy_initialize(self):
self.em_display_model.initialize()
def commit(self):
with BusyCursor():
self.em_display_model.commit()
return LazyConfigWidgetBase.commit(self)
def restore_defaults(self):
LazyConfigWidgetBase.restore_defaults(self)
self.em_display_model.restore_defaults()
self.changed_signal.emit()

View File

@ -0,0 +1,274 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="edit_metadata_tab">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/edit_input.png</normaloff>:/images/edit_input.png</iconset>
</attribute>
<attribute name="title">
<string>Edit &amp;metadata</string>
</attribute>
<layout class="QVBoxLayout" name="vbox_layout_61">
<item>
<layout class="QFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="edit_metadata_single_label">
<property name="text">
<string>Edit metadata (single) &amp;layout:</string>
</property>
<property name="buddy">
<cstring>opt_edit_metadata_single_layout</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_edit_metadata_single_layout">
<property name="toolTip">
<string>Choose a different layout for the Edit metadata dialog. The compact metadata layout favors editing custom metadata over changing covers and formats.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select the custom columns (for this library) to display in the edit metadata dialogs and their order:</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="1">
<widget class="QToolButton" name="em_down_button">
<property name="toolTip">
<string>Move down. Keyboard shortcut: Ctrl-Down arrow</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QToolButton" name="em_up_button">
<property name="toolTip">
<string>Move up. Keyboard shortcut: Ctrl-Up arrow</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" rowspan="3">
<widget class="ListViewWithMoveByKeyPress" name="em_display_order">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="em_reset_layout_button">
<property name="toolTip">
<string>Click this button to reset the list to its default order.</string>
</property>
<property name="text">
<string>Reset list</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="em_import_layout_button">
<property name="toolTip">
<string>&lt;p&gt;Click this button to set the list to one
previously exported. This could be useful if you have several libraries with
similar structure and you want to use the same column order for each one. Columns
in the imported list that aren't in the current library are ignored. Columns in
the library that are not in the imported list are put at the end and marked
as displayable.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Import list</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="em_export_layout_button">
<property name="toolTip">
<string>&lt;p&gt;Click this button to write the current display
settings to a file. This could be useful if you have several libraries with similar
structure and you want to use the same column order for each one.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Export list</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<layout class="QFormLayout">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_edit_metadata_ignore_display_order">
<property name="toolTip">
<string>&lt;p&gt;Check this box to make the edit metadata dialogs ignore the
above specifications, showing all the columns in the default order. This is
useful for temporarily seeing all your columns in the dialogs without losing
the display and order specifications.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Show &amp;all columns in default order when editing metadata</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="opt_edit_metadata_elide_labels">
<property name="toolTip">
<string>If checked then labels wider than the label width
will be elided, otherwise they will be word wrapped.</string>
</property>
<property name="text">
<string>&amp;Elide labels when editing custom columns</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_1023">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_102">
<property name="text">
<string>Elision point:</string>
</property>
<property name="buddy">
<cstring>opt_edit_metadata_elision_point</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_edit_metadata_elision_point">
<property name="toolTip">
<string>Choose where in the label to put the...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="opt_edit_metadata_templates_only_F2_on_booklist">
<property name="toolTip">
<string>&lt;p&gt;Check this box to allow only the F2 (Edit) key to
open the template editor in the book list for a &quot;Column built from other
columns&quot;. Editing with mouse clicks and the Tab key will be disabled.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Only the &amp;F2 (Edit) key edits column templates in the book list</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_edit_metadata_single_use_2_cols_for_custom_fields">
<property name="text">
<string>Use &amp;two columns for custom columns in the Default layout</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_102">
<property name="text">
<string>&amp;Bulk edit custom column label length:</string>
</property>
<property name="buddy">
<cstring>opt_edit_metadata_bulk_cc_label_length</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_edit_metadata_bulk_cc_label_length">
<property name="toolTip">
<string>The maximum width of a custom column label for the bulk metadata edit dialog in average characters.</string>
</property>
<property name="suffix">
<string> chars</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_1022">
<property name="text">
<string>&amp;Single edit custom column label length:</string>
</property>
<property name="buddy">
<cstring>opt_edit_metadata_single_cc_label_length</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="opt_edit_metadata_single_cc_label_length">
<property name="toolTip">
<string>The maximum width of a custom column label for the single metadata edit dialog in average characters.</string>
</property>
<property name="suffix">
<string> chars</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ListViewWithMoveByKeyPress</class>
<extends>QListView</extends>
<header>calibre/gui2/preferences.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
</ui>

View File

@ -0,0 +1,193 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2025, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from qt.core import QApplication, QDialog, QFont, QFontDialog, QFontInfo
from calibre.constants import ismacos, iswindows
from calibre.gui2 import config, gprefs, icon_resource_manager, qt_app
from calibre.gui2.preferences import LazyConfigWidgetBase, Setting, set_help_tips
from calibre.gui2.preferences.look_feel_tabs.main_interface_ui import Ui_Form
from calibre.gui2.widgets import BusyCursor
from calibre.utils.config import prefs
from calibre.utils.localization import available_translations, get_lang, get_language
class LanguageSetting(Setting):
def commit(self):
val = self.get_gui_val()
oldval = self.get_config_val()
if val != oldval:
gprefs.set('last_used_language', oldval)
return super().commit()
class MainInterfaceTab(LazyConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
self.ui_style_available = True
if not ismacos and not iswindows:
self.label_widget_style.setVisible(False)
self.opt_ui_style.setVisible(False)
self.ui_style_available = False
r = self.register
try:
self.icon_theme_title = icon_resource_manager.user_theme_title
except Exception:
self.icon_theme_title = _('Default icons')
self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
self.commit_icon_theme = None
self.icon_theme_button.clicked.connect(self.choose_icon_theme)
r('ui_style', gprefs, restart_required=True, choices=[(_('System default'), 'system'), (_('calibre style'), 'calibre')])
r('book_list_tooltips', gprefs)
r('dnd_merge', gprefs)
r('wrap_toolbar_text', gprefs, restart_required=True)
r('show_layout_buttons', gprefs)
r('show_sb_all_actions_button', gprefs)
# r('show_sb_preference_button', gprefs)
r('row_numbers_in_book_list', gprefs)
r('cover_corner_radius', gprefs)
r('cover_corner_radius_unit', gprefs, choices=[(_('Pixels'), 'px'), (_('Percentage'), '%')])
r('book_list_extra_row_spacing', gprefs)
r('booklist_grid', gprefs)
def get_esc_lang(l):
if l == 'en':
return 'English'
return get_language(l)
lang = get_lang()
if lang is None or lang not in available_translations():
lang = 'en'
items = [(l, get_esc_lang(l)) for l in available_translations()
if l != lang]
if lang != 'en':
items.append(('en', get_esc_lang('en')))
items.sort(key=lambda x: x[1].lower())
choices = [(y, x) for x, y in items]
# Default language is the autodetected one
choices = [(get_language(lang), lang)] + choices
lul = gprefs.get('last_used_language')
if lul and (lul in available_translations() or lul in ('en', 'eng')):
choices.insert(1, ((get_language(lul), lul)))
r('language', prefs, choices=choices, restart_required=True, setting=LanguageSetting)
r('disable_animations', config)
r('systray_icon', config, restart_required=True)
r('show_splash_screen', gprefs)
r('disable_tray_notification', config)
choices = [(_('Off'), 'off'), (_('Small'), 'small'),
(_('Medium-small'), 'mid-small'), (_('Medium'), 'medium'), (_('Large'), 'large')]
r('toolbar_icon_size', gprefs, choices=choices)
choices = [(_('If there is enough room'), 'auto'), (_('Always'), 'always'),
(_('Never'), 'never')]
r('toolbar_text', gprefs, choices=choices)
self.current_font = self.initial_font = None
self.change_font_button.clicked.connect(self.change_font)
self.opt_ui_style.currentIndexChanged.connect(self.update_color_palette_state)
self.opt_gui_layout.addItem(_('Wide'), 'wide')
self.opt_gui_layout.addItem(_('Narrow'), 'narrow')
self.opt_gui_layout.currentIndexChanged.connect(self.changed_signal)
set_help_tips(self.opt_gui_layout, config.help('gui_layout'))
self.button_adjust_colors.clicked.connect(self.adjust_colors)
def lazy_initialize(self):
font = gprefs['font']
if font is not None:
font = list(font)
font.append(gprefs.get('font_stretch', QFont.Stretch.Unstretched))
self.current_font = self.initial_font = font
self.update_font_display()
self.update_color_palette_state()
self.opt_gui_layout.setCurrentIndex(0 if self.gui.layout_container.is_wide else 1)
def adjust_colors(self):
from calibre.gui2.dialogs.palette import PaletteConfig
d = PaletteConfig(self)
if d.exec() == QDialog.DialogCode.Accepted:
d.apply_settings()
self.changed_signal.emit()
def update_color_palette_state(self):
if self.ui_style_available:
enabled = self.opt_ui_style.currentData() == 'calibre'
self.button_adjust_colors.setEnabled(enabled)
def choose_icon_theme(self):
from calibre.gui2.icon_theme import ChooseTheme
d = ChooseTheme(self)
if d.exec() == QDialog.DialogCode.Accepted:
self.commit_icon_theme = d.commit_changes
self.icon_theme_title = d.new_theme_title or _('Default icons')
self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
self.changed_signal.emit()
def build_font_obj(self):
font_info = qt_app.original_font if self.current_font is None else self.current_font
font = QFont(*(font_info[:4]))
font.setStretch(font_info[4])
return font
def change_font(self, *args):
fd = QFontDialog(self.build_font_obj(), self)
if fd.exec() == QDialog.DialogCode.Accepted:
font = fd.selectedFont()
fi = QFontInfo(font)
self.current_font = [str(fi.family()), fi.pointSize(),
fi.weight(), fi.italic(), font.stretch()]
self.update_font_display()
self.changed_signal.emit()
def update_font_display(self):
font = self.build_font_obj()
fi = QFontInfo(font)
name = str(fi.family())
self.font_display.setFont(font)
self.font_display.setText(name + f' [{fi.pointSize()}pt]')
def commit(self):
rr = LazyConfigWidgetBase.commit(self)
with BusyCursor():
if self.current_font != self.initial_font:
gprefs['font'] = (self.current_font[:4] if self.current_font else None)
gprefs['font_stretch'] = (self.current_font[4] if self.current_font
is not None else QFont.Stretch.Unstretched)
QApplication.setFont(self.font_display.font())
rr = True
if self.commit_icon_theme is not None:
self.commit_icon_theme()
self.gui.layout_container.change_layout(self.gui, self.opt_gui_layout.currentIndex() == 0)
return rr
def restore_defaults(self):
LazyConfigWidgetBase.restore_defaults(self)
ofont = self.current_font
self.current_font = None
if ofont is not None:
self.changed_signal.emit()
self.main_interface_tab.update_font_display()
self.opt_gui_layout.setCurrentIndex(0)
self.changed_signal.emit()
def refresh_gui(self, gui):
gui.place_layout_buttons()
self.update_font_display()
gui.library_view.set_row_header_visibility()
for view in 'library memory card_a card_b'.split():
getattr(gui, view + '_view').set_row_header_visibility()
gui.library_view.refresh_row_sizing()
gui.sb_all_gui_actions_button.setVisible(gprefs['show_sb_all_actions_button'])
# gui.sb_preferences_button.setVisible(gprefs['show_sb_preference_button'])

View File

@ -0,0 +1,356 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="main_interface_tab">
<layout class="QGridLayout" name="gridLayout_main_interface">
<item row="0" column="0">
<widget class="QLabel" name="label_widget_style">
<property name="text">
<string>User interface style (&amp;needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_ui_style</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_ui_style"/>
</item>
<item row="0" column="2">
<layout class="QHBoxLayout">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="icon_theme">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="icon_theme_button">
<property name="text">
<string>Change &amp;icon theme</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="opt_color_palette_label">
<property name="text">
<string>User interface &amp;colors:</string>
</property>
<property name="buddy">
<cstring>button_adjust_colors</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="button_adjust_colors">
<property name="text">
<string>Adjust &amp;colors</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>&amp;User interface layout:</string>
</property>
<property name="buddy">
<cstring>opt_gui_layout</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="opt_gui_layout">
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>20</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Choose &amp;language (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_language</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="opt_language">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>20</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Round the corners of covers:</string>
</property>
<property name="buddy">
<cstring>opt_cover_corner_radius</cstring>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QSpinBox" name="opt_cover_corner_radius">
<property name="toolTip">
<string>Round the corners of covers in many places they are displayed such as the Cover grid, Book details panel, etc. Adjust the amount of rounding with this setting. A value of between 5 and 10 looks good with most covers sizes. Zero disables rounding and is the default.</string>
</property>
<property name="specialValueText">
<string>no rounding</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_cover_corner_radius_unit">
<property name="toolTip">
<string>The unit for the cover corner rounding. Either pixel values or as a percentage of the cover size.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="opt_systray_icon">
<property name="text">
<string>Enable s&amp;ystem tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="opt_disable_animations">
<property name="toolTip">
<string>Disable all animations. Useful if you have a slow/old computer.</string>
</property>
<property name="text">
<string>Disable &amp;animations</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="opt_disable_tray_notification">
<property name="toolTip">
<string>Disable popup notifications when calibre completes jobs such a conversion, sending to device etc. The notifications are sent via the operating system notification facility, if available.</string>
</property>
<property name="text">
<string>Disable n&amp;otifications on job completion</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="opt_show_splash_screen">
<property name="text">
<string>Show the &amp;splash screen at startup</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="opt_book_list_tooltips">
<property name="text">
<string>Show &amp;tooltips in the book list</string>
</property>
</widget>
</item>
<item row="9" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Interface font:</string>
</property>
<property name="buddy">
<cstring>font_display</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="9" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Icon si&amp;ze:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="opt_wrap_toolbar_text">
<property name="text">
<string>Use t&amp;wo lines for the text under the icons (needs restart)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="10" column="1">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Status bar buttons</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QCheckBox" name="opt_show_layout_buttons">
<property name="toolTip">
<string>Show individual buttons to control the layout of the interface rather than a single button with a popup.</string>
</property>
<property name="text">
<string>Show &amp;layout buttons</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="opt_show_sb_all_actions_button">
<property name="toolTip">
<string>Show a button that gives quick access to all available actions with a single click.</string>
</property>
<property name="text">
<string>S&amp;how actions button</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>E&amp;xtra spacing to add between rows in the book list (can be negative):</string>
</property>
<property name="buddy">
<cstring>opt_book_list_extra_row_spacing</cstring>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QSpinBox" name="opt_book_list_extra_row_spacing">
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>-20</number>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QCheckBox" name="opt_row_numbers_in_book_list">
<property name="text">
<string>Show &amp;row numbers in the book list</string>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QCheckBox" name="opt_booklist_grid">
<property name="text">
<string>Draw a &amp;grid in the book list</string>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QCheckBox" name="opt_dnd_merge">
<property name="text">
<string>Allow using &amp;drag and drop to merge books</string>
</property>
</widget>
</item>
<item row="15" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
</ui>

View File

@ -10,12 +10,12 @@ from functools import partial
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
from calibre.gui2.actions.show_quickview import get_quickview_action_plugin from calibre.gui2.actions.show_quickview import get_quickview_action_plugin
from calibre.gui2.dialogs.quickview import get_qv_field_list from calibre.gui2.dialogs.quickview import get_qv_field_list
from calibre.gui2.preferences import ConfigWidgetBase, LazyConfigWidgetBase from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, move_field_down, move_field_up from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, move_field_down, move_field_up
from calibre.gui2.preferences.look_feel_tabs.quickview_ui import Ui_Form from calibre.gui2.preferences.look_feel_tabs.quickview_ui import Ui_Form
class QVDisplayedFields(DisplayedFields): # {{{ class QVDisplayedFields(DisplayedFields):
def __init__(self, db, parent=None): def __init__(self, db, parent=None):
DisplayedFields.__init__(self, db, parent) DisplayedFields.__init__(self, db, parent)
@ -31,8 +31,6 @@ class QVDisplayedFields(DisplayedFields): # {{{
if self.changed: if self.changed:
self.db.new_api.set_pref('qv_display_fields', self.fields) self.db.new_api.set_pref('qv_display_fields', self.fields)
# }}}
class QuickviewTab(LazyConfigWidgetBase, Ui_Form): class QuickviewTab(LazyConfigWidgetBase, Ui_Form):
@ -59,7 +57,7 @@ class QuickviewTab(LazyConfigWidgetBase, Ui_Form):
self.qv_display_model.initialize() self.qv_display_model.initialize()
def restore_defaults(self): def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self) LazyConfigWidgetBase.restore_defaults(self)
self.qv_display_model.restore_defaults() self.qv_display_model.restore_defaults()
def refresh_gui(self, gui): def refresh_gui(self, gui):
@ -67,7 +65,6 @@ class QuickviewTab(LazyConfigWidgetBase, Ui_Form):
if qv: if qv:
qv.refill_quickview() qv.refill_quickview()
def commit(self, *args): def commit(self):
rr = ConfigWidgetBase.commit(self, *args)
self.qv_display_model.commit() self.qv_display_model.commit()
return rr return LazyConfigWidgetBase.commit(self)

View File

@ -2,10 +2,6 @@
<ui version="4.0"> <ui version="4.0">
<class>Form</class> <class>Form</class>
<widget class="QWidget" name="cover_browser_tab"> <widget class="QWidget" name="cover_browser_tab">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/cover_flow.png</normaloff>:/images/cover_flow.png</iconset>
</attribute>
<layout class="QGridLayout" name="gridLayout_122"> <layout class="QGridLayout" name="gridLayout_122">
<item row="1" column="0" colspan="2"> <item row="1" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_122"> <layout class="QGridLayout" name="gridLayout_122">

View File

@ -9,12 +9,12 @@ from functools import partial
from calibre.db.categories import is_standard_category from calibre.db.categories import is_standard_category
from calibre.gui2 import config, gprefs from calibre.gui2 import config, gprefs
from calibre.gui2.preferences import ConfigWidgetBase, LazyConfigWidgetBase from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, export_layout, import_layout, move_field_down, move_field_up, reset_layout from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, export_layout, import_layout, move_field_down, move_field_up, reset_layout
from calibre.gui2.preferences.look_feel_tabs.tb_display_ui import Ui_Form from calibre.gui2.preferences.look_feel_tabs.tb_display_ui import Ui_Form
class TBDisplayedFields(DisplayedFields): # {{{ class TBDisplayedFields(DisplayedFields):
# The code in this class depends on the fact that the tag browser is # The code in this class depends on the fact that the tag browser is
# initialized before this class is instantiated. # initialized before this class is instantiated.
@ -44,7 +44,6 @@ class TBDisplayedFields(DisplayedFields): # {{{
if self.changed: if self.changed:
self.db.prefs.set('tag_browser_hidden_categories', [k for k,v in self.fields if not v]) self.db.prefs.set('tag_browser_hidden_categories', [k for k,v in self.fields if not v])
self.db.prefs.set('tag_browser_category_order', [k for k,v in self.fields]) self.db.prefs.set('tag_browser_category_order', [k for k,v in self.fields])
# }}}
class TbDisplayTab(LazyConfigWidgetBase, Ui_Form): class TbDisplayTab(LazyConfigWidgetBase, Ui_Form):
@ -104,9 +103,12 @@ class TbDisplayTab(LazyConfigWidgetBase, Ui_Form):
move_field_up(self.tb_display_order, model) move_field_up(self.tb_display_order, model)
def restore_defaults(self): def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self) LazyConfigWidgetBase.restore_defaults(self)
self.tb_display_model.restore_defaults() self.tb_display_model.restore_defaults()
def commit(self): def commit(self):
self.tb_display_model.commit() self.tb_display_model.commit()
return ConfigWidgetBase.commit(self) return LazyConfigWidgetBase.commit(self)
def refresh_gui(self, gui):
gui.tags_view.set_look_and_feel()

View File

@ -2,17 +2,6 @@
<ui version="4.0"> <ui version="4.0">
<class>Form</class> <class>Form</class>
<widget class="QWidget" name="Form"> <widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1096</width>
<height>791</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="verticalLayout_42"> <layout class="QGridLayout" name="verticalLayout_42">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">

View File

@ -16,7 +16,7 @@ from calibre.gui2.preferences.look_feel_tabs import DisplayedFields
from calibre.gui2.preferences.look_feel_tabs.tb_hierarchy_ui import Ui_Form from calibre.gui2.preferences.look_feel_tabs.tb_hierarchy_ui import Ui_Form
class TBHierarchicalFields(DisplayedFields): # {{{ class TBHierarchicalFields(DisplayedFields):
# The code in this class depends on the fact that the tag browser is # The code in this class depends on the fact that the tag browser is
# initialized before this class is instantiated. # initialized before this class is instantiated.
@ -51,7 +51,6 @@ class TBHierarchicalFields(DisplayedFields): # {{{
def commit(self): def commit(self):
if self.changed: if self.changed:
self.db.prefs.set('categories_using_hierarchy', [k for k,v in self.fields if v]) self.db.prefs.set('categories_using_hierarchy', [k for k,v in self.fields if v])
# }}}
class TbHierarchyTab(LazyConfigWidgetBase, Ui_Form): class TbHierarchyTab(LazyConfigWidgetBase, Ui_Form):
@ -175,3 +174,4 @@ class TbHierarchyTab(LazyConfigWidgetBase, Ui_Form):
def commit(self): def commit(self):
self.tb_search_order_commit() self.tb_search_order_commit()
self.tb_hierarchical_cats_model.commit() self.tb_hierarchical_cats_model.commit()
return LazyConfigWidgetBase.commit(self)

View File

@ -2,17 +2,6 @@
<ui version="4.0"> <ui version="4.0">
<class>Form</class> <class>Form</class>
<widget class="QWidget" name="Form"> <widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1035</width>
<height>547</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout"> <layout class="QGridLayout">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">

View File

@ -2,17 +2,6 @@
<ui version="4.0"> <ui version="4.0">
<class>Form</class> <class>Form</class>
<widget class="QWidget" name="Form"> <widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1035</width>
<height>547</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout"> <layout class="QVBoxLayout">
<item> <item>
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">

View File

@ -9,12 +9,12 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
from calibre.gui2.preferences import ConfigWidgetBase, LazyConfigWidgetBase from calibre.gui2.preferences import LazyConfigWidgetBase
from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, export_layout, import_layout, reset_layout from calibre.gui2.preferences.look_feel_tabs import DisplayedFields, export_layout, import_layout, reset_layout
from calibre.gui2.preferences.look_feel_tabs.tb_partitioning_ui import Ui_Form from calibre.gui2.preferences.look_feel_tabs.tb_partitioning_ui import Ui_Form
class TBPartitionedFields(DisplayedFields): # {{{ class TBPartitionedFields(DisplayedFields):
# The code in this class depends on the fact that the tag browser is # The code in this class depends on the fact that the tag browser is
# initialized before this class is instantiated. # initialized before this class is instantiated.
@ -60,7 +60,6 @@ class TBPartitionedFields(DisplayedFields): # {{{
if self.changed: if self.changed:
# Migrate to a per-library setting # Migrate to a per-library setting
self.db.prefs.set('tag_browser_dont_collapse', [k for k,v in self.fields if not v]) self.db.prefs.set('tag_browser_dont_collapse', [k for k,v in self.fields if not v])
# }}}
class TbPartitioningTab(LazyConfigWidgetBase, Ui_Form): class TbPartitioningTab(LazyConfigWidgetBase, Ui_Form):
@ -92,4 +91,7 @@ class TbPartitioningTab(LazyConfigWidgetBase, Ui_Form):
def commit(self): def commit(self):
self.tb_categories_to_part_model.commit() self.tb_categories_to_part_model.commit()
return ConfigWidgetBase.commit(self) return LazyConfigWidgetBase.commit(self)
def refresh_gui(self, gui):
gui.tags_view.reread_collapse_parameters()

View File

@ -2,17 +2,6 @@
<ui version="4.0"> <ui version="4.0">
<class>Form</class> <class>Form</class>
<widget class="QWidget" name="Form"> <widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1035</width>
<height>547</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4"> <layout class="QVBoxLayout" name="verticalLayout_4">
<item> <item>
<layout class="QGridLayout" name="gridLayout_10"> <layout class="QGridLayout" name="gridLayout_10">

View File

@ -397,7 +397,7 @@ class Preferences(QDialog):
def commit(self, *args): def commit(self, *args):
# Commit the child widgets first in case the main widget uses the information # Commit the child widgets first in case the main widget uses the information
must_restart = bool(self.showing_widget.do_on_child_tabs('commit')) | self.showing_widget.commit() must_restart = bool(self.showing_widget.do_on_child_tabs('commit')) | bool(self.showing_widget.commit())
rc = self.showing_widget.restart_critical rc = self.showing_widget.restart_critical
self.committed = True self.committed = True
do_restart = False do_restart = False