mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
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:
parent
d4a3e4828b
commit
931e1d80f2
19 changed files with 359 additions and 119 deletions
28
.mypy.ini
28
.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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue