# Copyright: Damien Elmes # -*- coding: utf-8 -*- # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html from PyQt4.QtGui import * from PyQt4.QtCore import * # fixme: sample files read only, need to copy import os, sys, re, types, gettext, stat, traceback import copy, shutil, time from PyQt4.QtCore import * from PyQt4.QtGui import * from anki import DeckStorage from anki.errors import * from anki.sound import hasSound, playFromText from anki.utils import addTags, deleteTags from anki.media import rebuildMediaDir from anki.db import OperationalError from anki.stdmodels import BasicModel import anki.lang import ankiqt ui = ankiqt.ui config = ankiqt.config class AnkiQt(QMainWindow): def __init__(self, app, config, args): QMainWindow.__init__(self) if sys.platform.startswith("darwin"): qt_mac_set_menubar_icons(False) ankiqt.mw = self self.app = app self.config = config self.deck = None self.views = [] self.setLang() self.setupFonts() self.setupBackupDir() self.setupHooks() self.loadUserCustomisations() self.mainWin = ankiqt.forms.main.Ui_MainWindow() self.mainWin.setupUi(self) self.alterShortcuts() self.help = ui.help.HelpArea(self.mainWin.helpFrame, self.config, self) self.trayIcon = ui.tray.AnkiTrayIcon( self ) self.connectMenuActions() self.resize(self.config['mainWindowSize']) self.move(self.config['mainWindowPos']) self.maybeMoveWindow() self.bodyView = ui.view.View(self, self.mainWin.mainText, self.mainWin.mainTextFrame) self.addView(self.bodyView) self.statusView = ui.status.StatusView(self) self.addView(self.statusView) self.setupButtons() self.setupAnchors() if not self.config['showToolbar']: self.removeToolBar(self.mainWin.toolBar) self.mainWin.toolBar.hide() self.show() if sys.platform.startswith("darwin"): self.setUnifiedTitleAndToolBarOnMac(True) pass # load deck try: self.maybeLoadLastDeck(args) finally: self.setEnabled(True) # the focus is not set while disabled, so fetch card again self.moveToState("auto") # run after-init hook try: self.runHook('init') except: print _("Error running initHook. Broken plugin?") print traceback.print_exc() # check for updates self.setupAutoUpdate() def maybeMoveWindow(self): # If the window is positioned off the screen, move it back into view moveWin = False if (self.pos().x() > (self.app.desktop().width() - 200) or self.pos().x() < 0): moveWin = True newX = self.app.desktop().width() - self.size().width() else: newX = self.pos().x() if (self.pos().y() > (self.app.desktop().height() - 200) or self.pos().y() < 0): moveWin = True newY = self.app.desktop().height() - self.size().height() else: newY = self.pos().y() if moveWin: self.move( newX, newY ) # State machine ########################################################################## def addView(self, view): self.views.append(view) def updateViews(self, status): if self.deck is None and status != "noDeck": raise "updateViews() called with no deck. status=%s" % status for view in self.views: view.setState(status) def pauseViews(self): if getattr(self, 'viewsBackup', None): return self.viewsBackup = self.views self.views = [] def restoreViews(self): self.views = self.viewsBackup self.viewsBackup = None def reset(self): if self.deck: self.deck.refresh() self.deck.updateAllPriorities() self.rebuildQueue() def rebuildQueue(self): # qt on mac is misbehaving mac = sys.platform.startswith("darwin") if not mac: self.setEnabled(False) self.mainWin.mainText.clear() self.mainWin.mainText.setHtml(_("

Building revision queue..

")) self.app.processEvents() self.deck.rebuildQueue() if not mac: self.setEnabled(True) self.moveToState("initial") def moveToState(self, state): if state == "initial": # reset current card and load again self.currentCard = None self.lastCard = None if self.deck: self.mainWin.menu_Lookup.setEnabled(True) self.enableDeckMenuItems() self.updateRecentFilesMenu() self.updateViews(state) return self.moveToState("getQuestion") else: return self.moveToState("noDeck") elif state == "auto": self.currentCard = None if self.deck: return self.moveToState("getQuestion") else: return self.moveToState("noDeck") # save the new & last state self.lastState = getattr(self, "state", None) self.state = state self.updateTitleBar() if state == "noDeck": self.help.hide() self.currentCard = None self.lastCard = None self.disableDeckMenuItems() self.resetButtons() # hide all deck-associated dialogs ui.dialogs.closeAll() elif state == "getQuestion": self.deck._countsDirty = True if self.deck.cardCount() == 0: return self.moveToState("deckEmpty") else: if not self.currentCard: self.currentCard = self.deck.getCard() if self.currentCard: if self.lastCard: if self.lastCard.id == self.currentCard.id: if self.currentCard.combinedDue > time.time(): # if the same card is being shown and it's not # due yet, give up return self.moveToState("deckFinished") self.enableCardMenuItems() return self.moveToState("showQuestion") else: return self.moveToState("deckFinished") elif state == "deckEmpty": self.resetButtons() self.disableCardMenuItems() self.mainWin.menu_Lookup.setEnabled(False) elif state == "deckFinished": self.deck.s.flush() self.resetButtons() self.mainWin.menu_Lookup.setEnabled(False) self.disableCardMenuItems() self.startRefreshTimer() self.bodyView.setState(state) elif state == "showQuestion": if self.deck.mediaDir(): os.chdir(self.deck.mediaDir()) self.resetButtons() self.showAnswerButton() self.updateMarkAction() self.runHook('showQuestion') elif state == "showAnswer": self.currentCard.stopTimer() self.resetButtons() self.showEaseButtons() self.enableCardMenuItems() self.updateViews(state) def keyPressEvent(self, evt): "Show answer on RET or register answer." if self.state == "showQuestion": if evt.key() in (Qt.Key_Enter, Qt.Key_Return): evt.accept() return self.moveToState("showAnswer") elif self.state == "showAnswer": key = unicode(evt.text()) if key and key >= "0" and key <= "4": # user entered a quality setting num=int(key) evt.accept() return self.cardAnswered(num) evt.ignore() def cardAnswered(self, quality): "Reschedule current card and move back to getQuestion state." # copy card for undo self.lastCardBackup = copy.copy(self.currentCard) # remove card from session before updating it self.deck.s.expunge(self.currentCard) self.deck.answerCard(self.currentCard, quality) self.lastScheduledTime = anki.utils.fmtTimeSpan( self.currentCard.due - time.time()) self.lastQuality = quality self.lastCard = self.currentCard self.currentCard = None if self.config['saveAfterAnswer']: num = self.config['saveAfterAnswerNum'] stats = self.deck.getStats() if stats['gTotal'] % num == 0: self.saveDeck() self.moveToState("getQuestion") 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: sys.stderr.write("earliest time returned negative value\n") 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.state == "deckFinished": # don't try refresh if the deck is closed during a sync if self.deck: self.deck.markExpiredCardsDue() self.moveToState("getQuestion") if self.state != "deckFinished": if self.refreshTimer: self.refreshTimer.stop() self.refreshTimer = None # Buttons ########################################################################## def setupButtons(self): self.outerButtonBox = QHBoxLayout(self.mainWin.buttonWidget) self.outerButtonBox.setMargin(3) self.outerButtonBox.setSpacing(0) self.innerButtonWidget = None def resetButtons(self): # this round-about process is trying to work around a bug in qt if self.lastState == self.state: return if self.innerButtonWidget: self.outerButtonBox.removeWidget(self.innerButtonWidget) self.innerButtonWidget.deleteLater() self.innerButtonWidget = QWidget() self.outerButtonBox.addWidget(self.innerButtonWidget) self.buttonBox = QVBoxLayout(self.innerButtonWidget) self.buttonBox.setSpacing(3) self.buttonBox.setMargin(3) if self.config['easeButtonHeight'] == "tall": self.easeButtonHeight = 50 else: if sys.platform.startswith("darwin"): self.easeButtonHeight = 35 else: self.easeButtonHeight = 25 def showAnswerButton(self): if self.lastState == self.state: return button = QPushButton(_("Show answer")) button.setFixedHeight(self.easeButtonHeight) self.buttonBox.addWidget(button) button.setFocus() button.setDefault(True) self.connect(button, SIGNAL("clicked()"), lambda: self.moveToState("showAnswer")) def getSpacer(self, hpolicy=QSizePolicy.Preferred): return QSpacerItem(20, 20, hpolicy, QSizePolicy.Preferred) def showEaseButtons(self): # if the state hasn't changed, do nothing if self.lastState == self.state: return # gather next intervals nextInts = {} for i in range(5): s=self.deck.nextIntervalStr(self.currentCard, i) nextInts["ease%d" % i] = s text = ( (_("Completely forgot"), ""), (_("Made a mistake"), ""), (_("Difficult"), _("Next in %(ease2)s")), (_("About right"), _("Next in %(ease3)s")), (_("Easy"), _("Next in %(ease4)s"))) # button grid grid = QGridLayout() grid.setSpacing(3) if self.config['easeButtonStyle'] == 'standard': button3 = self.showStandardEaseButtons(grid, nextInts, text) else: button3 = self.showCompactEaseButtons(grid, nextInts) self.buttonBox.addLayout(grid) button3.setFocus() def showStandardEaseButtons(self, grid, nextInts, text): # show 'how well?' message hbox = QHBoxLayout() hbox.addItem(self.getSpacer(QSizePolicy.Expanding)) label = QLabel(self.withInterfaceFont( _("How well did you remember?"))) hbox.addWidget(label) hbox.addItem(self.getSpacer(QSizePolicy.Expanding)) self.buttonBox.addLayout(hbox) # populate buttons button3 = None for i in range(5): button = QPushButton(str(i)) button.setFixedWidth(100) button.setFixedHeight(self.easeButtonHeight) if i == 3: button3 = button grid.addItem(self.getSpacer(QSizePolicy.Expanding), i, 0) grid.addWidget(button, i, 1) grid.addItem(self.getSpacer(), i, 2) grid.addWidget(QLabel(self.withInterfaceFont(text[i][0])), i, 3) grid.addItem(self.getSpacer(), i, 4) if not self.config['suppressEstimates']: grid.addWidget(QLabel(self.withInterfaceFont( text[i][1] % nextInts)), i, 5) grid.addItem(self.getSpacer(QSizePolicy.Expanding), i, 6) self.connect(button, SIGNAL("clicked()"), lambda i=i: self.cardAnswered(i)) return button3 def showCompactEaseButtons(self, grid, nextInts): text = ( (_("%(ease0)s")), (_("%(ease1)s")), (_("%(ease2)s")), (_("%(ease3)s")), (_("%(ease4)s"))) button3 = None for i in range(5): button = QPushButton(str(i)) button.setFixedHeight(self.easeButtonHeight) #button.setFixedWidth(70) if i == 3: button3 = button grid.addWidget(button, 0, (i*2)+1) if not self.config['suppressEstimates']: label = QLabel(self.withInterfaceFont(text[i] % nextInts)) label.setAlignment(Qt.AlignHCenter) grid.addWidget(label, 1, (i*2)+1) self.connect(button, SIGNAL("clicked()"), lambda i=i: self.cardAnswered(i)) return button3 def withInterfaceFont(self, text): family = self.config["interfaceFontFamily"] size = self.config["interfaceFontSize"] colour = self.config["interfaceColour"] css = ('.interface {font-family: "%s"; font-size: %spx; color: %s}\n' % (family, size, colour)) css = "\n" text = css + '' + text + "" return text # Hooks ########################################################################## def setupHooks(self): self.hooks = {} def addHook(self, hookName, func): if not self.hooks.get(hookName, None): self.hooks[hookName] = [] if func not in self.hooks[hookName]: self.hooks[hookName].append(func) def removeHook(self, hookName, func): hook = self.hooks.get(hookName, []) if func in hook: hook.remove(func) def runHook(self, hookName, *args): hook = self.hooks.get(hookName, None) if hook: for func in hook: func(*args) # Deck loading & saving: backend ########################################################################## def setupBackupDir(self): anki.deck.backupDir = os.path.join( self.config.configPath, "backups") def loadDeck(self, deckPath, sync=True): "Load a deck and update the user interface. Maybe sync." # return None if error should be reported # return 0 to fail with no error # return True on success try: self.pauseViews() if not self.saveAndClose(): return 0 finally: self.restoreViews() if not os.path.exists(deckPath): return try: self.deck = DeckStorage.Deck(deckPath, rebuild=False) except (IOError, ImportError): return except DeckWrongFormatError, e: self.importOldDeck(deckPath) if not self.deck: return except DeckAccessError, e: if e.data.get('type') == 'inuse': ui.utils.showWarning(_("Unable to load the same deck twice.")) return 0 return self.updateRecentFiles(self.deck.path) if sync and self.config['syncOnLoad']: self.syncDeck(False) else: try: self.rebuildQueue() except OperationalError: ui.utils.showWarning(_( "Error building queue. Attempting recovery..")) self.onCheckDB() # try again self.rebuildQueue() return True def importOldDeck(self, deckPath): from anki.importing.anki03 import Anki03Importer # back up the old file newPath = re.sub("\.anki$", ".anki-v3", deckPath) while os.path.exists(newPath): newPath += "-1" os.rename(deckPath, newPath) try: self.deck = DeckStorage.Deck(deckPath) imp = Anki03Importer(self.deck, newPath) imp.doImport() except DeckWrongFormatError, e: ui.utils.showWarning(_( "An error occurred while upgrading:\n%s") % `e.data`) return self.rebuildQueue() def maybeLoadLastDeck(self, args): "Open the last deck if possible." # try a command line argument if available try: if args: f = unicode(args[0], sys.getfilesystemencoding()) return self.loadDeck(f) except: sys.stderr.write("Error loading last deck.\n") traceback.print_exc() self.deck = None return self.moveToState("initial") # try recent deck paths for path in self.config['recentDeckPaths']: try: r = self.loadDeck(path) if r == 0: # in use continue return r except: sys.stderr.write("Error loading last deck.\n") traceback.print_exc() self.deck = None return self.moveToState("initial") return self.moveToState("initial") def getDefaultDir(self, save=False): "Try and get default dir from most recently opened file." defaultDir = "" if self.config['recentDeckPaths']: latest = self.config['recentDeckPaths'][0] defaultDir = os.path.dirname(latest) else: if save: defaultDir = unicode(os.path.expanduser("~/"), sys.getfilesystemencoding()) else: samples = self.getSamplesDir() if samples: return samples return defaultDir def getSamplesDir(self): path = os.path.join(ankiqt.runningDir, "libanki") if not os.path.exists(path): path = os.path.join( os.path.join(ankiqt.runningDir, ".."), "libanki") if not os.path.exists(path): path = ankiqt.runningDir if sys.platform.startswith("win32"): path = os.path.split( os.path.split(ankiqt.runningDir)[0])[0] elif sys.platform.startswith("darwin"): path = ankiqt.runningDir + "/../../.." else: path = os.path.join(path, "anki") path = os.path.join(path, "samples") path = os.path.normpath(path) if os.path.exists(path): if sys.platform.startswith("darwin"): return self.openMacSamplesDir(path) return path return "" def openMacSamplesDir(self, path): # some versions of macosx don't allow the open dialog to point inside # a .App file, it seems - so we copy the files onto the desktop. newDir = os.path.expanduser("~/Documents/Anki 0.3 Sample Decks") import shutil if os.path.exists(newDir): files = os.listdir(path) for file in files: loc = os.path.join(path, file) if not os.path.exists(os.path.join(newDir, file)): shutil.copy2(loc, newDir) return newDir shutil.copytree(path, newDir) return newDir def updateRecentFiles(self, path): "Add the current deck to the list of recent files." path = os.path.normpath(path) if path in self.config['recentDeckPaths']: self.config['recentDeckPaths'].remove(path) self.config['recentDeckPaths'].insert(0, path) del self.config['recentDeckPaths'][4:] self.config.save() self.updateRecentFilesMenu() def updateRecentFilesMenu(self): if not self.config['recentDeckPaths']: self.mainWin.menuOpenRecent.setEnabled(False) return self.mainWin.menuOpenRecent.setEnabled(True) self.mainWin.menuOpenRecent.clear() n = 1 for file in self.config['recentDeckPaths']: a = QAction(self) if not sys.platform.startswith("darwin"): a.setShortcut(_("Alt+%d" % n)) a.setText(os.path.basename(file)) a.setStatusTip(os.path.abspath(file)) self.connect(a, SIGNAL("triggered()"), lambda n=n: self.loadRecent(n-1)) self.mainWin.menuOpenRecent.addAction(a) n += 1 def loadRecent(self, n): self.loadDeck(self.config['recentDeckPaths'][n]) # New files, loading & saving ########################################################################## def saveAndClose(self, exit=False): "(Auto)save and close. Prompt if necessary. True if okay to proceed." if self.deck is not None: # sync (saving automatically) if self.config['syncOnClose'] and self.deck.syncName: self.syncDeck(False, reload=False) while self.deckPath: self.app.processEvents() time.sleep(0.1) return True # save if self.deck.modifiedSinceSave(): if self.config['saveOnClose'] or self.config['syncOnClose']: self.saveDeck() else: res = ui.unsaved.ask(self) if res == ui.unsaved.save: self.saveDeck() elif res == ui.unsaved.cancel: return False elif res == ui.unsaved.discard: pass # close self.deck.rollback() self.deck = None if not exit: self.moveToState("noDeck") return True def onNew(self): if not self.saveAndClose(exit=True): return self.deck = DeckStorage.Deck() self.deck.addModel(BasicModel()) self.saveDeck() self.moveToState("initial") def onOpenOnline(self): if not self.saveAndClose(exit=True): return self.deck = DeckStorage.Deck() # ensure all changes come to us self.deck.syncName = None self.deck.modified = 0 self.deck.lastLoaded = self.deck.modified if not self.config['syncUsername'] or not self.config['syncPassword']: d = QDialog(self) vbox = QVBoxLayout() l = QLabel(_( '

Open Online Deck

' 'To load a deck from your free online account,
' "please enter your details below.
")) 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) self.connect(bb, SIGNAL("accepted()"), d.accept) vbox.addWidget(bb) d.setLayout(vbox) d.exec_() self.config['syncUsername'] = unicode(user.text()) self.config['syncPassword'] = unicode(passwd.text()) if self.config['syncUsername'] and self.config['syncPassword']: if self.syncDeck(onlyMerge=True): return self.deck = None self.moveToState("initial") def onOpen(self, samples=False): key = _("Deck files (*.anki)") if samples: defaultDir = self.getSamplesDir() else: defaultDir = self.getDefaultDir() file = QFileDialog.getOpenFileName(self, _("Open deck"), defaultDir, key) file = unicode(file) if not file: return False if samples: # we need to copy into a writeable location new = DeckStorage.newDeckPath() shutil.copyfile(file, new) file = new ret = self.loadDeck(file) if not ret: if ret is None: ui.utils.showWarning(_("Unable to load file.")) self.deck = None return False else: self.updateRecentFiles(file) return True def onOpenSamples(self): self.onOpen(samples=True) def onSave(self): if self.deck.modifiedSinceSave(): self.saveDeck() else: self.setStatus(_("Deck is not modified.")) self.updateTitleBar() def onSaveAs(self): "Prompt for a file name, then save." title = _("Save deck") dir = os.path.dirname(self.deck.path) file = QFileDialog.getSaveFileName(self, title, dir, _("Deck files (*.anki)"), None, QFileDialog.DontConfirmOverwrite) file = unicode(file) if not file: return if not file.lower().endswith(".anki"): file += ".anki" if os.path.exists(file): # check for existence after extension if not ui.utils.askUser( "This file exists. Are you sure you want to overwrite it?"): return self.deck = self.deck.saveAs(file) self.updateTitleBar() self.moveToState("initial") def saveDeck(self): self.setStatus(_("Saving..")) self.deck.save() self.updateRecentFiles(self.deck.path) self.updateTitleBar() self.setStatus(_("Saving..done")) # Opening and closing the app ########################################################################## def prepareForExit(self): "Save config and window geometry." self.runHook('quit') self.help.hide() self.config['mainWindowPos'] = self.pos() self.config['mainWindowSize'] = self.size() # save config try: self.config.save() except (IOError, OSError), e: ui.utils.showWarning(_("Anki was unable to save your " "configuration file:\n%s" % e)) def closeEvent(self, event): "User hit the X button, etc." if not self.saveAndClose(exit=True): event.ignore() else: self.prepareForExit() event.accept() self.app.quit() # Anchor clicks ########################################################################## def onWelcomeAnchor(self, str): if str == "new": self.onNew() elif str == "sample": self.onOpenSamples() elif str == "open": self.onOpen() def setupAnchors(self): self.anchorPrefixes = { 'welcome': self.onWelcomeAnchor, } self.connect(self.mainWin.mainText, SIGNAL("anchorClicked(QUrl)"), self.anchorClicked) def anchorClicked(self, url): # prevent the link being handled self.mainWin.mainText.setSource(QUrl("")) addr = unicode(url.toString()) fields = addr.split(":") if len(fields) > 1 and fields[0] in self.anchorPrefixes: self.anchorPrefixes[fields[0]](*fields[1:]) else: # open in browser QDesktopServices.openUrl(QUrl(url)) # Tools - looking up words in the dictionary ########################################################################## def initLookup(self): if not getattr(self, "lookup", None): self.lookup = ui.lookup.Lookup(self) def onLookupExpression(self): self.initLookup() try: self.lookup.alc(self.currentCard.fact['Expression']) except KeyError: self.setStatus(_("No expression in current card.")) def onLookupMeaning(self): self.initLookup() try: self.lookup.alc(self.currentCard.fact['Meaning']) except KeyError: self.setStatus(_("No meaning in current card.")) def onLookupEdictSelection(self): self.initLookup() self.lookup.selection(self.lookup.edict) def onLookupEdictKanjiSelection(self): self.initLookup() self.lookup.selection(self.lookup.edictKanji) def onLookupAlcSelection(self): self.initLookup() self.lookup.selection(self.lookup.alc) # Tools - statistics ########################################################################## def onKanjiStats(self): rep = anki.stats.KanjiStats(self.deck).report() rep += _("Missing Kanji
") self.help.showText(rep, py={"miss": self.onMissingStats}) def onMissingStats(self): ks = anki.stats.KanjiStats(self.deck) ks.genKanjiSets() self.help.showText(ks.missingReport()) def onDeckStats(self): txt = anki.stats.DeckStats(self.deck).report() self.help.showText(txt) def onCardStats(self): self.addHook("showQuestion", self.onCardStats) self.addHook("helpChanged", self.removeCardStatsHook) txt = "" if self.currentCard: txt += _("

Current card

") txt += anki.stats.CardStats(self.deck, self.currentCard).report() if self.lastCard and self.lastCard != self.currentCard: txt += _("

Last card

") txt += anki.stats.CardStats(self.deck, self.lastCard).report() if not txt: txt = _("No current card or last card.") self.help.showText(txt, key="cardStats") def removeCardStatsHook(self): "Remove the update hook if the help menu was changed." if self.help.currentKey != "cardStats": self.removeHook("showQuestion", self.onCardStats) def onShowGraph(self): self.setStatus(_("Loading graphs (may take time)..")) self.app.processEvents() import anki.graphs if anki.graphs.graphsAvailable(): try: ui.dialogs.get("Graphs", self, self.deck) except (ImportError, ValueError): if sys.platform.startswith("win32"): ui.utils.showInfo( _("To display graphs, Anki needs a .dll file which\n" "you don't have. Please install:\n") + "http://www.dll-files.com/dllindex/dll-files.shtml?msvcp71") else: ui.utils.showInfo(_( "Your version of Matplotlib is broken.\n" "Please see http://repose.ath.cx/tracker/anki/issue102")) else: ui.utils.showInfo(_("Please install python-matplotlib to access graphs.")) def onKanjiOccur(self): self.setStatus(_("Generating report (may take time)..")) self.app.processEvents() import tempfile (fd, name) = tempfile.mkstemp(suffix=".html") f = os.fdopen(fd, 'w') ko = anki.stats.KanjiOccurStats(self.deck) ko.reportFile(f) f.close() if sys.platform == "win32": url = "file:///" else: url = "file://" url += os.path.abspath(name) QDesktopServices.openUrl(QUrl(url)) # Marking, suspending and undoing ########################################################################## def onMark(self, toggled): if self.currentCard.hasTag("Marked"): self.currentCard.tags = deleteTags("Marked", self.currentCard.tags) else: self.currentCard.tags = addTags("Marked", self.currentCard.tags) self.currentCard.setModified() self.deck.setModified() def onSuspend(self): self.currentCard.tags = addTags("Suspended", self.currentCard.tags) self.deck.updatePriority(self.currentCard) self.currentCard.setModified() self.deck.setModified() self.lastScheduledTime = None self.moveToState("initial") def onUndoAnswer(self): # quick and dirty undo for now self.currentCard = None self.deck.s.flush() self.lastCardBackup.toDB(self.deck.s) self.reset() # Other menu operations ########################################################################## def onAddCard(self): ui.dialogs.get("AddCards", self) def onEditDeck(self): ui.dialogs.get("CardList", self) def onDeckProperties(self): self.deckProperties = ui.deckproperties.DeckProperties(self) def onModelProperties(self): if self.currentCard: model = self.currentCard.fact.model else: model = self.deck.currentModel ui.modelproperties.ModelProperties(self, model) def onDisplayProperties(self): ui.dialogs.get("DisplayProperties", self) def onPrefs(self): ui.preferences.Preferences(self, self.config) def onReportBug(self): QDesktopServices.openUrl(QUrl(ankiqt.appIssueTracker)) def onForum(self): QDesktopServices.openUrl(QUrl(ankiqt.appForum)) def onAbout(self): ui.about.show(self) # Importing & exporting ########################################################################## def onImport(self): if self.deck is None: self.onNew() if self.deck is not None: ui.importing.ImportDialog(self) def onExport(self): ui.exporting.ExportDialog(self) # Language handling ########################################################################## def setLang(self): "Set the user interface language." languageDir=os.path.join(ankiqt.modDir, "locale") self.languageTrans = gettext.translation('ankiqt', languageDir, languages=[self.config["interfaceLang"]], fallback=True) self.installTranslation() if getattr(self, 'mainWin', None): self.mainWin.retranslateUi(self) self.alterShortcuts() anki.lang.setLang(self.config["interfaceLang"]) self.updateTitleBar() def getTranslation(self, text): return self.languageTrans.ugettext(text) def installTranslation(self): import __builtin__ __builtin__.__dict__['_'] = self.getTranslation # Syncing ########################################################################## def syncDeck(self, interactive=True, create=False, onlyMerge=False, reload=True, checkSources=True): "Synchronise a deck with the server." # vet input u=self.config['syncUsername'] p=self.config['syncPassword'] if not u or not p: msg = _("Not syncing, username or password unset.") if interactive: ui.utils.showWarning(msg) return if self.deck is None and self.deckPath is None: # qt on linux incorrectly accepts shortcuts for disabled actions return if self.deck: # save first, so we can rollback on failure self.deck.save() self.deck.close() # 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 = None self.loadAfterSync = reload # bug triggered by preferences dialog - underlying c++ widgets are not # garbage collected until the middle of the child thread import gc; gc.collect() self.bodyView.clearWindow() self.bodyView.flush() self.syncThread = ui.sync.Sync(self, u, p, interactive, create, onlyMerge, self.sourcesToCheck) self.connect(self.syncThread, SIGNAL("setStatus"), self.setSyncStatus) self.connect(self.syncThread, SIGNAL("showWarning"), ui.utils.showWarning) 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.syncFinished) self.syncThread.start() self.setEnabled(False) while not self.syncThread.isFinished(): self.app.processEvents() self.syncThread.wait(100) self.setEnabled(True) return self.syncThread.ok def syncFinished(self): "Reopen after sync finished." if self.loadAfterSync: self.loadDeck(self.deckPath, sync=False) self.deck.syncName = self.syncName self.deck.s.flush() self.deck.s.commit() else: self.moveToState("noDeck") self.deckPath = None def selectSyncDeck(self, decks, create=True): name = ui.sync.DeckChooser(self, decks, create).getName() self.syncName = name if name: if name == self.syncName: self.syncDeck(create=True) else: self.syncDeck() else: if not create: # called via 'new' - close self.cleanNewDeck() else: self.syncFinished() def cleanNewDeck(self): "Unload a new deck if an initial sync failed." self.deck = None self.moveToState("initial") def setSyncStatus(self, text, *args): self.setStatus(text, *args) self.mainWin.mainText.append("" + text + "") def syncClockOff(self, diff): ui.utils.showWarning( _("Your computer clock is not set to the correct time.\n" "It is off by %d seconds.\n\n" "Since this can cause many problems with syncing,\n" "syncing is disabled until you fix the problem.") % diff) self.syncFinished() # Menu, title bar & status ########################################################################## deckRelatedMenuItems = ( "Save", "SaveAs", "Close", "Addcards", "Editdeck", "Syncdeck", "DisplayProperties", "DeckProperties", "ModelProperties", "UndoAnswer", "Export", "MarkCard", "Graphs", "Dstats", "Kstats", "Cstats", ) deckRelatedMenus = ( "Tools", "Advanced", ) def connectMenuActions(self): self.connect(self.mainWin.actionNew, SIGNAL("triggered()"), self.onNew) self.connect(self.mainWin.actionOpenOnline, SIGNAL("triggered()"), self.onOpenOnline) self.connect(self.mainWin.actionOpen, SIGNAL("triggered()"), self.onOpen) self.connect(self.mainWin.actionOpenSamples, SIGNAL("triggered()"), self.onOpenSamples) self.connect(self.mainWin.actionSave, SIGNAL("triggered()"), self.onSave) self.connect(self.mainWin.actionSaveAs, SIGNAL("triggered()"), self.onSaveAs) self.connect(self.mainWin.actionClose, SIGNAL("triggered()"), self.saveAndClose) self.connect(self.mainWin.actionExit, SIGNAL("triggered()"), self, SLOT("close()")) self.connect(self.mainWin.actionSyncdeck, SIGNAL("triggered()"), self.syncDeck) self.connect(self.mainWin.actionDeckProperties, SIGNAL("triggered()"), self.onDeckProperties) self.connect(self.mainWin.actionDisplayProperties, SIGNAL("triggered()"),self.onDisplayProperties) self.connect(self.mainWin.actionAddcards, SIGNAL("triggered()"), self.onAddCard) self.connect(self.mainWin.actionEditdeck, SIGNAL("triggered()"), self.onEditDeck) self.connect(self.mainWin.actionPreferences, SIGNAL("triggered()"), self.onPrefs) self.connect(self.mainWin.actionLookup_es, SIGNAL("triggered()"), self.onLookupEdictSelection) self.connect(self.mainWin.actionLookup_esk, SIGNAL("triggered()"), self.onLookupEdictKanjiSelection) self.connect(self.mainWin.actionLookup_expr, SIGNAL("triggered()"), self.onLookupExpression) self.connect(self.mainWin.actionLookup_mean, SIGNAL("triggered()"), self.onLookupMeaning) self.connect(self.mainWin.actionLookup_as, SIGNAL("triggered()"), self.onLookupAlcSelection) self.connect(self.mainWin.actionDstats, SIGNAL("triggered()"), self.onDeckStats) self.connect(self.mainWin.actionKstats, SIGNAL("triggered()"), self.onKanjiStats) self.connect(self.mainWin.actionCstats, SIGNAL("triggered()"), self.onCardStats) self.connect(self.mainWin.actionGraphs, SIGNAL("triggered()"), self.onShowGraph) self.connect(self.mainWin.actionAbout, SIGNAL("triggered()"), self.onAbout) self.connect(self.mainWin.actionReportbug, SIGNAL("triggered()"), self.onReportBug) self.connect(self.mainWin.actionForum, SIGNAL("triggered()"), self.onForum) self.connect(self.mainWin.actionStarthere, SIGNAL("triggered()"), self.onStartHere) self.connect(self.mainWin.actionImport, SIGNAL("triggered()"), self.onImport) self.connect(self.mainWin.actionExport, SIGNAL("triggered()"), self.onExport) self.connect(self.mainWin.actionMarkCard, SIGNAL("toggled(bool)"), self.onMark) self.connect(self.mainWin.actionSuspendCard, SIGNAL("triggered()"), self.onSuspend) self.connect(self.mainWin.actionModelProperties, SIGNAL("triggered()"), self.onModelProperties) self.connect(self.mainWin.actionRepeatQuestionAudio, SIGNAL("triggered()"), self.onRepeatQuestion) self.connect(self.mainWin.actionRepeatAnswerAudio, SIGNAL("triggered()"), self.onRepeatAnswer) self.connect(self.mainWin.actionRepeatAudio, SIGNAL("triggered()"), self.onRepeatAudio) self.connect(self.mainWin.actionUndoAnswer, SIGNAL("triggered()"), self.onUndoAnswer) self.connect(self.mainWin.actionCheckDatabaseIntegrity, SIGNAL("triggered()"), self.onCheckDB) self.connect(self.mainWin.actionOptimizeDatabase, SIGNAL("triggered()"), self.onOptimizeDB) self.connect(self.mainWin.actionMergeModels, SIGNAL("triggered()"), self.onMergeModels) self.connect(self.mainWin.actionCheckMediaDatabase, SIGNAL("triggered()"), self.onCheckMediaDB) def enableDeckMenuItems(self, enabled=True): "setEnabled deck-related items." for item in self.deckRelatedMenus: getattr(self.mainWin, "menu" + item).setEnabled(enabled) for item in self.deckRelatedMenuItems: getattr(self.mainWin, "action" + item).setEnabled(enabled) def disableDeckMenuItems(self): "Disable deck-related items." self.enableDeckMenuItems(enabled=False) def updateTitleBar(self): "Display the current deck and card count in the titlebar." title=ankiqt.appName + " " + ankiqt.appVersion if self.deck != None: deckpath = self.deck.name() if self.deck.modifiedSinceSave(): deckpath += "*" title = _("%(path)s (%(facts)d facts, %(cards)d cards)" " - %(title)s") % { "path": deckpath, "title": title, "cards": self.deck.cardCount(), "facts": self.deck.factCount(), } self.setWindowTitle(title) def setStatus(self, text, timeout=3000): self.mainWin.statusbar.showMessage(text, timeout) def onStartHere(self): QDesktopServices.openUrl(QUrl(ankiqt.appHelpSite)) def alterShortcuts(self): if sys.platform.startswith("darwin"): self.mainWin.actionAddcards.setShortcut(_("Ctrl+D")) self.mainWin.actionClose.setShortcut("") def updateMarkAction(self): self.mainWin.actionMarkCard.blockSignals(True) if self.currentCard.hasTag("Marked"): self.mainWin.actionMarkCard.setChecked(True) else: self.mainWin.actionMarkCard.setChecked(False) self.mainWin.actionMarkCard.blockSignals(False) def disableCardMenuItems(self): self.mainWin.actionUndoAnswer.setEnabled(not not self.lastCard) self.mainWin.actionMarkCard.setEnabled(False) self.mainWin.actionSuspendCard.setEnabled(False) self.mainWin.actionRepeatQuestionAudio.setEnabled(False) self.mainWin.actionRepeatAnswerAudio.setEnabled(False) self.mainWin.actionRepeatAudio.setEnabled(False) def enableCardMenuItems(self): self.mainWin.actionUndoAnswer.setEnabled(not not self.lastCard) self.mainWin.actionMarkCard.setEnabled(True) self.mainWin.actionSuspendCard.setEnabled(True) self.mainWin.actionRepeatQuestionAudio.setEnabled( hasSound(self.currentCard.question)) self.mainWin.actionRepeatAnswerAudio.setEnabled( hasSound(self.currentCard.answer) and self.state != "getQuestion") self.mainWin.actionRepeatAudio.setEnabled( self.mainWin.actionRepeatQuestionAudio.isEnabled() or self.mainWin.actionRepeatAnswerAudio.isEnabled()) # Auto update ########################################################################## def setupAutoUpdate(self): self.autoUpdate = ui.update.LatestVersionFinder(self) self.connect(self.autoUpdate, SIGNAL("newVerAvail"), self.newVerAvail) self.connect(self.autoUpdate, SIGNAL("clockIsOff"), self.clockIsOff) self.autoUpdate.start() def newVerAvail(self, version): if self.config['suppressUpdate'] < version['latestVersion']: ui.update.askAndUpdate(self, version) def clockIsOff(self, diff): if diff < 0: ret = _("late") else: ret = _("early") ui.utils.showWarning( _("Your computer clock is not set to the correct time.\n" "It is %(sec)d seconds %(type)s.\n" " Please ensure it is set correctly and then restart Anki.") % { "sec": abs(diff), "type": ret } ) # User customisations ########################################################################## def loadUserCustomisations(self): # look for config file dir = self.config.configPath file = os.path.join(dir, "custom.py") plugdir = os.path.join(dir, "plugins") sys.path.insert(0, dir) if os.path.exists(file): try: import custom except: print "Error in custom.py" print traceback.print_exc() sys.path.insert(0, plugdir) import glob plugins = [f.replace(".py", "") for f in os.listdir(plugdir) \ if f.endswith(".py")] plugins.sort() for plugin in plugins: try: __import__(plugin) except: print "Error in %s.py" % plugin print traceback.print_exc() # Font localisation ########################################################################## def setupFonts(self): for (s, p) in anki.fonts.substitutions(): QFont.insertSubstitution(s, p) # Sounds ########################################################################## def onRepeatQuestion(self): playFromText(self.currentCard.question) def onRepeatAnswer(self): playFromText(self.currentCard.answer) def onRepeatAudio(self): playFromText(self.currentCard.question) if self.state != "showQuestion": playFromText(self.currentCard.answer) # Advanced features ########################################################################## def onCheckDB(self): "True if no problems" ret = self.deck.fixIntegrity() if ret == "ok": ret = _("""\ No problems found. Some data structures have been rebuilt in case they were causing problems. On the next sync, all cards will be sent to the server.""") ui.utils.showInfo(ret) ret = True else: ret = _("Problems found:\n%s") % ret ui.utils.showWarning(ret) ret = False self.rebuildQueue() return ret def onOptimizeDB(self): size = self.deck.optimize() ui.utils.showInfo("Database optimized.\nShrunk by %d bytes" % size) def onMergeModels(self): ret = self.deck.canMergeModels() if ret[0] == "ok": if not ret[1]: ui.utils.showInfo(_( "No models found to merge. If you want to merge models,\n" "all models must have the same name.")) return if ui.utils.askUser(_( "Would you like to merge models that have the same name?")): self.deck.mergeModels(ret[1]) ui.utils.showInfo(_("Merge complete.")) else: ui.utils.showWarning(_("""%s. Anki can only merge models if they have exactly the same field count and card count.""") % ret[1]) def onCheckMediaDB(self): mb = QMessageBox(self) mb.setText(_("""\ Would you like to remove unused files from the media directory, and tag or delete references to missing files?""")) bTag = QPushButton("Tag facts missing media") mb.addButton(bTag, QMessageBox.RejectRole) bDelete = QPushButton("Delete references to missing media") mb.addButton(bDelete, QMessageBox.RejectRole) bCancel = QPushButton("Cancel") mb.addButton(bCancel, QMessageBox.RejectRole) mb.exec_() if mb.clickedButton() == bTag: (missing, unused) = rebuildMediaDir(self.deck, False) elif mb.clickedButton() == bDelete: (missing, unused) = rebuildMediaDir(self.deck, True) else: return ui.utils.showInfo(_( "%(a)d missing references.\n" "%(b)d unused files removed.") % { 'a': missing, 'b': unused})