move "Look & Feel/Book details" to its own widget

This commit is contained in:
un-pogaz 2025-02-01 07:38:24 +01:00
parent 7622074887
commit b32e1beaf4
5 changed files with 586 additions and 532 deletions

View File

@ -5,33 +5,11 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from collections import defaultdict
from functools import partial
from qt.core import (
QComboBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QHeaderView,
QIcon,
QKeySequence,
QLabel,
QLineEdit,
QListWidgetItem,
QPushButton,
QSize,
Qt,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
pyqtSignal,
)
from qt.core import QIcon, QKeySequence, QListWidgetItem, Qt
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
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 ConfigWidgetBase, test_widget
from calibre.gui2.preferences.coloring import EditRules
@ -46,187 +24,6 @@ from calibre.gui2.preferences.look_feel_tabs import (
)
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.icu import sort_key
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): # {{{
@ -246,40 +43,6 @@ class EMDisplayedFields(DisplayedFields): # {{{
# }}}
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 ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
@ -289,17 +52,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r = self.register
self.default_author_link = DefaultAuthorLink(self.default_author_link_container)
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)
choices = [(_('Default'), 'default'), (_('Compact metadata'), 'alt1'),
(_('All on 1 tab'), 'alt2')]
r('edit_metadata_single_layout', gprefs,
@ -315,15 +67,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('edit_metadata_single_cc_label_length', gprefs)
r('edit_metadata_templates_only_F2_on_booklist', gprefs)
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)
@ -336,10 +79,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
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'))
@ -354,11 +93,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
'Ctrl+Shift+F', QKeySequence.SequenceFormat.PortableText)]
keys = [str(x.toString(QKeySequence.SequenceFormat.NativeText)) for x in keys]
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()):
self.sections_view.addItem(QListWidgetItem(self.tabWidget.tabIcon(i), self.tabWidget.tabText(i).replace('&', '')))
self.sections_view.setCurrentRow(self.tabWidget.currentIndex())
@ -371,58 +105,33 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def initial_tab_changed(self):
self.sections_view.setCurrentRow(self.tabWidget.currentIndex())
def edit_id_link_rules(self):
if IdLinksEditor(self).exec() == QDialog.DialogCode.Accepted:
self.changed_signal.emit()
def initialize(self):
ConfigWidgetBase.initialize(self)
self.default_author_link.value = default_author_link()
self.display_model.initialize()
self.em_display_model.initialize()
self.bd_vertical_cats_model.initialize()
db = self.gui.current_db
mi = selected_rows_metadatas()
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.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)
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.default_author_link.value = DEFAULT_AUTHOR_LINK
self.display_model.restore_defaults()
self.em_display_model.restore_defaults()
self.bd_vertical_cats_model.restore_defaults()
self.edit_rules.clear()
self.icon_rules.clear()
self.changed_signal.emit()
self.opt_book_details_css.setPlainText(P('templates/book_details.css', allow_user_override=False, data=True).decode('utf-8'))
def commit(self, *args):
with BusyCursor():
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)
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)
def refresh_gui(self, gui):
gui.book_details.book_info.refresh_css()
m = gui.library_view.model()
m.update_db_prefs_cache()
m.beginResetModel(), m.endResetModel()
gui.tags_view.model().reset_tag_browser()
gui.library_view.refresh_book_details(force=True)
gui.library_view.refresh_composite_edit()
if __name__ == '__main__':

View File

@ -37,7 +37,7 @@
<string>Cover &amp;grid</string>
</attribute>
</widget>
<widget class="QWidget" name="book_details_tab">
<widget class="BookDetailsTab" name="book_details_tab">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/book.png</normaloff>:/images/book.png</iconset>
@ -45,243 +45,6 @@
<attribute name="title">
<string>&amp;Book details</string>
</attribute>
<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="QWidget" name="default_author_link_container" 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>
<widget class="QWidget" name="edit_metadata_tab">
<attribute name="icon">
@ -631,6 +394,11 @@ columns&quot;. Editing with mouse clicks and the Tab key will be disabled.&lt;/p
<extends>ConfigWidgetBase</extends>
<header>calibre/gui2/preferences/look_feel_tabs/cover_grid.h</header>
</customwidget>
<customwidget>
<class>BookDetailsTab</class>
<extends>ConfigWidgetBase</extends>
<header>calibre/gui2/preferences/look_feel_tabs/book_details.h</header>
</customwidget>
<customwidget>
<class>TbDisplayTab</class>
<extends>ConfigWidgetBase</extends>

View File

@ -7,13 +7,76 @@ __docformat__ = 'restructuredtext en'
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.book_details import get_field_list
from calibre.gui2.ui import get_gui
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):

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.default_author_link.value = default_author_link()
self.display_model.initialize()
self.blockSignals(True)
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>