diff --git a/.mypy.ini b/.mypy.ini index ef32ed7a2..be7fcefa8 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -1,15 +1,15 @@ [mypy] python_version = 3.9 -pretty = false -no_strict_optional = true -show_error_codes = true -check_untyped_defs = true +pretty = False +strict_optional = False +show_error_codes = True +check_untyped_defs = True disallow_untyped_decorators = True warn_redundant_casts = True warn_unused_configs = True -strict_equality = true -namespace_packages = true -explicit_package_bases = true +strict_equality = True +namespace_packages = True +explicit_package_bases = True mypy_path = pylib, out/pylib, @@ -26,10 +26,14 @@ disallow_untyped_defs = True disallow_untyped_defs = False [mypy-anki.exporting] disallow_untyped_defs = False +[mypy-aqt] +strict_optional = True +[mypy-aqt.browser.*] +strict_optional = True [mypy-aqt.operations.*] -no_strict_optional = false +strict_optional = True [mypy-anki.scheduler.base] -no_strict_optional = false +strict_optional = True [mypy-anki._backend.rsbridge] ignore_missing_imports = True [mypy-anki._vendor.stringcase] @@ -37,10 +41,10 @@ disallow_untyped_defs = False [mypy-stringcase] ignore_missing_imports = True [mypy-aqt.mpv] -disallow_untyped_defs=false -ignore_errors=true +disallow_untyped_defs=False +ignore_errors=True [mypy-aqt.winpaths] -disallow_untyped_defs=false +disallow_untyped_defs=False [mypy-win32file] ignore_missing_imports = True diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 355cfa95c..cf5dd1538 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -187,7 +187,7 @@ Christian Donat Asuka Minato Dillon Baldwin Voczi -Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> +Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> Themis Demetriades Luke Bartholomew Gregory Abrasaldo diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 66b2fb618..ae5caa050 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -472,7 +472,7 @@ class Collection(DeprecatedNamesMixin): # Object helpers ########################################################################## - def get_card(self, id: CardId) -> Card: + def get_card(self, id: CardId | None) -> Card: return Card(self, id) def update_cards( diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index a8514b541..faa72e82f 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -280,7 +280,10 @@ def setupLangAndBackend( if _qtrans.load(f"qtbase_{qt_lang}", qt_dir): app.installTranslator(_qtrans) - return anki.lang.current_i18n + backend = anki.lang.current_i18n + assert backend is not None + + return backend # App initialisation @@ -291,11 +294,13 @@ class NativeEventFilter(QAbstractNativeEventFilter): def nativeEventFilter( self, eventType: Any, message: Any ) -> tuple[bool, sip.voidptr | None]: + if eventType == "windows_generic_MSG": - import ctypes + import ctypes.wintypes msg = ctypes.wintypes.MSG.from_address(int(message)) if msg.message == 17: # WM_QUERYENDSESSION + assert mw is not None if mw.can_auto_sync(): mw.app._set_windows_shutdown_block_reason(tr.sync_syncing()) mw.progress.single_shot(100, mw.unloadProfileAndExit) @@ -325,6 +330,7 @@ class AnkiApp(QApplication): import ctypes from ctypes import windll, wintypes # type: ignore + assert mw is not None windll.user32.ShutdownBlockReasonCreate( wintypes.HWND.from_param(int(mw.effectiveWinId())), ctypes.c_wchar_p(reason), @@ -334,6 +340,7 @@ class AnkiApp(QApplication): if is_win: from ctypes import windll, wintypes # type: ignore + assert mw is not None windll.user32.ShutdownBlockReasonDestroy( wintypes.HWND.from_param(int(mw.effectiveWinId())), ) @@ -388,7 +395,9 @@ class AnkiApp(QApplication): # OS X file/url handler ################################################## - def event(self, evt: QEvent) -> bool: + def event(self, evt: QEvent | None) -> bool: + assert evt is not None + if evt.type() == QEvent.Type.FileOpen: self.appMsg.emit(evt.file() or "raise") # type: ignore return True @@ -397,7 +406,9 @@ class AnkiApp(QApplication): # Global cursor: pointer for Qt buttons ################################################## - def eventFilter(self, src: Any, evt: QEvent) -> bool: + def eventFilter(self, src: Any, evt: QEvent | None) -> bool: + assert evt is not None + pointer_classes = ( QPushButton, QCheckBox, @@ -547,6 +558,8 @@ PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE") def write_profile_results() -> None: + assert profiler is not None + profiler.disable() profile = "out/anki.prof" profiler.dump_stats(profile) diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 0ddb47371..524887b08 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -27,7 +27,7 @@ from anki.scheduler.base import ScheduleCardsAsNew from anki.tags import MARKED_TAG from anki.utils import is_mac from aqt import AnkiQt, gui_hooks -from aqt.editor import Editor +from aqt.editor import Editor, EditorWebView from aqt.errors import show_exception from aqt.exporting import ExportDialog as LegacyExportDialog from aqt.import_export.exporting import ExportDialog @@ -137,7 +137,11 @@ class Browser(QMainWindow): self.form.setupUi(self) self.form.splitter.setChildrenCollapsible(False) splitter_handle_event_filter = QSplitterHandleEventFilter(self.form.splitter) - self.form.splitter.handle(1).installEventFilter(splitter_handle_event_filter) + + splitter_handle = self.form.splitter.handle(1) + assert splitter_handle is not None + + splitter_handle.installEventFilter(splitter_handle_event_filter) # set if exactly 1 row is selected; used by the previewer self.card: Card | None = None self.current_card: Card | None = None @@ -180,6 +184,8 @@ class Browser(QMainWindow): if handler is not self.editor: # fixme: this will leave the splitter shown, but with no current # note being edited + assert self.editor is not None + note = self.editor.note if note: try: @@ -241,7 +247,9 @@ class Browser(QMainWindow): else: self.form.splitter.setOrientation(Qt.Orientation.Horizontal) - def resizeEvent(self, event: QResizeEvent) -> None: + def resizeEvent(self, event: QResizeEvent | None) -> None: + assert event is not None + if self.height() != 0: aspect_ratio = self.width() / self.height() @@ -287,15 +295,19 @@ class Browser(QMainWindow): qconnect(f.actionFullScreen.triggered, self.mw.on_toggle_full_screen) qconnect( f.actionZoomIn.triggered, - lambda: self.editor.web.setZoomFactor(self.editor.web.zoomFactor() + 0.1), + lambda: self._editor_web_view().setZoomFactor( + self._editor_web_view().zoomFactor() + 0.1 + ), ) qconnect( f.actionZoomOut.triggered, - lambda: self.editor.web.setZoomFactor(self.editor.web.zoomFactor() - 0.1), + lambda: self._editor_web_view().setZoomFactor( + self._editor_web_view().zoomFactor() - 0.1 + ), ) qconnect( f.actionResetZoom.triggered, - lambda: self.editor.web.setZoomFactor(1), + lambda: self._editor_web_view().setZoomFactor(1), ) qconnect( self.form.actionLayoutAuto.triggered, @@ -368,14 +380,27 @@ class Browser(QMainWindow): add_ellipsis_to_action_label(f.actionCopy) add_ellipsis_to_action_label(f.action_forget) - def closeEvent(self, evt: QCloseEvent) -> None: + def _editor_web_view(self) -> EditorWebView: + assert self.editor is not None + editor_web_view = self.editor.web + assert editor_web_view is not None + return editor_web_view + + def closeEvent(self, evt: QCloseEvent | None) -> None: + assert evt is not None + if self._closeEventHasCleanedUp: evt.accept() return + + assert self.editor is not None + self.editor.call_after_note_saved(self._closeWindow) evt.ignore() def _closeWindow(self) -> None: + assert self.editor is not None + self._cleanup_preview() self._card_info.close() self.editor.cleanup() @@ -396,7 +421,9 @@ class Browser(QMainWindow): self._closeWindow() onsuccess() - def keyPressEvent(self, evt: QKeyEvent) -> None: + def keyPressEvent(self, evt: QKeyEvent | None) -> None: + assert evt is not None + if evt.key() == Qt.Key.Key_Escape: self.close() else: @@ -426,12 +453,13 @@ class Browser(QMainWindow): card: Card | None = None, search: tuple[str | SearchNode] | None = None, ) -> None: - qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) + assert self.mw.pm.profile is not None + + line_edit = self._line_edit() + qconnect(line_edit.returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) - self.form.searchEdit.lineEdit().setPlaceholderText( - tr.browsing_search_bar_hint() - ) - self.form.searchEdit.lineEdit().setMaxLength(2000000) + line_edit.setPlaceholderText(tr.browsing_search_bar_hint()) + line_edit.setMaxLength(2000000) self.form.searchEdit.addItems( [""] + self.mw.pm.profile.get("searchHistory", []) ) @@ -464,11 +492,11 @@ class Browser(QMainWindow): self._lastSearchTxt = search prompt = search if prompt is None else prompt self.form.searchEdit.setCurrentIndex(-1) - self.form.searchEdit.lineEdit().setText(prompt) + self._line_edit().setText(prompt) self.search() def current_search(self) -> str: - return self.form.searchEdit.lineEdit().text() + return self._line_edit().text() def search(self) -> None: """Search triggered programmatically. Caller must have saved note first.""" @@ -479,6 +507,8 @@ class Browser(QMainWindow): showWarning(str(err)) def update_history(self) -> None: + assert self.mw.pm.profile is not None + sh = self.mw.pm.profile.get("searchHistory", []) if self._lastSearchTxt in sh: sh.remove(self._lastSearchTxt) @@ -524,6 +554,8 @@ class Browser(QMainWindow): # caller must have called editor.saveNow() before calling this or .reset() def begin_reset(self) -> None: + assert self.editor is not None + self.editor.set_note(None, hide=False) self.mw.progress.start() self.table.begin_reset() @@ -573,8 +605,17 @@ class Browser(QMainWindow): # it might differ from the current card self.card = self.table.get_single_selected_card() self.singleCard = bool(self.card) - self.form.splitter.widget(1).setVisible(self.singleCard) + + splitter_widget = self.form.splitter.widget(1) + assert splitter_widget is not None + + splitter_widget.setVisible(self.singleCard) + + assert self.editor is not None + if self.singleCard: + assert self.card is not None + self.editor.set_note(self.card.note(), focusTo=self.focusTo) self.focusTo = None self.editor.card = self.card @@ -740,7 +781,10 @@ class Browser(QMainWindow): def on_create_copy(self) -> None: if note := self.table.get_current_note(): - deck_id = self.table.get_current_card().current_deck_id() + current_card = self.table.get_current_card() + assert current_card is not None + + deck_id = current_card.current_deck_id() aqt.dialogs.open("AddCards", self.mw).set_note(note, deck_id) @no_arg_trigger @@ -761,6 +805,8 @@ class Browser(QMainWindow): ###################################################################### def onTogglePreview(self) -> None: + assert self.editor is not None + if self._previewer: self._previewer.close() elif self.editor.note: @@ -776,6 +822,8 @@ class Browser(QMainWindow): self.onTogglePreview() def toggle_preview_button_state(self, active: bool) -> None: + assert self.editor is not None + if self.editor.web: self.editor.web.eval(f"togglePreviewButtonState({json.dumps(active)});") @@ -801,6 +849,8 @@ class Browser(QMainWindow): if focus != self.form.tableView: return + assert self.editor is not None + self.editor.set_note(None) nids = self.table.to_row_of_unselected_note() remove_notes(parent=self, note_ids=nids).run_in_background() @@ -818,14 +868,24 @@ class Browser(QMainWindow): def set_deck_of_selected_cards(self) -> None: from aqt.studydeck import StudyDeck + assert self.mw.col is not None + assert self.mw.col.db is not None + cids = self.table.get_selected_card_ids() did = self.mw.col.db.scalar("select did from cards where id = ?", cids[0]) - current = self.mw.col.decks.get(did)["name"] + + deck_dict = self.mw.col.decks.get(did) + assert deck_dict is not None + + current = deck_dict["name"] def callback(ret: StudyDeck) -> None: if not ret.name: return did = self.col.decks.id(ret.name) + + assert did is not None + set_card_deck(parent=self, card_ids=cids, deck_id=did).run_in_background() StudyDeck( @@ -1105,10 +1165,14 @@ class Browser(QMainWindow): return self.table.has_next() def onPreviousCard(self) -> None: + assert self.editor is not None + self.focusTo = self.editor.currentField self.editor.call_after_note_saved(self.table.to_previous_row) def onNextCard(self) -> None: + assert self.editor is not None + self.focusTo = self.editor.currentField self.editor.call_after_note_saved(self.table.to_next_row) @@ -1120,11 +1184,19 @@ class Browser(QMainWindow): def onFind(self) -> None: self.form.searchEdit.setFocus() - self.form.searchEdit.lineEdit().selectAll() + self._line_edit().selectAll() def onNote(self) -> None: + assert self.editor is not None + assert self.editor.web is not None + self.editor.web.setFocus() self.editor.loadNote(focusTo=0) def onCardList(self) -> None: self.form.tableView.setFocus() + + def _line_edit(self) -> QLineEdit: + line_edit = self.form.searchEdit.lineEdit() + assert line_edit is not None + return line_edit diff --git a/qt/aqt/browser/card_info.py b/qt/aqt/browser/card_info.py index 75403edc0..6042ea1db 100644 --- a/qt/aqt/browser/card_info.py +++ b/qt/aqt/browser/card_info.py @@ -52,7 +52,9 @@ class CardInfoDialog(QDialog): addCloseShortcut(self) setWindowIcon(self) - self.web = AnkiWebView(kind=AnkiWebViewKind.BROWSER_CARD_INFO) + self.web: AnkiWebView | None = AnkiWebView( + kind=AnkiWebViewKind.BROWSER_CARD_INFO + ) self.web.setVisible(False) self.web.load_sveltekit_page(f"card-info/{card_id}") layout = QVBoxLayout() @@ -76,11 +78,13 @@ class CardInfoDialog(QDialog): extra = "#night" else: extra = "" + assert self.web is not None self.web.eval(f"window.location.href = '/card-info/{card_id}{extra}';") def reject(self) -> None: if self._on_close: self._on_close() + assert self.web is not None self.web.cleanup() self.web = None saveGeom(self, self.GEOMETRY_KEY) diff --git a/qt/aqt/browser/find_and_replace.py b/qt/aqt/browser/find_and_replace.py index c1d51e43c..1dfc9648c 100644 --- a/qt/aqt/browser/find_and_replace.py +++ b/qt/aqt/browser/find_and_replace.py @@ -80,13 +80,16 @@ class FindAndReplaceDialog(QDialog): self._find_history = restore_combo_history( self.form.find, self.COMBO_NAME + "Find" ) - self.form.find.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive) + + find_completer = self.form.find.completer() + assert find_completer is not None + find_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive) self._replace_history = restore_combo_history( self.form.replace, self.COMBO_NAME + "Replace" ) - self.form.replace.completer().setCaseSensitivity( - Qt.CaseSensitivity.CaseSensitive - ) + replace_completer = self.form.replace.completer() + assert replace_completer is not None + replace_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive) if not self.note_ids: # no selected notes to affect @@ -131,10 +134,13 @@ class FindAndReplaceDialog(QDialog): # an empty list means *all* notes self.note_ids = [] + parent_widget = self.parentWidget() + assert parent_widget is not None + # tags? if self.form.field.currentIndex() == 1: op = find_and_replace_tag( - parent=self.parentWidget(), + parent=parent_widget, note_ids=self.note_ids, search=search, replacement=replace, @@ -149,7 +155,7 @@ class FindAndReplaceDialog(QDialog): field = self.field_names[self.form.field.currentIndex()] op = find_and_replace( - parent=self.parentWidget(), + parent=parent_widget, note_ids=self.note_ids, search=search, replacement=replace, diff --git a/qt/aqt/browser/find_duplicates.py b/qt/aqt/browser/find_duplicates.py index 60cd11242..5ffb97ba5 100644 --- a/qt/aqt/browser/find_duplicates.py +++ b/qt/aqt/browser/find_duplicates.py @@ -76,6 +76,9 @@ class FindDuplicatesDialog(QDialog): search = form.buttonBox.addButton( tr.actions_search(), QDialogButtonBox.ButtonRole.ActionRole ) + + assert search is not None + qconnect(search.clicked, on_click) self.show() @@ -87,6 +90,9 @@ class FindDuplicatesDialog(QDialog): self._dupesButton = b = self.form.buttonBox.addButton( tr.browsing_tag_duplicates(), QDialogButtonBox.ButtonRole.ActionRole ) + + assert b is not None + qconnect(b.clicked, self._tag_duplicates) text = "" groups = len(dupes) diff --git a/qt/aqt/browser/layout.py b/qt/aqt/browser/layout.py index 616a68187..4acc7600a 100644 --- a/qt/aqt/browser/layout.py +++ b/qt/aqt/browser/layout.py @@ -1,6 +1,8 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + from enum import Enum from aqt.qt import QEvent, QObject, QSplitter, Qt @@ -19,9 +21,14 @@ class QSplitterHandleEventFilter(QObject): super().__init__(splitter) self._splitter = splitter - def eventFilter(self, object: QObject, event: QEvent) -> bool: + def eventFilter(self, object: QObject | None, event: QEvent | None) -> bool: + assert event is not None + if event.type() == QEvent.Type.MouseButtonDblClick: splitter_parent = self._splitter.parentWidget() + + assert splitter_parent is not None + if self._splitter.orientation() == Qt.Orientation.Horizontal: half_size = splitter_parent.width() // 2 else: diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py index e35623a6f..4c9a97fb8 100644 --- a/qt/aqt/browser/previewer.py +++ b/qt/aqt/browser/previewer.py @@ -23,7 +23,6 @@ from aqt.qt import ( Qt, QTimer, QVBoxLayout, - QWidget, qconnect, ) from aqt.reviewer import replay_audio @@ -43,7 +42,10 @@ class Previewer(QDialog): _show_both_sides = False def __init__( - self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None] + self, + parent: aqt.browser.Browser | None, + mw: AnkiQt, + on_close: Callable[[], None], ) -> None: super().__init__(None, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) @@ -80,7 +82,7 @@ class Previewer(QDialog): self.silentlyClose = True self.vbox = QVBoxLayout() self.vbox.setContentsMargins(0, 0, 0, 0) - self._web = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER) + self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER) self.vbox.addWidget(self._web) self.bbox = QDialogButtonBox() self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight) @@ -90,6 +92,7 @@ class Previewer(QDialog): self._replay = self.bbox.addButton( tr.actions_replay_audio(), QDialogButtonBox.ButtonRole.ActionRole ) + assert self._replay is not None self._replay.setAutoDefault(False) self._replay.setShortcut(QKeySequence("R")) self._replay.setToolTip(tr.actions_shortcut_key(val="R")) @@ -113,20 +116,29 @@ class Previewer(QDialog): self._on_close() def _on_replay_audio(self) -> None: - gui_hooks.audio_will_replay(self._web, self.card(), self._state == "question") + assert self._web is not None + card = self.card() + assert card is not None + + gui_hooks.audio_will_replay(self._web, card, self._state == "question") if self._state == "question": - replay_audio(self.card(), True) + replay_audio(card, True) elif self._state == "answer": - replay_audio(self.card(), False) + replay_audio(card, False) def _on_close(self) -> None: self._open = False self._close_callback() + + assert self._web is not None + self._web.cleanup() self._web = None def _setup_web_view(self) -> None: + assert self._web is not None + self._web.stdHtml( self.mw.reviewer.revHtml(), css=["css/reviewer.css"], @@ -143,7 +155,10 @@ class Previewer(QDialog): def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): - play_clicked_audio(cmd, self.card()) + card = self.card() + assert card is not None + + play_clicked_audio(cmd, card) def _update_flag_and_mark_icons(self, card: Card | None) -> None: if card: @@ -152,6 +167,9 @@ class Previewer(QDialog): else: flag = 0 marked = False + + assert self._web is not None + self._web.eval(f"_drawFlag({flag}); _drawMark({json.dumps(marked)});") def render_card(self) -> None: @@ -210,6 +228,8 @@ class Previewer(QDialog): bodyclass = theme_manager.body_classes_for_card_ord(c.ord) + assert self._web is not None + if c.autoplay(): self._web.setPlaybackRequiresGesture(False) if self._show_both_sides: @@ -239,14 +259,22 @@ class Previewer(QDialog): js = f"{func}({json.dumps(txt)}, {json.dumps(ans_txt)}, '{bodyclass}');" else: js = f"{func}({json.dumps(txt)}, '{bodyclass}');" + + assert self._web is not None self._web.eval(js) self._card_changed = False def _on_show_both_sides(self, toggle: bool) -> None: + assert self._web is not None + self._show_both_sides = toggle self.mw.col.set_config_bool(Config.Bool.PREVIEW_BOTH_SIDES, toggle) + + card = self.card() + assert card is not None + gui_hooks.previewer_will_redraw_after_show_both_sides_toggled( - self._web, self.card(), self._state == "question", toggle + self._web, card, self._state == "question", toggle ) if self._state == "answer" and not toggle: @@ -255,6 +283,9 @@ class Previewer(QDialog): def _state_and_mod(self) -> tuple[str, int, int]: c = self.card() + + assert c is not None + n = c.note() n.load() return (self._state, c.id, n.mod) @@ -278,6 +309,9 @@ class MultiCardPreviewer(Previewer): ">" if self.layoutDirection() == Qt.LayoutDirection.RightToLeft else "<", QDialogButtonBox.ButtonRole.ActionRole, ) + + assert self._prev is not None + self._prev.setAutoDefault(False) self._prev.setShortcut(QKeySequence("Left")) self._prev.setToolTip(tr.qt_misc_shortcut_key_left_arrow()) @@ -286,6 +320,9 @@ class MultiCardPreviewer(Previewer): "<" if self.layoutDirection() == Qt.LayoutDirection.RightToLeft else ">", QDialogButtonBox.ButtonRole.ActionRole, ) + + assert self._next is not None + self._next.setAutoDefault(True) self._next.setShortcut(QKeySequence("Right")) self._next.setToolTip(tr.qt_misc_shortcut_key_right_arrow_or_enter()) @@ -316,6 +353,10 @@ class MultiCardPreviewer(Previewer): def _updateButtons(self) -> None: if not self._open: return + + assert self._prev is not None + assert self._next is not None + self._prev.setEnabled(self._should_enable_prev()) self._next.setEnabled(self._should_enable_next()) @@ -341,6 +382,8 @@ class BrowserPreviewer(MultiCardPreviewer): super().__init__(parent=parent, mw=mw, on_close=on_close) def card(self) -> Card | None: + assert self._parent is not None + if self._parent.singleCard: return self._parent.card else: @@ -356,15 +399,23 @@ class BrowserPreviewer(MultiCardPreviewer): return changed def _on_prev_card(self) -> None: + assert self._parent is not None + self._parent.onPreviousCard() def _on_next_card(self) -> None: + assert self._parent is not None + self._parent.onNextCard() def _should_enable_prev(self) -> bool: + assert self._parent is not None + return super()._should_enable_prev() or self._parent.has_previous_card() def _should_enable_next(self) -> bool: + assert self._parent is not None + return super()._should_enable_next() or self._parent.has_next_card() def _render_scheduled(self) -> None: diff --git a/qt/aqt/browser/sidebar/item.py b/qt/aqt/browser/sidebar/item.py index 8e1f166c8..ce5ccb62f 100644 --- a/qt/aqt/browser/sidebar/item.py +++ b/qt/aqt/browser/sidebar/item.py @@ -153,6 +153,9 @@ class SidebarItem: SidebarItemType.NOTETYPE_TEMPLATE, SidebarItemType.NOTETYPE_FIELD, ]: + assert other._parent_item is not None + assert self._parent_item is not None + return ( other.id == self.id and other._parent_item.id == self._parent_item.id diff --git a/qt/aqt/browser/sidebar/model.py b/qt/aqt/browser/sidebar/model.py index a3c8b41bc..286811aca 100644 --- a/qt/aqt/browser/sidebar/model.py +++ b/qt/aqt/browser/sidebar/model.py @@ -30,6 +30,7 @@ class SidebarModel(QAbstractItemModel): return idx.internalPointer() def index_for_item(self, item: SidebarItem) -> QModelIndex: + assert item._row_in_parent is not None return self.createIndex(item._row_in_parent, 0, item) def search(self, text: str) -> bool: @@ -74,6 +75,7 @@ class SidebarModel(QAbstractItemModel): return QModelIndex() row = parentItem._row_in_parent + assert row is not None return self.createIndex(row, 0, parentItem) diff --git a/qt/aqt/browser/sidebar/searchbar.py b/qt/aqt/browser/sidebar/searchbar.py index b73d90f12..cb56d74af 100644 --- a/qt/aqt/browser/sidebar/searchbar.py +++ b/qt/aqt/browser/sidebar/searchbar.py @@ -29,7 +29,8 @@ class SidebarSearchBar(QLineEdit): def onSearch(self) -> None: self.sidebar.search_for(self.text()) - def keyPressEvent(self, evt: QKeyEvent) -> None: + def keyPressEvent(self, evt: QKeyEvent | None) -> None: + assert evt is not None if evt.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down): self.sidebar.setFocus() elif evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return): diff --git a/qt/aqt/browser/sidebar/toolbar.py b/qt/aqt/browser/sidebar/toolbar.py index 78906b580..0b16c4647 100644 --- a/qt/aqt/browser/sidebar/toolbar.py +++ b/qt/aqt/browser/sidebar/toolbar.py @@ -49,6 +49,7 @@ class SidebarToolbar(QToolBar): action = self.addAction( theme_manager.icon_from_resources(tool[1]), tool[2]() ) + assert action is not None action.setCheckable(True) action.setShortcut(f"Alt+{row + 1}") self._action_group.addAction(action) diff --git a/qt/aqt/browser/sidebar/tree.py b/qt/aqt/browser/sidebar/tree.py index a10cc2ced..6f2029dc1 100644 --- a/qt/aqt/browser/sidebar/tree.py +++ b/qt/aqt/browser/sidebar/tree.py @@ -190,16 +190,19 @@ class SidebarTreeView(QTreeView): self.setUpdatesEnabled(True) # needs to be set after changing model - qconnect(self.selectionModel().selectionChanged, self._on_selection_changed) + qconnect( + self._selection_model().selectionChanged, self._on_selection_changed + ) QueryOp( parent=self.browser, op=lambda _: self._root_tree(), success=on_done ).run_in_background() def restore_current(self, current: SidebarItem) -> None: - if current := self.find_item(current.has_same_id): - index = self.model().index_for_item(current) - self.selectionModel().setCurrentIndex( + if current_item := self.find_item(current.has_same_id): + index = self.model().index_for_item(current_item) + + self._selection_model().setCurrentIndex( index, QItemSelectionModel.SelectionFlag.SelectCurrent ) self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter) @@ -255,7 +258,7 @@ class SidebarTreeView(QTreeView): if item.show_expanded(searching): self.setExpanded(idx, True) if item.is_highlighted() and scroll_to_first_match: - self.selectionModel().setCurrentIndex( + self._selection_model().setCurrentIndex( idx, QItemSelectionModel.SelectionFlag.SelectCurrent, ) @@ -301,17 +304,21 @@ class SidebarTreeView(QTreeView): ########### def drawRow( - self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex + self, painter: QPainter | None, options: QStyleOptionViewItem, idx: QModelIndex ) -> None: if self.current_search and (item := self.model().item_for_index(idx)): if item.is_highlighted(): + assert painter is not None + brush = QBrush(theme_manager.qcolor(colors.HIGHLIGHT_BG)) painter.save() painter.fillRect(options.rect, brush) painter.restore() return super().drawRow(painter, options, idx) - def dropEvent(self, event: QDropEvent) -> None: + def dropEvent(self, event: QDropEvent | None) -> None: + assert event is not None + model = self.model() if qtmajor == 5: pos = event.pos() # type: ignore @@ -321,7 +328,9 @@ class SidebarTreeView(QTreeView): if self.handle_drag_drop(self._selected_items(), target_item): event.acceptProposedAction() - def mouseReleaseEvent(self, event: QMouseEvent) -> None: + def mouseReleaseEvent(self, event: QMouseEvent | None) -> None: + assert event is not None + super().mouseReleaseEvent(event) if ( self.tool == SidebarTool.SEARCH @@ -334,7 +343,9 @@ class SidebarTreeView(QTreeView): if (index := self.currentIndex()) == self.indexAt(pos): self._on_search(index) - def keyPressEvent(self, event: QKeyEvent) -> None: + def keyPressEvent(self, event: QKeyEvent | None) -> None: + assert event is not None + index = self.currentIndex() if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if not self.isPersistentEditorOpen(index): @@ -491,11 +502,9 @@ class SidebarTreeView(QTreeView): ########################### def _root_tree(self) -> SidebarItem: - root: SidebarItem | None = None + root = SidebarItem("", "", item_type=SidebarItemType.ROOT) for stage in SidebarStage: - if stage == SidebarStage.ROOT: - root = SidebarItem("", "", item_type=SidebarItemType.ROOT) handled = gui_hooks.browser_will_build_tree( False, root, stage, self.browser ) @@ -533,6 +542,8 @@ class SidebarTreeView(QTreeView): collapse_key: Config.Bool.V, type: SidebarItemType | None = None, ) -> SidebarItem: + assert type is not None + def update(expanded: bool) -> None: CollectionOp( self.browser, @@ -889,7 +900,7 @@ class SidebarTreeView(QTreeView): def onContextMenu(self, point: QPoint) -> None: index: QModelIndex = self.indexAt(point) item = self.model().item_for_index(index) - if item and self.selectionModel().isSelected(index): + if item and self._selection_model().isSelected(index): self.show_context_menu(item, index) def show_context_menu(self, item: SidebarItem, index: QModelIndex) -> None: @@ -981,6 +992,8 @@ class SidebarTreeView(QTreeView): menu.addAction(tr.actions_search(), lambda: self.update_search(*nodes)) return sub_menu = menu.addMenu(tr.actions_search()) + assert sub_menu is not None + sub_menu.addAction( tr.actions_all_selected(), lambda: self.update_search(*nodes) ) @@ -1223,11 +1236,17 @@ class SidebarTreeView(QTreeView): ) def manage_template(self, item: SidebarItem) -> None: + assert item._parent_item is not None + note = Note(self.col, self.col.models.get(NotetypeId(item._parent_item.id))) CardLayout(self.mw, note, ord=item.id, parent=self, fill_empty=True) def manage_fields(self, item: SidebarItem) -> None: + assert item._parent_item is not None + notetype = self.mw.col.models.get(NotetypeId(item._parent_item.id)) + assert notetype is not None + FieldDialog(self.mw, notetype, parent=self, open_at=item.id) # Helpers @@ -1256,3 +1275,8 @@ class SidebarTreeView(QTreeView): for item in self._selected_items() if item.item_type == SidebarItemType.TAG ] + + def _selection_model(self) -> QItemSelectionModel: + selection_model = self.selectionModel() + assert selection_model is not None + return selection_model diff --git a/qt/aqt/browser/table/__init__.py b/qt/aqt/browser/table/__init__.py index a5a885311..ac3348bbb 100644 --- a/qt/aqt/browser/table/__init__.py +++ b/qt/aqt/browser/table/__init__.py @@ -122,7 +122,7 @@ def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> dict[str, str] | No return adjusted_bg_color(temp_color) -def adjusted_bg_color(color: dict[str, str]) -> dict[str, str]: +def adjusted_bg_color(color: dict[str, str] | None) -> dict[str, str] | None: if color: adjusted_color = copy.copy(color) light = QColor(color["light"]).lighter(150) diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 55f467f53..12a5162d1 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -54,6 +54,7 @@ class DataModel(QAbstractTableModel): 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 + assert aqt.mw is not None self._want_tooltips = aqt.mw.pm.show_browser_table_tooltips() # Row Object Interface diff --git a/qt/aqt/browser/table/state.py b/qt/aqt/browser/table/state.py index 75ee69ccb..8054d2597 100644 --- a/qt/aqt/browser/table/state.py +++ b/qt/aqt/browser/table/state.py @@ -33,11 +33,13 @@ class ItemState(ABC): # Stateless Helpers def note_ids_from_card_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]: + assert self.col.db is not None return self.col.db.list( f"select distinct nid from cards where id in {ids2str(items)}" ) def card_ids_from_note_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]: + assert self.col.db is not None return self.col.db.list(f"select id from cards where nid in {ids2str(items)}") def column_key_at(self, index: int) -> str: diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index d7ba18baa..f3d543d93 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -1,5 +1,6 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + from __future__ import annotations from collections.abc import Callable, Sequence @@ -77,7 +78,7 @@ class Table: return self._len_selection def has_current(self) -> bool: - return self._view.selectionModel().currentIndex().isValid() + return self._selection_model().currentIndex().isValid() def has_previous(self) -> bool: return self.has_current() and self._current().row() > 0 @@ -117,17 +118,19 @@ class Table: # Selecting def select_all(self) -> None: + assert self._view is not None self._view.selectAll() def clear_selection(self) -> None: self._len_selection = 0 self._selected_rows = None - self._view.selectionModel().clear() + self._selection_model().clear() def invert_selection(self) -> None: - selection = self._view.selectionModel().selection() + selection_model = self._selection_model() + selection = selection_model.selection() self.select_all() - self._view.selectionModel().select( + selection_model.select( selection, QItemSelectionModel.SelectionFlag.Deselect | QItemSelectionModel.SelectionFlag.Rows, @@ -139,6 +142,7 @@ class Table: """Try to set the selection to the item corresponding to the given card.""" self._reset_selection() if (row := self._model.get_card_row(card_id)) is not None: + assert self._view is not None self._view.selectRow(row) self._scroll_to_row(row, scroll_even_if_visible) else: @@ -249,7 +253,7 @@ class Table: return nids def clear_current(self) -> None: - self._view.selectionModel().setCurrentIndex( + self._selection_model().setCurrentIndex( QModelIndex(), QItemSelectionModel.SelectionFlag.NoUpdate, ) @@ -260,18 +264,16 @@ class Table: # Helpers def _current(self) -> QModelIndex: - return self._view.selectionModel().currentIndex() + return self._selection_model().currentIndex() def _selected(self) -> list[QModelIndex]: if self._selected_rows is None: - self._selected_rows = self._view.selectionModel().selectedRows() + self._selected_rows = self._selection_model().selectedRows() return self._selected_rows def _set_current(self, row: int, column: int = 0) -> None: - index = self._model.index( - row, self._view.horizontalHeader().logicalIndex(column) - ) - self._view.selectionModel().setCurrentIndex( + index = self._model.index(row, self._horizontal_header().logicalIndex(column)) + self._selection_model().setCurrentIndex( index, QItemSelectionModel.SelectionFlag.NoUpdate, ) @@ -281,7 +283,7 @@ class Table: If no selection change is triggered afterwards, `browser.on_all_or_selected_rows_changed()` and `browser.on_current_row_changed()` must be called. """ - self._view.selectionModel().reset() + self._selection_model().reset() self._len_selection = 0 self._selected_rows = None @@ -292,12 +294,12 @@ class Table: self._model.index(row, 0), self._model.index(row, self._model.len_columns() - 1), ) - self._view.selectionModel().select( + self._selection_model().select( selection, QItemSelectionModel.SelectionFlag.SelectCurrent ) def _set_sort_indicator(self) -> None: - hh = self._view.horizontalHeader() + hh = self._horizontal_header() index = self._model.active_column_index(self._state.sort_column) if index is None: hh.setSortIndicatorShown(False) @@ -312,7 +314,7 @@ class Table: hh.setSortIndicatorShown(True) def _set_column_sizes(self) -> None: - hh = self._view.horizontalHeader() + hh = self._horizontal_header() hh.setSectionResizeMode(QHeaderView.ResizeMode.Interactive) hh.setSectionResizeMode( hh.logicalIndex(self._model.len_columns() - 1), @@ -322,29 +324,32 @@ class Table: hh.setCascadingSectionResizes(False) def _save_header(self) -> None: - saveHeader(self._view.horizontalHeader(), self._state.GEOMETRY_KEY_PREFIX) + saveHeader(self._horizontal_header(), self._state.GEOMETRY_KEY_PREFIX) def _restore_header(self) -> None: - self._view.horizontalHeader().blockSignals(True) - restoreHeader(self._view.horizontalHeader(), self._state.GEOMETRY_KEY_PREFIX) + hh = self._horizontal_header() + hh.blockSignals(True) + restoreHeader(hh, self._state.GEOMETRY_KEY_PREFIX) self._set_column_sizes() self._set_sort_indicator() - self._view.horizontalHeader().blockSignals(False) + hh.blockSignals(False) # Setup def _setup_view(self) -> None: + assert self._view is not None self._view.setSortingEnabled(True) self._view.setModel(self._model) self._view.selectionModel() self._view.setItemDelegate(StatusDelegate(self.browser, self._model)) - qconnect( - self._view.selectionModel().selectionChanged, self._on_selection_changed - ) - qconnect(self._view.selectionModel().currentChanged, self._on_current_changed) + selection_model = self._selection_model() + qconnect(selection_model.selectionChanged, self._on_selection_changed) + qconnect(selection_model.currentChanged, self._on_current_changed) self._view.setWordWrap(False) self._view.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) - self._view.horizontalScrollBar().setSingleStep(10) + horizontal_scroll_bar = self._view.horizontalScrollBar() + assert horizontal_scroll_bar is not None + horizontal_scroll_bar.setSingleStep(10) self._update_font() self._view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) qconnect(self._view.customContextMenuRequested, self._on_context_menu) @@ -358,11 +363,17 @@ class Table: bsize = t.get("bsize", 0) if bsize > curmax: curmax = bsize - self._view.verticalHeader().setDefaultSectionSize(curmax + 6) + + assert self._view is not None + vh = self._view.verticalHeader() + assert vh is not None + vh.setDefaultSectionSize(curmax + 6) def _setup_headers(self) -> None: + assert self._view is not None vh = self._view.verticalHeader() - hh = self._view.horizontalHeader() + assert vh is not None + hh = self._horizontal_header() vh.hide() hh.show() hh.setHighlightSections(False) @@ -397,13 +408,13 @@ class Table: ) // 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._len_selection = len(self._selection_model().selectedRows()) self._selected_rows = None self.browser.on_all_or_selected_rows_changed() def _on_row_state_will_change(self, index: QModelIndex, was_restored: bool) -> None: if not was_restored: - if self._view.selectionModel().isSelected(index): + if self._selection_model().isSelected(index): self._len_selection -= 1 self._selected_rows = None self.browser.on_all_or_selected_rows_changed() @@ -414,7 +425,7 @@ class Table: def _on_row_state_changed(self, index: QModelIndex, was_restored: bool) -> None: if was_restored: - if self._view.selectionModel().isSelected(index): + if self._selection_model().isSelected(index): self._len_selection += 1 self._selected_rows = None self.browser.on_all_or_selected_rows_changed() @@ -445,6 +456,7 @@ class Table: menu.addAction(action) menu.addSeparator() sub_menu = menu.addMenu(other_name) + assert sub_menu is not None for action in other.actions(): sub_menu.addAction(action) gui_hooks.browser_will_show_context_menu(self.browser, menu) @@ -452,11 +464,13 @@ class Table: menu.exec(QCursor.pos()) def _on_header_context(self, pos: QPoint) -> None: + assert self._view is not None gpos = self._view.mapToGlobal(pos) m = QMenu() m.setToolTipsVisible(True) for key, column in self._model.columns.items(): a = m.addAction(self._state.column_label(column)) + assert a is not None a.setCheckable(True) a.setChecked(self._model.active_column_index(key) is not None) a.setToolTip(self._state.column_tooltip(column)) @@ -577,63 +591,91 @@ class Table: def _scroll_to_row(self, row: int, scroll_even_if_visible: bool = False) -> None: """Scroll vertically to row.""" + assert self._view is not None top_border = self._view.rowViewportPosition(row) bottom_border = top_border + self._view.rowHeight(0) - visible = top_border >= 0 and bottom_border < self._view.viewport().height() + viewport = self._view.viewport() + assert viewport is not None + visible = top_border >= 0 and bottom_border < viewport.height() if not visible or scroll_even_if_visible: - horizontal = self._view.horizontalScrollBar().value() + horizontal_scroll_bar = self._view.horizontalScrollBar() + assert horizontal_scroll_bar is not None + horizontal = horizontal_scroll_bar.value() self._view.scrollTo( self._model.index(row, 0), QAbstractItemView.ScrollHint.PositionAtTop ) - self._view.horizontalScrollBar().setValue(horizontal) + horizontal_scroll_bar.setValue(horizontal) def _scroll_to_column(self, column: int) -> None: """Scroll horizontally to column.""" + assert self._view is not None position = self._view.columnViewportPosition(column) - visible = 0 <= position < self._view.viewport().width() + viewport = self._view.viewport() + assert viewport is not None + visible = 0 <= position < viewport.width() if not visible: - vertical = self._view.verticalScrollBar().value() + vertical_scroll_bar = self._view.verticalScrollBar() + assert vertical_scroll_bar is not None + vertical = vertical_scroll_bar.value() self._view.scrollTo( self._model.index(0, column), QAbstractItemView.ScrollHint.PositionAtCenter, ) - self._view.verticalScrollBar().setValue(vertical) + vertical_scroll_bar.setValue(vertical) - def _move_current( - self, - direction: QAbstractItemView.CursorAction, - index: QModelIndex | None = None, - ) -> None: + def _move_current_to_index(self, index: QModelIndex) -> None: if not self.has_current(): return - if index is None: - index = self._view.moveCursor( - direction, - self.browser.mw.app.keyboardModifiers(), - ) + + assert self._view is not None + # Setting current like this avoids a bug with shift-click selection # https://github.com/ankitects/anki/issues/2469 self._view.setCurrentIndex(index) - self._view.selectionModel().select( + self._selection_model().select( index, QItemSelectionModel.SelectionFlag.Clear | QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Rows, ) + def _move_current( + self, + direction: QAbstractItemView.CursorAction, + ) -> None: + assert self._view is not None + index = self._view.moveCursor( + direction, + self.browser.mw.app.keyboardModifiers(), + ) + self._move_current_to_index(index) + def _move_current_to_row(self, row: int) -> None: - old = self._view.selectionModel().currentIndex() - self._move_current(None, self._model.index(row, 0)) + selection_model = self._selection_model() + old = selection_model.currentIndex() + self._move_current_to_index(self._model.index(row, 0)) if not KeyboardModifiersPressed().shift: return - new = self._view.selectionModel().currentIndex() + new = selection_model.currentIndex() selection = QItemSelection(new, old) - self._view.selectionModel().select( + selection_model.select( selection, QItemSelectionModel.SelectionFlag.SelectCurrent | QItemSelectionModel.SelectionFlag.Rows, ) + def _selection_model(self) -> QItemSelectionModel: + assert self._view is not None + selection_model = self._view.selectionModel() + assert selection_model is not None + return selection_model + + def _horizontal_header(self) -> QHeaderView: + assert self._view is not None + hh = self._view.horizontalHeader() + assert hh is not None + return hh + class StatusDelegate(QItemDelegate): def __init__(self, browser: aqt.browser.Browser, model: DataModel) -> None: @@ -641,13 +683,14 @@ class StatusDelegate(QItemDelegate): self._model = model def paint( - self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex + self, painter: QPainter | None, option: QStyleOptionViewItem, index: QModelIndex ) -> None: option.textElideMode = self._model.get_cell(index).elide_mode if self._model.get_cell(index).is_rtl: option.direction = Qt.LayoutDirection.RightToLeft if row_color := self._model.get_row(index).color: brush = QBrush(theme_manager.qcolor(row_color)) + assert painter painter.save() painter.fillRect(option.rect, brush) painter.restore()