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