refactor editor focus handling

this fixes a bug where navigating to the next/previous card using
shortcut keys resulted in the first field being clobbered

- get rid of the stealFocus option in favour of explicitly passing
focusTo to setNote()
- setFields() is no longer responsible for setting focus
- add focusTo var to the browser so that the row changed hook can
restore focus when navigating to next/previous card
- fix the row changed hook being called twice
- the blur event now includes the field number instead of relying on the
 editor to have the correct currentField
- the current field is set to null on blur
- use deferred js and a callback rather than keeping track of when we
were loaded
- add shift+tab shortcut to go to previous field
This commit is contained in:
Damien Elmes 2017-08-05 15:15:19 +10:00
parent 96938e583a
commit 797a7ea229
5 changed files with 82 additions and 99 deletions

View file

@ -34,7 +34,8 @@ class AddCards(QDialog):
addHook('currentModelChanged', self.onModelChange)
addCloseShortcut(self)
self.show()
self.setupNewNote()
n = self.mw.col.newNote()
self.setAndFocusNote(n)
def setupEditor(self):
self.editor = aqt.editor.Editor(
@ -79,15 +80,12 @@ class AddCards(QDialog):
b.setEnabled(False)
self.historyButton = b
def setupNewNote(self, set=True):
f = self.mw.col.newNote()
if set:
self.editor.setNote(f, focus=True)
return f
def setAndFocusNote(self, note):
self.editor.setNote(note, focusTo=0)
def onModelChange(self):
oldNote = self.editor.note
note = self.setupNewNote(set=False)
note = self.mw.col.newNote()
if oldNote:
oldFields = list(oldNote.keys())
newFields = list(note.keys())
@ -107,12 +105,11 @@ class AddCards(QDialog):
except IndexError:
pass
self.removeTempNote(oldNote)
self.editor.currentField = 0
self.editor.setNote(note, focus=True)
self.editor.setNote(note)
def onReset(self, model=None, keep=False):
oldNote = self.editor.note
note = self.setupNewNote(set=False)
note = self.mw.col.newNote()
flds = note.model()['flds']
# copy fields from old note
if oldNote:
@ -126,8 +123,7 @@ class AddCards(QDialog):
note.fields[n] = ""
except IndexError:
break
self.editor.currentField = 0
self.editor.setNote(note, focus=True)
self.setAndFocusNote(note)
def removeTempNote(self, note):
if not note or not note.id:

View file

@ -359,6 +359,7 @@ class Browser(QMainWindow):
self.col = self.mw.col
self.forceClose = False
self.lastFilter = ""
self.focusTo = None
self._previewWindow = None
self._closeEventHasCleanedUp = False
self.form = aqt.forms.browser.Ui_Dialog()
@ -611,7 +612,6 @@ class Browser(QMainWindow):
def setupEditor(self):
self.editor = aqt.editor.Editor(
self.mw, self.form.fieldsArea, self)
self.editor.stealFocus = False
def onRowChanged(self, current, previous):
"Update current note and hide/show editor."
@ -627,7 +627,8 @@ class Browser(QMainWindow):
else:
self.card = self.model.getCard(
self.form.tableView.selectionModel().currentIndex())
self.editor.setNote(self.card.note(reload=True))
self.editor.setNote(self.card.note(reload=True), focusTo=self.focusTo)
self.focusTo = None
self.editor.card = self.card
self.singleCard = True
self._renderPreview(True)
@ -1517,34 +1518,25 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
tv = self.form.tableView
if idx is None:
idx = tv.moveCursor(dir, self.mw.app.keyboardModifiers())
tv.selectionModel().clear()
tv.setCurrentIndex(idx)
tv.selectionModel().setCurrentIndex(
idx,
QItemSelectionModel.Clear|
QItemSelectionModel.Select|
QItemSelectionModel.Rows)
def onPreviousCard(self):
self.focusTo = self.editor.currentField
self.editor.saveNow(self._onPreviousCard)
def _onPreviousCard(self):
tagfocus = self.editor.tags.hasFocus()
f = self.editor.currentField
self._moveCur(QAbstractItemView.MoveUp)
if tagfocus:
self.editor.tags.setFocus()
return
self.editor.web.setFocus()
self.editor.web.eval("focusField(%d)" % f)
def onNextCard(self):
self.focusTo = self.editor.currentField
self.editor.saveNow(self._onNextCard)
def _onNextCard(self):
tagfocus = self.editor.tags.hasFocus()
f = self.editor.currentField
self._moveCur(QAbstractItemView.MoveDown)
if tagfocus:
self.editor.tags.setFocus()
return
self.editor.web.setFocus()
self.editor.web.eval("focusField(%d)" % f)
def onFirstCard(self):
sm = self.form.tableView.selectionModel()
@ -1574,7 +1566,6 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
self.form.searchEdit.lineEdit().selectAll()
def onNote(self):
self.editor.focus()
self.editor.web.setFocus()
self.editor.web.eval("focusField(0);")

View file

@ -26,7 +26,7 @@ class EditCurrent(QDialog):
self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut(
QKeySequence("Ctrl+Return"))
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
self.editor.setNote(self.mw.reviewer.card.note())
self.editor.setNote(self.mw.reviewer.card.note(), focusTo=0)
restoreGeom(self, "editcurrent")
addHook("reset", self.onReset)
self.mw.requireReset()

View file

@ -42,10 +42,8 @@ class Editor:
self.widget = widget
self.parentWindow = parentWindow
self.note = None
self.stealFocus = True
self.addMode = addMode
self._loaded = False
self.currentField = 0
self.currentField = None
# current card, for card layout
self.card = None
self.setupOuter()
@ -69,7 +67,6 @@ class Editor:
self.web.allowDrops = True
self.web.onBridgeCmd = self.onBridgeCmd
self.outerLayout.addWidget(self.web, 1)
self.web.onLoadFinished = self._loadFinished
righttopbtns = list()
righttopbtns.append(self._addButton('text_bold', 'bold', "Bold text (Ctrl+B)", id='bold'))
@ -158,7 +155,7 @@ class Editor:
("Ctrl+T, E", self.insertLatexEqn),
("Ctrl+T, M", self.insertLatexMathEnv),
("Ctrl+Shift+X", self.onHtmlEdit),
("Ctrl+Shift+T", lambda: self.tags.setFocus),
("Ctrl+Shift+T", self.onFocusTags)
]
runFilter("setupEditorShortcuts", cuts)
for keys, fn in cuts:
@ -194,7 +191,8 @@ class Editor:
return
# focus lost or key/button pressed?
if cmd.startswith("blur") or cmd.startswith("key"):
(type, txt) = cmd.split(":", 1)
(type, ord, txt) = cmd.split(":", 2)
ord = int(ord)
txt = urllib.parse.unquote(txt)
txt = unicodedata.normalize("NFC", txt)
txt = self.mungeHTML(txt)
@ -202,22 +200,18 @@ class Editor:
txt = txt.replace("\x00", "")
# reverse the url quoting we added to get images to display
txt = self.mw.col.media.escapeImages(txt, unescape=True)
self.note.fields[self.currentField] = txt
self.note.fields[ord] = txt
if not self.addMode:
self.note.flush()
self.mw.requireReset()
if type == "blur":
self.currentField = None
# run any filters
if runFilter(
"editFocusLost", False, self.note, self.currentField):
# something updated the note; schedule reload
def onUpdate():
if not self.note:
return
self.stealFocus = True
self.loadNote()
self.checkValid()
self.mw.progress.timer(100, onUpdate, False)
"editFocusLost", False, self.note, ord):
# something updated the note; update it after a subsequent focus
# event has had time to fire
self.mw.progress.timer(100, self.loadNote, False)
else:
self.checkValid()
else:
@ -241,58 +235,42 @@ class Editor:
# Setting/unsetting the current note
######################################################################
def _loadFinished(self):
self._loaded = True
# setup colour button
self.setupForegroundButton()
if self.note:
self.loadNote()
def setNote(self, note, hide=True, focus=False):
def setNote(self, note, hide=True, focusTo=None):
"Make NOTE the current note."
self.note = note
self.currentField = 0
if focus:
self.stealFocus = True
self.currentField = None
if self.note:
self.loadNote()
self.loadNote(focusTo=focusTo)
else:
self.hideCompleters()
if hide:
self.widget.hide()
def loadNote(self):
def loadNote(self, focusTo=None):
if not self.note:
return
if self.stealFocus:
field = self.currentField
else:
field = -1
if not self._loaded:
# will be loaded when page is ready
return
data = []
for fld, val in list(self.note.items()):
data.append((fld, self.mw.col.media.escapeImages(val)))
self.web.eval("setFields(%s, %d, %s);" % (
json.dumps(data), field, json.dumps(self.prewrapMode())))
self.web.eval("setFonts(%s);" % (
json.dumps(self.fonts())))
self.checkValid()
self.updateTags()
self.widget.show()
if self.stealFocus:
self.web.setFocus()
self.stealFocus = False
self.updateTags()
def oncallback(arg):
if not self.note:
return
self.setupForegroundButton()
self.checkValid()
runHook("loadNote", self)
self.web.evalWithCallback("setFields(%s, %s); setFonts(%s); focusField(%s)" % (
json.dumps(data), json.dumps(self.prewrapMode()),
json.dumps(self.fonts()), json.dumps(focusTo)),
oncallback)
def prewrapMode(self):
return self.note.model().get('prewrap', False)
def focus(self):
self.web.setFocus()
def fonts(self):
return [(f['font'], f['size'], f['rtl'])
for f in self.note.model()['flds']]
@ -339,14 +317,15 @@ class Editor:
######################################################################
def onHtmlEdit(self):
self.saveNow(self._onHtmlEdit)
field = self.currentField
self.saveNow(lambda: self._onHtmlEdit(field))
def _onHtmlEdit(self):
def _onHtmlEdit(self, field):
d = QDialog(self.widget)
form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d)
form.buttonBox.helpRequested.connect(lambda: openHelp("editor"))
form.textEdit.setPlainText(self.note.fields[self.currentField])
form.textEdit.setPlainText(self.note.fields[field])
form.textEdit.moveCursor(QTextCursor.End)
d.exec_()
html = form.textEdit.toPlainText()
@ -355,11 +334,8 @@ class Editor:
with warnings.catch_warnings() as w:
warnings.simplefilter('ignore', UserWarning)
html = str(BeautifulSoup(html, "html.parser"))
self.note.fields[self.currentField] = html
self.loadNote()
# focus field so it's saved
self.web.setFocus()
self.web.eval("focusField(%d);" % self.currentField)
self.note.fields[field] = html
self.loadNote(focusTo=field)
# Tag handling
######################################################################
@ -408,6 +384,9 @@ class Editor:
def hideCompleters(self):
self.tags.hideCompleter()
def onFocusTags(self):
self.tags.setFocus()
# Format buttons
######################################################################

