mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
1345 lines
50 KiB
Python
1345 lines
50 KiB
Python
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# -*- 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
|
|
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(_("<h1>Building revision queue..</h1>"))
|
|
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 <b>%(ease2)s</b>")),
|
|
(_("About right"),
|
|
_("Next in <b>%(ease3)s</b>")),
|
|
(_("Easy"),
|
|
_("Next in <b>%(ease4)s</b>")))
|
|
# 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(
|
|
_("<b>How well did you remember?</b>")))
|
|
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 = (
|
|
(_("<b>%(ease0)s</b>")),
|
|
(_("<b>%(ease1)s</b>")),
|
|
(_("<b>%(ease2)s</b>")),
|
|
(_("<b>%(ease3)s</b>")),
|
|
(_("<b>%(ease4)s</b>")))
|
|
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 = "<style>\n" + css + "</style>\n"
|
|
text = css + '<span class="interface">' + text + "</span>"
|
|
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:
|
|
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(): return
|
|
self.deck = DeckStorage.Deck()
|
|
m = ui.modelchooser.AddModel(self, online=True).getModel()
|
|
if m:
|
|
if m != "online":
|
|
self.deck.addModel(m)
|
|
self.saveDeck()
|
|
self.moveToState("initial")
|
|
return
|
|
# ensure all changes come to us
|
|
self.deck.syncName = None
|
|
self.deck.modified = 0
|
|
self.deck.lastLoaded = self.deck.modified
|
|
self.deck.s.flush()
|
|
self.deck.s.commit()
|
|
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 += _("<a href=py:miss>Missing Kanji</a><br>")
|
|
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 += _("<h1>Current card</h1>")
|
|
txt += anki.stats.CardStats(self.deck, self.currentCard).report()
|
|
if self.lastCard and self.lastCard != self.currentCard:
|
|
txt += _("<h1>Last card</h1>")
|
|
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):
|
|
"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()
|
|
self.deckPath = self.deck.path
|
|
self.syncName = self.deck.syncName or self.deck.name()
|
|
self.lastSync = self.deck.lastSync
|
|
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.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("<font size=+6>" + text + "</font>")
|
|
|
|
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",
|
|
"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.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})
|