From 19a2115618a3a5d0624eaa8f5ca0b3aa821033a7 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 10 May 2025 17:39:54 +0100 Subject: [PATCH 1/6] Change the plugin configuration do_user_config() to reopen the the dialog if the validator fails. This happens only if the plugin has a config widget and validators. --- src/calibre/customize/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 568267b6be..f6dbd64ce1 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -180,14 +180,19 @@ class Plugin: # {{{ v.addWidget(button_box) if not config_dialog.restore_geometry(gprefs, prefname): QApplication.instance().ensure_window_on_screen(config_dialog) - config_dialog.exec() - - if config_dialog.result() == QDialog.DialogCode.Accepted: - if hasattr(config_widget, 'validate'): - if config_widget.validate(): + while True: + validation_error = False + config_dialog.exec() + if config_dialog.result() == QDialog.DialogCode.Accepted: + if hasattr(config_widget, 'validate'): + if config_widget.validate(): + self.save_settings(config_widget) + else: + validation_error = True + else: self.save_settings(config_widget) - else: - self.save_settings(config_widget) + if not validation_error: + break else: from calibre.customize.ui import customize_plugin, plugin_customization help_text = self.customization_help(gui=True) From 7416130dfc525e95c97f1e0cf98af84dafa72cfa Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 10 May 2025 17:40:56 +0100 Subject: [PATCH 2/6] Change the validation formatter to throw exceptions. As all uses of that formatter in base calibre expect exceptions, validation wasn't working. --- src/calibre/utils/formatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 6dfeb7819c..5c9985d86d 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -2035,7 +2035,7 @@ class ValidateFormatter(TemplateFormatter): def validate(self, x): from calibre.ebooks.metadata.book.base import Metadata - return self.safe_format(x, {}, 'VALIDATE ERROR', Metadata('')) + return self.unsafe_format(x, {}, 'VALIDATE ERROR', Metadata('')) validation_formatter = ValidateFormatter() From 768c575bb283646e355680650f09fe4f6171ac86 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 10 May 2025 17:46:26 +0100 Subject: [PATCH 3/6] Several changes: - Add validation to the list of lookup names entered into the tags. - Add validation to the template used to choose whether to kepubify a book. - Make the validation dialogs work. This requires the change to do_user_config(). - Add the name of the option failing validation to the title of the error message. --- src/calibre/devices/kobo/kobotouch_config.py | 47 ++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py index 353b14fb8a..d4b6c280f3 100644 --- a/src/calibre/devices/kobo/kobotouch_config.py +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -88,6 +88,7 @@ class KOBOTOUCHConfig(TabbedDeviceConfig): def validate(self): validated = super().validate() validated &= self.tab2.validate() + validated &= self.tab1.validate() return validated @property @@ -193,6 +194,12 @@ class Tab1Config(DeviceConfigTab): # {{{ self.addDeviceWidget(self.book_uploads_options) self.l.addStretch() + + def validate(self): + v = self.collections_options.validate() + v &= self.book_uploads_options.validate() + return v + # }}} @@ -364,6 +371,7 @@ class BookUploadsGroupBox(DeviceOptionsGroupBox): self.template_la = la = QLabel('\xa0\xa0' + _('Template to decide conversion:')) self.kepubify_template_edit = TemplateConfig( + self.kepubify_checkbox.text(), device.get_pref('template_for_kepubify'), tooltip='

' + _( 'Enter a template to decide if an EPUB book is to be auto converted to KEPUB. ' @@ -399,6 +407,9 @@ class BookUploadsGroupBox(DeviceOptionsGroupBox): def update_template_state(self): self.kepubify_template_edit.setEnabled(self.kepubify) + def validate(self): + return self.kepubify_template_edit.validate() + @property def override_kobo_replace_existing(self): return self.override_kobo_replace_existing_checkbox.isChecked() @@ -518,6 +529,7 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): device.get_pref('use_collections_template') ) self.collections_template_edit = TemplateConfig( + self.use_collections_template_checkbox.text(), device.get_pref('collections_template'), tooltip=_("Enter a template to generate collections." " The result of the template will be combined with the values from Collections column." @@ -558,6 +570,29 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): self.use_collections_template_checkbox.clicked.connect(self.use_collections_template_checkbox_clicked) self.use_collections_columns_checkbox_clicked(device.get_pref('use_collections_columns')) self.use_collections_template_checkbox_clicked(device.get_pref('use_collections_template')) + self.collections_columns_edit.editingFinished.connect(self.validate_collections_columns) + + def validate(self): + v = self.validate_collections_columns() + v &= self.collections_template_edit.validate() + return v + + def validate_collections_columns(self): + from calibre.gui2.ui import get_gui + db = get_gui().current_db + fm = db.field_metadata + bad_names = [] + for l in [v.strip() for v in self.collections_columns.split(',') if v.strip()]: + if l not in fm.keys(): + bad_names.append(l) + if bad_names: + s = ', '.join(bad_names) + error_dialog(self, _('Kobo configuration: Invalid collection column names'), + '

'+_("Collection column names that don't exist in the library: {0}").format(s), + show=True) + self.collections_columns_edit.setFocus(Qt.FocusReason.OtherFocusReason) + return False + return True def use_collections_columns_checkbox_clicked(self, checked): self.collections_columns_edit.setEnabled(checked) @@ -872,6 +907,7 @@ class MetadataGroupBox(DeviceOptionsGroupBox): device.get_pref('update_subtitle') ) self.subtitle_template_edit = TemplateConfig( + self.update_subtitle_checkbox.text(), device.get_pref('subtitle_template'), tooltip=_('Enter a template to use to set the subtitle. ' 'If the template is empty, the subtitle will be cleared.' @@ -883,6 +919,7 @@ class MetadataGroupBox(DeviceOptionsGroupBox): device.get_pref('update_bookstats') ) self.bookstats_wordcount_template_edit = TemplateConfig( + self.update_bookstats_checkbox.text(), device.get_pref('bookstats_wordcount_template'), label=_('Words:'), tooltip=_('Enter a template to use to set the word count for the book. ' @@ -890,6 +927,7 @@ class MetadataGroupBox(DeviceOptionsGroupBox): ) ) self.bookstats_pagecount_template_edit = TemplateConfig( + _('Pages'), device.get_pref('bookstats_pagecount_template'), label=_('Pages:'), tooltip=_('Enter a template to use to set the page count for the book. ' @@ -899,6 +937,7 @@ class MetadataGroupBox(DeviceOptionsGroupBox): self.bookstats_timetoread_label = QLabel(_('Hours to read estimates:')) self.bookstats_timetoread_upper_template_edit = TemplateConfig( + _('Upper estimate'), device.get_pref('bookstats_timetoread_upper_template'), label=_('Upper:'), tooltip=_('Enter a template to use to set the upper estimate of the time to read for the book. ' @@ -907,6 +946,7 @@ class MetadataGroupBox(DeviceOptionsGroupBox): ) ) self.bookstats_timetoread_lower_template_edit = TemplateConfig( + _('Lower estimate'), device.get_pref('bookstats_timetoread_lower_template'), label=_('Lower:'), tooltip=_('Enter a template to use to set the lower estimate of the time to read for the book. ' @@ -1042,8 +1082,9 @@ class MetadataGroupBox(DeviceOptionsGroupBox): class TemplateConfig(QWidget): # {{{ - def __init__(self, val, label=None, tooltip=None): + def __init__(self, name, val, label=None, tooltip=None): super().__init__() + self.name = name self.l = l = QGridLayout(self) self.setLayout(l) col = 0 @@ -1080,10 +1121,10 @@ class TemplateConfig(QWidget): # {{{ tmpl = self.template try: - validation_formatter.validate(tmpl) + v = validation_formatter.validate(tmpl) return True except Exception as err: - error_dialog(self, _('Invalid template'), + error_dialog(self, _('Invalid template for {0}').format(self.name), '

