From f4150a5df42fd668d7dc09bf8e3854d2a4effcd1 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 24 Nov 2011 12:48:58 +0900 Subject: [PATCH] refactor; add profile support --- aqt/__init__.py | 94 +--- aqt/about.py | 5 +- aqt/activetags.py | 135 ----- aqt/addcards.py | 8 +- aqt/browser.py | 126 ++--- aqt/clayout.py | 18 +- aqt/config.py | 135 ----- aqt/deckopts.py | 10 +- aqt/editcurrent.py | 4 +- aqt/editor.py | 18 +- aqt/getshared.py | 6 +- aqt/groupconf.py | 22 +- aqt/groups.py | 14 +- aqt/importing.py | 18 +- aqt/main.py | 514 +++++------------- aqt/profiles.py | 151 +++++ designer/browser.ui | 64 +-- designer/{deckopts.ui => colopts.ui} | 0 designer/icons.qrc | 4 + designer/icons/edit-find 2.png | Bin 0 -> 1631 bytes designer/icons/edit-find-replace.png | Bin 0 -> 2010 bytes .../graphite_smooth_folder_noncommercial.png | Bin 0 -> 2087 bytes designer/icons/user-identity.png | Bin 0 -> 1320 bytes designer/importup.ui | 4 +- designer/main.ui | 248 ++------- 25 files changed, 532 insertions(+), 1066 deletions(-) delete mode 100644 aqt/activetags.py delete mode 100644 aqt/config.py create mode 100644 aqt/profiles.py rename designer/{deckopts.ui => colopts.ui} (100%) create mode 100644 designer/icons/edit-find 2.png create mode 100644 designer/icons/edit-find-replace.png create mode 100644 designer/icons/graphite_smooth_folder_noncommercial.png create mode 100644 designer/icons/user-identity.png diff --git a/aqt/__init__.py b/aqt/__init__.py index 329e22a59..d2f011f2e 100644 --- a/aqt/__init__.py +++ b/aqt/__init__.py @@ -9,13 +9,13 @@ appVersion="1.99" appWebsite="http://ankisrs.net/" appHelpSite="http://ankisrs.net/docs/dev/" appDonate="http://ankisrs.net/support/" -modDir=os.path.dirname(os.path.abspath(__file__)) -runningDir=os.path.split(modDir)[0] mw = None # set on init + +moduleDir = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] + # py2exe -if hasattr(sys, "frozen"): - sys.path.append(modDir) - modDir = os.path.dirname(sys.argv[0]) +# if hasattr(sys, "frozen"): +# sys.path.append(moduleDir) def openHelp(name): if "#" in name: @@ -60,46 +60,6 @@ class DialogManager(object): dialogs = DialogManager() -# Splash screen -########################################################################## - -class SplashScreen(object): - - def __init__(self, max=100): - self.finished = False - self.pixmap = QPixmap(":/icons/anki-logo.png") - self.splash = QSplashScreen(self.pixmap) - self.prog = QProgressBar(self.splash) - self.prog.setMaximum(max) - if QApplication.instance().style().objectName() != "plastique": - self.style = QStyleFactory.create("plastique") - self.prog.setStyle(self.style) - self.prog.setStyleSheet("""* { -color: #ffffff; -background-color: #061824; -margin:0px; -border:0px; -padding: 0px; -text-align: center;} -*::chunk { -color: #13486c; -} -""") - x = 8 - self.prog.setGeometry(self.splash.width()/10, 8.85*self.splash.height()/10, - x*self.splash.width()/10, self.splash.height()/10) - self.splash.show() - self.val = 1 - - def update(self): - self.prog.setValue(self.val) - self.val += 1 - QApplication.instance().processEvents() - - def finish(self, obj): - self.splash.finish(obj) - self.finished = True - # App initialisation ########################################################################## @@ -116,52 +76,26 @@ def run(): global mw from anki.utils import isWin, isMac - # home on win32 is broken - mustQuit = False - if isWin: - # use appdata if available - if 'APPDATA' in os.environ: - os.environ['HOME'] = os.environ['APPDATA'] - else: - mustQuit = True - # make and check accessible - try: - os.makedirs(os.path.expanduser("~/.anki")) - except: - pass - try: - os.listdir(os.path.expanduser("~/.anki")) - except: - mustQuit = True - # on osx we'll need to add the qt plugins to the search path - rd = runningDir if isMac and getattr(sys, 'frozen', None): - rd = os.path.abspath(runningDir + "/../../..") + rd = os.path.abspath(moduleDir + "/../../..") QCoreApplication.setLibraryPaths([rd]) # create the app app = AnkiApp(sys.argv) QCoreApplication.setApplicationName("Anki") - if mustQuit: - QMessageBox.warning( - None, "Anki", "Can't open APPDATA, nor c:\\anki.\n" - "Please try removing foreign characters from your username.") - sys.exit(1) - splash = SplashScreen(3) # parse args import optparse parser = optparse.OptionParser() - parser.usage = "%prog []" - parser.add_option("-c", "--config", help="path to config dir", - default=os.path.expanduser("~/.anki")) + parser.usage = "%prog [OPTIONS]" + parser.add_option("-b", "--base", help="Path to base folder") + parser.add_option("-p", "--profile", help="Profile name to load") (opts, args) = parser.parse_args(sys.argv[1:]) - # setup config - import aqt.config - conf = aqt.config.Config( - unicode(os.path.abspath(opts.config), sys.getfilesystemencoding())) + # profile manager + from aqt.profiles import ProfileManager + pm = ProfileManager(opts.base, opts.profile) # qt translations translationPath = '' @@ -175,10 +109,8 @@ def run(): qtTranslator.load("qt_" + short, translationPath): app.installTranslator(qtTranslator) - # load main window - splash.update() import aqt.main - mw = aqt.main.AnkiQt(app, conf, args, splash) + mw = aqt.main.AnkiQt(app, pm) app.exec_() if __name__ == "__main__": diff --git a/aqt/about.py b/aqt/about.py index 8e69a5b03..ccf6e19bd 100644 --- a/aqt/about.py +++ b/aqt/about.py @@ -19,7 +19,7 @@ system. It's free and open source.") abouttext += '

' + _("Written by Damien Elmes, with patches, translation,\ testing and design from:

