diff --git a/README.addons b/README.addons index 1e4a9866e..73a870b22 100644 --- a/README.addons +++ b/README.addons @@ -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 diff --git a/aqt/addcards.py b/aqt/addcards.py index 40b3a66b4..71d72a4a8 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -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) diff --git a/aqt/browser.py b/aqt/browser.py index 6446b1c98..4c12554cb 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -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 = _("") + 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 = _("") 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 != \ - _(""): + 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() diff --git a/aqt/editcurrent.py b/aqt/editcurrent.py index 49e7ead10..3d675dc9e 100644 --- a/aqt/editcurrent.py +++ b/aqt/editcurrent.py @@ -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() diff --git a/aqt/editor.py b/aqt/editor.py index f204f1843..7809b0409 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -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)