'+_('The template "%s" is invalid:')%tmpl + '
'+str(err), show=True) From ff4e978ce51d5c95b0175cdad871838c335bfd51 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 11 May 2025 10:39:05 +0100 Subject: [PATCH 4/6] Revert "Change the plugin configuration do_user_config() to reopen the the dialog if the validator fails. This happens only if the plugin has a config widget and validators." This reverts commit 19a2115618a3a5d0624eaa8f5ca0b3aa821033a7. --- src/calibre/customize/__init__.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index f6dbd64ce1..568267b6be 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -180,19 +180,14 @@ class Plugin: # {{{ v.addWidget(button_box) if not config_dialog.restore_geometry(gprefs, prefname): QApplication.instance().ensure_window_on_screen(config_dialog) - while True: - validation_error = False - config_dialog.exec() - if config_dialog.result() == QDialog.DialogCode.Accepted: - if hasattr(config_widget, 'validate'): - if config_widget.validate(): - self.save_settings(config_widget) - else: - validation_error = True - else: + config_dialog.exec() + + if config_dialog.result() == QDialog.DialogCode.Accepted: + if hasattr(config_widget, 'validate'): + if config_widget.validate(): self.save_settings(config_widget) - if not validation_error: - break + else: + self.save_settings(config_widget) else: from calibre.customize.ui import customize_plugin, plugin_customization help_text = self.customization_help(gui=True) From d4fdd34d7405084b211f86c31407cbe151d6ab90 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 11 May 2025 11:15:58 +0100 Subject: [PATCH 5/6] If there is both an attribute validate_before_accept=True and a method validate() in the config_widget then calll validate() when the config dialog OK button is pressed. --- src/calibre/customize/__init__.py | 28 +++++++++++++++---- src/calibre/devices/kobo/kobotouch_config.py | 8 +++--- .../device_drivers/tabbed_device_config.py | 6 +++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 568267b6be..52f1085d39 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -149,17 +149,35 @@ class Plugin: # {{{ from calibre.gui2 import gprefs + class ConfigDialog(QDialog): + + def __init__(self, parent, config_widget): + super().__init__(parent) + self.config_widget = config_widget + + def accept(self): + print('in accept') + if ((validate := getattr(self.config_widget, 'validate', None)) and + getattr(self.config_widget, 'validate_before_accept', False)): + print('have validate and validate_before_accept') + if not validate(): + return + print('accepting') + super().accept() + + try: + config_widget = self.config_widget() + except NotImplementedError: + config_widget = None + prefname = 'plugin config dialog:'+self.type + ':' + self.name - config_dialog = QDialog(parent) + + config_dialog = ConfigDialog(parent, config_widget) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) v = QVBoxLayout(config_dialog) button_box.accepted.connect(config_dialog.accept) button_box.rejected.connect(config_dialog.reject) config_dialog.setWindowTitle(_('Customize') + ' ' + self.name) - try: - config_widget = self.config_widget() - except NotImplementedError: - config_widget = None if isinstance(config_widget, tuple): from calibre.gui2 import warning_dialog diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py index d4b6c280f3..4f01b2adbc 100644 --- a/src/calibre/devices/kobo/kobotouch_config.py +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -54,11 +54,13 @@ class KOBOTOUCHConfig(TabbedDeviceConfig): def __init__(self, device_settings, all_formats, supports_subdirs, must_read_metadata, supports_use_author_sort, - extra_customization_message, device, extra_customization_choices=None, parent=None): + extra_customization_message, device, extra_customization_choices=None, + parent=None): super().__init__(device_settings, all_formats, supports_subdirs, must_read_metadata, supports_use_author_sort, - extra_customization_message, device, extra_customization_choices, parent) + extra_customization_message, device, extra_customization_choices, parent, + validate_before_accept=True) self.device_settings = device_settings self.all_formats = all_formats @@ -570,7 +572,6 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): self.use_collections_template_checkbox.clicked.connect(self.use_collections_template_checkbox_clicked) self.use_collections_columns_checkbox_clicked(device.get_pref('use_collections_columns')) self.use_collections_template_checkbox_clicked(device.get_pref('use_collections_template')) - self.collections_columns_edit.editingFinished.connect(self.validate_collections_columns) def validate(self): v = self.validate_collections_columns() @@ -590,7 +591,6 @@ class CollectionsGroupBox(DeviceOptionsGroupBox): error_dialog(self, _('Kobo configuration: Invalid collection column names'), '

'+_("Collection column names that don't exist in the library: {0}").format(s), show=True) - self.collections_columns_edit.setFocus(Qt.FocusReason.OtherFocusReason) return False return True diff --git a/src/calibre/gui2/device_drivers/tabbed_device_config.py b/src/calibre/gui2/device_drivers/tabbed_device_config.py index d8bae0d0ee..1fe7bbab1b 100644 --- a/src/calibre/gui2/device_drivers/tabbed_device_config.py +++ b/src/calibre/gui2/device_drivers/tabbed_device_config.py @@ -63,15 +63,19 @@ class TabbedDeviceConfig(QTabWidget): DeviceConfigTab, for each set of options. Within the tabs, group boxes, subclassed from DeviceOptionsGroupBox, are created to further group the options. The group boxes can be coded to support any control type and dependencies between them. + + Set validate_before_accept to True if you want validation() to be called + when OK is pressed ''' def __init__(self, device_settings, all_formats, supports_subdirs, must_read_metadata, supports_use_author_sort, extra_customization_message, device, - extra_customization_choices=None, parent=None): + extra_customization_choices=None, parent=None, validate_before_accept = False): QTabWidget.__init__(self, parent) self._device = weakref.ref(device) + self.validate_before_accept = validate_before_accept self.device_settings = device_settings self.all_formats = set(all_formats) self.supports_subdirs = supports_subdirs From c4a57defff13748b86694b3607f357f36c2e0763 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 11 May 2025 11:27:30 +0100 Subject: [PATCH 6/6] Remove print statements --- src/calibre/customize/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 52f1085d39..7f63ea7a3c 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -156,13 +156,10 @@ class Plugin: # {{{ self.config_widget = config_widget def accept(self): - print('in accept') if ((validate := getattr(self.config_widget, 'validate', None)) and getattr(self.config_widget, 'validate_before_accept', False)): - print('have validate and validate_before_accept') if not validate(): return - print('accepting') super().accept() try: