use callback when closing windows

remove the old forceClose hack in favour of a callback when closing, so
all windows have a chance to save properly before the collection is
unloaded

also:

- fix a warning shown when opening about screen
- require a call to editor.cleanup() when closing a window, to make sure
 any pending js callbacks don't try to fire on a deleted object
- make sure we gc webview when closing editcurrent
- main.py still needs refactoring to make use of the change
This commit is contained in:
Damien Elmes 2017-08-16 12:45:33 +10:00
parent ca9d80c014
commit 8ab5a3a176
8 changed files with 97 additions and 64 deletions

View file

@ -38,8 +38,8 @@ except ImportError as e:
from anki.utils import checksum from anki.utils import checksum
# Dialog manager - manages modeless windows # Dialog manager - manages non-modal windows
##########################################################################emacs ##########################################################################
class DialogManager: class DialogManager:
@ -65,18 +65,32 @@ class DialogManager:
self._dialogs[name][1] = instance self._dialogs[name][1] = instance
return instance return instance
def close(self, name): def markClosed(self, name):
self._dialogs[name] = [self._dialogs[name][0], None] self._dialogs[name] = [self._dialogs[name][0], None]
def closeAll(self): def allClosed(self):
"True if all closed successfully." return not any(x[1] for x in self._dialogs.values())
for (n, (creator, instance)) in list(self._dialogs.items()):
if instance: def closeAll(self, onsuccess):
if not instance.canClose(): # can we close immediately?
return False if self.allClosed():
instance.forceClose = True onsuccess()
instance.close() return
self.close(n)
# ask all windows to close and await a reply
for (name, (creator, instance)) in self._dialogs.items():
if not instance:
continue
def callback():
if self.allClosed():
onsuccess()
else:
# still waiting for others to close
pass
instance.closeWithCallback(callback)
return True return True
dialogs = DialogManager() dialogs = DialogManager()

View file

@ -7,17 +7,18 @@ import aqt.forms
from aqt import appVersion from aqt import appVersion
class ClosableQDialog(QDialog): class ClosableQDialog(QDialog):
def canClose(self):
return True
def reject(self): def reject(self):
aqt.dialogs.close("About") aqt.dialogs.markClosed("About")
QDialog.reject(self) QDialog.reject(self)
def accept(self): def accept(self):
aqt.dialogs.close("About") aqt.dialogs.markClosed("About")
QDialog.accept(self) QDialog.accept(self)
def closeWithCallback(self, callback):
self.reject()
callback()
def show(mw): def show(mw):
dialog = ClosableQDialog(mw) dialog = ClosableQDialog(mw)
mw.setupDialogGC(dialog) mw.setupDialogGC(dialog)
@ -124,6 +125,8 @@ please get in touch.")
abouttext += '<p>' + _("A big thanks to all the people who have provided \ abouttext += '<p>' + _("A big thanks to all the people who have provided \
suggestions, bug reports and donations.") suggestions, bug reports and donations.")
abt.label.stdHtml(abouttext, js=" ") abt.label.stdHtml(abouttext, js=" ")
def resizeAndShow(arg):
dialog.adjustSize() dialog.adjustSize()
dialog.show() dialog.show()
abt.label.evalWithCallback("1", resizeAndShow)
return dialog return dialog

View file

@ -28,7 +28,6 @@ class AddCards(QDialog):
self.setupButtons() self.setupButtons()
self.onReset() self.onReset()
self.history = [] self.history = []
self.forceClose = False
restoreGeom(self, "add") restoreGeom(self, "add")
addHook('reset', self.onReset) addHook('reset', self.onReset)
addHook('currentModelChanged', self.onModelChange) addHook('currentModelChanged', self.onModelChange)
@ -205,22 +204,32 @@ question on all cards."""), help="AddItems")
return QDialog.keyPressEvent(self, evt) return QDialog.keyPressEvent(self, evt)
def reject(self): def reject(self):
if not self.canClose(): self.ifCanClose(self._reject)
return
def _reject(self):
remHook('reset', self.onReset) remHook('reset', self.onReset)
remHook('currentModelChanged', self.onModelChange) remHook('currentModelChanged', self.onModelChange)
clearAudioQueue() clearAudioQueue()
self.removeTempNote(self.editor.note) self.removeTempNote(self.editor.note)
self.editor.setNote(None) self.editor.cleanup()
self.modelChooser.cleanup() self.modelChooser.cleanup()
self.deckChooser.cleanup() self.deckChooser.cleanup()
self.mw.maybeReset() self.mw.maybeReset()
saveGeom(self, "add") saveGeom(self, "add")
aqt.dialogs.close("AddCards") aqt.dialogs.markClosed("AddCards")
QDialog.reject(self) QDialog.reject(self)
def canClose(self): def ifCanClose(self, onOk):
if (self.forceClose or self.editor.fieldsAreBlank() or def afterSave():
askUser(_("Close and lose current input?"))): ok = (self.editor.fieldsAreBlank() or
return True askUser(_("Close and lose current input?")))
return False if ok:
onOk()
self.editor.saveNow(afterSave)
def closeWithCallback(self, cb):
def doClose():
self._reject()
cb()
self.ifCanClose(doClose)

View file

@ -366,7 +366,6 @@ class Browser(QMainWindow):
applyStyles(self) applyStyles(self)
self.mw = mw self.mw = mw
self.col = self.mw.col self.col = self.mw.col
self.forceClose = False
self.lastFilter = "" self.lastFilter = ""
self.focusTo = None self.focusTo = None
self._previewWindow = None self._previewWindow = None
@ -459,26 +458,15 @@ class Browser(QMainWindow):
curmax + 6) curmax + 6)
def closeEvent(self, evt): def closeEvent(self, evt):
if not self._closeEventHasCleanedUp: if self._closeEventHasCleanedUp:
if self.editor.note and not self.forceClose: evt.accept()
# ignore event for now to allow us to save return
self.editor.saveNow(self._closeEventAfterSave) self.editor.saveNow(self._closeWindow)
evt.ignore() evt.ignore()
else:
self._closeEventCleanup()
evt.accept()
self.mw.gcWindow(self)
else:
evt.accept()
self.mw.gcWindow(self)
def _closeEventAfterSave(self): def _closeWindow(self):
self._closeEventCleanup()
self.close()
def _closeEventCleanup(self):
self._cancelPreviewTimer() self._cancelPreviewTimer()
self.editor.setNote(None) self.editor.cleanup()
saveSplitter(self.form.splitter, "editor3") saveSplitter(self.form.splitter, "editor3")
saveGeom(self, "editor") saveGeom(self, "editor")
saveState(self, "editor") saveState(self, "editor")
@ -487,14 +475,18 @@ class Browser(QMainWindow):
self.col.setMod() self.col.setMod()
self.teardownHooks() self.teardownHooks()
self.mw.maybeReset() self.mw.maybeReset()
aqt.dialogs.close("Browser") aqt.dialogs.markClosed("Browser")
self._closeEventHasCleanedUp = True self._closeEventHasCleanedUp = True
self.mw.gcWindow(self)
self.close()
def canClose(self): def closeWithCallback(self, onsuccess):
return True def callback():
self._closeWindow()
onsuccess()
self.editor.saveNow(callback)
def keyPressEvent(self, evt): def keyPressEvent(self, evt):
"Show answer on RET or register answer."
if evt.key() == Qt.Key_Escape: if evt.key() == Qt.Key_Escape:
self.close() self.close()
else: else:

View file

@ -12,13 +12,13 @@ class EditCurrent(QDialog):
def __init__(self, mw): def __init__(self, mw):
QDialog.__init__(self, None, Qt.Window) QDialog.__init__(self, None, Qt.Window)
mw.setupDialogGC(self)
self.mw = mw self.mw = mw
self.form = aqt.forms.editcurrent.Ui_Dialog() self.form = aqt.forms.editcurrent.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
self.setWindowTitle(_("Edit Current")) self.setWindowTitle(_("Edit Current"))
self.setMinimumHeight(400) self.setMinimumHeight(400)
self.setMinimumWidth(500) self.setMinimumWidth(500)
self.rejected.connect(self.onSave)
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)
@ -40,15 +40,18 @@ class EditCurrent(QDialog):
remHook("reset", self.onReset) remHook("reset", self.onReset)
self.editor.setNote(None) self.editor.setNote(None)
self.mw.reset() self.mw.reset()
aqt.dialogs.close("EditCurrent") aqt.dialogs.markClosed("EditCurrent")
self.close() self.close()
return return
self.editor.setNote(n) self.editor.setNote(n)
def onSave(self): def reject(self):
self.editor.saveNow(self._onSave) self.saveAndClose()
def _onSave(self): def saveAndClose(self):
self.editor.saveNow(self._saveAndClose)
def _saveAndClose(self):
remHook("reset", self.onReset) remHook("reset", self.onReset)
r = self.mw.reviewer r = self.mw.reviewer
try: try:
@ -58,9 +61,14 @@ class EditCurrent(QDialog):
pass pass
else: else:
self.mw.reviewer.cardQueue.append(self.mw.reviewer.card) self.mw.reviewer.cardQueue.append(self.mw.reviewer.card)
self.editor.cleanup()
self.mw.moveToState("review") self.mw.moveToState("review")
saveGeom(self, "editcurrent") saveGeom(self, "editcurrent")
aqt.dialogs.close("EditCurrent") aqt.dialogs.markClosed("EditCurrent")
QDialog.reject(self)
def canClose(self): def closeWithCallback(self, onsuccess):
return True def callback():
self._saveAndClose()
onsuccess()
self.editor.saveNow(callback)

View file

@ -319,6 +319,11 @@ class Editor:
return False return False
return True return True
def cleanup(self):
self.setNote(None)
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
self.web = None
# HTML editing # HTML editing
###################################################################### ######################################################################

View file

@ -530,7 +530,8 @@ title="%s" %s>%s</button>''' % (
self.form.centralwidget.setLayout(self.mainLayout) self.form.centralwidget.setLayout(self.mainLayout)
def closeAllCollectionWindows(self): def closeAllCollectionWindows(self):
return aqt.dialogs.closeAll() aqt.dialogs.closeAll(lambda: 1)
return True
# Components # Components
########################################################################## ##########################################################################

View file

@ -42,14 +42,15 @@ class DeckStats(QDialog):
self.show() self.show()
self.activateWindow() self.activateWindow()
def canClose(self):
return True
def reject(self): def reject(self):
saveGeom(self, self.name) saveGeom(self, self.name)
aqt.dialogs.close("DeckStats") aqt.dialogs.markClosed("DeckStats")
QDialog.reject(self) QDialog.reject(self)
def closeWithCallback(self, callback):
self.reject()
callback()
def _imagePath(self): def _imagePath(self):
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf",
time.localtime(time.time())) time.localtime(time.time()))