From f6167d7f0e4e7f6d3572a001009d5346d1069f91 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 10 Feb 2025 07:52:16 +0530 Subject: [PATCH] Better fix for the Qt book list phantom edits issue The phantom edits are happening because of the mirroring with the pin view. Apparently in some update Qt has started triggering edits on currentChanged events. Sigh. For the moment we disable editing of cells in the mirrored view when it is hidden this worksaround the problem for most people, need a better fix for when the view is being used. --- src/calibre/gui2/library/delegates.py | 14 +---- src/calibre/gui2/library/views.py | 89 ++++++++++++++++----------- src/calibre/gui2/pin_columns.py | 7 ++- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index 3b7264199f..f92547d70c 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -206,23 +206,13 @@ def get_val_for_textlike_columns(index_): class StyledItemDelegate(QStyledItemDelegate): ''' - When closing an editor and opening another, Qt sometimes picks what appears - to be a random line and column for the second editor. This function checks - that the current index for a new editor is the same as the current view. If - it isn't then the editor shouldn't be opened. - Set the flag ignore_kb_mods_on_edit before opening an editor if you don't want keyboard modifiers taken into account, for example when using Shift-Tab as a backtab when editing cells. This prevents opening dialogs by mistake. See giu2.library.views.closeEditor() for an example. ''' - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.table_widget = args[0] - # Set this to True here. It is up the the subclasses to set it to False if needed. - self.is_editable_with_tab = True - self.ignore_kb_mods_on_edit = False + is_editable_with_tab = True # sub-classes set to False is needed + ignore_kb_mods_on_edit = False def createEditor(self, parent, option, index): current_indices = [self.table_widget.currentIndex()] diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 4151c41929..dd40581c05 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -449,7 +449,7 @@ class BooksView(QTableView): # {{{ for wv in self, self.pin_view: wv.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) wv.setSortingEnabled(True) - self.selectionModel().currentRowChanged.connect(self._model.current_changed, type=Qt.ConnectionType.QueuedConnection) + self.selectionModel().currentRowChanged.connect(self.on_current_row_change) self.selectionModel().selectionChanged.connect(self.selection_changed.emit) self.preserve_state = partial(PreserveViewState, self) self.marked_changed_listener = FunctionDispatcher(self.marked_changed) @@ -1637,48 +1637,65 @@ class BooksView(QTableView): # {{{ def close(self): self._model.close() + def is_index_editable_with_tab(self, index) -> bool: + if not index.isValid(): + return False + col = self.column_map[index.column()] + m = self.model() + if m.is_custom_column(col): + # Don't try to open editors implemented by dialogs such as + # markdown, composites and comments + return self.itemDelegateForIndex(index).is_editable_with_tab + return bool(m.flags(index) & Qt.ItemFlag.ItemIsEditable) + def closeEditor(self, editor, hint): - # As of Qt 6.7.2, for some reason, Qt opens the next editor after - # closing this editor and then immediately closes it again. So - # workaround the bug by opening the editor again after an event loop - # tick. + # We want to implement our own go to next/previous cell behavior + # so do it here. orig = self.currentIndex() - move_by = None + do_move = False + delta = 1 if hint is QAbstractItemDelegate.EndEditHint.EditNextItem: - move_by = QAbstractItemView.CursorAction.MoveNext + do_move = True elif hint is QAbstractItemDelegate.EndEditHint.EditPreviousItem: - move_by = QAbstractItemView.CursorAction.MovePrevious - if move_by is not None: - hint = QAbstractItemDelegate.EndEditHint.NoHint - super().closeEditor(editor, hint) - if move_by is not None and self.currentIndex() == orig and self.state() is not QAbstractItemView.State.EditingState: - # Skip over columns that aren't editable or are implemented by a dialog - m = self._model - while True: - index = self.moveCursor(move_by, Qt.KeyboardModifier.NoModifier) - if not index.isValid(): - break - self.setCurrentIndex(index) - col = self.column_map[index.column()] - if m.is_custom_column(col): + do_move = True + delta = -1 + super().closeEditor(editor, QAbstractItemDelegate.EndEditHint.NoHint) + self.selectionModel().setCurrentIndex(orig, QItemSelectionModel.SelectionFlag.NoUpdate) + if do_move: + QTimer.singleShot(0, lambda: self.edit_next_cell(orig, delta)) + + def on_current_row_change(self, current, previous): + self._model.current_changed(current, previous) + + def edit(self, index, trigger=QAbstractItemView.EditTrigger.AllEditTriggers, event=None): + edited = super().edit(index, trigger, event) + return edited + + def edit_next_cell(self, current, delta=1): + m = self.model() + idx = m.index(current.row(), current.column(), current.parent()) + while True: + col = idx.column() + delta + if col < 0 or col >= len(self.column_map): + return + colname = self.column_map[col] + idx = m.index(current.row(), col, current.parent()) + if m.is_custom_column(colname): + if self.itemDelegateForIndex(idx).is_editable_with_tab: # Don't try to open editors implemented by dialogs such as # markdown, composites and comments - if self.itemDelegateForIndex(index).is_editable_with_tab: - break - elif m.flags(index) & Qt.ItemFlag.ItemIsEditable: - # Standard editable column break - if index.isValid(): - def edit(): - if index.isValid(): - self.setCurrentIndex(index) - # Tell the delegate to ignore keyboard modifiers in case - # Shift-Tab is being used to move the cell. - d = self.itemDelegateForIndex(index) - if d is not None: - d.ignore_kb_mods_on_edit = True - self.edit(index) - QTimer.singleShot(0, edit) + elif m.flags(idx) & Qt.ItemFlag.ItemIsEditable: + break + + if idx.isValid(): + # Tell the delegate to ignore keyboard modifiers in case + # Shift-Tab is being used to move the cell. + d = self.itemDelegateForIndex(idx) + if d is not None: + d.ignore_kb_mods_on_edit = True + self.setCurrentIndex(idx) + self.edit(idx) def set_editable(self, editable, supports_backloading): self._model.set_editable(editable) diff --git a/src/calibre/gui2/pin_columns.py b/src/calibre/gui2/pin_columns.py index 28f6b183fb..30a672d9d5 100644 --- a/src/calibre/gui2/pin_columns.py +++ b/src/calibre/gui2/pin_columns.py @@ -2,7 +2,7 @@ # License: GPLv3 Copyright: 2018, Kovid Goyal -from qt.core import QSplitter, QTableView +from qt.core import QAbstractItemView, QSplitter, QTableView from calibre.gui2 import gprefs from calibre.gui2.library import DEFAULT_SORT @@ -17,6 +17,11 @@ class PinTableView(QTableView): self.splitter = None self.disable_save_state = False + def edit(self, index, trigger=QAbstractItemView.EditTrigger.AllEditTriggers, event=None): + if not self.isVisible(): + return False + return super().edit(index, trigger, event) + @property def column_map(self): return self.books_view.column_map