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.
This commit is contained in:
Kovid Goyal 2025-02-10 07:52:16 +05:30
parent b56849623f
commit f6167d7f0e
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 61 additions and 49 deletions

View File

@ -206,23 +206,13 @@ def get_val_for_textlike_columns(index_):
class StyledItemDelegate(QStyledItemDelegate): 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 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 want keyboard modifiers taken into account, for example when using Shift-Tab
as a backtab when editing cells. This prevents opening dialogs by mistake. as a backtab when editing cells. This prevents opening dialogs by mistake.
See giu2.library.views.closeEditor() for an example. See giu2.library.views.closeEditor() for an example.
''' '''
is_editable_with_tab = True # sub-classes set to False is needed
def __init__(self, *args, **kwargs): ignore_kb_mods_on_edit = False
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
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
current_indices = [self.table_widget.currentIndex()] current_indices = [self.table_widget.currentIndex()]

View File

@ -449,7 +449,7 @@ class BooksView(QTableView): # {{{
for wv in self, self.pin_view: for wv in self, self.pin_view:
wv.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) wv.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
wv.setSortingEnabled(True) 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.selectionModel().selectionChanged.connect(self.selection_changed.emit)
self.preserve_state = partial(PreserveViewState, self) self.preserve_state = partial(PreserveViewState, self)
self.marked_changed_listener = FunctionDispatcher(self.marked_changed) self.marked_changed_listener = FunctionDispatcher(self.marked_changed)
@ -1637,48 +1637,65 @@ class BooksView(QTableView): # {{{
def close(self): def close(self):
self._model.close() 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): def closeEditor(self, editor, hint):
# As of Qt 6.7.2, for some reason, Qt opens the next editor after # We want to implement our own go to next/previous cell behavior
# closing this editor and then immediately closes it again. So # so do it here.
# workaround the bug by opening the editor again after an event loop
# tick.
orig = self.currentIndex() orig = self.currentIndex()
move_by = None do_move = False
delta = 1
if hint is QAbstractItemDelegate.EndEditHint.EditNextItem: if hint is QAbstractItemDelegate.EndEditHint.EditNextItem:
move_by = QAbstractItemView.CursorAction.MoveNext do_move = True
elif hint is QAbstractItemDelegate.EndEditHint.EditPreviousItem: elif hint is QAbstractItemDelegate.EndEditHint.EditPreviousItem:
move_by = QAbstractItemView.CursorAction.MovePrevious do_move = True
if move_by is not None: delta = -1
hint = QAbstractItemDelegate.EndEditHint.NoHint super().closeEditor(editor, QAbstractItemDelegate.EndEditHint.NoHint)
super().closeEditor(editor, hint) self.selectionModel().setCurrentIndex(orig, QItemSelectionModel.SelectionFlag.NoUpdate)
if move_by is not None and self.currentIndex() == orig and self.state() is not QAbstractItemView.State.EditingState: if do_move:
# Skip over columns that aren't editable or are implemented by a dialog QTimer.singleShot(0, lambda: self.edit_next_cell(orig, delta))
m = self._model
while True: def on_current_row_change(self, current, previous):
index = self.moveCursor(move_by, Qt.KeyboardModifier.NoModifier) self._model.current_changed(current, previous)
if not index.isValid():
break def edit(self, index, trigger=QAbstractItemView.EditTrigger.AllEditTriggers, event=None):
self.setCurrentIndex(index) edited = super().edit(index, trigger, event)
col = self.column_map[index.column()] return edited
if m.is_custom_column(col):
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 # Don't try to open editors implemented by dialogs such as
# markdown, composites and comments # markdown, composites and comments
if self.itemDelegateForIndex(index).is_editable_with_tab:
break
elif m.flags(index) & Qt.ItemFlag.ItemIsEditable:
# Standard editable column
break break
if index.isValid(): elif m.flags(idx) & Qt.ItemFlag.ItemIsEditable:
def edit(): break
if index.isValid():
self.setCurrentIndex(index) if idx.isValid():
# Tell the delegate to ignore keyboard modifiers in case # Tell the delegate to ignore keyboard modifiers in case
# Shift-Tab is being used to move the cell. # Shift-Tab is being used to move the cell.
d = self.itemDelegateForIndex(index) d = self.itemDelegateForIndex(idx)
if d is not None: if d is not None:
d.ignore_kb_mods_on_edit = True d.ignore_kb_mods_on_edit = True
self.edit(index) self.setCurrentIndex(idx)
QTimer.singleShot(0, edit) self.edit(idx)
def set_editable(self, editable, supports_backloading): def set_editable(self, editable, supports_backloading):
self._model.set_editable(editable) self._model.set_editable(editable)

View File

@ -2,7 +2,7 @@
# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from qt.core import QSplitter, QTableView from qt.core import QAbstractItemView, QSplitter, QTableView
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
from calibre.gui2.library import DEFAULT_SORT from calibre.gui2.library import DEFAULT_SORT
@ -17,6 +17,11 @@ class PinTableView(QTableView):
self.splitter = None self.splitter = None
self.disable_save_state = False 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 @property
def column_map(self): def column_map(self):
return self.books_view.column_map return self.books_view.column_map