Anki/ankiqt/ui/main.py

3275 lines
124 KiB
Python
Executable file

# Copyright: Damien Elmes <anki@ichi2.net>
# -*- coding: utf-8 -*-
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import os, sys, re, types, gettext, stat, traceback, inspect, signal
import shutil, time, glob, tempfile, datetime, zipfile, locale
from operator import itemgetter
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtWebKit import QWebPage
from PyQt4 import pyqtconfig
QtConfig = pyqtconfig.Configuration()
from anki import DeckStorage
from anki.errors import *
from anki.sound import hasSound, playFromText, clearAudioQueue, stripSounds
from anki.utils import addTags, deleteTags, parseTags, canonifyTags, \
stripHTML, checksum
from anki.media import rebuildMediaDir, downloadMissing, downloadRemote
from anki.db import OperationalError, SessionHelper, sqlite
from anki.stdmodels import BasicModel
from anki.hooks import runHook, addHook, removeHook, _hooks, wrap
from anki.deck import newCardOrderLabels, newCardSchedulingLabels
from anki.deck import revCardOrderLabels, failedCardOptionLabels
from ankiqt.ui.utils import saveGeom, restoreGeom, saveState, restoreState
import anki.lang
import anki.deck
import ankiqt
ui = ankiqt.ui
config = ankiqt.config
class AnkiQt(QMainWindow):
def __init__(self, app, config, args):
QMainWindow.__init__(self)
try:
self.errorOccurred = False
self.inDbHandler = False
self.reviewingStarted = False
if sys.platform.startswith("darwin"):
qt_mac_set_menubar_icons(False)
elif sys.platform.startswith("win32"):
# make sure they're picked up on bundle
from ctypes import windll, wintypes
ankiqt.mw = self
self.app = app
self.config = config
self.deck = None
self.state = "initial"
self.hideWelcome = False
self.views = []
signal.signal(signal.SIGINT, self.onSigInt)
self.setLang()
self.setupStyle()
self.setupFonts()
self.setupBackupDir()
self.setupProxy()
self.setupMainWindow()
self.setupDeckBrowser()
self.setupSystemHacks()
self.setupSound()
self.setupTray()
self.setupSync()
self.connectMenuActions()
ui.splash.update()
self.setupViews()
self.setupEditor()
self.setupStudyScreen()
self.setupButtons()
self.setupAnchors()
self.setupToolbar()
self.setupProgressInfo()
self.setupBackups()
if self.config['mainWindowState']:
restoreGeom(self, "mainWindow", 21)
restoreState(self, "mainWindow")
else:
self.resize(500, 500)
# load deck
ui.splash.update()
self.setupErrorHandler()
self.setupMisc()
# check if we've been updated
if "version" not in self.config:
# could be new user, or upgrade from older version
# which didn't have version variable
self.appUpdated = "first"
elif self.config['version'] != ankiqt.appVersion:
self.appUpdated = self.config['version']
else:
self.appUpdated = False
if self.appUpdated:
self.config['version'] = ankiqt.appVersion
# plugins might be looking at this
self.state = "noDeck"
self.loadPlugins()
self.setupAutoUpdate()
self.rebuildPluginsMenu()
# plugins loaded, now show interface
ui.splash.finish(self)
self.show()
# program open sync
if self.config['syncOnProgramOpen']:
self.syncDeck(interactive=False)
if (args or self.config['loadLastDeck'] or
len(self.config['recentDeckPaths']) == 1):
# open the last deck
self.maybeLoadLastDeck(args)
if self.deck:
# deck open sync?
if self.config['syncOnLoad'] and self.deck.syncName:
self.syncDeck(interactive=False)
elif not self.config['syncOnProgramOpen'] or not self.browserDecks:
# sync disabled or no user/pass, so draw deck browser manually
self.moveToState("noDeck")
# all setup is done, run after-init hook
try:
runHook('init')
except:
ui.utils.showWarning(
_("Broken plugin:\n\n%s") %
unicode(traceback.format_exc(), "utf-8", "replace"))
# activate & raise is useful when run from the command line on osx
self.activateWindow()
self.raise_()
except:
ui.utils.showInfo("Error during startup:\n%s" %
traceback.format_exc())
sys.exit(1)
def onSigInt(self, signum, frame):
self.close()
def onReload(self):
self.moveToState("auto")
def setupMainWindow(self):
# main window
self.mainWin = ankiqt.forms.main.Ui_MainWindow()
self.mainWin.setupUi(self)
self.mainWin.mainText = ui.view.AnkiWebView(self.mainWin.mainTextFrame)
self.mainWin.mainText.setObjectName("mainText")
self.mainWin.mainText.setFocusPolicy(Qt.ClickFocus)
self.mainWin.mainStack.addWidget(self.mainWin.mainText)
self.help = ui.help.HelpArea(self.mainWin.helpFrame, self.config, self)
self.connect(self.mainWin.mainText.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.mainWin.noticeFrame.setShown(False)
self.connect(self.mainWin.noticeButton, SIGNAL("clicked()"),
lambda: self.mainWin.noticeFrame.setShown(False))
if sys.platform.startswith("win32"):
self.mainWin.noticeButton.setFixedWidth(24)
elif sys.platform.startswith("darwin"):
self.mainWin.noticeButton.setFixedWidth(20)
self.mainWin.noticeButton.setFixedHeight(20)
addHook("cardAnswered", self.onCardAnsweredHook)
addHook("undoEnd", self.maybeEnableUndo)
addHook("notify", self.onNotify)
def onNotify(self, msg):
if self.mainThread != QThread.currentThread():
# decks may be opened in a sync thread
sys.stderr.write(msg + "\n")
else:
ui.utils.showInfo(msg)
def setNotice(self, str=""):
if str:
self.mainWin.noticeLabel.setText(str)
self.mainWin.noticeFrame.setShown(True)
else:
self.mainWin.noticeFrame.setShown(False)
def setupViews(self):
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)
def setupTray(self):
self.trayIcon = ui.tray.AnkiTrayIcon(self)
def setupErrorHandler(self):
class ErrorPipe(object):
def __init__(self, parent):
self.parent = parent
self.timer = None
self.pool = ""
self.poolUpdated = 0
def write(self, data):
try:
print data.encode("utf-8"),
except:
print data
self.pool += data
self.poolUpdated = time.time()
def haveError(self):
if self.pool:
if (time.time() - self.poolUpdated) > 1:
return True
def getError(self):
p = self.pool
self.pool = ""
try:
return unicode(p, 'utf8', 'replace')
except TypeError:
# already unicode
return p
self.errorPipe = ErrorPipe(self)
sys.stderr = self.errorPipe
self.errorTimer = QTimer(self)
self.errorTimer.start(1000)
self.connect(self.errorTimer,
SIGNAL("timeout()"),
self.onErrorTimer)
def onErrorTimer(self):
if self.errorPipe.haveError():
error = self.errorPipe.getError()
if "font_manager.py" in error:
# hack for matplotlib errors on osx
return
if "Audio player not found" in error:
ui.utils.showInfo(
_("Couldn't play sound. Please install mplayer."))
return
if "ERR-0100" in error:
ui.utils.showInfo(error)
return
if "ERR-0101" in error:
ui.utils.showInfo(error)
return
stdText = _("""\
An error occurred. It may have been caused by a harmless bug, <br>
or your deck may have a problem.
<p>To confirm it's not a problem with your deck, please <b>restart<br>
Anki</b> and run <b>Tools > Advanced > Check Database</b>.
<p>If that doesn't fix the problem, please copy the following<br>
into a bug report:<br>
""")
pluginText = _("""\
An error occurred in a plugin. Please contact the plugin author.<br>
Please do not file a bug report with Anki.<br>""")
if "plugin" in error:
txt = pluginText
else:
txt = stdText
self.errorOccurred = True
# show dialog
diag = QDialog(self.app.activeWindow())
diag.setWindowTitle("Anki")
layout = QVBoxLayout(diag)
diag.setLayout(layout)
text = QTextEdit()
text.setReadOnly(True)
text.setHtml(txt + "<div style='white-space: pre-wrap'>" + error + "</div>")
layout.addWidget(text)
box = QDialogButtonBox(QDialogButtonBox.Close)
layout.addWidget(box)
self.connect(box, SIGNAL("rejected()"), diag, SLOT("reject()"))
diag.setMinimumHeight(400)
diag.setMinimumWidth(500)
diag.exec_()
self.clearProgress()
def closeAllDeckWindows(self):
ui.dialogs.closeAll()
self.help.hide()
# State machine
##########################################################################
def addView(self, view):
self.views.append(view)
def updateViews(self, status):
if self.deck is None and status != "noDeck":
raise Exception("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, count=True, priorities=False, runHooks=True):
if self.deck:
self.deck.refreshSession()
if priorities:
self.deck.updateAllPriorities()
self.deck.reset()
if runHooks:
runHook("guiReset")
self.moveToState("initial")
def moveToState(self, state):
t = time.time()
if state == "initial":
# reset current card and load again
self.currentCard = None
self.lastCard = None
self.editor.deck = self.deck
if self.deck:
self.enableDeckMenuItems()
self.updateViews(state)
if self.state == "studyScreen":
return self.moveToState("studyScreen")
else:
return self.moveToState("getQuestion")
else:
return self.moveToState("noDeck")
elif state == "auto":
self.currentCard = None
self.lastCard = None
if self.deck:
if self.state == "studyScreen":
return self.moveToState("studyScreen")
else:
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' and state != 'editCurrentFact':
self.switchToReviewScreen()
if state == "noDeck":
self.deck = None
self.help.hide()
self.currentCard = None
self.lastCard = None
self.disableDeckMenuItems()
# hide all deck-associated dialogs
self.closeAllDeckWindows()
self.showDeckBrowser()
elif state == "getQuestion":
# stop anything playing
clearAudioQueue()
if self.deck.isEmpty():
return self.moveToState("deckEmpty")
else:
# timeboxing only supported using the standard scheduler
if not self.deck.finishScheduler:
if (self.config['showStudyScreen'] and
not self.deck.sessionStartTime):
return self.moveToState("studyScreen")
if self.deck.sessionLimitReached():
self.showToolTip(_("Session limit reached."))
self.moveToState("studyScreen")
# switch to timeboxing screen
self.mainWin.tabWidget.setCurrentIndex(2)
return
if not self.currentCard:
self.currentCard = self.deck.getCard()
if self.currentCard:
if self.lastCard:
if self.lastCard.id == self.currentCard.id:
pass
# 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.switchToWelcomeScreen()
self.disableCardMenuItems()
elif state == "deckFinished":
self.currentCard = None
self.deck.s.flush()
self.hideButtons()
self.disableCardMenuItems()
self.switchToCongratsScreen()
self.mainWin.learnMoreButton.setEnabled(
not not self.deck.newCount)
self.startRefreshTimer()
self.bodyView.setState(state)
# focus finish button
self.mainWin.finishButton.setFocus()
runHook('deckFinished')
elif state == "showQuestion":
self.reviewingStarted = True
# ensure cwd set to media dir
self.deck.mediaDir()
self.showAnswerButton()
self.updateMarkAction()
runHook('showQuestion')
elif state == "showAnswer":
self.showEaseButtons()
self.enableCardMenuItems()
elif state == "editCurrentFact":
if self.lastState == "editCurrentFact":
return self.moveToState("saveEdit")
self.mainWin.actionRepeatAudio.setEnabled(False)
self.deck.s.flush()
self.showEditor()
elif state == "saveEdit":
self.mainWin.actionRepeatAudio.setEnabled(True)
self.editor.saveFieldsNow()
self.mainWin.buttonStack.show()
return self.reset()
elif state == "studyScreen":
self.currentCard = None
if self.deck.finishScheduler:
self.deck.finishScheduler()
self.disableCardMenuItems()
self.showStudyScreen()
self.updateViews(state)
def keyPressEvent(self, evt):
"Show answer on RET or register answer."
if evt.key() in (Qt.Key_Up,Qt.Key_Down,Qt.Key_Left,Qt.Key_Right,
Qt.Key_PageUp,Qt.Key_PageDown):
mf = self.bodyView.body.page().currentFrame()
if evt.key() == Qt.Key_Up:
mf.evaluateJavaScript("window.scrollBy(0,-20)")
elif evt.key() == Qt.Key_Down:
mf.evaluateJavaScript("window.scrollBy(0,20)")
elif evt.key() == Qt.Key_Left:
mf.evaluateJavaScript("window.scrollBy(-20,0)")
elif evt.key() == Qt.Key_Right:
mf.evaluateJavaScript("window.scrollBy(20,0)")
elif evt.key() == Qt.Key_PageUp:
mf.evaluateJavaScript("window.scrollBy(0,-%d)" %
int(0.9*self.bodyView.body.size().
height()))
elif evt.key() == Qt.Key_PageDown:
mf.evaluateJavaScript("window.scrollBy(0,%d)" %
int(0.9*self.bodyView.body.size().
height()))
evt.accept()
return
if self.state == "showQuestion":
if evt.key() in (Qt.Key_Enter,
Qt.Key_Return):
evt.accept()
return self.mainWin.showAnswerButton.click()
elif evt.key() == Qt.Key_Space and not (
self.currentCard.cardModel.typeAnswer):
evt.accept()
return self.mainWin.showAnswerButton.click()
elif self.state == "showAnswer":
if evt.key() == Qt.Key_Space:
key = str(self.defaultEaseButton())
else:
key = unicode(evt.text())
if key and key >= "1" and key <= "4":
# user entered a quality setting
num=int(key)
evt.accept()
return getattr(self.mainWin, "easeButton%d" %
num).animateClick()
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 cardAnswered(self, quality):
"Reschedule current card and move back to getQuestion state."
if self.state != "showAnswer":
return
# force refresh of card then remove from session as we update in pure sql
self.deck.s.refresh(self.currentCard)
self.deck.s.refresh(self.currentCard.fact)
self.deck.s.refresh(self.currentCard.cardModel)
self.deck.s.expunge(self.currentCard)
# answer
self.deck.answerCard(self.currentCard, quality)
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.save()
self.moveToState("getQuestion")
def onCardAnsweredHook(self, cardId, isLeech):
if not isLeech:
self.setNotice()
return
txt = (_("""\
<b>%s</b>... is a <a href="http://ichi2.net/anki/wiki/Leeches">leech</a>.""")
% stripHTML(stripSounds(self.currentCard.question)).\
replace("\n", " ")[0:30])
if isLeech and self.deck.s.scalar(
"select 1 from cards where id = :id and priority < 1", 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.newCountToday))
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
##########################################################################
def switchToBlankScreen(self):
self.mainWin.mainStack.setCurrentIndex(0)
self.hideButtons()
def switchToWelcomeScreen(self):
self.mainWin.mainStack.setCurrentIndex(1)
self.hideButtons()
def switchToEditScreen(self):
self.mainWin.mainStack.setCurrentIndex(2)
def switchToStudyScreen(self):
self.mainWin.mainStack.setCurrentIndex(3)
def switchToCongratsScreen(self):
self.mainWin.mainStack.setCurrentIndex(4)
def switchToReviewScreen(self):
self.mainWin.mainStack.setCurrentIndex(6)
def switchToDecksScreen(self):
self.mainWin.mainStack.setCurrentIndex(5)
self.hideButtons()
# Buttons
##########################################################################
def setupButtons(self):
# ask
self.connect(self.mainWin.showAnswerButton, SIGNAL("clicked()"),
lambda: self.moveToState("showAnswer"))
if sys.platform.startswith("win32"):
if self.config['alternativeTheme']:
self.mainWin.showAnswerButton.setFixedWidth(370)
else:
self.mainWin.showAnswerButton.setFixedWidth(358)
else:
self.mainWin.showAnswerButton.setFixedWidth(351)
self.mainWin.showAnswerButton.setFixedHeight(41)
# answer
for i in range(1, 5):
b = getattr(self.mainWin, "easeButton%d" % i)
b.setFixedWidth(85)
self.connect(b, SIGNAL("clicked()"),
lambda i=i: self.cardAnswered(i))
# type answer
outer = QHBoxLayout()
outer.setSpacing(0)
outer.setContentsMargins(0,0,0,0)
outer.addStretch(0)
class QLineEditNoUndo(QLineEdit):
def __init__(self, parent):
self.parent = parent
QLineEdit.__init__(self, parent)
def keyPressEvent(self, evt):
if evt.matches(QKeySequence.Undo):
evt.accept()
if self.parent.mainWin.actionUndo.isEnabled():
self.parent.onUndo()
else:
return QLineEdit.keyPressEvent(self, evt)
self.typeAnswerField = QLineEditNoUndo(self)
self.typeAnswerField.setObjectName("typeAnswerField")
self.typeAnswerField.setFixedWidth(351)
f = QFont()
f.setPixelSize(self.config['typeAnswerFontSize'])
self.typeAnswerField.setFont(f)
# add some extra space as layout is wrong on osx
self.typeAnswerField.setFixedHeight(
self.typeAnswerField.sizeHint().height() + 10)
vbox = QVBoxLayout()
vbox.setSpacing(0)
vbox.setContentsMargins(0,0,0,0)
vbox.addWidget(self.typeAnswerField)
self.typeAnswerShowButton = QPushButton(_("Show Answer"))
hbox = QHBoxLayout()
hbox.setContentsMargins(0,0,0,0)
hbox.addWidget(self.typeAnswerShowButton)
vbox.addLayout(hbox)
self.connect(self.typeAnswerShowButton, SIGNAL("clicked()"),
lambda: self.moveToState("showAnswer"))
outer.addLayout(vbox)
outer.addStretch(0)
self.mainWin.typeAnswerPage.setLayout(outer)
def hideButtons(self):
self.mainWin.buttonStack.hide()
def showAnswerButton(self):
if self.currentCard.cardModel.typeAnswer:
self.mainWin.buttonStack.setCurrentIndex(2)
self.typeAnswerField.setFocus()
self.typeAnswerField.setText("")
else:
self.mainWin.buttonStack.setCurrentIndex(0)
self.mainWin.showAnswerButton.setFocus()
self.mainWin.buttonStack.show()
def showEaseButtons(self):
self.updateEaseButtons()
self.mainWin.buttonStack.setCurrentIndex(1)
self.mainWin.buttonStack.show()
self.mainWin.buttonStack.setLayoutDirection(Qt.LeftToRight)
if self.learningButtons():
self.mainWin.easeButton2.setText(_("Good"))
self.mainWin.easeButton3.setText(_("Easy"))
self.mainWin.easeButton4.setText(_("Very Easy"))
else:
self.mainWin.easeButton2.setText(_("Hard"))
self.mainWin.easeButton3.setText(_("Good"))
self.mainWin.easeButton4.setText(_("Easy"))
getattr(self.mainWin, "easeButton%d" % self.defaultEaseButton()).\
setFocus()
def learningButtons(self):
return not self.currentCard.successive
def defaultEaseButton(self):
if not self.currentCard.successive:
return 2
else:
return 3
def updateEaseButtons(self):
nextInts = {}
for i in range(1, 5):
l = getattr(self.mainWin, "easeLabel%d" % i)
if self.config['suppressEstimates']:
l.setText("")
elif i == 1:
txt = _("Soon")
if self.config['colourTimes']:
txt = '<span style="color: #700"><b>%s</b></span>' % txt
l.setText(txt)
else:
txt = self.deck.nextIntervalStr(
self.currentCard, i)
txt = "<b>" + txt + "</b>"
if i == self.defaultEaseButton() and self.config['colourTimes']:
txt = '<span style="color: #070">' + txt + '</span>'
l.setText(txt)
# Deck loading & saving: backend
##########################################################################
def setupBackupDir(self):
anki.deck.backupDir = os.path.join(
self.config.configPath, "backups")
def loadDeck(self, deckPath, sync=True, interactive=True, uprecent=True):
"Load a deck and update the user interface. Maybe sync."
self.reviewingStarted = False
# return True on success
try:
self.pauseViews()
if not self.saveAndClose(hideWelcome=True):
return 0
finally:
self.restoreViews()
if not os.path.exists(deckPath):
self.moveToState("noDeck")
return
try:
self.deck = DeckStorage.Deck(deckPath)
except Exception, e:
if hasattr(e, 'data') and e.data.get('type') == 'inuse':
if interactive:
ui.utils.showWarning(_("Deck is already open."))
else:
return
else:
ui.utils.showCritical(_("""\
File is corrupt or not an Anki database. Click help for more info.\n
Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
self.moveToState("noDeck")
return 0
if uprecent:
self.updateRecentFiles(self.deck.path)
if (sync and self.config['syncOnLoad']
and self.deck.syncName):
if self.syncDeck(interactive=False):
return True
self.setupMedia(self.deck)
try:
self.deck.initUndo()
self.moveToState("initial")
except:
traceback.print_exc()
if ui.utils.askUser(_(
"An error occurred while trying to build the queue.\n"
"Would you like to try check the deck for errors?\n"
"This may take some time.")):
self.onCheckDB()
# try again
try:
self.reset()
except:
ui.utils.showWarning(
_("Unable to recover. Deck load failed."))
self.deck = None
else:
self.deck = None
return 0
self.moveToState("noDeck")
runHook("loadDeck")
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, sync=False)
# try recent deck paths
for path in self.config['recentDeckPaths']:
r = self.loadDeck(path, interactive=False, sync=False)
if r:
return r
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:
defaultDir = unicode(os.path.expanduser("~/"),
sys.getfilesystemencoding())
return defaultDir
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)
self.config.save()
def onSwitchToDeck(self):
diag = QDialog(self)
diag.setWindowTitle(_("Open Recent Deck"))
vbox = QVBoxLayout()
combo = QComboBox()
self.switchDecks = (
[(os.path.basename(x).replace(".anki", ""), x)
for x in self.config['recentDeckPaths']
if not self.deck or self.deck.path != x and
os.path.exists(x)])
self.switchDecks.sort()
combo.addItems(QStringList([x[0] for x in self.switchDecks]))
self.connect(combo, SIGNAL("activated(int)"),
self.onSwitchActivated)
vbox.addWidget(combo)
bbox = QDialogButtonBox(QDialogButtonBox.Cancel)
self.connect(bbox, SIGNAL("rejected()"),
lambda: self.switchDeckDiag.close())
vbox.addWidget(bbox)
diag.setLayout(vbox)
diag.show()
self.app.processEvents()
combo.setFocus()
combo.showPopup()
self.switchDeckDiag = diag
diag.exec_()
def onSwitchActivated(self, idx):
self.switchDeckDiag.close()
self.loadDeck(self.switchDecks[idx][1])
# New files, loading & saving
##########################################################################
def onClose(self):
# allow focusOut to save
if self.inMainWindow() or not self.app.activeWindow():
self.saveAndClose()
else:
self.app.activeWindow().close()
def saveAndClose(self, hideWelcome=False, parent=None):
"(Auto)save and close. Prompt if necessary. True if okay to proceed."
# allow any focusOut()s to run first
self.setFocus()
if not parent:
parent = self
self.hideWelcome = hideWelcome
self.closeAllDeckWindows()
synced = False
if self.deck is not None:
if self.deck.finishScheduler:
self.deck.finishScheduler()
self.deck.reset()
# update counts
for d in self.browserDecks:
if d['path'] == self.deck.path:
d['due'] = self.deck.failedSoonCount + self.deck.revCount
d['new'] = self.deck.newCountToday
d['mod'] = self.deck.modified
d['time'] = self.deck._dailyStats.reviewTime
d['reps'] = self.deck._dailyStats.reps
if self.deck.modifiedSinceSave():
if (self.deck.path is None or
(not self.config['saveOnClose'] and
not self.config['syncOnLoad'])):
# backed in memory or autosave/sync off, must confirm
while 1:
res = ui.unsaved.ask(parent)
if res == ui.unsaved.save:
if self.save(required=True):
break
elif res == ui.unsaved.cancel:
self.hideWelcome = False
return False
else:
break
# auto sync (saving automatically)
if self.config['syncOnLoad'] and self.deck.syncName:
# force save, the user may not have set passwd/etc
self.deck.save()
if self.syncDeck(False, reload=False):
synced = True
while self.deckPath:
self.app.processEvents()
time.sleep(0.1)
self.hideWelcome = False
return True
# auto save
if self.config['saveOnClose'] or self.config['syncOnLoad']:
if self.deck:
self.save()
# close if the deck wasn't already closed by a failed sync
if self.deck:
self.deck.rollback()
self.deck.close()
self.deck = None
if not hideWelcome and not synced:
self.moveToState("noDeck")
self.hideWelcome = False
return True
def inMainWindow(self):
if not self.app.activeWindow():
# make sure window is shown
self.setWindowState(self.windowState() & ~Qt.WindowMinimized)
return True
# FIXME: no longer necessary?
return self.app.activeWindow() == self
def onNew(self, path=None, prompt=None):
if not self.inMainWindow() and not path: return
if not self.saveAndClose(hideWelcome=True): return
register = not path
bad = ":/\\"
name = _("mydeck")
if not path:
if not prompt:
prompt = _("Please give your deck a name:")
while 1:
name = ui.utils.getOnlyText(
prompt, default=name, title=_("New Deck"))
if not name:
return
found = False
for c in bad:
if c in name:
ui.utils.showInfo(
_("Sorry, '%s' can't be used in deck names.") % c)
found = True
break
if found:
continue
if not name.endswith(".anki"):
name += ".anki"
break
path = os.path.join(self.documentDir, name)
if os.path.exists(path):
if ui.utils.askUser(_("That deck already exists. Overwrite?"),
defaultno=True):
os.unlink(path)
else:
return
self.deck = DeckStorage.Deck(path)
self.deck.initUndo()
self.deck.addModel(BasicModel())
self.deck.save()
if register:
self.updateRecentFiles(self.deck.path)
self.browserLastRefreshed = 0
self.moveToState("initial")
def ensureSyncParams(self):
if not self.config['syncUsername'] or not self.config['syncPassword']:
d = QDialog(self)
vbox = QVBoxLayout()
l = QLabel(_(
'<h1>Online Account</h1>'
'To use your free <a href="http://anki.ichi2.net/">online account</a>,<br>'
"please enter your details below.<br><br>"
"You can change your details later with<br>"
"Settings->Preferences->Sync<br>"))
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())
def onOpenOnline(self):
if not self.inMainWindow(): return
self.ensureSyncParams()
if not self.saveAndClose(hideWelcome=True): return
# we need a disk-backed file for syncing
dir = unicode(tempfile.mkdtemp(prefix="anki"), sys.getfilesystemencoding())
path = os.path.join(dir, u"untitled.anki")
self.onNew(path=path)
# ensure all changes come to us
self.deck.modified = 0
self.deck.s.commit()
self.deck.syncName = u"something"
self.deck.lastLoaded = self.deck.modified
if self.config['syncUsername'] and self.config['syncPassword']:
if self.syncDeck(onlyMerge=True, reload=2, interactive=False):
return
self.deck = None
self.browserLastRefreshed = 0
self.moveToState("initial")
def onGetSharedDeck(self):
if not self.inMainWindow(): return
ui.getshared.GetShared(self, 0)
self.browserLastRefreshed = 0
def onGetSharedPlugin(self):
if not self.inMainWindow(): return
ui.getshared.GetShared(self, 1)
def onOpen(self):
if not self.inMainWindow(): return
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, interactive=True)
if not ret:
if ret is None:
ui.utils.showWarning(_("Unable to load file."))
self.deck = None
return False
else:
self.updateRecentFiles(file)
self.browserLastRefreshed = 0
return True
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("""\
<table cellpadding=10>
<tr>
<td><img src=":/icons/help-hint.png"></td>
<td>%s</td>
</tr>
</table>""" % 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(_("""\
<h1>Unsaved Deck</h1>
Careful. You're editing an unsaved deck.<br>
Choose File -> Save to start autosaving<br>
your deck."""))
return
self.deck.save()
self.updateTitleBar()
return True
def onSave(self):
self.save(required=True)
def onSaveAs(self):
"Prompt for a file name, then save."
title = _("Save Deck As")
if self.deck.path:
dir = os.path.dirname(self.deck.path)
else:
dir = self.documentDir
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 self.deck.path:
if os.path.abspath(file) == os.path.abspath(self.deck.path):
return self.onSave()
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.closeAllDeckWindows()
self.deck = self.deck.saveAs(file)
self.deck.initUndo()
self.updateTitleBar()
self.updateRecentFiles(self.deck.path)
self.browserLastRefreshed = 0
self.moveToState("initial")
return file
# Deck browser
##########################################################################
def setupDeckBrowser(self):
class PaddedScroll(QScrollArea):
def sizeHint(self):
hint = QScrollArea.sizeHint(self)
if sys.platform.startswith("darwin"):
m = 500
else:
m = 450
return QSize(max(hint.width(), m), hint.height())
self.decksScrollArea = PaddedScroll()
self.decksScrollArea.setFrameStyle(QFrame.NoFrame)
self.decksScrollArea.setWidgetResizable(True)
self.mainWin.verticalLayout_14.insertWidget(2, self.decksScrollArea)
self.decksFrame = QFrame()
self.connect(self.mainWin.downloadDeckButton,
SIGNAL("clicked()"),
self.onGetSharedDeck)
self.connect(self.mainWin.newDeckButton,
SIGNAL("clicked()"),
self.onNew)
self.connect(self.mainWin.importDeckButton,
SIGNAL("clicked()"),
self.onImport)
self.browserLastRefreshed = 0
self.browserDecks = []
def refreshBrowserDecks(self, forget=False):
self.browserDecks = []
if not self.config['recentDeckPaths']:
return
toRemove = []
self.startProgress(max=len(self.config['recentDeckPaths']),
immediate=True)
for c, d in enumerate(self.config['recentDeckPaths']):
if ui.splash.finished:
self.updateProgress(_("Checking deck %(x)d of %(y)d...") % {
'x': c+1, 'y': len(self.config['recentDeckPaths'])})
if not os.path.exists(d):
if forget:
toRemove.append(d)
continue
try:
mod = os.stat(d)[stat.ST_MTIME]
deck = DeckStorage.Deck(d, backup=False)
self.browserDecks.append({
'path': d,
'name': deck.name(),
'due': deck.failedSoonCount + deck.revCount,
'new': deck.newCountToday,
'mod': deck.modified,
'time': deck._dailyStats.reviewTime,
'reps': deck._dailyStats.reps,
})
deck.close()
try:
os.utime(d, (mod, mod))
except:
# some misbehaving filesystems may fail here
pass
except Exception, e:
if "File is in use" in unicode(e):
continue
else:
toRemove.append(d)
for d in toRemove:
self.config['recentDeckPaths'].remove(d)
self.config.save()
if ui.splash.finished:
self.finishProgress()
self.browserLastRefreshed = time.time()
self.reorderBrowserDecks()
def reorderBrowserDecks(self):
if self.config['deckBrowserOrder'] == 0:
self.browserDecks.sort(key=itemgetter('mod'),
reverse=True)
else:
def custcmp(a, b):
x = cmp(not not b['due'], not not a['due'])
if x:
return x
x = cmp(not not b['new'], not not a['new'])
if x:
return x
return cmp(a['mod'], b['mod'])
self.browserDecks.sort(cmp=custcmp)
def forceBrowserRefresh(self):
self.browserLastRefreshed = 0
self.showDeckBrowser()
def showDeckBrowser(self):
self.switchToBlankScreen()
import sip
focusButton = None
# remove all widgets from layout & layout itself
self.moreMenus = []
if self.decksFrame.layout():
while 1:
obj = self.decksFrame.layout().takeAt(0)
if not obj:
break
if obj.widget():
obj.widget().deleteLater()
self.app.processEvents(QEventLoop.DeferredDeletion)
sip.delete(self.decksFrame.layout())
# build new layout
layout = QGridLayout()
self.decksFrame.setLayout(layout)
if sys.platform.startswith("darwin"):
layout.setSpacing(6)
else:
layout.setSpacing(2)
if (time.time() - self.browserLastRefreshed >
self.config['deckBrowserRefreshPeriod']):
self.refreshBrowserDecks()
else:
self.reorderBrowserDecks()
if self.browserDecks:
layout.addWidget(QLabel(_("<b>Deck</b>")), 0, 0)
layout.setColumnStretch(0, 1)
l = QLabel(_("<b>Due<br>Today</b>"))
l.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout.addWidget(l, 0, 1)
layout.setColumnMinimumWidth(2, 10)
l = QLabel(_("<b>New<br>Today</b>"))
l.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout.addWidget(l, 0, 3)
layout.setColumnMinimumWidth(4, 10)
for c, deck in enumerate(self.browserDecks):
# name
n = deck['name']
lim = self.config['deckBrowserNameLength']
if len(n) > lim:
n = n[:lim] + "..."
mod = _("%s ago") % anki.utils.fmtTimeSpan(
time.time() - deck['mod'])
mod = "<font size=-1>%s</font>" % mod
l = QLabel("%d. <b>%s</b><br>&nbsp;&nbsp;&nbsp;&nbsp;%s" %
(c+1, n, mod))
l.setWordWrap(True)
layout.addWidget(l, c+1, 0)
# due
col = '<b><font color=#0000ff>%s</font></b>'
if deck['due'] > 0:
s = col % str(deck['due'])
else:
s = ""
l = QLabel(s)
l.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout.addWidget(l, c+1, 1)
# new
if deck['new']:
s = str(deck['new'])
else:
s = ""
l = QLabel(s)
l.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout.addWidget(l, c+1, 3)
# open
openButton = QPushButton(_("Open"))
if c < 9:
if sys.platform.startswith("darwin"):
extra = ""
# appears to be broken on osx
#extra = _(" (Command+Option+%d)") % (c+1)
#openButton.setShortcut(_("Ctrl+Alt+%d" % (c+1)))
else:
extra = _(" (Alt+%d)") % (c+1)
openButton.setShortcut(_("Alt+%d" % (c+1)))
else:
extra = ""
openButton.setToolTip(_("Open this deck%s") % extra)
self.connect(openButton, SIGNAL("clicked()"),
lambda d=deck['path']: self.loadDeck(d))
layout.addWidget(openButton, c+1, 5)
if c == 0:
focusButton = openButton
# more
moreButton = QPushButton(_("More"))
if sys.platform.startswith("win32") and \
self.config['alternativeTheme']:
moreButton.setFixedHeight(24)
moreMenu = QMenu()
a = moreMenu.addAction(QIcon(":/icons/edit-undo.png"),
_("Hide From List"))
a.connect(a, SIGNAL("triggered()"),
lambda c=c: self.onDeckBrowserForget(c))
a = moreMenu.addAction(QIcon(":/icons/editdelete.png"),
_("Delete"))
a.connect(a, SIGNAL("triggered()"),
lambda c=c: self.onDeckBrowserDelete(c))
moreButton.setMenu(moreMenu)
self.moreMenus.append(moreMenu)
layout.addWidget(moreButton, c+1, 6)
refresh = QPushButton(_("Refresh"))
refresh.setToolTip(_("Check due counts again (F5)"))
refresh.setShortcut(_("F5"))
self.connect(refresh, SIGNAL("clicked()"),
self.forceBrowserRefresh)
layout.addItem(QSpacerItem(1,20, QSizePolicy.Preferred,
QSizePolicy.Preferred), c+2, 5)
layout.addWidget(refresh, c+3, 5)
more = QPushButton(_("More"))
moreMenu = QMenu()
a = moreMenu.addAction(QIcon(":/icons/edit-undo.png"),
_("Forget Inaccessible Decks"))
a.connect(a, SIGNAL("triggered()"),
self.onDeckBrowserForgetInaccessible)
more.setMenu(moreMenu)
layout.addWidget(more, c+3, 6)
self.moreMenus.append(moreMenu)
# make sure top labels don't expand
layout.addItem(QSpacerItem(1,1, QSizePolicy.Expanding,
QSizePolicy.Expanding),
c+4, 5)
# summarize
reps = 0
mins = 0
revC = 0
newC = 0
for d in self.browserDecks:
reps += d['reps']
mins += d['time']
revC += d['due']
newC += d['new']
line1 = ngettext(
"Studied <b>%(reps)d card</b> in <b>%(time)s</b> today.",
"Studied <b>%(reps)d cards</b> in <b>%(time)s</b> today.",
reps) % {
'reps': reps,
'time': anki.utils.fmtTimeSpan(mins, point=2),
}
rev = ngettext(
"<b><font color=#0000ff>%d</font></b> review",
"<b><font color=#0000ff>%d</font></b> reviews",
revC) % revC
new = ngettext("<b>%d</b> new card", "<b>%d</b> new cards", newC) % newC
line2 = _("Due: %(rev)s, %(new)s") % {
'rev': rev, 'new': new}
self.mainWin.deckBrowserSummary.setText(line1 + "<br>" + line2)
else:
l = QLabel(_("""\
<br>
<font size=+1>
Welcome to Anki! Click <b>'Download'</b> to get started. You can return here
later by using File>Close.
</font>
<br>
"""))
l.setWordWrap(True)
layout.addWidget(l, 0, 0)
self.decksScrollArea.setWidget(self.decksFrame)
if focusButton:
focusButton.setFocus()
# do this last
self.switchToDecksScreen()
def onDeckBrowserForget(self, c):
if ui.utils.askUser(_("""\
Hide %s from the list? You can File>Open it again later.""") %
self.browserDecks[c]['name']):
self.config['recentDeckPaths'].remove(self.browserDecks[c]['path'])
del self.browserDecks[c]
self.doLater(100, self.showDeckBrowser)
def onDeckBrowserDelete(self, c):
deck = self.browserDecks[c]['path']
if ui.utils.askUser(_("""\
Delete %s? If this deck is synchronized the online version will \
not be touched.""") %
self.browserDecks[c]['name']):
del self.browserDecks[c]
os.unlink(deck)
try:
shutil.rmtree(re.sub(".anki$", ".media", deck))
except OSError:
pass
self.config['recentDeckPaths'].remove(deck)
self.doLater(100, self.showDeckBrowser)
def onDeckBrowserForgetInaccessible(self):
self.refreshBrowserDecks(forget=True)
def doLater(self, msecs, func):
timer = QTimer(self)
timer.setSingleShot(True)
timer.start(msecs)
self.connect(timer, SIGNAL("timeout()"), func)
# Opening and closing the app
##########################################################################
def prepareForExit(self):
"Save config and window geometry."
runHook("quit")
self.help.hide()
self.config['mainWindowGeom'] = self.saveGeometry()
self.config['mainWindowState'] = self.saveState()
# 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 self.state == "editCurrentFact":
event.ignore()
return self.moveToState("saveEdit")
if not self.saveAndClose(hideWelcome=True):
event.ignore()
else:
if self.config['syncOnProgramOpen']:
self.hideWelcome = True
self.syncDeck(interactive=False)
self.prepareForExit()
event.accept()
self.app.quit()
# Anchor clicks
##########################################################################
def onWelcomeAnchor(self, str):
if str == "back":
self.saveAndClose()
if str == "addfacts":
if self.deck:
self.onAddCard()
def setupAnchors(self):
# welcome
self.anchorPrefixes = {
'welcome': self.onWelcomeAnchor,
}
self.connect(self.mainWin.welcomeText,
SIGNAL("anchorClicked(QUrl)"),
self.anchorClicked)
# main
self.mainWin.mainText.page().setLinkDelegationPolicy(
QWebPage.DelegateAllLinks)
self.connect(self.mainWin.mainText,
SIGNAL("linkClicked(QUrl)"),
self.linkClicked)
def anchorClicked(self, url):
# prevent the link being handled
self.mainWin.welcomeText.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))
def linkClicked(self, url):
QDesktopServices.openUrl(QUrl(url))
# Edit current fact
##########################################################################
def setupEditor(self):
self.editor = ui.facteditor.FactEditor(
self, self.mainWin.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.mainWin.saveEditorButton, SIGNAL("clicked()"),
lambda: self.moveToState("saveEdit"))
def showEditor(self):
self.mainWin.buttonStack.hide()
self.switchToEditScreen()
self.editor.setFact(self.currentCard.fact)
self.editor.card = self.currentCard
def onFactValid(self, fact):
self.mainWin.saveEditorButton.setEnabled(True)
def onFactInvalid(self, fact):
self.mainWin.saveEditorButton.setEnabled(False)
# Study screen
##########################################################################
def setupStudyScreen(self):
self.mainWin.buttonStack.hide()
self.mainWin.newCardOrder.insertItems(
0, QStringList(newCardOrderLabels().values()))
self.mainWin.newCardScheduling.insertItems(
0, QStringList(newCardSchedulingLabels().values()))
self.mainWin.revCardOrder.insertItems(
0, QStringList(revCardOrderLabels().values()))
self.connect(self.mainWin.optionsHelpButton,
SIGNAL("clicked()"),
lambda: QDesktopServices.openUrl(QUrl(
ankiqt.appWiki + "StudyOptions")))
self.connect(self.mainWin.minuteLimit,
SIGNAL("textChanged(QString)"), self.onMinuteLimitChanged)
self.connect(self.mainWin.questionLimit,
SIGNAL("textChanged(QString)"), self.onQuestionLimitChanged)
self.connect(self.mainWin.newPerDay,
SIGNAL("textChanged(QString)"), self.onNewLimitChanged)
self.connect(self.mainWin.startReviewingButton,
SIGNAL("clicked()"),
self.onStartReview)
self.connect(self.mainWin.newCardOrder,
SIGNAL("activated(int)"), self.onNewCardOrderChanged)
self.connect(self.mainWin.failedCardMax,
SIGNAL("editingFinished()"),
self.onFailedMaxChanged)
self.connect(self.mainWin.newCategories,
SIGNAL("clicked()"), self.onNewCategoriesClicked)
self.connect(self.mainWin.revCategories,
SIGNAL("clicked()"), self.onRevCategoriesClicked)
self.mainWin.tabWidget.setCurrentIndex(self.config['studyOptionsScreen'])
def onNewCategoriesClicked(self):
ui.activetags.show(self, "new")
def onRevCategoriesClicked(self):
ui.activetags.show(self, "rev")
def onFailedMaxChanged(self):
try:
v = int(self.mainWin.failedCardMax.text())
if v == 1 or v < 0:
v = 2
self.deck.failedCardMax = v
except ValueError:
pass
self.mainWin.failedCardMax.setText(str(self.deck.failedCardMax))
self.deck.flushMod()
def onMinuteLimitChanged(self, qstr):
try:
val = float(self.mainWin.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.mainWin.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.mainWin.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.deck.startProgress()
self.deck.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.mainWin.newCategoryLabel.setText(new)
if self.deck.getVar("revActive") or self.deck.getVar("revInactive"):
rev = labels[1]
else:
rev = labels[0]
self.mainWin.revCategoryLabel.setText(rev)
def updateStudyStats(self):
self.mainWin.buttonStack.hide()
self.deck.reset()
self.updateActives()
wasReached = self.deck.sessionLimitReached()
sessionColour = '<font color=#0000ff>%s</font>'
cardColour = '<font color=#0000ff>%s</font>'
# top label
h = {}
s = self.deck.getStats()
h['ret'] = cardColour % (s['rev']+s['failed'])
h['new'] = cardColour % s['new']
h['newof'] = str(self.deck.newCountAll())
dtoday = s['dTotal']
yesterday = self.deck._dailyStats.day - datetime.timedelta(1)
res = self.deck.s.first("""
select reps, reviewTime from stats where type = 1 and
day = :d""", d=yesterday)
if res:
(dyest, tyest) = res
else:
dyest = 0; tyest = 0
h['repsToday'] = sessionColour % dtoday
h['repsTodayChg'] = str(dyest)
limit = self.deck.sessionTimeLimit
start = self.deck.sessionStartTime or time.time() - limit
start2 = self.deck.lastSessionStart or start - limit
last10 = self.deck.s.scalar(
"select count(*) from reviewHistory where time >= :t",
t=start)
last20 = self.deck.s.scalar(
"select count(*) from reviewHistory where "
"time >= :t and time < :t2",
t=start2, t2=start)
h['repsInSes'] = sessionColour % last10
h['repsInSesChg'] = str(last20)
ttoday = s['dReviewTime']
h['timeToday'] = sessionColour % (
anki.utils.fmtTimeSpan(ttoday, short=True, point=1))
h['timeTodayChg'] = str(anki.utils.fmtTimeSpan(
tyest, short=True, point=1))
h['cs_header'] = "<b>" + _("Cards/session:") + "</b>"
h['cd_header'] = "<b>" + _("Cards/day:") + "</b>"
h['td_header'] = "<b>" + _("Time/day:") + "</b>"
h['rd_header'] = "<b>" + _("Reviews due:") + "</b>"
h['ntod_header'] = "<b>" + _("New today:") + "</b>"
h['ntot_header'] = "<b>" + _("New total:") + "</b>"
stats1 = ("""\
<table>
<tr><td width=150>%(cs_header)s</td><td width=50><b>%(repsInSesChg)s</b></td>
<td><b>%(repsInSes)s</b></td></tr></table>
<hr>
<table>
<tr><td width=150>
%(cd_header)s</td><td width=50><b>%(repsTodayChg)s</b></td>
<td><b>%(repsToday)s</b></td></tr>
<tr><td>%(td_header)s</td><td><b>%(timeTodayChg)s</b></td>
<td><b>%(timeToday)s</b></td></tr>
</table>""") % h
stats2 = ("""\
<table>
<tr><td width=180>%(rd_header)s</td><td align=right><b>%(ret)s</b></td></tr>
<tr><td>%(ntod_header)s</td><td align=right><b>%(new)s</b></td></tr>
<tr><td>%(ntot_header)s</td><td align=right>%(newof)s</td></tr>
</table>""") % h
if (not dyest and not dtoday) or not self.config['showStudyStats']:
self.haveYesterday = False
stats1 = ""
else:
self.haveYesterday = True
stats1 = (
"<td>%s</td><td>&nbsp;&nbsp;&nbsp;&nbsp;</td>" % stats1)
self.mainWin.optionsLabel.setText("""\
<p><table><tr>
%s
</tr><tr>
<td><hr>%s<hr></td></tr></table>""" % (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 = ("""<h1>%(tt_header)s</h1>
<dl><dt><b>%(cs_header)s</b></dt><dd>%(cs_tip)s</dd></dl>
<dl><dt><b>%(cd_header)s</b></dt><dd>%(cd_tip)s</dd></dl>
<dl><dt><b>%(td_header)s</b></dt><dd>%(td_tip)s</dd></dl>
<dl><dt><b>%(rd_header)s</b></dt><dd>%(rd_tip)s</dd></dl>
<dl><dt><b>%(ntod_header)s</b></dt><dd>%(ntod_tip)s</dd></dl>
<dl><dt><b>%(ntot_header)s</b></dt><dd>%(ntot_tip)s<</dd></dl>""") % h
self.mainWin.optionsLabel.setToolTip(statToolTip)
def showStudyScreen(self):
# forget last card
self.lastCard = None
self.switchToStudyScreen()
self.updateStudyStats()
self.mainWin.startReviewingButton.setFocus()
self.setupStudyOptions()
self.mainWin.studyOptionsFrame.setMaximumWidth(500)
self.mainWin.studyOptionsFrame.show()
def setupStudyOptions(self):
self.mainWin.newPerDay.setText(str(self.deck.newCardsPerDay))
lim = self.deck.sessionTimeLimit/60
if int(lim) == lim:
lim = int(lim)
self.mainWin.minuteLimit.setText(str(lim))
self.mainWin.questionLimit.setText(str(self.deck.sessionRepLimit))
self.mainWin.newCardOrder.setCurrentIndex(self.deck.newCardOrder)
self.mainWin.newCardScheduling.setCurrentIndex(self.deck.newCardSpacing)
self.mainWin.revCardOrder.setCurrentIndex(self.deck.revCardOrder)
self.mainWin.failedCardsOption.clear()
if self.deck.getFailedCardPolicy() == 5:
labels = failedCardOptionLabels().values()
else:
labels = failedCardOptionLabels().values()[0:-1]
self.mainWin.failedCardsOption.insertItems(0, labels)
self.mainWin.failedCardsOption.setCurrentIndex(self.deck.getFailedCardPolicy())
self.mainWin.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.mainWin.studyOptionsFrame.hide()
# make sure the size is updated before button stack shown
self.app.processEvents()
uf(self.deck, 'newCardSpacing',
self.mainWin.newCardScheduling.currentIndex())
uf(self.deck, 'revCardOrder',
self.mainWin.revCardOrder.currentIndex())
pol = self.deck.getFailedCardPolicy()
if (pol != 5 and pol !=
self.mainWin.failedCardsOption.currentIndex()):
self.deck.setFailedCardPolicy(
self.mainWin.failedCardsOption.currentIndex())
self.deck.flushMod()
self.deck.reset()
if not self.deck.finishScheduler:
self.deck.startSession()
self.config['studyOptionsScreen'] = self.mainWin.tabWidget.currentIndex()
self.moveToState("getQuestion")
def onStudyOptions(self):
if self.state == "studyScreen":
pass
else:
self.moveToState("studyScreen")
# Toolbar
##########################################################################
def setupToolbar(self):
mw = self.mainWin
mw.toolBar.addAction(mw.actionAddcards)
mw.toolBar.addAction(mw.actionEditCurrent)
mw.toolBar.addAction(mw.actionEditLayout)
mw.toolBar.addAction(mw.actionEditdeck)
mw.toolBar.addAction(mw.actionStudyOptions)
mw.toolBar.addAction(mw.actionGraphs)
mw.toolBar.addAction(mw.actionMarkCard)
mw.toolBar.addAction(mw.actionRepeatAudio)
mw.toolBar.addAction(mw.actionClose)
mw.toolBar.setIconSize(QSize(self.config['iconSize'],
self.config['iconSize']))
toggle = mw.toolBar.toggleViewAction()
toggle.setText(_("Toggle Toolbar"))
self.connect(toggle, SIGNAL("triggered()"),
self.onToolbarToggle)
if not self.config['showToolbar']:
mw.toolBar.hide()
def onToolbarToggle(self):
tb = self.mainWin.toolBar
self.config['showToolbar'] = tb.isVisible()
# Tools - statistics
##########################################################################
def onDeckStats(self):
txt = anki.stats.DeckStats(self.deck).report()
self.help.showText(txt)
def onCardStats(self):
addHook("showQuestion", self.onCardStats)
addHook("deckFinished", self.onCardStats)
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, py={'hide': self.removeCardStatsHook})
def removeCardStatsHook(self):
"Remove the update hook if the help menu was changed."
removeHook("showQuestion", self.onCardStats)
removeHook("deckFinished", 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):
traceback.print_exc()
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://ichi2.net/anki/wiki/MatplotlibBroken"))
else:
ui.utils.showInfo(_("Please install python-matplotlib to access graphs."))
# Marking, suspending and undoing
##########################################################################
def onMark(self, toggled):
if self.currentCard.hasTag("Marked"):
self.currentCard.fact.tags = canonifyTags(deleteTags(
"Marked", self.currentCard.fact.tags))
else:
self.currentCard.fact.tags = canonifyTags(addTags(
"Marked", self.currentCard.fact.tags))
self.currentCard.fact.setModified(textChanged=True, deck=self.deck)
self.deck.updateFactTags([self.currentCard.fact.id])
self.deck.setModified()
def onSuspend(self):
undo = _("Suspend")
self.deck.setUndoStart(undo)
self.deck.suspendCards([self.currentCard.id])
self.reset()
self.deck.setUndoEnd(undo)
def onDelete(self):
undo = _("Delete")
if self.state == "editCurrent":
self.moveToState("saveEdit")
self.deck.setUndoStart(undo)
self.deck.deleteCard(self.currentCard.id)
self.reset()
self.deck.setUndoEnd(undo)
runHook("currentCardDeleted")
def onBuryFact(self):
undo = _("Bury")
self.deck.setUndoStart(undo)
self.deck.buryFact(self.currentCard.fact)
self.reset()
self.deck.setUndoEnd(undo)
def onUndo(self):
name = self.deck.undoName()
self.deck.undo()
self.reset()
if name == "Answer Card":
self.setStatus(_("Card placed back in queue."))
def onRedo(self):
self.deck.redo()
self.reset()
# Other menu operations
##########################################################################
def onAddCard(self):
ui.dialogs.get("AddCards", self)
def onEditDeck(self):
ui.dialogs.get("CardList", self)
def onEditCurrent(self):
self.moveToState("editCurrentFact")
def onCardLayout(self):
ui.clayout.CardLayout(self, 0, self.currentCard.fact,
card=self.currentCard)
def onDeckProperties(self):
self.deckProperties = ui.deckproperties.DeckProperties(self, self.deck)
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 onReleaseNotes(self):
QDesktopServices.openUrl(QUrl(ankiqt.appReleaseNotes))
def onAbout(self):
ui.about.show(self)
def onDonate(self):
QDesktopServices.openUrl(QUrl(ankiqt.appDonate))
# Importing & exporting
##########################################################################
def onImport(self):
if self.deck is None:
self.onNew(prompt=_("""\
Importing copies cards to the current deck,
so we need to create a new deck first.
Please give your deck a name:"""))
if not self.deck:
return
if self.deck.path:
ui.importing.ImportDialog(self)
def onExport(self):
ui.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 = ui.tagedit.TagEdit(self)
te.setDeck(self.deck, "all")
diag = ui.utils.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:
ui.utils.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:
ui.utils.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('''\
<html><body>
This is an exported packaged deck created by Anki.<p>
To share this deck with other people, upload it to
<a href="http://anki.ichi2.net/file/upload">
http://anki.ichi2.net/file/upload</a>, or email
it to your friends.
</body></html>''')
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(_("""\
<h1>Learning More</h1>Click the stopwatch at the top to finish."""))
def onReviewEarly(self):
self.deck.setupReviewEarlyScheduler()
self.reset()
self.showToolTip(_("""\
<h1>Reviewing Early</h1>Click the stopwatch at the top to finish."""))
# Language handling
##########################################################################
def setLang(self):
"Set the user interface language."
try:
locale.setlocale(locale.LC_ALL, '')
except:
pass
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)
anki.lang.setLang(self.config["interfaceLang"], local=False)
self.updateTitleBar()
if self.config['interfaceLang'] in ("he","ar","fa") and \
not self.config['forceLTR']:
self.app.setLayoutDirection(Qt.RightToLeft)
else:
self.app.setLayoutDirection(Qt.LeftToRight)
def getTranslation(self, text):
return self.languageTrans.ugettext(text)
def getTranslation2(self, text1, text2, n):
return self.languageTrans.ungettext(text1, text2, n)
def installTranslation(self):
import __builtin__
__builtin__.__dict__['_'] = self.getTranslation
__builtin__.__dict__['ngettext'] = self.getTranslation2
# Syncing
##########################################################################
def setupSync(self):
if not self.config['syncDisableWhenMoved']:
anki.deck.Deck.checkSyncHash = lambda self: True
def syncDeck(self, interactive=True, onlyMerge=False, reload=True):
"Synchronise a deck with the server."
if not self.inMainWindow() and interactive and interactive!=-1: return
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.s.scalar("select 1 from media limit 1")):
ui.utils.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()
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.mainWin.welcomeText.setText(u"")
self.syncThread = ui.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 self.syncThread.ok
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 = ui.utils.askUserDialog(_("""\
<b>%s</b> 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 = ui.utils.askUserDialog(_("""\
You are about to upload <b>%s</b>
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.mainWin.buttonStack.show()
try:
try:
if self.hideWelcome:
# 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("noDeck")
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, sync=False)
else:
self.moveToState("noDeck")
except:
self.moveToState("noDeck")
raise
finally:
self.deckPath = None
self.syncFinished = True
def selectSyncDeck(self, decks):
name = ui.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 = ui.utils.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("noDeck")
self.syncFinished = True
def setSyncStatus(self, text, *args):
self.mainWin.welcomeText.append("<font size=+2>" + text + "</font>")
def syncClockOff(self, diff):
ui.utils.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):
ui.utils.showWarning(text, self)
self.setStatus("")
def badUserPass(self):
ui.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):
ui.utils.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
##########################################################################
deckRelatedMenuItems = (
"Save",
"SaveAs",
"Close",
"Addcards",
"Editdeck",
"DeckProperties",
"Undo",
"Redo",
"Export",
"Graphs",
"Dstats",
"Cstats",
"Cram",
"StudyOptions",
)
deckRelatedMenus = (
"Edit",
"Tools",
)
def connectMenuActions(self):
m = self.mainWin
s = SIGNAL("triggered()")
self.connect(m.actionNew, s, self.onNew)
self.connect(m.actionOpenOnline, s, self.onOpenOnline)
self.connect(m.actionDownloadSharedDeck, s, self.onGetSharedDeck)
self.connect(m.actionDownloadSharedPlugin, s, self.onGetSharedPlugin)
self.connect(m.actionOpenRecent, s, self.onSwitchToDeck)
self.connect(m.actionOpen, s, self.onOpen)
self.connect(m.actionSave, s, self.onSave)
self.connect(m.actionSaveAs, s, self.onSaveAs)
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.actionDeckProperties, s, self.onDeckProperties)
self.connect(m.actionAddcards, s, self.onAddCard)
self.connect(m.actionEditdeck, s, self.onEditDeck)
self.connect(m.actionEditCurrent, s, self.onEditCurrent)
self.connect(m.actionPreferences, s, self.onPrefs)
self.connect(m.actionDstats, s, self.onDeckStats)
self.connect(m.actionCstats, s, self.onCardStats)
self.connect(m.actionGraphs, s, self.onShowGraph)
self.connect(m.actionEditLayout, s, self.onCardLayout)
self.connect(m.actionAbout, s, self.onAbout)
self.connect(m.actionReportbug, s, self.onReportBug)
self.connect(m.actionForum, s, self.onForum)
self.connect(m.actionStarthere, s, self.onStartHere)
self.connect(m.actionImport, s, self.onImport)
self.connect(m.actionExport, s, self.onExport)
self.connect(m.actionMarkCard, SIGNAL("toggled(bool)"), self.onMark)
self.connect(m.actionSuspendCard, s, self.onSuspend)
self.connect(m.actionDelete, s, self.onDelete)
self.connect(m.actionRepeatAudio, s, self.onRepeatAudio)
self.connect(m.actionUndo, s, self.onUndo)
self.connect(m.actionRedo, s, self.onRedo)
self.connect(m.actionFullDatabaseCheck, s, self.onCheckDB)
self.connect(m.actionOptimizeDatabase, s, self.onOptimizeDB)
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.actionOpenPluginFolder, s, self.onOpenPluginFolder)
self.connect(m.actionEnableAllPlugins, s, self.onEnableAllPlugins)
self.connect(m.actionDisableAllPlugins, s, self.onDisableAllPlugins)
self.connect(m.actionReleaseNotes, s, self.onReleaseNotes)
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):
"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)
if not enabled:
self.disableCardMenuItems()
runHook("enableDeckMenuItems", 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
if self.deck != None:
deckpath = self.deck.name()
if self.deck.modifiedSinceSave():
deckpath += "*"
if not self.config['showProgress']:
title = deckpath + " - " + title
else:
title = _("%(path)s (%(due)d of %(cards)d due)"
" - %(title)s") % {
"path": deckpath,
"title": title,
"cards": self.deck.cardCount,
"due": self.deck.failedSoonCount + self.deck.revCount
}
self.setWindowTitle(title)
def setStatus(self, text, timeout=3000):
self.mainWin.statusbar.showMessage(text, timeout)
def onStartHere(self):
QDesktopServices.openUrl(QUrl(ankiqt.appHelpSite))
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.maybeEnableUndo()
self.mainWin.actionEditCurrent.setEnabled(False)
self.mainWin.actionEditLayout.setEnabled(False)
self.mainWin.actionMarkCard.setEnabled(False)
self.mainWin.actionSuspendCard.setEnabled(False)
self.mainWin.actionDelete.setEnabled(False)
self.mainWin.actionBuryFact.setEnabled(False)
self.mainWin.actionRepeatAudio.setEnabled(False)
runHook("disableCardMenuItems")
def enableCardMenuItems(self):
self.maybeEnableUndo()
snd = (hasSound(self.currentCard.question) or
(hasSound(self.currentCard.answer) and
self.state != "getQuestion"))
self.mainWin.actionEditLayout.setEnabled(True)
self.mainWin.actionRepeatAudio.setEnabled(snd)
self.mainWin.actionMarkCard.setEnabled(True)
self.mainWin.actionSuspendCard.setEnabled(True)
self.mainWin.actionDelete.setEnabled(True)
self.mainWin.actionBuryFact.setEnabled(True)
enableEdits = (not self.config['preventEditUntilAnswer'] or
self.state != "getQuestion")
self.mainWin.actionEditCurrent.setEnabled(enableEdits)
self.mainWin.actionEditdeck.setEnabled(enableEdits)
runHook("enableCardMenuItems")
def maybeEnableUndo(self):
if self.deck and self.deck.undoAvailable():
self.mainWin.actionUndo.setText(_("Undo %s") %
self.deck.undoName())
self.mainWin.actionUndo.setEnabled(True)
else:
self.mainWin.actionUndo.setEnabled(False)
if self.deck and self.deck.redoAvailable():
self.mainWin.actionRedo.setText(_("Redo %s") %
self.deck.redoName())
self.mainWin.actionRedo.setEnabled(True)
else:
self.mainWin.actionRedo.setEnabled(False)
# Auto update
##########################################################################
def setupAutoUpdate(self):
self.autoUpdate = ui.update.LatestVersionFinder(self)
self.connect(self.autoUpdate, SIGNAL("newVerAvail"), self.newVerAvail)
self.connect(self.autoUpdate, SIGNAL("newMsg"), self.newMsg)
self.connect(self.autoUpdate, SIGNAL("clockIsOff"), self.clockIsOff)
self.autoUpdate.start()
def newVerAvail(self, data):
if self.config['suppressUpdate'] < data['latestVersion']:
ui.update.askAndUpdate(self, data)
def newMsg(self, data):
ui.update.showMessages(self, data)
def clockIsOff(self, diff):
if diff < 0:
ret = _("late")
else:
ret = _("early")
ui.utils.showWarning(
_("The time or date on your computer is not correct.\n") +
ngettext("It is %(sec)d second %(type)s.\n",
"It is %(sec)d seconds %(type)s.\n", abs(diff))
% {"sec": abs(diff), "type": ret} +
_(" Please ensure it is set correctly and then restart Anki.")
)
def updateStarted(self):
self.updateProgressDialog = QProgressDialog(_(
"Updating Anki...\n - you can keep studying"
"\n - please don't close this"), "", 0, 0, self)
self.updateProgressDialog.setMinimum(0)
self.updateProgressDialog.setMaximum(100)
self.updateProgressDialog.setCancelButton(None)
self.updateProgressDialog.setMinimumDuration(0)
def updateDownloading(self, perc):
self.updateProgressDialog.setValue(perc)
def updateFinished(self):
self.updateProgressDialog.cancel()
# Plugins
##########################################################################
def pluginsFolder(self):
dir = self.config.configPath
if sys.platform.startswith("win32"):
dir = dir.encode(sys.getfilesystemencoding())
return os.path.join(dir, "plugins")
def loadPlugins(self):
if sys.platform.startswith("win32"):
self.clearPluginCache()
self.disableObsoletePlugins()
plugdir = self.pluginsFolder()
sys.path.insert(0, plugdir)
plugins = self.enabledPlugins()
plugins.sort()
self.registeredPlugins = {}
for plugin in plugins:
try:
nopy = plugin.replace(".py", "")
__import__(nopy)
except:
print "Error in %s" % plugin
traceback.print_exc()
self.checkForUpdatedPlugins()
self.disableCardMenuItems()
def clearPluginCache(self):
"Clear .pyc files which may cause crashes if Python version updated."
dir = self.pluginsFolder()
for curdir, dirs, files in os.walk(dir):
for f in files:
if not f.endswith(".pyc"):
continue
os.unlink(os.path.join(curdir, f))
def disableObsoletePlugins(self):
dir = self.pluginsFolder()
native = _(
"The %s plugin has been disabled, as Anki supports "+
"this natively now.")
plugins = [
("Custom Media Directory.py",
(native % "custom media folder") + _(""" \
Please visit Settings>Preferences.""")),
("Regenerate Reading Field.py", _("""\
The regenerate reading field plugin has been disabled, as the Japanese \
support plugin supports this now. Please download the latest version.""")),
("Sync LaTeX with iPhone client.py",
native % "sync LaTeX"),
("Incremental Reading.py",
_("""The incremental reading plugin has been disabled because \
it needs updates.""")),
("Learn Mode.py", _("""\
The learn mode plugin has been disabled because it needs to be rewritten \
to work with this version of Anki."""))
]
for p in plugins:
path = os.path.join(dir, p[0])
if os.path.exists(path):
new = path.replace(".py", ".disabled")
if os.path.exists(new):
os.unlink(new)
os.rename(path, new)
ui.utils.showInfo(p[1])
def rebuildPluginsMenu(self):
if getattr(self, "pluginActions", None) is None:
self.pluginActions = []
for action in self.pluginActions:
self.mainWin.menuStartup.removeAction(action)
all = self.allPlugins()
all.sort()
for fname in all:
enabled = fname.endswith(".py")
p = re.sub("\.py(\.off)?", "", fname)
if p+".py" in self.registeredPlugins:
p = self.registeredPlugins[p+".py"]['name']
a = QAction(p, self)
a.setCheckable(True)
a.setChecked(enabled)
self.connect(a, SIGNAL("triggered()"),
lambda fname=fname: self.togglePlugin(fname))
self.mainWin.menuStartup.addAction(a)
self.pluginActions.append(a)
def enabledPlugins(self):
return [p for p in os.listdir(self.pluginsFolder())
if p.endswith(".py")]
def disabledPlugins(self):
return [p for p in os.listdir(self.pluginsFolder())
if p.endswith(".py.off")]
def allPlugins(self):
return [p for p in os.listdir(self.pluginsFolder())
if p.endswith(".py.off") or p.endswith(".py")]
def onOpenPluginFolder(self, path=None):
if path is None:
path = self.pluginsFolder()
if sys.platform == "win32":
if isinstance(path, unicode):
path = path.encode(sys.getfilesystemencoding())
anki.utils.call(["explorer", path], wait=False)
else:
QDesktopServices.openUrl(QUrl("file://" + path))
def onGetPlugins(self):
QDesktopServices.openUrl(QUrl("http://ichi2.net/anki/wiki/Plugins"))
def enablePlugin(self, p):
pd = self.pluginsFolder()
old = os.path.join(pd, p)
new = os.path.join(pd, p.replace(".off", ""))
try:
os.unlink(new)
except:
pass
os.rename(old, new)
def disablePlugin(self, p):
pd = self.pluginsFolder()
old = os.path.join(pd, p)
new = os.path.join(pd, p.replace(".py", ".py.off"))
try:
os.unlink(new)
except:
pass
os.rename(old, new)
def onEnableAllPlugins(self):
for p in self.disabledPlugins():
self.enablePlugin(p)
self.rebuildPluginsMenu()
def onDisableAllPlugins(self):
for p in self.enabledPlugins():
self.disablePlugin(p)
self.rebuildPluginsMenu()
def togglePlugin(self, plugin):
if plugin.endswith(".py"):
self.disablePlugin(plugin)
else:
self.enablePlugin(plugin)
self.rebuildPluginsMenu()
def registerPlugin(self, name, updateId):
src = os.path.basename(inspect.getfile(inspect.currentframe(1)))
self.registeredPlugins[src] = {'name': name,
'id': updateId}
def checkForUpdatedPlugins(self):
pass
# Font localisation
##########################################################################
def setupFonts(self):
for (s, p) in anki.fonts.substitutions():
QFont.insertSubstitution(s, p)
# Custom styles
##########################################################################
def setupStyle(self):
ui.utils.applyStyles(self)
# Sounds
##########################################################################
def setupSound(self):
anki.sound.noiseProfile = os.path.join(
self.config.configPath, "noise.profile").\
encode(sys.getfilesystemencoding())
anki.sound.checkForNoiseProfile()
if sys.platform.startswith("darwin"):
self.mainWin.actionRecordNoiseProfile.setEnabled(False)
def onRepeatAudio(self):
clearAudioQueue()
if (not self.currentCard.cardModel.questionInAnswer
or self.state == "showQuestion") and \
self.config['repeatQuestionAudio']:
playFromText(self.currentCard.question)
if self.state != "showQuestion":
playFromText(self.currentCard.answer)
def onRecordNoiseProfile(self):
from ankiqt.ui.sound import recordNoiseProfile
recordNoiseProfile(self)
# Progress info
##########################################################################
def setupProgressInfo(self):
addHook("startProgress", self.startProgress)
addHook("updateProgress", self.updateProgress)
addHook("finishProgress", self.finishProgress)
addHook("dbProgress", self.onDbProgress)
addHook("dbFinished", self.onDbFinished)
self.progressParent = None
self.progressWins = []
self.busyCursor = False
self.updatingBusy = False
self.mainThread = QThread.currentThread()
self.oldSessionHelperGetter = SessionHelper.__getattr__
SessionHelper.__getattr__ = wrap(SessionHelper.__getattr__,
self.checkProgressHandler,
pos="before")
def checkProgressHandler(self, ses, k):
"Catch attempts to access the DB from a progress handler."
if self.inDbHandler:
raise Exception("Accessed DB while in progress handler")
def setProgressParent(self, parent):
self.progressParent = parent
def startProgress(self, max=0, min=0, title=None, immediate=False):
if self.mainThread != QThread.currentThread():
return
self.setBusy()
if not self.progressWins:
parent = self.progressParent or self.app.activeWindow() or self
p = ui.utils.ProgressWin(parent, max, min, title, immediate)
else:
p = None
self.progressWins.append(p)
def updateProgress(self, label=None, value=None, process=True):
if self.mainThread != QThread.currentThread():
return
if len(self.progressWins) == 1:
self.progressWins[0].update(label, value, process)
else:
# just redraw
if process:
self.app.processEvents()
def finishProgress(self):
if self.mainThread != QThread.currentThread():
return
if self.progressWins:
p = self.progressWins.pop()
if p:
p.finish()
if not self.progressWins:
self.unsetBusy()
def clearProgress(self):
# recover on error
self.progressWins = []
self.finishProgress()
def onDbProgress(self):
if self.mainThread != QThread.currentThread():
return
self.setBusy()
self.inDbHandler = True
if self.progressWins:
self.progressWins[0].maybeShow()
self.app.processEvents(QEventLoop.ExcludeUserInputEvents)
self.inDbHandler = False
def onDbFinished(self):
if self.mainThread != QThread.currentThread():
return
if not self.progressWins:
self.unsetBusy()
def setBusy(self):
if not self.busyCursor and not self.updatingBusy:
self.busyCursor = True
self.app.setOverrideCursor(QCursor(Qt.WaitCursor))
self.updatingBusy = True
self.setEnabled(False)
self.updatingBusy = False
def unsetBusy(self):
if self.busyCursor and not self.updatingBusy:
self.app.restoreOverrideCursor()
self.busyCursor = None
self.updatingBusy = True
self.setEnabled(True)
self.updatingBusy = False
# Media locations
##########################################################################
def setupMedia(self, deck):
prefix = self.config['mediaLocation']
prev = deck.getVar("mediaLocation") or ""
# set the media prefix
if not prefix:
next = ""
elif prefix == "dropbox":
p = self.dropboxFolder()
next = os.path.join(p, "Public", "Anki")
else:
next = prefix
# check if the media has moved
migrateFrom = None
if prev != next:
# check if they were using plugin
if not prev:
p = self.dropboxFolder()
p = os.path.join(p, "Public")
deck.mediaPrefix = p
migrateFrom = deck.mediaDir()
if not migrateFrom:
# find the old location
deck.mediaPrefix = prev
dir = deck.mediaDir()
if dir and os.listdir(dir):
# it contains files; we'll need to migrate
migrateFrom = dir
# setup new folder
deck.mediaPrefix = next
if migrateFrom:
# force creation of new folder
dir = deck.mediaDir(create=True)
# migrate old files
self.migrateMedia(migrateFrom, dir)
else:
# chdir if dir exists
dir = deck.mediaDir()
# update location
deck.setVar("mediaLocation", next, mod=False)
if dir and prefix == "dropbox":
self.setupDropbox(deck)
def migrateMedia(self, from_, to):
if from_ == to:
return
files = os.listdir(from_)
skipped = False
for f in files:
src = os.path.join(from_, f)
dst = os.path.join(to, f)
if not os.path.isfile(src):
skipped = True
continue
if not os.path.exists(dst):
shutil.copy2(src, dst)
if not skipped:
# everything copied, we can remove old folder
shutil.rmtree(from_, ignore_errors=True)
def dropboxFolder(self):
try:
import ankiqt.ui.dropbox as db
p = db.getPath()
except:
if sys.platform.startswith("win32"):
s = QSettings(QSettings.UserScope, "Microsoft", "Windows")
s.beginGroup("CurrentVersion/Explorer/Shell Folders")
p = os.path.join(unicode(s.value("Personal").toString()),
"My Dropbox")
else:
p = os.path.expanduser("~/Dropbox")
return p
def setupDropbox(self, deck):
if not self.config['dropboxPublicFolder']:
# put a file in the folder
open(os.path.join(
deck.mediaPrefix, "right-click-me.txt"), "w").write("")
# tell user what to do
ui.utils.showInfo(_("""\
A file called right-click-me.txt has been placed in DropBox's public folder. \
After clicking OK, this folder will appear. Please right click on the file (\
command+click on a Mac), choose DropBox>Copy Public Link, and paste the \
link into Anki."""))
# open folder and text prompt
self.onOpenPluginFolder(deck.mediaPrefix)
txt = ui.utils.getText(_("Paste path here:"), parent=self)
if txt[0]:
fail = False
if not txt[0].lower().startswith("http"):
fail = True
if not txt[0].lower().endswith("right-click-me.txt"):
fail = True
if fail:
ui.utils.showInfo(_("""\
That doesn't appear to be a public link. You'll be asked again when the deck \
is next loaded."""))
else:
self.config['dropboxPublicFolder'] = os.path.dirname(txt[0])
if self.config['dropboxPublicFolder']:
# update media url
deck.setVar(
"mediaURL", self.config['dropboxPublicFolder'] + "/" +
os.path.basename(deck.mediaDir()) + "/")
# Advanced features
##########################################################################
def onCheckDB(self):
"True if no problems"
if self.errorOccurred:
ui.utils.showWarning(_(
"Please restart Anki before checking the DB."))
return
if not ui.utils.askUser(_("""\
This operation will find and fix some common problems.<br>
<br>
On the next sync, all cards will be sent to the server.<br>
Any changes on the server since your last sync will be lost.<br>
<br>
<b>This operation is not undoable.</b><br>
Proceed?""")):
return
ret = self.deck.fixIntegrity()
if ret == "ok":
ret = True
ui.utils.showInfo(_("No problems found."))
else:
ret = _("Problems found:\n%s") % ret
diag = QDialog(self)
diag.setWindowTitle("Anki")
layout = QVBoxLayout(diag)
diag.setLayout(layout)
text = QTextEdit()
text.setReadOnly(True)
text.setPlainText(ret)
layout.addWidget(text)
box = QDialogButtonBox(QDialogButtonBox.Close)
layout.addWidget(box)
self.connect(box, SIGNAL("rejected()"), diag, SLOT("reject()"))
diag.exec_()
ret = False
self.reset()
return ret
def onOptimizeDB(self):
size = self.deck.optimize()
ui.utils.showInfo(_("Database optimized.\nShrunk by %dKB") % (size/1024.0))
def onCheckMediaDB(self):
mb = QMessageBox(self)
mb.setWindowTitle(_("Anki"))
mb.setIcon(QMessageBox.Warning)
mb.setText(_("""\
This operation looks through the content of your cards for media, and \
registers it so that it can be used with the online and mobile clients.
If you choose Scan+Delete, any media in your media folder that is not \
used by cards will be deleted. Please note that media is only \
counted as used if it appears on the question or answer of a card. If \
media is in a field that is not on your cards, the media will \
be deleted, and there is no way to undo this. Please make a backup if in \
doubt."""))
bScan = QPushButton(_("Scan"))
mb.addButton(bScan, QMessageBox.RejectRole)
bDelete = QPushButton(_("Scan+Delete"))
mb.addButton(bDelete, QMessageBox.RejectRole)
bCancel = QPushButton(_("Cancel"))
mb.addButton(bCancel, QMessageBox.RejectRole)
mb.exec_()
if mb.clickedButton() == bScan:
delete = False
elif mb.clickedButton() == bDelete:
delete = True
else:
return
(nohave, unused) = rebuildMediaDir(self.deck, delete=delete)
# generate report
report = ""
if nohave:
report += _(
"Used on cards but missing from media folder:")
report += "\n" + "\n".join(nohave)
if unused:
if report:
report += "\n\n"
if delete:
report += _("Deleted unused:")
else:
report += _(
"In media folder but not used by any cards:")
report += "\n" + "\n".join(unused)
if not report:
report = _("No unused or missing files found.")
ui.utils.showText(report, parent=self, type="text")
def onDownloadMissingMedia(self):
res = downloadMissing(self.deck)
if res is None:
ui.utils.showInfo(_("No media URL defined for this deck."),
help="MediaSupport")
return
if res[0] == True:
# success
(grabbed, missing) = res[1:]
msg = _("%d successfully retrieved.") % grabbed
if missing:
msg += "\n" + ngettext("%d missing.", "%d missing.", missing) % missing
else:
msg = _("Unable to download %s\nDownload aborted.") % res[1]
ui.utils.showInfo(msg)
def onLocalizeMedia(self):
if not ui.utils.askUser(_("""\
This will look for remote images and sounds on your cards, download them to \
your media folder, and convert the links to local ones. \
It can take a long time. Proceed?""")):
return
res = downloadRemote(self.deck)
count = len(res[0])
msg = ngettext("%d successfully downloaded.",
"%d successfully downloaded.", count) % count
if len(res[1]):
msg += "\n\n" + _("Couldn't find:") + "\n" + "\n".join(res[1])
ui.utils.showText(msg, parent=self, type="text")
def addHook(self, *args):
addHook(*args)
# System specific misc
##########################################################################
def setupSystemHacks(self):
self.setupDocumentDir()
self.changeLayoutSpacing()
addHook("macLoadEvent", self.onMacLoad)
if sys.platform.startswith("darwin"):
self.setUnifiedTitleAndToolBarOnMac(True)
self.mainWin.actionMarkCard.setShortcut(_("Alt+m"))
self.mainWin.verticalLayout_14.setContentsMargins(2,2,2,2)
# mac users expect a minimize option
self.minimizeShortcut = QShortcut("Ctrl+m", self)
self.connect(self.minimizeShortcut, SIGNAL("activated()"),
self.onMacMinimize)
self.hideAccelerators()
self.hideStatusTips()
if sys.platform.startswith("win32"):
self.mainWin.deckBrowserOuterFrame.setFrameStyle(QFrame.Panel)
self.mainWin.frame_2.setFrameStyle(QFrame.Panel)
self.mainWin.studyOptionsFrame.setFrameStyle(QFrame.Panel)
def hideAccelerators(self):
for action in self.findChildren(QAction):
txt = unicode(action.text())
m = re.match("^(.+)\(&.+\)(.+)?", txt)
if m:
action.setText(m.group(1) + (m.group(2) or ""))
def hideStatusTips(self):
for action in self.findChildren(QAction):
action.setStatusTip("")
def onMacMinimize(self):
self.setWindowState(self.windowState() | Qt.WindowMinimized)
def onMacLoad(self, fname):
self.loadDeck(fname)
def setupDocumentDir(self):
if self.config['documentDir']:
self.documentDir = self.config['documentDir']
elif sys.platform.startswith("win32"):
s = QSettings(QSettings.UserScope, "Microsoft", "Windows")
s.beginGroup("CurrentVersion/Explorer/Shell Folders")
self.documentDir = unicode(s.value("Personal").toString())
if os.path.exists(self.documentDir):
self.documentDir = os.path.join(self.documentDir, "Anki")
else:
self.documentDir = os.path.expanduser("~/.anki/decks")
elif sys.platform.startswith("darwin"):
self.documentDir = os.path.expanduser("~/Documents/Anki")
else:
self.documentDir = os.path.expanduser("~/.anki/decks")
try:
os.mkdir(self.documentDir)
except (OSError, IOError):
pass
def changeLayoutSpacing(self):
if sys.platform.startswith("darwin"):
self.mainWin.studyOptionsReviewBar.setContentsMargins(0, 20, 0, 0)
# Proxy support
##########################################################################
def setupProxy(self):
import urllib2
if self.config['proxyHost']:
proxy = "http://"
if self.config['proxyUser']:
proxy += (self.config['proxyUser'] + ":" +
self.config['proxyPass'] + "@")
proxy += (self.config['proxyHost'] + ":" +
str(self.config['proxyPort']))
os.environ["http_proxy"] = proxy
proxy_handler = urllib2.ProxyHandler()
opener = urllib2.build_opener(proxy_handler)
urllib2.install_opener(opener)
# Misc
##########################################################################
def setupMisc(self):
# if they've just upgraded, set created time based on deck age
if time.time() - self.config['created'] < 60 and self.deck:
self.config['created'] = self.deck.created
# tweaks for small screens
if self.config['optimizeSmall']:
p = self.mainWin.deckBrowserOuterFrame.sizePolicy()
p.setHorizontalStretch(1)
self.mainWin.deckBrowserOuterFrame.setSizePolicy(p)
self.mainWin.decksLabel.hide()
self.mainWin.decksLine.hide()
self.mainWin.studyOptsLabel.hide()
def setupBackups(self):
# set backups
anki.deck.numBackups = self.config['numBackups']