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.
This commit is contained in:
Charles Haley 2024-12-08 13:12:29 +00:00
parent 5038fb5994
commit 9acf9aed24

View File

@ -17,7 +17,8 @@ from qt.core import (
QVBoxLayout, 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 from calibre.utils.localization import ngettext
@ -35,6 +36,11 @@ class CountTableWidgetItem(QTableWidgetItem):
class EnumValuesEdit(QDialog): class EnumValuesEdit(QDialog):
VALUE_COLUMN = 0
WAS_COLUMN = 1
COLOR_COLUMN = 2
COUNT_COLUMN = 3
def __init__(self, parent, db, key): def __init__(self, parent, db, key):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
@ -46,8 +52,8 @@ class EnumValuesEdit(QDialog):
bbox.addStretch(10) bbox.addStretch(10)
self.del_button = QToolButton() self.del_button = QToolButton()
self.del_button.setIcon(QIcon.ic('trash.png')) self.del_button.setIcon(QIcon.ic('trash.png'))
self.del_button.setToolTip(_('Remove the currently selected value. Only ' self.del_button.setToolTip(_('Remove the currently selected value. The '
'values with a count of zero can be removed')) 'value will be removed from all books.'))
self.ins_button = QToolButton() self.ins_button = QToolButton()
self.ins_button.setIcon(QIcon.ic('plus.png')) self.ins_button.setIcon(QIcon.ic('plus.png'))
self.ins_button.setToolTip(_('Add a new permissible value')) self.ins_button.setToolTip(_('Add a new permissible value'))
@ -65,6 +71,7 @@ class EnumValuesEdit(QDialog):
bbox.addStretch(10) bbox.addStretch(10)
l.addItem(bbox, 0, 0) l.addItem(bbox, 0, 0)
self.deleted_values = {}
self.del_button.clicked.connect(self.del_line) self.del_button.clicked.connect(self.del_line)
self.all_colors = {str(s) for s in list(QColor.colorNames())} self.all_colors = {str(s) for s in list(QColor.colorNames())}
@ -72,14 +79,15 @@ class EnumValuesEdit(QDialog):
tl = QVBoxLayout() tl = QVBoxLayout()
l.addItem(tl, 0, 1) l.addItem(tl, 0, 1)
self.table = t = QTableWidget(parent) self.table = t = QTableWidget(parent)
t.setColumnCount(3) t.setColumnCount(4)
t.setRowCount(1) t.setRowCount(1)
t.setHorizontalHeaderLabels([_('Value'), _('Color'), _('Count')]) t.setHorizontalHeaderLabels([_('Value'), _('Was'), _('Color'), _('Count')])
t.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) t.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
tl.addWidget(t) tl.addWidget(t)
counts = self.db.new_api.get_usage_count_by_id(key) 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.key = key
self.fm = fm = db.field_metadata[key] self.fm = fm = db.field_metadata[key]
@ -93,6 +101,7 @@ class EnumValuesEdit(QDialog):
c.setCurrentIndex(c.findText(colors[i])) c.setCurrentIndex(c.findText(colors[i]))
else: else:
c.setCurrentIndex(0) c.setCurrentIndex(0)
self.make_was_item(i)
self.make_count_item(i, v) self.make_count_item(i, v)
t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
@ -112,10 +121,21 @@ class EnumValuesEdit(QDialog):
def cell_changed(self, row, col): def cell_changed(self, row, col):
if col == 0: if col == 0:
item = self.table.item(row, 2) val_item = self.table.item(row, self.VALUE_COLUMN)
if item is not None and self.table.item(row, 0) is not None: if val_item is None:
count = self.name_to_count.get(self.table.item(row, 0).text()) 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) 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): def sizeHint(self):
sz = QDialog.sizeHint(self) sz = QDialog.sizeHint(self)
@ -126,9 +146,8 @@ class EnumValuesEdit(QDialog):
def make_name_item(self, row, txt): def make_name_item(self, row, txt):
it = QTableWidgetItem(txt) it = QTableWidgetItem(txt)
it.setData(Qt.ItemDataRole.UserRole, txt) it.setData(Qt.ItemDataRole.UserRole, txt)
it.setCheckState(Qt.CheckState.Unchecked) it.setToolTip(_('Changing the value will rename it in all books'))
it.setToolTip('<p>' + _('Check the box if you change the value and want it renamed in books where it is used') + '</p>') self.table.setItem(row, self.VALUE_COLUMN, it)
self.table.setItem(row, 0, it)
def make_color_combobox(self, row, dex): def make_color_combobox(self, row, dex):
c = QComboBox(self) c = QComboBox(self)
@ -136,14 +155,18 @@ class EnumValuesEdit(QDialog):
c.addItems(QColor.colorNames()) c.addItems(QColor.colorNames())
c.setToolTip('<p>' + _('Selects the color of the text when displayed in the book list. ' c.setToolTip('<p>' + _('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') + '</p>') 'Either all rows must have a color or no rows have a color') + '</p>')
self.table.setCellWidget(row, 1, c) self.table.setCellWidget(row, self.COLOR_COLUMN, c)
if dex >= 0: if dex >= 0:
c.setCurrentIndex(dex) c.setCurrentIndex(dex)
return c 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): def make_count_item(self, row, txt):
it = CountTableWidgetItem(self.name_to_count.get(txt)) it = CountTableWidgetItem(self.name_to_count.get(lower(txt)))
self.table.setItem(row, 2, it) self.table.setItem(row, self.COUNT_COLUMN, it)
def move_up_clicked(self): def move_up_clicked(self):
row = self.table.currentRow() row = self.table.currentRow()
@ -156,15 +179,17 @@ class EnumValuesEdit(QDialog):
self.move_row(row, -1) self.move_row(row, -1)
def move_row(self, row, direction): def move_row(self, row, direction):
t = self.table.takeItem(row, 0) t = self.table.takeItem(row, self.VALUE_COLUMN)
c = self.table.cellWidget(row, 1).currentIndex() c = self.table.cellWidget(row, self.COLOR_COLUMN).currentIndex()
count = self.table.takeItem(row, 2) was = self.table.takeItem(row, self.WAS_COLUMN)
count = self.table.takeItem(row, self.COUNT_COLUMN)
self.table.removeRow(row) self.table.removeRow(row)
row += direction row += direction
self.table.insertRow(row) 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.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) self.table.setCurrentCell(row, 0)
def move_down_clicked(self): def move_down_clicked(self):
@ -180,16 +205,17 @@ class EnumValuesEdit(QDialog):
def del_line(self): def del_line(self):
row = self.table.currentRow() row = self.table.currentRow()
if row >= 0: if row >= 0:
txt = self.table.item(row, 0).text() txt = self.table.item(row, self.VALUE_COLUMN).text()
count = self.name_to_count.get(txt, 0) count = self.name_to_count.get(lower(txt), 0)
if count > 0: if count > 0:
error_dialog(self, r = question_dialog(self,
_('Cannot remove value "{}"').format(txt), _('Value "{}" is used').format(txt),
ngettext('The value "{0}" is used in {1} book and cannot be removed.', ngettext('The value "{0}" is used in {1} book. Do you really want to remove it?',
'The value "{0}" is used in {1} books and cannot be removed.', 'The value "{0}" is used in {1} books. Do you really want to remove it?',
count).format(txt, count), count).format(txt, count))
show=True) if r != QDialog.DialogCode.Accepted:
return return
self.deleted_values[lower(txt)] = txt
self.table.removeRow(self.table.currentRow()) self.table.removeRow(self.table.currentRow())
def ins_button_clicked(self): def ins_button_clicked(self):
@ -201,6 +227,7 @@ class EnumValuesEdit(QDialog):
self.table.insertRow(row) self.table.insertRow(row)
self.make_name_item(row, '') self.make_name_item(row, '')
self.make_color_combobox(row, -1) self.make_color_combobox(row, -1)
self.make_was_item(row)
self.make_count_item(row, '') self.make_count_item(row, '')
def save_geometry(self): def save_geometry(self):
@ -212,21 +239,20 @@ class EnumValuesEdit(QDialog):
colors = [] colors = []
id_map = {} id_map = {}
for i in range(0, self.table.rowCount()): 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()) v = str(it.text())
if not v: if not v:
error_dialog(self, _('Empty value'), error_dialog(self, _('Empty value'),
_('Empty values are not allowed'), show=True) _('Empty values are not allowed'), show=True)
return return
ov = str(it.data(Qt.ItemDataRole.UserRole)) 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) fid = self.db.new_api.get_item_id(self.key, ov)
id_map[fid] = v id_map[fid] = v
values.append(v) values.append(v)
c = str(self.table.cellWidget(i, 1).currentText()) c = str(self.table.cellWidget(i, self.COLOR_COLUMN).currentText())
if c: if c:
colors.append(c) colors.append(c)
l_lower = [v.lower() for v in values] l_lower = [v.lower() for v in values]
for i,v in enumerate(l_lower): for i,v in enumerate(l_lower):
if v in l_lower[i+1:]: 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) 'Either all values or no values must have colors'), show=True)
return 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_values'] = values
disp['enum_colors'] = colors disp['enum_colors'] = colors
self.db.set_custom_column_metadata(self.fm['colnum'], display=disp, self.db.set_custom_column_metadata(self.fm['colnum'], display=disp,