From 7160de141f8f2287accb630a9a98346256edfe6f Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Fri, 14 Feb 2025 16:21:07 +0000 Subject: [PATCH] Several tag browser rule editor fixes, including: * Handle non-existent lookup keys and field values. * Allow choosing a new lookup key if the one in the pref doesn't exist. * Disable changing the "Only in library" checkbox to avoid throwing away edits. --- .../look_feel_tabs/tb_icon_rules.py | 144 +++++++++++++----- .../look_feel_tabs/tb_icon_rules.ui | 4 + 2 files changed, 109 insertions(+), 39 deletions(-) diff --git a/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.py b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.py index 1ee1b3c829..c5b0bd67f1 100644 --- a/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.py +++ b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.py @@ -53,21 +53,38 @@ class StateTableWidgetItem(QTableWidgetItem): class CategoryTableWidgetItem(QTableWidgetItem): - def __init__(self, lookup_name, category_icons, display_name, field_metadata, table): - if in_library := (lookup_name in field_metadata): - txt = display_name + f' ({lookup_name})' - else: - txt = display_name + ' (' + _('Not in library') + ')' - super().__init__(txt) - self._lookup_name = lookup_name + def __init__(self, lookup_name, category_icons, field_metadata, table): + super().__init__('') self._table = table + self._category_icons = category_icons + self._field_metadata = field_metadata self._is_deleted = False - if in_library: - self.setIcon(category_icons.get(lookup_name) or QIcon.cached_icon('column.png')) + self._is_editable = False + self._is_modified = False + self._original_lookup_name = lookup_name + self._original_in_library = lookup_name in self._field_metadata + self.setText(lookup_name) + + def setText(self, lookup_name): + self._lookup_name = lookup_name + if in_library := (self._lookup_name in self._field_metadata): + txt = f"{self._field_metadata[self.lookup_name]['name']} ({self._lookup_name})" + else: + txt = f"{lookup_name} ({_('Not in library')})" + super().setText(txt) + self.setToolTip(txt) + print('aaa', self._original_lookup_name, lookup_name) + if self._original_lookup_name != lookup_name: + self.setIcon(QIcon.cached_icon('modified.png')) + elif in_library: + self.setIcon(self.category_icons.get(self._lookup_name) or QIcon.cached_icon('column.png')) else: self.setIcon(QIcon.cached_icon('dialog_error.png')) + if self._original_in_library: + self.setFlags(self.flags() & ~Qt.ItemFlag.ItemIsEditable) + else: + self._is_editable = True self._txt = txt - self.setFlags(self.flags() & ~Qt.ItemFlag.ItemIsEditable) @property def is_deleted(self): @@ -83,28 +100,59 @@ class CategoryTableWidgetItem(QTableWidgetItem): def lookup_name(self): return self._lookup_name + @property + def original_lookup_name(self): + return self._original_lookup_name + def undo(self): self.is_deleted = False + @property + def is_editable(self): + return self._is_editable + + @property + def is_modified(self): + # Don't allow undo if the user selects a new column lookup key + return False + + @property + def category_icons(self): + return self._category_icons + + +class CategoryTableItemDelegate(QStyledItemDelegate): + + def __init__(self, parent, table, changed_signal): + super().__init__(parent) + self._table = table + self._parent = parent + self._changed_signal = changed_signal + + def createEditor(self, parent, option, index): + editor = DelegateCB(parent) + items = sorted(self._parent.all_values.keys(), key=sort_key) + for text in items: + editor.addItem(text) + return editor + + def setModelData(self, editor, model, index): + val = editor.currentText() # We know val is a valid lookup name + item = self._table.item(index.row(), index.column()) + item.setText(val) + self._changed_signal.emit() + class ValueTableWidgetItem(QTableWidgetItem): def __init__(self, txt, table, all_values): self._original_text = txt self._table = table - self._all_values = all_values self._is_template = is_template = txt == TEMPLATE_ICON_INDICATOR self._is_modified = False - self._is_editable = False + self._all_values = all_values super().__init__(('{' + _('template') + '}') if is_template else txt) - if not is_template and txt not in all_values: - icon = 'dialog_error.png' - self.setToolTip(_("The value {} doesn't exist in the library").format(txt)) - self._is_editable = True - else: - icon = 'debug.png' if is_template else 'icon_choose.png' - self.setFlags(self.flags() & ~Qt.ItemFlag.ItemIsEditable) - self.setIcon(QIcon.cached_icon(icon)) + self.set_icon(txt) @property def original_text(self): @@ -129,12 +177,21 @@ class ValueTableWidgetItem(QTableWidgetItem): @property def is_editable(self): - return self._is_editable + return not self._is_template + + def set_icon(self, txt): + if not self._is_template and txt not in self._all_values: + icon = 'dialog_error.png' + self.setToolTip(_("The value {} doesn't exist in the library").format(txt)) + else: + icon = 'debug.png' if self._is_template else 'icon_choose.png' + self.setToolTip(txt) + self.setIcon(QIcon.cached_icon(icon)) def undo(self): self.is_modified = False self.setText(self._original_text) - self.setIcon(QIcon.cached_icon('dialog_error.png')) + self.set_icon(self._original_text) class ValueTableItemDelegate(QStyledItemDelegate): @@ -165,6 +222,7 @@ class ValueTableItemDelegate(QStyledItemDelegate): val = editor.currentText() item = self._table.item(index.row(), index.column()) item.setText(val) + item.setToolTip(val) item.is_modified = True self._changed_signal.emit() @@ -429,16 +487,20 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): pass def lazy_initialize(self): - self.rules_table.setItemDelegateForColumn(ICON_COLUMN, IconColumnDelegate(self, self.rules_table, self.changed_signal)) + self.rules_table.setItemDelegateForColumn(CATEGORY_COLUMN, + CategoryTableItemDelegate(self, self.rules_table, self.changed_signal)) + self.rules_table.setItemDelegateForColumn(ICON_COLUMN, + IconColumnDelegate(self, self.rules_table, self.changed_signal)) self.rules_table.setItemDelegateForColumn(FOR_CHILDREN_COLUMN, - ChildrenColumnDelegate(self, self.rules_table, self.changed_signal)) + ChildrenColumnDelegate(self, self.rules_table, self.changed_signal)) self.rules_table.setItemDelegateForColumn(VALUE_COLUMN, - ValueTableItemDelegate(self, self.rules_table, self.changed_signal)) + ValueTableItemDelegate(self, self.rules_table, self.changed_signal)) self.populate_content() self.section_order = [0, 1, 1, 0, 0, 0, 0] self.last_section_sorted = 0 self.do_sort(VALUE_COLUMN) self.do_sort(CATEGORY_COLUMN) + self.changed_signal.connect(self.something_changed) def populate_content(self): field_metadata = self.gui.current_db.field_metadata @@ -451,26 +513,21 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): row = 0 t = self.rules_table t.clearContents() - all_values = {} - all_field_keys = field_metadata.all_field_keys() + cats = self.gui.current_db.new_api.get_categories() + self.all_values = all_values = {cat: set(t.name for t in cats[cat]) for cat in cats.keys()} for category,vdict in v.items(): if category in field_metadata: - display_name = field_metadata[category]['name'] - try: - all_values[category] = set(self.gui.current_db.new_api.all_field_names(category)) - except ValueError: + if category not in all_values: all_values[category] = set() if is_hierarchical_category(category): - for value in all_values: + for value in set(all_values[category]): idx = 0 - while idx >= 0: - all_values[category].add(value) - idx = value.rfind('.') + while (idx := value.rfind('.')) >= 0: value = value[:idx] + all_values[category].add(value) elif only_current_library: continue else: - display_name = category.removeprefix('#') all_values[category] = set() self.all_values = all_values @@ -483,7 +540,7 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): d = v[category][item_value] t.setItem(row, DELETED_COLUMN, StateTableWidgetItem('')) t.setItem(row, CATEGORY_COLUMN, - CategoryTableWidgetItem(category, category_icons, display_name, field_metadata, t)) + CategoryTableWidgetItem(category, category_icons, field_metadata, t)) t.setItem(row, ICON_MODIFIED_COLUMN, StateTableWidgetItem('')) t.setItem(row, VALUE_COLUMN, ValueTableWidgetItem(item_value, t, all_values[category])) t.setItem(row, ICON_COLUMN, IconFileTableWidgetItem(d[0], item_value, t)) @@ -492,6 +549,9 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): t.setItem(row, FOR_CHILDREN_COLUMN, item) row += 1 + def something_changed(self): + self.show_only_current_library.setEnabled(False) + def show_context_menu(self, point): item = self.rules_table.itemAt(point) if item is None: @@ -501,6 +561,9 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): return m = QMenu(self) if column == CATEGORY_COLUMN: + if item.is_editable: + ac = m.addAction(_('Modify this value'), partial(self.context_menu_handler, 'modify', item)) + m.addSeparator() ac = m.addAction(_('Delete this rule'), partial(self.context_menu_handler, 'delete', item)) ac.setEnabled(not item.is_deleted) ac = m.addAction(_('Undo delete'), partial(self.context_menu_handler, 'undo_delete', item)) @@ -549,7 +612,7 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): self.delete_button.setEnabled(column == CATEGORY_COLUMN) if column == CATEGORY_COLUMN and item.is_deleted: self.undo_button.setEnabled(True) - if column in (VALUE_COLUMN, ICON_COLUMN, FOR_CHILDREN_COLUMN): + if column in (CATEGORY_COLUMN, VALUE_COLUMN, ICON_COLUMN, FOR_CHILDREN_COLUMN): if item.is_modified: self.undo_button.setEnabled(True) if item.is_editable: @@ -575,7 +638,7 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): idx = self.rules_table.currentIndex() if idx.isValid(): column = idx.column() - if column in (VALUE_COLUMN, ICON_COLUMN, FOR_CHILDREN_COLUMN): + if column in (CATEGORY_COLUMN, VALUE_COLUMN, ICON_COLUMN, FOR_CHILDREN_COLUMN): self.rules_table.edit(idx) self.check_button_state(None) # Here to make buttons enabled/disabled @@ -655,6 +718,9 @@ class TbIconRulesTab(LazyConfigWidgetBase, Ui_Form): pass v[cat_item.lookup_name].pop(value_text, None) continue + if cat_item.original_lookup_name != cat_item.lookup_name: + v[cat_item.lookup_name] = v[cat_item.original_lookup_name] + v.pop(cat_item.original_lookup_name, None) d = list(v[cat_item.lookup_name][value_text]) diff --git a/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.ui b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.ui index c8afcc81fe..4081ffa2ca 100644 --- a/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.ui +++ b/src/calibre/gui2/preferences/look_feel_tabs/tb_icon_rules.ui @@ -45,6 +45,10 @@ or the context menu. The value icon rules are defined per-user, not per-library. Show only categories and values available in the current library + + This check box cannot be changed after any other changes have been made. +Otherwise all edits would be lost. +