From 698ba38e74920bf812e4647cbb1fae184e351d66 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 27 Mar 2011 13:51:13 +0900 Subject: [PATCH] move various major functions into separate files --- aqt/cram.py | 46 ++ aqt/editcurrent.py | 49 ++ aqt/main.py | 1272 ++++++-------------------------------------- aqt/reviewer.py | 17 + aqt/share.py | 89 ++++ aqt/studyopts.py | 284 ++++++++++ aqt/sync.py | 301 +++++++++++ 7 files changed, 937 insertions(+), 1121 deletions(-) create mode 100644 aqt/cram.py create mode 100644 aqt/editcurrent.py create mode 100644 aqt/share.py create mode 100644 aqt/studyopts.py diff --git a/aqt/cram.py b/aqt/cram.py new file mode 100644 index 000000000..6806f1ac9 --- /dev/null +++ b/aqt/cram.py @@ -0,0 +1,46 @@ +# Copyright: Damien Elmes +# -*- coding: utf-8 -*- +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + + def onCram(self, cardIds=[]): + te = aqt.tagedit.TagEdit(self) + te.setDeck(self.deck, "all") + diag = GetTextDialog( + self, _("Tags to cram:"), help="CramMode", edit=te) + l = diag.layout() + g = QGroupBox(_("Review Mode")) + l.insertWidget(2, g) + box = QVBoxLayout() + g.setLayout(box) + keep = QRadioButton(_("Show oldest modified first")) + box.addWidget(keep) + keep.setChecked(True) + diag.setTabOrder(diag.l, keep) + order = QRadioButton(_("Show in order added")) + box.addWidget(order) + random = QRadioButton(_("Show in random order")) + box.addWidget(random) + # hide tag list if we have ids + if cardIds: + diag.l.hide() + diag.qlabel.hide() + if diag.exec_(): + if keep.isChecked(): + order = "type, modified" + elif order.isChecked(): + order = "created" + else: + order = "random()" + if cardIds: + active = cardIds + else: + active = unicode(diag.l.text()) + self.deck.setupCramScheduler(active, order) + if self.state == "studyScreen": + self.onStartReview() + else: + self.deck.reset() + self.deck.getCard() # so scheduler will reset if empty + self.moveToState("initial") + if not self.deck.finishScheduler: + showInfo(_("No cards matched the provided tags.")) diff --git a/aqt/editcurrent.py b/aqt/editcurrent.py new file mode 100644 index 000000000..363c070ff --- /dev/null +++ b/aqt/editcurrent.py @@ -0,0 +1,49 @@ +# Copyright: Damien Elmes +# -*- coding: utf-8 -*- +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html + +class EditCurrent(object): + + def __init__(self, mw): + self.mw = mw + + def _editCurrentState(self, oldState): + if self.lastState == "editCurrentFact": + return self.moveToState("saveEdit") + self.form.actionRepeatAudio.setEnabled(False) + self.deck.db.flush() + self.showEditor() + + def _saveEditState(self, oldState): + self.form.actionRepeatAudio.setEnabled(True) + self.editor.saveFieldsNow() + self.form.buttonStack.show() + return self.reset() + + # Edit current fact + ########################################################################## + + def setupEditor(self): + print "setupeditor" + return + self.editor = aqt.facteditor.FactEditor( + self, self.form.fieldsArea, self.deck) + self.editor.clayout.setShortcut("") + self.editor.onFactValid = self.onFactValid + self.editor.onFactInvalid = self.onFactInvalid + self.editor.resetOnEdit = False + # editor + self.connect(self.form.saveEditorButton, SIGNAL("clicked()"), + lambda: self.moveToState("saveEdit")) + + def showEditor(self): + self.form.buttonStack.hide() + self.switchToEditScreen() + self.editor.setFact(self.currentCard.fact) + self.editor.card = self.currentCard + + def onFactValid(self, fact): + self.form.saveEditorButton.setEnabled(True) + + def onFactInvalid(self, fact): + self.form.saveEditorButton.setEnabled(False) diff --git a/aqt/main.py b/aqt/main.py index eafe6e40e..cffd1d937 100755 --- a/aqt/main.py +++ b/aqt/main.py @@ -45,9 +45,9 @@ class AnkiQt(QMainWindow): self.activateWindow() self.raise_() # sync on program open? - if self.config['syncOnProgramOpen']: - if self.syncDeck(interactive=False): - return + # if self.config['syncOnProgramOpen']: + # if self.syncDeck(interactive=False): + # return # load a deck? if (args or self.config['loadLastDeck'] or len(self.config['recentDeckPaths']) == 1): @@ -66,7 +66,6 @@ class AnkiQt(QMainWindow): self.setupMainWindow() self.setupStyle() self.setupProxy() - self.setupSound() self.setupMenus() self.setupToolbar() self.setupProgress() @@ -81,7 +80,6 @@ class AnkiQt(QMainWindow): self.setupOverview() self.setupReviewer() self.setupEditor() - self.setupStudyScreen() # State machine ########################################################################## @@ -112,25 +110,8 @@ class AnkiQt(QMainWindow): def _reviewState(self, oldState): self.reviewer.show() - def _editCurrentFactState(self, oldState): - if self.lastState == "editCurrentFact": - return self.moveToState("saveEdit") - self.form.actionRepeatAudio.setEnabled(False) - self.deck.db.flush() - self.showEditor() - - def _saveEditState(self, oldState): - self.form.actionRepeatAudio.setEnabled(True) - self.editor.saveFieldsNow() - self.form.buttonStack.show() - return self.reset() - - def _studyScreenState(self, oldState): - self.currentCard = None - # if self.deck.finishScheduler: - # self.deck.finishScheduler() - self.disableCardMenuItems() - self.showStudyScreen() + def _editCurrentState(self, oldState): + pass def reset(self): self.deck.reset() @@ -161,28 +142,6 @@ hr { margin:5 0 5 0; border:0; height:1px; background-color:#ccc; } title="%s">%s''' % ( id, class_, link, key, name) - # Signal handling - ########################################################################## - - def setupSignals(self): - signal.signal(signal.SIGINT, self.onSigInt) - - def onSigInt(self, signum, frame): - self.close() - - # Progress handling - ########################################################################## - - def setupProgress(self): - self.progress = aqt.progress.ProgressManager(self) - - # Error handling - ########################################################################## - - def setupErrorHandler(self): - import aqt.errors - self.errorHandler = aqt.errors.ErrorHandler(self) - # Main window setup ########################################################################## @@ -197,127 +156,32 @@ title="%s">%s''' % ( self.mainLayout.addWidget(self.web) self.mainLayout.setContentsMargins(0,0,0,0) self.form.centralwidget.setLayout(self.mainLayout) - #self.help = aqt.help.HelpArea(self.form.helpFrame, self.config, self) - #self.connect(self.web.pageAction(QWebPage.Reload), - # SIGNAL("triggered()"), - # self.onReload) - # congrats - # self.connect(self.mainWin.learnMoreButton, - # SIGNAL("clicked()"), - # self.onLearnMore) - # self.connect(self.mainWin.reviewEarlyButton, - # SIGNAL("clicked()"), - # self.onReviewEarly) - # self.connect(self.mainWin.finishButton, - # SIGNAL("clicked()"), - # self.onClose) - # notices - #self.form.noticeFrame.setShown(False) - # self.connect(self.form.noticeButton, SIGNAL("clicked()"), - # lambda: self.form.noticeFrame.setShown(False)) - # if sys.platform.startswith("win32"): - # self.form.noticeButton.setFixedWidth(24) - # elif sys.platform.startswith("darwin"): - # self.form.noticeButton.setFixedWidth(20) - # self.form.noticeButton.setFixedHeight(20) - addHook("cardAnswered", self.onCardAnsweredHook) addHook("undoEnd", self.maybeEnableUndo) - addHook("notify", self.onNotify) if self.config['mainWindowState']: restoreGeom(self, "mainWindow", 21) restoreState(self, "mainWindow") else: - self.resize(500, 500) + self.resize(500, 400) def closeAllDeckWindows(self): print "closealldeckwindows()" #aqt.dialogs.closeAll() - # to port - # elif self.state == "studyScreen": - # if evt.key() in (Qt.Key_Enter, - # Qt.Key_Return): - # evt.accept() - # return self.onStartReview() - # elif self.state == "editCurrentFact": - # if evt.key() == Qt.Key_Escape: - # evt.accept() - # return self.moveToState("saveEdit") - # evt.ignore() - - def onCardAnsweredHook(self, cardId, isLeech): - if not isLeech: - self.setNotice() - return - txt = (_("""\ -%s... is a leech.""") - % stripHTML(stripSounds(self.currentCard.question)).\ - replace("\n", " ")[0:30]) - if isLeech and self.deck.db.scalar( - "select 1 from cards where id = :id and type < 0", id=cardId): - txt += _(" It has been suspended.") - self.setNotice(txt) - - def startRefreshTimer(self): - "Update the screen once a minute until next card is displayed." - if getattr(self, 'refreshTimer', None): - return - self.refreshTimer = QTimer(self) - self.refreshTimer.start(60000) - self.connect(self.refreshTimer, SIGNAL("timeout()"), self.refreshStatus) - # start another time to refresh exactly after we've finished - next = self.deck.earliestTime() - if next: - delay = next - time.time() - if delay > 86400: - return - if delay < 0: - c = self.deck.getCard() - if c: - return self.moveToState("auto") - sys.stderr.write("""\ -earliest time returned %f - -please report this error, but it's not serious. -closing and opening your deck should fix it. - -counts are %d %d %d -""" % (delay, - self.deck.failedSoonCount, - self.deck.revCount, - self.deck.newCount)) - return - t = QTimer(self) - t.setSingleShot(True) - self.connect(t, SIGNAL("timeout()"), self.refreshStatus) - t.start((delay+1)*1000) - - def refreshStatus(self): - "If triggered when the deck is finished, reset state." - if self.inDbHandler: - return - if self.state == "deckFinished": - # don't try refresh if the deck is closed during a sync - if self.deck: - self.moveToState("getQuestion") - if self.state != "deckFinished": - if self.refreshTimer: - self.refreshTimer.stop() - self.refreshTimer = None - - # Main stack + # Components ########################################################################## - # def switchToEditScreen(self): - # self.form.mainStack.setCurrentIndex(0) + def setupSignals(self): + signal.signal(signal.SIGINT, self.onSigInt) - # def switchToMainScreen(self): - # self.form.mainStack.setCurrentIndex(1) + def onSigInt(self, signum, frame): + self.close() + def setupProgress(self): + self.progress = aqt.progress.ProgressManager(self) - - # misc - ########################################################################## + def setupErrorHandler(self): + import aqt.errors + self.errorHandler = aqt.errors.ErrorHandler(self) def setupVersion(self): # check if we've been updated @@ -342,28 +206,27 @@ counts are %d %d %d def inMainThread(self): return self._mainThread == QThread.currentThread() - def onReload(self): - self.moveToState("auto") + def setupDeckBrowser(self): + from aqt.deckbrowser import DeckBrowser + self.deckBrowser = DeckBrowser(self) - def onNotify(self, msg): - if not self.inMainThread(): - # decks may be opened in a sync thread - sys.stderr.write(msg + "\n") - else: - showInfo(msg) + def setupOverview(self): + from aqt.overview import Overview + self.overview = Overview(self) - def setNotice(self, str=""): - if str: - self.form.noticeLabel.setText(str) - self.form.noticeFrame.setShown(True) - else: - self.form.noticeFrame.setShown(False) + def setupReviewer(self): + from aqt.reviewer import Reviewer + self.reviewer = Reviewer(self) - # Deck loading & saving: backend + def setupEditor(self): + from aqt.editcurrent import EditCurrent + self.editor = EditCurrent(self) + + # Deck loading ########################################################################## def loadDeck(self, deckPath, showErrors=True): - "Load a deck and update the user interface. Maybe sync." + "Load a deck and update the user interface." try: self.deck = Deck(deckPath, queue=False) except Exception, e: @@ -385,12 +248,33 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") self.moveToState("deckLoading") return True + def onOpen(self): + self.raiseMain() + key = _("Deck files (*.anki)") + defaultDir = self.getDefaultDir() + file = QFileDialog.getOpenFileName(self, _("Open deck"), + defaultDir, key) + file = unicode(file) + if not file: + return False + ret = self.loadDeck(file) + if not ret: + if ret is None: + showWarning(_("Unable to load file.")) + self.deck = None + return False + else: + self.updateRecentFiles(file) + self.browserLastRefreshed = 0 + return True + def maybeLoadLastDeck(self, args): "Open the last deck if possible." # try a command line argument if available if args: f = unicode(args[0], sys.getfilesystemencoding()) - return self.loadDeck(f) + if os.path.exists(f): + return self.loadDeck(f) # try recent deck paths for path in self.config['recentDeckPaths']: r = self.loadDeck(path, showErrors=False) @@ -416,6 +300,9 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") self.config['recentDeckPaths'].insert(0, path) self.config.save() + # Open recent + ########################################################################## + def onSwitchToDeck(self): diag = QDialog(self) diag.setWindowTitle(_("Open Recent Deck")) @@ -447,37 +334,9 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") self.switchDeckDiag.close() self.loadDeck(self.switchDecks[idx][1]) - # New files, loading & saving + # New deck ########################################################################## - def onClose(self): - aw = self.app.activeWindow() - if not aw or aw == self: - self.close() - else: - aw.close() - - def close(self, showBrowser=True): - "(Auto)save and close. Prompt if necessary. True if okay to proceed." - if not self.deck: - return - # if we were cramming, restore the standard scheduler - if self.deck.stdSched(): - self.deck.reset() - runHook("deckClosing") - print "focusOut() should be handled with deckClosing now" - self.closeAllDeckWindows() - self.deck.close() - self.deck = None - if showBrowser: - self.moveToState("deckBrowser") - - def raiseMain(self): - if not self.app.activeWindow(): - # make sure window is shown - self.setWindowState(self.windowState() & ~Qt.WindowMinimized) - return True - def onNew(self, path=None, prompt=None): self.raiseMain() self.close() @@ -517,37 +376,34 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") if register: self.updateRecentFiles(self.deck.path) - def ensureSyncParams(self): - if not self.config['syncUsername'] or not self.config['syncPassword']: - d = QDialog(self) - vbox = QVBoxLayout() - l = QLabel(_( - '

Online Account

' - 'To use your free online account,
' - "please enter your details below.

" - "You can change your details later with
" - "Settings->Preferences->Sync
")) - l.setOpenExternalLinks(True) - vbox.addWidget(l) - g = QGridLayout() - l1 = QLabel(_("Username:")) - g.addWidget(l1, 0, 0) - user = QLineEdit() - g.addWidget(user, 0, 1) - l2 = QLabel(_("Password:")) - g.addWidget(l2, 1, 0) - passwd = QLineEdit() - passwd.setEchoMode(QLineEdit.Password) - g.addWidget(passwd, 1, 1) - vbox.addLayout(g) - bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) - self.connect(bb, SIGNAL("accepted()"), d.accept) - self.connect(bb, SIGNAL("rejected()"), d.reject) - vbox.addWidget(bb) - d.setLayout(vbox) - d.exec_() - self.config['syncUsername'] = unicode(user.text()) - self.config['syncPassword'] = unicode(passwd.text()) + # Closing + ########################################################################## + + def onClose(self): + "Called from a shortcut key. Close current active window." + aw = self.app.activeWindow() + if not aw or aw == self: + self.close() + else: + aw.close() + + def close(self, showBrowser=True): + "Close current deck." + if not self.deck: + return + # if we were cramming, restore the standard scheduler + if self.deck.stdSched(): + self.deck.reset() + runHook("deckClosing") + print "focusOut() should be handled with deckClosing now" + self.closeAllDeckWindows() + self.deck.close() + self.deck = None + if showBrowser: + self.moveToState("deckBrowser") + + # Downloading + ########################################################################## def onOpenOnline(self): self.raiseMain() @@ -578,95 +434,32 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") self.raiseMain() aqt.getshared.GetShared(self, 1) - def onOpen(self): - self.raiseMain() - key = _("Deck files (*.anki)") - defaultDir = self.getDefaultDir() - file = QFileDialog.getOpenFileName(self, _("Open deck"), - defaultDir, key) - file = unicode(file) - if not file: - return False - ret = self.loadDeck(file) - if not ret: - if ret is None: - showWarning(_("Unable to load file.")) - self.deck = None - return False - else: - self.updateRecentFiles(file) - self.browserLastRefreshed = 0 - return True + # Tools + ########################################################################## - def showToolTip(self, msg): - class CustomLabel(QLabel): - def mousePressEvent(self, evt): - evt.accept() - self.hide() - old = getattr(self, 'toolTipFrame', None) - if old: - old.deleteLater() - old = getattr(self, 'toolTipTimer', None) - if old: - old.stop() - old.deleteLater() - self.toolTipLabel = CustomLabel("""\ - - - - - -
%s
""" % msg) - self.toolTipLabel.setFrameStyle(QFrame.Panel) - self.toolTipLabel.setLineWidth(2) - self.toolTipLabel.setWindowFlags(Qt.ToolTip) - p = QPalette() - p.setColor(QPalette.Window, QColor("#feffc4")) - self.toolTipLabel.setPalette(p) - aw = (self.app.instance().activeWindow() or - self) - self.toolTipLabel.move( - aw.mapToGlobal(QPoint(0, -100 + aw.height()))) - self.toolTipLabel.show() - self.toolTipTimer = QTimer(self) - self.toolTipTimer.setSingleShot(True) - self.toolTipTimer.start(5000) - self.connect(self.toolTipTimer, SIGNAL("timeout()"), - self.closeToolTip) - - def closeToolTip(self): - label = getattr(self, 'toolTipLabel', None) - if label: - label.deleteLater() - self.toolTipLabel = None - timer = getattr(self, 'toolTipTimer', None) - if timer: - timer.stop() - timer.deleteLater() - self.toolTipTimer = None - - def save(self, required=False): - if not self.deck.modifiedSinceSave() and self.deck.path: - return True - if not self.deck.path: - if required: - # backed in memory, make sure it's saved - return self.onSaveAs() - else: - self.showToolTip(_("""\ -

Unsaved Deck

-Careful. You're editing an unsaved deck.
-Choose File -> Save to start autosaving
-your deck.""")) - return - self.deck.save() - self.updateTitleBar() + def raiseMain(self): + if not self.app.activeWindow(): + # make sure window is shown + self.setWindowState(self.windowState() & ~Qt.WindowMinimized) return True + def setStatus(self, text, timeout=3000): + self.form.statusbar.showMessage(text, timeout) + + def onStartHere(self): + QDesktopServices.openUrl(QUrl(aqt.appHelpSite)) + + def setupStyle(self): + applyStyles(self) + + # Renaming + ########################################################################## + def onRename(self): - "Prompt for a file name, then save." + "Rename deck." + print "rename" return - title = _("Save Deck As") + title = _("Rename Deck") if self.deck.path: dir = os.path.dirname(self.deck.path) else: @@ -698,22 +491,7 @@ your deck.""")) self.moveToState("initial") return file - # Components - ########################################################################## - - def setupDeckBrowser(self): - from aqt.deckbrowser import DeckBrowser - self.deckBrowser = DeckBrowser(self) - - def setupOverview(self): - from aqt.overview import Overview - self.overview = Overview(self) - - def setupReviewer(self): - from aqt.reviewer import Reviewer - self.reviewer = Reviewer(self) - - # Opening and closing the app + # App exit ########################################################################## def prepareForExit(self): @@ -730,323 +508,18 @@ your deck.""")) def closeEvent(self, event): "User hit the X button, etc." + print "fixme: exit from edit current, review, etc" if self.state == "editCurrentFact": event.ignore() return self.moveToState("saveEdit") self.close(showBrowser=False) - if self.config['syncOnProgramOpen']: - self.showBrowser = False - self.syncDeck(interactive=False) + # if self.config['syncOnProgramOpen']: + # self.showBrowser = False + # self.syncDeck(interactive=False) self.prepareForExit() event.accept() self.app.quit() - # Edit current fact - ########################################################################## - - def setupEditor(self): - print "setupeditor" - return - self.editor = aqt.facteditor.FactEditor( - self, self.form.fieldsArea, self.deck) - self.editor.clayout.setShortcut("") - self.editor.onFactValid = self.onFactValid - self.editor.onFactInvalid = self.onFactInvalid - self.editor.resetOnEdit = False - # editor - self.connect(self.form.saveEditorButton, SIGNAL("clicked()"), - lambda: self.moveToState("saveEdit")) - - - def showEditor(self): - self.form.buttonStack.hide() - self.switchToEditScreen() - self.editor.setFact(self.currentCard.fact) - self.editor.card = self.currentCard - - def onFactValid(self, fact): - self.form.saveEditorButton.setEnabled(True) - - def onFactInvalid(self, fact): - self.form.saveEditorButton.setEnabled(False) - - # Study screen - ########################################################################## - - def setupStudyScreen(self): - return - self.form.buttonStack.hide() - self.form.newCardOrder.insertItems( - 0, QStringList(anki.consts.newCardOrderLabels().values())) - self.form.newCardScheduling.insertItems( - 0, QStringList(anki.consts.newCardSchedulingLabels().values())) - self.form.revCardOrder.insertItems( - 0, QStringList(anki.consts.revCardOrderLabels().values())) - self.connect(self.form.optionsHelpButton, - SIGNAL("clicked()"), - lambda: QDesktopServices.openUrl(QUrl( - aqt.appWiki + "StudyOptions"))) - self.connect(self.form.minuteLimit, - SIGNAL("textChanged(QString)"), self.onMinuteLimitChanged) - self.connect(self.form.questionLimit, - SIGNAL("textChanged(QString)"), self.onQuestionLimitChanged) - self.connect(self.form.newPerDay, - SIGNAL("textChanged(QString)"), self.onNewLimitChanged) - self.connect(self.form.startReviewingButton, - SIGNAL("clicked()"), - self.onStartReview) - self.connect(self.form.newCardOrder, - SIGNAL("activated(int)"), self.onNewCardOrderChanged) - self.connect(self.form.failedCardMax, - SIGNAL("editingFinished()"), - self.onFailedMaxChanged) - self.connect(self.form.newCategories, - SIGNAL("clicked()"), self.onNewCategoriesClicked) - self.connect(self.form.revCategories, - SIGNAL("clicked()"), self.onRevCategoriesClicked) - self.form.tabWidget.setCurrentIndex(self.config['studyOptionsTab']) - - def onNewCategoriesClicked(self): - aqt.activetags.show(self, "new") - - def onRevCategoriesClicked(self): - aqt.activetags.show(self, "rev") - - def onFailedMaxChanged(self): - try: - v = int(self.form.failedCardMax.text()) - if v == 1 or v < 0: - v = 2 - self.deck.failedCardMax = v - except ValueError: - pass - self.form.failedCardMax.setText(str(self.deck.failedCardMax)) - self.deck.flushMod() - - def onMinuteLimitChanged(self, qstr): - try: - val = float(self.form.minuteLimit.text()) * 60 - if self.deck.sessionTimeLimit == val: - return - self.deck.sessionTimeLimit = val - except ValueError: - pass - self.deck.flushMod() - self.updateStudyStats() - - def onQuestionLimitChanged(self, qstr): - try: - val = int(self.form.questionLimit.text()) - if self.deck.sessionRepLimit == val: - return - self.deck.sessionRepLimit = val - except ValueError: - pass - self.deck.flushMod() - self.updateStudyStats() - - def onNewLimitChanged(self, qstr): - try: - val = int(self.form.newPerDay.text()) - if self.deck.newCardsPerDay == val: - return - self.deck.newCardsPerDay = val - except ValueError: - pass - self.deck.flushMod() - self.deck.reset() - self.statusView.redraw() - self.updateStudyStats() - - def onNewCardOrderChanged(self, ncOrd): - def uf(obj, field, value): - if getattr(obj, field) != value: - setattr(obj, field, value) - self.deck.flushMod() - if ncOrd != 0: - if self.deck.newCardOrder == 0: - # need to put back in order - self.mw.startProgress() - self.mw.updateProgress(_("Ordering...")) - self.deck.orderNewCards() - self.deck.finishProgress() - uf(self.deck, 'newCardOrder', ncOrd) - elif ncOrd == 0: - # (re-)randomize - self.deck.startProgress() - self.deck.updateProgress(_("Randomizing...")) - self.deck.randomizeNewCards() - self.deck.finishProgress() - uf(self.deck, 'newCardOrder', ncOrd) - - def updateActives(self): - labels = [ - _("Show All Due Cards"), - _("Show Chosen Categories") - ] - if self.deck.getVar("newActive") or self.deck.getVar("newInactive"): - new = labels[1] - else: - new = labels[0] - self.form.newCategoryLabel.setText(new) - if self.deck.getVar("revActive") or self.deck.getVar("revInactive"): - rev = labels[1] - else: - rev = labels[0] - self.form.revCategoryLabel.setText(rev) - - def updateStudyStats(self): - self.form.buttonStack.hide() - self.deck.reset() - self.updateActives() - wasReached = self.deck.timeboxReached() - sessionColour = '%s' - cardColour = '%s' - # top label - h = {} - h['ret'] = cardColour % (self.deck.revCount+self.deck.failedSoonCount) - h['new'] = cardColour % self.deck.newCount - h['newof'] = str(self.deck.newCountAll()) - # counts & time for today - todayStart = self.deck.failedCutoff - 86400 - sql = "select count(), sum(userTime) from revlog" - (reps, time_) = self.deck.db.first( - sql + " where time > :start", start=todayStart) - h['timeToday'] = sessionColour % ( - anki.utils.fmtTimeSpan(time_ or 0, short=True, point=1)) - h['repsToday'] = sessionColour % reps - # and yesterday - yestStart = todayStart - 86400 - (reps, time_) = self.deck.db.first( - sql + " where time > :start and time <= :end", - start=yestStart, end=todayStart) - h['timeTodayChg'] = str( - anki.utils.fmtTimeSpan(time_ or 0, short=True, point=1)) - h['repsTodayChg'] = str(reps) - # session counts - limit = self.deck.sessionTimeLimit - start = self.deck.sessionStartTime or time.time() - limit - start2 = self.deck.lastSessionStart or start - limit - last10 = self.deck.db.scalar( - "select count(*) from revlog where time >= :t", - t=start) - last20 = self.deck.db.scalar( - "select count(*) from revlog where " - "time >= :t and time < :t2", - t=start2, t2=start) - h['repsInSes'] = sessionColour % last10 - h['repsInSesChg'] = str(last20) - h['cs_header'] = "" + _("Cards/session:") + "" - h['cd_header'] = "" + _("Cards/day:") + "" - h['td_header'] = "" + _("Time/day:") + "" - h['rd_header'] = "" + _("Reviews due:") + "" - h['ntod_header'] = "" + _("New today:") + "" - h['ntot_header'] = "" + _("New total:") + "" - stats1 = ("""\ - - -
%(cs_header)s%(repsInSesChg)s%(repsInSes)s
-
- - - - - -
-%(cd_header)s%(repsTodayChg)s%(repsToday)s
%(td_header)s%(timeTodayChg)s%(timeToday)s
""") % h - - stats2 = ("""\ - - - - -
%(rd_header)s%(ret)s
%(ntod_header)s%(new)s
%(ntot_header)s%(newof)s
""") % h - self.form.optionsLabel.setText("""\ -

-%s - -

%s
""" % (stats1, stats2)) - h['tt_header'] = _("Session Statistics") - h['cs_tip'] = _("The number of cards you studied in the current \ -session (blue) and previous session (black)") - h['cd_tip'] = _("The number of cards you studied today (blue) and \ -yesterday (black)") - h['td_tip'] = _("The number of minutes you studied today (blue) and \ -yesterday (black)") - h['rd_tip'] = _("The number of cards that are waiting to be reviewed \ -today") - h['ntod_tip'] = _("The number of new cards that are waiting to be \ -learnt today") - h['ntot_tip'] = _("The total number of new cards in the deck") - statToolTip = ("""

%(tt_header)s

-
%(cs_header)s
%(cs_tip)s
-
%(cd_header)s
%(cd_tip)s
-
%(td_header)s
%(td_tip)s
-
%(rd_header)s
%(rd_tip)s
-
%(ntod_header)s
%(ntod_tip)s
-
%(ntot_header)s
%(ntot_tip)s<
""") % h - - self.form.optionsLabel.setToolTip(statToolTip) - - def showStudyScreen(self): - # forget last card - self.lastCard = None - self.switchToStudyScreen() - self.updateStudyStats() - self.form.startReviewingButton.setFocus() - self.setupStudyOptions() - self.form.studyOptionsFrame.setMaximumWidth(500) - self.form.studyOptionsFrame.show() - - def setupStudyOptions(self): - self.form.newPerDay.setText(str(self.deck.newCardsPerDay)) - lim = self.deck.sessionTimeLimit/60 - if int(lim) == lim: - lim = int(lim) - self.form.minuteLimit.setText(str(lim)) - self.form.questionLimit.setText(str(self.deck.sessionRepLimit)) - self.form.newCardOrder.setCurrentIndex(self.deck.newCardOrder) - self.form.newCardScheduling.setCurrentIndex(self.deck.newCardSpacing) - self.form.revCardOrder.setCurrentIndex(self.deck.revCardOrder) - self.form.failedCardsOption.clear() - if self.deck.getFailedCardPolicy() == 5: - labels = failedCardOptionLabels().values() - else: - labels = failedCardOptionLabels().values()[0:-1] - self.form.failedCardsOption.insertItems(0, labels) - self.form.failedCardsOption.setCurrentIndex(self.deck.getFailedCardPolicy()) - self.form.failedCardMax.setText(unicode(self.deck.failedCardMax)) - - def onStartReview(self): - def uf(obj, field, value): - if getattr(obj, field) != value: - setattr(obj, field, value) - self.deck.flushMod() - self.form.studyOptionsFrame.hide() - # make sure the size is updated before button stack shown - self.app.processEvents() - uf(self.deck, 'newCardSpacing', - self.form.newCardScheduling.currentIndex()) - uf(self.deck, 'revCardOrder', - self.form.revCardOrder.currentIndex()) - pol = self.deck.getFailedCardPolicy() - if (pol != 5 and pol != - self.form.failedCardsOption.currentIndex()): - self.deck.setFailedCardPolicy( - self.form.failedCardsOption.currentIndex()) - self.deck.flushMod() - self.deck.reset() - if not self.deck.finishScheduler: - self.deck.startTimebox() - self.config['studyOptionsTab'] = self.form.tabWidget.currentIndex() - self.moveToState("getQuestion") - - def onStudyOptions(self): - if self.state == "studyScreen": - pass - else: - self.moveToState("studyScreen") - # Toolbar ########################################################################## @@ -1088,23 +561,16 @@ learnt today") def rmDockable(self, dock): self.removeDockWidget(dock) - # Stats and graphs + # Marking, suspending and deleting ########################################################################## - def setupCardStats(self): - self.cardStats = aqt.stats.CardStats(self) - - def onCardStats(self): - self.cardStats.show() - - def onDeckStats(self): - aqt.stats.deckStats(self) - - def onGraphs(self): - aqt.stats.graphs(self) - - # Marking, suspending and undoing - ########################################################################## + def updateMarkAction(self): + self.form.actionMarkCard.blockSignals(True) + if self.deck.cardHasTag(self.currentCard, "Marked"): + self.form.actionMarkCard.setChecked(True) + else: + self.form.actionMarkCard.setChecked(False) + self.form.actionMarkCard.blockSignals(False) def onMark(self, toggled): if self.deck.cardHasTag(self.currentCard, "Marked"): @@ -1141,6 +607,9 @@ learnt today") self.reset() self.deck.setUndoEnd(undo) + # Undo/redo + ########################################################################## + def onUndo(self): name = self.deck.undoName() self.deck.undo() @@ -1152,6 +621,20 @@ learnt today") self.deck.redo() self.reset() + def maybeEnableUndo(self): + if self.deck and self.deck.undoAvailable(): + self.form.actionUndo.setText(_("Undo %s") % + self.deck.undoName()) + self.form.actionUndo.setEnabled(True) + else: + self.form.actionUndo.setEnabled(False) + if self.deck and self.deck.redoAvailable(): + self.form.actionRedo.setText(_("Redo %s") % + self.deck.redoName()) + self.form.actionRedo.setEnabled(True) + else: + self.form.actionRedo.setEnabled(False) + # Other menu operations ########################################################################## @@ -1164,6 +647,18 @@ learnt today") def onEditCurrent(self): self.moveToState("editCurrentFact") + def setupCardStats(self): + self.cardStats = aqt.stats.CardStats(self) + + def onCardStats(self): + self.cardStats.show() + + def onDeckStats(self): + aqt.stats.deckStats(self) + + def onGraphs(self): + aqt.stats.graphs(self) + def onCardLayout(self): aqt.clayout.CardLayout(self, 0, self.currentCard.fact, card=self.currentCard) @@ -1198,155 +693,6 @@ Please give your deck a name:""")) def onExport(self): aqt.exporting.ExportDialog(self) - # Cramming & Sharing - ########################################################################## - - def _copyToTmpDeck(self, name="cram.anki", tags="", ids=[]): - ndir = tempfile.mkdtemp(prefix="anki") - path = os.path.join(ndir, name) - from anki.exporting import AnkiExporter - e = AnkiExporter(self.deck) - e.includeMedia = False - if tags: - e.limitTags = parseTags(tags) - if ids: - e.limitCardIds = ids - path = unicode(path, sys.getfilesystemencoding()) - e.exportInto(path) - return (e, path) - - def onCram(self, cardIds=[]): - te = aqt.tagedit.TagEdit(self) - te.setDeck(self.deck, "all") - diag = GetTextDialog( - self, _("Tags to cram:"), help="CramMode", edit=te) - l = diag.layout() - g = QGroupBox(_("Review Mode")) - l.insertWidget(2, g) - box = QVBoxLayout() - g.setLayout(box) - keep = QRadioButton(_("Show oldest modified first")) - box.addWidget(keep) - keep.setChecked(True) - diag.setTabOrder(diag.l, keep) - order = QRadioButton(_("Show in order added")) - box.addWidget(order) - random = QRadioButton(_("Show in random order")) - box.addWidget(random) - # hide tag list if we have ids - if cardIds: - diag.l.hide() - diag.qlabel.hide() - if diag.exec_(): - if keep.isChecked(): - order = "type, modified" - elif order.isChecked(): - order = "created" - else: - order = "random()" - if cardIds: - active = cardIds - else: - active = unicode(diag.l.text()) - self.deck.setupCramScheduler(active, order) - if self.state == "studyScreen": - self.onStartReview() - else: - self.deck.reset() - self.deck.getCard() # so scheduler will reset if empty - self.moveToState("initial") - if not self.deck.finishScheduler: - showInfo(_("No cards matched the provided tags.")) - - def onShare(self, tags): - pwd = os.getcwd() - # open tmp deck - (e, path) = self._copyToTmpDeck(name="shared.anki", tags=tags) - if not e.exportedCards: - showInfo(_("No cards matched the provided tags.")) - return - self.deck.startProgress() - self.deck.updateProgress() - d = DeckStorage.Deck(path, backup=False) - # reset scheduling to defaults - d.newCardsPerDay = 20 - d.delay0 = 600 - d.delay1 = 0 - d.delay2 = 0 - d.hardIntervalMin = 1.0 - d.hardIntervalMax = 1.1 - d.midIntervalMin = 3.0 - d.midIntervalMax = 5.0 - d.easyIntervalMin = 7.0 - d.easyIntervalMax = 9.0 - d.syncName = None - d.setVar("newActive", u"") - d.setVar("newInactive", u"") - d.setVar("revActive", u"") - d.setVar("revInactive", u"") - self.deck.updateProgress() - # unsuspend cards - d.unsuspendCards(d.s.column0("select id from cards where type < 0")) - self.deck.updateProgress() - d.utcOffset = -2 - d.flushMod() - d.save() - self.deck.updateProgress() - # media - d.s.statement("update deckVars set value = '' where key = 'mediaURL'") - self.deck.updateProgress() - d.s.statement("vacuum") - self.deck.updateProgress() - nfacts = d.factCount - mdir = self.deck.mediaDir() - d.close() - dir = os.path.dirname(path) - zippath = os.path.join(dir, "shared-%d.zip" % time.time()) - # zip it up - zip = zipfile.ZipFile(zippath, "w", zipfile.ZIP_DEFLATED) - zip.writestr("facts", str(nfacts)) - zip.writestr("version", str(2)) - readmep = os.path.join(dir, "README.html") - readme = open(readmep, "w") - readme.write('''\ - -This is an exported packaged deck created by Anki.

- -To share this deck with other people, upload it to - -http://anki.ichi2.net/file/upload, or email -it to your friends. -''') - readme.close() - zip.write(readmep, "README.html") - zip.write(path, "shared.anki") - if mdir: - for f in os.listdir(mdir): - zip.write(os.path.join(mdir, f), - os.path.join("shared.media/", f)) - os.chdir(pwd) - os.chdir(pwd) - self.deck.updateProgress() - zip.close() - os.unlink(path) - self.deck.finishProgress() - self.onOpenPluginFolder(dir) - - # Reviewing and learning ahead - ########################################################################## - - def onLearnMore(self): - self.deck.setupLearnMoreScheduler() - self.reset() - self.showToolTip(_("""\ -

Learning More

Click the stopwatch at the top to finish.""")) - - def onReviewEarly(self): - self.deck.setupReviewEarlyScheduler() - self.reset() - self.showToolTip(_("""\ -

Reviewing Early

Click the stopwatch at the top to finish.""")) - # Language handling ########################################################################## @@ -1382,272 +728,6 @@ it to your friends. __builtin__.__dict__['_'] = self.getTranslation __builtin__.__dict__['ngettext'] = self.getTranslation2 - # Syncing - ########################################################################## - - def syncDeck(self, interactive=True, onlyMerge=False, reload=True): - "Synchronise a deck with the server." - self.raiseMain() - #self.setNotice() - # vet input - if interactive: - self.ensureSyncParams() - u=self.config['syncUsername'] - p=self.config['syncPassword'] - if not u or not p: - return - if self.deck: - if not self.deck.path: - if not self.save(required=True): - return - if self.deck and not self.deck.syncName: - if interactive: - if (not self.config['mediaLocation'] - and self.deck.db.scalar("select 1 from media limit 1")): - showInfo(_("""\ -Syncing sounds and images requires a free file synchronization service like \ -DropBox. Click help to learn more, and OK to continue syncing."""), - help="SyncingMedia") - # enable syncing - self.deck.enableSyncing() - else: - return - if self.deck is None and getattr(self, 'deckPath', None) is None: - # sync all decks - self.loadAfterSync = -1 - self.syncName = None - self.syncDecks = self.decksToSync() - if not self.syncDecks: - if interactive: - showInfo(_("""\ -Please open a deck and run File>Sync. After you do this once, the deck \ -will sync automatically from then on.""")) - return - 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.name() - self.lastSync = self.deck.lastSync - 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" - import gc; gc.collect() - self.form.welcomeText.setText(u"") - self.syncThread = aqt.sync.Sync(self, u, p, interactive, onlyMerge) - self.connect(self.syncThread, SIGNAL("setStatus"), self.setSyncStatus) - self.connect(self.syncThread, SIGNAL("showWarning"), self.showSyncWarning) - self.connect(self.syncThread, SIGNAL("moveToState"), self.moveToState) - 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.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) - self.connect(self.syncThread, SIGNAL("bulkSyncFailed"), self.bulkSyncFailed) - self.connect(self.syncThread, SIGNAL("fullSyncStarted"), self.fullSyncStarted) - self.connect(self.syncThread, SIGNAL("fullSyncFinished"), self.fullSyncFinished) - self.connect(self.syncThread, SIGNAL("fullSyncProgress"), self.fullSyncProgress) - self.connect(self.syncThread, SIGNAL("badUserPass"), self.badUserPass) - self.connect(self.syncThread, SIGNAL("syncConflicts"), self.onConflict) - self.connect(self.syncThread, SIGNAL("syncClobber"), self.onClobber) - self.syncThread.start() - self.switchToWelcomeScreen() - self.setEnabled(False) - self.syncFinished = False - while not self.syncFinished: - self.app.processEvents() - self.syncThread.wait(100) - self.setEnabled(True) - return True - - def decksToSync(self): - ok = [] - for d in self.config['recentDeckPaths']: - if os.path.exists(d): - ok.append(d) - return ok - - def onConflict(self, deckName): - diag = askUserDialog(_("""\ -%s has been changed on both -the local and remote side. What do -you want to do?""" % deckName), - [_("Keep Local"), - _("Keep Remote"), - _("Cancel")]) - diag.setDefault(2) - ret = diag.run() - if ret == _("Keep Local"): - self.syncThread.conflictResolution = "keepLocal" - elif ret == _("Keep Remote"): - self.syncThread.conflictResolution = "keepRemote" - else: - self.syncThread.conflictResolution = "cancel" - - def onClobber(self, deckName): - diag = askUserDialog(_("""\ -You are about to upload %s -to AnkiOnline. This will overwrite -the online copy of this deck. -Are you sure?""" % deckName), - [_("Upload"), - _("Cancel")]) - diag.setDefault(1) - ret = diag.run() - if ret == _("Upload"): - self.syncThread.clobberChoice = "overwrite" - else: - self.syncThread.clobberChoice = "cancel" - - def onSyncFinished(self): - "Reopen after sync finished." - self.form.buttonStack.show() - try: - try: - if not self.showBrowser: - # no deck load & no deck browser, as we're about to quit or do - # something manually - pass - else: - if self.loadAfterSync == -1: - # after sync all, so refresh browser list - self.browserLastRefreshed = 0 - self.moveToState("deckBrowser") - elif self.loadAfterSync and self.deckPath: - if self.loadAfterSync == 2: - name = re.sub("[<>]", "", self.syncName) - p = os.path.join(self.documentDir, name + ".anki") - shutil.copy2(self.deckPath, p) - self.deckPath = p - # since we've moved the deck, we have to set sync path - # ourselves - c = sqlite.connect(p) - v = c.execute( - "select version from decks").fetchone()[0] - if v >= 52: - # deck has bene upgraded already, so we can - # use a checksum - name = checksum(p.encode("utf-8")) - else: - # FIXME: compat code because deck hasn't been - # upgraded yet. can be deleted in the future. - # strip off .anki part - name = os.path.splitext( - os.path.basename(p))[0] - c.execute("update decks set syncName = ?", (name,)) - c.commit() - c.close() - self.loadDeck(self.deckPath) - else: - self.moveToState("deckBrowser") - except: - self.moveToState("deckBrowser") - raise - finally: - self.deckPath = None - self.syncFinished = True - - def selectSyncDeck(self, decks): - name = aqt.sync.DeckChooser(self, decks).getName() - self.syncName = name - if name: - # name chosen - p = os.path.join(self.documentDir, name + ".anki") - if os.path.exists(p): - d = askUserDialog(_("""\ -This deck already exists on your computer. Overwrite the local copy?"""), - ["Overwrite", "Cancel"]) - d.setDefault(1) - if d.run() == "Overwrite": - self.syncDeck(interactive=False, onlyMerge=True) - else: - self.syncFinished = True - self.cleanNewDeck() - else: - self.syncDeck(interactive=False, onlyMerge=True) - return - self.syncFinished = True - self.cleanNewDeck() - - def cleanNewDeck(self): - "Unload a new deck if an initial sync failed." - self.deck = None - self.deckPath = None - self.moveToState("deckBrowser") - self.syncFinished = True - - def setSyncStatus(self, text, *args): - self.form.welcomeText.append("" + text + "") - - def syncClockOff(self, diff): - showWarning( - _("The time or date on your computer is not correct.\n") + - ngettext("It is off by %d second.\n\n", - "It is off by %d seconds.\n\n", diff) % diff + - _("Since this can cause many problems with syncing,\n" - "syncing is disabled until you fix the problem.") - ) - self.onSyncFinished() - - def showSyncWarning(self, text): - showWarning(text, self) - self.setStatus("") - - def badUserPass(self): - aqt.preferences.Preferences(self, self.config).dialog.tabWidget.\ - setCurrentIndex(1) - - def openSyncProgress(self): - self.syncProgressDialog = QProgressDialog(_("Syncing Media..."), - "", 0, 0, self) - self.syncProgressDialog.setWindowTitle(_("Syncing Media...")) - self.syncProgressDialog.setCancelButton(None) - self.syncProgressDialog.setAutoClose(False) - self.syncProgressDialog.setAutoReset(False) - - def closeSyncProgress(self): - self.syncProgressDialog.cancel() - - def updateSyncProgress(self, args): - (type, x, y, fname) = args - self.syncProgressDialog.setMaximum(y) - self.syncProgressDialog.setValue(x) - self.syncProgressDialog.setMinimumDuration(0) - if type == "up": - self.syncProgressDialog.setLabelText("Uploading %s..." % fname) - else: - self.syncProgressDialog.setLabelText("Downloading %s..." % fname) - - def bulkSyncFailed(self): - showWarning(_( - "Failed to upload media. Please run 'check media db'."), self) - - def fullSyncStarted(self, max): - self.startProgress(max=max) - - def fullSyncFinished(self): - self.finishProgress() - # need to deactivate interface again - self.setEnabled(False) - - def fullSyncProgress(self, type, val): - if type == "fromLocal": - s = _("Uploaded %dKB to server...") - self.updateProgress(label=s % (val / 1024), value=val) - else: - s = _("Downloaded %dKB from server...") - self.updateProgress(label=s % (val / 1024)) - # Menu, title bar & status ########################################################################## @@ -1684,7 +764,7 @@ This deck already exists on your computer. Overwrite the local copy?"""), self.connect(m.actionRename, s, self.onRename) self.connect(m.actionClose, s, self.onClose) self.connect(m.actionExit, s, self, SLOT("close()")) - self.connect(m.actionSyncdeck, s, self.syncDeck) + #self.connect(m.actionSyncdeck, s, self.syncDeck) self.connect(m.actionDeckProperties, s, self.onDeckProperties) self.connect(m.actionAddcards, s, self.onAddCard) self.connect(m.actionEditdeck, s, self.onEditDeck) @@ -1708,10 +788,9 @@ This deck already exists on your computer. Overwrite the local copy?"""), self.connect(m.actionCheckMediaDatabase, s, self.onCheckMediaDB) self.connect(m.actionDownloadMissingMedia, s, self.onDownloadMissingMedia) self.connect(m.actionLocalizeMedia, s, self.onLocalizeMedia) - self.connect(m.actionCram, s, self.onCram) - self.connect(m.actionStudyOptions, s, self.onStudyOptions) + #self.connect(m.actionCram, s, self.onCram) + #self.connect(m.actionStudyOptions, s, self.onStudyOptions) self.connect(m.actionDonate, s, self.onDonate) - self.connect(m.actionRecordNoiseProfile, s, self.onRecordNoiseProfile) self.connect(m.actionBuryFact, s, self.onBuryFact) def enableDeckMenuItems(self, enabled=True): @@ -1747,20 +826,6 @@ This deck already exists on your computer. Overwrite the local copy?"""), title += " - " + _("Decks") self.setWindowTitle(title) - def setStatus(self, text, timeout=3000): - self.form.statusbar.showMessage(text, timeout) - - def onStartHere(self): - QDesktopServices.openUrl(QUrl(aqt.appHelpSite)) - - def updateMarkAction(self): - self.form.actionMarkCard.blockSignals(True) - if self.deck.cardHasTag(self.currentCard, "Marked"): - self.form.actionMarkCard.setChecked(True) - else: - self.form.actionMarkCard.setChecked(False) - self.form.actionMarkCard.blockSignals(False) - def disableCardMenuItems(self): self.maybeEnableUndo() self.form.actionEditCurrent.setEnabled(False) @@ -1783,20 +848,6 @@ This deck already exists on your computer. Overwrite the local copy?"""), self.form.actionEditdeck.setEnabled(True) runHook("enableCardMenuItems") - def maybeEnableUndo(self): - if self.deck and self.deck.undoAvailable(): - self.form.actionUndo.setText(_("Undo %s") % - self.deck.undoName()) - self.form.actionUndo.setEnabled(True) - else: - self.form.actionUndo.setEnabled(False) - if self.deck and self.deck.redoAvailable(): - self.form.actionRedo.setText(_("Redo %s") % - self.deck.redoName()) - self.form.actionRedo.setEnabled(True) - else: - self.form.actionRedo.setEnabled(False) - # Auto update ########################################################################## @@ -1828,23 +879,9 @@ This deck already exists on your computer. Overwrite the local copy?"""), _(" Please ensure it is set correctly and then restart Anki.") ) - # Custom styles - ########################################################################## - - def setupStyle(self): - applyStyles(self) - # Sounds ########################################################################## - def setupSound(self): - anki.sound.noiseProfile = os.path.join( - self.config.confDir, "noise.profile").\ - encode(sys.getfilesystemencoding()) - anki.sound.checkForNoiseProfile() - if sys.platform.startswith("darwin"): - self.form.actionRecordNoiseProfile.setEnabled(False) - def onRepeatAudio(self): clearAudioQueue() if (not self.currentCard.cardModel.questionInAnswer @@ -1854,10 +891,6 @@ This deck already exists on your computer. Overwrite the local copy?"""), if self.state != "showQuestion": playFromText(self.currentCard.answer) - def onRecordNoiseProfile(self): - from aqt.sound import recordNoiseProfile - recordNoiseProfile(self) - # Media locations ########################################################################## @@ -2061,9 +1094,6 @@ It can take a long time. Proceed?""")): msg += "\n\n" + _("Couldn't find:") + "\n" + "\n".join(res[1]) aqt.utils.showText(msg, parent=self, type="text") - def addHook(self, *args): - addHook(*args) - # System specific code ########################################################################## diff --git a/aqt/reviewer.py b/aqt/reviewer.py index 6d132566d..23fe70ed7 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -23,6 +23,7 @@ class Reviewer(object): self._answeredIds = [] self.state = None self._setupStatus() + addHook("leech", self.onLeech) def show(self): self.web.setKeyHandler(self._keyHandler) @@ -579,3 +580,19 @@ div#filler { p.setColor(QPalette.Highlight, QColor("#00ee00")) self.progressBar.setPalette(p) self.progressBar.setValue(perc) + + # Leeches + ########################################################################## + + # fixme: update; clear on card transition + def onLeech(self, card): + print "leech" + return + txt = (_("""\ +%s... is a leech.""") + % stripHTML(stripSounds(self.currentCard.question)).\ + replace("\n", " ")[0:30]) + if isLeech and self.deck.db.scalar( + "select 1 from cards where id = :id and type < 0", id=cardId): + txt += _(" It has been suspended.") + self.setNotice(txt) diff --git a/aqt/share.py b/aqt/share.py new file mode 100644 index 000000000..3ee7d4f51 --- /dev/null +++ b/aqt/share.py @@ -0,0 +1,89 @@ +# to be moved into libanki + + def _copyToTmpDeck(self, name="cram.anki", tags="", ids=[]): + ndir = tempfile.mkdtemp(prefix="anki") + path = os.path.join(ndir, name) + from anki.exporting import AnkiExporter + e = AnkiExporter(self.deck) + e.includeMedia = False + if tags: + e.limitTags = parseTags(tags) + if ids: + e.limitCardIds = ids + path = unicode(path, sys.getfilesystemencoding()) + e.exportInto(path) + return (e, path) + + def onShare(self, tags): + pwd = os.getcwd() + # open tmp deck + (e, path) = self._copyToTmpDeck(name="shared.anki", tags=tags) + if not e.exportedCards: + showInfo(_("No cards matched the provided tags.")) + return + self.deck.startProgress() + self.deck.updateProgress() + d = DeckStorage.Deck(path, backup=False) + # reset scheduling to defaults + d.newCardsPerDay = 20 + d.delay0 = 600 + d.delay1 = 0 + d.delay2 = 0 + d.hardIntervalMin = 1.0 + d.hardIntervalMax = 1.1 + d.midIntervalMin = 3.0 + d.midIntervalMax = 5.0 + d.easyIntervalMin = 7.0 + d.easyIntervalMax = 9.0 + d.syncName = None + d.setVar("newActive", u"") + d.setVar("newInactive", u"") + d.setVar("revActive", u"") + d.setVar("revInactive", u"") + self.deck.updateProgress() + # unsuspend cards + d.unsuspendCards(d.s.column0("select id from cards where type < 0")) + self.deck.updateProgress() + d.utcOffset = -2 + d.flushMod() + d.save() + self.deck.updateProgress() + # media + d.s.statement("update deckVars set value = '' where key = 'mediaURL'") + self.deck.updateProgress() + d.s.statement("vacuum") + self.deck.updateProgress() + nfacts = d.factCount + mdir = self.deck.mediaDir() + d.close() + dir = os.path.dirname(path) + zippath = os.path.join(dir, "shared-%d.zip" % time.time()) + # zip it up + zip = zipfile.ZipFile(zippath, "w", zipfile.ZIP_DEFLATED) + zip.writestr("facts", str(nfacts)) + zip.writestr("version", str(2)) + readmep = os.path.join(dir, "README.html") + readme = open(readmep, "w") + readme.write('''\ + +This is an exported packaged deck created by Anki.

