Fixes #2091139 [Request for Feature: Reordering Columns in Custom Column Editor](https://bugs.launchpad.net/calibre/+bug/2091139)
Fixes #2091189 [Enumerated columns: Item count in permissible values dialog](https://bugs.launchpad.net/calibre/+bug/2091189)
This commit is contained in:
Kovid Goyal 2024-12-07 19:39:52 +05:30
commit 14e1025a71
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 114 additions and 32 deletions

View File

@ -18,6 +18,19 @@ from qt.core import (
) )
from calibre.gui2 import error_dialog, gprefs from calibre.gui2 import error_dialog, gprefs
from calibre.utils.localization import ngettext
class CountTableWidgetItem(QTableWidgetItem):
def __init__(self, count):
QTableWidgetItem.__init__(self, str(count) if count is not None else '0')
self.setTextAlignment(Qt.AlignmentFlag.AlignRight|Qt.AlignmentFlag.AlignVCenter)
self.setFlags(self.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
self._count = count
def set_count(self, count):
self.setText(str(count) if count is not None else '0')
class EnumValuesEdit(QDialog): class EnumValuesEdit(QDialog):
@ -33,7 +46,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')) self.del_button.setToolTip(_('Remove the currently selected value. Only '
'values with a count of zero can be removed'))
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'))
@ -58,12 +72,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(2) t.setColumnCount(3)
t.setRowCount(1) t.setRowCount(1)
t.setHorizontalHeaderLabels([_('Value'), _('Color')]) t.setHorizontalHeaderLabels([_('Value'), _('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)
self.name_to_count = {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]
permitted_values = fm.get('display', {}).get('enum_values', '') permitted_values = fm.get('display', {}).get('enum_values', '')
@ -76,6 +93,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_count_item(i, v)
t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
@ -86,11 +104,19 @@ class EnumValuesEdit(QDialog):
bb.rejected.connect(self.reject) bb.rejected.connect(self.reject)
l.addWidget(bb, 1, 0, 1, 2) l.addWidget(bb, 1, 0, 1, 2)
self.table.cellChanged.connect(self.cell_changed)
self.ins_button.clicked.connect(self.ins_button_clicked) self.ins_button.clicked.connect(self.ins_button_clicked)
self.move_down_button.clicked.connect(self.move_down_clicked) self.move_down_button.clicked.connect(self.move_down_clicked)
self.move_up_button.clicked.connect(self.move_up_clicked) self.move_up_button.clicked.connect(self.move_up_clicked)
self.restore_geometry(gprefs, 'enum-values-edit-geometry') self.restore_geometry(gprefs, 'enum-values-edit-geometry')
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())
item.set_count(count)
def sizeHint(self): def sizeHint(self):
sz = QDialog.sizeHint(self) sz = QDialog.sizeHint(self)
sz.setWidth(max(sz.width(), 600)) sz.setWidth(max(sz.width(), 600))
@ -115,6 +141,10 @@ class EnumValuesEdit(QDialog):
c.setCurrentIndex(dex) c.setCurrentIndex(dex)
return c return c
def make_count_item(self, row, txt):
it = CountTableWidgetItem(self.name_to_count.get(txt))
self.table.setItem(row, 2, it)
def move_up_clicked(self): def move_up_clicked(self):
row = self.table.currentRow() row = self.table.currentRow()
if row < 0: if row < 0:
@ -128,11 +158,13 @@ class EnumValuesEdit(QDialog):
def move_row(self, row, direction): def move_row(self, row, direction):
t = self.table.takeItem(row, 0) t = self.table.takeItem(row, 0)
c = self.table.cellWidget(row, 1).currentIndex() c = self.table.cellWidget(row, 1).currentIndex()
count = self.table.takeItem(row, 2)
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, 0, t)
self.make_color_combobox(row, c) self.make_color_combobox(row, c)
self.table.setItem(row, 2, count)
self.table.setCurrentCell(row, 0) self.table.setCurrentCell(row, 0)
def move_down_clicked(self): def move_down_clicked(self):
@ -146,7 +178,18 @@ class EnumValuesEdit(QDialog):
self.move_row(row, 1) self.move_row(row, 1)
def del_line(self): def del_line(self):
if self.table.currentRow() >= 0: row = self.table.currentRow()
if row >= 0:
txt = self.table.item(row, 0).text()
count = self.name_to_count.get(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
self.table.removeRow(self.table.currentRow()) self.table.removeRow(self.table.currentRow())
def ins_button_clicked(self): def ins_button_clicked(self):
@ -158,6 +201,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_count_item(row, '')
def save_geometry(self): def save_geometry(self):
super().save_geometry(gprefs, 'enum-values-edit-geometry') super().save_geometry(gprefs, 'enum-values-edit-geometry')

View File

@ -19,6 +19,16 @@ from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
ORDER_COLUMN = 0
HEADER_COLUMN = 1
KEY_COLUMN = 2
TYPE_COLUMN = 3
DESCRIPTION_COLUMN = 4
STATUS_COLUMN = 5
column_headings = (_('Order'), _('Column header'), _('Lookup name'),
_('Type'), _('Description'), _('Status'))
restart_critical = True restart_critical = True
def genesis(self, gui): def genesis(self, gui):
@ -46,6 +56,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
signal.connect(self.columns_changed) signal.connect(self.columns_changed)
self.show_all_button.clicked.connect(self.show_all) self.show_all_button.clicked.connect(self.show_all)
self.hide_all_button.clicked.connect(self.hide_all) self.hide_all_button.clicked.connect(self.hide_all)
self.column_positions = None
def initialize(self): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)
@ -57,10 +68,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit() self.changed_signal.emit()
def commit(self): def commit(self):
widths = []
for i in range(0, self.opt_columns.columnCount()):
widths.append(self.opt_columns.columnWidth(i))
gprefs.set('custcol-prefs-table-geometry', widths)
rr = ConfigWidgetBase.commit(self) rr = ConfigWidgetBase.commit(self)
return self.apply_custom_column_changes() or rr return self.apply_custom_column_changes() or rr
@ -79,13 +86,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.field_metadata = db.field_metadata self.field_metadata = db.field_metadata
self.opt_columns.setColumnCount(6) self.opt_columns.setColumnCount(6)
self.opt_columns.setHorizontalHeaderItem(0, QTableWidgetItem(_('Order'))) # Set up the columns in logical index order
self.opt_columns.setHorizontalHeaderItem(1, QTableWidgetItem(_('Column header'))) for p in range(0, len(self.column_headings)):
self.opt_columns.setHorizontalHeaderItem(2, QTableWidgetItem(_('Lookup name'))) self.opt_columns.setHorizontalHeaderItem(p, QTableWidgetItem(self.column_headings[p]))
self.opt_columns.setHorizontalHeaderItem(3, QTableWidgetItem(_('Type')))
self.opt_columns.setHorizontalHeaderItem(4, QTableWidgetItem(_('Description'))) # Now reorder the columns into the desired visual order. Note: ignore
self.opt_columns.setHorizontalHeaderItem(5, QTableWidgetItem(_('Status'))) # visual order when looking at items. Qt automatically maps the visual
self.opt_columns.horizontalHeader().sectionClicked.connect(self.table_sorted) # order onto the logical order.
self.column_positions = gprefs.get('custcol-prefs-column_order', [0, 1, 2, 3, 4, 5])
header = self.opt_columns.horizontalHeader()
for dvi,li in enumerate(self.column_positions):
cvi = header.visualIndex(li)
if cvi != dvi:
header.moveSection(cvi, dvi)
header.sectionClicked.connect(self.table_sorted)
header.setSectionsMovable(True)
header.setFirstSectionMovable(False)
header.sectionMoved.connect(self.header_moved)
header.sectionResized.connect(self.save_geometry)
self.opt_columns.verticalHeader().hide() self.opt_columns.verticalHeader().hide()
self.opt_columns.setRowCount(len(colmap)) self.opt_columns.setRowCount(len(colmap))
@ -104,6 +122,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.set_up_down_enabled(self.opt_columns.currentItem(), None) self.set_up_down_enabled(self.opt_columns.currentItem(), None)
self.opt_columns.blockSignals(False) self.opt_columns.blockSignals(False)
def header_moved(self, log_index, old_v_index, new_v_index):
self.column_positions = []
for vi in range(0, self.opt_columns.columnCount()):
self.column_positions.append(self.opt_columns.horizontalHeader().logicalIndex(vi))
self.save_geometry()
def set_up_down_enabled(self, current_item, _): def set_up_down_enabled(self, current_item, _):
h = self.opt_columns.horizontalHeader() h = self.opt_columns.horizontalHeader()
row = current_item.row() row = current_item.row()
@ -130,7 +154,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def row_double_clicked(self, r, c): def row_double_clicked(self, r, c):
self.edit_custcol() self.edit_custcol()
def save_geometry(self):
# Save both the column widths and the column order
widths = []
for i in range(0, self.opt_columns.columnCount()):
widths.append(self.opt_columns.columnWidth(i))
gprefs.set('custcol-prefs-table-geometry', widths)
gprefs.set('custcol-prefs-column_order', self.column_positions)
def restore_geometry(self): def restore_geometry(self):
# restore the column widths. Order is done when the table is created.
geom = gprefs.get('custcol-prefs-table-geometry', None) geom = gprefs.get('custcol-prefs-table-geometry', None)
if geom is not None and len(geom) == self.opt_columns.columnCount(): if geom is not None and len(geom) == self.opt_columns.columnCount():
with suppress(Exception): with suppress(Exception):
@ -141,14 +174,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def hide_all(self): def hide_all(self):
for row in range(self.opt_columns.rowCount()): for row in range(self.opt_columns.rowCount()):
item = self.opt_columns.item(row, 0) item = self.opt_columns.item(row, self.ORDER_COLUMN)
if item.checkState() != Qt.CheckState.PartiallyChecked: if item.checkState() != Qt.CheckState.PartiallyChecked:
item.setCheckState(Qt.CheckState.Unchecked) item.setCheckState(Qt.CheckState.Unchecked)
self.changed_signal.emit() self.changed_signal.emit()
def show_all(self): def show_all(self):
for row in range(self.opt_columns.rowCount()): for row in range(self.opt_columns.rowCount()):
item = self.opt_columns.item(row, 0) item = self.opt_columns.item(row, self.ORDER_COLUMN)
if item.checkState() != Qt.CheckState.PartiallyChecked: if item.checkState() != Qt.CheckState.PartiallyChecked:
item.setCheckState(Qt.CheckState.Checked) item.setCheckState(Qt.CheckState.Checked)
self.changed_signal.emit() self.changed_signal.emit()
@ -169,7 +202,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
item.setToolTip(str(order)) item.setToolTip(str(order))
item.setData(Qt.ItemDataRole.UserRole, key) item.setData(Qt.ItemDataRole.UserRole, key)
item.setFlags(flags) item.setFlags(flags)
self.opt_columns.setItem(row, 0, item) self.opt_columns.setItem(row, self.ORDER_COLUMN, item)
flags |= Qt.ItemFlag.ItemIsUserCheckable flags |= Qt.ItemFlag.ItemIsUserCheckable
if key == 'ondevice': if key == 'ondevice':
@ -182,17 +215,20 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
else: else:
item.setCheckState(force_checked_to) item.setCheckState(force_checked_to)
# The columns are added in logical index order, not visual index order,
# so we can process them without a loop
item = QTableWidgetItem(cc['name']) item = QTableWidgetItem(cc['name'])
item.setToolTip(cc['name']) item.setToolTip(cc['name'])
item.setFlags(flags) item.setFlags(flags)
if self.is_custom_key(key): if self.is_custom_key(key):
item.setData(Qt.ItemDataRole.DecorationRole, (QIcon.ic('column.png'))) item.setData(Qt.ItemDataRole.DecorationRole, (QIcon.ic('column.png')))
self.opt_columns.setItem(row, 1, item) self.opt_columns.setItem(row, self.HEADER_COLUMN, item)
item = QTableWidgetItem(key) item = QTableWidgetItem(key)
item.setToolTip(key) item.setToolTip(key)
item.setFlags(flags) item.setFlags(flags)
self.opt_columns.setItem(row, 2, item) self.opt_columns.setItem(row, self.KEY_COLUMN, item)
if key == 'title': if key == 'title':
coltype = _('Text') coltype = _('Text')
@ -210,13 +246,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
item = QTableWidgetItem(coltype) item = QTableWidgetItem(coltype)
item.setToolTip(coltype) item.setToolTip(coltype)
item.setFlags(flags) item.setFlags(flags)
self.opt_columns.setItem(row, 3, item) self.opt_columns.setItem(row, self.TYPE_COLUMN, item)
desc = cc['display'].get('description', "") desc = cc['display'].get('description', "")
item = QTableWidgetItem(desc) item = QTableWidgetItem(desc)
item.setToolTip(desc) item.setToolTip(desc)
item.setFlags(flags) item.setFlags(flags)
self.opt_columns.setItem(row, 4, item) self.opt_columns.setItem(row, self.DESCRIPTION_COLUMN, item)
if '*deleted' in cc: if '*deleted' in cc:
col_status = _('Deleted column. Double-click to undelete it') col_status = _('Deleted column. Double-click to undelete it')
@ -231,13 +267,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
item = QTableWidgetItem(col_status) item = QTableWidgetItem(col_status)
item.setToolTip(col_status) item.setToolTip(col_status)
item.setFlags(flags) item.setFlags(flags)
self.opt_columns.setItem(row, 5, item) self.opt_columns.setItem(row, self.STATUS_COLUMN, item)
self.opt_columns.setSortingEnabled(True) self.opt_columns.setSortingEnabled(True)
def recreate_row(self, row): def recreate_row(self, row):
checked = self.opt_columns.item(row, 0).checkState() checked = self.opt_columns.item(row, self.ORDER_COLUMN).checkState()
title = self.opt_columns.item(row, 2).text() # Again, use the logical index, not the visual index
self.setup_row(row, title, row, force_checked_to=checked) key = self.opt_columns.item(row, self.KEY_COLUMN).text()
self.setup_row(row, key, row, force_checked_to=checked)
def get_move_count(self): def get_move_count(self):
mods = QApplication.keyboardModifiers() mods = QApplication.keyboardModifiers()
@ -297,7 +335,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if row < 0: if row < 0:
return error_dialog(self, '', _('You must select a column to delete it'), return error_dialog(self, '', _('You must select a column to delete it'),
show=True) show=True)
key = str(self.opt_columns.item(row, 0).data(Qt.ItemDataRole.UserRole) or '') key = str(self.opt_columns.item(row, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole) or '')
if key not in self.custcols: if key not in self.custcols:
return error_dialog(self, '', return error_dialog(self, '',
_('The selected column is not a custom column'), show=True) _('The selected column is not a custom column'), show=True)
@ -340,13 +378,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
return key.startswith('#') return key.startswith('#')
def column_order_val(self, row): def column_order_val(self, row):
return int(self.opt_columns.item(row, 0).text()) return int(self.opt_columns.item(row, self.ORDER_COLUMN).text())
def edit_custcol(self): def edit_custcol(self):
model = self.gui.library_view.model() model = self.gui.library_view.model()
row = self.opt_columns.currentRow() row = self.opt_columns.currentRow()
try: try:
key = str(self.opt_columns.item(row, 0).data(Qt.ItemDataRole.UserRole)) key = str(self.opt_columns.item(row, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole))
if key not in self.custcols: if key not in self.custcols:
return error_dialog(self, '', return error_dialog(self, '',
_('The selected column is not a user-defined column'), _('The selected column is not a user-defined column'),
@ -382,14 +420,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
model = self.gui.library_view.model() model = self.gui.library_view.model()
db = model.db db = model.db
self.opt_columns.sortItems(0, Qt.SortOrder.AscendingOrder) self.opt_columns.sortItems(0, Qt.SortOrder.AscendingOrder)
config_cols = [str(self.opt_columns.item(i, 0).data(Qt.ItemDataRole.UserRole) or '') config_cols = [str(self.opt_columns.item(i, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole) or '')
for i in range(self.opt_columns.rowCount())] for i in range(self.opt_columns.rowCount())]
if not config_cols: if not config_cols:
config_cols = ['title'] config_cols = ['title']
removed_cols = set(model.column_map) - set(config_cols) removed_cols = set(model.column_map) - set(config_cols)
hidden_cols = {str(self.opt_columns.item(i, 0).data(Qt.ItemDataRole.UserRole) or '') hidden_cols = {str(self.opt_columns.item(i, self.ORDER_COLUMN).data(Qt.ItemDataRole.UserRole) or '')
for i in range(self.opt_columns.rowCount()) for i in range(self.opt_columns.rowCount())
if self.opt_columns.item(i, 0).checkState()==Qt.CheckState.Unchecked} if self.opt_columns.item(i, self.ORDER_COLUMN).checkState()==Qt.CheckState.Unchecked}
hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols
hidden_cols = list(hidden_cols.intersection(set(model.column_map))) hidden_cols = list(hidden_cols.intersection(set(model.column_map)))
if 'ondevice' in hidden_cols: if 'ondevice' in hidden_cols: