From 2b5bf23bbd15df62b40955e2e6fe71903bf533da Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 25 Sep 2021 15:21:06 +0200 Subject: [PATCH 01/12] Check 'index.isValid()' in 'table.model' --- qt/aqt/browser/table/model.py | 4 ++++ qt/aqt/browser/table/table.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index f04e2d63e..1d4cae4ac 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -175,6 +175,8 @@ class DataModel(QAbstractTableModel): def get_card(self, index: QModelIndex) -> Optional[Card]: """Try to return the indicated, possibly deleted card.""" + if not index.isValid(): + return None try: return self._state.get_card(self.get_item(index)) except NotFoundError: @@ -182,6 +184,8 @@ class DataModel(QAbstractTableModel): def get_note(self, index: QModelIndex) -> Optional[Note]: """Try to return the indicated, possibly deleted note.""" + if not index.isValid(): + return None try: return self._state.get_note(self.get_item(index)) except NotFoundError: diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 3f555fc25..2ea67e0c8 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -78,13 +78,9 @@ class Table: # Get objects def get_current_card(self) -> Optional[Card]: - if not self.has_current(): - return None return self._model.get_card(self._current()) def get_current_note(self) -> Optional[Note]: - if not self.has_current(): - return None return self._model.get_note(self._current()) def get_single_selected_card(self) -> Optional[Card]: From 73ec0a2b2ed446a556bdff175ad86687a4acd37e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 25 Sep 2021 15:27:19 +0200 Subject: [PATCH 02/12] Add callbacks to row fetching routine Called when a row is detected that has been deleted or restored. --- qt/aqt/browser/table/model.py | 37 ++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 1d4cae4ac..8e9a17ac8 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -4,7 +4,7 @@ from __future__ import annotations import time -from typing import Any, Dict, List, Optional, Sequence, Union, cast +from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast import aqt from anki.cards import Card, CardId @@ -32,7 +32,13 @@ class DataModel(QAbstractTableModel): _stale_cutoff -- A threshold to decide whether a cached row has gone stale. """ - def __init__(self, col: Collection, state: ItemState) -> None: + def __init__( + self, + col: Collection, + state: ItemState, + row_state_will_change_callback: Callable, + row_state_changed_callback: Callable, + ) -> None: QAbstractTableModel.__init__(self) self.col: Collection = col self.columns: Dict[str, Column] = dict( @@ -44,6 +50,8 @@ class DataModel(QAbstractTableModel): self._rows: Dict[int, CellRow] = {} self._block_updates = False self._stale_cutoff = 0.0 + self._on_row_state_will_change = row_state_will_change_callback + self._on_row_state_changed = row_state_changed_callback self._want_tooltips = aqt.mw.pm.show_browser_table_tooltips() # Row Object Interface @@ -59,15 +67,34 @@ class DataModel(QAbstractTableModel): if row := self._rows.get(item): if not self._block_updates and row.is_stale(self._stale_cutoff): # need to refresh - self._rows[item] = self._fetch_row_from_backend(item) - return self._rows[item] + return self._fetch_row_and_update_cache(index, item, row) # return row, even if it's stale return row if self._block_updates: # blank row until we unblock return CellRow.placeholder(self.len_columns()) # missing row, need to build - self._rows[item] = self._fetch_row_from_backend(item) + return self._fetch_row_and_update_cache(index, item, None) + + def _fetch_row_and_update_cache( + self, index: QModelIndex, item: ItemId, old_row: Optional[CellRow] + ) -> CellRow: + """Fetch a row from the backend, add it to the cache and return it. + Thereby, handle callbacks if the row is being deleted or restored. + """ + new_row = self._fetch_row_from_backend(item) + # row state has changed if existence of cached and fetched counterparts differ + # if the row was previously uncached, it is assumed to have existed + state_change = ( + new_row.is_deleted + if old_row is None + else old_row.is_deleted != new_row.is_deleted + ) + if state_change: + self._on_row_state_will_change(index, not new_row.is_deleted) + self._rows[item] = new_row + if state_change: + self._on_row_state_changed(index, not new_row.is_deleted) return self._rows[item] def _fetch_row_from_backend(self, item: ItemId) -> CellRow: From 3a8c4945448a159759938a3d1d88fa0909185353 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 25 Sep 2021 15:32:22 +0200 Subject: [PATCH 03/12] Only check flags of cached rows Speeds up the selection process (esp. Ctrl+A) by avoiding to fetch rows. Co-authored-by: BlueGreenMagick --- qt/aqt/browser/table/model.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 8e9a17ac8..22f64b0a4 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -119,6 +119,10 @@ class DataModel(QAbstractTableModel): ) return row + def get_cached_row(self, index: QModelIndex) -> Optional[CellRow]: + """Get row if it is cached, regardless of staleness.""" + return self._rows.get(self.get_item(index)) + # Reset def mark_cache_stale(self) -> None: @@ -326,8 +330,10 @@ class DataModel(QAbstractTableModel): return None def flags(self, index: QModelIndex) -> Qt.ItemFlags: - if self.get_row(index).is_deleted: - return Qt.ItemFlags(Qt.NoItemFlags) + # shortcut for large selections (Ctrl+A) to avoid fetching large numbers of rows at once + if row := self.get_cached_row(index): + if row.is_deleted: + return Qt.ItemFlags(Qt.NoItemFlags) return cast(Qt.ItemFlags, Qt.ItemIsEnabled | Qt.ItemIsSelectable) From f54f15cd44317b12b9b327cb90109918ff3fc2ac Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 25 Sep 2021 15:39:27 +0200 Subject: [PATCH 04/12] Handle deleted or restored rows - Cache the result of 'table.len_selection()' - Update this cache manually when a row was deleted or restored - Emit 'dataChanged()' after such a change to fix flags not updating correctly to the shortcut in 'model.flags()' - Remove/retsore focus if the current element was deleted/restored --- qt/aqt/browser/browser.py | 4 +-- qt/aqt/browser/table/table.py | 64 +++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index d3c2d4900..cc74f4990 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -417,9 +417,7 @@ class Browser(QMainWindow): gui_hooks.editor_did_init.remove(add_preview_button) @ensure_editor_saved - def onRowChanged( - self, _current: Optional[QItemSelection], _previous: Optional[QItemSelection] - ) -> None: + def on_row_changed(self) -> None: """Update current note and hide/show editor.""" if self._closeEventHasCleanedUp: return diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 2ea67e0c8..9a3b33ea2 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -39,8 +39,14 @@ class Table: if self.col.get_config_bool(Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE) else CardState(self.col) ) - self._model = DataModel(self.col, self._state) + self._model = DataModel( + self.col, + self._state, + self._on_row_state_will_change, + self._on_row_state_changed, + ) self._view: Optional[QTableView] = None + self._len_selection = 0 self._current_item: Optional[ItemId] = None self._selected_items: Sequence[ItemId] = [] @@ -60,8 +66,12 @@ class Table: def len(self) -> int: return self._model.len_rows() - def len_selection(self) -> int: - return len(self._view.selectionModel().selectedRows()) + def len_selection(self, refresh: bool = False) -> int: + # This may be slow because Qt queries flags() for the whole selection, + # so we update the cached value directly where possible + if refresh: + self._len_selection = len(self._view.selectionModel().selectedRows()) + return self._len_selection def has_current(self) -> bool: return self._view.selectionModel().currentIndex().isValid() @@ -197,6 +207,11 @@ class Table: def to_last_row(self) -> None: self._move_current_to_row(self._model.len_rows() - 1) + def clear_current(self) -> None: + self._view.selectionModel().setCurrentIndex( + QModelIndex(), QItemSelectionModel.NoUpdate + ) + # Private methods ###################################################################### @@ -265,7 +280,7 @@ class Table: self._view.selectionModel() self._view.setItemDelegate(StatusDelegate(self.browser, self._model)) qconnect( - self._view.selectionModel().selectionChanged, self.browser.onRowChanged + self._view.selectionModel().selectionChanged, self._on_selection_changed ) self._view.setWordWrap(False) self._view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) @@ -311,6 +326,43 @@ class Table: # Slots + def _on_selection_changed(self, _c: Any, _p: Any) -> None: + self.len_selection(refresh=True) + self.browser.on_row_changed() + + def _on_row_state_will_change(self, index: QModelIndex, was_restored: bool) -> None: + if not was_restored: + row_changed = False + if self._view.selectionModel().isSelected(index): + # calculate directly, because 'self.len_selection()' is slow and + # this method will be called a lot if a lot of rows were deleted + self._len_selection -= 1 + row_changed = True + if index.row() == self._current().row(): + # avoid focus on deleted (disabled) rows + self.clear_current() + row_changed = True + if row_changed: + self.browser.on_row_changed() + + def _on_row_state_changed(self, index: QModelIndex, was_restored: bool) -> None: + if was_restored: + if self._view.selectionModel().isSelected(index): + self._len_selection += 1 + if not self._current().isValid() and self.len_selection() == 0: + # restore focus for convenience + self._select_rows([index.row()]) + self._set_current(index.row()) + self._scroll_to_row(index.row()) + # row change has been triggered + return + self.browser.on_row_changed() + # Workaround for a bug where the flags for the first column don't update + # automatically (due to the shortcut in 'model.flags()') + top_left = self._model.index(index.row(), 0) + bottom_right = self._model.index(index.row(), self._model.len_columns() - 1) + self._model.dataChanged.emit(top_left, bottom_right) # type: ignore + def _on_context_menu(self, _point: QPoint) -> None: menu = QMenu() if self.is_notes_mode(): @@ -404,9 +456,9 @@ class Table: self._select_rows(rows) self._set_current(current) self._scroll_to_row(current) - if self.len_selection() == 0: + if self.len_selection(refresh=True) == 0: # no row change will fire - self.browser.onRowChanged(QItemSelection(), QItemSelection()) + self.browser.on_row_changed() self._selected_items = [] self._current_item = None From 18f0d026b5b20aa45ea47ac282da928693c6d152 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 25 Sep 2021 17:28:38 +0200 Subject: [PATCH 05/12] Switch to new row before deleting notes The table now properly deselects deleted rows, but that takes effort and it's more convenient to have a selected row after deleting. --- qt/aqt/browser/browser.py | 21 +++------------------ qt/aqt/browser/table/model.py | 5 +++++ qt/aqt/browser/table/table.py | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index cc74f4990..2cf2c1b19 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -9,13 +9,7 @@ import aqt import aqt.forms from anki._legacy import deprecated from anki.cards import Card, CardId -from anki.collection import ( - Collection, - Config, - OpChanges, - OpChangesWithCount, - SearchNode, -) +from anki.collection import Collection, Config, OpChanges, SearchNode from anki.consts import * from anki.errors import NotFoundError from anki.lang import without_unicode_isolation @@ -60,7 +54,6 @@ from aqt.utils import ( saveState, showWarning, skip_if_selection_is_empty, - tooltip, tr, ) @@ -627,16 +620,8 @@ class Browser(QMainWindow): return nids = self.table.get_selected_note_ids() - - def after_remove(changes: OpChangesWithCount) -> None: - tooltip(tr.browsing_cards_deleted(count=changes.count)) - # select the next card if there is one - self.focusTo = self.editor.currentField - self.table.to_next_row() - - remove_notes(parent=self, note_ids=nids).success( - after_remove - ).run_in_background() + self.table.to_row_of_unselected_note() + remove_notes(parent=self, note_ids=nids).run_in_background() # legacy diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 22f64b0a4..5cc81f027 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -184,6 +184,11 @@ class DataModel(QAbstractTableModel): def get_note_ids(self, indices: List[QModelIndex]) -> Sequence[NoteId]: return self._state.get_note_ids(self.get_items(indices)) + def get_note_id(self, index: QModelIndex) -> Optional[NoteId]: + if nid_list := self._state.get_note_ids([self.get_item(index)]): + return nid_list[0] + return None + # Get row numbers from items def get_item_row(self, item: ItemId) -> Optional[int]: diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 9a3b33ea2..4d2e1b6f4 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -207,6 +207,25 @@ class Table: def to_last_row(self) -> None: self._move_current_to_row(self._model.len_rows() - 1) + def to_row_of_unselected_note(self) -> None: + """Select and set focus to a row whose note is not selected, + starting with the nearest row below, then above the focused row. + If that's not possible, clear selection. + """ + nids = self.get_selected_note_ids() + for row in range(self._current().row(), self.len()): + nid = self._model.get_note_id(self._model.index(row, 0)) + if nid is not None and nid not in nids: + self._move_current_to_row(row) + return + for row in range(self._current().row() - 1, -1, -1): + nid = self._model.get_note_id(self._model.index(row, 0)) + if nid is not None and nid not in nids: + self._move_current_to_row(row) + return + self.clear_selection() + self.clear_current() + def clear_current(self) -> None: self._view.selectionModel().setCurrentIndex( QModelIndex(), QItemSelectionModel.NoUpdate From f89811870ed8e9410860451aa3675cf8af8d4011 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 27 Sep 2021 10:15:53 +0200 Subject: [PATCH 06/12] Calculate number of selected rows manually `len(self._view.selectionModel().selectedRows())` is slow for large selections, because Qt queries flags() for every selected cell, so we calculate the number of selected rows ourselves. --- qt/aqt/browser/table/table.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 4d2e1b6f4..8e7585c40 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -67,10 +67,9 @@ class Table: return self._model.len_rows() def len_selection(self, refresh: bool = False) -> int: - # This may be slow because Qt queries flags() for the whole selection, - # so we update the cached value directly where possible - if refresh: - self._len_selection = len(self._view.selectionModel().selectedRows()) + # `len(self._view.selectionModel().selectedRows())` is slow for large + # selections, because Qt queries flags() for every selected cell, so we + # calculate the number of selected rows ourselves return self._len_selection def has_current(self) -> bool: @@ -117,6 +116,7 @@ class Table: self._view.selectAll() def clear_selection(self) -> None: + self._len_selection = 0 self._view.selectionModel().clear() def invert_selection(self) -> None: @@ -345,8 +345,11 @@ class Table: # Slots - def _on_selection_changed(self, _c: Any, _p: Any) -> None: - self.len_selection(refresh=True) + def _on_selection_changed( + self, selected: QItemSelection, deselected: QItemSelection + ) -> None: + self._len_selection += len(selected.indexes()) // self._model.len_columns() + self._len_selection -= len(deselected.indexes()) // self._model.len_columns() self.browser.on_row_changed() def _on_row_state_will_change(self, index: QModelIndex, was_restored: bool) -> None: @@ -475,7 +478,7 @@ class Table: self._select_rows(rows) self._set_current(current) self._scroll_to_row(current) - if self.len_selection(refresh=True) == 0: + if self.len_selection() == 0: # no row change will fire self.browser.on_row_changed() self._selected_items = [] From 287854d014fa3818fed2c9c23a106595e0170974 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 28 Sep 2021 10:19:33 +0200 Subject: [PATCH 07/12] Prefer `selectionModel().reset()` over `.clear()` The latter triggers `selectionChanged()` unreliably, probably due to the aggregation of chronologically close events, causing problems in tracking `_len_selection`. `reset()` never emits signals. --- qt/aqt/browser/table/table.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 8e7585c40..436429049 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -132,10 +132,12 @@ class Table: def select_single_card(self, card_id: CardId) -> None: """Try to set the selection to the item corresponding to the given card.""" - self.clear_selection() + self._reset_selection() if (row := self._model.get_card_row(card_id)) is not None: self._view.selectRow(row) self._scroll_to_row(row, scroll_even_if_visible=True) + else: + self.browser.on_row_changed() # Reset @@ -223,8 +225,8 @@ class Table: if nid is not None and nid not in nids: self._move_current_to_row(row) return - self.clear_selection() - self.clear_current() + self._reset_selection() + self.browser.on_row_changed() def clear_current(self) -> None: self._view.selectionModel().setCurrentIndex( @@ -248,6 +250,11 @@ class Table: ) self._view.selectionModel().setCurrentIndex(index, QItemSelectionModel.NoUpdate) + def _reset_selection(self) -> None: + """Remove selection and focus without emitting signals.""" + self._view.selectionModel().reset() + self._len_selection = 0 + def _select_rows(self, rows: List[int]) -> None: selection = QItemSelection() for row in rows: @@ -470,7 +477,7 @@ class Table: """Restore the saved selection and current element as far as possible and scroll to the new current element. Clear the saved selection. """ - self.clear_selection() + self._reset_selection() if not self._model.is_empty(): rows, current = new_selected_and_current() rows = self._qualify_selected_rows(rows, current) From eac8972a2809d2aac2ff199e1bc9a69ee449bbe1 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 28 Sep 2021 10:22:20 +0200 Subject: [PATCH 08/12] Calculate `len_selection` depending on modifiers If no modifiers are pressed, a single row has probably been clicked and `selectedRows()` is fast, while a lot of rows might have been deselcted. --- qt/aqt/browser/table/table.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 436429049..cce83de90 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -355,8 +355,17 @@ class Table: def _on_selection_changed( self, selected: QItemSelection, deselected: QItemSelection ) -> None: - self._len_selection += len(selected.indexes()) // self._model.len_columns() - self._len_selection -= len(deselected.indexes()) // self._model.len_columns() + # `selection.indexes()` calls `flags()` for all the selection's indexes, + # whereas `selectedRows()` calls it for the indexes of the resulting selection. + # Both may be slow, so we try to optimise. + if KeyboardModifiersPressed().shift or KeyboardModifiersPressed().control: + # Current selection is modified. The number of added/removed rows is + # usually smaller than the number of rows in the resulting selection. + self._len_selection += len(selected.indexes()) // self._model.len_columns() + self._len_selection -= len(deselected.indexes()) // self._model.len_columns() + else: + # New selection is created. Usually a single row or none at all. + self._len_selection = len(self._view.selectionModel().selectedRows()) self.browser.on_row_changed() def _on_row_state_will_change(self, index: QModelIndex, was_restored: bool) -> None: From a592c5b3a9e672147fc83f26662a7e1d4476219f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 28 Sep 2021 11:45:22 +0200 Subject: [PATCH 09/12] Cache `selectionModel().selectedRows()` --- qt/aqt/browser/table/table.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index cce83de90..3ff64bae8 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -46,7 +46,10 @@ class Table: self._on_row_state_changed, ) self._view: Optional[QTableView] = None + # cached for performance self._len_selection = 0 + self._selected_rows: Optional[List[QModelIndex]] = None + # temporarily set for selection preservation self._current_item: Optional[ItemId] = None self._selected_items: Sequence[ItemId] = [] @@ -117,6 +120,7 @@ class Table: def clear_selection(self) -> None: self._len_selection = 0 + self._selected_rows = None self._view.selectionModel().clear() def invert_selection(self) -> None: @@ -242,7 +246,9 @@ class Table: return self._view.selectionModel().currentIndex() def _selected(self) -> List[QModelIndex]: - return self._view.selectionModel().selectedRows() + if self._selected_rows is None: + self._selected_rows = self._view.selectionModel().selectedRows() + return self._selected_rows def _set_current(self, row: int, column: int = 0) -> None: index = self._model.index( @@ -251,9 +257,13 @@ class Table: self._view.selectionModel().setCurrentIndex(index, QItemSelectionModel.NoUpdate) def _reset_selection(self) -> None: - """Remove selection and focus without emitting signals.""" + """Remove selection and focus without emitting signals. + If no selection change is triggerd afterwards, `browser.on_row_changed()` + must be called. + """ self._view.selectionModel().reset() self._len_selection = 0 + self._selected_rows = None def _select_rows(self, rows: List[int]) -> None: selection = QItemSelection() @@ -362,10 +372,13 @@ class Table: # Current selection is modified. The number of added/removed rows is # usually smaller than the number of rows in the resulting selection. self._len_selection += len(selected.indexes()) // self._model.len_columns() - self._len_selection -= len(deselected.indexes()) // self._model.len_columns() + self._len_selection -= ( + len(deselected.indexes()) // self._model.len_columns() + ) else: # New selection is created. Usually a single row or none at all. self._len_selection = len(self._view.selectionModel().selectedRows()) + self._selected_rows = None self.browser.on_row_changed() def _on_row_state_will_change(self, index: QModelIndex, was_restored: bool) -> None: @@ -376,6 +389,7 @@ class Table: # this method will be called a lot if a lot of rows were deleted self._len_selection -= 1 row_changed = True + self._selected_rows = None if index.row() == self._current().row(): # avoid focus on deleted (disabled) rows self.clear_current() @@ -387,6 +401,7 @@ class Table: if was_restored: if self._view.selectionModel().isSelected(index): self._len_selection += 1 + self._selected_rows = None if not self._current().isValid() and self.len_selection() == 0: # restore focus for convenience self._select_rows([index.row()]) From 46ffbb936da31890f439470470cda3bd4a5bbd5c Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 28 Sep 2021 11:47:13 +0200 Subject: [PATCH 10/12] Speed up `to_row_of_unselected_note()` Skip rows between selected rows and return fetched note ids. --- qt/aqt/browser/browser.py | 3 +-- qt/aqt/browser/table/table.py | 39 +++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 2cf2c1b19..3e0d10e01 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -619,8 +619,7 @@ class Browser(QMainWindow): if focus != self.form.tableView: return - nids = self.table.get_selected_note_ids() - self.table.to_row_of_unselected_note() + nids = self.table.to_row_of_unselected_note() remove_notes(parent=self, note_ids=nids).run_in_background() # legacy diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 3ff64bae8..65981c87e 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -213,24 +213,37 @@ class Table: def to_last_row(self) -> None: self._move_current_to_row(self._model.len_rows() - 1) - def to_row_of_unselected_note(self) -> None: - """Select and set focus to a row whose note is not selected, - starting with the nearest row below, then above the focused row. + def to_row_of_unselected_note(self) -> Sequence[NoteId]: + """Select and set focus to a row whose note is not selected, trying + the rows below the bottomost, then above the topmost selected row. If that's not possible, clear selection. + Return previously selected note ids. """ nids = self.get_selected_note_ids() - for row in range(self._current().row(), self.len()): - nid = self._model.get_note_id(self._model.index(row, 0)) - if nid is not None and nid not in nids: - self._move_current_to_row(row) - return - for row in range(self._current().row() - 1, -1, -1): - nid = self._model.get_note_id(self._model.index(row, 0)) - if nid is not None and nid not in nids: - self._move_current_to_row(row) - return + + bottom = max(r.row() for r in self._selected()) + 1 + for row in range(bottom, self.len()): + index = self._model.index(row, 0) + if self._model.get_row(index).is_deleted: + continue + if self._model.get_note_id(index) in nids: + continue + self._move_current_to_row(row) + return nids + + top = min(r.row() for r in self._selected()) - 1 + for row in range(top, -1, -1): + index = self._model.index(row, 0) + if self._model.get_row(index).is_deleted: + continue + if self._model.get_note_id(index) in nids: + continue + self._move_current_to_row(row) + return nids + self._reset_selection() self.browser.on_row_changed() + return nids def clear_current(self) -> None: self._view.selectionModel().setCurrentIndex( From 72f6f9a47a55fb1bb3aec6c238906d80888f0549 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 28 Sep 2021 11:48:35 +0200 Subject: [PATCH 11/12] Tweak comment Co-authored-by: Damien Elmes --- qt/aqt/browser/table/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 5cc81f027..96be01121 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -80,7 +80,7 @@ class DataModel(QAbstractTableModel): self, index: QModelIndex, item: ItemId, old_row: Optional[CellRow] ) -> CellRow: """Fetch a row from the backend, add it to the cache and return it. - Thereby, handle callbacks if the row is being deleted or restored. + Then fire callbacks if the row is being deleted or restored. """ new_row = self._fetch_row_from_backend(item) # row state has changed if existence of cached and fetched counterparts differ From 326cf0f77fcfbb3994b0b070e8c55546da5fd187 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 28 Sep 2021 12:11:20 +0200 Subject: [PATCH 12/12] Remove redundant call to `len_columns()` --- qt/aqt/browser/table/table.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index 65981c87e..8bd6ea1ac 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -384,10 +384,9 @@ class Table: if KeyboardModifiersPressed().shift or KeyboardModifiersPressed().control: # Current selection is modified. The number of added/removed rows is # usually smaller than the number of rows in the resulting selection. - self._len_selection += len(selected.indexes()) // self._model.len_columns() - self._len_selection -= ( - len(deselected.indexes()) // self._model.len_columns() - ) + self._len_selection += ( + len(selected.indexes()) - len(deselected.indexes()) + ) // self._model.len_columns() else: # New selection is created. Usually a single row or none at all. self._len_selection = len(self._view.selectionModel().selectedRows())