+ +To share this deck with other people, upload it to + +http://anki.ichi2.net/file/upload, or email +it to your friends. +''') + readme.close() + zip.write(readmep, "README.html") + zip.write(path, "shared.anki") + if mdir: + for f in os.listdir(mdir): + zip.write(os.path.join(mdir, f), + os.path.join("shared.media/", f)) + os.chdir(pwd) + os.chdir(pwd) + self.deck.updateProgress() + zip.close() + os.unlink(path) + self.deck.finishProgress() + self.onOpenPluginFolder(dir) diff --git a/aqt/studyopts.py b/aqt/studyopts.py new file mode 100644 index 000000000..0a46f1a8b --- /dev/null +++ b/aqt/studyopts.py @@ -0,0 +1,284 @@ + + def _studyScreenState(self, oldState): + self.currentCard = None + # if self.deck.finishScheduler: + # self.deck.finishScheduler() + self.disableCardMenuItems() + self.showStudyScreen() + + # Study screen + ########################################################################## + + def setupStudyScreen(self): + return + self.form.buttonStack.hide() + self.form.newCardOrder.insertItems( + 0, QStringList(anki.consts.newCardOrderLabels().values())) + self.form.newCardScheduling.insertItems( + 0, QStringList(anki.consts.newCardSchedulingLabels().values())) + self.form.revCardOrder.insertItems( + 0, QStringList(anki.consts.revCardOrderLabels().values())) + self.connect(self.form.optionsHelpButton, + SIGNAL("clicked()"), + lambda: QDesktopServices.openUrl(QUrl( + aqt.appWiki + "StudyOptions"))) + self.connect(self.form.minuteLimit, + SIGNAL("textChanged(QString)"), self.onMinuteLimitChanged) + self.connect(self.form.questionLimit, + SIGNAL("textChanged(QString)"), self.onQuestionLimitChanged) + self.connect(self.form.newPerDay, + SIGNAL("textChanged(QString)"), self.onNewLimitChanged) + self.connect(self.form.startReviewingButton, + SIGNAL("clicked()"), + self.onStartReview) + self.connect(self.form.newCardOrder, + SIGNAL("activated(int)"), self.onNewCardOrderChanged) + self.connect(self.form.failedCardMax, + SIGNAL("editingFinished()"), + self.onFailedMaxChanged) + self.connect(self.form.newCategories, + SIGNAL("clicked()"), self.onNewCategoriesClicked) + self.connect(self.form.revCategories, + SIGNAL("clicked()"), self.onRevCategoriesClicked) + self.form.tabWidget.setCurrentIndex(self.config['studyOptionsTab']) + + def onNewCategoriesClicked(self): + aqt.activetags.show(self, "new") + + def onRevCategoriesClicked(self): + aqt.activetags.show(self, "rev") + + def onFailedMaxChanged(self): + try: + v = int(self.form.failedCardMax.text()) + if v == 1 or v < 0: + v = 2 + self.deck.failedCardMax = v + except ValueError: + pass + self.form.failedCardMax.setText(str(self.deck.failedCardMax)) + self.deck.flushMod() + + def onMinuteLimitChanged(self, qstr): + try: + val = float(self.form.minuteLimit.text()) * 60 + if self.deck.sessionTimeLimit == val: + return + self.deck.sessionTimeLimit = val + except ValueError: + pass + self.deck.flushMod() + self.updateStudyStats() + + def onQuestionLimitChanged(self, qstr): + try: + val = int(self.form.questionLimit.text()) + if self.deck.sessionRepLimit == val: + return + self.deck.sessionRepLimit = val + except ValueError: + pass + self.deck.flushMod() + self.updateStudyStats() + + def onNewLimitChanged(self, qstr): + try: + val = int(self.form.newPerDay.text()) + if self.deck.newCardsPerDay == val: + return + self.deck.newCardsPerDay = val + except ValueError: + pass + self.deck.flushMod() + self.deck.reset() + self.statusView.redraw() + self.updateStudyStats() + + def onNewCardOrderChanged(self, ncOrd): + def uf(obj, field, value): + if getattr(obj, field) != value: + setattr(obj, field, value) + self.deck.flushMod() + if ncOrd != 0: + if self.deck.newCardOrder == 0: + # need to put back in order + self.mw.startProgress() + self.mw.updateProgress(_("Ordering...")) + self.deck.orderNewCards() + self.deck.finishProgress() + uf(self.deck, 'newCardOrder', ncOrd) + elif ncOrd == 0: + # (re-)randomize + self.deck.startProgress() + self.deck.updateProgress(_("Randomizing...")) + self.deck.randomizeNewCards() + self.deck.finishProgress() + uf(self.deck, 'newCardOrder', ncOrd) + + def updateActives(self): + labels = [ + _("Show All Due Cards"), + _("Show Chosen Categories") + ] + if self.deck.getVar("newActive") or self.deck.getVar("newInactive"): + new = labels[1] + else: + new = labels[0] + self.form.newCategoryLabel.setText(new) + if self.deck.getVar("revActive") or self.deck.getVar("revInactive"): + rev = labels[1] + else: + rev = labels[0] + self.form.revCategoryLabel.setText(rev) + + def updateStudyStats(self): + self.form.buttonStack.hide() + self.deck.reset() + self.updateActives() + wasReached = self.deck.timeboxReached() + sessionColour = '%s' + cardColour = '%s' + # top label + h = {} + h['ret'] = cardColour % (self.deck.revCount+self.deck.failedSoonCount) + h['new'] = cardColour % self.deck.newCount + h['newof'] = str(self.deck.newCountAll()) + # counts & time for today + todayStart = self.deck.failedCutoff - 86400 + sql = "select count(), sum(userTime) from revlog" + (reps, time_) = self.deck.db.first( + sql + " where time > :start", start=todayStart) + h['timeToday'] = sessionColour % ( + anki.utils.fmtTimeSpan(time_ or 0, short=True, point=1)) + h['repsToday'] = sessionColour % reps + # and yesterday + yestStart = todayStart - 86400 + (reps, time_) = self.deck.db.first( + sql + " where time > :start and time <= :end", + start=yestStart, end=todayStart) + h['timeTodayChg'] = str( + anki.utils.fmtTimeSpan(time_ or 0, short=True, point=1)) + h['repsTodayChg'] = str(reps) + # session counts + limit = self.deck.sessionTimeLimit + start = self.deck.sessionStartTime or time.time() - limit + start2 = self.deck.lastSessionStart or start - limit + last10 = self.deck.db.scalar( + "select count(*) from revlog where time >= :t", + t=start) + last20 = self.deck.db.scalar( + "select count(*) from revlog where " + "time >= :t and time < :t2", + t=start2, t2=start) + h['repsInSes'] = sessionColour % last10 + h['repsInSesChg'] = str(last20) + h['cs_header'] = "" + _("Cards/session:") + "" + h['cd_header'] = "" + _("Cards/day:") + "" + h['td_header'] = "" + _("Time/day:") + "" + h['rd_header'] = "" + _("Reviews due:") + "" + h['ntod_header'] = "" + _("New today:") + "" + h['ntot_header'] = "" + _("New total:") + "" + stats1 = ("""\ + + +
%(cs_header)s%(repsInSesChg)s%(repsInSes)s
+


