From f93910128f1a14f2b5139511b3b900d369757fff Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 21 Jul 2010 13:16:07 +0900 Subject: [PATCH] multi-deck sync and related improvements - deck open & browser refresh done after splash screen hidden now - splash reduced to 3 steps - new options sync on program load/close - per-deck auto sync disabled on upgrade - plugins are now always loaded before a deck has been opened - don't prompt for sync params in auto sync, for both all and single deck - refresh deck browser after multi sync - wait on sync thread until syncFinished called, could fix crashes - after a full sync, ensure interface is still disabled - sync menu option now available in deck browser - new option to tell a progress window it should appear immediately --- ankiqt/__init__.py | 2 +- ankiqt/config.py | 10 ++- ankiqt/ui/main.py | 128 +++++++++++++++++++++++---------------- ankiqt/ui/preferences.py | 4 ++ ankiqt/ui/sync.py | 78 +++++++++++++++++------- ankiqt/ui/utils.py | 7 ++- designer/main.ui | 2 +- designer/preferences.ui | 28 +++++++-- 8 files changed, 173 insertions(+), 86 deletions(-) diff --git a/ankiqt/__init__.py b/ankiqt/__init__.py index 896b7b916..8fc0502f8 100644 --- a/ankiqt/__init__.py +++ b/ankiqt/__init__.py @@ -142,7 +142,7 @@ def run(): import forms import ui - ui.splash = SplashScreen(5) + ui.splash = SplashScreen(3) import anki if anki.version != appVersion: diff --git a/ankiqt/config.py b/ankiqt/config.py index 3bb76d476..d3d3ce92f 100644 --- a/ankiqt/config.py +++ b/ankiqt/config.py @@ -99,12 +99,18 @@ class Config(dict): 'suppressEstimates': False, 'suppressUpdate': False, 'syncInMsgBox': False, - 'syncOnClose': True, - 'syncOnLoad': True, + 'syncOnClose': False, + 'syncOnLoad': False, + 'syncOnProgramClose': True, + 'syncOnProgramOpen': True, 'syncPassword': "", 'syncUsername': "", 'typeAnswerFontSize': 20, } + # disable sync on deck load when upgrading + if not self.has_key("syncOnProgramOpen"): + self['syncOnLoad'] = False + self['syncOnClose'] = False for (k,v) in fields.items(): if not self.has_key(k): self[k] = v diff --git a/ankiqt/ui/main.py b/ankiqt/ui/main.py index 75c3428bc..1e78ba68c 100755 --- a/ankiqt/ui/main.py +++ b/ankiqt/ui/main.py @@ -42,6 +42,7 @@ class AnkiQt(QMainWindow): self.state = "initial" self.hideWelcome = False self.views = [] + signal.signal(signal.SIGINT, self.onSigInt) self.setLang() self.setupStyle() self.setupFonts() @@ -69,37 +70,40 @@ class AnkiQt(QMainWindow): self.resize(500, 500) # load deck ui.splash.update() - if (args or self.config['loadLastDeck'] or - len(self.config['recentDeckPaths']) == 1) and \ - not self.maybeLoadLastDeck(args): - self.setEnabled(True) - self.moveToState("auto") - # check for updates - ui.splash.update() self.setupErrorHandler() self.setupMisc() # activate & raise is useful when run from the command line on osx self.activateWindow() self.raise_() + # plugins might be looking at this + self.state = "noDeck" self.loadPlugins() self.setupAutoUpdate() self.rebuildPluginsMenu() - # run after-init hook + # plugins loaded, now show interface + ui.splash.finish(self) + self.show() + # program open sync + if self.config['syncOnProgramOpen']: + self.syncDeck(interactive=False) + if (args or self.config['loadLastDeck'] or + len(self.config['recentDeckPaths']) == 1): + # open the last deck + self.maybeLoadLastDeck(args) + if self.deck: + # deck open sync? + if self.config['syncOnLoad'] and self.deck.syncName: + self.syncDeck(interactive=False) + elif not self.config['syncOnProgramOpen'] or not self.browserDecks: + # sync disabled or no user/pass, so draw deck browser manually + self.moveToState("noDeck") + # all setup is done, run after-init hook try: runHook('init') except: ui.utils.showWarning( _("Broken plugin:\n\n%s") % unicode(traceback.format_exc(), "utf-8", "replace")) - ui.splash.update() - ui.splash.finish(self) - # ensure actions are updated after plugins loaded - self.moveToState("auto") - self.show() - if (self.deck and self.config['syncOnLoad'] and - self.deck.syncName): - self.syncDeck(interactive=False) - signal.signal(signal.SIGINT, self.onSigInt) except: ui.utils.showInfo("Error during startup:\n%s" % traceback.format_exc()) @@ -1117,8 +1121,8 @@ your deck.""")) if not self.config['recentDeckPaths']: return toRemove = [] - if ui.splash.finished: - self.startProgress(max=len(self.config['recentDeckPaths'])) + self.startProgress(max=len(self.config['recentDeckPaths']), + immediate=True) for c, d in enumerate(self.config['recentDeckPaths']): if ui.splash.finished: self.updateProgress(_("Checking deck %(x)d of %(y)d...") % { @@ -1378,6 +1382,8 @@ later by using File>Close. if not self.saveAndClose(hideWelcome=True): event.ignore() else: + if self.config['syncOnProgramClose']: + self.syncDeck(interactive=False) self.prepareForExit() event.accept() self.app.quit() @@ -2095,7 +2101,8 @@ it to your friends. if not self.inMainWindow() and interactive: return self.setNotice() # vet input - self.ensureSyncParams() + if interactive: + self.ensureSyncParams() u=self.config['syncUsername'] p=self.config['syncPassword'] if not u or not p: @@ -2110,28 +2117,34 @@ it to your friends. self.deckProperties.dialog.qtabwidget.setCurrentIndex(1) self.showToolTip(_("Enable syncing, choose a name, then sync again.")) return - if self.deck is None and self.deckPath is None: - # qt on linux incorrectly accepts shortcuts for disabled actions - return - # hide all deck-associated dialogs - self.closeAllDeckWindows() - if self.deck: - # save first, so we can rollback on failure - self.deck.save() - # store data we need before closing the deck - self.deckPath = self.deck.path - self.syncName = self.deck.syncName or self.deck.name() - self.lastSync = self.deck.lastSync - if checkSources: - self.sourcesToCheck = self.deck.s.column0( - "select id from sources where syncPeriod != -1 " - "and syncPeriod = 0 or :t - lastSync > syncPeriod", - t=time.time()) - else: - self.sourcesToCheck = [] - self.deck.close() - self.deck = None - self.loadAfterSync = reload + if self.deck is None and getattr(self, 'deckPath', None) is None: + # sync all decks + self.loadAfterSync = -1 + self.syncName = None + self.sourcesToCheck = [] + self.syncDecks = self.decksToSync() + else: + # sync one deck + # hide all deck-associated dialogs + self.closeAllDeckWindows() + + if self.deck: + # save first, so we can rollback on failure + self.deck.save() + # store data we need before closing the deck + self.deckPath = self.deck.path + self.syncName = self.deck.syncName or self.deck.name() + self.lastSync = self.deck.lastSync + if checkSources: + self.sourcesToCheck = self.deck.s.column0( + "select id from sources where syncPeriod != -1 " + "and syncPeriod = 0 or :t - lastSync > syncPeriod", + t=time.time()) + else: + self.sourcesToCheck = [] + self.deck.close() + self.deck = None + self.loadAfterSync = reload # bug triggered by preferences dialog - underlying c++ widgets are not # garbage collected until the middle of the child thread self.state = "nostate" @@ -2146,7 +2159,7 @@ it to your friends. self.connect(self.syncThread, SIGNAL("noMatchingDeck"), self.selectSyncDeck) self.connect(self.syncThread, SIGNAL("syncClockOff"), self.syncClockOff) self.connect(self.syncThread, SIGNAL("cleanNewDeck"), self.cleanNewDeck) - self.connect(self.syncThread, SIGNAL("syncFinished"), self.syncFinished) + self.connect(self.syncThread, SIGNAL("syncFinished"), self.onSyncFinished) self.connect(self.syncThread, SIGNAL("openSyncProgress"), self.openSyncProgress) self.connect(self.syncThread, SIGNAL("closeSyncProgress"), self.closeSyncProgress) self.connect(self.syncThread, SIGNAL("updateSyncProgress"), self.updateSyncProgress) @@ -2158,16 +2171,27 @@ it to your friends. self.syncThread.start() self.switchToWelcomeScreen() self.setEnabled(False) - while not self.syncThread.isFinished(): + self.syncFinished = False + while not self.syncFinished: self.app.processEvents() self.syncThread.wait(100) self.setEnabled(True) return self.syncThread.ok - def syncFinished(self): + def decksToSync(self): + ok = [] + for d in self.config['recentDeckPaths']: + if os.path.exists(d): + ok.append(d) + return ok + + def onSyncFinished(self): "Reopen after sync finished." self.mainWin.buttonStack.show() - if self.loadAfterSync: + if self.loadAfterSync == -1: + # after sync all, so refresh browser list + self.moveToState("noDeck") + elif self.loadAfterSync: if self.loadAfterSync == 2: name = re.sub("[<>]", "", self.syncName) p = os.path.join(self.documentDir, name + ".anki") @@ -2183,6 +2207,7 @@ it to your friends. elif not self.hideWelcome: self.moveToState("noDeck") self.deckPath = None + self.syncFinished = True def selectSyncDeck(self, decks, create=True): name = ui.sync.DeckChooser(self, decks, create).getName() @@ -2195,7 +2220,7 @@ it to your friends. if not create: self.cleanNewDeck() else: - self.syncFinished() + self.onSyncFinished() def cleanNewDeck(self): "Unload a new deck if an initial sync failed." @@ -2213,7 +2238,7 @@ it to your friends. _("Since this can cause many problems with syncing,\n" "syncing is disabled until you fix the problem.") ) - self.syncFinished() + self.onSyncFinished() def showSyncWarning(self, text): ui.utils.showWarning(text, self) @@ -2259,6 +2284,8 @@ it to your friends. def fullSyncFinished(self): self.finishProgress() + # need to deactivate interface again + self.setEnabled(False) def fullSyncProgress(self, type, val): if type == "fromLocal": @@ -2277,7 +2304,6 @@ it to your friends. "Close", "Addcards", "Editdeck", - "Syncdeck", "DisplayProperties", "DeckProperties", "Undo", @@ -2654,13 +2680,13 @@ it to your friends. def setProgressParent(self, parent): self.progressParent = parent - def startProgress(self, max=0, min=0, title=None): + def startProgress(self, max=0, min=0, title=None, immediate=False): if self.mainThread != QThread.currentThread(): return self.setBusy() if not self.progressWins: parent = self.progressParent or self.app.activeWindow() or self - p = ui.utils.ProgressWin(parent, max, min, title) + p = ui.utils.ProgressWin(parent, max, min, title, immediate) else: p = None self.progressWins.append(p) diff --git a/ankiqt/ui/preferences.py b/ankiqt/ui/preferences.py index 4b0695b45..a1f962af0 100644 --- a/ankiqt/ui/preferences.py +++ b/ankiqt/ui/preferences.py @@ -101,6 +101,8 @@ class Preferences(QDialog): def setupNetwork(self): self.dialog.syncOnOpen.setChecked(self.config['syncOnLoad']) self.dialog.syncOnClose.setChecked(self.config['syncOnClose']) + self.dialog.syncOnProgramOpen.setChecked(self.config['syncOnProgramOpen']) + self.dialog.syncOnProgramClose.setChecked(self.config['syncOnProgramClose']) self.dialog.syncUser.setText(self.config['syncUsername']) self.dialog.syncPass.setText(self.config['syncPassword']) self.dialog.proxyHost.setText(self.config['proxyHost']) @@ -113,6 +115,8 @@ class Preferences(QDialog): def updateNetwork(self): self.config['syncOnLoad'] = self.dialog.syncOnOpen.isChecked() self.config['syncOnClose'] = self.dialog.syncOnClose.isChecked() + self.config['syncOnProgramOpen'] = self.dialog.syncOnProgramOpen.isChecked() + self.config['syncOnProgramClose'] = self.dialog.syncOnProgramClose.isChecked() self.config['syncUsername'] = unicode(self.dialog.syncUser.text()) self.config['syncPassword'] = unicode(self.dialog.syncPass.text()) self.config['proxyHost'] = unicode(self.dialog.proxyHost.text()) diff --git a/ankiqt/ui/sync.py b/ankiqt/ui/sync.py index 4c2093713..1711d63e2 100755 --- a/ankiqt/ui/sync.py +++ b/ankiqt/ui/sync.py @@ -10,6 +10,7 @@ from anki.sync import SyncClient, HttpSyncServerProxy, copyLocalMedia from anki.sync import SYNC_HOST, SYNC_PORT from anki.errors import * from anki import DeckStorage +from anki.db import sqlite import ankiqt.forms from anki.hooks import addHook, removeHook @@ -29,6 +30,7 @@ class Sync(QThread): self.ok = True self.onlyMerge = onlyMerge self.sourcesToCheck = sourcesToCheck + self.proxy = None addHook('fullSyncStarted', self.fullSyncStarted) addHook('fullSyncFinished', self.fullSyncFinished) addHook('fullSyncProgress', self.fullSyncProgress) @@ -37,7 +39,10 @@ class Sync(QThread): self.emit(SIGNAL("setStatus"), msg, timeout) def run(self): - self.syncDeck() + if self.parent.syncName: + self.syncDeck() + else: + self.syncAllDecks() removeHook('fullSyncStarted', self.fullSyncStarted) removeHook('fullSyncFinished', self.fullSyncFinished) removeHook('fullSyncProgress', self.fullSyncProgress) @@ -77,22 +82,44 @@ class Sync(QThread): def connect(self, *args): # connect, check auth - proxy = HttpSyncServerProxy(self.user, self.pwd) - proxy.sourcesToCheck = self.sourcesToCheck - proxy.connect("ankiqt-" + ankiqt.appVersion) - return proxy + if not self.proxy: + self.setStatus(_("Connecting..."), 0) + proxy = HttpSyncServerProxy(self.user, self.pwd) + proxy.sourcesToCheck = self.sourcesToCheck + proxy.connect("ankiqt-" + ankiqt.appVersion) + self.proxy = proxy + return self.proxy - def syncDeck(self): - self.setStatus(_("Connecting..."), 0) + def syncAllDecks(self): + decks = self.parent.syncDecks + for d in decks: + self.syncDeck(deck=d) + self.emit(SIGNAL("syncFinished")) + + def syncDeck(self, deck=None): + # multi-mode setup + if deck: + c = sqlite.connect(deck) + syncName = c.execute("select syncName from decks").fetchone()[0] + c.close() + if not syncName: + return + path = deck + else: + syncName = self.parent.syncName + path = self.parent.deckPath + # ensure deck mods cached try: proxy = self.connect() except SyncError, e: return self.error(e) # exists on server? - if not proxy.hasDeck(self.parent.syncName): + if not proxy.hasDeck(syncName): + if deck: + return if self.create: try: - proxy.createDeck(self.parent.syncName) + proxy.createDeck(syncName) except SyncError, e: return self.error(e) else: @@ -100,17 +127,18 @@ class Sync(QThread): self.emit(SIGNAL("noMatchingDeck"), keys, not self.onlyMerge) self.setStatus("") return + self.setStatus(_("Syncing %s...") % syncName, 0) timediff = abs(proxy.timestamp - time.time()) if timediff > 300: self.emit(SIGNAL("syncClockOff"), timediff) return - # reconnect + # reopen self.deck = None try: - self.deck = DeckStorage.Deck(self.parent.deckPath) + self.deck = DeckStorage.Deck(path) client = SyncClient(self.deck) client.setServer(proxy) - proxy.deckName = self.parent.syncName + proxy.deckName = syncName # need to do anything? start = time.time() if client.prepareSync(): @@ -129,15 +157,16 @@ class Sync(QThread): client.fullSyncFromServer(ret[1], ret[2]) self.setStatus(_("Sync complete."), 0) # reopen the deck in case we have sources - self.deck = DeckStorage.Deck(self.parent.deckPath) + self.deck = DeckStorage.Deck(path) client.deck = self.deck else: # diff self.setStatus(_("Determining differences..."), 0) payload = client.genPayload(sums) # send payload - pr = client.payloadChangeReport(payload) - self.setStatus("
" + pr + "
", 0) + if not deck: + pr = client.payloadChangeReport(payload) + self.setStatus("
" + pr + "
", 0) self.setStatus(_("Transferring payload..."), 0) res = client.server.applyPayload(payload) # apply reply @@ -150,7 +179,8 @@ class Sync(QThread): self.deck.s.commit() else: changes = False - self.setStatus(_("No changes found.")) + if not deck: + self.setStatus(_("No changes found.")) # check sources srcChanged = False if self.sourcesToCheck: @@ -177,12 +207,13 @@ class Sync(QThread): self.deck.s.commit() # close and send signal to main thread self.deck.close() - taken = time.time() - start - if (changes or srcChanged) and taken < 2.5: - time.sleep(2.5 - taken) - else: - time.sleep(0.25) - self.emit(SIGNAL("syncFinished")) + if not deck: + taken = time.time() - start + if (changes or srcChanged) and taken < 2.5: + time.sleep(2.5 - taken) + else: + time.sleep(0.25) + self.emit(SIGNAL("syncFinished")) except Exception, e: self.ok = False #traceback.print_exc() @@ -192,7 +223,8 @@ class Sync(QThread): err = `getattr(e, 'data', None) or e` self.setStatus(_("Syncing failed: %(a)s") % { 'a': err}) - self.error(e) + if not deck: + self.error(e) # Choosing a deck to sync to ########################################################################## diff --git a/ankiqt/ui/utils.py b/ankiqt/ui/utils.py index aba453256..1650e2522 100644 --- a/ankiqt/ui/utils.py +++ b/ankiqt/ui/utils.py @@ -235,7 +235,7 @@ def getBase(deck, card): class ProgressWin(object): - def __init__(self, parent, max=0, min=0, title=None): + def __init__(self, parent, max=0, min=0, title=None, immediate=False): if not title: title = "Anki" self.diag = QProgressDialog("", "", min, max, parent) @@ -245,7 +245,10 @@ class ProgressWin(object): self.diag.setAutoReset(False) # qt doesn't seem to honour this consistently, and it's not triggered # by the db progress handler, so we set it high and use maybeShow() below - self.diag.setMinimumDuration(100000) + if immediate: + self.diag.show() + else: + self.diag.setMinimumDuration(100000) self.counter = min self.min = min self.max = max diff --git a/designer/main.ui b/designer/main.ui index 2ff1a9a0f..ce117390b 100644 --- a/designer/main.ui +++ b/designer/main.ui @@ -2972,7 +2972,7 @@ S&ync - Synchronize this deck with Anki Online + Synchronize with Anki Online Ctrl+Shift+Y diff --git a/designer/preferences.ui b/designer/preferences.ui index 5fc1acbf8..5e0792eb0 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -204,7 +204,7 @@ - Sync on close + Sync on deck close true @@ -221,13 +221,27 @@ - Sync on open + Sync on deck open true + + + + Sync on program open + + + + + + + Sync on program close + + + @@ -689,6 +703,8 @@ syncPass syncOnOpen syncOnClose + syncOnProgramOpen + syncOnProgramClose proxyHost proxyPort proxyUser @@ -720,8 +736,8 @@ accept() - 270 - 412 + 279 + 457 157 @@ -736,8 +752,8 @@ reject() - 317 - 412 + 326 + 457 286