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

View file

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

View file

@ -26,7 +26,7 @@ class EditCurrent(QDialog):
self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut( self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut(
QKeySequence("Ctrl+Return")) QKeySequence("Ctrl+Return"))
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) 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") restoreGeom(self, "editcurrent")
addHook("reset", self.onReset) addHook("reset", self.onReset)
self.mw.requireReset() self.mw.requireReset()

View file

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

View file

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