diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py index 824606af0..9f2dc2c5e 100644 --- a/aqt/deckbrowser.py +++ b/aqt/deckbrowser.py @@ -24,7 +24,6 @@ class DeckBrowser: clearAudioQueue() self.web.resetHandlers() self.web.onBridgeCmd = self._linkHandler - self.mw.keyHandler = self._keyHandler self._renderPage() def refresh(self): @@ -63,9 +62,6 @@ class DeckBrowser: self._collapse(arg) return False - def _keyHandler(self, evt): - return False - def _selDeck(self, did): self.mw.col.decks.select(did) self.mw.onOverview() diff --git a/aqt/main.py b/aqt/main.py index 06e2cf925..416953449 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -25,7 +25,7 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \ restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \ openHelp, openLink, checkInvalidFilename import anki.db - +import sip class AnkiQt(QMainWindow): def __init__(self, app, profileManager, args): @@ -383,6 +383,7 @@ the manual for information on how to restore from an automatic backup.")) cleanup = getattr(self, "_"+oldState+"Cleanup", None) if cleanup: cleanup(state) + self.clearStateShortcuts() self.state = state runHook('beforeStateChange', state, oldState, *args) getattr(self, "_"+state+"State")(oldState, *args) @@ -512,7 +513,6 @@ title="%s" %s>%s''' % ( tweb = self.toolbarWeb = aqt.webview.AnkiWebView() tweb.title = "top toolbar" tweb.setFocusPolicy(Qt.WheelFocus) - tweb.keyEventDelegate = self.globalKeyHandler self.toolbar = aqt.toolbar.Toolbar(self, tweb) self.toolbar.draw() # main area @@ -520,12 +520,10 @@ title="%s" %s>%s''' % ( self.web.title = "main webview" self.web.setFocusPolicy(Qt.WheelFocus) self.web.setMinimumWidth(400) - self.web.keyEventDelegate = self.globalKeyHandler # bottom area sweb = self.bottomWeb = aqt.webview.AnkiWebView() sweb.title = "bottom toolbar" sweb.setFocusPolicy(Qt.WheelFocus) - sweb.keyEventDelegate = self.globalKeyHandler # add in a layout self.mainLayout = QVBoxLayout() self.mainLayout.setContentsMargins(0,0,0,0) @@ -619,44 +617,39 @@ title="%s" %s>%s''' % ( ########################################################################## def setupKeys(self): - self.keyHandler = None - # debug shortcut - self.debugShortcut = QShortcut(QKeySequence("Ctrl+Shift+;"), self) - self.debugShortcut.activated.connect(self.onDebug) + globalShortcuts = [ + ("Ctrl+Shift+;", self.onDebug), + ("d", lambda: self.moveToState("deckBrowser")), + ("s", self.onStudyKey), + ("a", self.onAddCard), + ("b", self.onBrowse), + ("Shift+s", self.onStats), + ("y", self.onSync) + ] + self.applyShortcuts(globalShortcuts) - def keyPressEvent(self, evt): - if not self.globalKeyHandler(evt): - QMainWindow.keyPressEvent(self, evt) + self.stateShortcuts = [] - # true if we handled key - # called via mw's keyPressEvent() or a webview's event filter - def globalKeyHandler(self, evt): - # do we have a delegate? - if self.keyHandler: - # did it eat the key? - if self.keyHandler(evt): - return True - # check global keys - key = str(evt.text()) - if key == "d": - self.moveToState("deckBrowser") - elif key == "s": - if self.state == "overview": - self.col.startTimebox() - self.moveToState("review") - else: - self.moveToState("overview") - elif key == "a": - self.onAddCard() - elif key == "b": - self.onBrowse() - elif key == "S": - self.onStats() - elif key == "y": - self.onSync() + def applyShortcuts(self, shortcuts): + qshortcuts = [] + for key, fn in shortcuts: + qshortcuts.append(QShortcut(QKeySequence(key), self, activated=fn)) + return qshortcuts + + def setStateShortcuts(self, shortcuts): + self.stateShortcuts = self.applyShortcuts(shortcuts) + + def clearStateShortcuts(self): + for qs in self.stateShortcuts: + sip.delete(qs) + self.stateShortcuts = [] + + def onStudyKey(self): + if self.state == "overview": + self.col.startTimebox() + self.moveToState("review") else: - return False - return True + self.moveToState("overview") # App exit ########################################################################## diff --git a/aqt/overview.py b/aqt/overview.py index 5a25e67cf..8c6e2e2e6 100644 --- a/aqt/overview.py +++ b/aqt/overview.py @@ -19,7 +19,7 @@ class Overview: clearAudioQueue() self.web.resetHandlers() self.web.onBridgeCmd = self._linkHandler - self.mw.keyHandler = self._keyHandler + self.mw.setStateShortcuts(self._shortcutKeys()) self.refresh() def refresh(self): @@ -63,25 +63,35 @@ class Overview: openLink(url) return False - def _keyHandler(self, evt): - cram = self.mw.col.decks.current()['dyn'] - key = str(evt.text()) - if key == "o": - self.mw.onDeckConf() - elif key == "r" and cram: + def _shortcutKeys(self): + return [ + ("o", self.mw.onDeckConf), + ("r", self.onRebuildKey), + ("e", self.onEmptyKey), + ("c", self.onCustomStudyKey), + ("u", self.onUnburyKey) + ] + + def _filteredDeck(self): + return self.mw.col.decks.current()['dyn'] + + def onRebuildKey(self): + if self._filteredDeck(): self.mw.col.sched.rebuildDyn() self.mw.reset() - elif key == "e" and cram: + + def onEmptyKey(self): + if self._filteredDeck(): self.mw.col.sched.emptyDyn(self.mw.col.decks.selected()) self.mw.reset() - elif key == "c" and not cram: + + def onCustomStudyKey(self): + if not self._filteredDeck(): self.onStudyMore() - elif key == "u": - self.mw.col.sched.unburyCardsForDeck() - self.mw.reset() - else: - return False - return True + + def onUnburyKey(self): + self.mw.col.sched.unburyCardsForDeck() + self.mw.reset() # HTML ############################################################ diff --git a/aqt/reviewer.py b/aqt/reviewer.py index 8d133af3b..f27f7b162 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -38,7 +38,7 @@ class Reviewer: def show(self): self.mw.col.reset() self.web.resetHandlers() - self.mw.keyHandler = self._keyHandler + self.mw.setStateShortcuts(self._shortcutKeys()) self.web.onBridgeCmd = self._linkHandler self.bottom.web.onBridgeCmd = self._linkHandler self._reps = None @@ -290,38 +290,33 @@ The front of this card is empty. Please run Tools>Empty Cards.""") # Handlers ############################################################ - def _keyHandler(self, evt): - key = str(evt.text()) - if key == "e": - self.mw.onEditCurrent() - elif (key == " " or evt.key() in (Qt.Key_Return, Qt.Key_Enter)): - if self.state == "question": - self._getTypedAnswer() - elif self.state == "answer": - self._answerCard(self._defaultEase()) - elif key == "r" or evt.key() == Qt.Key_F5: - self.replayAudio() - elif key == "*": - self.onMark() - elif key == "=": - self.onBuryNote() - elif key == "-": - self.onBuryCard() - elif key == "!": - self.onSuspend() - elif key == "@": - self.onSuspendCard() - elif key == "V": - self.onRecordVoice() - elif key == "o": - self.onOptions() - elif key in ("1", "2", "3", "4"): - self._answerCard(int(key)) - elif key == "v": - self.onReplayRecorded() - else: - return False - return True + def _shortcutKeys(self): + return [ + ("e", self.mw.onEditCurrent), + (" ", self.onEnterKey), + (Qt.Key_Return, self.onEnterKey), + (Qt.Key_Enter, self.onEnterKey), + ("r", self.replayAudio), + (Qt.Key_F5, self.replayAudio), + ("*", self.onMark), + ("=", self.onBuryNote), + ("-", self.onBuryCard), + ("!", self.onSuspend), + ("@", self.onSuspendCard), + ("v", self.onReplayRecorded), + ("Shift+v", self.onRecordVoice), + ("o", self.onOptions), + ("1", lambda: self._answerCard(1)), + ("2", lambda: self._answerCard(2)), + ("3", lambda: self._answerCard(3)), + ("4", lambda: self._answerCard(4)), + ] + + def onEnterKey(self): + if self.state == "question": + self._getTypedAnswer() + elif self.state == "answer": + self._answerCard(self._defaultEase()) def _linkHandler(self, url): if url == "ans": diff --git a/aqt/webview.py b/aqt/webview.py index aee62a1fd..d4e3ca9dd 100644 --- a/aqt/webview.py +++ b/aqt/webview.py @@ -67,8 +67,8 @@ class AnkiWebPage(QWebEnginePage): class AnkiWebView(QWebEngineView): - def __init__(self, canFocus=True): - QWebEngineView.__init__(self) + def __init__(self, parent=None): + QWebEngineView.__init__(self, parent=parent) self.title = "default" self._page = AnkiWebPage(self._onBridgeCmd) @@ -80,46 +80,32 @@ class AnkiWebView(QWebEngineView): self._page.profile().setHttpCacheType(QWebEngineProfile.MemoryHttpCache) self.resetHandlers() self.allowDrops = False - self.setCanFocus(canFocus) - self.installEventFilter(self) + QShortcut(QKeySequence("Esc"), self, + context=Qt.WidgetWithChildrenShortcut, activated=self.onEsc) + if isMac: + for key, fn in [ + (QKeySequence.Copy, self.onCopy), + (QKeySequence.Paste, self.onPaste), + (QKeySequence.Cut, self.onCut), + (QKeySequence.SelectAll, self.onSelectAll), + ]: + QShortcut(key, self, + context=Qt.WidgetWithChildrenShortcut, + activated=fn) - def eventFilter(self, obj, evt): - if not isinstance(evt, QKeyEvent) or obj != self: - return False - if evt.matches(QKeySequence.Copy) and isMac: - self.onCopy() - return True - if evt.matches(QKeySequence.Cut) and isMac: - self.onCut() - return True - if evt.matches(QKeySequence.Paste) and isMac: - self.onPaste() - return True - if evt.matches(QKeySequence.SelectAll): - self.triggerPageAction(QWebEnginePage.SelectAll) - return False - if evt.key() == Qt.Key_Escape: - # cheap hack to work around webengine swallowing escape key that - # usually closes dialogs - w = self.parent() - while w: - if isinstance(w, QDialog) or isinstance(w, QMainWindow): - from aqt import mw - if w != mw: - w.close() - else: - self.parent().setFocus() - break - w = w.parent() - return True - - if self.keyEventDelegate: - ret = self.keyEventDelegate(evt) - if ret is None: - raise Exception("add-ons that modify key handlers should make sure true/false is returned") - return ret - - return False + def onEsc(self): + w = self.parent() + while w: + if isinstance(w, QDialog) or isinstance(w, QMainWindow): + from aqt import mw + # esc in a child window closes the window + if w != mw: + w.close() + else: + # in the main window, removes focus from type in area + self.parent().setFocus() + break + w = w.parent() def onCopy(self): self.triggerPageAction(QWebEnginePage.Copy) @@ -130,12 +116,13 @@ class AnkiWebView(QWebEngineView): def onPaste(self): self.triggerPageAction(QWebEnginePage.Paste) + def onSelectAll(self): + self.triggerPageAction(QWebEnginePage.SelectAll) + def contextMenuEvent(self, evt): - if not self._canFocus: - return m = QMenu(self) a = m.addAction(_("Copy")) - a.triggered.connect(lambda: self.triggerPageAction(QWebEnginePage.Copy)) + a.triggered.connect(self.onCopy) runHook("AnkiWebView.contextMenuEvent", self, m) m.popup(QCursor.pos()) @@ -209,13 +196,6 @@ document.addEventListener("keydown", function(evt) { css, js or anki.js.jquery+anki.js.browserSel, head, bodyClass, body)) - def setCanFocus(self, canFocus=False): - self._canFocus = canFocus - if self._canFocus: - self.setFocusPolicy(Qt.WheelFocus) - else: - self.setFocusPolicy(Qt.NoFocus) - def eval(self, js): self.page().runJavaScript(js)