diff --git a/src/calibre/gui2/dialogs/enum_values_edit.py b/src/calibre/gui2/dialogs/enum_values_edit.py index 3b87001cc6..b5ae615a29 100644 --- a/src/calibre/gui2/dialogs/enum_values_edit.py +++ b/src/calibre/gui2/dialogs/enum_values_edit.py @@ -18,6 +18,19 @@ from qt.core import ( ) 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): @@ -33,7 +46,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')) + 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.setIcon(QIcon.ic('plus.png')) self.ins_button.setToolTip(_('Add a new permissible value')) @@ -58,12 +72,15 @@ class EnumValuesEdit(QDialog): tl = QVBoxLayout() l.addItem(tl, 0, 1) self.table = t = QTableWidget(parent) - t.setColumnCount(2) + t.setColumnCount(3) t.setRowCount(1) - t.setHorizontalHeaderLabels([_('Value'), _('Color')]) + t.setHorizontalHeaderLabels([_('Value'), _('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.key = key self.fm = fm = db.field_metadata[key] permitted_values = fm.get('display', {}).get('enum_values', '') @@ -76,6 +93,7 @@ class EnumValuesEdit(QDialog): c.setCurrentIndex(c.findText(colors[i])) else: c.setCurrentIndex(0) + self.make_count_item(i, v) t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) @@ -86,11 +104,19 @@ class EnumValuesEdit(QDialog): bb.rejected.connect(self.reject) l.addWidget(bb, 1, 0, 1, 2) + self.table.cellChanged.connect(self.cell_changed) self.ins_button.clicked.connect(self.ins_button_clicked) self.move_down_button.clicked.connect(self.move_down_clicked) self.move_up_button.clicked.connect(self.move_up_clicked) 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): sz = QDialog.sizeHint(self) sz.setWidth(max(sz.width(), 600)) @@ -115,6 +141,10 @@ class EnumValuesEdit(QDialog): c.setCurrentIndex(dex) 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): row = self.table.currentRow() if row < 0: @@ -128,11 +158,13 @@ class EnumValuesEdit(QDialog): 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) self.table.removeRow(row) row += direction self.table.insertRow(row) self.table.setItem(row, 0, t) self.make_color_combobox(row, c) + self.table.setItem(row, 2, count) self.table.setCurrentCell(row, 0) def move_down_clicked(self): @@ -146,7 +178,18 @@ class EnumValuesEdit(QDialog): self.move_row(row, 1) 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()) def ins_button_clicked(self): @@ -158,6 +201,7 @@ class EnumValuesEdit(QDialog): self.table.insertRow(row) self.make_name_item(row, '') self.make_color_combobox(row, -1) + self.make_count_item(row, '') def save_geometry(self): super().save_geometry(gprefs, 'enum-values-edit-geometry') diff --git a/src/calibre/gui2/preferences/columns.py b/src/calibre/gui2/preferences/columns.py index f067911be6..aa4f661eda 100644 --- a/src/calibre/gui2/preferences/columns.py +++ b/src/calibre/gui2/preferences/columns.py @@ -19,6 +19,16 @@ from calibre.gui2.preferences.create_custom_column import CreateCustomColumn 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 def genesis(self, gui): @@ -46,6 +56,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): signal.connect(self.columns_changed) self.show_all_button.clicked.connect(self.show_all) self.hide_all_button.clicked.connect(self.hide_all) + self.column_positions = None def initialize(self): ConfigWidgetBase.initialize(self) @@ -57,10 +68,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.changed_signal.emit() 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) return self.apply_custom_column_changes() or rr @@ -79,13 +86,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.field_metadata = db.field_metadata self.opt_columns.setColumnCount(6) - self.opt_columns.setHorizontalHeaderItem(0, QTableWidgetItem(_('Order'))) - self.opt_columns.setHorizontalHeaderItem(1, QTableWidgetItem(_('Column header'))) - self.opt_columns.setHorizontalHeaderItem(2, QTableWidgetItem(_('Lookup name'))) - self.opt_columns.setHorizontalHeaderItem(3, QTableWidgetItem(_('Type'))) - self.opt_columns.setHorizontalHeaderItem(4, QTableWidgetItem(_('Description'))) - self.opt_columns.setHorizontalHeaderItem(5, QTableWidgetItem(_('Status'))) - self.opt_columns.horizontalHeader().sectionClicked.connect(self.table_sorted) + # Set up the columns in logical index order + for p in range(0, len(self.column_headings)): + self.opt_columns.setHorizontalHeaderItem(p, QTableWidgetItem(self.column_headings[p])) + + # Now reorder the columns into the desired visual order. Note: ignore + # visual order when looking at items. Qt automatically maps the visual + # 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.setRowCount(len(colmap)) @@ -104,6 +122,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.set_up_down_enabled(self.opt_columns.currentItem(), None) 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, _): h = self.opt_columns.horizontalHeader() row = current_item.row() @@ -130,7 +154,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def row_double_clicked(self, r, c): 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): + # restore the column widths. Order is done when the table is created. geom = gprefs.get('custcol-prefs-table-geometry', None) if geom is not None and len(geom) == self.opt_columns.columnCount(): with suppress(Exception): @@ -141,14 +174,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def hide_all(self): 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: item.setCheckState(Qt.CheckState.Unchecked) self.changed_signal.emit() def show_all(self): 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: item.setCheckState(Qt.CheckState.Checked) self.changed_signal.emit() @@ -169,7 +202,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): item.setToolTip(str(order)) item.setData(Qt.ItemDataRole.UserRole, key) item.setFlags(flags) - self.opt_columns.setItem(row, 0, item) + self.opt_columns.setItem(row, self.ORDER_COLUMN, item) flags |= Qt.ItemFlag.ItemIsUserCheckable if key == 'ondevice': @@ -182,17 +215,20 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): else: 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.setToolTip(cc['name']) item.setFlags(flags) if self.is_custom_key(key): 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.setToolTip(key) item.setFlags(flags) - self.opt_columns.setItem(row, 2, item) + self.opt_columns.setItem(row, self.KEY_COLUMN, item) if key == 'title': coltype = _('Text') @@ -210,13 +246,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): item = QTableWidgetItem(coltype) item.setToolTip(coltype) item.setFlags(flags) - self.opt_columns.setItem(row, 3, item) + self.opt_columns.setItem(row, self.TYPE_COLUMN, item) desc = cc['display'].get('description', "") item = QTableWidgetItem(desc) item.setToolTip(desc) item.setFlags(flags) - self.opt_columns.setItem(row, 4, item) + self.opt_columns.setItem(row, self.DESCRIPTION_COLUMN, item) if '*deleted' in cc: col_status = _('Deleted column. Double-click to undelete it') @@ -231,13 +267,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): item = QTableWidgetItem(col_status) item.setToolTip(col_status) item.setFlags(flags) - self.opt_columns.setItem(row, 5, item) + self.opt_columns.setItem(row, self.STATUS_COLUMN, item) + self.opt_columns.setSortingEnabled(True) def recreate_row(self, row): - checked = self.opt_columns.item(row, 0).checkState() - title = self.opt_columns.item(row, 2).text() - self.setup_row(row, title, row, force_checked_to=checked) + checked = self.opt_columns.item(row, self.ORDER_COLUMN).checkState() + # Again, use the logical index, not the visual index + 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): mods = QApplication.keyboardModifiers() @@ -297,7 +335,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): if row < 0: return error_dialog(self, '', _('You must select a column to delete it'), 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: return error_dialog(self, '', _('The selected column is not a custom column'), show=True) @@ -340,13 +378,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): return key.startswith('#') 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): model = self.gui.library_view.model() row = self.opt_columns.currentRow() 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: return error_dialog(self, '', _('The selected column is not a user-defined column'), @@ -382,14 +420,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): model = self.gui.library_view.model() db = model.db 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())] if not config_cols: config_cols = ['title'] 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()) - 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 = list(hidden_cols.intersection(set(model.column_map))) if 'ondevice' in hidden_cols: