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 textwrap 10 11from qt.core import (QWidget, pyqtSignal, QCheckBox, QAbstractSpinBox, QApplication, 12 QLineEdit, QComboBox, Qt, QIcon, QDialog, QVBoxLayout, 13 QDialogButtonBox) 14 15from calibre.customize.ui import preferences_plugins 16from calibre.utils.config import ConfigProxy 17from calibre.gui2.complete2 import EditWithComplete 18from calibre.gui2.widgets import HistoryLineEdit 19from polyglot.builtins import string_or_bytes 20 21 22class AbortCommit(Exception): 23 pass 24 25 26class AbortInitialize(Exception): 27 pass 28 29 30class ConfigWidgetInterface: 31 32 ''' 33 This class defines the interface that all widgets displayed in the 34 Preferences dialog must implement. See :class:`ConfigWidgetBase` for 35 a base class that implements this interface and defines various convenience 36 methods as well. 37 ''' 38 39 #: This signal must be emitted whenever the user changes a value in this 40 #: widget 41 changed_signal = None 42 43 #: Set to True iff the :meth:`restore_to_defaults` method is implemented. 44 supports_restoring_to_defaults = True 45 46 #: The tooltip for the "Restore defaults" button 47 restore_defaults_desc = _('Restore settings to default values. ' 48 'You have to click Apply to actually save the default settings.') 49 50 #: If True the Preferences dialog will not allow the user to set any more 51 #: preferences. Only has effect if :meth:`commit` returns True. 52 restart_critical = False 53 54 def genesis(self, gui): 55 ''' 56 Called once before the widget is displayed, should perform any 57 necessary setup. 58 59 :param gui: The main calibre graphical user interface 60 ''' 61 raise NotImplementedError() 62 63 def initialize(self): 64 ''' 65 Should set all config values to their initial values (the values 66 stored in the config files). A "return" statement is optional. Return 67 False if the dialog is not to be shown. 68 ''' 69 raise NotImplementedError() 70 71 def restore_defaults(self): 72 ''' 73 Should set all config values to their defaults. 74 ''' 75 pass 76 77 def commit(self): 78 ''' 79 Save any changed settings. Return True if the changes require a 80 restart, False otherwise. Raise an :class:`AbortCommit` exception 81 to indicate that an error occurred. You are responsible for giving the 82 user feedback about what the error is and how to correct it. 83 ''' 84 return False 85 86 def refresh_gui(self, gui): 87 ''' 88 Called once after this widget is committed. Responsible for causing the 89 gui to reread any changed settings. Note that by default the GUI 90 re-initializes various elements anyway, so most widgets won't need to 91 use this method. 92 ''' 93 pass 94 95 96class Setting: 97 98 CHOICES_SEARCH_FLAGS = Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive 99 100 def __init__(self, name, config_obj, widget, gui_name=None, 101 empty_string_is_None=True, choices=None, restart_required=False): 102 self.name, self.gui_name = name, gui_name 103 self.empty_string_is_None = empty_string_is_None 104 self.restart_required = restart_required 105 self.choices = choices 106 if gui_name is None: 107 self.gui_name = 'opt_'+name 108 self.config_obj = config_obj 109 self.gui_obj = getattr(widget, self.gui_name) 110 self.widget = widget 111 112 if isinstance(self.gui_obj, QCheckBox): 113 self.datatype = 'bool' 114 self.gui_obj.stateChanged.connect(self.changed) 115 elif isinstance(self.gui_obj, QAbstractSpinBox): 116 self.datatype = 'number' 117 self.gui_obj.valueChanged.connect(self.changed) 118 elif isinstance(self.gui_obj, (QLineEdit, HistoryLineEdit)): 119 self.datatype = 'string' 120 self.gui_obj.textChanged.connect(self.changed) 121 if isinstance(self.gui_obj, HistoryLineEdit): 122 self.gui_obj.initialize('preferences_setting_' + self.name) 123 elif isinstance(self.gui_obj, QComboBox): 124 self.datatype = 'choice' 125 self.gui_obj.editTextChanged.connect(self.changed) 126 self.gui_obj.currentIndexChanged.connect(self.changed) 127 else: 128 raise ValueError('Unknown data type %s' % self.gui_obj.__class__) 129 130 if isinstance(self.config_obj, ConfigProxy) and \ 131 not str(self.gui_obj.toolTip()): 132 h = self.config_obj.help(self.name) 133 if h: 134 self.gui_obj.setToolTip(h) 135 tt = str(self.gui_obj.toolTip()) 136 if tt: 137 if not str(self.gui_obj.whatsThis()): 138 self.gui_obj.setWhatsThis(tt) 139 if not str(self.gui_obj.statusTip()): 140 self.gui_obj.setStatusTip(tt) 141 tt = '\n'.join(textwrap.wrap(tt, 70)) 142 self.gui_obj.setToolTip(tt) 143 144 def changed(self, *args): 145 self.widget.changed_signal.emit() 146 147 def initialize(self): 148 self.gui_obj.blockSignals(True) 149 if self.datatype == 'choice': 150 choices = self.choices or [] 151 if isinstance(self.gui_obj, EditWithComplete): 152 self.gui_obj.all_items = choices 153 else: 154 self.gui_obj.clear() 155 for x in choices: 156 if isinstance(x, string_or_bytes): 157 x = (x, x) 158 self.gui_obj.addItem(x[0], (x[1])) 159 self.set_gui_val(self.get_config_val(default=False)) 160 self.gui_obj.blockSignals(False) 161 self.initial_value = self.get_gui_val() 162 163 def commit(self): 164 val = self.get_gui_val() 165 oldval = self.get_config_val() 166 changed = val != oldval 167 if changed: 168 self.set_config_val(self.get_gui_val()) 169 return changed and self.restart_required 170 171 def restore_defaults(self): 172 self.set_gui_val(self.get_config_val(default=True)) 173 174 def get_config_val(self, default=False): 175 if default: 176 val = self.config_obj.defaults[self.name] 177 else: 178 val = self.config_obj[self.name] 179 return val 180 181 def set_config_val(self, val): 182 self.config_obj[self.name] = val 183 184 def set_gui_val(self, val): 185 if self.datatype == 'bool': 186 self.gui_obj.setChecked(bool(val)) 187 elif self.datatype == 'number': 188 self.gui_obj.setValue(val) 189 elif self.datatype == 'string': 190 self.gui_obj.setText(val if val else '') 191 elif self.datatype == 'choice': 192 if isinstance(self.gui_obj, EditWithComplete): 193 self.gui_obj.setText(val) 194 else: 195 idx = self.gui_obj.findData((val), role=Qt.ItemDataRole.UserRole, 196 flags=self.CHOICES_SEARCH_FLAGS) 197 if idx == -1: 198 idx = 0 199 self.gui_obj.setCurrentIndex(idx) 200 201 def get_gui_val(self): 202 if self.datatype == 'bool': 203 val = bool(self.gui_obj.isChecked()) 204 elif self.datatype == 'number': 205 val = self.gui_obj.value() 206 elif self.datatype == 'string': 207 val = str(self.gui_obj.text()).strip() 208 if self.empty_string_is_None and not val: 209 val = None 210 elif self.datatype == 'choice': 211 if isinstance(self.gui_obj, EditWithComplete): 212 val = str(self.gui_obj.text()) 213 else: 214 idx = self.gui_obj.currentIndex() 215 if idx < 0: 216 idx = 0 217 val = str(self.gui_obj.itemData(idx) or '') 218 return val 219 220 221class CommaSeparatedList(Setting): 222 223 def set_gui_val(self, val): 224 x = '' 225 if val: 226 x = ', '.join(val) 227 self.gui_obj.setText(x) 228 229 def get_gui_val(self): 230 val = str(self.gui_obj.text()).strip() 231 ans = [] 232 if val: 233 ans = [x.strip() for x in val.split(',')] 234 ans = [x for x in ans if x] 235 return ans 236 237 238class ConfigWidgetBase(QWidget, ConfigWidgetInterface): 239 240 ''' 241 Base class that contains code to easily add standard config widgets like 242 checkboxes, combo boxes, text fields and so on. See the :meth:`register` 243 method. 244 245 This class automatically handles change notification, resetting to default, 246 translation between gui objects and config objects, etc. for registered 247 settings. 248 249 If your config widget inherits from this class but includes setting that 250 are not registered, you should override the :class:`ConfigWidgetInterface` methods 251 and call the base class methods inside the overrides. 252 ''' 253 254 changed_signal = pyqtSignal() 255 restart_now = pyqtSignal() 256 supports_restoring_to_defaults = True 257 restart_critical = False 258 259 def __init__(self, parent=None): 260 QWidget.__init__(self, parent) 261 if hasattr(self, 'setupUi'): 262 self.setupUi(self) 263 self.settings = {} 264 265 def register(self, name, config_obj, gui_name=None, choices=None, 266 restart_required=False, empty_string_is_None=True, setting=Setting): 267 ''' 268 Register a setting. 269 270 :param name: The setting name 271 :param config: The config object that reads/writes the setting 272 :param gui_name: The name of the GUI object that presents an interface 273 to change the setting. By default it is assumed to be 274 ``'opt_' + name``. 275 :param choices: If this setting is a multiple choice (combobox) based 276 setting, the list of choices. The list is a list of two 277 element tuples of the form: ``[(gui name, value), ...]`` 278 :param setting: The class responsible for managing this setting. The 279 default class handles almost all cases, so this param 280 is rarely used. 281 ''' 282 setting = setting(name, config_obj, self, gui_name=gui_name, 283 choices=choices, restart_required=restart_required, 284 empty_string_is_None=empty_string_is_None) 285 return self.register_setting(setting) 286 287 def register_setting(self, setting): 288 self.settings[setting.name] = setting 289 return setting 290 291 def initialize(self): 292 for setting in self.settings.values(): 293 setting.initialize() 294 295 def commit(self, *args): 296 restart_required = False 297 for setting in self.settings.values(): 298 rr = setting.commit() 299 if rr: 300 restart_required = True 301 return restart_required 302 303 def restore_defaults(self, *args): 304 for setting in self.settings.values(): 305 setting.restore_defaults() 306 307 308def get_plugin(category, name): 309 for plugin in preferences_plugins(): 310 if plugin.category == category and plugin.name == name: 311 return plugin 312 raise ValueError( 313 'No Preferences Plugin with category: %s and name: %s found' % 314 (category, name)) 315 316 317class ConfigDialog(QDialog): 318 319 def set_widget(self, w): 320 self.w = w 321 322 def accept(self): 323 try: 324 self.restart_required = self.w.commit() 325 except AbortCommit: 326 return 327 QDialog.accept(self) 328 329 330def init_gui(): 331 from calibre.gui2.ui import Main 332 from calibre.gui2.main import option_parser 333 from calibre.library import db 334 parser = option_parser() 335 opts, args = parser.parse_args([]) 336 actions = tuple(Main.create_application_menubar()) 337 db = db() 338 gui = Main(opts) 339 gui.initialize(db.library_path, db, actions, show_gui=False) 340 return gui 341 342 343def show_config_widget(category, name, gui=None, show_restart_msg=False, 344 parent=None, never_shutdown=False): 345 ''' 346 Show the preferences plugin identified by category and name 347 348 :param gui: gui instance, if None a hidden gui is created 349 :param show_restart_msg: If True and the preferences plugin indicates a 350 restart is required, show a message box telling the user to restart 351 :param parent: The parent of the displayed dialog 352 353 :return: True iff a restart is required for the changes made by the user to 354 take effect 355 ''' 356 from calibre.gui2 import gprefs 357 pl = get_plugin(category, name) 358 d = ConfigDialog(parent) 359 d.resize(750, 550) 360 conf_name = 'config_widget_dialog_geometry_%s_%s'%(category, name) 361 geom = gprefs.get(conf_name, None) 362 d.setWindowTitle(_('Configure ') + pl.gui_name) 363 d.setWindowIcon(QIcon(I('config.png'))) 364 bb = QDialogButtonBox(d) 365 bb.setStandardButtons(QDialogButtonBox.StandardButton.Apply|QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.RestoreDefaults) 366 bb.accepted.connect(d.accept) 367 bb.rejected.connect(d.reject) 368 w = pl.create_widget(d) 369 d.set_widget(w) 370 bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).clicked.connect(w.restore_defaults) 371 bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults) 372 bb.button(QDialogButtonBox.StandardButton.Apply).setEnabled(False) 373 bb.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(d.accept) 374 375 def onchange(): 376 b = bb.button(QDialogButtonBox.StandardButton.Apply) 377 b.setEnabled(True) 378 b.setDefault(True) 379 b.setAutoDefault(True) 380 w.changed_signal.connect(onchange) 381 bb.button(QDialogButtonBox.StandardButton.Cancel).setFocus(Qt.FocusReason.OtherFocusReason) 382 l = QVBoxLayout() 383 d.setLayout(l) 384 l.addWidget(w) 385 l.addWidget(bb) 386 mygui = gui is None 387 if gui is None: 388 gui = init_gui() 389 mygui = True 390 w.genesis(gui) 391 w.initialize() 392 if geom is not None: 393 QApplication.instance().safe_restore_geometry(d, geom) 394 d.exec() 395 geom = bytearray(d.saveGeometry()) 396 gprefs[conf_name] = geom 397 rr = getattr(d, 'restart_required', False) 398 if show_restart_msg and rr: 399 from calibre.gui2 import warning_dialog 400 warning_dialog(gui, 'Restart required', 'Restart required', show=True) 401 if mygui and not never_shutdown: 402 gui.shutdown() 403 return rr 404 405# Testing {{{ 406 407 408def test_widget(category, name, gui=None): 409 show_config_widget(category, name, gui=gui, show_restart_msg=True) 410 411 412def test_all(): 413 from qt.core import QApplication 414 app = QApplication([]) 415 app 416 gui = init_gui() 417 for plugin in preferences_plugins(): 418 test_widget(plugin.category, plugin.name, gui=gui) 419 gui.shutdown() 420 421 422if __name__ == '__main__': 423 test_all() 424# }}} 425