saveNow() now requires a callback

the current code was freezing when clicking on 'cards' in the
browser - it looks like like the javascript callback was never
being called despite calling processEvents(). so we need to
refactor the code to call saveNow() with a callback that does the
subsequent processing.

a lot of the browser code was implicitly calling saveNow() via
beginReset(), so we've had to change all that code to save
immediately before it begins any processing. found a probable bug in
the process - it doesn't look like onRowChange() was saving before
overwriting the note, so theoretically edits could be lost if the
user switched to another card very quickly after typing something.

onSearch() has been split into a GUI-activated onSearchActivated()
that takes care of saving, and a lower level search() that refreshes
the current search. it keeps track of the last search via an instance
variable so that it refreshes properly if a user accidentally adds
some characters to their search without activating the search, then
does something like reverse the sort order.
This commit is contained in:
Damien Elmes 2016-07-14 20:23:44 +10:00
parent 37bac3979c
commit 8e71554ac4
5 changed files with 157 additions and 91 deletions

View file

@ -45,6 +45,11 @@ Anki's webviews are now using WebEngine. Of note:
modified to use this.
- Javascript is evaluated asynchronously, so if you need the result of a JS
expression you can use ankiwebview's evalWithCallback().
- As a result of this asynchronous behaviour, editor.saveNow() now requires a
callback. If your add-on performs actions in the browser, you likely need to
call editor.saveNow() first and then run the rest of your code in the callback.
Calls to .onSearch() will need to be changed to .search()/.onSearchActivated()
as well. See the browser's .deleteNotes() for an example.
- You can now debug the webviews using an external Chrome instance, by setting
the env var QTWEBENGINE_REMOTE_DEBUGGING to 8080 prior to starting Anki,
then surfing to localhost:8080 in Chrome. If you run into issues, try

View file

