From 34dcf64d760d01d3f3e4f7aa4a6738b725e53afa Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 22 Jun 2017 16:36:54 +1000 Subject: [PATCH] another attempt at fixing key handling we can't use an event filter on the top level webview, because it ignores the return value of the filter and leads to Anki thinking keys have been pressed twice and if we use an event filter on the focusProxy(), the keypress/release events are sent even when a text field is currently focused, leading to shortcuts being triggered when typing in the answer to solve this, we move away from handling the key press events directly, and instead install shortcuts for the events we want to trigger. in addition to the global shortcuts, each state can install its own shortcuts, which we remove when transitioning to a new state also remove the unused canFocus argument to ankiwebview, and accept a parent argument as required by the code in forms/ --- aqt/deckbrowser.py | 4 --- aqt/main.py | 71 ++++++++++++++++++--------------------- aqt/overview.py | 40 +++++++++++++--------- aqt/reviewer.py | 61 ++++++++++++++++------------------ aqt/webview.py | 82 ++++++++++++++++++---------------------------- 5 files changed, 116 insertions(+), 142 deletions(-) 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)