mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
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:
parent
22f2fdf7d6
commit
34dcf64d76
5 changed files with 116 additions and 142 deletions
|
@ -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()
|
||||||
|
|
71
aqt/main.py
71
aqt/main.py
|
@ -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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -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
|
||||||
############################################################
|
############################################################
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue