diff --git a/aqt/main.py b/aqt/main.py index 3616caa27..d2ce09d1c 100755 --- a/aqt/main.py +++ b/aqt/main.py @@ -30,6 +30,18 @@ class AnkiQt(QMainWindow): aqt.mw = self self.app = app self.pm = profileManager + # use the global language for early init; once a profile is loaded we + # can switch to a user's preferred language + self.setupLang(force=self.pm.meta['defaultLang']) + # running 2.0 for the first time? + if self.pm.meta['firstRun']: + # upgrade if necessary + from aqt.upgrade import Upgrader + u = Upgrader(self) + u.maybeUpgrade() + self.pm.meta['firstRun'] = False + self.pm.save() + # init rest of app try: self.setupUI() self.setupAddons() @@ -41,7 +53,6 @@ class AnkiQt(QMainWindow): def setupUI(self): self.col = None self.state = None - self.setupLang("en") # bootstrap with english; profile will adjust self.setupThreads() self.setupMainWindow() self.setupStyle() @@ -67,7 +78,7 @@ class AnkiQt(QMainWindow): def setupProfile(self): # profile not provided on command line? - if False: # not self.pm.name: + if not self.pm.name: # if there's a single profile, load it automatically profs = self.pm.profiles() if len(profs) == 1: @@ -657,7 +668,7 @@ Please choose a new deck name:""")) ########################################################################## def setupLang(self, force=None): - "Set the user interface language." + "Set the user interface language for the current profile." import locale, gettext import anki.lang try: diff --git a/aqt/profiles.py b/aqt/profiles.py index 41850254a..b9102ccfb 100644 --- a/aqt/profiles.py +++ b/aqt/profiles.py @@ -7,9 +7,11 @@ # - Saves in sqlite rather than a flat file so the config can't be corrupted from aqt.qt import * -import os, sys, time, random, cPickle, shutil +import os, sys, time, random, cPickle, shutil, locale, re from anki.db import DB from anki.utils import isMac, isWin, intTime, checksum +from anki.lang import langs +import aqt.forms metaConf = dict( ver=0, @@ -19,6 +21,7 @@ metaConf = dict( lastMsg=-1, suppressUpdate=False, firstRun=True, + defaultLang=None, ) profileConf = dict( @@ -103,8 +106,10 @@ documentation for information on using a flash drive.""") self.db.commit() def create(self, name): + prof = profileConf.copy() + prof['lang'] = self.meta['defaultLang'] self.db.execute("insert into profiles values (?, ?)", - name, cPickle.dumps(profileConf)) + name, cPickle.dumps(prof)) self.db.commit() def remove(self, name): @@ -159,6 +164,7 @@ create table if not exists profiles self.meta = metaConf.copy() self.db.execute("insert into profiles values ('_global', ?)", cPickle.dumps(metaConf)) + self._setDefaultLang() # and save a default user profile for later (commits) self.create("User 1") else: @@ -169,3 +175,50 @@ create table if not exists profiles def _pwhash(self, passwd): return checksum(unicode(self.meta['id'])+unicode(passwd)) + + + # Default language + ###################################################################### + # On first run, allow the user to choose the default language + + def _setDefaultLang(self): + # the dialog expects _ to be defined, but we're running before + # setLang() has been called. so we create a dummy op for now + import __builtin__ + __builtin__.__dict__['_'] = lambda x: x + # create dialog + class NoCloseDiag(QDialog): + def reject(self): + pass + d = self.langDiag = NoCloseDiag() + f = self.langForm = aqt.forms.setlang.Ui_Dialog() + f.setupUi(d) + d.connect(d, SIGNAL("accepted()"), self._onLangSelected) + d.connect(d, SIGNAL("rejected()"), lambda: True) + # default to the system language + (lang, enc) = locale.getdefaultlocale() + if lang and lang not in ("pt_BR", "zh_CN", "zh_TW"): + lang = re.sub("(.*)_.*", "\\1", lang) + # find index + idx = None + en = None + for c, (name, code) in enumerate(langs): + if code == "en": + en = c + if code == lang: + idx = c + # if the system language isn't available, revert to english + if idx is None: + idx = en + # update list + f.lang.addItems([x[0] for x in langs]) + f.lang.setCurrentRow(idx) + d.exec_() + + def _onLangSelected(self): + f = self.langForm + code = langs[f.lang.currentRow()][1] + self.meta['defaultLang'] = code + sql = "update profiles set data = ? where name = ?" + self.db.execute(sql, cPickle.dumps(self.meta), "_global") + self.db.commit() diff --git a/aqt/upgrade.py b/aqt/upgrade.py new file mode 100644 index 000000000..f277653ee --- /dev/null +++ b/aqt/upgrade.py @@ -0,0 +1,236 @@ +# Copyright: Damien Elmes +# -*- coding: utf-8 -*- +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import os, cPickle +from aqt.qt import * +from anki.utils import isMac, isWin +from anki import Collection +from anki.importing import Anki1Importer + +class Upgrader(object): + + def __init__(self, mw): + self.mw = mw + + def maybeUpgrade(self): + p = self._oldConfigPath() + # does an old config file exist? + if not os.path.exists(p): + return + # load the new deck user profile + self.mw.pm.load(self.mw.pm.profiles()[0]) + # load old settings and copy over + self._loadConf(p) + self._copySettings() + # and show the wizard + self._showWizard() + + # Settings + ###################################################################### + + def _oldConfigPath(self): + if isWin: + p = "~/.anki/config.db" + elif isMac: + p = "~/Library/Application Support/Anki/config.db" + else: + p = "~/.anki/config.db" + return os.path.expanduser(p) + + def _loadConf(self, path): + self.conf = cPickle.load(open(path)) + + def _copySettings(self): + p = self.mw.pm.profile + for k in ( + "recentColours", "stripHTML", "editFontFamily", "editFontSize", + "editLineSize", "deleteMedia", "preserveKeyboard", "numBackups", + "proxyHost", "proxyPass", "proxyPort", "proxyUser", + "showProgress"): + p[k] = self.conf[k] + p['autoplay'] = self.conf['autoplaySounds'] + p['showDueTimes'] = not self.conf['suppressEstimates'] + self.mw.pm.save() + + # Wizard + ###################################################################### + + def _showWizard(self): + class Wizard(QWizard): + def reject(self): + pass + self.wizard = w = Wizard() + w.addPage(self._welcomePage()) + w.addPage(self._decksPage()) + w.addPage(self._mediaPage()) + w.addPage(self._readyPage()) + w.addPage(self._upgradePage()) + w.addPage(self._finishedPage()) + w.setWindowTitle("Upgrade Wizard") + w.setWizardStyle(QWizard.ModernStyle) + w.exec_() + + def _labelPage(self, title, txt): + p = QWizardPage() + p.setTitle(title) + l = QLabel(txt) + l.setTextFormat(Qt.RichText) + l.setTextInteractionFlags(Qt.TextSelectableByMouse) + l.setWordWrap(True) + v = QVBoxLayout() + v.addWidget(l) + p.setLayout(v) + return p + + def _welcomePage(self): + return self._labelPage(_("Welcome"), _("""\ +This wizard will guide you through the Anki 2.0 upgrade process. +For a smooth upgrade, please read the following pages carefully. +""")) + + def _decksPage(self): + return self._labelPage(_("Your Decks"), _("""\ +Anki 2 stores your decks in a new format. This wizard will automatically +convert your decks to that format. Your decks will be backed up before +the upgrade, so if you need to revert to the previous version of Anki, your +decks will still be usable.""")) + + def _mediaPage(self): + return self._labelPage(_("Sounds & Images"), _("""\ +When your decks are upgraded, Anki will attempt to copy any sounds and images +from the old decks. If you were using a custom DropBox folder or custom media +folder, the upgrade process may not be able to locate your media. Later on, a +report of the upgrade will be presented to you. If you notice media was not +copied when it should have been, please see the upgrade guide for more +instructions. +

+AnkiWeb now supports media syncing directly. No special setup is required, and +media will be synchronized along with your cards when you sync to AnkiWeb.""")) + + def _readyPage(self): + class ReadyPage(QWizardPage): + def initializePage(self): + self.setTitle(_("Ready to Upgrade")) + self.setCommitPage(True) + l = QLabel(_("""\ +When you're ready to upgrade, click the commit button to continue. The upgrade +guide will open in your browser while the upgrade proceeds. Please read it +carefully, as a lot has changed since the previous Anki version.""")) + l.setTextFormat(Qt.RichText) + l.setTextInteractionFlags(Qt.TextSelectableByMouse) + l.setWordWrap(True) + v = QVBoxLayout() + v.addWidget(l) + self.setLayout(v) + return ReadyPage() + + def _upgradePage(self): + decks = self.conf['recentDeckPaths'] + colpath = self.mw.pm.collectionPath() + upgrader = self + class UpgradePage(QWizardPage): + def isComplete(self): + return False + def initializePage(self): + self.setTitle(_("Upgrading")) + self.label = l = QLabel() + l.setTextInteractionFlags(Qt.TextSelectableByMouse) + l.setWordWrap(True) + v = QVBoxLayout() + v.addWidget(l) + prog = QProgressBar() + prog.setMaximum(0) + v.addWidget(prog) + l2 = QLabel(_("Please be patient; this can take a while.")) + l2.setTextInteractionFlags(Qt.TextSelectableByMouse) + l2.setWordWrap(True) + v.addWidget(l2) + self.setLayout(v) + # run the upgrade in a different thread + self.thread = UpgradeThread(decks, colpath) + self.thread.start() + # and periodically update the GUI + self.timer = QTimer(self) + self.timer.connect(self.timer, SIGNAL("timeout()"), self.onTimer) + self.timer.start(1000) + self.onTimer() + def onTimer(self): + prog = self.thread.progress() + if not prog: + self.timer.stop() + upgrader.log = self.thread.log + upgrader.wizard.next() + self.label.setText(prog) + return UpgradePage() + + def _finishedPage(self): + upgrader = self + class FinishedPage(QWizardPage): + def initializePage(self): + buf = "" + for file in upgrader.log: + buf += "%s" % file[0] + buf += "

