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