%(cont)s") % {'cont': u""" -Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene +Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen, Emilio Wuerges, Emmanuel Jarri, Frank Harper, H. Mijail, Ian Lewis, Iroiro, Jin Eun-Deok, Jarvik7, Jo Nakashima, Christian Krause, LaC, Laurent Steffan, Marco @@ -30,6 +30,9 @@ Petr Michalec, Piotr Kubowicz, Richard Colley, Samson Melamed, Stefaan De Pooter, Susanna Björverud, Tacutu, Timm Preetz, Timo Paulssen, Ursus, Victor Suba, and Xtru. +Anki icon by Alex Fraser (CC GNU GPL) +Deck icon by Laurent Baumann (CC BY-NC-SA 3.0) +Other icons under LGPL or public domain. """ } diff --git a/aqt/activetags.py b/aqt/activetags.py deleted file mode 100644 index 1113b04ca..000000000 --- a/aqt/activetags.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright: Damien Elmes -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -from aqt.qt import * -import aqt -from anki.utils import parseTags, joinTags, canonifyTags -from aqt.ui.utils import saveGeom, restoreGeom - -class ActiveTagsChooser(QDialog): - - def __init__(self, parent, type): - QDialog.__init__(self, parent, Qt.Window) - self.parent = parent - self.deck = self.parent.deck - self.dialog = aqt.forms.activetags.Ui_Dialog() - self.dialog.setupUi(self) - if type == "new": - self.active = "newActive" - self.inactive = "newInactive" - else: - self.active = "revActive" - self.inactive = "revInactive" - if (self.deck.getVar("newActive") == self.deck.getVar("revActive") and - self.deck.getVar("newInactive") == self.deck.getVar("revInactive")): - self.dialog.bothButton.click() - elif type == "new": - self.dialog.newButton.click() - else: - self.dialog.revButton.click() - self.connect(self.dialog.buttonBox, SIGNAL("helpRequested()"), - self.onHelp) - self.rebuildTagList() - restoreGeom(self, "activeTags") - - def rebuildTagList(self): - usertags = self.deck.allTags() - self.items = [] - self.suspended = {} - yes = parseTags(self.deck.getVar(self.active)) - no = parseTags(self.deck.getVar(self.inactive)) - yesHash = {} - noHash = {} - for y in yes: - yesHash[y] = True - for n in no: - noHash[n] = True - groupedTags = [] - usertags.sort() - # render models and templates - for (type, sql, icon) in ( - ("models", "select tags from models", "contents.png"), - ("cms", "select name from cardModels", "Anki_Card.png")): - d = {} - tagss = self.deck.db.column0(sql) - for tags in tagss: - for tag in parseTags(tags): - d[tag] = 1 - sortedtags = sorted(d.keys()) - icon = QIcon(":/icons/" + icon) - groupedTags.append([icon, sortedtags]) - # remove from user tags - for tag in groupedTags[0][1] + groupedTags[1][1]: - try: - usertags.remove(tag) - except: - pass - # user tags - icon = QIcon(":/icons/Anki_Fact.png") - groupedTags.append([icon, usertags]) - self.tags = [] - for (icon, tags) in groupedTags: - for t in tags: - self.tags.append(t) - item = QListWidgetItem(icon, t.replace("_", " ")) - self.dialog.activeList.addItem(item) - if t in yesHash: - mode = QItemSelectionModel.Select - self.dialog.activeCheck.setChecked(True) - else: - mode = QItemSelectionModel.Deselect - idx = self.dialog.activeList.indexFromItem(item) - self.dialog.activeList.selectionModel().select(idx, mode) - # inactive - item = QListWidgetItem(icon, t.replace("_", " ")) - self.dialog.inactiveList.addItem(item) - if t in noHash: - mode = QItemSelectionModel.Select - self.dialog.inactiveCheck.setChecked(True) - else: - mode = QItemSelectionModel.Deselect - idx = self.dialog.inactiveList.indexFromItem(item) - self.dialog.inactiveList.selectionModel().select(idx, mode) - - def accept(self): - self.hide() - n = 0 - yes = [] - no = [] - for c in range(self.dialog.activeList.count()): - # active - item = self.dialog.activeList.item(c) - idx = self.dialog.activeList.indexFromItem(item) - if self.dialog.activeList.selectionModel().isSelected(idx): - yes.append(self.tags[c]) - # inactive - item = self.dialog.inactiveList.item(c) - idx = self.dialog.inactiveList.indexFromItem(item) - if self.dialog.inactiveList.selectionModel().isSelected(idx): - no.append(self.tags[c]) - types = [] - if (self.dialog.newButton.isChecked() or - self.dialog.bothButton.isChecked()): - types.append(["newActive", "newInactive"]) - if (self.dialog.revButton.isChecked() or - self.dialog.bothButton.isChecked()): - types.append(["revActive", "revInactive"]) - for (active, inactive) in types: - if self.dialog.activeCheck.isChecked(): - self.deck.setVar(active, joinTags(yes)) - else: - self.deck.setVar(active, "") - if self.dialog.inactiveCheck.isChecked(): - self.deck.setVar(inactive, joinTags(no)) - else: - self.deck.setVar(inactive, "") - self.parent.reset() - saveGeom(self, "activeTags") - QDialog.accept(self) - - def onHelp(self): - aqt.openHelp("SelectiveStudy") - -def show(parent, type): - at = ActiveTagsChooser(parent, type) - at.exec_() diff --git a/aqt/addcards.py b/aqt/addcards.py index 598207ad7..01ca8ac90 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -76,7 +76,7 @@ class AddCards(QDialog): # FIXME: need to make sure to clean up note on exit def setupNewNote(self, set=True): - f = self.mw.deck.newNote() + f = self.mw.col.newNote() f.tags = f.model()['tags'] if set: self.editor.setNote(f) @@ -104,7 +104,7 @@ class AddCards(QDialog): if not note or not note.id: return # we don't have to worry about cards; just the note - self.mw.deck._remNotes([note.id]) + self.mw.col._remNotes([note.id]) def addHistory(self, note): txt = stripHTMLMedia(",".join(note.fields))[:30] @@ -131,7 +131,7 @@ class AddCards(QDialog): "Some fields are missing or not unique."), help="AddItems#AddError") return - cards = self.mw.deck.addNote(note) + cards = self.mw.col.addNote(note) if not cards: showWarning(_("""\ The input you have provided would make an empty @@ -151,7 +151,7 @@ question or answer on all cards."""), help="AddItems") # stop anything playing clearAudioQueue() self.onReset(keep=True) - self.mw.deck.autosave() + self.mw.col.autosave() def keyPressEvent(self, evt): "Show answer on RET or register answer." diff --git a/aqt/browser.py b/aqt/browser.py index a8f782830..2c36b3de7 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -28,14 +28,14 @@ COLOUR_MARKED2 = "#aaaaff" # Data model ########################################################################## -class DeckModel(QAbstractTableModel): +class DataModel(QAbstractTableModel): def __init__(self, browser): QAbstractTableModel.__init__(self) self.browser = browser - self.deck = browser.deck + self.col = browser.col self.sortKey = None - self.activeCols = self.deck.conf.get( + self.activeCols = self.col.conf.get( "activeCols", ["noteFld", "template", "cardDue", "cardEase"]) self.cards = [] self.cardObjs = {} @@ -43,7 +43,7 @@ class DeckModel(QAbstractTableModel): def getCard(self, index): id = self.cards[index.row()] if not id in self.cardObjs: - self.cardObjs[id] = self.deck.getCard(id) + self.cardObjs[id] = self.col.getCard(id) return self.cardObjs[id] def refreshNote(self, note): @@ -112,7 +112,7 @@ class DeckModel(QAbstractTableModel): # the db progress handler may cause a refresh, so we need to zero out # old data first self.cards = [] - self.cards = self.deck.findCards(txt, self.browser.mw.config['fullSearch']) + self.cards = self.col.findCards(txt, self.browser.mw.config['fullSearch']) print "fetch cards in %dms" % ((time.time() - t)*1000) if reset: self.endReset() @@ -206,7 +206,7 @@ class DeckModel(QAbstractTableModel): return self.answer() elif type == "noteFld": f = c.note() - return self.formatQA(f.fields[self.deck.models.sortIdx(f.model())]) + return self.formatQA(f.fields[self.col.models.sortIdx(f.model())]) elif type == "template": return c.template()['name'] elif type == "cardDue": @@ -230,9 +230,9 @@ class DeckModel(QAbstractTableModel): return _("(new)") return "%d%%" % (c.factor/10) elif type == "cardGroup": - return self.browser.mw.deck.groups.name(c.gid) + return self.browser.mw.col.groups.name(c.gid) elif type == "noteGroup": - return self.browser.mw.deck.groups.name(c.note().gid) + return self.browser.mw.col.groups.name(c.note().gid) def question(self): return self.formatQA(c.a()) @@ -255,7 +255,7 @@ class DeckModel(QAbstractTableModel): elif c.queue == 1: date = c.due elif c.queue == 2: - date = time.time() + ((c.due - self.deck.sched.today)*86400) + date = time.time() + ((c.due - self.col.sched.today)*86400) else: return _("(susp.)") return time.strftime("%Y-%m-%d", time.localtime(date)) @@ -301,7 +301,7 @@ class Browser(QMainWindow): QMainWindow.__init__(self, None) #applyStyles(self) self.mw = mw - self.deck = self.mw.deck + self.col = self.mw.col self.currentRow = None self.lastFilter = "" self.form = aqt.forms.browser.Ui_Dialog() @@ -385,7 +385,7 @@ class Browser(QMainWindow): saveGeom(self, "editor") saveState(self, "editor") saveHeader(self.form.tableView.horizontalHeader(), "editor") - self.deck.conf['activeCols'] = self.model.activeCols + self.col.conf['activeCols'] = self.model.activeCols self.hide() aqt.dialogs.close("Browser") self.teardownHooks() @@ -467,7 +467,7 @@ class Browser(QMainWindow): cur) % { "cur": cur, "sel": ngettext("%d selected", "%d selected", selected) % selected - } + " - " + self.deck.name()) + } + " - " + self.col.name()) return selected def onReset(self): @@ -478,7 +478,7 @@ class Browser(QMainWindow): ###################################################################### def setupTable(self): - self.model = DeckModel(self) + self.model = DataModel(self) self.form.tableView.setSortingEnabled(True) self.form.tableView.setShowGrid(False) self.form.tableView.setModel(self.model) @@ -537,28 +537,28 @@ class Browser(QMainWindow): if type in noSort: showInfo(_("Sorting on this column is not supported. Please " "choose another.")) - type = self.deck.conf['sortType'] - if self.deck.conf['sortType'] != type: - self.deck.conf['sortType'] = type + type = self.col.conf['sortType'] + if self.col.conf['sortType'] != type: + self.col.conf['sortType'] = type # default to descending for non-text fields if type == "noteFld": ord = not ord - self.deck.conf['sortBackwards'] = ord + self.col.conf['sortBackwards'] = ord self.onSearch() else: - if self.deck.conf['sortBackwards'] != ord: - self.deck.conf['sortBackwards'] = ord + if self.col.conf['sortBackwards'] != ord: + self.col.conf['sortBackwards'] = ord self.model.reverse() self.setSortIndicator() def setSortIndicator(self): hh = self.form.tableView.horizontalHeader() - type = self.deck.conf['sortType'] + type = self.col.conf['sortType'] if type not in self.model.activeCols: hh.setSortIndicatorShown(False) return idx = self.model.activeCols.index(type) - if self.deck.conf['sortBackwards']: + if self.col.conf['sortBackwards']: ord = Qt.DescendingOrder else: ord = Qt.AscendingOrder @@ -649,7 +649,7 @@ class Browser(QMainWindow): self.onSearch() def _modelTree(self, root): - for m in sorted(self.deck.models.all(), key=itemgetter("name")): + for m in sorted(self.col.models.all(), key=itemgetter("name")): mitem = self.CallbackItem( m['name'], lambda m=m: self.setFilter("model", m['name'])) mitem.setIcon(0, QIcon(":/icons/product_design.png")) @@ -662,7 +662,7 @@ class Browser(QMainWindow): mitem.addChild(titem) def _groupTree(self, root): - grps = self.deck.sched.groupTree() + grps = self.col.sched.groupTree() def fillGroups(root, grps, head=""): for g in grps: item = self.CallbackItem( @@ -694,7 +694,7 @@ class Browser(QMainWindow): return root def _userTagTree(self, root): - for t in sorted(self.deck.tags.all()): + for t in sorted(self.col.tags.all()): item = self.CallbackItem( t, lambda t=t: self.setFilter("tag", t)) item.setIcon(0, QIcon(":/icons/anki-tag.png")) @@ -706,7 +706,7 @@ class Browser(QMainWindow): def setupCardInfo(self): from anki.stats import CardStats self.card = None - self.cardStats = CardStats(self.deck, None) + self.cardStats = CardStats(self.col, None) self.connect(self.form.cardLabel, SIGNAL("linkActivated(const QString&)"), self.onCardLink) @@ -717,7 +717,7 @@ class Browser(QMainWindow): rep = "" + rep m = self.card.model() # add sort field - sortf = m['flds'][self.mw.deck.models.sortIdx(m)]['name'] + sortf = m['flds'][self.mw.col.models.sortIdx(m)]['name'] extra = self.cardStats.makeLine( _("Sort Field"), "%s" % sortf) # and revlog @@ -767,7 +767,7 @@ class Browser(QMainWindow): s = "" % _("Date") s += ("" * 5) % ( _("Type"), _("Ease"), _("Interval"), _("Factor"), _("Time")) - for (date, ease, ivl, factor, taken, type) in self.mw.deck.db.execute( + for (date, ease, ivl, factor, taken, type) in self.mw.col.db.execute( "select time/1000, ease, ivl, factor, taken/1000.0, type " "from revlog where cid = ?", self.card.id): s += "" % time.strftime(_("%Y-%m-%d @ %H:%M"), @@ -810,14 +810,14 @@ class Browser(QMainWindow): self.form.tableView.selectionModel().selectedRows()] def selectedNotes(self): - return self.deck.db.list(""" + return self.col.db.list(""" select distinct nid from cards where id in %s""" % ids2str( [self.model.cards[idx.row()] for idx in self.form.tableView.selectionModel().selectedRows()])) def selectedNotesAsCards(self): - return self.deck.db.list( + return self.col.db.list( "select id from cards where nid in (%s)" % ",".join([str(s) for s in self.selectedNotes()])) @@ -825,7 +825,7 @@ where id in %s""" % ids2str( sf = self.selectedNotes() if not sf: return - mods = self.deck.db.scalar(""" + mods = self.col.db.scalar(""" select count(distinct mid) from notes where id in %s""" % ids2str(sf)) if mods > 1: @@ -861,7 +861,7 @@ where id in %s""" % ids2str(sf)) self.mw.checkpoint(_("Delete Cards")) self.model.beginReset() oldRow = self.form.tableView.selectionModel().currentIndex().row() - self.deck.remCards(self.selectedCards()) + self.col.remCards(self.selectedCards()) self.onSearch(reset=False) if len(self.model.cards): new = min(oldRow, len(self.model.cards) - 1) @@ -880,7 +880,7 @@ where id in %s""" % ids2str(sf)) from aqt.tagedit import TagEdit te = TagEdit(d, type=1) frm.groupBox.layout().insertWidget(0, te) - te.setDeck(self.deck) + te.setCol(self.col) d.connect(d, SIGNAL("accepted()"), lambda: self.onSetGroup(frm, te)) self.setTabOrder(frm.setCur, te) self.setTabOrder(te, frm.setInitial) @@ -894,16 +894,16 @@ where id in %s""" % ids2str(sf)) self.mw.checkpoint(_("Set Group")) mod = intTime() if frm.setCur.isChecked(): - gid = self.deck.groups.id(unicode(te.text())) - self.deck.db.execute( + gid = self.col.groups.id(unicode(te.text())) + self.col.db.execute( "update cards set mod=?, gid=? where id in " + ids2str( self.selectedCards()), mod, gid) if frm.setInitial.isChecked(): - self.deck.db.execute( + self.col.db.execute( "update notes set mod=?, gid=? where id in " + ids2str( self.selectedNotes()), mod, gid) else: - self.deck.db.execute(""" + self.col.db.execute(""" update cards set mod=?, gid=(select gid from notes where id = cards.nid) where id in %s""" % ids2str(self.selectedCards()), mod) self.onSearch(reset=False) @@ -917,13 +917,13 @@ where id in %s""" % ids2str(self.selectedCards()), mod) if prompt is None: prompt = _("Enter tags to add:") if tags is None: - (tags, r) = getTag(self, self.deck, prompt) + (tags, r) = getTag(self, self.col, prompt) else: r = True if not r: return if func is None: - func = self.deck.tags.bulkAdd + func = self.col.tags.bulkAdd self.model.beginReset() if label is None: label = _("Add Tags") @@ -938,7 +938,7 @@ where id in %s""" % ids2str(self.selectedCards()), mod) if label is None: label = _("Delete Tags") self.addTags(tags, label, _("Enter tags to delete:"), - func=self.deck.tags.bulkRem) + func=self.col.tags.bulkRem) # Suspending and marking ###################################################################### @@ -955,9 +955,9 @@ where id in %s""" % ids2str(self.selectedCards()), mod) self.editor.saveNow() c = self.selectedCards() if sus: - self.deck.sched.suspendCards(c) + self.col.sched.suspendCards(c) else: - self.deck.sched.unsuspendCards(c) + self.col.sched.unsuspendCards(c) self.model.reset() self.mw.requireReset() @@ -975,7 +975,7 @@ where id in %s""" % ids2str(self.selectedCards()), mod) def reposition(self): cids = self.selectedCards() - cids = self.deck.db.list( + cids = self.col.db.list( "select id from cards where type = 0 and id in " + ids2str(cids)) if not cids: return showInfo(_("Only new cards can be repositioned.")) @@ -983,7 +983,7 @@ where id in %s""" % ids2str(self.selectedCards()), mod) d.setWindowModality(Qt.WindowModal) frm = aqt.forms.reposition.Ui_Dialog() frm.setupUi(d) - (pmin, pmax) = self.deck.db.first( + (pmin, pmax) = self.col.db.first( "select min(due), max(due) from cards where type=0") txt = _("Queue top: %d") % pmin txt += "\n" + _("Queue bottom: %d") % pmax @@ -992,7 +992,7 @@ where id in %s""" % ids2str(self.selectedCards()), mod) return self.model.beginReset() self.mw.checkpoint(_("Reposition")) - self.deck.sched.sortCards( + self.col.sched.sortCards( cids, start=frm.start.value(), step=frm.step.value(), shuffle=frm.randomize.isChecked(), shift=frm.shift.isChecked()) self.onSearch(reset=False) @@ -1012,9 +1012,9 @@ where id in %s""" % ids2str(self.selectedCards()), mod) self.model.beginReset() self.mw.checkpoint(_("Reschedule")) if frm.asNew.isChecked(): - self.deck.sched.forgetCards(self.selectedCards()) + self.col.sched.forgetCards(self.selectedCards()) else: - self.deck.sched.reschedCards( + self.col.sched.reschedCards( self.selectedCards(), frm.min.value(), frm.max.value()) self.onSearch(reset=False) self.mw.requireReset() @@ -1088,7 +1088,7 @@ where id in %s""" % ids2str(self.selectedCards()), mod) if not sf: return import anki.find - fields = sorted(anki.find.fieldNames(self.deck, downcase=False)) + fields = sorted(anki.find.fieldNames(self.col, downcase=False)) d = QDialog(self) frm = aqt.forms.findreplace.Ui_Dialog() frm.setupUi(d) @@ -1106,7 +1106,7 @@ where id in %s""" % ids2str(self.selectedCards()), mod) self.mw.progress.start() self.model.beginReset() try: - changed = self.deck.findReplace(sf, + changed = self.col.findReplace(sf, unicode(frm.find.text()), unicode(frm.replace.text()), frm.re.isChecked(), @@ -1143,12 +1143,12 @@ where id in %s""" % ids2str(self.selectedCards()), mod) restoreGeom(win, "findDupes") fields = sorted(self.card.note.model.fieldModels, key=attrgetter("name")) # per-model data - data = self.deck.db.all(""" + data = self.col.db.all(""" select fm.id, m.name || '>' || fm.name from fieldmodels fm, models m where fm.modelId = m.id""") data.sort(key=itemgetter(1)) # all-model data - data2 = self.deck.db.all(""" + data2 = self.col.db.all(""" select fm.id, fm.name from fieldmodels fm""") byName = {} for d in data2: @@ -1187,9 +1187,9 @@ select fm.id, fm.name from fieldmodels fm""") win.show() def duplicatesReport(self, web, fmids): - self.deck.startProgress(2) - self.deck.updateProgress(_("Finding...")) - res = self.deck.findDuplicates(fmids) + self.col.startProgress(2) + self.col.updateProgress(_("Finding...")) + res = self.col.findDuplicates(fmids) t = "" t += _("Duplicate Groups: %d") % len(res) t += "

    " @@ -1202,7 +1202,7 @@ select fm.id, fm.name from fieldmodels fm""") t += "
" t += "" web.setHtml(t) - self.deck.finishProgress() + self.col.finishProgress() def dupeLinkClicked(self, link): self.form.searchEdit.setText(link.toString()) @@ -1262,7 +1262,7 @@ class GenCards(QDialog): def getSelection(self): # get cards to enable - f = self.browser.deck.getNote(self.nids[0]) + f = self.browser.col.getNote(self.nids[0]) self.model = f.model() self.items = [] for t in self.model.templates: @@ -1297,15 +1297,15 @@ class GenCards(QDialog): mw.checkpoint(_("Generate Cards")) mw.progress.start() for c, nid in enumerate(self.nids): - f = mw.deck.getNote(nid) - mw.deck.genCards(f, tplates) + f = mw.col.getNote(nid) + mw.col.genCards(f, tplates) if c % 100 == 0: mw.progress.update() if unused: - cids = mw.deck.db.list(""" + cids = mw.col.db.list(""" select id from cards where nid in %s and ord in %s""" % ( ids2str(self.nids), ids2str(unused))) - mw.deck.remCards(cids) + mw.col.remCards(cids) mw.progress.finish() mw.requireReset() self.browser.onSearch() @@ -1344,8 +1344,8 @@ class ChangeModel(QDialog): self.form.templateMap.setLayout(self.tlayout) # model chooser import aqt.modelchooser - self.oldCurrentModel = self.browser.deck.conf['currentModelId'] - self.browser.deck.conf['currentModelId'] = self.oldModel.id + self.oldCurrentModel = self.browser.col.conf['currentModelId'] + self.browser.col.conf['currentModelId'] = self.oldModel.id self.form.oldModelLabel.setText(self.oldModel.name) self.modelChooser = aqt.modelchooser.ModelChooser( self.browser.mw, self.form.modelChooserWidget, cards=False, label=False) @@ -1356,7 +1356,7 @@ class ChangeModel(QDialog): self.pauseUpdate = False def onReset(self): - self.modelChanged(self.browser.deck.currentModel()) + self.modelChanged(self.browser.col.currentModel()) def modelChanged(self, model): self.targetModel = model @@ -1446,7 +1446,7 @@ class ChangeModel(QDialog): def cleanup(self): removeHook("reset", self.onReset) removeHook("currentModelChanged", self.onReset) - self.oldCurrentModel = self.browser.deck.conf['currentModelId'] + self.oldCurrentModel = self.browser.col.conf['currentModelId'] self.modelChooser.cleanup() saveGeom(self, "changeModel") diff --git a/aqt/clayout.py b/aqt/clayout.py index 5ba4b599f..2298fde8d 100644 --- a/aqt/clayout.py +++ b/aqt/clayout.py @@ -28,8 +28,8 @@ class CardLayout(QDialog): self.note = note self.type = type self.ord = ord - self.deck = self.mw.deck - self.mm = self.mw.deck.models + self.col = self.mw.col + self.mm = self.mw.col.models self.model = note.model() self.form = aqt.forms.clayout.Ui_Dialog() self.form.setupUi(self) @@ -50,7 +50,7 @@ class CardLayout(QDialog): self.exec_() def reload(self, first=False): - self.cards = self.deck.previewCards(self.note, self.type) + self.cards = self.col.previewCards(self.note, self.type) if not self.cards: self.accept() if first: @@ -220,7 +220,7 @@ class CardLayout(QDialog): styles += "\n.cloze { font-weight: bold; color: blue; }" self.form.preview.setHtml( ('%s' % - (getBase(self.deck), c.cssClass())) + + (getBase(self.col), c.cssClass())) + "" + mungeQA(c.q(reload=True)) + self.maybeTextInput() + @@ -251,22 +251,22 @@ class CardLayout(QDialog): modified = False self.mw.startProgress() - self.deck.updateProgress(_("Applying changes...")) + self.col.updateProgress(_("Applying changes...")) reset=True if len(self.fieldOrdinalUpdatedIds) > 0: - self.deck.rebuildFieldOrdinals(self.model.id, self.fieldOrdinalUpdatedIds) + self.col.rebuildFieldOrdinals(self.model.id, self.fieldOrdinalUpdatedIds) modified = True if self.needFieldRebuild: modified = True if modified: self.note.model.setModified() - self.deck.flushMod() + self.col.flushMod() if self.noteedit and self.noteedit.onChange: self.noteedit.onChange("all") reset=False if reset: self.mw.reset() - self.deck.finishProgress() + self.col.finishProgress() QDialog.reject(self) def onHelp(self): @@ -358,7 +358,7 @@ class CardLayout(QDialog): if fld['name'] != name: self.mm.renameField(self.model, fld, name) # as the field name has changed, we have to regenerate cards - self.cards = self.deck.previewCards(self.note, self.type) + self.cards = self.col.previewCards(self.note, self.type) self.cardChanged(0) self.renderPreview() self.fillFieldList() diff --git a/aqt/config.py b/aqt/config.py deleted file mode 100644 index e606fca26..000000000 --- a/aqt/config.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright: Damien Elmes -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# User configuration handling -########################################################################## -# The majority of the config is serialized into a string, so we can access it -# easily and pickle objects like window state. A separate table keeps track of -# seen decks, so that multiple instances can update the recent decks list. - -import os, sys, time, random, cPickle -from anki.db import DB -from anki.utils import isMac - -defaultConf = { - 'confVer': 3, - 'interfaceLang': "en", - 'fullSearch': False, - 'autoplaySounds': True, - 'searchHistory': [], - 'checkForUpdates': True, # ui? - 'created': time.time(), - 'deleteMedia': False, - 'documentDir': u"", - 'dropboxPublicFolder': u"", - 'editFontFamily': 'Arial', - 'editFontSize': 12, - 'editLineSize': 20, - 'iconSize': 32, - 'id': random.randrange(0, 2**63), - 'lastMsg': -1, - 'loadLastDeck': False, - 'mainWindowGeom': None, - 'mainWindowState': None, - 'mediaLocation': "", - 'numBackups': 30, - 'preserveKeyboard': True, - 'proxyHost': '', - 'proxyPass': '', - 'proxyPort': 8080, - 'proxyUser': '', - 'recentColours': ["#000000", "#0000ff"], - 'showProgress': True, - 'showToolbar': True, - 'centerQA': True, - 'stripHTML': True, - 'suppressEstimates': False, - 'suppressUpdate': False, - 'syncDisableWhenMoved': True, - 'syncOnProgramOpen': True, - 'syncPassword': "", - 'syncUsername': "", -} - -class Config(object): - configDbName = "ankiprefs.db" - - def __init__(self, confDir): - self.confDir = confDir - self._conf = {} - if isMac and ( - self.confDir == os.path.expanduser("~/.anki")): - self.confDir = os.path.expanduser( - "~/Library/Application Support/Anki") - self._addAnkiDirs() - self.load() - - # dict interface - def get(self, *args): - return self._conf.get(*args) - def __getitem__(self, key): - return self._conf[key] - def __setitem__(self, key, val): - self._conf[key] = val - def __contains__(self, key): - return self._conf.__contains__(key) - - # load/save - def load(self): - path = self._dbPath() - self.db = DB(path, text=str) - self.db.executescript(""" -create table if not exists decks (path text primary key); -create table if not exists config (conf text not null); -""") - conf = self.db.scalar("select conf from config") - if conf: - self._conf.update(cPickle.loads(conf)) - else: - self._conf.update(defaultConf) - # ensure there's something to update - self.db.execute("insert or ignore into config values ('')") - self._addDefaults() - - def save(self): - self.db.execute("update config set conf = ?", - cPickle.dumps(self._conf)) - self.db.commit() - - # recent deck support - def recentDecks(self): - "Return a list of paths to remembered decks." - # have to convert to unicode manually because of the text factory - return [unicode(d[0], 'utf8') for d in - self.db.execute("select path from decks")] - - def addRecentDeck(self, path): - "Add PATH to the list of recent decks if not already. Must be unicode." - self.db.execute("insert or ignore into decks values (?)", - path.encode("utf-8")) - - def delRecentDeck(self, path): - "Remove PATH from the list if it exists. Must be unicode." - self.db.execute("delete from decks where path = ?", - path.encode("utf-8")) - - # helpers - def _addDefaults(self): - if self.get('confVer') >= defaultConf['confVer']: - return - for (k,v) in defaultConf.items(): - if k not in self: - self[k] = v - - def _dbPath(self): - return os.path.join(self.confDir, self.configDbName) - - def _addAnkiDirs(self): - base = self.confDir - for x in (base, - os.path.join(base, "addons"), - os.path.join(base, "backups")): - try: - os.mkdir(x) - except: - pass diff --git a/aqt/deckopts.py b/aqt/deckopts.py index 95745e61b..5c12573b8 100644 --- a/aqt/deckopts.py +++ b/aqt/deckopts.py @@ -6,13 +6,13 @@ import sys, re import aqt from aqt.utils import maybeHideClose -class DeckOptions(QDialog): +class ColOptions(QDialog): def __init__(self, mw): QDialog.__init__(self, mw, Qt.Window) self.mw = mw - self.d = mw.deck - self.form = aqt.forms.deckopts.Ui_Dialog() + self.d = mw.col + self.form = aqt.forms.colopts.Ui_Dialog() self.form.setupUi(self) self.setup() self.exec_() @@ -28,7 +28,7 @@ class DeckOptions(QDialog): self.form.mediaURL.setText(self.d.conf['mediaURL']) def helpRequested(self): - aqt.openHelp("DeckOptions") + aqt.openHelp("ColOptions") def reject(self): needSync = False @@ -49,4 +49,4 @@ class DeckOptions(QDialog): self.d.conf['mediaURL'] = url QDialog.reject(self) if needSync: - aqt.mw.syncDeck(interactive=-1) + aqt.mw.syncCol(interactive=-1) diff --git a/aqt/editcurrent.py b/aqt/editcurrent.py index ff33b65cf..f02c7771c 100644 --- a/aqt/editcurrent.py +++ b/aqt/editcurrent.py @@ -39,8 +39,8 @@ class EditCurrent(QDialog): r = self.mw.reviewer r.card.load() r.keep = True - # we don't need to reset the deck, but there may be new groups - self.mw.deck.sched._resetConf() + # we don't need to reset the col, but there may be new groups + self.mw.col.sched._resetConf() self.mw.moveToState("review") saveGeom(self, "editcurrent") self.close() diff --git a/aqt/editor.py b/aqt/editor.py index 62096968a..c21058b43 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -371,7 +371,7 @@ class Editor(object): self.note = note # change timer if self.note: - self.web.setHtml(_html % (getBase(self.mw.deck), anki.js.all, + self.web.setHtml(_html % (getBase(self.mw.col), anki.js.all, _("Show Duplicates")), loadCB=self._loadFinished) self.updateTagsAndGroup() @@ -490,28 +490,28 @@ class Editor(object): self.outerLayout.addWidget(g) def updateTagsAndGroup(self): - if self.tags.deck != self.mw.deck: - self.tags.setDeck(self.mw.deck) + if self.tags.col != self.mw.col: + self.tags.setCol(self.mw.col) if self.addMode: - self.group.setDeck(self.mw.deck) + self.group.setCol(self.mw.col) self.tags.setText(self.note.stringTags().strip()) if getattr(self.note, 'gid', None): gid = self.note.gid else: gid = self.note.model().conf['gid'] - self.group.setText(self.mw.deck.groups.name(gid)) + self.group.setText(self.mw.col.groups.name(gid)) def saveTagsAndGroup(self): if not self.note: return - self.note.tags = self.mw.deck.tags.split(unicode(self.tags.text())) + self.note.tags = self.mw.col.tags.split(unicode(self.tags.text())) if self.addMode: # save group and tags to model - self.note.gid = self.mw.deck.groups.id(unicode(self.group.text())) + self.note.gid = self.mw.col.groups.id(unicode(self.group.text())) m = self.note.model() m['gid'] = self.note.gid m['tags'] = self.note.tags - self.mw.deck.models.save(m) + self.mw.col.models.save(m) self.note.flush() runHook("tagsAndGroupUpdated", self.note) @@ -696,7 +696,7 @@ class Editor(object): def _addMedia(self, path, canDelete=False): "Add to media folder and return basename." # copy to media folder - name = self.mw.deck.media.addFile(path) + name = self.mw.col.media.addFile(path) # remove original? if canDelete and self.mw.config['deleteMedia']: if os.path.abspath(name) != os.path.abspath(path): diff --git a/aqt/getshared.py b/aqt/getshared.py index 9b9a0b920..083130cc1 100644 --- a/aqt/getshared.py +++ b/aqt/getshared.py @@ -40,7 +40,7 @@ Error was:
%s
""") self.setupTable() self.onChangeType(type) if type == 0: - self.setWindowTitle(_("Download Shared Deck")) + self.setWindowTitle(_("Download Shared Col")) else: self.setWindowTitle(_("Download Shared Plugin")) if self.ok: @@ -221,7 +221,7 @@ Error was:
%s
""") tit = re.sub("[^][A-Za-z0-9 ()\-]", "", tit) tit = tit[0:40] if self.type == 0: - # deck + # col dd = self.parent.config['documentDir'] p = os.path.join(dd, tit + ".anki") if os.path.exists(p): @@ -237,7 +237,7 @@ Error was:
%s
""") pass open(os.path.join(dd, tit + ".media", os.path.basename(l)),"wb").write(z.read(l)) - self.parent.loadDeck(dpath) + self.parent.loadCol(dpath) else: pd = self.parent.pluginsFolder() if z: diff --git a/aqt/groupconf.py b/aqt/groupconf.py index 03fe35a51..304e8d09a 100644 --- a/aqt/groupconf.py +++ b/aqt/groupconf.py @@ -17,7 +17,7 @@ class GroupConf(QDialog): self.gcid = gcid self.form = aqt.forms.groupconf.Ui_Dialog() self.form.setupUi(self) - (self.name, self.conf) = self.mw.deck.db.first( + (self.name, self.conf) = self.mw.col.db.first( "select name, conf from gconf where id = ?", self.gcid) self.conf = simplejson.loads(self.conf) self.setWindowTitle(self.name) @@ -131,7 +131,7 @@ class GroupConf(QDialog): c['maxTaken'] = f.maxTaken.value() # update db self.mw.checkpoint(_("Group Options")) - self.mw.deck.db.execute( + self.mw.col.db.execute( "update gconf set conf = ? where id = ?", simplejson.dumps(self.conf), self.gcid) @@ -147,7 +147,7 @@ class GroupConfSelector(QDialog): self.form.setupUi(self) self.connect(self.form.list, SIGNAL("itemChanged(QListWidgetItem*)"), self.onRename) - self.defaultId = self.mw.deck.db.scalar( + self.defaultId = self.mw.col.db.scalar( "select gcid from groups where id = ?", self.gids[0]) self.reload() self.addButtons() @@ -155,7 +155,7 @@ class GroupConfSelector(QDialog): def accept(self): # save - self.mw.deck.db.execute( + self.mw.col.db.execute( "update groups set gcid = ? where id in "+ids2str(self.gids), self.gcid()) QDialog.accept(self) @@ -164,7 +164,7 @@ class GroupConfSelector(QDialog): self.accept() def reload(self): - self.confs = self.mw.deck.groupConfs() + self.confs = self.mw.col.groupConfs() self.form.list.clear() deflt = None for c in self.confs: @@ -190,7 +190,7 @@ class GroupConfSelector(QDialog): def onRename(self, item): gcid = self.gcid() - self.mw.deck.db.execute("update gconf set name = ? where id = ?", + self.mw.col.db.execute("update gconf set name = ? where id = ?", unicode(item.text()), gcid) def onEdit(self): @@ -198,10 +198,10 @@ class GroupConfSelector(QDialog): def onCopy(self): gcid = self.gcid() - gc = list(self.mw.deck.db.first("select * from gconf where id = ?", gcid)) - gc[0] = self.mw.deck.nextID("gcid") + gc = list(self.mw.col.db.first("select * from gconf where id = ?", gcid)) + gc[0] = self.mw.col.nextID("gcid") gc[2] = _("%s copy")%gc[2] - self.mw.deck.db.execute("insert into gconf values (?,?,?,?)", *gc) + self.mw.col.db.execute("insert into gconf values (?,?,?,?)", *gc) self.reload() def onDelete(self): @@ -210,8 +210,8 @@ class GroupConfSelector(QDialog): showInfo(_("The default configuration can't be removed."), self) else: self.mw.checkpoint(_("Delete Group Config")) - self.mw.deck.db.execute( + self.mw.col.db.execute( "update groups set gcid = 1 where gcid = ?", gcid) - self.mw.deck.db.execute( + self.mw.col.db.execute( "delete from gconf where id = ?", gcid) self.reload() diff --git a/aqt/groups.py b/aqt/groups.py index b3e4bc6b0..d34dd75de 100644 --- a/aqt/groups.py +++ b/aqt/groups.py @@ -27,7 +27,7 @@ class Groups(QDialog): def reload(self): self.mw.progress.start() - grps = self.mw.deck.sched.groupCountTree() + grps = self.mw.col.sched.groupCountTree() self.mw.progress.finish() self.groupMap = {} self.fullNames = {} @@ -107,7 +107,7 @@ class Groups(QDialog): err.append( _("The default group can't be deleted.")) continue - self.mw.deck.delGroup(gid) + self.mw.col.delGroup(gid) self.reload() if err: showInfo("\n".join(err)) @@ -127,7 +127,7 @@ class Groups(QDialog): gid = self.groupMap[cold] cnew = self.fullNames[cold].replace(old, txt) if gid: - self.mw.deck.db.execute( + self.mw.col.db.execute( "update groups set name = ? where id = ?", cnew, gid) for i in range(item.childCount()): @@ -167,15 +167,15 @@ class Groups(QDialog): if len(gids) == self.gidCount: # all enabled is same as empty gids = [] - # if gids != self.mw.deck.conf['groups']: - # self.mw.deck.conf['groups'] = gids + # if gids != self.mw.col.conf['groups']: + # self.mw.col.conf['groups'] = gids # self.mw.reset() QDialog.accept(self) def _makeItems(self, grps): self.gidCount = 0 on = {} - a = self.mw.deck.groups.active() + a = self.mw.col.groups.active() if not a: on = None else: @@ -196,7 +196,7 @@ class Groups(QDialog): branch.setCheckState(COLCHECK, Qt.Unchecked) branch.setText(COLNAME, grp[0]) if gid: - branch.setText(COLOPTS, self.mw.deck.groups.name(gid)) + branch.setText(COLOPTS, self.mw.col.groups.name(gid)) branch.setText(COLCOUNT, "") branch.setText(COLDUE, str(grp[2])) branch.setText(COLNEW, str(grp[3])) diff --git a/aqt/importing.py b/aqt/importing.py index 89bed3ec1..99d530420 100644 --- a/aqt/importing.py +++ b/aqt/importing.py @@ -61,7 +61,7 @@ class UpdateMap(QDialog): for i in range(numFields): self.dialog.fileField.addItem("Field %d" % (i+1)) for m in fieldModels: - self.dialog.deckField.addItem(m.name) + self.dialog.colField.addItem(m.name) self.exec_() def helpRequested(self): @@ -70,7 +70,7 @@ class UpdateMap(QDialog): def accept(self): self.updateKey = ( self.dialog.fileField.currentIndex(), - self.fieldModels[self.dialog.deckField.currentIndex()].id) + self.fieldModels[self.dialog.colField.currentIndex()].id) QDialog.accept(self) class ImportDialog(QDialog): @@ -95,10 +95,10 @@ class ImportDialog(QDialog): self.exec_() def setupOptions(self): - self.model = self.parent.deck.currentModel + self.model = self.parent.col.currentModel self.modelChooser = ui.modelchooser.ModelChooser(self, self.parent, - self.parent.deck, + self.parent.col, self.modelChanged) self.dialog.modelArea.setLayout(self.modelChooser) self.connect(self.dialog.importButton, SIGNAL("clicked()"), @@ -197,7 +197,7 @@ you can enter it here. Use \\t to represent tab."""), self.importer.mapping = self.mapping try: n = _("Import") - self.parent.deck.setUndoStart(n) + self.parent.col.setUndoStart(n) try: self.importer.doImport() except ImportFormatError, e: @@ -211,8 +211,8 @@ you can enter it here. Use \\t to represent tab."""), self.dialog.status.setText(msg) return finally: - self.parent.deck.finishProgress() - self.parent.deck.setUndoEnd(n) + self.parent.col.finishProgress() + self.parent.col.setUndoEnd(n) txt = ( _("Importing complete. %(num)d notes imported from %(file)s.\n") % {"num": self.importer.total, "file": os.path.basename(self.file)}) @@ -223,7 +223,7 @@ you can enter it here. Use \\t to represent tab."""), self.dialog.status.setText(txt) self.file = None self.maybePreview() - self.parent.deck.db.flush() + self.parent.col.db.flush() self.parent.reset() self.modelChooser.deinit() @@ -242,7 +242,7 @@ you can enter it here. Use \\t to represent tab."""), def showMapping(self, keepMapping=False, hook=None): # first, check that we can read the file try: - self.importer = self.importerFunc(self.parent.deck, self.file) + self.importer = self.importerFunc(self.parent.col, self.file) if hook: hook() if not keepMapping: diff --git a/aqt/main.py b/aqt/main.py index 5966aa0b1..23c464abc 100755 --- a/aqt/main.py +++ b/aqt/main.py @@ -9,7 +9,7 @@ from operator import itemgetter from aqt.qt import * QtConfig = pyqtconfig.Configuration() -from anki import Deck +from anki import Collection from anki.sound import playFromText, clearAudioQueue, stripSounds from anki.utils import stripHTML, checksum, isWin, isMac from anki.hooks import runHook, addHook, removeHook @@ -27,40 +27,28 @@ config = aqt.config ## models remembering the previous group class AnkiQt(QMainWindow): - def __init__(self, app, config, args, splash): + def __init__(self, app, config, args): QMainWindow.__init__(self) aqt.mw = self - self.splash = splash self.app = app self.config = config try: # initialize everything self.setup() - splash.update() # load plugins self.setupAddons() - splash.update() # show main window - splash.finish(self) self.show() # raise window for osx self.activateWindow() self.raise_() - # sync on program open? - # if self.config['syncOnProgramOpen']: - # if self.syncDeck(interactive=False): - # return - - # delay load so deck errors don't cause program to close - self.progress.timer(10, lambda a=args: \ - self.maybeLoadLastDeck(a), - False) + # except: showInfo("Error during startup:\n%s" % traceback.format_exc()) sys.exit(1) def setup(self): - self.deck = None + self.col = None self.state = None self.setupThreads() self.setupLang() @@ -80,7 +68,7 @@ class AnkiQt(QMainWindow): self.setupSchema() self.updateTitleBar() # screens - self.setupDeckBrowser() + #self.setupColBrowser() self.setupOverview() self.setupReviewer() @@ -98,16 +86,16 @@ class AnkiQt(QMainWindow): def _deckBrowserState(self, oldState): # shouldn't call this directly; call close - self.disableDeckMenuItems() - self.closeAllDeckWindows() + self.disableColMenuItems() + self.closeAllColWindows() self.deckBrowser.show() - def _deckLoadingState(self, oldState): - "Run once, when deck is loaded." - self.enableDeckMenuItems() + def _colLoadingState(self, oldState): + "Run once, when col is loaded." + self.enableColMenuItems() # ensure cwd is set if media dir exists - self.deck.media.dir() - runHook("deckLoading", self.deck) + self.col.media.dir() + runHook("colLoading", self.col) self.moveToState("overview") def _overviewState(self, oldState): @@ -132,8 +120,8 @@ class AnkiQt(QMainWindow): def reset(self, type="all", *args): "Called for non-trivial edits. Rebuilds queue and updates UI." - if self.deck: - self.deck.reset() + if self.col: + self.col.reset() runHook("reset") self.moveToState(self.state) @@ -216,7 +204,7 @@ title="%s">%s''' % ( else: self.resize(500, 400) - def closeAllDeckWindows(self): + def closeAllColWindows(self): aqt.dialogs.closeAll() # Components @@ -281,14 +269,14 @@ title="%s">%s''' % ( self.progress.setupDB(db) self.progress.start(label=_("Upgrading. Please be patient...")) - # Deck loading + # Collection loading ########################################################################## def loadDeck(self, deckPath, showErrors=True): "Load a deck and update the user interface." self.upgrading = False try: - self.deck = Deck(deckPath, queue=False) + self.col = Deck(deckPath, queue=False) except Exception, e: if not showErrors: return 0 @@ -304,117 +292,13 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") finally: # we may have a progress window open if we were upgrading self.progress.finish() - self.config.addRecentDeck(self.deck.path) - self.setupMedia(self.deck) + self.config.addRecentDeck(self.col.path) + self.setupMedia(self.col) if not self.upgrading: - self.progress.setupDB(self.deck.db) + self.progress.setupDB(self.col.db) self.moveToState("deckLoading") return True - def onOpen(self): - self.raiseMain() - filter = _("Deck files (*.anki)") - if self.deck: - dir = os.path.dirname(self.deck.path) - else: - dir = self.config['documentDir'] - def accept(file): - ret = self.loadDeck(file) - if not ret: - showWarning(_("Unable to load file.")) - self.deck = None - getFile(self, _("Open deck"), accept, filter, dir) - - 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()) - if os.path.exists(f): - return self.loadDeck(f) - # try recent deck paths - for path in self.config.recentDecks(): - r = self.loadDeck(path, showErrors=False) - if r: - return r - self.moveToState("deckBrowser") - - # Open recent - ########################################################################## - - 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.recentDecks() - if not self.deck or self.deck.path != x and - os.path.exists(x)]) - self.switchDecks.sort() - combo.addItems([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 deck - ########################################################################## - - def onNew(self, path=None, prompt=None): - self.raiseMain() - self.close() - register = not path - bad = ":/\\" - name = _("mydeck") - if not path: - if not prompt: - prompt = _("Please give your deck a name:") - while 1: - name = getOnlyText( - prompt, default=name, title=_("New Deck")) - if not name: - self.moveToState("deckBrowser") - return - found = False - for c in bad: - if c in name: - 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.config['documentDir'], name) - if os.path.exists(path): - if askUser(_("That deck already exists. Overwrite?"), - defaultno=True): - os.unlink(path) - else: - self.moveToState("deckBrowser") - return - self.loadDeck(path) - if register: - self.config.addRecentDeck(self.deck.path) - # Closing ########################################################################## @@ -428,53 +312,19 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") def close(self, showBrowser=True): "Close current deck." - if not self.deck: + if not self.col: return # if we were cramming, restore the standard scheduler - if self.deck.stdSched(): - self.deck.reset() + if self.col.stdSched(): + self.col.reset() runHook("deckClosing") print "focusOut() should be handled with deckClosing now" self.closeAllDeckWindows() - self.deck.close() - self.deck = None + self.col.close() + self.col = None if showBrowser: self.moveToState("deckBrowser") - # Downloading - ########################################################################## - - def onOpenOnline(self): - return showInfo("not yet implemented") - self.raiseMain() - self.ensureSyncParams() - self.close() - # we need a disk-backed file for syncing - path = namedtmp(u"untitled.anki") - self.onNew(path=path) - # ensure all changes come to us - self.deck.modified = 0 - self.deck.db.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): - return showInfo("not yet implemented") - self.raiseMain() - aqt.getshared.GetShared(self, 0) - self.browserLastRefreshed = 0 - - def onGetSharedPlugin(self): - return showInfo("not yet implemented") - self.raiseMain() - aqt.getshared.GetShared(self, 1) - # Syncing ########################################################################## @@ -496,33 +346,6 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") def setupStyle(self): applyStyles(self) - # Renaming - ########################################################################## - - def onRename(self): - "Rename deck." - dir = os.path.dirname(self.deck.path) - path = QFileDialog.getSaveFileName(self, _("Rename Deck"), - dir, - _("Deck files (*.anki)"), - options=QFileDialog.DontConfirmOverwrite) - path = unicode(path) - if not path: - return - if not path.lower().endswith(".anki"): - path += ".anki" - if os.path.abspath(path) == os.path.abspath(self.deck.path): - return - if os.path.exists(path): - if not askUser( - "Selected file exists. Overwrite it?"): - return - old = self.deck.path - self.deck.rename(path) - self.config.addRecentDeck(path) - self.config.delRecentDeck(old) - return path - # App exit ########################################################################## @@ -556,6 +379,8 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") ########################################################################## def setupToolbar(self): + print "setup toolbar" + return frm = self.form tb = frm.toolBar tb.addAction(frm.actionAddcards) @@ -615,31 +440,31 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") def onSuspend(self): self.checkpoint(_("Suspend")) - self.deck.sched.suspendCards([self.reviewer.card.id]) + self.col.sched.suspendCards([self.reviewer.card.id]) self.reviewer.nextCard() def onDelete(self): self.checkpoint(_("Delete")) - self.deck.remCards([self.reviewer.card.id]) + self.col.remCards([self.reviewer.card.id]) self.reviewer.nextCard() def onBuryNote(self): self.checkpoint(_("Bury")) - self.deck.sched.buryNote(self.reviewer.card.nid) + self.col.sched.buryNote(self.reviewer.card.nid) self.reviewer.nextCard() # Undo & autosave ########################################################################## def onUndo(self): - self.deck.undo() + self.col.undo() self.reset() self.maybeEnableUndo() def maybeEnableUndo(self): - if self.deck and self.deck.undoName(): + if self.col and self.col.undoName(): self.form.actionUndo.setText(_("Undo %s") % - self.deck.undoName()) + self.col.undoName()) self.form.actionUndo.setEnabled(True) runHook("undoState", True) else: @@ -647,11 +472,11 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") runHook("undoState", False) def checkpoint(self, name): - self.deck.save(name) + self.col.save(name) self.maybeEnableUndo() def autosave(self): - self.deck.autosave() + self.col.autosave() self.maybeEnableUndo() # Other menu operations @@ -660,7 +485,7 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") def onAddCard(self): aqt.dialogs.open("AddCards", self) - def onEditDeck(self): + def onBrowse(self): aqt.dialogs.open("Browser", self) def onEditCurrent(self): @@ -720,16 +545,16 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") def onImport(self): return showInfo("not yet implemented") - if self.deck is None: + if self.col is None: self.onNew(prompt=_("""\ Importing copies cards to the current deck, and since you have no deck open, we need to create a new deck first. Please choose a new deck name:""")) - if not self.deck: + if not self.col: return - if self.deck.path: + if self.col.path: aqt.importing.ImportDialog(self) def onExport(self): @@ -779,7 +604,6 @@ Please choose a new deck name:""")) "Close", "Addcards", "Editdeck", - "DeckProperties", "Undo", "Export", "Stats", @@ -793,20 +617,12 @@ Please choose a new deck name:""")) def setupMenus(self): m = self.form 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.actionRename, s, self.onRename) - self.connect(m.actionClose, s, self.onClose) + #self.connect(m.actionDownloadSharedPlugin, s, self.onGetSharedPlugin) self.connect(m.actionExit, s, self, SLOT("close()")) - self.connect(m.actionSyncdeck, s, self.onSync) - self.connect(m.actionDeckProperties, s, self.onDeckOpts) + self.connect(m.actionSync, s, self.onSync) self.connect(m.actionModels, s, self.onModels) - self.connect(m.actionAddcards, s, self.onAddCard) - self.connect(m.actionEditdeck, s, self.onEditDeck) + self.connect(m.actionAdd, s, self.onAddCard) + self.connect(m.actionBrowse, s, self.onBrowse) self.connect(m.actionEditCurrent, s, self.onEditCurrent) self.connect(m.actionPreferences, s, self.onPrefs) self.connect(m.actionStats, s, self.onStats) @@ -822,8 +638,6 @@ Please choose a new deck name:""")) self.connect(m.actionUndo, s, self.onUndo) self.connect(m.actionFullDatabaseCheck, s, self.onCheckDB) self.connect(m.actionCheckMediaDatabase, s, self.onCheckMediaDB) - self.connect(m.actionDownloadMissingMedia, s, self.onDownloadMissingMedia) - self.connect(m.actionLocalizeMedia, s, self.onLocalizeMedia) self.connect(m.actionStudyOptions, s, self.onStudyOptions) self.connect(m.actionOverview, s, self.onOverview) self.connect(m.actionGroups, s, self.onGroups) @@ -833,7 +647,7 @@ Please choose a new deck name:""")) def enableDeckMenuItems(self, enabled=True): "setEnabled deck-related items." - for item in self.deckRelatedMenuItems: + for item in self.colRelatedMenuItems: getattr(self.form, "action" + item).setEnabled(enabled) self.form.menuAdvanced.setEnabled(enabled) if not enabled: @@ -923,111 +737,111 @@ haven't been synced here yet. Continue?""")) # Media locations ########################################################################## - def setupMedia(self, deck): - print "setup media" - return - 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 setupMedia(self, deck): +# print "setup media" +# return +# 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 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 aqt.dropbox as db - p = db.getPath() - except: - if isWin: - s = QSettings(QSettings.UserScope, "Microsoft", "Windows") - s.beginGroup("CurrentVersion/Explorer/Shell Folders") - p = os.path.join(s.value("Personal"), "My Dropbox") - else: - p = os.path.expanduser("~/Dropbox") - return p +# def dropboxFolder(self): +# try: +# import aqt.dropbox as db +# p = db.getPath() +# except: +# if isWin: +# s = QSettings(QSettings.UserScope, "Microsoft", "Windows") +# s.beginGroup("CurrentVersion/Explorer/Shell Folders") +# p = os.path.join(s.value("Personal"), "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 - 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 = 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: - 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()) + "/") +# 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 +# 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 = 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: +# 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 ########################################################################## @@ -1041,7 +855,7 @@ Any changes on the server since your last sync will be lost.

This operation is not undoable. Proceed?""")): return self.progress.start(immediate=True) - ret = self.deck.fixIntegrity() + ret = self.col.fixIntegrity() self.progress.finish() showText(ret) self.reset() @@ -1074,7 +888,7 @@ doubt.""")) else: return self.progress.start(immediate=True) - (nohave, unused) = self.deck.media.check(delete) + (nohave, unused) = self.col.media.check(delete) self.progress.finish() # generate report report = "" @@ -1095,36 +909,6 @@ doubt.""")) report = _("No unused or missing files found.") showText(report, parent=self, type="text") - def onDownloadMissingMedia(self): - res = downloadMissing(self.deck) - if res is None: - 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] - showInfo(msg) - - def onLocalizeMedia(self): - if not 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]) - aqt.utils.showText(msg, parent=self, type="text") - # System specific code ########################################################################## @@ -1133,7 +917,7 @@ It can take a long time. Proceed?""")): addHook("macLoadEvent", self.onMacLoad) if isMac: qt_mac_set_menubar_icons(False) - self.setUnifiedTitleAndToolBarOnMac(self.config['showToolbar']) + #self.setUnifiedTitleAndToolBarOnMac(self.config['showToolbar']) # mac users expect a minimize option self.minimizeShortcut = QShortcut("Ctrl+m", self) self.connect(self.minimizeShortcut, SIGNAL("activated()"), diff --git a/aqt/profiles.py b/aqt/profiles.py new file mode 100644 index 000000000..a36933168 --- /dev/null +++ b/aqt/profiles.py @@ -0,0 +1,151 @@ +# Copyright: Damien Elmes +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +# Profile handling +########################################################################## +# - Saves in pickles rather than json to easily store Qt window state. +# - 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, re +from anki.db import DB +from anki.utils import isMac, isWin, intTime + +metaConf = dict( + ver=0, + updates=True, + created=intTime(), + id=random.randrange(0, 2**63), + lastMsg=-1, + suppressUpdate=False, +) + +profileConf = dict( + # profile + key=None, + mainWindowGeom=None, + mainWindowState=None, + numBackups=30, + lang="en", + + # editing + fullSearch=False, + searchHistory=[], + recentColours=["#000000", "#0000ff"], + stripHTML=True, + editFontFamily='Arial', + editFontSize=12, + editLineSize=20, + deleteMedia=False, + preserveKeyboard=True, + + # reviewing + autoplay=True, + showDueTimes=True, + showProgress=True, + + # syncing + syncKey=None, + proxyHost='', + proxyPass='', + proxyPort=8080, + proxyUser='', +) + +class ProfileManager(object): + + def __init__(self, base=None, profile=None): + self.name = None + # instantiate base folder + if not base: + base = self._defaultBase() + if not os.path.exists(base): + try: + os.makedirs(base) + except: + QMessageBox.critical( + None, "Error", """\ +Anki can't write to the harddisk. Please see the \ +documentation for information on using a flash drive.""") + raise + self.base = base + # load database and cmdline-provided profile + self._load() + if profile: + try: + self.load(profile) + except TypeError: + raise Exception("Provided profile does not exist.") + + # Profile load/save + ###################################################################### + + def profiles(self): + return [x for x in self.db.scalar("select name from profiles") + if x != "_global"] + + def load(self, name): + self.name = name + self.idself.profile = cPickle.loads( + self.db.scalar("select oid, data from profiles where name = ?", name)) + + def save(self): + sql = "update profiles set data = ? where name = ?" + self.db.execute(sql, cPickle.dumps(self.profile), self.name) + self.db.execute(sql, cPickle.dumps(self.meta, "_global")) + + def create(self, name): + assert re.match("^[A-Za-z0-9 ]+$", name) + self.db.execute("insert into profiles values (?, ?)", + name, cPickle.dumps(profileConf)) + + # Folder handling + ###################################################################### + + def profileFolder(self): + return self._ensureExists(os.path.join(self.base, self.name)) + + def addonFolder(self): + return self._ensureExists(os.path.join(self.base, "addons")) + + def backupFolder(self): + return self._ensureExists( + os.path.join(self.profileFolder(), "backups")) + + def collectionPath(self): + return os.path.join(self.profileFolder(), "collection.anki2") + + # Helpers + ###################################################################### + + def _ensureExists(self, path): + if not exists(path): + os.makedirs(path) + return path + + def _defaultBase(self): + if isWin: + s = QSettings(QSettings.UserScope, "Microsoft", "Windows") + s.beginGroup("CurrentVersion/Explorer/Shell Folders") + d = s.value("Personal") + return os.path.join(d, "Anki") + elif isMac: + return os.path.expanduser("~/Documents/Anki") + else: + return os.path.expanduser("~/Anki") + + def _load(self): + path = os.path.join(self.base, "prefs.db") + self.db = DB(path, text=str) + self.db.execute(""" +create table if not exists profiles +(name text primary key, data text not null);""") + try: + self.meta = self.loadProfile("_global") + except: + # create a default global profile + self.meta = metaConf.copy() + self.db.execute("insert into profiles values ('_global', ?)", + cPickle.dumps(metaConf)) + # and save a default user profile for later (commits) + self.create("User 1") diff --git a/designer/browser.ui b/designer/browser.ui index 2e752608b..710369ef1 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -6,8 +6,8 @@ 0 0 - 436 - 333 + 612 + 455 @@ -185,7 +185,7 @@ 0 0 - 436 + 612 22 @@ -195,9 +195,8 @@ - - + @@ -206,7 +205,7 @@ Cards - + @@ -221,7 +220,7 @@ - + @@ -233,12 +232,11 @@ - + - Facts + Notes - @@ -249,10 +247,10 @@ - + - + @@ -263,10 +261,13 @@ - 32 - 32 + 24 + 24 + + Qt::ToolButtonIconOnly + TopToolBarArea @@ -274,9 +275,9 @@ false - - + + @@ -315,15 +316,6 @@ &Delete Tags... - - - - :/icons/Anki_Card.png:/icons/Anki_Card.png - - - &Generate Cards... - - @@ -373,7 +365,7 @@ Ctrl+F - + :/icons/Anki_Fact.png:/icons/Anki_Fact.png @@ -433,15 +425,19 @@ Ctrl+Shift+M - + - Select &Facts + Select &Notes Ctrl+Shift+A + + + :/icons/edit-find-replace.png:/icons/edit-find-replace.png + Find and Re&place... @@ -554,17 +550,21 @@ + + + :/icons/edit-find 2.png:/icons/edit-find 2.png + Find &Duplicates... - + - :/icons/stock_group.png:/icons/stock_group.png + :/icons/graphite_smooth_folder_noncommercial.png:/icons/graphite_smooth_folder_noncommercial.png - Set Group... + Move to Deck... Ctrl+G @@ -579,7 +579,7 @@ Reposition... - + :/icons/editdelete.png:/icons/editdelete.png diff --git a/designer/deckopts.ui b/designer/colopts.ui similarity index 100% rename from designer/deckopts.ui rename to designer/colopts.ui diff --git a/designer/icons.qrc b/designer/icons.qrc index 6c0150065..cc751f34d 100644 --- a/designer/icons.qrc +++ b/designer/icons.qrc @@ -1,5 +1,9 @@ + icons/edit-find 2.png + icons/edit-find-replace.png + icons/graphite_smooth_folder_noncommercial.png + icons/user-identity.png icons/layout.png icons/generate_07.png icons/view-sort-descending.png diff --git a/designer/icons/edit-find 2.png b/designer/icons/edit-find 2.png new file mode 100644 index 0000000000000000000000000000000000000000..64a1e28d3afe98367cfe6e0dc06e617fd3045772 GIT binary patch literal 1631 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 literal 0 HcmV?d00001 diff --git a/designer/icons/graphite_smooth_folder_noncommercial.png b/designer/icons/graphite_smooth_folder_noncommercial.png new file mode 100644 index 0000000000000000000000000000000000000000..49ca50d85bd099468d7b021a34db83ce0c37a7bf GIT binary patch literal 2087 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 - + @@ -75,7 +75,7 @@ fileField - deckField + colField buttonBox diff --git a/designer/main.ui b/designer/main.ui index ebdb5de9b..decb72d08 100644 --- a/designer/main.ui +++ b/designer/main.ui @@ -58,31 +58,25 @@ - + - + - + - + &File - - - - - - + - - + - + @@ -90,24 +84,15 @@ &Tools - - - Ad&vanced - - - - - - - - + + - - + + @@ -141,12 +126,12 @@ - + - + @@ -157,26 +142,6 @@ true - - - true - - - Qt::Horizontal - - - - 32 - 32 - - - - TopToolBarArea - - - false - - @@ -189,67 +154,7 @@ Ctrl+Q - - - - :/icons/document-new.png:/icons/document-new.png - - - &New - - - Ctrl+N - - - - - - :/icons/document-open.png:/icons/document-open.png - - - &Open... - - - Ctrl+O - - - - - - :/icons/view_text.png:/icons/view_text.png - - - &Close Deck - - - - - - Ctrl+W - - - Qt::ApplicationShortcut - - - - - - :/icons/document-save.png:/icons/document-save.png - - - &Save - - - Save this deck now - - - Ctrl+S - - - Qt::ApplicationShortcut - - - + :/icons/multisynk.png:/icons/multisynk.png @@ -264,13 +169,13 @@ Ctrl+S - + :/icons/list-add.png:/icons/list-add.png - &Add... + &Add Note... @@ -279,7 +184,7 @@ A - + :/icons/find.png:/icons/find.png @@ -339,13 +244,13 @@ Shift+C - + :/icons/contents.png:/icons/contents.png - &Deck Options... + &Col Options... Customize models, syncing and scheduling @@ -360,7 +265,7 @@ &Import... - Import cards into the current deck + Import cards into collection Ctrl+I @@ -390,7 +295,7 @@ Expor&t... - Save cards in a new deck or text file for sharing with others + Export data to a text file or deck Ctrl+T @@ -405,7 +310,7 @@ :/icons/rating.png:/icons/rating.png - &Mark Fact + &Mark Note @@ -462,25 +367,13 @@ Ctrl+Z - - - - :/icons/document-save-as.png:/icons/document-save-as.png - - - Rena&me... - - - Save this deck, giving it a new name - - :/icons/text-speak.png:/icons/text-speak.png - Check &Media Database... + Check &Media... Check the files in the media directory @@ -507,7 +400,7 @@ :/icons/edit-rename.png:/icons/edit-rename.png - &Edit Current... + &Edit Note... @@ -545,37 +438,7 @@ :/icons/emblem-favorite.png:/icons/emblem-favorite.png - &Donate... - - - - - - :/icons/document-open-remote.png:/icons/document-open-remote.png - - - Open Synced... - - - AnkiWeb - - - Download a deck that you synced from another computer - - - - - - :/icons/download.png:/icons/download.png - - - Open Shared... - - - Download - - - Download a deck that people have shared publicly + &Support Anki... @@ -586,16 +449,16 @@ Download a plugin to add new features or change Anki's behaviour - + :/icons/khtml_kget.png:/icons/khtml_kget.png - &Bury Fact + &Bury Note - Suspend the current fact until the deck is closed and opened again + Suspend the current note until Anki is reopened Shift+B @@ -607,28 +470,7 @@ :/icons/sqlitebrowser.png:/icons/sqlitebrowser.png - &Check Database... - - - - - - :/icons/document-open-recent.png:/icons/document-open-recent.png - - - Open &Recent... - - - Ctrl+R - - - - - - :/icons/download.png:/icons/download.png - - - Download Missing Media + &Optimize... @@ -643,11 +485,6 @@ L - - - Localize Media - - @@ -681,13 +518,38 @@ + + + :/icons/help-hint.png:/icons/help-hint.png + - Documentation... + &Guide... F1 + + + + :/icons/user-identity.png:/icons/user-identity.png + + + &Profile... + + + + + true + + + + :/icons/graphite_smooth_folder_noncommercial.png:/icons/graphite_smooth_folder_noncommercial.png + + + &Decks... + +
%s%s
%s