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/
This commit is contained in:
Damien Elmes 2017-06-22 16:36:54 +10:00
parent 22f2fdf7d6
commit 34dcf64d76
5 changed files with 116 additions and 142 deletions

View file

@ -24,7 +24,6 @@ class DeckBrowser:
clearAudioQueue() clearAudioQueue()
self.web.resetHandlers() self.web.resetHandlers()
self.web.onBridgeCmd = self._linkHandler self.web.onBridgeCmd = self._linkHandler
self.mw.keyHandler = self._keyHandler
self._renderPage() self._renderPage()
def refresh(self): def refresh(self):
@ -63,9 +62,6 @@ class DeckBrowser:
self._collapse(arg) self._collapse(arg)
return False return False
def _keyHandler(self, evt):
return False
def _selDeck(self, did): def _selDeck(self, did):
self.mw.col.decks.select(did) self.mw.col.decks.select(did)
self.mw.onOverview() self.mw.onOverview()

View file

@ -25,7 +25,7 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \ restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \
openHelp, openLink, checkInvalidFilename openHelp, openLink, checkInvalidFilename
import anki.db import anki.db
import sip
class AnkiQt(QMainWindow): class AnkiQt(QMainWindow):
def __init__(self, app, profileManager, args): 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) cleanup = getattr(self, "_"+oldState+"Cleanup", None)
if cleanup: if cleanup:
cleanup(state) cleanup(state)
self.clearStateShortcuts()
self.state = state self.state = state
runHook('beforeStateChange', state, oldState, *args) runHook('beforeStateChange', state, oldState, *args)
getattr(self, "_"+state+"State")(oldState, *args) getattr(self, "_"+state+"State")(oldState, *args)
@ -512,7 +513,6 @@ title="%s" %s>%s</button>''' % (
tweb = self.toolbarWeb = aqt.webview.AnkiWebView() tweb = self.toolbarWeb = aqt.webview.AnkiWebView()
tweb.title = "top toolbar" tweb.title = "top toolbar"
tweb.setFocusPolicy(Qt.WheelFocus) tweb.setFocusPolicy(Qt.WheelFocus)
tweb.keyEventDelegate = self.globalKeyHandler
self.toolbar = aqt.toolbar.Toolbar(self, tweb) self.toolbar = aqt.toolbar.Toolbar(self, tweb)
self.toolbar.draw() self.toolbar.draw()
# main area # main area
@ -520,12 +520,10 @@ title="%s" %s>%s</button>''' % (
self.web.title = "main webview" self.web.title = "main webview"
self.web.setFocusPolicy(Qt.WheelFocus) self.web.setFocusPolicy(Qt.WheelFocus)
self.web.setMinimumWidth(400) self.web.setMinimumWidth(400)
self.web.keyEventDelegate = self.globalKeyHandler
# bottom area # bottom area
sweb = self.bottomWeb = aqt.webview.AnkiWebView() sweb = self.bottomWeb = aqt.webview.AnkiWebView()
sweb.title = "bottom toolbar" sweb.title = "bottom toolbar"
sweb.setFocusPolicy(Qt.WheelFocus) sweb.setFocusPolicy(Qt.WheelFocus)
sweb.keyEventDelegate = self.globalKeyHandler
# add in a layout # add in a layout
self.mainLayout = QVBoxLayout() self.mainLayout = QVBoxLayout()
self.mainLayout.setContentsMargins(0,0,0,0) self.mainLayout.setContentsMargins(0,0,0,0)
@ -619,44 +617,39 @@ title="%s" %s>%s</button>''' % (
########################################################################## ##########################################################################
def setupKeys(self): def setupKeys(self):
self.keyHandler = None globalShortcuts = [
# debug shortcut ("Ctrl+Shift+;", self.onDebug),
self.debugShortcut = QShortcut(QKeySequence("Ctrl+Shift+;"), self) ("d", lambda: self.moveToState("deckBrowser")),
self.debugShortcut.activated.connect(self.onDebug) ("s", self.onStudyKey),
("a", self.onAddCard),
("b", self.onBrowse),
("Shift+s", self.onStats),
("y", self.onSync)
]
self.applyShortcuts(globalShortcuts)
def keyPressEvent(self, evt): self.stateShortcuts = []
if not self.globalKeyHandler(evt):
QMainWindow.keyPressEvent(self, evt)
# true if we handled key def applyShortcuts(self, shortcuts):
# called via mw's keyPressEvent() or a webview's event filter qshortcuts = []
def globalKeyHandler(self, evt): for key, fn in shortcuts:
# do we have a delegate? qshortcuts.append(QShortcut(QKeySequence(key), self, activated=fn))
if self.keyHandler: return qshortcuts
# did it eat the key?
if self.keyHandler(evt): def setStateShortcuts(self, shortcuts):
return True self.stateShortcuts = self.applyShortcuts(shortcuts)
# check global keys
key = str(evt.text()) def clearStateShortcuts(self):
if key == "d": for qs in self.stateShortcuts:
self.moveToState("deckBrowser") sip.delete(qs)
elif key == "s": self.stateShortcuts = []
if self.state == "overview":
self.col.startTimebox() def onStudyKey(self):
self.moveToState("review") if self.state == "overview":
else: self.col.startTimebox()
self.moveToState("overview") self.moveToState("review")
elif key == "a":
self.onAddCard()
elif key == "b":
self.onBrowse()
elif key == "S":
self.onStats()
elif key == "y":
self.onSync()
else: else:
return False self.moveToState("overview")
return True
# App exit # App exit
########################################################################## ##########################################################################

View file

@ -19,7 +19,7 @@ class Overview:
clearAudioQueue() clearAudioQueue()
self.web.resetHandlers() self.web.resetHandlers()
self.web.onBridgeCmd = self._linkHandler self.web.onBridgeCmd = self._linkHandler
self.mw.keyHandler = self._keyHandler self.mw.setStateShortcuts(self._shortcutKeys())
self.refresh() self.refresh()
def refresh(self): def refresh(self):
@ -63,25 +63,35 @@ class Overview:
openLink(url) openLink(url)
return False return False
def _keyHandler(self, evt): def _shortcutKeys(self):
cram = self.mw.col.decks.current()['dyn'] return [
key = str(evt.text()) ("o", self.mw.onDeckConf),
if key == "o": ("r", self.onRebuildKey),
self.mw.onDeckConf() ("e", self.onEmptyKey),
elif key == "r" and cram: ("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.col.sched.rebuildDyn()
self.mw.reset() 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.col.sched.emptyDyn(self.mw.col.decks.selected())
self.mw.reset() self.mw.reset()
elif key == "c" and not cram:
def onCustomStudyKey(self):
if not self._filteredDeck():
self.onStudyMore() self.onStudyMore()
elif key == "u":
self.mw.col.sched.unburyCardsForDeck() def onUnburyKey(self):
self.mw.reset() self.mw.col.sched.unburyCardsForDeck()
else: self.mw.reset()
return False
return True
# HTML # HTML
############################################################ ############################################################

View file

@ -38,7 +38,7 @@ class Reviewer:
def show(self): def show(self):
self.mw.col.reset() self.mw.col.reset()
self.web.resetHandlers() self.web.resetHandlers()
self.mw.keyHandler = self._keyHandler self.mw.setStateShortcuts(self._shortcutKeys())
self.web.onBridgeCmd = self._linkHandler self.web.onBridgeCmd = self._linkHandler
self.bottom.web.onBridgeCmd = self._linkHandler self.bottom.web.onBridgeCmd = self._linkHandler
self._reps = None self._reps = None
@ -290,38 +290,33 @@ The front of this card is empty. Please run Tools>Empty Cards.""")
# Handlers # Handlers
############################################################ ############################################################
def _keyHandler(self, evt): def _shortcutKeys(self):
key = str(evt.text()) return [
if key == "e": ("e", self.mw.onEditCurrent),
self.mw.onEditCurrent() (" ", self.onEnterKey),
elif (key == " " or evt.key() in (Qt.Key_Return, Qt.Key_Enter)): (Qt.Key_Return, self.onEnterKey),
if self.state == "question": (Qt.Key_Enter, self.onEnterKey),
self._getTypedAnswer() ("r", self.replayAudio),
elif self.state == "answer": (Qt.Key_F5, self.replayAudio),
self._answerCard(self._defaultEase()) ("*", self.onMark),
elif key == "r" or evt.key() == Qt.Key_F5: ("=", self.onBuryNote),
self.replayAudio() ("-", self.onBuryCard),
elif key == "*": ("!", self.onSuspend),
self.onMark() ("@", self.onSuspendCard),
elif key == "=": ("v", self.onReplayRecorded),
self.onBuryNote() ("Shift+v", self.onRecordVoice),
elif key == "-": ("o", self.onOptions),
self.onBuryCard() ("1", lambda: self._answerCard(1)),
elif key == "!": ("2", lambda: self._answerCard(2)),
self.onSuspend() ("3", lambda: self._answerCard(3)),
elif key == "@": ("4", lambda: self._answerCard(4)),
self.onSuspendCard() ]
elif key == "V":
self.onRecordVoice() def onEnterKey(self):
elif key == "o": if self.state == "question":
self.onOptions() self._getTypedAnswer()
elif key in ("1", "2", "3", "4"): elif self.state == "answer":
self._answerCard(int(key)) self._answerCard(self._defaultEase())
elif key == "v":
self.onReplayRecorded()
else:
return False
return True
def _linkHandler(self, url): def _linkHandler(self, url):
if url == "ans": if url == "ans":

View file

@ -67,8 +67,8 @@ class AnkiWebPage(QWebEnginePage):
class AnkiWebView(QWebEngineView): class AnkiWebView(QWebEngineView):
def __init__(self, canFocus=True): def __init__(self, parent=None):
QWebEngineView.__init__(self) QWebEngineView.__init__(self, parent=parent)
self.title = "default" self.title = "default"
self._page = AnkiWebPage(self._onBridgeCmd) self._page = AnkiWebPage(self._onBridgeCmd)
@ -80,46 +80,32 @@ class AnkiWebView(QWebEngineView):
self._page.profile().setHttpCacheType(QWebEngineProfile.MemoryHttpCache) self._page.profile().setHttpCacheType(QWebEngineProfile.MemoryHttpCache)
self.resetHandlers() self.resetHandlers()
self.allowDrops = False self.allowDrops = False
self.setCanFocus(canFocus) QShortcut(QKeySequence("Esc"), self,
self.installEventFilter(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): def onEsc(self):
if not isinstance(evt, QKeyEvent) or obj != self: w = self.parent()
return False while w:
if evt.matches(QKeySequence.Copy) and isMac: if isinstance(w, QDialog) or isinstance(w, QMainWindow):
self.onCopy() from aqt import mw
return True # esc in a child window closes the window
if evt.matches(QKeySequence.Cut) and isMac: if w != mw:
self.onCut() w.close()
return True else:
if evt.matches(QKeySequence.Paste) and isMac: # in the main window, removes focus from type in area
self.onPaste() self.parent().setFocus()
return True break
if evt.matches(QKeySequence.SelectAll): w = w.parent()
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 onCopy(self): def onCopy(self):
self.triggerPageAction(QWebEnginePage.Copy) self.triggerPageAction(QWebEnginePage.Copy)
@ -130,12 +116,13 @@ class AnkiWebView(QWebEngineView):
def onPaste(self): def onPaste(self):
self.triggerPageAction(QWebEnginePage.Paste) self.triggerPageAction(QWebEnginePage.Paste)
def onSelectAll(self):
self.triggerPageAction(QWebEnginePage.SelectAll)
def contextMenuEvent(self, evt): def contextMenuEvent(self, evt):
if not self._canFocus:
return
m = QMenu(self) m = QMenu(self)
a = m.addAction(_("Copy")) a = m.addAction(_("Copy"))
a.triggered.connect(lambda: self.triggerPageAction(QWebEnginePage.Copy)) a.triggered.connect(self.onCopy)
runHook("AnkiWebView.contextMenuEvent", self, m) runHook("AnkiWebView.contextMenuEvent", self, m)
m.popup(QCursor.pos()) m.popup(QCursor.pos())
@ -209,13 +196,6 @@ document.addEventListener("keydown", function(evt) {
css, js or anki.js.jquery+anki.js.browserSel, css, js or anki.js.jquery+anki.js.browserSel,
head, bodyClass, body)) 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): def eval(self, js):
self.page().runJavaScript(js) self.page().runJavaScript(js)