@ -151,7 +151,7 @@ class AddCards(QDialog):
def editHistory(self, nid):
browser = aqt.dialogs.open("Browser", self.mw)
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
browser.onSearch()
browser.onSearchActivated()
def addNote(self, note):
note.model()['did'] = self.deckChooser.selectedId()
@ -178,7 +178,9 @@ question on all cards."""), help="AddItems")
return note
def addCards(self):
self.editor.saveNow()
self.editor.saveNow(self._addCards)
def _addCards(self):
self.editor.saveAddModeVars()
note = self.editor.note
note = self.addNote(note)

View file

@ -115,9 +115,8 @@ class DataModel(QAbstractTableModel):
# Filtering
######################################################################
def search(self, txt, reset=True):
if reset:
self.beginReset()
def search(self, txt):
self.beginReset()
t = time.time()
# the db progress handler may cause a refresh, so we need to zero out
# old data first
@ -125,15 +124,14 @@ class DataModel(QAbstractTableModel):
self.cards = self.col.findCards(txt, order=True)
#self.browser.mw.pm.profile['fullSearch'])
#print "fetch cards in %dms" % ((time.time() - t)*1000)
if reset:
self.endReset()
self.endReset()
def reset(self):
self.beginReset()
self.endReset()
# caller must have called editor.saveNow() before calling this or .reset()
def beginReset(self):
self.browser.editor.saveNow()
self.browser.editor.setNote(None, hide=False)
self.browser.mw.progress.start()
self.saveSelection()
@ -147,6 +145,9 @@ class DataModel(QAbstractTableModel):
self.browser.mw.progress.finish()
def reverse(self):
self.browser.editor.saveNow(self._reverse)
def _reverse(self):
self.beginReset()
self.cards.reverse()
self.endReset()
@ -351,6 +352,7 @@ class Browser(QMainWindow):
self.col = self.mw.col
self.lastFilter = ""
self._previewWindow = None
self._closeEventHasCleanedUp = False
self.form = aqt.forms.browser.Ui_Dialog()
self.form.setupUi(self)
restoreGeom(self, "editor", 0)
@ -364,17 +366,13 @@ class Browser(QMainWindow):
self.setupColumns()
self.setupTable()
self.setupMenus()
self.setupSearch()
self.setupTree()
self.setupHeaders()
self.setupHooks()
self.setupEditor()
self.updateFont()
self.onUndoState(self.mw.form.actionUndo.isEnabled())
self.form.searchEdit.setFocus()
self.form.searchEdit.lineEdit().setText("is:current")
self.form.searchEdit.lineEdit().selectAll()
self.onSearch()
self.setupSearch()
self.show()
def setupToolbar(self):
@ -391,7 +389,6 @@ class Browser(QMainWindow):
f.actionClose.setVisible(False)
f.actionReposition.triggered.connect(self.reposition)
f.actionReschedule.triggered.connect(self.reschedule)
f.actionCram.triggered.connect(self.cram)
f.actionChangeModel.triggered.connect(self.onChangeModel)
# edit
f.actionUndo.triggered.connect(self.mw.onUndo)
@ -458,21 +455,36 @@ class Browser(QMainWindow):
curmax + 6)
def closeEvent(self, evt):
if not self._closeEventHasCleanedUp:
if self.editor.note:
# ignore event for now to allow us to save
self.editor.saveNow(self._closeEventAfterSave)
evt.ignore()
else:
self._closeEventCleanup()
evt.accept()
self.mw.gcWindow(self)
else:
evt.accept()
self.mw.gcWindow(self)
def _closeEventAfterSave(self):
self._closeEventCleanup()
self.close()
def _closeEventCleanup(self):
self.editor.setNote(None)
saveSplitter(self.form.splitter_2, "editor2")
saveSplitter(self.form.splitter, "editor3")
self.editor.saveNow()
self.editor.setNote(None)
saveGeom(self, "editor")
saveState(self, "editor")
saveHeader(self.form.tableView.horizontalHeader(), "editor")
self.col.conf['activeCols'] = self.model.activeCols
self.col.setMod()
self.hide()
aqt.dialogs.close("Browser")
self.teardownHooks()
self.mw.maybeReset()
self.mw.gcWindow(self)
evt.accept()
aqt.dialogs.close("Browser")
self._closeEventHasCleanedUp = True
def canClose(self):
return True
@ -510,19 +522,31 @@ class Browser(QMainWindow):
######################################################################
def setupSearch(self):
self.filterTimer = None
self.form.searchEdit.setLineEdit(FavouritesLineEdit(self.mw, self))
self.form.searchButton.clicked.connect(self.onSearch)
self.form.searchEdit.lineEdit().returnPressed.connect(self.onSearch)
self.form.searchButton.clicked.connect(self.onSearchActivated)
self.form.searchEdit.lineEdit().returnPressed.connect(self.onSearchActivated)
self.form.searchEdit.setCompleter(None)
self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory'])
self._searchPrompt = _("<type here to search; hit enter to show current deck>")
self._lastSearchTxt = "is:current"
self.search()
# then replace text for easily showing the deck
self.form.searchEdit.lineEdit().setText(self._searchPrompt)
self.form.searchEdit.lineEdit().selectAll()
self.form.searchEdit.setFocus()
def onSearch(self, reset=True):
"Careful: if reset is true, the current note is saved."
# search triggered by user
def onSearchActivated(self):
self.editor.saveNow(self._onSearchActivated)
def _onSearchActivated(self):
# convert guide text before we save history
if self.form.searchEdit.lineEdit().text() == self._searchPrompt:
self.form.searchEdit.lineEdit().setText("deck:current ")
# update history
txt = str(self.form.searchEdit.lineEdit().text()).strip()
prompt = _("<type here to search; hit enter to show current deck>")
sh = self.mw.pm.profile['searchHistory']
# update search history
if txt in sh:
sh.remove(txt)
sh.insert(0, txt)
@ -530,30 +554,25 @@ class Browser(QMainWindow):
self.form.searchEdit.clear()
self.form.searchEdit.addItems(sh)
self.mw.pm.profile['searchHistory'] = sh
if self.mw.state == "review" and "is:current" in txt:
# search for current card, but set search to easily display whole
# deck
if reset:
self.model.beginReset()
self.model.focusedCard = self.mw.reviewer.card.id
self.model.search("nid:%d"%self.mw.reviewer.card.nid, False)
if reset:
self.model.endReset()
self.form.searchEdit.lineEdit().setText(prompt)
self.form.searchEdit.lineEdit().selectAll()
return
elif "is:current" in txt:
self.form.searchEdit.lineEdit().setText(prompt)
self.form.searchEdit.lineEdit().selectAll()
elif txt == prompt:
self.form.searchEdit.lineEdit().setText("deck:current ")
txt = "deck:current "
self.model.search(txt, reset)
# keep track of search string so that we reuse identical search when
# refreshing, rather than whatever is currently in the search field
self._lastSearchTxt = txt
self.search()
# search triggered programmatically. caller must have saved note first.
def search(self):
if "is:current" in self._lastSearchTxt:
# show current card if there is one
c = self.mw.reviewer.card
nid = c and c.nid or 0
self.model.search("nid:%d"%nid)
else:
self.model.search(self._lastSearchTxt)
if not self.model.cards:
# no row change will fire
self.onRowChanged(None, None)
elif self.mw.state == "review":
self.focusCid(self.mw.reviewer.card.id)
self._onRowChanged(None, None)
def updateTitle(self):
selected = len(self.form.tableView.selectionModel().selectedRows())
@ -568,7 +587,7 @@ class Browser(QMainWindow):
def onReset(self):
self.editor.setNote(None)
self.onSearch()
self.search()
# Table view & editor
######################################################################
@ -588,6 +607,9 @@ class Browser(QMainWindow):
def onRowChanged(self, current, previous):
"Update current note and hide/show editor."
self.editor.saveNow(lambda: self._onRowChanged(current, previous))
def _onRowChanged(self, current, previous):
update = self.updateTitle()
show = self.model.cards and update == 1
self.form.splitter.widget(1).setVisible(not not show)
@ -636,14 +658,16 @@ class Browser(QMainWindow):
hh.sectionMoved.connect(self.onColumnMoved)
def onSortChanged(self, idx, ord):
self.editor.saveNow(lambda: self._onSortChanged(idx, ord))
def _onSortChanged(self, idx, ord):
type = self.model.activeCols[idx]
noSort = ("question", "answer", "template", "deck", "note", "noteTags")
if type in noSort:
if type == "template":
# fixme: change to 'card:1' to be clearer in future dev round
showInfo(_("""\
This column can't be sorted on, but you can search for individual card types, \
such as 'card:Card 1'."""))
such as 'card:1'."""))
elif type == "deck":
showInfo(_("""\
This column can't be sorted on, but you can search for specific decks \
@ -658,7 +682,7 @@ by clicking on one on the left."""))
if type == "noteFld":
ord = not ord
self.col.conf['sortBackwards'] = ord
self.onSearch()
self.search()
else:
if self.col.conf['sortBackwards'] != ord:
self.col.conf['sortBackwards'] = ord
@ -692,6 +716,9 @@ by clicking on one on the left."""))
m.exec_(gpos)
def toggleField(self, type):
self.editor.saveNow(lambda: self._toggleField(type))
def _toggleField(self, type):
self.model.beginReset()
if type in self.model.activeCols:
if len(self.model.activeCols) < 2:
@ -778,15 +805,14 @@ by clicking on one on the left."""))
txt = "-"+txt
if self.mw.app.keyboardModifiers() & Qt.ControlModifier:
cur = str(self.form.searchEdit.lineEdit().text())
if cur and cur != \
_("<type here to search; hit enter to show current deck>"):
if cur and cur != self._searchPrompt:
txt = cur + " " + txt
elif self.mw.app.keyboardModifiers() & Qt.ShiftModifier:
cur = str(self.form.searchEdit.lineEdit().text())
if cur:
txt = cur + " or " + txt
self.form.searchEdit.lineEdit().setText(txt)
self.onSearch()
self.onSearchActivated()
def _systemTagTree(self, root):
tags = (
@ -975,15 +1001,13 @@ where id in %s""" % ids2str(sf))
######################################################################
def onChangeModel(self):
self.editor.saveNow(self._onChangeModel)
def _onChangeModel(self):
nids = self.oneModelNotes()
if nids:
ChangeModel(self, nids)
def cram(self):
return showInfo("not yet implemented")
self.close()
self.mw.onCram(self.selectedCards())
# Preview
######################################################################
@ -1102,6 +1126,9 @@ where id in %s""" % ids2str(sf))
######################################################################
def deleteNotes(self):
self.editor.saveNow(self._deleteNotes)
def _deleteNotes(self):
nids = self.selectedNotes()
if not nids:
return
@ -1122,7 +1149,7 @@ where id in %s""" % ids2str(sf))
# last selection at top; place one above topmost selection
newRow = min(selectedRows) - 1
self.col.remNotes(nids)
self.onSearch(reset=False)
self.search()
if len(self.model.cards):
newRow = min(newRow, len(self.model.cards) - 1)
newRow = max(newRow, 0)
@ -1135,6 +1162,9 @@ where id in %s""" % ids2str(sf))
######################################################################
def setDeck(self):
self.editor.saveNow(self._setDeck)
def _setDeck(self):
from aqt.studydeck import StudyDeck
cids = self.selectedCards()
if not cids:
@ -1171,6 +1201,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
######################################################################
def addTags(self, tags=None, label=None, prompt=None, func=None):
self.editor.saveNow(lambda: self._addTags(tags, label, prompt, func))
def _addTags(self, tags, label, prompt, func):
if prompt is None:
prompt = _("Enter tags to add:")
if tags is None:
@ -1202,11 +1235,11 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
def isSuspended(self):
return not not (self.card and self.card.queue == -1)
def onSuspend(self, sus=None):
if sus is None:
sus = not self.isSuspended()
# focus lost hook may not have chance to fire
self.editor.saveNow()
def onSuspend(self):
self.editor.saveNow(self._onSuspend)
def _onSuspend(self):
sus = not self.isSuspended()
c = self.selectedCards()
if sus:
self.col.sched.suspendCards(c)
@ -1230,6 +1263,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
######################################################################
def reposition(self):
self.editor.saveNow(self._reposition)
def _reposition(self):
cids = self.selectedCards()
cids2 = self.col.db.list(
"select id from cards where type = 0 and id in " + ids2str(cids))
@ -1253,7 +1289,7 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
self.col.sched.sortCards(
cids, start=frm.start.value(), step=frm.step.value(),
shuffle=frm.randomize.isChecked(), shift=frm.shift.isChecked())
self.onSearch(reset=False)
self.search()
self.mw.requireReset()
self.model.endReset()
@ -1261,6 +1297,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
######################################################################
def reschedule(self):
self.editor.saveNow(self._reschedule)
def _reschedule(self):
d = QDialog(self)
d.setWindowModality(Qt.WindowModal)
frm = aqt.forms.reschedule.Ui_Dialog()
@ -1277,7 +1316,7 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
fmax = max(fmin, fmax)
self.col.sched.reschedCards(
self.selectedCards(), fmin, fmax)
self.onSearch(reset=False)
self.search()
self.mw.requireReset()
self.model.endReset()
@ -1285,12 +1324,17 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
######################################################################
def selectNotes(self):
self.editor.saveNow(self._selectNotes)
def _selectNotes(self):
nids = self.selectedNotes()
self.form.searchEdit.lineEdit().setText("nid:"+",".join([str(x) for x in nids]))
# bypass search history
self._lastSearchTxt = "nid:"+",".join([str(x) for x in nids])
self.form.searchEdit.lineEdit().setText(self._lastSearchTxt)
# clear the selection so we don't waste energy preserving it
tv = self.form.tableView
tv.selectionModel().clear()
self.onSearch()
self.search()
tv.selectAll()
def invertSelection(self):
@ -1327,6 +1371,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
######################################################################
def onFindReplace(self):
self.editor.saveNow(self._onFindReplace)
def _onFindReplace(self):
sf = self.selectedNotes()
if not sf:
return
@ -1361,7 +1408,7 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
showInfo(_("Invalid regular expression."), parent=self)
return
else:
self.onSearch()
self.search()
self.mw.requireReset()
finally:
self.model.endReset()
@ -1380,6 +1427,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
######################################################################
def onFindDupes(self):
self.editor.saveNow(self._onFindDupes)
def _onFindDupes(self):
d = QDialog(self)
self.mw.setupDialogGC(d)
frm = aqt.forms.finddupes.Ui_Dialog()
@ -1441,7 +1491,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
def dupeLinkClicked(self, link):
self.form.searchEdit.lineEdit().setText(link)
self.onSearch()
# manually, because we've already saved
self._lastSearchTxt = link
self.search()
self.onNote()
# Jumping
@ -1450,7 +1502,6 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
def _moveCur(self, dir=None, idx=None):
if not self.model.cards:
return
self.editor.saveNow()
tv = self.form.tableView
if idx is None:
idx = tv.moveCursor(dir, self.mw.app.keyboardModifiers())
@ -1458,12 +1509,18 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
tv.setCurrentIndex(idx)
def onPreviousCard(self):
self.editor.saveNow(self._onPreviousCard)
def _onPreviousCard(self):
f = self.editor.currentField
self._moveCur(QAbstractItemView.MoveUp)
self.editor.web.setFocus()
self.editor.web.eval("focusField(%d)" % f)
def onNextCard(self):
self.editor.saveNow(self._onNextCard)
def _onNextCard(self):
f = self.editor.currentField
self._moveCur(QAbstractItemView.MoveDown)
self.editor.web.setFocus()
@ -1675,7 +1732,7 @@ Are you sure you want to continue?""")):
b.model.beginReset()
mm = b.mw.col.models
mm.change(self.oldModel, self.nids, self.targetModel, fmap, cmap)
b.onSearch(reset=False)
b.search()
b.model.endReset()
b.mw.progress.finish()
b.mw.reset()

View file

@ -51,8 +51,10 @@ class EditCurrent(QDialog):
self.editor.setNote(n)
def onSave(self):
self.editor.saveNow(self._onSave)
def _onSave(self):
remHook("reset", self.onReset)
self.editor.saveNow()
r = self.mw.reviewer
try:
r.card.load()

View file

@ -80,6 +80,7 @@ function setFGButton(col) {
};
function saveNow() {
clearChangeTimer();
if (currentField) {
currentField.blur();
}
@ -453,13 +454,17 @@ class Editor(object):
# _("Record audio (F5)")
def onFields(self):
self.saveNow(self._onFields)
def _onFields(self):
from aqt.fields import FieldDialog
self.saveNow()
FieldDialog(self.mw, self.note, parent=self.parentWindow)
def onCardLayout(self):
self.saveNow(self._onCardLayout)
def _onCardLayout(self):
from aqt.clayout import CardLayout
self.saveNow()
if self.card:
ord = self.card.ord
else:
@ -581,20 +586,13 @@ class Editor(object):
return [(f['font'], f['size'], f['rtl'])
for f in self.note.model()['flds']]
def saveNow(self):
"Must call this before adding cards, closing dialog, etc."
def saveNow(self, callback):
"Save unsaved edits then call callback()."
if not self.note:
callback()
return
self.saveTags()
# move focus out of fields and save tags
self._saveNowWaiting = True
self.web.evalWithCallback("saveNow()", self._saveNowDone)
while self._saveNowWaiting:
# and process events so any focus-lost hooks fire
self.mw.app.processEvents()
def _saveNowDone(self, res):
self._saveNowWaiting = False
self.web.evalWithCallback("saveNow()", lambda res: callback())
def checkValid(self):
cols = []
@ -615,7 +613,7 @@ class Editor(object):
browser.form.searchEdit.lineEdit().setText(
'"dupe:%s,%s"' % (self.note.model()['id'],
contents))
browser.onSearch()
browser.onSearchActivated()
def fieldsAreBlank(self):
if not self.note:
@ -630,7 +628,9 @@ class Editor(object):
######################################################################
def onHtmlEdit(self):
self.saveNow()
self.saveNow(self._onHtmlEdit)
def _onHtmlEdit(self):
d = QDialog(self.widget)
form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d)