View file

@ -33,6 +33,11 @@ function onKey() {
insertNewline();
return;
}
// shift+tab goes to previous field
if (window.event.which === 9 && window.event.shiftKey) {
focusPrevious();
return;
}
clearChangeTimer();
changeTimer = setTimeout(function () {
updateButtonState();
@ -120,7 +125,7 @@ function clearChangeTimer() {
function onFocus(elem) {
currentField = elem;
pycmd("focus:" + currentField.id.substring(1));
pycmd("focus:" + currentFieldOrdinal());
enableButtons();
// don't adjust cursor on mouse clicks
if (mouseDown) {
@ -145,9 +150,22 @@ function onFocus(elem) {
}
function focusField(n) {
if (n === null) {
return;
}
$("#f" + n).focus();
}
function focusPrevious() {
if (!currentField) {
return;
}
var previous = currentFieldOrdinal() - 1;
if (previous >= 0) {
focusField(previous);
}
}
function onDragOver(elem) {
// if we focus the target element immediately, the drag&drop turns into a
// copy, so note it down for later instead
@ -175,6 +193,7 @@ function caretToEnd() {
function onBlur() {
if (currentField) {
saveField("blur");
currentField = null;
}
clearChangeTimer();
disableButtons();
@ -186,10 +205,14 @@ function saveField(type) {
return;
}
// type is either 'blur' or 'key'
pycmd(type + ":" + currentField.innerHTML);
pycmd(type + ":" + currentFieldOrdinal() + ":" + currentField.innerHTML);
clearChangeTimer();
}
function currentFieldOrdinal() {
return currentField.id.substring(1);
}
function wrappedExceptForWhitespace(text, front, back) {
var match = text.match(/^(\s*)([^]*?)(\s*)$/);
return match[1] + front + match[2] + back + match[3];
@ -234,7 +257,7 @@ function wrap(front, back) {
}
}
function setFields(fields, focusTo, prewrap) {
function setFields(fields, prewrap) {
var txt = "";
for (var i = 0; i < fields.length; i++) {
var n = fields[i][0];
@ -250,12 +273,6 @@ function setFields(fields, focusTo, prewrap) {
txt += "</td></tr>";
}
$("#fields").html("<table cellpadding=0 width=100%>" + txt + "</table>");
if (!focusTo) {
focusTo = 0;
}
if (focusTo >= 0) {
$("#f" + focusTo).focus();
}
maybeDisableButtons();
prewrapMode = prewrap;
if (prewrap) {