mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 16:26:40 -04:00
refactor profile and collection loading/unloading
- unloadCollection() now waits for all collection windows to indicate they've closed, and calls a callback when it's done - autosync runs when the collection is unloaded, and is no longer responsible for reloading it - make sure backup thread runs until completion - ensure we return to profile manager when collection can't be loaded - don't run the profile manager with exec_(), or opening+closing a broken profile ends up nesting runloops - warn if a window wasn't cleaned up as part of collection unloading
This commit is contained in:
parent
b3a569ed57
commit
489d16ed14
1 changed files with 107 additions and 97 deletions
204
aqt/main.py
204
aqt/main.py
|
@ -105,13 +105,17 @@ class AnkiQt(QMainWindow):
|
||||||
self.loadProfile()
|
self.loadProfile()
|
||||||
|
|
||||||
def showProfileManager(self):
|
def showProfileManager(self):
|
||||||
|
self.pm.profile = None
|
||||||
self.state = "profileManager"
|
self.state = "profileManager"
|
||||||
d = self.profileDiag = QDialog()
|
d = self.profileDiag = QDialog()
|
||||||
f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
|
f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
|
||||||
f.setupUi(d)
|
f.setupUi(d)
|
||||||
f.login.clicked.connect(self.onOpenProfile)
|
f.login.clicked.connect(self.onOpenProfile)
|
||||||
f.profiles.itemDoubleClicked.connect(self.onOpenProfile)
|
f.profiles.itemDoubleClicked.connect(self.onOpenProfile)
|
||||||
f.quit.clicked.connect(self.cleanupAndExit)
|
def onQuit():
|
||||||
|
d.close()
|
||||||
|
self.cleanupAndExit()
|
||||||
|
f.quit.clicked.connect(onQuit)
|
||||||
f.add.clicked.connect(self.onAddProfile)
|
f.add.clicked.connect(self.onAddProfile)
|
||||||
f.rename.clicked.connect(self.onRenameProfile)
|
f.rename.clicked.connect(self.onRenameProfile)
|
||||||
f.delete_2.clicked.connect(self.onRemProfile)
|
f.delete_2.clicked.connect(self.onRemProfile)
|
||||||
|
@ -120,9 +124,6 @@ class AnkiQt(QMainWindow):
|
||||||
self.refreshProfilesList()
|
self.refreshProfilesList()
|
||||||
# raise first, for osx testing
|
# raise first, for osx testing
|
||||||
d.show()
|
d.show()
|
||||||
d.activateWindow()
|
|
||||||
d.raise_()
|
|
||||||
d.exec_()
|
|
||||||
|
|
||||||
def refreshProfilesList(self):
|
def refreshProfilesList(self):
|
||||||
f = self.profileForm
|
f = self.profileForm
|
||||||
|
@ -154,8 +155,8 @@ class AnkiQt(QMainWindow):
|
||||||
if not self.openProfile():
|
if not self.openProfile():
|
||||||
showWarning(_("Invalid password."))
|
showWarning(_("Invalid password."))
|
||||||
return
|
return
|
||||||
self.loadProfile()
|
|
||||||
self.profileDiag.close()
|
self.profileDiag.close()
|
||||||
|
self.loadProfile()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def profileNameOk(self, str):
|
def profileNameOk(self, str):
|
||||||
|
@ -204,6 +205,11 @@ Are you sure?""")):
|
||||||
self.refreshProfilesList()
|
self.refreshProfilesList()
|
||||||
|
|
||||||
def loadProfile(self):
|
def loadProfile(self):
|
||||||
|
self.maybeAutoSync()
|
||||||
|
|
||||||
|
if not self.loadCollection():
|
||||||
|
return
|
||||||
|
|
||||||
# show main window
|
# show main window
|
||||||
if self.pm.profile['mainWindowState']:
|
if self.pm.profile['mainWindowState']:
|
||||||
restoreGeom(self, "mainWindow")
|
restoreGeom(self, "mainWindow")
|
||||||
|
@ -214,13 +220,7 @@ Are you sure?""")):
|
||||||
self.show()
|
self.show()
|
||||||
self.activateWindow()
|
self.activateWindow()
|
||||||
self.raise_()
|
self.raise_()
|
||||||
# maybe sync (will load DB)
|
|
||||||
if self.pendingImport and os.path.basename(
|
|
||||||
self.pendingImport).startswith("backup-"):
|
|
||||||
# skip sync when importing a backup
|
|
||||||
self.loadCollection()
|
|
||||||
else:
|
|
||||||
self.onSync(auto=True)
|
|
||||||
# import pending?
|
# import pending?
|
||||||
if self.pendingImport:
|
if self.pendingImport:
|
||||||
if self.pm.profile['key']:
|
if self.pm.profile['key']:
|
||||||
|
@ -232,24 +232,37 @@ To import into a password protected profile, please open the profile before atte
|
||||||
self.pendingImport = None
|
self.pendingImport = None
|
||||||
runHook("profileLoaded")
|
runHook("profileLoaded")
|
||||||
|
|
||||||
def unloadProfile(self, browser=True):
|
def unloadProfile(self, onsuccess):
|
||||||
if not self.pm.profile:
|
def callback():
|
||||||
# already unloaded
|
self._unloadProfile()
|
||||||
return
|
onsuccess()
|
||||||
|
|
||||||
runHook("unloadProfile")
|
runHook("unloadProfile")
|
||||||
if not self.unloadCollection():
|
self.unloadCollection(callback)
|
||||||
return
|
|
||||||
self.state = "profileManager"
|
def _unloadProfile(self):
|
||||||
self.onSync(auto=True, reload=False)
|
|
||||||
self.pm.profile['mainWindowGeom'] = self.saveGeometry()
|
self.pm.profile['mainWindowGeom'] = self.saveGeometry()
|
||||||
self.pm.profile['mainWindowState'] = self.saveState()
|
self.pm.profile['mainWindowState'] = self.saveState()
|
||||||
self.pm.save()
|
self.pm.save()
|
||||||
self.pm.profile = None
|
|
||||||
self.hide()
|
self.hide()
|
||||||
if browser:
|
|
||||||
self.showProfileManager()
|
# at this point there should be no windows left
|
||||||
else:
|
self._checkForUnclosedWidgets()
|
||||||
self.cleanupAndExit()
|
|
||||||
|
self.maybeAutoSync()
|
||||||
|
|
||||||
|
self.pm.profile = None
|
||||||
|
|
||||||
|
def _checkForUnclosedWidgets(self):
|
||||||
|
for w in self.app.topLevelWidgets():
|
||||||
|
if w.isVisible():
|
||||||
|
showWarning(f"Window should have been closed: {w}")
|
||||||
|
|
||||||
|
def unloadProfileAndExit(self):
|
||||||
|
self.unloadProfile(self.cleanupAndExit)
|
||||||
|
|
||||||
|
def unloadProfileAndShowProfileManager(self):
|
||||||
|
self.unloadProfile(self.showProfileManager)
|
||||||
|
|
||||||
def cleanupAndExit(self):
|
def cleanupAndExit(self):
|
||||||
self.errorHandler.unload()
|
self.errorHandler.unload()
|
||||||
|
@ -263,62 +276,51 @@ To import into a password protected profile, please open the profile before atte
|
||||||
cpath = self.pm.collectionPath()
|
cpath = self.pm.collectionPath()
|
||||||
try:
|
try:
|
||||||
self.col = Collection(cpath, log=True)
|
self.col = Collection(cpath, log=True)
|
||||||
except anki.db.Error:
|
except Exception as e:
|
||||||
# warn user
|
|
||||||
showWarning(_("""\
|
showWarning(_("""\
|
||||||
Your collection is corrupt. Please create a new profile, then \
|
Anki was unable to open your collection file. If problems persist after \
|
||||||
see the manual for how to restore from an automatic backup.
|
restarting your computer, please see the manual for how to restore from \
|
||||||
|
an automatic backup.
|
||||||
|
|
||||||
Debug info:
|
Debug info:
|
||||||
""")+traceback.format_exc())
|
""")+traceback.format_exc())
|
||||||
self.unloadProfile()
|
self.showProfileManager()
|
||||||
except Exception as e:
|
return False
|
||||||
# the custom exception handler won't catch this if we immediately
|
|
||||||
# unload, so we have to manually handle it
|
|
||||||
if "invalidTempFolder" in repr(str(e)):
|
|
||||||
showWarning(self.errorHandler.tempFolderMsg())
|
|
||||||
self.unloadProfile()
|
|
||||||
return
|
|
||||||
self.unloadProfile()
|
|
||||||
raise
|
|
||||||
self.progress.setupDB(self.col.db)
|
self.progress.setupDB(self.col.db)
|
||||||
self.maybeEnableUndo()
|
self.maybeEnableUndo()
|
||||||
self.moveToState("deckBrowser")
|
self.moveToState("deckBrowser")
|
||||||
|
return True
|
||||||
|
|
||||||
def unloadCollection(self):
|
def unloadCollection(self, onsuccess):
|
||||||
"""
|
def callback():
|
||||||
Unload the collection.
|
self._unloadCollection()
|
||||||
|
onsuccess()
|
||||||
|
|
||||||
This unloads a collection if there is one and returns True if
|
self.closeAllWindows(callback)
|
||||||
there is no collection after the call. (Because the unload
|
|
||||||
worked or because there was no collection to start with.)
|
def _unloadCollection(self):
|
||||||
"""
|
self.progress.start(label=_("Backing Up..."), immediate=True)
|
||||||
if self.col:
|
corrupt = False
|
||||||
if not self.closeAllCollectionWindows():
|
try:
|
||||||
return
|
self.maybeOptimize()
|
||||||
self.progress.start(immediate=True)
|
if not devMode:
|
||||||
corrupt = False
|
corrupt = self.col.db.scalar("pragma integrity_check") != "ok"
|
||||||
try:
|
except:
|
||||||
self.maybeOptimize()
|
corrupt = True
|
||||||
except:
|
if corrupt:
|
||||||
corrupt = True
|
showWarning(_("Your collection file appears to be corrupt. \
|
||||||
if not corrupt:
|
|
||||||
if devMode:
|
|
||||||
corrupt = False
|
|
||||||
else:
|
|
||||||
corrupt = self.col.db.scalar("pragma integrity_check") != "ok"
|
|
||||||
if corrupt:
|
|
||||||
showWarning(_("Your collection file appears to be corrupt. \
|
|
||||||
This can happen when the file is copied or moved while Anki is open, or \
|
This can happen when the file is copied or moved while Anki is open, or \
|
||||||
when the collection is stored on a network or cloud drive. Please see \
|
when the collection is stored on a network or cloud drive. Please see \
|
||||||
the manual for information on how to restore from an automatic backup."))
|
the manual for information on how to restore from an automatic backup."))
|
||||||
|
try:
|
||||||
self.col.close()
|
self.col.close()
|
||||||
self.col = None
|
except:
|
||||||
if not corrupt:
|
pass
|
||||||
self.backup()
|
self.col = None
|
||||||
self.progress.finish()
|
if not corrupt:
|
||||||
return True
|
self.backup()
|
||||||
|
self.progress.finish()
|
||||||
|
|
||||||
# Backup and auto-optimize
|
# Backup and auto-optimize
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -328,6 +330,8 @@ the manual for information on how to restore from an automatic backup."))
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.data = data
|
self.data = data
|
||||||
|
# make sure we complete before exiting the program
|
||||||
|
self.setDaemon(True)
|
||||||
# create the file in calling thread to ensure the same
|
# create the file in calling thread to ensure the same
|
||||||
# file is not created twice
|
# file is not created twice
|
||||||
open(self.path, "wb").close()
|
open(self.path, "wb").close()
|
||||||
|
@ -529,9 +533,8 @@ title="%s" %s>%s</button>''' % (
|
||||||
self.mainLayout.addWidget(sweb)
|
self.mainLayout.addWidget(sweb)
|
||||||
self.form.centralwidget.setLayout(self.mainLayout)
|
self.form.centralwidget.setLayout(self.mainLayout)
|
||||||
|
|
||||||
def closeAllCollectionWindows(self):
|
def closeAllWindows(self, onsuccess):
|
||||||
aqt.dialogs.closeAll(lambda: 1)
|
return aqt.dialogs.closeAll(onsuccess)
|
||||||
return True
|
|
||||||
|
|
||||||
# Components
|
# Components
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -579,21 +582,34 @@ title="%s" %s>%s</button>''' % (
|
||||||
# Syncing
|
# Syncing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def onSync(self, auto=False, reload=True):
|
# expects a current profile and a loaded collection; reloads
|
||||||
if not auto or (self.pm.profile['syncKey'] and
|
# collection after sync completes
|
||||||
self.pm.profile['autoSync'] and
|
def onSync(self):
|
||||||
not self.safeMode):
|
self.unloadCollection(self._onSync)
|
||||||
from aqt.sync import SyncManager
|
|
||||||
if not self.unloadCollection():
|
def _onSync(self):
|
||||||
return
|
self._sync()
|
||||||
# set a sync state so the refresh timer doesn't fire while deck
|
if not self.loadCollection():
|
||||||
# unloaded
|
return
|
||||||
self.state = "sync"
|
|
||||||
self.syncer = SyncManager(self, self.pm)
|
# expects a current profile, but no collection loaded
|
||||||
self.syncer.sync()
|
def maybeAutoSync(self):
|
||||||
if reload:
|
if (not self.pm.profile['syncKey']
|
||||||
if not self.col:
|
or not self.pm.profile['autoSync']
|
||||||
self.loadCollection()
|
or self.safeMode):
|
||||||
|
return
|
||||||
|
if self.pendingImport and os.path.basename(
|
||||||
|
self.pendingImport).startswith("backup-"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# ok to sync
|
||||||
|
self._sync()
|
||||||
|
|
||||||
|
def _sync(self):
|
||||||
|
from aqt.sync import SyncManager
|
||||||
|
self.state = "sync"
|
||||||
|
self.syncer = SyncManager(self, self.pm)
|
||||||
|
self.syncer.sync()
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -654,16 +670,9 @@ title="%s" %s>%s</button>''' % (
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"User hit the X button, etc."
|
"User hit the X button, etc."
|
||||||
event.accept()
|
# ignore the event for now, as we need time to clean up
|
||||||
self.onClose(force=True)
|
event.ignore()
|
||||||
|
self.unloadProfileAndExit()
|
||||||
def onClose(self, force=False):
|
|
||||||
"Called from a shortcut key. Close current active window."
|
|
||||||
aw = self.app.activeWindow()
|
|
||||||
if not aw or aw == self or force:
|
|
||||||
self.unloadProfile(browser=False)
|
|
||||||
else:
|
|
||||||
aw.close()
|
|
||||||
|
|
||||||
# Undo & autosave
|
# Undo & autosave
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -797,7 +806,8 @@ title="%s" %s>%s</button>''' % (
|
||||||
|
|
||||||
def setupMenus(self):
|
def setupMenus(self):
|
||||||
m = self.form
|
m = self.form
|
||||||
m.actionSwitchProfile.triggered.connect(lambda b: self.unloadProfile())
|
m.actionSwitchProfile.triggered.connect(
|
||||||
|
self.unloadProfileAndShowProfileManager)
|
||||||
m.actionImport.triggered.connect(self.onImport)
|
m.actionImport.triggered.connect(self.onImport)
|
||||||
m.actionExport.triggered.connect(self.onExport)
|
m.actionExport.triggered.connect(self.onExport)
|
||||||
m.actionExit.triggered.connect(self.close)
|
m.actionExit.triggered.connect(self.close)
|
||||||
|
|
Loading…
Reference in a new issue