" + self.setTitle(_("Upgrade Complete")) + l = QLabel(_("""\ +The upgrade has finished, and you're ready to start using Anki 2.0. +

+Below is a log of the update: +

+%s

""") % buf) + l.setTextFormat(Qt.RichText) + l.setTextInteractionFlags(Qt.TextSelectableByMouse) + l.setWordWrap(True) + l.setMaximumWidth(400) + a = QScrollArea() + a.setWidget(l) + v = QVBoxLayout() + v.addWidget(a) + self.setLayout(v) + return FinishedPage() + +class UpgradeThread(QThread): + def __init__(self, paths, colpath): + QThread.__init__(self) + self.paths = paths + self.max = len(paths) + self.current = 1 + self.finished = False + self.colpath = colpath + self.name = "" + self.log = [] + def run(self): + # open profile deck + self.col = Collection(self.colpath) + # loop through paths + while True: + path = self.paths.pop() + self.name = os.path.basename(path) + self.upgrade(path) + # abort if finished + if not self.paths: + break + self.current += 1 + self.col.close() + self.finished = True + def progress(self): + if self.finished: + return + return _("Upgrading deck %(a)s of %(b)s...\n%(c)s") % \ + dict(a=self.current, b=self.max, c=self.name) + def upgrade(self, path): + log = self._upgrade(path) + self.log.append((self.name, log)) + def _upgrade(self, path): + if not os.path.exists(path): + return [_("File was missing.")] + imp = Anki1Importer(self.col, path) + try: + imp.run() + except Exception, e: + if unicode(e) == "invalidFile": + # already logged + pass + self.col.save() + return imp.log diff --git a/designer/setlang.ui b/designer/setlang.ui new file mode 100644 index 000000000..44503186f --- /dev/null +++ b/designer/setlang.ui @@ -0,0 +1,74 @@ + + + Dialog + + + + 0 + 0 + 400 + 300 + + + + Anki + + + + + + Interface language: + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +