Enable strict_optional in aqt/. and aqt/browser (#3486)

* Boolean naming convention

* Rename no_strict_optional -> strict_optional

* Update CONTRIBUTORS

* Enable strict optional for aqt module

* Fix errors

* Enable strict optional for aqt browser

* Fix layout.py errors

* Fix find_duplicates.py errors

* Fix browser.py errors

* Revert a0 a1 names

* Fix tree.py errors

* Fix previewer.py errors

* Fix model.py errors

* Fix find_and_replace.py errors

* Fix item.py errors

* Fix toolbar.py errors

* Fix table/__init__.py errors

* Fix model.py errors

* Fix state.py errors

* Fix table.py errors

* Fix errors in card_info.py

* Fix searchbar.py errors

* Fix name

* Fix assert in browser.py

* Formatting

* Fix assert vh

* assert is not None instead of truthy

* Split _move_current() up to correct type signature (dae)

We want either index or direction, but not both.
This commit is contained in:
Ben Nguyen 2024-10-11 06:12:48 -07:00 committed by GitHub
parent d4a3e4828b
commit 931e1d80f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 359 additions and 119 deletions

View file

@ -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

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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()