+ + + + + +
+%(cd_header)s%(repsTodayChg)s%(repsToday)s
%(td_header)s%(timeTodayChg)s%(timeToday)s
""") % h + + stats2 = ("""\ + + + + +
%(rd_header)s%(ret)s
%(ntod_header)s%(new)s
%(ntot_header)s%(newof)s
""") % h + self.form.optionsLabel.setText("""\ +

+%s + +

%s
""" % (stats1, stats2)) + h['tt_header'] = _("Session Statistics") + h['cs_tip'] = _("The number of cards you studied in the current \ +session (blue) and previous session (black)") + h['cd_tip'] = _("The number of cards you studied today (blue) and \ +yesterday (black)") + h['td_tip'] = _("The number of minutes you studied today (blue) and \ +yesterday (black)") + h['rd_tip'] = _("The number of cards that are waiting to be reviewed \ +today") + h['ntod_tip'] = _("The number of new cards that are waiting to be \ +learnt today") + h['ntot_tip'] = _("The total number of new cards in the deck") + statToolTip = ("""

%(tt_header)s

+
%(cs_header)s
%(cs_tip)s
+
%(cd_header)s
%(cd_tip)s
+
%(td_header)s
%(td_tip)s
+
%(rd_header)s
%(rd_tip)s
+
%(ntod_header)s
%(ntod_tip)s
+
%(ntot_header)s
%(ntot_tip)s<
""") % h + + self.form.optionsLabel.setToolTip(statToolTip) + + def showStudyScreen(self): + # forget last card + self.lastCard = None + self.switchToStudyScreen() + self.updateStudyStats() + self.form.startReviewingButton.setFocus() + self.setupStudyOptions() + self.form.studyOptionsFrame.setMaximumWidth(500) + self.form.studyOptionsFrame.show() + + def setupStudyOptions(self): + self.form.newPerDay.setText(str(self.deck.newCardsPerDay)) + lim = self.deck.sessionTimeLimit/60 + if int(lim) == lim: + lim = int(lim) + self.form.minuteLimit.setText(str(lim)) + self.form.questionLimit.setText(str(self.deck.sessionRepLimit)) + self.form.newCardOrder.setCurrentIndex(self.deck.newCardOrder) + self.form.newCardScheduling.setCurrentIndex(self.deck.newCardSpacing) + self.form.revCardOrder.setCurrentIndex(self.deck.revCardOrder) + self.form.failedCardsOption.clear() + if self.deck.getFailedCardPolicy() == 5: + labels = failedCardOptionLabels().values() + else: + labels = failedCardOptionLabels().values()[0:-1] + self.form.failedCardsOption.insertItems(0, labels) + self.form.failedCardsOption.setCurrentIndex(self.deck.getFailedCardPolicy()) + self.form.failedCardMax.setText(unicode(self.deck.failedCardMax)) + + def onStartReview(self): + def uf(obj, field, value): + if getattr(obj, field) != value: + setattr(obj, field, value) + self.deck.flushMod() + self.form.studyOptionsFrame.hide() + # make sure the size is updated before button stack shown + self.app.processEvents() + uf(self.deck, 'newCardSpacing', + self.form.newCardScheduling.currentIndex()) + uf(self.deck, 'revCardOrder', + self.form.revCardOrder.currentIndex()) + pol = self.deck.getFailedCardPolicy() + if (pol != 5 and pol != + self.form.failedCardsOption.currentIndex()): + self.deck.setFailedCardPolicy( + self.form.failedCardsOption.currentIndex()) + self.deck.flushMod() + self.deck.reset() + if not self.deck.finishScheduler: + self.deck.startTimebox() + self.config['studyOptionsTab'] = self.form.tabWidget.currentIndex() + self.moveToState("getQuestion") + + def onStudyOptions(self): + if self.state == "studyScreen": + pass + else: + self.moveToState("studyScreen") diff --git a/aqt/sync.py b/aqt/sync.py index 8f7ac3a9b..d7b12c35c 100755 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -14,6 +14,307 @@ from anki.db import sqlite import aqt.forms from anki.hooks import addHook, removeHook +class SyncManager(object): + + # Syncing + ########################################################################## + + def syncDeck(self, interactive=True, onlyMerge=False, reload=True): + "Synchronise a deck with the server." + self.raiseMain() + #self.setNotice() + # vet input + if interactive: + self.ensureSyncParams() + u=self.config['syncUsername'] + p=self.config['syncPassword'] + if not u or not p: + return + if self.deck: + if not self.deck.path: + if not self.save(required=True): + return + if self.deck and not self.deck.syncName: + if interactive: + if (not self.config['mediaLocation'] + and self.deck.db.scalar("select 1 from media limit 1")): + showInfo(_("""\ +Syncing sounds and images requires a free file synchronization service like \ +DropBox. Click help to learn more, and OK to continue syncing."""), + help="SyncingMedia") + # enable syncing + self.deck.enableSyncing() + else: + return + if self.deck is None and getattr(self, 'deckPath', None) is None: + # sync all decks + self.loadAfterSync = -1 + self.syncName = None + self.syncDecks = self.decksToSync() + if not self.syncDecks: + if interactive: + showInfo(_("""\ +Please open a deck and run File>Sync. After you do this once, the deck \ +will sync automatically from then on.""")) + return + 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.name() + self.lastSync = self.deck.lastSync + 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" + import gc; gc.collect() + self.form.welcomeText.setText(u"") + self.syncThread = aqt.sync.Sync(self, u, p, interactive, onlyMerge) + self.connect(self.syncThread, SIGNAL("setStatus"), self.setSyncStatus) + self.connect(self.syncThread, SIGNAL("showWarning"), self.showSyncWarning) + self.connect(self.syncThread, SIGNAL("moveToState"), self.moveToState) + 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.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) + self.connect(self.syncThread, SIGNAL("bulkSyncFailed"), self.bulkSyncFailed) + self.connect(self.syncThread, SIGNAL("fullSyncStarted"), self.fullSyncStarted) + self.connect(self.syncThread, SIGNAL("fullSyncFinished"), self.fullSyncFinished) + self.connect(self.syncThread, SIGNAL("fullSyncProgress"), self.fullSyncProgress) + self.connect(self.syncThread, SIGNAL("badUserPass"), self.badUserPass) + self.connect(self.syncThread, SIGNAL("syncConflicts"), self.onConflict) + self.connect(self.syncThread, SIGNAL("syncClobber"), self.onClobber) + self.syncThread.start() + self.switchToWelcomeScreen() + self.setEnabled(False) + self.syncFinished = False + while not self.syncFinished: + self.app.processEvents() + self.syncThread.wait(100) + self.setEnabled(True) + return True + + def decksToSync(self): + ok = [] + for d in self.config['recentDeckPaths']: + if os.path.exists(d): + ok.append(d) + return ok + + def onConflict(self, deckName): + diag = askUserDialog(_("""\ +%s has been changed on both +the local and remote side. What do +you want to do?""" % deckName), + [_("Keep Local"), + _("Keep Remote"), + _("Cancel")]) + diag.setDefault(2) + ret = diag.run() + if ret == _("Keep Local"): + self.syncThread.conflictResolution = "keepLocal" + elif ret == _("Keep Remote"): + self.syncThread.conflictResolution = "keepRemote" + else: + self.syncThread.conflictResolution = "cancel" + + def onClobber(self, deckName): + diag = askUserDialog(_("""\ +You are about to upload %s +to AnkiOnline. This will overwrite +the online copy of this deck. +Are you sure?""" % deckName), + [_("Upload"), + _("Cancel")]) + diag.setDefault(1) + ret = diag.run() + if ret == _("Upload"): + self.syncThread.clobberChoice = "overwrite" + else: + self.syncThread.clobberChoice = "cancel" + + def onSyncFinished(self): + "Reopen after sync finished." + self.form.buttonStack.show() + try: + try: + if not self.showBrowser: + # no deck load & no deck browser, as we're about to quit or do + # something manually + pass + else: + if self.loadAfterSync == -1: + # after sync all, so refresh browser list + self.browserLastRefreshed = 0 + self.moveToState("deckBrowser") + elif self.loadAfterSync and self.deckPath: + if self.loadAfterSync == 2: + name = re.sub("[<>]", "", self.syncName) + p = os.path.join(self.documentDir, name + ".anki") + shutil.copy2(self.deckPath, p) + self.deckPath = p + # since we've moved the deck, we have to set sync path + # ourselves + c = sqlite.connect(p) + v = c.execute( + "select version from decks").fetchone()[0] + if v >= 52: + # deck has bene upgraded already, so we can + # use a checksum + name = checksum(p.encode("utf-8")) + else: + # FIXME: compat code because deck hasn't been + # upgraded yet. can be deleted in the future. + # strip off .anki part + name = os.path.splitext( + os.path.basename(p))[0] + c.execute("update decks set syncName = ?", (name,)) + c.commit() + c.close() + self.loadDeck(self.deckPath) + else: + self.moveToState("deckBrowser") + except: + self.moveToState("deckBrowser") + raise + finally: + self.deckPath = None + self.syncFinished = True + + def selectSyncDeck(self, decks): + name = aqt.sync.DeckChooser(self, decks).getName() + self.syncName = name + if name: + # name chosen + p = os.path.join(self.documentDir, name + ".anki") + if os.path.exists(p): + d = askUserDialog(_("""\ +This deck already exists on your computer. Overwrite the local copy?"""), + ["Overwrite", "Cancel"]) + d.setDefault(1) + if d.run() == "Overwrite": + self.syncDeck(interactive=False, onlyMerge=True) + else: + self.syncFinished = True + self.cleanNewDeck() + else: + self.syncDeck(interactive=False, onlyMerge=True) + return + self.syncFinished = True + self.cleanNewDeck() + + def cleanNewDeck(self): + "Unload a new deck if an initial sync failed." + self.deck = None + self.deckPath = None + self.moveToState("deckBrowser") + self.syncFinished = True + + def setSyncStatus(self, text, *args): + self.form.welcomeText.append("" + text + "") + + def syncClockOff(self, diff): + showWarning( + _("The time or date on your computer is not correct.\n") + + ngettext("It is off by %d second.\n\n", + "It is off by %d seconds.\n\n", diff) % diff + + _("Since this can cause many problems with syncing,\n" + "syncing is disabled until you fix the problem.") + ) + self.onSyncFinished() + + def showSyncWarning(self, text): + showWarning(text, self) + self.setStatus("") + + def badUserPass(self): + aqt.preferences.Preferences(self, self.config).dialog.tabWidget.\ + setCurrentIndex(1) + + def openSyncProgress(self): + self.syncProgressDialog = QProgressDialog(_("Syncing Media..."), + "", 0, 0, self) + self.syncProgressDialog.setWindowTitle(_("Syncing Media...")) + self.syncProgressDialog.setCancelButton(None) + self.syncProgressDialog.setAutoClose(False) + self.syncProgressDialog.setAutoReset(False) + + def closeSyncProgress(self): + self.syncProgressDialog.cancel() + + def updateSyncProgress(self, args): + (type, x, y, fname) = args + self.syncProgressDialog.setMaximum(y) + self.syncProgressDialog.setValue(x) + self.syncProgressDialog.setMinimumDuration(0) + if type == "up": + self.syncProgressDialog.setLabelText("Uploading %s..." % fname) + else: + self.syncProgressDialog.setLabelText("Downloading %s..." % fname) + + def bulkSyncFailed(self): + showWarning(_( + "Failed to upload media. Please run 'check media db'."), self) + + def fullSyncStarted(self, max): + self.startProgress(max=max) + + def fullSyncFinished(self): + self.finishProgress() + # need to deactivate interface again + self.setEnabled(False) + + def fullSyncProgress(self, type, val): + if type == "fromLocal": + s = _("Uploaded %dKB to server...") + self.updateProgress(label=s % (val / 1024), value=val) + else: + s = _("Downloaded %dKB from server...") + self.updateProgress(label=s % (val / 1024)) + + def ensureSyncParams(self): + if not self.config['syncUsername'] or not self.config['syncPassword']: + d = QDialog(self) + vbox = QVBoxLayout() + l = QLabel(_( + '

Online Account

' + 'To use your free online account,
' + "please enter your details below.

" + "You can change your details later with
" + "Settings->Preferences->Sync
")) + l.setOpenExternalLinks(True) + vbox.addWidget(l) + g = QGridLayout() + l1 = QLabel(_("Username:")) + g.addWidget(l1, 0, 0) + user = QLineEdit() + g.addWidget(user, 0, 1) + l2 = QLabel(_("Password:")) + g.addWidget(l2, 1, 0) + passwd = QLineEdit() + passwd.setEchoMode(QLineEdit.Password) + g.addWidget(passwd, 1, 1) + vbox.addLayout(g) + bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + self.connect(bb, SIGNAL("accepted()"), d.accept) + self.connect(bb, SIGNAL("rejected()"), d.reject) + vbox.addWidget(bb) + d.setLayout(vbox) + d.exec_() + self.config['syncUsername'] = unicode(user.text()) + self.config['syncPassword'] = unicode(passwd.text()) + + # Synchronising a deck with a public server ##########################################################################