diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index e503c712b..95fd829c4 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -10,7 +10,7 @@ import os import sys import tempfile import traceback -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast import anki.lang from anki import version as _version @@ -299,7 +299,7 @@ class AnkiApp(QApplication): if not sock.waitForReadyRead(self.TMOUT): sys.stderr.write(sock.errorString()) return - path = bytes(sock.readAll()).decode("utf8") + path = bytes(cast(bytes, sock.readAll())).decode("utf8") self.appMsg.emit(path) # type: ignore sock.disconnectFromServer() diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index e50ac35db..258227996 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -715,7 +715,7 @@ class AddonsDialog(QDialog): gui_hooks.addons_dialog_will_show(self) self.show() - def dragEnterEvent(self, event: QEvent) -> None: + def dragEnterEvent(self, event: QDragEnterEvent) -> None: mime = event.mimeData() if not mime.hasUrls(): return None @@ -724,7 +724,7 @@ class AddonsDialog(QDialog): if all(url.toLocalFile().endswith(ext) for url in urls): event.acceptProposedAction() - def dropEvent(self, event: QEvent) -> None: + def dropEvent(self, event: QDropEvent) -> None: mime = event.mimeData() paths = [] for url in mime.urls(): @@ -908,7 +908,7 @@ class AddonsDialog(QDialog): class GetAddons(QDialog): - def __init__(self, dlg: QDialog) -> None: + def __init__(self, dlg: AddonsDialog) -> None: QDialog.__init__(self, dlg) self.addonsDlg = dlg self.mgr = dlg.mgr @@ -1079,7 +1079,9 @@ class DownloaderInstaller(QObject): self.on_done = on_done - self.mgr.mw.progress.start(immediate=True, parent=self.parent()) + parent = self.parent() + assert isinstance(parent, QWidget) + self.mgr.mw.progress.start(immediate=True, parent=parent) self.mgr.mw.taskman.run_in_background(self._download_all, self._download_done) def _progress_callback(self, up: int, down: int) -> None: @@ -1438,7 +1440,7 @@ def prompt_to_update( class ConfigEditor(QDialog): - def __init__(self, dlg: QDialog, addon: str, conf: Dict) -> None: + def __init__(self, dlg: AddonsDialog, addon: str, conf: Dict) -> None: super().__init__(dlg) self.addon = addon self.conf = conf @@ -1506,7 +1508,7 @@ class ConfigEditor(QDialog): txt = gui_hooks.addon_config_editor_will_save_json(txt) try: new_conf = json.loads(txt) - jsonschema.validate(new_conf, self.parent().mgr._addon_schema(self.addon)) + jsonschema.validate(new_conf, self.mgr._addon_schema(self.addon)) except ValidationError as e: # The user did edit the configuration and entered a value # which can not be interpreted. diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 8ded9eaf3..acd7aa26a 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -35,11 +35,12 @@ from aqt.scheduling_ops import ( suspend_cards, unsuspend_cards, ) -from aqt.sidebar import SidebarSearchBar, SidebarToolbar, SidebarTreeView +from aqt.sidebar import SidebarTreeView from aqt.theme import theme_manager from aqt.utils import ( TR, HelpPage, + KeyboardModifiersPressed, askUser, current_top_level_widget, disable_help_button, @@ -832,7 +833,9 @@ QTableView {{ gridline-color: {grid} }} gui_hooks.editor_did_init_left_buttons.remove(add_preview_button) @ensure_editor_saved - def onRowChanged(self, current: Optional[QItemSelection], previous: Optional[QItemSelection]) -> None: + def onRowChanged( + self, current: Optional[QItemSelection], previous: Optional[QItemSelection] + ) -> None: """Update current note and hide/show editor.""" if self._closeEventHasCleanedUp: return @@ -975,15 +978,13 @@ QTableView {{ gridline-color: {grid} }} self.sidebar = SidebarTreeView(self) self.sidebarTree = self.sidebar # legacy alias dw.setWidget(self.sidebar) - self.sidebar.toolbar = toolbar = SidebarToolbar(self.sidebar) - self.sidebar.searchBar = searchBar = SidebarSearchBar(self.sidebar) qconnect( self.form.actionSidebarFilter.triggered, self.focusSidebarSearchBar, ) grid = QGridLayout() - grid.addWidget(searchBar, 0, 0) - grid.addWidget(toolbar, 0, 1) + grid.addWidget(self.sidebar.searchBar, 0, 0) + grid.addWidget(self.sidebar.toolbar, 0, 1) grid.addWidget(self.sidebar, 1, 0, 1, 2) grid.setContentsMargins(0, 0, 0, 0) grid.setSpacing(0) @@ -1116,10 +1117,7 @@ where id in %s""" def createFilteredDeck(self) -> None: search = self.form.searchEdit.lineEdit().text() - if ( - self.mw.col.schedVer() != 1 - and self.mw.app.keyboardModifiers() & Qt.AltModifier - ): + if self.mw.col.schedVer() != 1 and KeyboardModifiersPressed().alt: aqt.dialogs.open("DynDeckConfDialog", self.mw, search_2=search) else: aqt.dialogs.open("DynDeckConfDialog", self.mw, search=search) @@ -1482,9 +1480,9 @@ where id in %s""" combo = "BrowserFindAndReplace" findhistory = restore_combo_history(frm.find, combo + "Find") - frm.find.completer().setCaseSensitivity(True) + frm.find._completer().setCaseSensitivity(True) replacehistory = restore_combo_history(frm.replace, combo + "Replace") - frm.replace.completer().setCaseSensitivity(True) + frm.replace._completer().setCaseSensitivity(True) restore_is_checked(frm.re, combo + "Regex") restore_is_checked(frm.ignoreCase, combo + "ignoreCase") @@ -1668,7 +1666,7 @@ where id in %s""" sm = self.form.tableView.selectionModel() idx = sm.currentIndex() self._moveCur(None, self.model.index(0, 0)) - if not self.mw.app.keyboardModifiers() & Qt.ShiftModifier: + if not KeyboardModifiersPressed().shift: return idx2 = sm.currentIndex() item = QItemSelection(idx2, idx) @@ -1678,7 +1676,7 @@ where id in %s""" sm = self.form.tableView.selectionModel() idx = sm.currentIndex() self._moveCur(None, self.model.index(len(self.model.cards) - 1, 0)) - if not self.mw.app.keyboardModifiers() & Qt.ShiftModifier: + if not KeyboardModifiersPressed().shift: return idx2 = sm.currentIndex() item = QItemSelection(idx, idx2) @@ -1728,6 +1726,9 @@ class ChangeModel(QDialog): restoreGeom(self, "changeModel") gui_hooks.state_did_reset.append(self.onReset) gui_hooks.current_note_type_did_change.append(self.on_note_type_change) + # ugh - these are set dynamically by rebuildTemplateMap() + self.tcombos: List[QComboBox] = [] + self.fcombos: List[QComboBox] = [] self.exec_() def on_note_type_change(self, notetype: NoteType) -> None: diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 01474cd21..318b942b0 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -795,7 +795,7 @@ class CardLayout(QDialog): showWarning(str(e)) return self.mw.reset() - tooltip(tr(TR.CARD_TEMPLATES_CHANGES_SAVED), parent=self.parent()) + tooltip(tr(TR.CARD_TEMPLATES_CHANGES_SAVED), parent=self.parentWidget()) self.cleanup() gui_hooks.sidebar_should_refresh_notetypes() return QDialog.accept(self) diff --git a/qt/aqt/deck_ops.py b/qt/aqt/deck_ops.py index ff7e3ba98..60a70a49a 100644 --- a/qt/aqt/deck_ops.py +++ b/qt/aqt/deck_ops.py @@ -6,14 +6,14 @@ from __future__ import annotations from typing import Sequence from anki.lang import TR -from aqt import AnkiQt, QDialog +from aqt import AnkiQt, QWidget from aqt.utils import tooltip, tr def remove_decks( *, mw: AnkiQt, - parent: QDialog, + parent: QWidget, deck_ids: Sequence[int], ) -> None: mw.perform_op( diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 2795e7a46..af6ca9cb4 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -162,7 +162,6 @@ class DeckBrowser: ], context=self, ) - self.web.key = "deckBrowser" self._drawButtons() if offset is not None: self._scrollToOffset(offset) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index b484e91ec..130cb96bd 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -34,6 +34,7 @@ from aqt.theme import theme_manager from aqt.utils import ( TR, HelpPage, + KeyboardModifiersPressed, disable_help_button, getFile, openHelp, @@ -753,7 +754,7 @@ class Editor: if m: highest = max(highest, sorted([int(x) for x in m])[-1]) # reuse last? - if not self.mw.app.keyboardModifiers() & Qt.AltModifier: + if not KeyboardModifiersPressed().alt: highest += 1 # must start at 1 highest = max(1, highest) @@ -1130,7 +1131,7 @@ class EditorWebView(AnkiWebView): strip_html = self.editor.mw.col.get_config_bool( Config.Bool.PASTE_STRIPS_FORMATTING ) - if self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier: + if KeyboardModifiersPressed().shift: strip_html = not strip_html return strip_html diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index 51e5ca14b..8ed9e6273 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -4,7 +4,7 @@ import html import re import sys import traceback -from typing import Optional +from typing import Optional, TextIO, cast from markdown import markdown @@ -37,7 +37,7 @@ class ErrorHandler(QObject): qconnect(self.errorTimer, self._setTimer) self.pool = "" self._oldstderr = sys.stderr - sys.stderr = self + sys.stderr = cast(TextIO, self) def unload(self) -> None: sys.stderr = self._oldstderr diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index e19668842..5fb326351 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -26,7 +26,7 @@ from aqt.utils import ( class FieldDialog(QDialog): def __init__( - self, mw: AnkiQt, nt: NoteType, parent: Optional[QDialog] = None + self, mw: AnkiQt, nt: NoteType, parent: Optional[QWidget] = None ) -> None: QDialog.__init__(self, parent or mw) self.mw = mw diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 8c8ff9cd4..d1c0e8bdb 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -72,6 +72,7 @@ from aqt.theme import theme_manager from aqt.utils import ( TR, HelpPage, + KeyboardModifiersPressed, askUser, checkInvalidFilename, current_top_level_widget, @@ -121,7 +122,7 @@ class AnkiQt(QMainWindow): def __init__( self, - app: QApplication, + app: aqt.AnkiApp, profileManager: ProfileManagerType, backend: _RustBackend, opts: Namespace, @@ -138,9 +139,7 @@ class AnkiQt(QMainWindow): self.app = app self.pm = profileManager # init rest of app - self.safeMode = ( - self.app.queryKeyboardModifiers() & Qt.ShiftModifier - ) or self.opts.safemode + self.safeMode = (KeyboardModifiersPressed().shift) or self.opts.safemode try: self.setupUI() self.setupAddons(args) @@ -927,10 +926,8 @@ title="%s" %s>%s""" % ( # force webengine processes to load before cwd is changed if isWin: - for o in self.web, self.bottomWeb: - o.requiresCol = False - o._domReady = False - o._page.setContent(bytes("", "ascii")) + for webview in self.web, self.bottomWeb: + webview.force_load_hack() def closeAllWindows(self, onsuccess: Callable) -> None: aqt.dialogs.closeAll(onsuccess) @@ -1103,8 +1100,7 @@ title="%s" %s>%s""" % ( ("y", self.on_sync_button_clicked), ] self.applyShortcuts(globalShortcuts) - - self.stateShortcuts: Sequence[Tuple[str, Callable]] = [] + self.stateShortcuts: List[QShortcut] = [] def applyShortcuts( self, shortcuts: Sequence[Tuple[str, Callable]] @@ -1281,7 +1277,7 @@ title="%s" %s>%s""" % ( deck = self._selectedDeck() if not deck: return - want_old = self.app.queryKeyboardModifiers() & Qt.ShiftModifier + want_old = KeyboardModifiersPressed().shift if want_old: aqt.dialogs.open("DeckStats", self) else: @@ -1538,13 +1534,14 @@ title="%s" %s>%s""" % ( frm = self.debug_diag_form = aqt.forms.debug.Ui_Dialog() class DebugDialog(QDialog): + silentlyClose = True + def reject(self) -> None: super().reject() saveSplitter(frm.splitter, "DebugConsoleWindow") saveGeom(self, "DebugConsoleWindow") d = self.debugDiag = DebugDialog() - d.silentlyClose = True disable_help_button(d) frm.setupUi(d) restoreGeom(d, "DebugConsoleWindow") @@ -1708,7 +1705,8 @@ title="%s" %s>%s""" % ( if not self.hideMenuAccels: return tgt = tgt or self - for action in tgt.findChildren(QAction): + for action_ in tgt.findChildren(QAction): + action = cast(QAction, action_) txt = str(action.text()) m = re.match(r"^(.+)\(&.+\)(.+)?", txt) if m: @@ -1716,7 +1714,7 @@ title="%s" %s>%s""" % ( def hideStatusTips(self) -> None: for action in self.findChildren(QAction): - action.setStatusTip("") + cast(QAction, action).setStatusTip("") def onMacMinimize(self) -> None: self.setWindowState(self.windowState() | Qt.WindowMinimized) # type: ignore diff --git a/qt/aqt/models.py b/qt/aqt/models.py index e03a08fe3..a893178cc 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -31,7 +31,7 @@ class Models(QDialog): def __init__( self, mw: AnkiQt, - parent: Optional[QDialog] = None, + parent: Optional[QWidget] = None, fromMain: bool = False, selected_notetype_id: Optional[int] = None, ): diff --git a/qt/aqt/note_ops.py b/qt/aqt/note_ops.py index d9861dccd..36592537c 100644 --- a/qt/aqt/note_ops.py +++ b/qt/aqt/note_ops.py @@ -7,9 +7,8 @@ from typing import Callable, Optional, Sequence from anki.lang import TR from anki.notes import Note -from aqt import AnkiQt +from aqt import AnkiQt, QWidget from aqt.main import PerformOpOptionalSuccessCallback -from aqt.qt import QDialog from aqt.utils import show_invalid_search_error, showInfo, tr @@ -52,7 +51,7 @@ def remove_tags( def find_and_replace( *, mw: AnkiQt, - parent: QDialog, + parent: QWidget, note_ids: Sequence[int], search: str, replacement: str, diff --git a/qt/aqt/previewer.py b/qt/aqt/previewer.py index 966b56d32..9cfaed271 100644 --- a/qt/aqt/previewer.py +++ b/qt/aqt/previewer.py @@ -1,11 +1,14 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -# mypy: check-untyped-defs + +from __future__ import annotations + import json import re import time from typing import Any, Callable, Optional, Tuple, Union +import aqt.browser from anki.cards import Card from anki.collection import Config from aqt import AnkiQt, gui_hooks @@ -300,6 +303,12 @@ class MultiCardPreviewer(Previewer): class BrowserPreviewer(MultiCardPreviewer): _last_card_id = 0 + _parent: Optional[aqt.browser.Browser] + + def __init__( + self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None] + ) -> None: + super().__init__(parent=parent, mw=mw, on_close=on_close) def card(self) -> Optional[Card]: if self._parent.singleCard: diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index 947ffc993..6f3eeab70 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -16,7 +16,7 @@ from aqt.utils import TR, disable_help_button, tr class ProgressManager: def __init__(self, mw: aqt.AnkiQt) -> None: self.mw = mw - self.app = QApplication.instance() + self.app = mw.app self.inDB = False self.blockUpdates = False self._show_timer: Optional[QTimer] = None @@ -75,7 +75,7 @@ class ProgressManager: max: int = 0, min: int = 0, label: Optional[str] = None, - parent: Optional[QDialog] = None, + parent: Optional[QWidget] = None, immediate: bool = False, ) -> Optional[ProgressDialog]: self._levels += 1 diff --git a/qt/aqt/scheduling_ops.py b/qt/aqt/scheduling_ops.py index f379cd42b..4d351405c 100644 --- a/qt/aqt/scheduling_ops.py +++ b/qt/aqt/scheduling_ops.py @@ -17,7 +17,7 @@ from aqt.utils import getText, tooltip, tr def set_due_date_dialog( *, mw: aqt.AnkiQt, - parent: QDialog, + parent: QWidget, card_ids: List[int], config_key: Optional[Config.String.Key.V], ) -> None: @@ -51,7 +51,7 @@ def set_due_date_dialog( ) -def forget_cards(*, mw: aqt.AnkiQt, parent: QDialog, card_ids: List[int]) -> None: +def forget_cards(*, mw: aqt.AnkiQt, parent: QWidget, card_ids: List[int]) -> None: if not card_ids: return diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 8017055b5..b4311c736 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -23,6 +23,7 @@ from aqt.qt import * from aqt.theme import ColoredIcon, theme_manager from aqt.utils import ( TR, + KeyboardModifiersPressed, askUser, getOnlyText, show_invalid_search_error, @@ -254,9 +255,7 @@ class SidebarModel(QAbstractItemModel): return QVariant(item.tooltip) return QVariant(theme_manager.icon_from_resources(item.icon)) - def setData( - self, index: QModelIndex, text: QVariant, _role: int = Qt.EditRole - ) -> bool: + def setData(self, index: QModelIndex, text: str, _role: int = Qt.EditRole) -> bool: return self.sidebar._on_rename(index.internalPointer(), text) def supportedDropActions(self) -> Qt.DropActions: @@ -354,6 +353,10 @@ def _want_right_border() -> bool: return not isMac or theme_manager.night_mode +# fixme: we should have a top-level Sidebar class inheriting from QWidget that +# handles the treeview, search bar and so on. Currently the treeview embeds the +# search bar which is wrong, and the layout code is handled in browser.py instead +# of here class SidebarTreeView(QTreeView): def __init__(self, browser: aqt.browser.Browser) -> None: super().__init__() @@ -390,6 +393,10 @@ class SidebarTreeView(QTreeView): self.setStyleSheet("QTreeView { %s }" % ";".join(styles)) + # these do not really belong here, they should be in a higher-level class + self.toolbar = SidebarToolbar(self) + self.searchBar = SidebarSearchBar(self) + @property def tool(self) -> SidebarTool: return self._tool @@ -410,7 +417,7 @@ class SidebarTreeView(QTreeView): self.setExpandsOnDoubleClick(double_click_expands) def model(self) -> SidebarModel: - return super().model() + return cast(SidebarModel, super().model()) # Refreshing ########################### @@ -512,22 +519,22 @@ class SidebarTreeView(QTreeView): joiner: SearchJoiner = "AND", ) -> None: """Modify the current search string based on modifier keys, then refresh.""" - mods = self.mw.app.keyboardModifiers() + mods = KeyboardModifiersPressed() previous = SearchNode(parsable_text=self.browser.current_search()) current = self.mw.col.group_searches(*terms, joiner=joiner) # if Alt pressed, invert - if mods & Qt.AltModifier: + if mods.alt: current = SearchNode(negated=current) try: - if mods & Qt.ControlModifier and mods & Qt.ShiftModifier: + if mods.control and mods.shift: # If Ctrl+Shift, replace searches nodes of the same type. search = self.col.replace_in_search_node(previous, current) - elif mods & Qt.ControlModifier: + elif mods.control: # If Ctrl, AND with previous search = self.col.join_searches(previous, current, "AND") - elif mods & Qt.ShiftModifier: + elif mods.shift: # If Shift, OR with previous search = self.col.join_searches(previous, current, "OR") else: diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index c4532754a..0e6a24a31 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -15,7 +15,7 @@ import wave from abc import ABC, abstractmethod from concurrent.futures import Future from operator import itemgetter -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, cast import aqt from anki import hooks @@ -568,7 +568,7 @@ class QtAudioInputRecorder(Recorder): super().start(on_done) def _on_read_ready(self) -> None: - self._buffer += self._iodevice.readAll() + self._buffer += cast(bytes, self._iodevice.readAll()) def stop(self, on_done: Callable[[str], None]) -> None: def on_stop_timer() -> None: diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index cedac958c..6f5df7161 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -33,9 +33,9 @@ class StudyDeck(QDialog): help: HelpPageArgument = HelpPage.KEYBOARD_SHORTCUTS, current: Optional[str] = None, cancel: bool = True, - parent: Optional[QDialog] = None, + parent: Optional[QWidget] = None, dyn: bool = False, - buttons: Optional[List[str]] = None, + buttons: Optional[List[Union[str, QPushButton]]] = None, geomKey: str = "default", ) -> None: QDialog.__init__(self, parent or mw) @@ -53,8 +53,10 @@ class StudyDeck(QDialog): self.form.buttonBox.button(QDialogButtonBox.Cancel) ) if buttons is not None: - for b in buttons: - self.form.buttonBox.addButton(b, QDialogButtonBox.ActionRole) + for button_or_label in buttons: + self.form.buttonBox.addButton( + button_or_label, QDialogButtonBox.ActionRole + ) else: b = QPushButton(tr(TR.ACTIONS_ADD)) b.setShortcut(QKeySequence("Ctrl+N")) @@ -89,7 +91,7 @@ class StudyDeck(QDialog): self.exec_() def eventFilter(self, obj: QObject, evt: QEvent) -> bool: - if evt.type() == QEvent.KeyPress: + if isinstance(evt, QKeyEvent) and evt.type() == QEvent.KeyPress: new_row = current_row = self.form.list.currentRow() rows_count = self.form.list.count() key = evt.key() @@ -98,7 +100,10 @@ class StudyDeck(QDialog): new_row = current_row - 1 elif key == Qt.Key_Down: new_row = current_row + 1 - elif evt.modifiers() & Qt.ControlModifier and Qt.Key_1 <= key <= Qt.Key_9: + elif ( + int(evt.modifiers()) & Qt.ControlModifier + and Qt.Key_1 <= key <= Qt.Key_9 + ): row_index = key - Qt.Key_1 if row_index < rows_count: new_row = row_index diff --git a/qt/aqt/tagedit.py b/qt/aqt/tagedit.py index 10ac52027..df01f3acf 100644 --- a/qt/aqt/tagedit.py +++ b/qt/aqt/tagedit.py @@ -12,24 +12,24 @@ from aqt.qt import * class TagEdit(QLineEdit): - completer: Union[QCompleter, TagCompleter] + _completer: Union[QCompleter, TagCompleter] lostFocus = pyqtSignal() # 0 = tags, 1 = decks - def __init__(self, parent: QDialog, type: int = 0) -> None: + def __init__(self, parent: QWidget, type: int = 0) -> None: QLineEdit.__init__(self, parent) self.col: Optional[Collection] = None self.model = QStringListModel() self.type = type if type == 0: - self.completer = TagCompleter(self.model, parent, self) + self._completer = TagCompleter(self.model, parent, self) else: - self.completer = QCompleter(self.model, parent) - self.completer.setCompletionMode(QCompleter.PopupCompletion) - self.completer.setCaseSensitivity(Qt.CaseInsensitive) - self.completer.setFilterMode(Qt.MatchContains) - self.setCompleter(self.completer) + self._completer = QCompleter(self.model, parent) + self._completer.setCompletionMode(QCompleter.PopupCompletion) + self._completer.setCaseSensitivity(Qt.CaseInsensitive) + self._completer.setFilterMode(Qt.MatchContains) + self.setCompleter(self._completer) def setCol(self, col: Collection) -> None: "Set the current col, updating list of available tags." @@ -47,29 +47,29 @@ class TagEdit(QLineEdit): def keyPressEvent(self, evt: QKeyEvent) -> None: if evt.key() in (Qt.Key_Up, Qt.Key_Down): # show completer on arrow key up/down - if not self.completer.popup().isVisible(): + if not self._completer.popup().isVisible(): self.showCompleter() return - if evt.key() == Qt.Key_Tab and evt.modifiers() & Qt.ControlModifier: + if evt.key() == Qt.Key_Tab and int(evt.modifiers()) & Qt.ControlModifier: # select next completion - if not self.completer.popup().isVisible(): + if not self._completer.popup().isVisible(): self.showCompleter() - index = self.completer.currentIndex() - self.completer.popup().setCurrentIndex(index) + index = self._completer.currentIndex() + self._completer.popup().setCurrentIndex(index) cur_row = index.row() - if not self.completer.setCurrentRow(cur_row + 1): - self.completer.setCurrentRow(0) + if not self._completer.setCurrentRow(cur_row + 1): + self._completer.setCurrentRow(0) return if ( evt.key() in (Qt.Key_Enter, Qt.Key_Return) - and self.completer.popup().isVisible() + and self._completer.popup().isVisible() ): # apply first completion if no suggestion selected - selected_row = self.completer.popup().currentIndex().row() + selected_row = self._completer.popup().currentIndex().row() if selected_row == -1: - self.completer.setCurrentRow(0) - index = self.completer.currentIndex() - self.completer.popup().setCurrentIndex(index) + self._completer.setCurrentRow(0) + index = self._completer.currentIndex() + self._completer.popup().setCurrentIndex(index) self.hideCompleter() QWidget.keyPressEvent(self, evt) return @@ -90,18 +90,18 @@ class TagEdit(QLineEdit): gui_hooks.tag_editor_did_process_key(self, evt) def showCompleter(self) -> None: - self.completer.setCompletionPrefix(self.text()) - self.completer.complete() + self._completer.setCompletionPrefix(self.text()) + self._completer.complete() def focusOutEvent(self, evt: QFocusEvent) -> None: QLineEdit.focusOutEvent(self, evt) self.lostFocus.emit() # type: ignore - self.completer.popup().hide() + self._completer.popup().hide() def hideCompleter(self) -> None: - if sip.isdeleted(self.completer): + if sip.isdeleted(self._completer): return - self.completer.popup().hide() + self._completer.popup().hide() class TagCompleter(QCompleter): diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py index 15c2e9bab..64459a7f1 100644 --- a/qt/aqt/taglimit.py +++ b/qt/aqt/taglimit.py @@ -15,8 +15,8 @@ class TagLimit(QDialog): self.tags: str = "" self.tags_list: List[str] = [] self.mw = mw - self.parent: Optional[QWidget] = parent - self.deck = self.parent.deck + self.parent_: Optional[CustomStudy] = parent + self.deck = self.parent_.deck self.dialog = aqt.forms.taglimit.Ui_Dialog() self.dialog.setupUi(self) disable_help_button(self) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 7362f6b7c..11b30d011 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -86,12 +86,12 @@ class ThemeManager: else: # specified colours icon = QIcon(path.path) - img = icon.pixmap(16) - painter = QPainter(img) + pixmap = icon.pixmap(16) + painter = QPainter(pixmap) painter.setCompositionMode(QPainter.CompositionMode_SourceIn) - painter.fillRect(img.rect(), QColor(path.current_color(self.night_mode))) + painter.fillRect(pixmap.rect(), QColor(path.current_color(self.night_mode))) painter.end() - icon = QIcon(img) + icon = QIcon(pixmap) return icon return cache.setdefault(path, icon) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index cc393f165..ceb4db073 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -111,7 +111,7 @@ def openHelp(section: HelpPageArgument) -> None: openLink(link) -def openLink(link: str) -> None: +def openLink(link: Union[str, QUrl]) -> None: tooltip(tr(TR.QT_MISC_LOADING), period=1000) with noBundledLibs(): QDesktopServices.openUrl(QUrl(link)) @@ -119,7 +119,7 @@ def openLink(link: str) -> None: def showWarning( text: str, - parent: Optional[QDialog] = None, + parent: Optional[QWidget] = None, help: HelpPageArgument = "", title: str = "Anki", textFormat: Optional[TextFormat] = None, @@ -139,7 +139,7 @@ def showCritical( return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat) -def show_invalid_search_error(err: Exception, parent: Optional[QDialog] = None) -> None: +def show_invalid_search_error(err: Exception, parent: Optional[QWidget] = None) -> None: "Render search errors in markdown, then display a warning." text = str(err) if isinstance(err, InvalidInput): @@ -149,7 +149,7 @@ def show_invalid_search_error(err: Exception, parent: Optional[QDialog] = None) def showInfo( text: str, - parent: Union[Literal[False], QDialog] = False, + parent: Optional[QWidget] = None, help: HelpPageArgument = "", type: str = "info", title: str = "Anki", @@ -158,7 +158,7 @@ def showInfo( ) -> int: "Show a small info window with an OK button." parent_widget: QWidget - if parent is False: + if parent is None: parent_widget = aqt.mw.app.activeWindow() or aqt.mw else: parent_widget = parent @@ -214,6 +214,7 @@ def showText( disable_help_button(diag) layout = QVBoxLayout(diag) diag.setLayout(layout) + text: Union[QPlainTextEdit, QTextBrowser] if plain_text_edit: # used by the importer text = QPlainTextEdit() @@ -222,10 +223,10 @@ def showText( else: text = QTextBrowser() text.setOpenExternalLinks(True) - if type == "text": - text.setPlainText(txt) - else: - text.setHtml(txt) + if type == "text": + text.setPlainText(txt) + else: + text.setHtml(txt) layout.addWidget(text) box = QDialogButtonBox(QDialogButtonBox.Close) layout.addWidget(box) @@ -263,7 +264,7 @@ def showText( def askUser( text: str, - parent: QDialog = None, + parent: QWidget = None, help: HelpPageArgument = None, defaultno: bool = False, msgfunc: Optional[Callable] = None, @@ -296,7 +297,7 @@ class ButtonedDialog(QMessageBox): self, text: str, buttons: List[str], - parent: Optional[QDialog] = None, + parent: Optional[QWidget] = None, help: HelpPageArgument = None, title: str = "Anki", ): @@ -329,7 +330,7 @@ class ButtonedDialog(QMessageBox): def askUserDialog( text: str, buttons: List[str], - parent: Optional[QDialog] = None, + parent: Optional[QWidget] = None, help: HelpPageArgument = None, title: str = "Anki", ) -> ButtonedDialog: @@ -342,7 +343,7 @@ def askUserDialog( class GetTextDialog(QDialog): def __init__( self, - parent: Optional[QDialog], + parent: Optional[QWidget], question: str, help: HelpPageArgument = None, edit: Optional[QLineEdit] = None, @@ -389,7 +390,7 @@ class GetTextDialog(QDialog): def getText( prompt: str, - parent: Optional[QDialog] = None, + parent: Optional[QWidget] = None, help: HelpPageArgument = None, edit: Optional[QLineEdit] = None, default: str = "", @@ -446,7 +447,7 @@ def chooseList( def getTag( - parent: QDialog, deck: Collection, question: str, **kwargs: Any + parent: QWidget, deck: Collection, question: str, **kwargs: Any ) -> Tuple[str, int]: from aqt.tagedit import TagEdit @@ -459,7 +460,8 @@ def getTag( def disable_help_button(widget: QWidget) -> None: "Disable the help button in the window titlebar." - flags = cast(Qt.WindowType, widget.windowFlags() & ~Qt.WindowContextHelpButtonHint) + flags_int = int(widget.windowFlags()) & ~Qt.WindowContextHelpButtonHint + flags = Qt.WindowFlags(flags_int) # type: ignore widget.setWindowFlags(flags) @@ -468,7 +470,7 @@ def disable_help_button(widget: QWidget) -> None: def getFile( - parent: QDialog, + parent: QWidget, title: str, # single file returned unless multi=True cb: Optional[Callable[[Union[str, Sequence[str]]], None]], @@ -548,9 +550,9 @@ def getSaveFile( return file -def saveGeom(widget: QDialog, key: str) -> None: +def saveGeom(widget: QWidget, key: str) -> None: key += "Geom" - if isMac and widget.windowState() & Qt.WindowFullScreen: + if isMac and int(widget.windowState()) & Qt.WindowFullScreen: geom = None else: geom = widget.saveGeometry() @@ -600,12 +602,12 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: widget.move(x, y) -def saveState(widget: QFileDialog, key: str) -> None: +def saveState(widget: Union[QFileDialog, QMainWindow], key: str) -> None: key += "State" aqt.mw.pm.profile[key] = widget.saveState() -def restoreState(widget: Union[aqt.AnkiQt, QFileDialog], key: str) -> None: +def restoreState(widget: Union[QFileDialog, QMainWindow], key: str) -> None: key += "State" if aqt.mw.pm.profile.get(key): widget.restoreState(aqt.mw.pm.profile[key]) @@ -633,12 +635,12 @@ def restoreHeader(widget: QHeaderView, key: str) -> None: widget.restoreState(aqt.mw.pm.profile[key]) -def save_is_checked(widget: QWidget, key: str) -> None: +def save_is_checked(widget: QCheckBox, key: str) -> None: key += "IsChecked" aqt.mw.pm.profile[key] = widget.isChecked() -def restore_is_checked(widget: QWidget, key: str) -> None: +def restore_is_checked(widget: QCheckBox, key: str) -> None: key += "IsChecked" if aqt.mw.pm.profile.get(key) is not None: widget.setChecked(aqt.mw.pm.profile[key]) @@ -719,8 +721,9 @@ def maybeHideClose(bbox: QDialogButtonBox) -> None: def addCloseShortcut(widg: QDialog) -> None: if not isMac: return - widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg) - qconnect(widg._closeShortcut.activated, widg.reject) + shortcut = QShortcut(QKeySequence("Ctrl+W"), widg) + qconnect(shortcut.activated, widg.reject) + setattr(widg, "_closeShortcut", shortcut) def downArrow() -> str: @@ -732,7 +735,7 @@ def downArrow() -> str: def top_level_widget(widget: QWidget) -> QWidget: window = None - while widget := widget.parent(): + while widget := widget.parentWidget(): window = widget return window @@ -754,7 +757,7 @@ _tooltipLabel: Optional[QLabel] = None def tooltip( msg: str, period: int = 3000, - parent: Optional[aqt.AnkiQt] = None, + parent: Optional[QWidget] = None, x_offset: int = 0, y_offset: int = 100, ) -> None: @@ -1015,3 +1018,24 @@ def ensure_editor_saved_on_trigger(func: Callable) -> Callable: into functions connected to a `triggered` signal. """ return pyqtSlot()(ensure_editor_saved(func)) # type: ignore + + +class KeyboardModifiersPressed: + "Util for type-safe checks of currently-pressed modifier keys." + + def __init__(self) -> None: + from aqt import mw + + self._modifiers = int(mw.app.keyboardModifiers()) + + @property + def shift(self) -> bool: + return bool(self._modifiers & Qt.ShiftModifier) + + @property + def control(self) -> bool: + return bool(self._modifiers & Qt.ControlModifier) + + @property + def alt(self) -> bool: + return bool(self._modifiers & Qt.AltModifier) diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index f5543eaeb..642033976 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.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 + import dataclasses import json import re @@ -31,12 +32,15 @@ class AnkiWebPage(QWebEnginePage): def _setupBridge(self) -> None: class Bridge(QObject): + def __init__(self, bridge_handler: Callable[[str], Any]) -> None: + super().__init__() + self.onCmd = bridge_handler + @pyqtSlot(str, result=str) # type: ignore def cmd(self, str: str) -> Any: return json.dumps(self.onCmd(str)) - self._bridge = Bridge() - self._bridge.onCmd = self._onCmd + self._bridge = Bridge(self._onCmd) self._channel = QWebChannel(self) self._channel.registerObject("py", self._bridge) @@ -46,7 +50,7 @@ class AnkiWebPage(QWebEnginePage): jsfile = QFile(qwebchannel) if not jsfile.open(QIODevice.ReadOnly): print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr) - jstext = bytes(jsfile.readAll()).decode("utf-8") + jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8") jsfile.close() script = QWebEngineScript() @@ -131,7 +135,7 @@ class AnkiWebPage(QWebEnginePage): openLink(url) return False - def _onCmd(self, str: str) -> None: + def _onCmd(self, str: str) -> Any: return self._onBridgeCmd(str) def javaScriptAlert(self, url: QUrl, text: str) -> None: @@ -252,7 +256,7 @@ class AnkiWebView(QWebEngineView): # disable pinch to zoom gesture if isinstance(evt, QNativeGestureEvent): return True - elif evt.type() == QEvent.MouseButtonRelease: + elif isinstance(evt, QMouseEvent) and evt.type() == QEvent.MouseButtonRelease: if evt.button() == Qt.MidButton and isLin: self.onMiddleClickPaste() return True @@ -273,7 +277,9 @@ class AnkiWebView(QWebEngineView): w.close() else: # in the main window, removes focus from type in area - self.parent().setFocus() + parent = self.parent() + assert isinstance(parent, QWidget) + parent.setFocus() break w = w.parent() @@ -315,15 +321,16 @@ class AnkiWebView(QWebEngineView): self.set_open_links_externally(True) def _setHtml(self, html: str) -> None: - app = QApplication.instance() - oldFocus = app.focusWidget() + from aqt import mw + + oldFocus = mw.app.focusWidget() self._domDone = False self._page.setHtml(html) # work around webengine stealing focus on setHtml() if oldFocus: oldFocus.setFocus() - def load(self, url: QUrl) -> None: + def load_url(self, url: QUrl) -> None: # allow queuing actions when loading url directly self._domDone = False super().load(url) @@ -641,5 +648,12 @@ document.head.appendChild(style); else: extra = "" self.hide_while_preserving_layout() - self.load(QUrl(f"{mw.serverURL()}_anki/pages/{name}.html{extra}")) + self.load_url(QUrl(f"{mw.serverURL()}_anki/pages/{name}.html{extra}")) self.inject_dynamic_style_and_show() + + def force_load_hack(self) -> None: + """Force process to initialize. + Must be done on Windows prior to changing current working directory.""" + self.requiresCol = False + self._domReady = False + self._page.setContent(bytes("", "ascii"))