diff --git a/aqt/__init__.py b/aqt/__init__.py index d2f011f2e..30e0dd967 100644 --- a/aqt/__init__.py +++ b/aqt/__init__.py @@ -5,7 +5,7 @@ import os, sys from aqt.qt import * appName="Anki" -appVersion="1.99" +appVersion="2.0-alpha2" appWebsite="http://ankisrs.net/" appHelpSite="http://ankisrs.net/docs/dev/" appDonate="http://ankisrs.net/support/" @@ -17,14 +17,6 @@ moduleDir = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] # if hasattr(sys, "frozen"): # sys.path.append(moduleDir) -def openHelp(name): - if "#" in name: - name = name.split("#") - name = name[0] + ".html#" + name[1] - else: - name = name + ".html" - QDesktopServices.openUrl(QUrl(appHelpSite + name)) - # Dialog manager - manages modeless windows ########################################################################## diff --git a/aqt/addons.py b/aqt/addons.py index 38fc0f592..9248bea65 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -10,6 +10,8 @@ from anki.hooks import runHook class AddonManager(object): def __init__(self, mw): + print "addons" + return self.mw = mw f = self.mw.form; s = SIGNAL("triggered()") self.mw.connect(f.actionOpenPluginFolder, s, self.onOpenPluginFolder) diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py index fdeb562e0..b62374db6 100644 --- a/aqt/deckbrowser.py +++ b/aqt/deckbrowser.py @@ -2,75 +2,20 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import time, os, stat, shutil, re -from operator import itemgetter from aqt.qt import * -from anki import Deck -from anki.utils import fmtTimeSpan -from anki.hooks import addHook -import aqt +from aqt.utils import askUser class DeckBrowser(object): - "Display a list of remembered decks." def __init__(self, mw): self.mw = mw self.web = mw.web - self._browserLastRefreshed = 0 - self._decks = [] - addHook("deckClosing", self._onClose) def show(self, _init=True): if _init: self.web.setLinkHandler(self._linkHandler) - self.web.setKeyHandler(self._keyHandler) - self._setupToolbar() - # refresh or reorder - self._checkDecks() - # show self._renderPage() - def _onClose(self): - # update counts - deck = self.mw.deck - def add(d): - counts = deck.sched.counts() - d['due'] = counts[1]+counts[2] - d['new'] = counts[0] - d['mod'] = deck.mod - d['time'] = deck.sched.timeToday() - d['reps'] = deck.sched.repsToday() - d['name'] = deck.name() - for d in self._decks: - if d['path'] == deck.path: - add(d) - return - # not found; add new - d = {'path': deck.path, 'state': 'ok'} - add(d) - self._decks.append(d) - - # Toolbar - ########################################################################## - - # we don't use the top toolbar - def _setupToolbar(self): - self.mw.form.toolBar.hide() - - # instead we have a html one under the deck list - def _toolbar(self): - items = [ - ("download", _("Download")), - ("new", _("Create")), -# ("import", _("Import")), - ("opensel", _("Open")), -# ("synced", _("Synced")), - ("refresh", _("Refresh")), - ] - h = "".join([self.mw.button( - link=row[0], name=row[1]) for row in items]) - return h - # Event handlers ########################################################################## @@ -81,7 +26,7 @@ class DeckBrowser(object): cmd = url if cmd == "open": deck = self._decks[int(arg)] - self._loadDeck(deck) + self._selDeck(deck) elif cmd == "opts": self._optsForRow(int(arg)) elif cmd == "download": @@ -97,177 +42,87 @@ class DeckBrowser(object): elif cmd == "refresh": self.refresh() - def _keyHandler(self, evt): - txt = evt.text() - if ((txt >= "0" and txt <= "9") or - (txt >= "a" and txt <= "z")): - self._openAccel(txt) - return True - - def _openAccel(self, txt): - for d in self._decks: - if d['accel'] == txt: - self._loadDeck(d) - - def _loadDeck(self, rec): - if rec['state'] == 'ok': - self.mw.loadDeck(rec['path']) + def _selDeck(self, rec): + print rec # HTML generation ########################################################################## _css = """ .sub { color: #555; } -a.deck { color: #000; text-decoration: none; font-size: 100%; } +a.deck { color: #000; text-decoration: none; font-size: 16px; } .num { text-align: right; padding: 0 5 0 5; } -td.opts { text-align: right; white-space: nowrap; } -td.menu { text-align: center; } +td.opts { white-space: nowrap; } +td.deck { width: 90% } a { font-size: 80%; } .extra { font-size: 90%; } +.due { vertical-align: text-bottom; } +table { margin: 1em; } """ _body = """

%(title)s

-%(tb)s -

- -%(rows)s +
+%(tree)s
-

-%(extra)s -
""" def _renderPage(self): css = self.mw.sharedCSS + self._css - if self._decks: - buf = "" - max=len(self._decks)-1 - buf += "%s" % _("Due") - buf += "%s" % _("New") - for c, deck in enumerate(self._decks): - buf += self._deckRow(c, max, deck) - self.web.stdHtml(self._body%dict( + tree = self._renderDeckTree(self.mw.col.sched.deckDueTree()) + self.web.stdHtml(self._body%dict( title=_("Decks"), - rows=buf, - tb=self._toolbar(), - extra="

%s

%s" % ( - self._summary(), - _("Click a deck to open it, or type a number."))), - css) - else: - self.web.stdHtml(self._body%dict( - title=_("Welcome!"), - rows="%s"%_( - "Click Download to get started."), - extra="", - tb=self._toolbar()), - css) + tree=tree), css=css) - def _deckRow(self, c, max, deck): - buf = "" - ok = deck['state'] == 'ok' - def accelName(deck): - if deck['accel']: - return "%s. " % deck['accel'] + def _renderDeckTree(self, nodes, depth=0): + if not nodes: return "" - if ok: - # name/link - buf += "%s%s" % ( - accelName(deck), - "%s"%(c, deck['name'])) - # due - col = '%s' - if deck['due'] > 0: - s = col % str(deck['due']) - else: - s = col % "" - buf += s - # new - if deck['new']: - s = str(deck['new']) - else: - s = "" - buf += "%s" % s - else: - # name/error - if deck['state'] == 'missing': - sub = _("(moved or removed)") - elif deck['state'] == 'corrupt': - sub = _("(corrupt)") - elif deck['state'] == 'in use': - sub = _("(already open)") - else: - sub = "unknown" - buf += "%s%s
%s" % ( - accelName(deck), - deck['name'], - sub) - # no counts - buf += "" - # options - buf += "%s" % ( - self.mw.button(link="opts:%d"%c, name=_("Options")+'▼')) - buf += "" + buf = "" + for node in nodes: + buf += self._deckRow(node, depth) return buf - def _summary(self): - # summarize - reps = 0 - mins = 0 - revC = 0 - newC = 0 - for d in self._decks: - if d['state']=='ok': - reps += d['reps'] - mins += d['time'] - revC += d['due'] - newC += d['new'] - line1 = ngettext( - "Studied %(reps)d card in %(time)s today.", - "Studied %(reps)d cards in %(time)s today.", - reps) % { - 'reps': reps, - 'time': fmtTimeSpan(mins, point=2), - } - rev = ngettext( - "%d review", - "%d reviews", - revC) % revC - new = ngettext("%d new card", "%d new cards", newC) % newC - line2 = _("Due: %(rev)s, %(new)s") % { - 'rev': rev, 'new': new} - return line1+'
'+line2 + def _deckRow(self, node, depth): + name, did, due, new, children = node + # due image + buf = "" + self._dueImg(due, new) + # deck link + buf += " %s"% (did, name) + # options + buf += "%s" % self.mw.button( + link="opts:%d"%did, name=_("Options")+'▼') + # children + buf += self._renderDeckTree(children, depth+1) + return buf + + def _dueImg(self, due, new): + if due and new: + i = "both" + elif due: + i = "green" + elif new: + i = "blue" + else: + i = "none" + return '' % i # Options ########################################################################## def _optsForRow(self, n): m = QMenu(self.mw) - # hide - a = m.addAction(QIcon(":/icons/edit-undo.png"), _("Hide From List")) - a.connect(a, SIGNAL("triggered()"), lambda n=n: self._hideRow(n)) # delete a = m.addAction(QIcon(":/icons/editdelete.png"), _("Delete")) a.connect(a, SIGNAL("triggered()"), lambda n=n: self._deleteRow(n)) m.exec_(QCursor.pos()) - def _hideRow(self, c): - d = self._decks[c] - if d['state'] == "missing" or aqt.utils.askUser(_("""\ -Hide %s from the list? You can File>Open it again later.""") % - d['name']): - self.mw.config.delRecentDeck(d['path']) - del self._decks[c] - self.refresh() - def _deleteRow(self, c): d = self._decks[c] if d['state'] == 'missing': return self._hideRow(c) - if aqt.utils.askUser(_("""\ + if askUser(_("""\ Delete %s? If this deck is synchronized the online version will \ not be touched.""") % d['name']): deck = d['path'] @@ -278,71 +133,3 @@ not be touched.""") % d['name']): pass self.mw.config.delRecentDeck(deck) self.refresh() - - # Data gathering - ########################################################################## - - def _checkDecks(self): - self._decks = [] - decks = self.mw.config.recentDecks() - if not decks: - return - tx = time.time() - self.mw.progress.start(max=len(decks)) - for c, d in enumerate(decks): - self.mw.progress.update(_("Checking deck %(x)d of %(y)d...") % { - 'x': c+1, 'y': len(decks)}) - base = os.path.basename(d) - if not os.path.exists(d): - self._decks.append({'name': base, 'state': 'missing', 'path':d}) - continue - try: - mod = os.stat(d)[stat.ST_MTIME] - t = time.time() - deck = Deck(d, queue=False, lock=False) - counts = deck.sched.selCounts() - dtime = deck.sched.timeToday() - dreps = deck.sched.repsToday() - self._decks.append({ - 'path': d, - 'state': 'ok', - 'name': deck.name(), - 'due': counts[1]+counts[2], - 'new': counts[0], - 'mod': deck.mod, - # these multiply deck check time by a factor of 6 - 'time': dtime, - 'reps': dreps - }) - deck.close(save=False) - # reset modification time for the sake of backup systems - try: - os.utime(d, (mod, mod)) - except: - # some misbehaving filesystems may fail here - pass - except Exception, e: - if "locked" in unicode(e): - state = "in use" - else: - state = "corrupt" - self._decks.append({'name': base, 'state':state, 'path':d}) - self.mw.progress.finish() - self._browserLastRefreshed = time.time() - self._reorderDecks() - - def _reorderDecks(self): - # for now, sort by deck name - self._decks.sort(key=itemgetter('name')) - # after the decks are sorted, assign shortcut keys to them - for c, d in enumerate(self._decks): - if c > 35: - d['accel'] = None - elif c < 9: - d['accel'] = str(c+1) - else: - d['accel'] = ord('a')+(c-10) - - def refresh(self): - self._browserLastRefreshed = 0 - self.show(_init=False) diff --git a/aqt/main.py b/aqt/main.py index 23c464abc..3616caa27 100755 --- a/aqt/main.py +++ b/aqt/main.py @@ -20,38 +20,29 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \ saveState, restoreState, getOnlyText, askUser, GetTextDialog, \ askUserDialog, applyStyles, getText, showText, showCritical, getFile -config = aqt.config - ## fixme: open plugin folder broken on win32? ## models remembering the previous group class AnkiQt(QMainWindow): - def __init__(self, app, config, args): + def __init__(self, app, profileManager): QMainWindow.__init__(self) aqt.mw = self self.app = app - self.config = config + self.pm = profileManager try: - # initialize everything - self.setup() - # load plugins + self.setupUI() self.setupAddons() - # show main window - self.show() - # raise window for osx - self.activateWindow() - self.raise_() - # + self.setupProfile() except: showInfo("Error during startup:\n%s" % traceback.format_exc()) sys.exit(1) - def setup(self): + def setupUI(self): self.col = None self.state = None + self.setupLang("en") # bootstrap with english; profile will adjust self.setupThreads() - self.setupLang() self.setupMainWindow() self.setupStyle() self.setupProxy() @@ -61,17 +52,136 @@ class AnkiQt(QMainWindow): self.setupErrorHandler() self.setupSystemSpecific() self.setupSignals() - self.setupVersion() self.setupAutoUpdate() self.setupUpgrade() self.setupCardStats() self.setupSchema() self.updateTitleBar() # screens - #self.setupColBrowser() + self.setupDeckBrowser() self.setupOverview() self.setupReviewer() + # Profiles + ########################################################################## + + def setupProfile(self): + # profile not provided on command line? + if False: # not self.pm.name: + # if there's a single profile, load it automatically + profs = self.pm.profiles() + if len(profs) == 1: + try: + self.pm.load(profs[0]) + except: + # password protected + pass + if not self.pm.name: + self.showProfileManager() + else: + self.loadProfile() + + def showProfileManager(self): + d = self.profileDiag = QDialog() + f = self.profileForm = aqt.forms.profiles.Ui_Dialog() + f.setupUi(d) + d.connect(f.login, SIGNAL("clicked()"), self.onOpenProfile) + d.connect(f.quit, SIGNAL("clicked()"), lambda: sys.exit(0)) + d.connect(f.add, SIGNAL("clicked()"), self.onAddProfile) + d.connect(f.delete_2, SIGNAL("clicked()"), self.onRemProfile) + d.connect(d, SIGNAL("rejected()"), lambda: d.close()) + d.connect(f.profiles, SIGNAL("currentRowChanged(int)"), + self.onProfileRowChange) + self.refreshProfilesList() + # raise first, for osx testing + d.show() + d.activateWindow() + d.raise_() + d.exec_() + + def refreshProfilesList(self): + f = self.profileForm + f.profiles.clear() + f.profiles.addItems(self.pm.profiles()) + f.profiles.setCurrentRow(0) + + def onProfileRowChange(self, n): + if n < 0: + # called on .clear() + return + name = self.pm.profiles()[n] + f = self.profileForm + passwd = False + try: + self.pm.load(name) + except: + passwd = True + f.passEdit.setShown(passwd) + f.passLabel.setShown(passwd) + + def openProfile(self): + name = self.pm.profiles()[self.profileForm.profiles.currentRow()] + passwd = self.profileForm.passEdit.text() + try: + self.pm.load(name, passwd) + except: + showWarning(_("Invalid password.")) + return + return True + + def onOpenProfile(self): + self.openProfile() + self.profileDiag.close() + self.loadProfile() + return True + + def onAddProfile(self): + name = getOnlyText("Name:") + if name: + if name in self.pm.profiles(): + return showWarning("Name exists.") + if not re.match("^[A-Za-z0-9 ]+$", name): + return showWarning( + "Only numbers, letters and spaces can be used.") + self.pm.create(name) + self.refreshProfilesList() + + def onRemProfile(self): + profs = self.pm.profiles() + if len(profs) < 2: + return showWarning("There must be at least one profile.") + # password correct? + if not self.openProfile(): + return + # sure? + if not askUser("""\ +All cards, notes, and media for this profile will be deleted. \ +Are you sure?"""): + return + self.pm.remove(self.pm.name) + self.refreshProfilesList() + + def loadProfile(self): + self.setupLang() + # show main window + if self.pm.profile['mainWindowState']: + restoreGeom(self, "mainWindow") + restoreState(self, "mainWindow") + else: + self.resize(500, 400) + self.show() + # raise window for osx + self.activateWindow() + self.raise_() + # maybe sync + self.onSync() + # then load collection and launch into the deck browser + self.col = Collection(self.pm.collectionPath()) + self.moveToState("deckBrowser") + + def unloadProfile(self): + self.col = None + # State machine ########################################################################## @@ -85,9 +195,8 @@ class AnkiQt(QMainWindow): getattr(self, "_"+state+"State")(oldState, *args) def _deckBrowserState(self, oldState): - # shouldn't call this directly; call close - self.disableColMenuItems() - self.closeAllColWindows() + self.disableDeckMenuItems() + self.closeAllWindows() self.deckBrowser.show() def _colLoadingState(self, oldState): @@ -163,8 +272,7 @@ class AnkiQt(QMainWindow): sharedCSS = """ body { -background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#bbb)); -/*background: #eee;*/ +background: #eee; margin: 2em; } a:hover { background-color: #aaa; } @@ -198,13 +306,8 @@ title="%s">%s''' % ( self.mainLayout.setContentsMargins(0,0,0,0) self.form.centralwidget.setLayout(self.mainLayout) addHook("undoEnd", self.maybeEnableUndo) - if self.config['mainWindowState']: - restoreGeom(self, "mainWindow") - restoreState(self, "mainWindow") - else: - self.resize(500, 400) - def closeAllColWindows(self): + def closeAllWindows(self): aqt.dialogs.closeAll() # Components @@ -223,19 +326,6 @@ title="%s">%s''' % ( import aqt.errors self.errorHandler = aqt.errors.ErrorHandler(self) - def setupVersion(self): - # 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'] != aqt.appVersion: - self.appUpdated = self.config['version'] - else: - self.appUpdated = False - if self.appUpdated: - self.config['version'] = aqt.appVersion - def setupAddons(self): import aqt.addons self.addonManager = aqt.addons.AddonManager(self) @@ -292,7 +382,7 @@ 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.col.path) + self.pm.profile.addRecentDeck(self.col.path) self.setupMedia(self.col) if not self.upgrading: self.progress.setupDB(self.col.db) @@ -319,7 +409,7 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") self.col.reset() runHook("deckClosing") print "focusOut() should be handled with deckClosing now" - self.closeAllDeckWindows() + self.closeAllWindows() self.col.close() self.col = None if showBrowser: @@ -329,7 +419,8 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") ########################################################################## def onSync(self): - return showInfo("not yet implemented") + return + return showInfo("sync not yet implemented") # Tools ########################################################################## @@ -344,7 +435,8 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") self.form.statusbar.showMessage(text, timeout) def setupStyle(self): - applyStyles(self) + print "applystyles" + #applyStyles(self) # App exit ########################################################################## @@ -352,11 +444,11 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") def prepareForExit(self): "Save config and window geometry." runHook("quit") - self.config['mainWindowGeom'] = self.saveGeometry() - self.config['mainWindowState'] = self.saveState() + self.pm.profile['mainWindowGeom'] = self.saveGeometry() + self.pm.profile['mainWindowState'] = self.saveState() # save config try: - self.config.save() + self.pm.save() except (IOError, OSError), e: showWarning(_("Anki was unable to save your " "configuration file:\n%s" % e)) @@ -368,7 +460,7 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") event.ignore() return self.moveToState("saveEdit") self.close(showBrowser=False) - # if self.config['syncOnProgramOpen']: + # if self.pm.profile['syncOnProgramOpen']: # self.showBrowser = False # self.syncDeck(interactive=False) self.prepareForExit() @@ -392,18 +484,18 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors") tb.addAction(frm.actionStats) tb.addAction(frm.actionMarkCard) tb.addAction(frm.actionRepeatAudio) - tb.setIconSize(QSize(self.config['iconSize'], - self.config['iconSize'])) + tb.setIconSize(QSize(self.pm.profile['iconSize'], + self.pm.profile['iconSize'])) toggle = tb.toggleViewAction() toggle.setText(_("Toggle Toolbar")) self.connect(toggle, SIGNAL("triggered()"), self.onToolbarToggle) - if not self.config['showToolbar']: + if not self.pm.profile['showToolbar']: tb.hide() def onToolbarToggle(self): tb = self.form.toolBar - self.config['showToolbar'] = tb.isVisible() + self.pm.profile['showToolbar'] = tb.isVisible() # Dockable widgets ########################################################################## @@ -564,7 +656,7 @@ Please choose a new deck name:""")) # Language handling ########################################################################## - def setupLang(self): + def setupLang(self, force=None): "Set the user interface language." import locale, gettext import anki.lang @@ -572,15 +664,16 @@ Please choose a new deck name:""")) locale.setlocale(locale.LC_ALL, '') except: pass - languageDir=os.path.join(aqt.modDir, "locale") + lang = force if force else self.pm.profile["lang"] + languageDir=os.path.join(aqt.moduleDir, "locale") self.languageTrans = gettext.translation('aqt', languageDir, - languages=[self.config["interfaceLang"]], + languages=[lang], fallback=True) self.installTranslation() if getattr(self, 'form', None): self.form.retranslateUi(self) - anki.lang.setLang(self.config["interfaceLang"], local=False) - if self.config['interfaceLang'] in ("he","ar","fa"): + anki.lang.setLang(lang, local=False) + if lang in ("he","ar","fa"): self.app.setLayoutDirection(Qt.RightToLeft) else: self.app.setLayoutDirection(Qt.LeftToRight) @@ -600,10 +693,8 @@ Please choose a new deck name:""")) ########################################################################## deckRelatedMenuItems = ( - "Rename", - "Close", - "Addcards", - "Editdeck", + "Add", + "Browse", "Undo", "Export", "Stats", @@ -647,9 +738,8 @@ Please choose a new deck name:""")) def enableDeckMenuItems(self, enabled=True): "setEnabled deck-related items." - for item in self.colRelatedMenuItems: + for item in self.deckRelatedMenuItems: getattr(self.form, "action" + item).setEnabled(enabled) - self.form.menuAdvanced.setEnabled(enabled) if not enabled: self.disableCardMenuItems() self.maybeEnableUndo() @@ -697,7 +787,7 @@ Please choose a new deck name:""")) self.autoUpdate.start() def newVerAvail(self, data): - if self.config['suppressUpdate'] < data['latestVersion']: + if self.pm.profile['suppressUpdate'] < data['latestVersion']: aqt.update.askAndUpdate(self, data) def newMsg(self, data): @@ -740,7 +830,7 @@ haven't been synced here yet. Continue?""")) # def setupMedia(self, deck): # print "setup media" # return -# prefix = self.config['mediaLocation'] +# prefix = self.pm.profile['mediaLocation'] # prev = deck.getVar("mediaLocation") or "" # # set the media prefix # if not prefix: @@ -812,7 +902,7 @@ haven't been synced here yet. Continue?""")) # return p # def setupDropbox(self, deck): -# if not self.config['dropboxPublicFolder']: +# if not self.pm.profile['dropboxPublicFolder']: # # put a file in the folder # open(os.path.join( # deck.mediaPrefix, "right-click-me.txt"), "w").write("") @@ -836,11 +926,11 @@ haven't been synced here yet. Continue?""")) # 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']: +# self.pm.profile['dropboxPublicFolder'] = os.path.dirname(txt[0]) +# if self.pm.profile['dropboxPublicFolder']: # # update media url # deck.setVar( -# "mediaURL", self.config['dropboxPublicFolder'] + "/" + +# "mediaURL", self.pm.profile['dropboxPublicFolder'] + "/" + # os.path.basename(deck.mediaDir()) + "/") # Advanced features @@ -913,11 +1003,10 @@ doubt.""")) ########################################################################## def setupSystemSpecific(self): - self.setupDocumentDir() addHook("macLoadEvent", self.onMacLoad) if isMac: qt_mac_set_menubar_icons(False) - #self.setUnifiedTitleAndToolBarOnMac(self.config['showToolbar']) + #self.setUnifiedTitleAndToolBarOnMac(self.pm.profile['showToolbar']) # mac users expect a minimize option self.minimizeShortcut = QShortcut("Ctrl+m", self) self.connect(self.minimizeShortcut, SIGNAL("activated()"), @@ -945,40 +1034,20 @@ doubt.""")) def onMacLoad(self, fname): self.loadDeck(fname) - def setupDocumentDir(self): - if self.config['documentDir']: - return - if isWin: - s = QSettings(QSettings.UserScope, "Microsoft", "Windows") - s.beginGroup("CurrentVersion/Explorer/Shell Folders") - d = s.value("Personal") - if os.path.exists(d): - d = os.path.join(d, "Anki") - else: - d = os.path.expanduser("~/.anki/decks") - elif isMac: - d = os.path.expanduser("~/Documents/Anki") - else: - d = os.path.expanduser("~/.anki/decks") - try: - os.mkdir(d) - except (OSError, IOError): - # already exists - pass - self.config['documentDir'] = d - # Proxy support ########################################################################## def setupProxy(self): + print "proxy" + return import urllib2 - if self.config['proxyHost']: + if self.pm.profile['proxyHost']: proxy = "http://" - if self.config['proxyUser']: - proxy += (self.config['proxyUser'] + ":" + - self.config['proxyPass'] + "@") - proxy += (self.config['proxyHost'] + ":" + - str(self.config['proxyPort'])) + if self.pm.profile['proxyUser']: + proxy += (self.pm.profile['proxyUser'] + ":" + + self.pm.profile['proxyPass'] + "@") + proxy += (self.pm.profile['proxyHost'] + ":" + + str(self.pm.profile['proxyPort'])) os.environ["http_proxy"] = proxy proxy_handler = urllib2.ProxyHandler() opener = urllib2.build_opener(proxy_handler) diff --git a/aqt/profiles.py b/aqt/profiles.py index a36933168..41850254a 100644 --- a/aqt/profiles.py +++ b/aqt/profiles.py @@ -7,9 +7,9 @@ # - 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 +import os, sys, time, random, cPickle, shutil from anki.db import DB -from anki.utils import isMac, isWin, intTime +from anki.utils import isMac, isWin, intTime, checksum metaConf = dict( ver=0, @@ -18,6 +18,7 @@ metaConf = dict( id=random.randrange(0, 2**63), lastMsg=-1, suppressUpdate=False, + firstRun=True, ) profileConf = dict( @@ -81,23 +82,35 @@ documentation for information on using a flash drive.""") ###################################################################### def profiles(self): - return [x for x in self.db.scalar("select name from profiles") - if x != "_global"] + return sorted( + x for x in self.db.list("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 load(self, name, passwd=None): + prof = cPickle.loads( + self.db.scalar("select data from profiles where name = ?", name)) + if prof['key'] and prof['key'] != self._pwhash(passwd): + self.name = None + raise Exception("Invalid password") + if name != "_global": + self.name = name + self.profile = prof 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")) + self.db.execute(sql, cPickle.dumps(self.meta), "_global") + self.db.commit() def create(self, name): - assert re.match("^[A-Za-z0-9 ]+$", name) self.db.execute("insert into profiles values (?, ?)", name, cPickle.dumps(profileConf)) + self.db.commit() + + def remove(self, name): + shutil.rmtree(self.profileFolder()) + self.db.execute("delete from profiles where name = ?", name) + self.db.commit() # Folder handling ###################################################################### @@ -119,7 +132,7 @@ documentation for information on using a flash drive.""") ###################################################################### def _ensureExists(self, path): - if not exists(path): + if not os.path.exists(path): os.makedirs(path) return path @@ -136,16 +149,23 @@ documentation for information on using a flash drive.""") def _load(self): path = os.path.join(self.base, "prefs.db") + new = not os.path.exists(path) 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: + if new: # 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") + else: + # load previously created + self.meta = cPickle.loads( + self.db.scalar( + "select data from profiles where name = '_global'")) + + def _pwhash(self, passwd): + return checksum(unicode(self.meta['id'])+unicode(passwd)) diff --git a/aqt/update.py b/aqt/update.py index ea10825f0..a0e5a51c9 100644 --- a/aqt/update.py +++ b/aqt/update.py @@ -14,6 +14,8 @@ class LatestVersionFinder(QThread): def __init__(self, main): QThread.__init__(self) + print "autoupdate" + return self.main = main self.config = main.config plat=sys.platform @@ -27,6 +29,7 @@ class LatestVersionFinder(QThread): self.stats = d def run(self): + return if not self.config['checkForUpdates']: return d = self.stats diff --git a/aqt/utils.py b/aqt/utils.py index db678059a..9c72c054a 100644 --- a/aqt/utils.py +++ b/aqt/utils.py @@ -7,6 +7,14 @@ import aqt from anki.sound import playFromText, stripSounds from anki.utils import call, isWin, isMac +def openHelp(name): + if "#" in name: + name = name.split("#") + name = name[0] + ".html#" + name[1] + else: + name = name + ".html" + QDesktopServices.openUrl(QUrl(appHelpSite + name)) + def openLink(link): QDesktopServices.openUrl(QUrl(link)) @@ -210,7 +218,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None): assert not dir or not key if not dir: dirkey = key+"Directory" - dir = aqt.mw.config.get(dirkey, "") + dir = aqt.mw.pm.profile.get(dirkey, "") else: dirkey = None d = QFileDialog(parent) @@ -226,7 +234,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None): file = unicode(list(d.selectedFiles())[0]) if dirkey: dir = os.path.dirname(file) - aqt.mw.config[dirkey] = dir + aqt.mw.pm.profile[dirkey] = dir cb(file) d.connect(d, SIGNAL("accepted()"), accept) @@ -234,7 +242,7 @@ def getSaveFile(parent, title, dir, key, ext): "Ask the user for a file to save. Use DIR as config variable." dirkey = dir+"Directory" file = unicode(QFileDialog.getSaveFileName( - parent, title, aqt.mw.config.get(dirkey, ""), key, + parent, title, aqt.mw.pm.profile.get(dirkey, ""), key, None, QFileDialog.DontConfirmOverwrite)) if file: # add extension @@ -242,7 +250,7 @@ def getSaveFile(parent, title, dir, key, ext): file += ext # save new default dir = os.path.dirname(file) - aqt.mw.config[dirkey] = dir + aqt.mw.pm.profile[dirkey] = dir # check if it exists if os.path.exists(file): if not askUser( @@ -253,12 +261,12 @@ def getSaveFile(parent, title, dir, key, ext): def saveGeom(widget, key): key += "Geom" - aqt.mw.config[key] = widget.saveGeometry() + aqt.mw.pm.profile[key] = widget.saveGeometry() def restoreGeom(widget, key, offset=None): key += "Geom" - if aqt.mw.config.get(key): - widget.restoreGeometry(aqt.mw.config[key]) + if aqt.mw.pm.profile.get(key): + widget.restoreGeometry(aqt.mw.pm.profile[key]) if isMac and offset: from aqt.main import QtConfig as q minor = (q.qt_version & 0x00ff00) >> 8 @@ -269,30 +277,30 @@ def restoreGeom(widget, key, offset=None): def saveState(widget, key): key += "State" - aqt.mw.config[key] = widget.saveState() + aqt.mw.pm.profile[key] = widget.saveState() def restoreState(widget, key): key += "State" - if aqt.mw.config.get(key): - widget.restoreState(aqt.mw.config[key]) + if aqt.mw.pm.profile.get(key): + widget.restoreState(aqt.mw.pm.profile[key]) def saveSplitter(widget, key): key += "Splitter" - aqt.mw.config[key] = widget.saveState() + aqt.mw.pm.profile[key] = widget.saveState() def restoreSplitter(widget, key): key += "Splitter" - if aqt.mw.config.get(key): - widget.restoreState(aqt.mw.config[key]) + if aqt.mw.pm.profile.get(key): + widget.restoreState(aqt.mw.pm.profile[key]) def saveHeader(widget, key): key += "Header" - aqt.mw.config[key] = widget.saveState() + aqt.mw.pm.profile[key] = widget.saveState() def restoreHeader(widget, key): key += "Header" - if aqt.mw.config.get(key): - widget.restoreState(aqt.mw.config[key]) + if aqt.mw.pm.profile.get(key): + widget.restoreState(aqt.mw.pm.profile[key]) def mungeQA(txt): txt = stripSounds(txt) @@ -302,7 +310,7 @@ def mungeQA(txt): def applyStyles(widget): try: - styleFile = open(os.path.join(aqt.mw.config.confDir, + styleFile = open(os.path.join(aqt.mw.pm.profile.confDir, "style.css")) widget.setStyleSheet(styleFile.read()) except (IOError, OSError): diff --git a/designer/icons.qrc b/designer/icons.qrc index cc751f34d..078c24e3b 100644 --- a/designer/icons.qrc +++ b/designer/icons.qrc @@ -1,5 +1,9 @@ + icons/blue.png + icons/both.png + icons/green.png + icons/none.png icons/edit-find 2.png icons/edit-find-replace.png icons/graphite_smooth_folder_noncommercial.png diff --git a/designer/icons/none.png b/designer/icons/none.png new file mode 100644 index 000000000..86d14a7a7 Binary files /dev/null and b/designer/icons/none.png differ diff --git a/designer/profiles.ui b/designer/profiles.ui new file mode 100644 index 000000000..59c68f1ee --- /dev/null +++ b/designer/profiles.ui @@ -0,0 +1,98 @@ + + + Dialog + + + + 0 + 0 + 352 + 283 + + + + Profiles + + + + + + Profile: + + + + + + + + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + + + + + Open + + + + + + + Add + + + + + + + Delete + + + + + + + Quit + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + +