From 9acf9aed24f33eb6e277d297c0fc59f7251202c8 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 8 Dec 2024 13:12:29 +0000 Subject: [PATCH] Two improvements to the enum values editor 1) Rename values in books automatically instead of asking. This avoids leaving invalid values in book metadata. 2) Delete values from books when deleted from the permissible values list. This avoids requiring bouncing between this dialog and the manage dialog. 3) Indicate renames with a "Was" column. --- src/calibre/gui2/dialogs/enum_values_edit.py | 108 +++++++++++++------ 1 file changed, 75 insertions(+), 33 deletions(-) diff --git a/src/calibre/gui2/dialogs/enum_values_edit.py b/src/calibre/gui2/dialogs/enum_values_edit.py index b5ae615a29..503679f2ac 100644 --- a/src/calibre/gui2/dialogs/enum_values_edit.py +++ b/src/calibre/gui2/dialogs/enum_values_edit.py @@ -17,7 +17,8 @@ from qt.core import ( QVBoxLayout, ) -from calibre.gui2 import error_dialog, gprefs +from calibre.gui2 import error_dialog, gprefs, question_dialog +from calibre.utils.icu import lower from calibre.utils.localization import ngettext @@ -35,6 +36,11 @@ class CountTableWidgetItem(QTableWidgetItem): class EnumValuesEdit(QDialog): + VALUE_COLUMN = 0 + WAS_COLUMN = 1 + COLOR_COLUMN = 2 + COUNT_COLUMN = 3 + def __init__(self, parent, db, key): QDialog.__init__(self, parent) @@ -46,8 +52,8 @@ class EnumValuesEdit(QDialog): bbox.addStretch(10) self.del_button = QToolButton() self.del_button.setIcon(QIcon.ic('trash.png')) - self.del_button.setToolTip(_('Remove the currently selected value. Only ' - 'values with a count of zero can be removed')) + self.del_button.setToolTip(_('Remove the currently selected value. The ' + 'value will be removed from all books.')) self.ins_button = QToolButton() self.ins_button.setIcon(QIcon.ic('plus.png')) self.ins_button.setToolTip(_('Add a new permissible value')) @@ -65,6 +71,7 @@ class EnumValuesEdit(QDialog): bbox.addStretch(10) l.addItem(bbox, 0, 0) + self.deleted_values = {} self.del_button.clicked.connect(self.del_line) self.all_colors = {str(s) for s in list(QColor.colorNames())} @@ -72,14 +79,15 @@ class EnumValuesEdit(QDialog): tl = QVBoxLayout() l.addItem(tl, 0, 1) self.table = t = QTableWidget(parent) - t.setColumnCount(3) + t.setColumnCount(4) t.setRowCount(1) - t.setHorizontalHeaderLabels([_('Value'), _('Color'), _('Count')]) + t.setHorizontalHeaderLabels([_('Value'), _('Was'), _('Color'), _('Count')]) t.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) tl.addWidget(t) counts = self.db.new_api.get_usage_count_by_id(key) - self.name_to_count = {self.db.new_api.get_item_name(key, item_id):count for item_id,count in counts.items()} + self.name_to_count = {lower(self.db.new_api.get_item_name(key, item_id)):count + for item_id,count in counts.items()} self.key = key self.fm = fm = db.field_metadata[key] @@ -93,6 +101,7 @@ class EnumValuesEdit(QDialog): c.setCurrentIndex(c.findText(colors[i])) else: c.setCurrentIndex(0) + self.make_was_item(i) self.make_count_item(i, v) t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) @@ -112,10 +121,21 @@ class EnumValuesEdit(QDialog): def cell_changed(self, row, col): if col == 0: - item = self.table.item(row, 2) - if item is not None and self.table.item(row, 0) is not None: - count = self.name_to_count.get(self.table.item(row, 0).text()) + val_item = self.table.item(row, self.VALUE_COLUMN) + if val_item is None: + return + item = self.table.item(row, self.COUNT_COLUMN) + if item is not None: + count = self.name_to_count.get(lower(self.table.item(row, self.VALUE_COLUMN).text())) item.set_count(count) + txt = val_item.text() + orig_txt = str(val_item.data(Qt.ItemDataRole.UserRole)) + was_item = self.table.item(row, self.WAS_COLUMN) + if was_item is not None: + if txt != orig_txt: + was_item.setText(orig_txt) + else: + was_item.setText('') def sizeHint(self): sz = QDialog.sizeHint(self) @@ -126,9 +146,8 @@ class EnumValuesEdit(QDialog): def make_name_item(self, row, txt): it = QTableWidgetItem(txt) it.setData(Qt.ItemDataRole.UserRole, txt) - it.setCheckState(Qt.CheckState.Unchecked) - it.setToolTip('

' + _('Check the box if you change the value and want it renamed in books where it is used') + '

') - self.table.setItem(row, 0, it) + it.setToolTip(_('Changing the value will rename it in all books')) + self.table.setItem(row, self.VALUE_COLUMN, it) def make_color_combobox(self, row, dex): c = QComboBox(self) @@ -136,14 +155,18 @@ class EnumValuesEdit(QDialog): c.addItems(QColor.colorNames()) c.setToolTip('

' + _('Selects the color of the text when displayed in the book list. ' 'Either all rows must have a color or no rows have a color') + '

') - self.table.setCellWidget(row, 1, c) + self.table.setCellWidget(row, self.COLOR_COLUMN, c) if dex >= 0: c.setCurrentIndex(dex) return c + def make_was_item(self, row): + it = QTableWidgetItem('') + self.table.setItem(row, self.WAS_COLUMN, it) + def make_count_item(self, row, txt): - it = CountTableWidgetItem(self.name_to_count.get(txt)) - self.table.setItem(row, 2, it) + it = CountTableWidgetItem(self.name_to_count.get(lower(txt))) + self.table.setItem(row, self.COUNT_COLUMN, it) def move_up_clicked(self): row = self.table.currentRow() @@ -156,15 +179,17 @@ class EnumValuesEdit(QDialog): self.move_row(row, -1) def move_row(self, row, direction): - t = self.table.takeItem(row, 0) - c = self.table.cellWidget(row, 1).currentIndex() - count = self.table.takeItem(row, 2) + t = self.table.takeItem(row, self.VALUE_COLUMN) + c = self.table.cellWidget(row, self.COLOR_COLUMN).currentIndex() + was = self.table.takeItem(row, self.WAS_COLUMN) + count = self.table.takeItem(row, self.COUNT_COLUMN) self.table.removeRow(row) row += direction self.table.insertRow(row) - self.table.setItem(row, 0, t) + self.table.setItem(row, self.VALUE_COLUMN, t) self.make_color_combobox(row, c) - self.table.setItem(row, 2, count) + self.table.setItem(row, self.WAS_COLUMN, was) + self.table.setItem(row, self.COUNT_COLUMN, count) self.table.setCurrentCell(row, 0) def move_down_clicked(self): @@ -180,16 +205,17 @@ class EnumValuesEdit(QDialog): def del_line(self): row = self.table.currentRow() if row >= 0: - txt = self.table.item(row, 0).text() - count = self.name_to_count.get(txt, 0) + txt = self.table.item(row, self.VALUE_COLUMN).text() + count = self.name_to_count.get(lower(txt), 0) if count > 0: - error_dialog(self, - _('Cannot remove value "{}"').format(txt), - ngettext('The value "{0}" is used in {1} book and cannot be removed.', - 'The value "{0}" is used in {1} books and cannot be removed.', - count).format(txt, count), - show=True) - return + r = question_dialog(self, + _('Value "{}" is used').format(txt), + ngettext('The value "{0}" is used in {1} book. Do you really want to remove it?', + 'The value "{0}" is used in {1} books. Do you really want to remove it?', + count).format(txt, count)) + if r != QDialog.DialogCode.Accepted: + return + self.deleted_values[lower(txt)] = txt self.table.removeRow(self.table.currentRow()) def ins_button_clicked(self): @@ -201,6 +227,7 @@ class EnumValuesEdit(QDialog): self.table.insertRow(row) self.make_name_item(row, '') self.make_color_combobox(row, -1) + self.make_was_item(row) self.make_count_item(row, '') def save_geometry(self): @@ -212,21 +239,20 @@ class EnumValuesEdit(QDialog): colors = [] id_map = {} for i in range(0, self.table.rowCount()): - it = self.table.item(i, 0) + it = self.table.item(i, self.VALUE_COLUMN) v = str(it.text()) if not v: error_dialog(self, _('Empty value'), _('Empty values are not allowed'), show=True) return ov = str(it.data(Qt.ItemDataRole.UserRole)) - if v != ov and it.checkState() == Qt.CheckState.Checked: + if v != ov: fid = self.db.new_api.get_item_id(self.key, ov) id_map[fid] = v values.append(v) - c = str(self.table.cellWidget(i, 1).currentText()) + c = str(self.table.cellWidget(i, self.COLOR_COLUMN).currentText()) if c: colors.append(c) - l_lower = [v.lower() for v in values] for i,v in enumerate(l_lower): if v in l_lower[i+1:]: @@ -241,6 +267,22 @@ class EnumValuesEdit(QDialog): 'Either all values or no values must have colors'), show=True) return + # Process deleted values. It is possible that a value was deleted then + # added back, possibly with a different case. If the case is the same then + # don't delete it. If the case is different then add it to the rename dict. + for v in values: + dv = self.deleted_values.get(lower(v)) + if dv is None: + continue + self.deleted_values.pop(lower(v)) + if v != dv: + fid = self.db.new_api.get_item_id(self.key, dv) + id_map[fid] = v + + ids_to_delete = (self.db.new_api.get_item_id(self.key, v) for v in self.deleted_values.values()) + if ids_to_delete: + self.db.new_api.remove_items(self.key, ids_to_delete) + disp['enum_values'] = values disp['enum_colors'] = colors self.db.set_custom_column_metadata(self.fm['colnum'], display=disp,