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
This commit is contained in:
Damien Elmes 2010-07-21 13:16:07 +09:00
parent 77ede46ebb
commit f93910128f
8 changed files with 173 additions and 86 deletions

View file

@ -142,7 +142,7 @@ def run():
import forms
import ui
ui.splash = SplashScreen(5)
ui.splash = SplashScreen(3)
import anki
if anki.version != appVersion:

View file

@ -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

View file

@ -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)

View file

@ -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())

View file

@ -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 <b>%s</b>...") % 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("<br>" + pr + "<br>", 0)
if not deck:
pr = client.payloadChangeReport(payload)
self.setStatus("<br>" + pr + "<br>", 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
##########################################################################

View file

@ -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

View file

@ -2972,7 +2972,7 @@
<string>S&amp;ync</string>
</property>
<property name="statusTip">
<string>Synchronize this deck with Anki Online</string>
<string>Synchronize with Anki Online</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+Y</string>

View file

@ -204,7 +204,7 @@
<item row="3" column="0">
<widget class="QCheckBox" name="syncOnClose">
<property name="text">
<string>Sync on close</string>
<string>Sync on deck close</string>
</property>
<property name="checked">
<bool>true</bool>
@ -221,13 +221,27 @@
<item row="2" column="0">
<widget class="QCheckBox" name="syncOnOpen">
<property name="text">
<string>Sync on open</string>
<string>Sync on deck open</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="syncOnProgramOpen">
<property name="text">
<string>Sync on program open</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="syncOnProgramClose">
<property name="text">
<string>Sync on program close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@ -689,6 +703,8 @@
<tabstop>syncPass</tabstop>
<tabstop>syncOnOpen</tabstop>
<tabstop>syncOnClose</tabstop>
<tabstop>syncOnProgramOpen</tabstop>
<tabstop>syncOnProgramClose</tabstop>
<tabstop>proxyHost</tabstop>
<tabstop>proxyPort</tabstop>
<tabstop>proxyUser</tabstop>
@ -720,8 +736,8 @@
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>270</x>
<y>412</y>
<x>279</x>
<y>457</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
@ -736,8 +752,8 @@
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>317</x>
<y>412</y>
<x>326</x>
<y>457</y>
</hint>
<hint type="destinationlabel">
<x>286</x>