From f2c1274824cc74631c64748ebb1665174b7254d6 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 7 Dec 2024 12:11:54 +0000 Subject: [PATCH] Enhancement #2091139: Reordering Columns in Custom Column Editor --- src/calibre/gui2/preferences/columns.py | 94 +++++++++++++++++-------- 1 file changed, 66 insertions(+), 28 deletions(-) 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: