1#!/usr/local/bin/python3.8
2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
3
4
5__license__   = 'GPL v3'
6__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
7__docformat__ = 'restructuredtext en'
8
9import json
10
11from collections import defaultdict
12from threading import Thread
13
14from qt.core import (
15    QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, QPainter, QDialog,
16    QAbstractListModel, Qt, QIcon, QKeySequence, QColor, pyqtSignal, QCursor,
17    QWidget, QSizePolicy, QBrush, QPixmap, QSize, QPushButton, QVBoxLayout, QItemSelectionModel,
18    QTableWidget, QTableWidgetItem, QLabel, QFormLayout, QLineEdit, QComboBox, QDialogButtonBox
19)
20
21from calibre import human_readable
22from calibre.ebooks.metadata.book.render import DEFAULT_AUTHOR_LINK
23from calibre.constants import ismacos, iswindows
24from calibre.ebooks.metadata.sources.prefs import msprefs
25from calibre.gui2 import default_author_link
26from calibre.gui2.dialogs.template_dialog import TemplateDialog
27from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
28from calibre.gui2.preferences.look_feel_ui import Ui_Form
29from calibre.gui2 import config, gprefs, qt_app, open_local_file, question_dialog, error_dialog
30from calibre.utils.localization import (available_translations,
31    get_language, get_lang)
32from calibre.utils.config import prefs
33from calibre.utils.icu import sort_key
34from calibre.gui2.book_details import get_field_list
35from calibre.gui2.dialogs.quickview import get_qv_field_list
36from calibre.gui2.preferences.coloring import EditRules
37from calibre.gui2.library.alternate_views import auto_height, CM_TO_INCH
38from calibre.gui2.widgets2 import Dialog
39from calibre.gui2.actions.show_quickview import get_quickview_action_plugin
40from calibre.utils.resources import set_data
41from polyglot.builtins import iteritems
42
43
44class BusyCursor:
45
46    def __enter__(self):
47        QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
48
49    def __exit__(self, *args):
50        QApplication.restoreOverrideCursor()
51
52
53class DefaultAuthorLink(QWidget):  # {{{
54
55    changed_signal = pyqtSignal()
56
57    def __init__(self, parent):
58        QWidget.__init__(self, parent)
59        l = QVBoxLayout(parent)
60        l.addWidget(self)
61        l.setContentsMargins(0, 0, 0, 0)
62        l = QFormLayout(self)
63        l.setContentsMargins(0, 0, 0, 0)
64        l.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
65        self.choices = c = QComboBox()
66        c.setMinimumContentsLength(30)
67        for text, data in [
68                (_('Search for the author on Goodreads'), 'search-goodreads'),
69                (_('Search for the author on Amazon'), 'search-amzn'),
70                (_('Search for the author in your calibre library'), 'search-calibre'),
71                (_('Search for the author on Wikipedia'), 'search-wikipedia'),
72                (_('Search for the author on Google Books'), 'search-google'),
73                (_('Search for the book on Goodreads'), 'search-goodreads-book'),
74                (_('Search for the book on Amazon'), 'search-amzn-book'),
75                (_('Search for the book on Google Books'), 'search-google-book'),
76                (_('Use a custom search URL'), 'url'),
77        ]:
78            c.addItem(text, data)
79        l.addRow(_('Clicking on &author names should:'), c)
80        self.custom_url = u = QLineEdit(self)
81        u.setToolTip(_(
82            'Enter the URL to search. It should contain the string {0}'
83            '\nwhich will be replaced by the author name. For example,'
84            '\n{1}').format('{author}', 'https://en.wikipedia.org/w/index.php?search={author}'))
85        u.textChanged.connect(self.changed_signal)
86        u.setPlaceholderText(_('Enter the URL'))
87        c.currentIndexChanged.connect(self.current_changed)
88        l.addRow(u)
89        self.current_changed()
90        c.currentIndexChanged.connect(self.changed_signal)
91
92    @property
93    def value(self):
94        k = self.choices.currentData()
95        if k == 'url':
96            return self.custom_url.text()
97        return k if k != DEFAULT_AUTHOR_LINK else None
98
99    @value.setter
100    def value(self, val):
101        i = self.choices.findData(val)
102        if i < 0:
103            i = self.choices.findData('url')
104            self.custom_url.setText(val)
105        self.choices.setCurrentIndex(i)
106
107    def current_changed(self):
108        k = self.choices.currentData()
109        self.custom_url.setVisible(k == 'url')
110# }}}
111
112# IdLinksEditor {{{
113
114
115class IdLinksRuleEdit(Dialog):
116
117    def __init__(self, key='', name='', template='', parent=None):
118        title = _('Edit rule') if key else _('Create a new rule')
119        Dialog.__init__(self, title=title, name='id-links-rule-editor', parent=parent)
120        self.key.setText(key), self.nw.setText(name), self.template.setText(template or 'https://example.com/{id}')
121        if self.size().height() < self.sizeHint().height():
122            self.resize(self.sizeHint())
123
124    @property
125    def rule(self):
126        return self.key.text().lower(), self.nw.text(), self.template.text()
127
128    def setup_ui(self):
129        self.l = l = QFormLayout(self)
130        l.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
131        l.addRow(QLabel(_(
132            'The key of the identifier, for example, in isbn:XXX, the key is "isbn"')))
133        self.key = k = QLineEdit(self)
134        l.addRow(_('&Key:'), k)
135        l.addRow(QLabel(_(
136            'The name that will appear in the Book details panel')))
137        self.nw = n = QLineEdit(self)
138        l.addRow(_('&Name:'), n)
139        la = QLabel(_(
140            'The template used to create the link.'
141            ' The placeholder {0} in the template will be replaced'
142            ' with the actual identifier value. Use {1} to avoid the value'
143            ' being quoted.').format('{id}', '{id_unquoted}'))
144        la.setWordWrap(True)
145        l.addRow(la)
146        self.template = t = QLineEdit(self)
147        l.addRow(_('&Template:'), t)
148        t.selectAll()
149        t.setFocus(Qt.FocusReason.OtherFocusReason)
150        l.addWidget(self.bb)
151
152    def accept(self):
153        r = self.rule
154        for i, which in enumerate([_('Key'), _('Name'), _('Template')]):
155            if not r[i]:
156                return error_dialog(self, _('Value needed'), _(
157                    'The %s field cannot be empty') % which, show=True)
158        Dialog.accept(self)
159
160
161class IdLinksEditor(Dialog):
162
163    def __init__(self, parent=None):
164        Dialog.__init__(self, title=_('Create rules for identifiers'), name='id-links-rules-editor', parent=parent)
165
166    def setup_ui(self):
167        self.l = l = QVBoxLayout(self)
168        self.la = la = QLabel(_(
169            'Create rules to convert identifiers into links.'))
170        la.setWordWrap(True)
171        l.addWidget(la)
172        items = []
173        for k, lx in iteritems(msprefs['id_link_rules']):
174            for n, t in lx:
175                items.append((k, n, t))
176        items.sort(key=lambda x:sort_key(x[1]))
177        self.table = t = QTableWidget(len(items), 3, self)
178        t.setHorizontalHeaderLabels([_('Key'), _('Name'), _('Template')])
179        for r, (key, val, template) in enumerate(items):
180            t.setItem(r, 0, QTableWidgetItem(key))
181            t.setItem(r, 1, QTableWidgetItem(val))
182            t.setItem(r, 2, QTableWidgetItem(template))
183        l.addWidget(t)
184        t.horizontalHeader().setSectionResizeMode(2, t.horizontalHeader().Stretch)
185        self.cb = b = QPushButton(QIcon(I('plus.png')), _('&Add rule'), self)
186        connect_lambda(b.clicked, self, lambda self: self.edit_rule())
187        self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
188        self.rb = b = QPushButton(QIcon(I('minus.png')), _('&Remove rule'), self)
189        connect_lambda(b.clicked, self, lambda self: self.remove_rule())
190        self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
191        self.eb = b = QPushButton(QIcon(I('modified.png')), _('&Edit rule'), self)
192        connect_lambda(b.clicked, self, lambda self: self.edit_rule(self.table.currentRow()))
193        self.bb.addButton(b, QDialogButtonBox.ButtonRole.ActionRole)
194        l.addWidget(self.bb)
195
196    def sizeHint(self):
197        return QSize(700, 550)
198
199    def accept(self):
200        rules = defaultdict(list)
201        for r in range(self.table.rowCount()):
202            def item(c):
203                return self.table.item(r, c).text()
204            rules[item(0)].append([item(1), item(2)])
205        msprefs['id_link_rules'] = dict(rules)
206        Dialog.accept(self)
207
208    def edit_rule(self, r=-1):
209        key = name = template = ''
210        if r > -1:
211            key, name, template = map(lambda c: self.table.item(r, c).text(), range(3))
212        d = IdLinksRuleEdit(key, name, template, self)
213        if d.exec() == QDialog.DialogCode.Accepted:
214            if r < 0:
215                self.table.setRowCount(self.table.rowCount() + 1)
216                r = self.table.rowCount() - 1
217            rule = d.rule
218            for c in range(3):
219                self.table.setItem(r, c, QTableWidgetItem(rule[c]))
220            self.table.scrollToItem(self.table.item(r, 0))
221
222    def remove_rule(self):
223        r = self.table.currentRow()
224        if r > -1:
225            self.table.removeRow(r)
226# }}}
227
228
229class DisplayedFields(QAbstractListModel):  # {{{
230
231    def __init__(self, db, parent=None, pref_name=None):
232        self.pref_name = pref_name or 'book_display_fields'
233        QAbstractListModel.__init__(self, parent)
234
235        self.fields = []
236        self.db = db
237        self.changed = False
238
239    def get_field_list(self, use_defaults=False):
240        return get_field_list(self.db.field_metadata, use_defaults=use_defaults, pref_name=self.pref_name)
241
242    def initialize(self, use_defaults=False):
243        self.beginResetModel()
244        self.fields = [[x[0], x[1]] for x in self.get_field_list(use_defaults=use_defaults)]
245        self.endResetModel()
246        self.changed = True
247
248    def rowCount(self, *args):
249        return len(self.fields)
250
251    def data(self, index, role):
252        try:
253            field, visible = self.fields[index.row()]
254        except:
255            return None
256        if role == Qt.ItemDataRole.DisplayRole:
257            name = field
258            try:
259                name = self.db.field_metadata[field]['name']
260            except:
261                pass
262            if not name:
263                name = field
264            return name
265        if role == Qt.ItemDataRole.CheckStateRole:
266            return Qt.CheckState.Checked if visible else Qt.CheckState.Unchecked
267        if role == Qt.ItemDataRole.DecorationRole and field.startswith('#'):
268            return QIcon(I('column.png'))
269        return None
270
271    def toggle_all(self, show=True):
272        for i in range(self.rowCount()):
273            idx = self.index(i)
274            if idx.isValid():
275                self.setData(idx, show, Qt.ItemDataRole.CheckStateRole)
276
277    def flags(self, index):
278        ans = QAbstractListModel.flags(self, index)
279        return ans | Qt.ItemFlag.ItemIsUserCheckable
280
281    def setData(self, index, val, role):
282        ret = False
283        if role == Qt.ItemDataRole.CheckStateRole:
284            self.fields[index.row()][1] = bool(val)
285            self.changed = True
286            ret = True
287            self.dataChanged.emit(index, index)
288        return ret
289
290    def restore_defaults(self):
291        self.initialize(use_defaults=True)
292
293    def commit(self):
294        if self.changed:
295            self.db.new_api.set_pref(self.pref_name, self.fields)
296
297    def move(self, idx, delta):
298        row = idx.row() + delta
299        if row >= 0 and row < len(self.fields):
300            t = self.fields[row]
301            self.fields[row] = self.fields[row-delta]
302            self.fields[row-delta] = t
303            self.dataChanged.emit(idx, idx)
304            idx = self.index(row)
305            self.dataChanged.emit(idx, idx)
306            self.changed = True
307            return idx
308
309
310def move_field_up(widget, model):
311    idx = widget.currentIndex()
312    if idx.isValid():
313        idx = model.move(idx, -1)
314        if idx is not None:
315            sm = widget.selectionModel()
316            sm.select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)
317            widget.setCurrentIndex(idx)
318
319
320def move_field_down(widget, model):
321    idx = widget.currentIndex()
322    if idx.isValid():
323        idx = model.move(idx, 1)
324        if idx is not None:
325            sm = widget.selectionModel()
326            sm.select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)
327            widget.setCurrentIndex(idx)
328
329# }}}
330
331
332class QVDisplayedFields(DisplayedFields):  # {{{
333
334    def __init__(self, db, parent=None):
335        DisplayedFields.__init__(self, db, parent)
336
337    def initialize(self, use_defaults=False):
338        self.beginResetModel()
339        self.fields = [[x[0], x[1]] for x in
340                get_qv_field_list(self.db.field_metadata, use_defaults=use_defaults)]
341        self.endResetModel()
342        self.changed = True
343
344    def commit(self):
345        if self.changed:
346            self.db.new_api.set_pref('qv_display_fields', self.fields)
347
348# }}}
349
350
351class Background(QWidget):  # {{{
352
353    def __init__(self, parent):
354        QWidget.__init__(self, parent)
355        self.bcol = QColor(*gprefs['cover_grid_color'])
356        self.btex = gprefs['cover_grid_texture']
357        self.update_brush()
358        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
359
360    def update_brush(self):
361        self.brush = QBrush(self.bcol)
362        if self.btex:
363            from calibre.gui2.preferences.texture_chooser import texture_path
364            path = texture_path(self.btex)
365            if path:
366                p = QPixmap(path)
367                try:
368                    dpr = self.devicePixelRatioF()
369                except AttributeError:
370                    dpr = self.devicePixelRatio()
371                p.setDevicePixelRatio(dpr)
372                self.brush.setTexture(p)
373        self.update()
374
375    def sizeHint(self):
376        return QSize(200, 120)
377
378    def paintEvent(self, ev):
379        painter = QPainter(self)
380        painter.fillRect(ev.rect(), self.brush)
381        painter.end()
382# }}}
383
384
385class ConfigWidget(ConfigWidgetBase, Ui_Form):
386
387    size_calculated = pyqtSignal(object)
388
389    def genesis(self, gui):
390        self.gui = gui
391        if not ismacos and not iswindows:
392            self.label_widget_style.setVisible(False)
393            self.opt_ui_style.setVisible(False)
394
395        db = gui.library_view.model().db
396
397        r = self.register
398
399        try:
400            self.icon_theme_title = json.loads(I('icon-theme.json', data=True))['name']
401        except Exception:
402            self.icon_theme_title = _('Default icons')
403        self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
404        self.commit_icon_theme = None
405        self.icon_theme_button.clicked.connect(self.choose_icon_theme)
406        self.default_author_link = DefaultAuthorLink(self.default_author_link_container)
407        self.default_author_link.changed_signal.connect(self.changed_signal)
408        r('gui_layout', config, restart_required=True, choices=[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')])
409        r('hidpi', gprefs, restart_required=True, choices=[(_('Automatic'), 'auto'), (_('On'), 'on'), (_('Off'), 'off')])
410        if ismacos:
411            self.opt_hidpi.setVisible(False), self.label_hidpi.setVisible(False)
412        r('ui_style', gprefs, restart_required=True, choices=[(_('System default'), 'system'), (_('calibre style'), 'calibre')])
413        r('book_list_tooltips', gprefs)
414        r('dnd_merge', gprefs)
415        r('wrap_toolbar_text', gprefs, restart_required=True)
416        r('show_layout_buttons', gprefs, restart_required=True)
417        r('row_numbers_in_book_list', gprefs)
418        r('tag_browser_old_look', gprefs)
419        r('tag_browser_hide_empty_categories', gprefs)
420        r('tag_browser_always_autocollapse', gprefs)
421        r('tag_browser_show_tooltips', gprefs)
422        r('tag_browser_allow_keyboard_focus', gprefs)
423        r('bd_show_cover', gprefs)
424        r('bd_overlay_cover_size', gprefs)
425        r('cover_grid_width', gprefs)
426        r('cover_grid_height', gprefs)
427        r('cover_grid_cache_size_multiple', gprefs)
428        r('cover_grid_disk_cache_size', gprefs)
429        r('cover_grid_spacing', gprefs)
430        r('cover_grid_show_title', gprefs)
431        r('tag_browser_show_counts', gprefs)
432        r('tag_browser_item_padding', gprefs)
433        r('books_autoscroll_time', gprefs)
434
435        r('qv_respects_vls', gprefs)
436        r('qv_dclick_changes_column', gprefs)
437        r('qv_retkey_changes_column', gprefs)
438        r('qv_follows_column', gprefs)
439
440        r('cover_flow_queue_length', config, restart_required=True)
441        r('cover_browser_reflections', gprefs)
442        r('cover_browser_title_template', db.prefs)
443        fm = db.field_metadata
444        r('cover_browser_subtitle_field', db.prefs, choices=[(_('No subtitle'), 'none')] + sorted(
445            (fm[k].get('name'), k) for k in fm.all_field_keys() if fm[k].get('name')
446        ))
447        r('emblem_size', gprefs)
448        r('emblem_position', gprefs, choices=[
449            (_('Left'), 'left'), (_('Top'), 'top'), (_('Right'), 'right'), (_('Bottom'), 'bottom')])
450        r('book_list_extra_row_spacing', gprefs)
451        r('booklist_grid', gprefs)
452        r('book_details_comments_heading_pos', gprefs, choices=[
453            (_('Never'), 'hide'), (_('Above text'), 'above'), (_('Beside text'), 'side')])
454        self.cover_browser_title_template_button.clicked.connect(self.edit_cb_title_template)
455        self.id_links_button.clicked.connect(self.edit_id_link_rules)
456
457        def get_esc_lang(l):
458            if l == 'en':
459                return 'English'
460            return get_language(l)
461
462        lang = get_lang()
463        if lang is None or lang not in available_translations():
464            lang = 'en'
465        items = [(l, get_esc_lang(l)) for l in available_translations()
466                 if l != lang]
467        if lang != 'en':
468            items.append(('en', get_esc_lang('en')))
469        items.sort(key=lambda x: x[1].lower())
470        choices = [(y, x) for x, y in items]
471        # Default language is the autodetected one
472        choices = [(get_language(lang), lang)] + choices
473        r('language', prefs, choices=choices, restart_required=True)
474
475        r('show_avg_rating', config)
476        r('disable_animations', config)
477        r('systray_icon', config, restart_required=True)
478        r('show_splash_screen', gprefs)
479        r('disable_tray_notification', config)
480        r('use_roman_numerals_for_series_number', config)
481        r('separate_cover_flow', config, restart_required=True)
482        r('cb_fullscreen', gprefs)
483        r('cb_preserve_aspect_ratio', gprefs)
484        r('cb_double_click_to_activate', gprefs)
485
486        choices = [(_('Off'), 'off'), (_('Small'), 'small'),
487            (_('Medium'), 'medium'), (_('Large'), 'large')]
488        r('toolbar_icon_size', gprefs, choices=choices)
489
490        choices = [(_('If there is enough room'), 'auto'), (_('Always'), 'always'),
491            (_('Never'), 'never')]
492        r('toolbar_text', gprefs, choices=choices)
493
494        choices = [(_('Disabled'), 'disable'), (_('By first letter'), 'first letter'),
495                   (_('Partitioned'), 'partition')]
496        r('tags_browser_partition_method', gprefs, choices=choices)
497        r('tags_browser_collapse_at', gprefs)
498        r('tags_browser_collapse_fl_at', gprefs)
499
500        choices = {k for k in db.field_metadata.all_field_keys()
501                if (db.field_metadata[k]['is_category'] and (
502                    db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration'
503                    ]) and not db.field_metadata[k]['display'].get('is_names', False)) or (
504                    db.field_metadata[k]['datatype'] in ['composite'
505                    ] and db.field_metadata[k]['display'].get('make_category', False))}
506        choices |= {'search'}
507        r('tag_browser_dont_collapse', gprefs, setting=CommaSeparatedList,
508          choices=sorted(choices, key=sort_key))
509
510        choices -= {'authors', 'publisher', 'formats', 'news', 'identifiers'}
511        r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList,
512          choices=sorted(choices, key=sort_key))
513
514        fm = db.field_metadata
515        choices = sorted(((fm[k]['name'], k) for k in fm.displayable_field_keys() if fm[k]['name']),
516                         key=lambda x:sort_key(x[0]))
517        r('field_under_covers_in_grid', db.prefs, choices=choices)
518
519        self.current_font = self.initial_font = None
520        self.change_font_button.clicked.connect(self.change_font)
521
522        self.display_model = DisplayedFields(self.gui.current_db,
523                self.field_display_order)
524        self.display_model.dataChanged.connect(self.changed_signal)
525        self.field_display_order.setModel(self.display_model)
526        connect_lambda(self.df_up_button.clicked, self,
527                lambda self: move_field_up(self.field_display_order, self.display_model))
528        connect_lambda(self.df_down_button.clicked, self,
529                lambda self: move_field_down(self.field_display_order, self.display_model))
530
531        self.qv_display_model = QVDisplayedFields(self.gui.current_db,
532                self.qv_display_order)
533        self.qv_display_model.dataChanged.connect(self.changed_signal)
534        self.qv_display_order.setModel(self.qv_display_model)
535        connect_lambda(self.qv_up_button.clicked, self,
536                lambda self: move_field_up(self.qv_display_order, self.qv_display_model))
537        connect_lambda(self.qv_down_button.clicked, self,
538                lambda self: move_field_down(self.qv_display_order, self.qv_display_model))
539
540        self.edit_rules = EditRules(self.tabWidget)
541        self.edit_rules.changed.connect(self.changed_signal)
542        self.tabWidget.addTab(self.edit_rules,
543                QIcon(I('format-fill-color.png')), _('Column &coloring'))
544
545        self.icon_rules = EditRules(self.tabWidget)
546        self.icon_rules.changed.connect(self.changed_signal)
547        self.tabWidget.addTab(self.icon_rules,
548                QIcon(I('icon_choose.png')), _('Column &icons'))
549
550        self.grid_rules = EditRules(self.emblems_tab)
551        self.grid_rules.changed.connect(self.changed_signal)
552        self.emblems_tab.setLayout(QVBoxLayout())
553        self.emblems_tab.layout().addWidget(self.grid_rules)
554
555        self.tabWidget.setCurrentIndex(0)
556        keys = [QKeySequence('F11', QKeySequence.SequenceFormat.PortableText), QKeySequence(
557            'Ctrl+Shift+F', QKeySequence.SequenceFormat.PortableText)]
558        keys = [str(x.toString(QKeySequence.SequenceFormat.NativeText)) for x in keys]
559        self.fs_help_msg.setText(self.fs_help_msg.text()%(
560            QKeySequence(QKeySequence.StandardKey.FullScreen).toString(QKeySequence.SequenceFormat.NativeText)))
561        self.size_calculated.connect(self.update_cg_cache_size, type=Qt.ConnectionType.QueuedConnection)
562        self.tabWidget.currentChanged.connect(self.tab_changed)
563
564        l = self.cg_background_box.layout()
565        self.cg_bg_widget = w = Background(self)
566        l.addWidget(w, 0, 0, 3, 1)
567        self.cover_grid_color_button = b = QPushButton(_('Change &color'), self)
568        b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
569        l.addWidget(b, 0, 1)
570        b.clicked.connect(self.change_cover_grid_color)
571        self.cover_grid_texture_button = b = QPushButton(_('Change &background image'), self)
572        b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
573        l.addWidget(b, 1, 1)
574        b.clicked.connect(self.change_cover_grid_texture)
575        self.cover_grid_default_appearance_button = b = QPushButton(_('Restore default &appearance'), self)
576        b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
577        l.addWidget(b, 2, 1)
578        b.clicked.connect(self.restore_cover_grid_appearance)
579        self.cover_grid_empty_cache.clicked.connect(self.empty_cache)
580        self.cover_grid_open_cache.clicked.connect(self.open_cg_cache)
581        connect_lambda(self.cover_grid_smaller_cover.clicked, self, lambda self: self.resize_cover(True))
582        connect_lambda(self.cover_grid_larger_cover.clicked, self, lambda self: self.resize_cover(False))
583        self.cover_grid_reset_size.clicked.connect(self.cg_reset_size)
584        self.opt_cover_grid_disk_cache_size.setMinimum(self.gui.grid_view.thumbnail_cache.min_disk_cache)
585        self.opt_cover_grid_disk_cache_size.setMaximum(self.gui.grid_view.thumbnail_cache.min_disk_cache * 100)
586        self.opt_cover_grid_width.valueChanged.connect(self.update_aspect_ratio)
587        self.opt_cover_grid_height.valueChanged.connect(self.update_aspect_ratio)
588        self.opt_book_details_css.textChanged.connect(self.changed_signal)
589        from calibre.gui2.tweak_book.editor.text import get_highlighter, get_theme
590        self.css_highlighter = get_highlighter('css')()
591        self.css_highlighter.apply_theme(get_theme(None))
592        self.css_highlighter.set_document(self.opt_book_details_css.document())
593
594    def choose_icon_theme(self):
595        from calibre.gui2.icon_theme import ChooseTheme
596        d = ChooseTheme(self)
597        if d.exec() == QDialog.DialogCode.Accepted:
598            self.commit_icon_theme = d.commit_changes
599            self.icon_theme_title = d.new_theme_title or _('Default icons')
600            self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
601            self.changed_signal.emit()
602
603    def edit_id_link_rules(self):
604        if IdLinksEditor(self).exec() == QDialog.DialogCode.Accepted:
605            self.changed_signal.emit()
606
607    @property
608    def current_cover_size(self):
609        cval = self.opt_cover_grid_height.value()
610        wval = self.opt_cover_grid_width.value()
611        if cval < 0.1:
612            dpi = self.opt_cover_grid_height.logicalDpiY()
613            cval = auto_height(self.opt_cover_grid_height) / dpi / CM_TO_INCH
614        if wval < 0.1:
615            wval = 0.75 * cval
616        return wval, cval
617
618    def update_aspect_ratio(self, *args):
619        width, height = self.current_cover_size
620        ar = width / height
621        self.cover_grid_aspect_ratio.setText(_('Current aspect ratio (width/height): %.2g') % ar)
622
623    def resize_cover(self, smaller):
624        wval, cval = self.current_cover_size
625        ar = wval / cval
626        delta = 0.2 * (-1 if smaller else 1)
627        cval += delta
628        cval = max(0, cval)
629        self.opt_cover_grid_height.setValue(cval)
630        self.opt_cover_grid_width.setValue(cval * ar)
631
632    def cg_reset_size(self):
633        self.opt_cover_grid_width.setValue(0)
634        self.opt_cover_grid_height.setValue(0)
635
636    def edit_cb_title_template(self):
637        t = TemplateDialog(self, self.opt_cover_browser_title_template.text(), fm=self.gui.current_db.field_metadata)
638        t.setWindowTitle(_('Edit template for caption'))
639        if t.exec():
640            self.opt_cover_browser_title_template.setText(t.rule[1])
641
642    def initialize(self):
643        ConfigWidgetBase.initialize(self)
644        self.default_author_link.value = default_author_link()
645        font = gprefs['font']
646        if font is not None:
647            font = list(font)
648            font.append(gprefs.get('font_stretch', QFont.Stretch.Unstretched))
649        self.current_font = self.initial_font = font
650        self.update_font_display()
651        self.display_model.initialize()
652        self.qv_display_model.initialize()
653        db = self.gui.current_db
654        try:
655            idx = self.gui.library_view.currentIndex().row()
656            mi = db.get_metadata(idx, index_is_id=False)
657        except:
658            mi=None
659        self.edit_rules.initialize(db.field_metadata, db.prefs, mi, 'column_color_rules')
660        self.icon_rules.initialize(db.field_metadata, db.prefs, mi, 'column_icon_rules')
661        self.grid_rules.initialize(db.field_metadata, db.prefs, mi, 'cover_grid_icon_rules')
662        self.set_cg_color(gprefs['cover_grid_color'])
663        self.set_cg_texture(gprefs['cover_grid_texture'])
664        self.update_aspect_ratio()
665        self.opt_book_details_css.blockSignals(True)
666        self.opt_book_details_css.setPlainText(P('templates/book_details.css', data=True).decode('utf-8'))
667        self.opt_book_details_css.blockSignals(False)
668        self.tb_focus_label.setVisible(self.opt_tag_browser_allow_keyboard_focus.isChecked())
669
670    def open_cg_cache(self):
671        open_local_file(self.gui.grid_view.thumbnail_cache.location)
672
673    def update_cg_cache_size(self, size):
674        self.cover_grid_current_disk_cache.setText(
675            _('Current space used: %s') % human_readable(size))
676
677    def tab_changed(self, index):
678        if self.tabWidget.currentWidget() is self.cover_grid_tab:
679            self.show_current_cache_usage()
680
681    def show_current_cache_usage(self):
682        t = Thread(target=self.calc_cache_size)
683        t.daemon = True
684        t.start()
685
686    def calc_cache_size(self):
687        self.size_calculated.emit(self.gui.grid_view.thumbnail_cache.current_size)
688
689    def set_cg_color(self, val):
690        self.cg_bg_widget.bcol = QColor(*val)
691        self.cg_bg_widget.update_brush()
692
693    def set_cg_texture(self, val):
694        self.cg_bg_widget.btex = val
695        self.cg_bg_widget.update_brush()
696
697    def empty_cache(self):
698        self.gui.grid_view.thumbnail_cache.empty()
699        self.calc_cache_size()
700
701    def restore_defaults(self):
702        ConfigWidgetBase.restore_defaults(self)
703        self.default_author_link.value = DEFAULT_AUTHOR_LINK
704        ofont = self.current_font
705        self.current_font = None
706        if ofont is not None:
707            self.changed_signal.emit()
708            self.update_font_display()
709        self.display_model.restore_defaults()
710        self.qv_display_model.restore_defaults()
711        self.edit_rules.clear()
712        self.icon_rules.clear()
713        self.grid_rules.clear()
714        self.changed_signal.emit()
715        self.set_cg_color(gprefs.defaults['cover_grid_color'])
716        self.set_cg_texture(gprefs.defaults['cover_grid_texture'])
717        self.opt_book_details_css.setPlainText(P('templates/book_details.css', allow_user_override=False, data=True).decode('utf-8'))
718
719    def change_cover_grid_color(self):
720        col = QColorDialog.getColor(self.cg_bg_widget.bcol,
721                              self.gui, _('Choose background color for the Cover grid'))
722        if col.isValid():
723            col = tuple(col.getRgb())[:3]
724            self.set_cg_color(col)
725            self.changed_signal.emit()
726            if self.cg_bg_widget.btex:
727                if question_dialog(
728                    self, _('Remove background image?'),
729                    _('There is currently a background image set, so the color'
730                      ' you have chosen will not be visible. Remove the background image?')):
731                    self.set_cg_texture(None)
732
733    def change_cover_grid_texture(self):
734        from calibre.gui2.preferences.texture_chooser import TextureChooser
735        d = TextureChooser(parent=self, initial=self.cg_bg_widget.btex)
736        if d.exec() == QDialog.DialogCode.Accepted:
737            self.set_cg_texture(d.texture)
738            self.changed_signal.emit()
739
740    def restore_cover_grid_appearance(self):
741        self.set_cg_color(gprefs.defaults['cover_grid_color'])
742        self.set_cg_texture(gprefs.defaults['cover_grid_texture'])
743        self.changed_signal.emit()
744
745    def build_font_obj(self):
746        font_info = qt_app.original_font if self.current_font is None else self.current_font
747        font = QFont(*(font_info[:4]))
748        font.setStretch(font_info[4])
749        return font
750
751    def update_font_display(self):
752        font = self.build_font_obj()
753        fi = QFontInfo(font)
754        name = str(fi.family())
755
756        self.font_display.setFont(font)
757        self.font_display.setText(name + ' [%dpt]'%fi.pointSize())
758
759    def change_font(self, *args):
760        fd = QFontDialog(self.build_font_obj(), self)
761        if fd.exec() == QDialog.DialogCode.Accepted:
762            font = fd.selectedFont()
763            fi = QFontInfo(font)
764            self.current_font = [str(fi.family()), fi.pointSize(),
765                    fi.weight(), fi.italic(), font.stretch()]
766            self.update_font_display()
767            self.changed_signal.emit()
768
769    def commit(self, *args):
770        with BusyCursor():
771            rr = ConfigWidgetBase.commit(self, *args)
772            if self.current_font != self.initial_font:
773                gprefs['font'] = (self.current_font[:4] if self.current_font else
774                        None)
775                gprefs['font_stretch'] = (self.current_font[4] if self.current_font
776                        is not None else QFont.Stretch.Unstretched)
777                QApplication.setFont(self.font_display.font())
778                rr = True
779            self.display_model.commit()
780            self.qv_display_model.commit()
781            self.edit_rules.commit(self.gui.current_db.prefs)
782            self.icon_rules.commit(self.gui.current_db.prefs)
783            self.grid_rules.commit(self.gui.current_db.prefs)
784            gprefs['cover_grid_color'] = tuple(self.cg_bg_widget.bcol.getRgb())[:3]
785            gprefs['cover_grid_texture'] = self.cg_bg_widget.btex
786            if self.commit_icon_theme is not None:
787                self.commit_icon_theme()
788                rr = True
789            gprefs['default_author_link'] = self.default_author_link.value
790            bcss = self.opt_book_details_css.toPlainText().encode('utf-8')
791            defcss = P('templates/book_details.css', data=True, allow_user_override=False)
792            if defcss == bcss:
793                bcss = None
794            set_data('templates/book_details.css', bcss)
795
796        return rr
797
798    def refresh_gui(self, gui):
799        gui.book_details.book_info.refresh_css()
800        m = gui.library_view.model()
801        m.beginResetModel(), m.endResetModel()
802        self.update_font_display()
803        gui.tags_view.set_look_and_feel()
804        gui.tags_view.reread_collapse_parameters()
805        gui.library_view.refresh_book_details(force=True)
806        gui.library_view.refresh_grid()
807        gui.library_view.set_row_header_visibility()
808        gui.cover_flow.setShowReflections(gprefs['cover_browser_reflections'])
809        gui.cover_flow.setPreserveAspectRatio(gprefs['cb_preserve_aspect_ratio'])
810        gui.cover_flow.setActivateOnDoubleClick(gprefs['cb_double_click_to_activate'])
811        gui.update_cover_flow_subtitle_font()
812        gui.cover_flow.template_inited = False
813        for view in 'library memory card_a card_b'.split():
814            getattr(gui, view + '_view').set_row_header_visibility()
815        gui.library_view.refresh_row_sizing()
816        gui.grid_view.refresh_settings()
817        gui.update_auto_scroll_timeout()
818        qv = get_quickview_action_plugin()
819        if qv:
820            qv.refill_quickview()
821
822
823if __name__ == '__main__':
824    from calibre.gui2 import Application
825    app = Application([])
826    test_widget('Interface', 'Look & Feel')
827