profile gui, new deck browser

This commit is contained in:
Damien Elmes 2011-11-24 18:42:06 +09:00
parent f4150a5df4
commit 7c68b58d44
10 changed files with 379 additions and 396 deletions

View file

@ -5,7 +5,7 @@ import os, sys
from aqt.qt import * from aqt.qt import *
appName="Anki" appName="Anki"
appVersion="1.99" appVersion="2.0-alpha2"
appWebsite="http://ankisrs.net/" appWebsite="http://ankisrs.net/"
appHelpSite="http://ankisrs.net/docs/dev/" appHelpSite="http://ankisrs.net/docs/dev/"
appDonate="http://ankisrs.net/support/" 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"): # if hasattr(sys, "frozen"):
# sys.path.append(moduleDir) # 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 # Dialog manager - manages modeless windows
########################################################################## ##########################################################################

View file

@ -10,6 +10,8 @@ from anki.hooks import runHook
class AddonManager(object): class AddonManager(object):
def __init__(self, mw): def __init__(self, mw):
print "addons"
return
self.mw = mw self.mw = mw
f = self.mw.form; s = SIGNAL("triggered()") f = self.mw.form; s = SIGNAL("triggered()")
self.mw.connect(f.actionOpenPluginFolder, s, self.onOpenPluginFolder) self.mw.connect(f.actionOpenPluginFolder, s, self.onOpenPluginFolder)

View file

@ -2,75 +2,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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 aqt.qt import *
from anki import Deck from aqt.utils import askUser
from anki.utils import fmtTimeSpan
from anki.hooks import addHook
import aqt
class DeckBrowser(object): class DeckBrowser(object):
"Display a list of remembered decks."
def __init__(self, mw): def __init__(self, mw):
self.mw = mw self.mw = mw
self.web = mw.web self.web = mw.web
self._browserLastRefreshed = 0
self._decks = []
addHook("deckClosing", self._onClose)
def show(self, _init=True): def show(self, _init=True):
if _init: if _init:
self.web.setLinkHandler(self._linkHandler) self.web.setLinkHandler(self._linkHandler)
self.web.setKeyHandler(self._keyHandler)
self._setupToolbar()
# refresh or reorder
self._checkDecks()
# show
self._renderPage() 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 # Event handlers
########################################################################## ##########################################################################
@ -81,7 +26,7 @@ class DeckBrowser(object):
cmd = url cmd = url
if cmd == "open": if cmd == "open":
deck = self._decks[int(arg)] deck = self._decks[int(arg)]
self._loadDeck(deck) self._selDeck(deck)
elif cmd == "opts": elif cmd == "opts":
self._optsForRow(int(arg)) self._optsForRow(int(arg))
elif cmd == "download": elif cmd == "download":
@ -97,177 +42,87 @@ class DeckBrowser(object):
elif cmd == "refresh": elif cmd == "refresh":
self.refresh() self.refresh()
def _keyHandler(self, evt): def _selDeck(self, rec):
txt = evt.text() print rec
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'])
# HTML generation # HTML generation
########################################################################## ##########################################################################
_css = """ _css = """
.sub { color: #555; } .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; } .num { text-align: right; padding: 0 5 0 5; }
td.opts { text-align: right; white-space: nowrap; } td.opts { white-space: nowrap; }
td.menu { text-align: center; } td.deck { width: 90% }
a { font-size: 80%; } a { font-size: 80%; }
.extra { font-size: 90%; } .extra { font-size: 90%; }
.due { vertical-align: text-bottom; }
table { margin: 1em; }
""" """
_body = """ _body = """
<center> <center>
<h1>%(title)s</h1> <h1>%(title)s</h1>
%(tb)s <table cellspacing=0 cellpading=3 width=100%%>
<p> %(tree)s
<table cellspacing=0 cellpadding=3 width=100%%>
%(rows)s
</table> </table>
<div class="extra">
%(extra)s
</div>
</center> </center>
""" """
def _renderPage(self): def _renderPage(self):
css = self.mw.sharedCSS + self._css css = self.mw.sharedCSS + self._css
if self._decks: tree = self._renderDeckTree(self.mw.col.sched.deckDueTree())
buf = ""
max=len(self._decks)-1
buf += "<tr><th></th><th align=right>%s</th>" % _("Due")
buf += "<th align=right>%s</th><th></th></tr>" % _("New")
for c, deck in enumerate(self._decks):
buf += self._deckRow(c, max, deck)
self.web.stdHtml(self._body%dict( self.web.stdHtml(self._body%dict(
title=_("Decks"), title=_("Decks"),
rows=buf, tree=tree), css=css)
tb=self._toolbar(),
extra="<p>%s<p>%s" % (
self._summary(),
_("Click a deck to open it, or type a number."))),
css)
else:
self.web.stdHtml(self._body%dict(
title=_("Welcome!"),
rows="<tr><td align=center>%s</td></tr>"%_(
"Click <b>Download</b> to get started."),
extra="",
tb=self._toolbar()),
css)
def _deckRow(self, c, max, deck): def _renderDeckTree(self, nodes, depth=0):
buf = "<tr>" if not nodes:
ok = deck['state'] == 'ok'
def accelName(deck):
if deck['accel']:
return "%s. " % deck['accel']
return "" return ""
if ok: buf = ""
# name/link for node in nodes:
buf += "<td>%s<b>%s</b></td>" % ( buf += self._deckRow(node, depth)
accelName(deck),
"<a class=deck href='open:%d'>%s</a>"%(c, deck['name']))
# due
col = '<td class=num><b><font color=#0000ff>%s</font></b></td>'
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 += "<td class=num>%s</td>" % 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 += "<td>%s<b>%s</b><br><span class=sub>%s</span></td>" % (
accelName(deck),
deck['name'],
sub)
# no counts
buf += "<td colspan=2></td>"
# options
buf += "<td class=opts>%s</td>" % (
self.mw.button(link="opts:%d"%c, name=_("Options")+'&#9660'))
buf += "</tr>"
return buf return buf
def _summary(self): def _deckRow(self, node, depth):
# summarize name, did, due, new, children = node
reps = 0 # due image
mins = 0 buf = "<tr><td colspan=5>" + self._dueImg(due, new)
revC = 0 # deck link
newC = 0 buf += " <a class=deck href='open:%d'>%s</a></td>"% (did, name)
for d in self._decks: # options
if d['state']=='ok': buf += "<td align=right class=opts>%s</td></tr>" % self.mw.button(
reps += d['reps'] link="opts:%d"%did, name=_("Options")+'&#9660')
mins += d['time'] # children
revC += d['due'] buf += self._renderDeckTree(children, depth+1)
newC += d['new'] return buf
line1 = ngettext(
"Studied <b>%(reps)d card</b> in <b>%(time)s</b> today.", def _dueImg(self, due, new):
"Studied <b>%(reps)d cards</b> in <b>%(time)s</b> today.", if due and new:
reps) % { i = "both"
'reps': reps, elif due:
'time': fmtTimeSpan(mins, point=2), i = "green"
} elif new:
rev = ngettext( i = "blue"
"<b><font color=#0000ff>%d</font></b> review", else:
"<b><font color=#0000ff>%d</font></b> reviews", i = "none"
revC) % revC return '<img valign=bottom src="qrc:/icons/%s.png">' % i
new = ngettext("<b>%d</b> new card", "<b>%d</b> new cards", newC) % newC
line2 = _("Due: %(rev)s, %(new)s") % {
'rev': rev, 'new': new}
return line1+'<br>'+line2
# Options # Options
########################################################################## ##########################################################################
def _optsForRow(self, n): def _optsForRow(self, n):
m = QMenu(self.mw) 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 # delete
a = m.addAction(QIcon(":/icons/editdelete.png"), _("Delete")) a = m.addAction(QIcon(":/icons/editdelete.png"), _("Delete"))
a.connect(a, SIGNAL("triggered()"), lambda n=n: self._deleteRow(n)) a.connect(a, SIGNAL("triggered()"), lambda n=n: self._deleteRow(n))
m.exec_(QCursor.pos()) 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): def _deleteRow(self, c):
d = self._decks[c] d = self._decks[c]
if d['state'] == 'missing': if d['state'] == 'missing':
return self._hideRow(c) return self._hideRow(c)
if aqt.utils.askUser(_("""\ if askUser(_("""\
Delete %s? If this deck is synchronized the online version will \ Delete %s? If this deck is synchronized the online version will \
not be touched.""") % d['name']): not be touched.""") % d['name']):
deck = d['path'] deck = d['path']
@ -278,71 +133,3 @@ not be touched.""") % d['name']):
pass pass
self.mw.config.delRecentDeck(deck) self.mw.config.delRecentDeck(deck)
self.refresh() 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)

View file

@ -20,38 +20,29 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
saveState, restoreState, getOnlyText, askUser, GetTextDialog, \ saveState, restoreState, getOnlyText, askUser, GetTextDialog, \
askUserDialog, applyStyles, getText, showText, showCritical, getFile askUserDialog, applyStyles, getText, showText, showCritical, getFile
config = aqt.config
## fixme: open plugin folder broken on win32? ## fixme: open plugin folder broken on win32?
## models remembering the previous group ## models remembering the previous group
class AnkiQt(QMainWindow): class AnkiQt(QMainWindow):
def __init__(self, app, config, args): def __init__(self, app, profileManager):
QMainWindow.__init__(self) QMainWindow.__init__(self)
aqt.mw = self aqt.mw = self
self.app = app self.app = app
self.config = config self.pm = profileManager
try: try:
# initialize everything self.setupUI()
self.setup()
# load plugins
self.setupAddons() self.setupAddons()
# show main window self.setupProfile()
self.show()
# raise window for osx
self.activateWindow()
self.raise_()
#
except: except:
showInfo("Error during startup:\n%s" % traceback.format_exc()) showInfo("Error during startup:\n%s" % traceback.format_exc())
sys.exit(1) sys.exit(1)
def setup(self): def setupUI(self):
self.col = None self.col = None
self.state = None self.state = None
self.setupLang("en") # bootstrap with english; profile will adjust
self.setupThreads() self.setupThreads()
self.setupLang()
self.setupMainWindow() self.setupMainWindow()
self.setupStyle() self.setupStyle()
self.setupProxy() self.setupProxy()
@ -61,17 +52,136 @@ class AnkiQt(QMainWindow):
self.setupErrorHandler() self.setupErrorHandler()
self.setupSystemSpecific() self.setupSystemSpecific()
self.setupSignals() self.setupSignals()
self.setupVersion()
self.setupAutoUpdate() self.setupAutoUpdate()
self.setupUpgrade() self.setupUpgrade()
self.setupCardStats() self.setupCardStats()
self.setupSchema() self.setupSchema()
self.updateTitleBar() self.updateTitleBar()
# screens # screens
#self.setupColBrowser() self.setupDeckBrowser()
self.setupOverview() self.setupOverview()
self.setupReviewer() 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 # State machine
########################################################################## ##########################################################################
@ -85,9 +195,8 @@ class AnkiQt(QMainWindow):
getattr(self, "_"+state+"State")(oldState, *args) getattr(self, "_"+state+"State")(oldState, *args)
def _deckBrowserState(self, oldState): def _deckBrowserState(self, oldState):
# shouldn't call this directly; call close self.disableDeckMenuItems()
self.disableColMenuItems() self.closeAllWindows()
self.closeAllColWindows()
self.deckBrowser.show() self.deckBrowser.show()
def _colLoadingState(self, oldState): def _colLoadingState(self, oldState):
@ -163,8 +272,7 @@ class AnkiQt(QMainWindow):
sharedCSS = """ sharedCSS = """
body { body {
background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#bbb)); background: #eee;
/*background: #eee;*/
margin: 2em; margin: 2em;
} }
a:hover { background-color: #aaa; } a:hover { background-color: #aaa; }
@ -198,13 +306,8 @@ title="%s">%s</button>''' % (
self.mainLayout.setContentsMargins(0,0,0,0) self.mainLayout.setContentsMargins(0,0,0,0)
self.form.centralwidget.setLayout(self.mainLayout) self.form.centralwidget.setLayout(self.mainLayout)
addHook("undoEnd", self.maybeEnableUndo) 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() aqt.dialogs.closeAll()
# Components # Components
@ -223,19 +326,6 @@ title="%s">%s</button>''' % (
import aqt.errors import aqt.errors
self.errorHandler = aqt.errors.ErrorHandler(self) 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): def setupAddons(self):
import aqt.addons import aqt.addons
self.addonManager = aqt.addons.AddonManager(self) self.addonManager = aqt.addons.AddonManager(self)
@ -292,7 +382,7 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
finally: finally:
# we may have a progress window open if we were upgrading # we may have a progress window open if we were upgrading
self.progress.finish() self.progress.finish()
self.config.addRecentDeck(self.col.path) self.pm.profile.addRecentDeck(self.col.path)
self.setupMedia(self.col) self.setupMedia(self.col)
if not self.upgrading: if not self.upgrading:
self.progress.setupDB(self.col.db) self.progress.setupDB(self.col.db)
@ -319,7 +409,7 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
self.col.reset() self.col.reset()
runHook("deckClosing") runHook("deckClosing")
print "focusOut() should be handled with deckClosing now" print "focusOut() should be handled with deckClosing now"
self.closeAllDeckWindows() self.closeAllWindows()
self.col.close() self.col.close()
self.col = None self.col = None
if showBrowser: if showBrowser:
@ -329,7 +419,8 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
########################################################################## ##########################################################################
def onSync(self): def onSync(self):
return showInfo("not yet implemented") return
return showInfo("sync not yet implemented")
# Tools # Tools
########################################################################## ##########################################################################
@ -344,7 +435,8 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
self.form.statusbar.showMessage(text, timeout) self.form.statusbar.showMessage(text, timeout)
def setupStyle(self): def setupStyle(self):
applyStyles(self) print "applystyles"
#applyStyles(self)
# App exit # App exit
########################################################################## ##########################################################################
@ -352,11 +444,11 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
def prepareForExit(self): def prepareForExit(self):
"Save config and window geometry." "Save config and window geometry."
runHook("quit") runHook("quit")
self.config['mainWindowGeom'] = self.saveGeometry() self.pm.profile['mainWindowGeom'] = self.saveGeometry()
self.config['mainWindowState'] = self.saveState() self.pm.profile['mainWindowState'] = self.saveState()
# save config # save config
try: try:
self.config.save() self.pm.save()
except (IOError, OSError), e: except (IOError, OSError), e:
showWarning(_("Anki was unable to save your " showWarning(_("Anki was unable to save your "
"configuration file:\n%s" % e)) "configuration file:\n%s" % e))
@ -368,7 +460,7 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
event.ignore() event.ignore()
return self.moveToState("saveEdit") return self.moveToState("saveEdit")
self.close(showBrowser=False) self.close(showBrowser=False)
# if self.config['syncOnProgramOpen']: # if self.pm.profile['syncOnProgramOpen']:
# self.showBrowser = False # self.showBrowser = False
# self.syncDeck(interactive=False) # self.syncDeck(interactive=False)
self.prepareForExit() self.prepareForExit()
@ -392,18 +484,18 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
tb.addAction(frm.actionStats) tb.addAction(frm.actionStats)
tb.addAction(frm.actionMarkCard) tb.addAction(frm.actionMarkCard)
tb.addAction(frm.actionRepeatAudio) tb.addAction(frm.actionRepeatAudio)
tb.setIconSize(QSize(self.config['iconSize'], tb.setIconSize(QSize(self.pm.profile['iconSize'],
self.config['iconSize'])) self.pm.profile['iconSize']))
toggle = tb.toggleViewAction() toggle = tb.toggleViewAction()
toggle.setText(_("Toggle Toolbar")) toggle.setText(_("Toggle Toolbar"))
self.connect(toggle, SIGNAL("triggered()"), self.connect(toggle, SIGNAL("triggered()"),
self.onToolbarToggle) self.onToolbarToggle)
if not self.config['showToolbar']: if not self.pm.profile['showToolbar']:
tb.hide() tb.hide()
def onToolbarToggle(self): def onToolbarToggle(self):
tb = self.form.toolBar tb = self.form.toolBar
self.config['showToolbar'] = tb.isVisible() self.pm.profile['showToolbar'] = tb.isVisible()
# Dockable widgets # Dockable widgets
########################################################################## ##########################################################################
@ -564,7 +656,7 @@ Please choose a new deck name:"""))
# Language handling # Language handling
########################################################################## ##########################################################################
def setupLang(self): def setupLang(self, force=None):
"Set the user interface language." "Set the user interface language."
import locale, gettext import locale, gettext
import anki.lang import anki.lang
@ -572,15 +664,16 @@ Please choose a new deck name:"""))
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
except: except:
pass 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, self.languageTrans = gettext.translation('aqt', languageDir,
languages=[self.config["interfaceLang"]], languages=[lang],
fallback=True) fallback=True)
self.installTranslation() self.installTranslation()
if getattr(self, 'form', None): if getattr(self, 'form', None):
self.form.retranslateUi(self) self.form.retranslateUi(self)
anki.lang.setLang(self.config["interfaceLang"], local=False) anki.lang.setLang(lang, local=False)
if self.config['interfaceLang'] in ("he","ar","fa"): if lang in ("he","ar","fa"):
self.app.setLayoutDirection(Qt.RightToLeft) self.app.setLayoutDirection(Qt.RightToLeft)
else: else:
self.app.setLayoutDirection(Qt.LeftToRight) self.app.setLayoutDirection(Qt.LeftToRight)
@ -600,10 +693,8 @@ Please choose a new deck name:"""))
########################################################################## ##########################################################################
deckRelatedMenuItems = ( deckRelatedMenuItems = (
"Rename", "Add",
"Close", "Browse",
"Addcards",
"Editdeck",
"Undo", "Undo",
"Export", "Export",
"Stats", "Stats",
@ -647,9 +738,8 @@ Please choose a new deck name:"""))
def enableDeckMenuItems(self, enabled=True): def enableDeckMenuItems(self, enabled=True):
"setEnabled deck-related items." "setEnabled deck-related items."
for item in self.colRelatedMenuItems: for item in self.deckRelatedMenuItems:
getattr(self.form, "action" + item).setEnabled(enabled) getattr(self.form, "action" + item).setEnabled(enabled)
self.form.menuAdvanced.setEnabled(enabled)
if not enabled: if not enabled:
self.disableCardMenuItems() self.disableCardMenuItems()
self.maybeEnableUndo() self.maybeEnableUndo()
@ -697,7 +787,7 @@ Please choose a new deck name:"""))
self.autoUpdate.start() self.autoUpdate.start()
def newVerAvail(self, data): def newVerAvail(self, data):
if self.config['suppressUpdate'] < data['latestVersion']: if self.pm.profile['suppressUpdate'] < data['latestVersion']:
aqt.update.askAndUpdate(self, data) aqt.update.askAndUpdate(self, data)
def newMsg(self, data): def newMsg(self, data):
@ -740,7 +830,7 @@ haven't been synced here yet. Continue?"""))
# def setupMedia(self, deck): # def setupMedia(self, deck):
# print "setup media" # print "setup media"
# return # return
# prefix = self.config['mediaLocation'] # prefix = self.pm.profile['mediaLocation']
# prev = deck.getVar("mediaLocation") or "" # prev = deck.getVar("mediaLocation") or ""
# # set the media prefix # # set the media prefix
# if not prefix: # if not prefix:
@ -812,7 +902,7 @@ haven't been synced here yet. Continue?"""))
# return p # return p
# def setupDropbox(self, deck): # def setupDropbox(self, deck):
# if not self.config['dropboxPublicFolder']: # if not self.pm.profile['dropboxPublicFolder']:
# # put a file in the folder # # put a file in the folder
# open(os.path.join( # open(os.path.join(
# deck.mediaPrefix, "right-click-me.txt"), "w").write("") # 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 \ # That doesn't appear to be a public link. You'll be asked again when the deck \
# is next loaded.""")) # is next loaded."""))
# else: # else:
# self.config['dropboxPublicFolder'] = os.path.dirname(txt[0]) # self.pm.profile['dropboxPublicFolder'] = os.path.dirname(txt[0])
# if self.config['dropboxPublicFolder']: # if self.pm.profile['dropboxPublicFolder']:
# # update media url # # update media url
# deck.setVar( # deck.setVar(
# "mediaURL", self.config['dropboxPublicFolder'] + "/" + # "mediaURL", self.pm.profile['dropboxPublicFolder'] + "/" +
# os.path.basename(deck.mediaDir()) + "/") # os.path.basename(deck.mediaDir()) + "/")
# Advanced features # Advanced features
@ -913,11 +1003,10 @@ doubt."""))
########################################################################## ##########################################################################
def setupSystemSpecific(self): def setupSystemSpecific(self):
self.setupDocumentDir()
addHook("macLoadEvent", self.onMacLoad) addHook("macLoadEvent", self.onMacLoad)
if isMac: if isMac:
qt_mac_set_menubar_icons(False) qt_mac_set_menubar_icons(False)
#self.setUnifiedTitleAndToolBarOnMac(self.config['showToolbar']) #self.setUnifiedTitleAndToolBarOnMac(self.pm.profile['showToolbar'])
# mac users expect a minimize option # mac users expect a minimize option
self.minimizeShortcut = QShortcut("Ctrl+m", self) self.minimizeShortcut = QShortcut("Ctrl+m", self)
self.connect(self.minimizeShortcut, SIGNAL("activated()"), self.connect(self.minimizeShortcut, SIGNAL("activated()"),
@ -945,40 +1034,20 @@ doubt."""))
def onMacLoad(self, fname): def onMacLoad(self, fname):
self.loadDeck(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 # Proxy support
########################################################################## ##########################################################################
def setupProxy(self): def setupProxy(self):
print "proxy"
return
import urllib2 import urllib2
if self.config['proxyHost']: if self.pm.profile['proxyHost']:
proxy = "http://" proxy = "http://"
if self.config['proxyUser']: if self.pm.profile['proxyUser']:
proxy += (self.config['proxyUser'] + ":" + proxy += (self.pm.profile['proxyUser'] + ":" +
self.config['proxyPass'] + "@") self.pm.profile['proxyPass'] + "@")
proxy += (self.config['proxyHost'] + ":" + proxy += (self.pm.profile['proxyHost'] + ":" +
str(self.config['proxyPort'])) str(self.pm.profile['proxyPort']))
os.environ["http_proxy"] = proxy os.environ["http_proxy"] = proxy
proxy_handler = urllib2.ProxyHandler() proxy_handler = urllib2.ProxyHandler()
opener = urllib2.build_opener(proxy_handler) opener = urllib2.build_opener(proxy_handler)

View file

@ -7,9 +7,9 @@
# - Saves in sqlite rather than a flat file so the config can't be corrupted # - Saves in sqlite rather than a flat file so the config can't be corrupted
from aqt.qt import * 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.db import DB
from anki.utils import isMac, isWin, intTime from anki.utils import isMac, isWin, intTime, checksum
metaConf = dict( metaConf = dict(
ver=0, ver=0,
@ -18,6 +18,7 @@ metaConf = dict(
id=random.randrange(0, 2**63), id=random.randrange(0, 2**63),
lastMsg=-1, lastMsg=-1,
suppressUpdate=False, suppressUpdate=False,
firstRun=True,
) )
profileConf = dict( profileConf = dict(
@ -81,23 +82,35 @@ documentation for information on using a flash drive.""")
###################################################################### ######################################################################
def profiles(self): def profiles(self):
return [x for x in self.db.scalar("select name from profiles") return sorted(
if x != "_global"] x for x in self.db.list("select name from profiles")
if x != "_global")
def load(self, 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.name = name
self.idself.profile = cPickle.loads( self.profile = prof
self.db.scalar("select oid, data from profiles where name = ?", name))
def save(self): def save(self):
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, cPickle.dumps(self.profile), self.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): def create(self, name):
assert re.match("^[A-Za-z0-9 ]+$", name)
self.db.execute("insert into profiles values (?, ?)", self.db.execute("insert into profiles values (?, ?)",
name, cPickle.dumps(profileConf)) 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 # Folder handling
###################################################################### ######################################################################
@ -119,7 +132,7 @@ documentation for information on using a flash drive.""")
###################################################################### ######################################################################
def _ensureExists(self, path): def _ensureExists(self, path):
if not exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
return path return path
@ -136,16 +149,23 @@ documentation for information on using a flash drive.""")
def _load(self): def _load(self):
path = os.path.join(self.base, "prefs.db") path = os.path.join(self.base, "prefs.db")
new = not os.path.exists(path)
self.db = DB(path, text=str) self.db = DB(path, text=str)
self.db.execute(""" self.db.execute("""
create table if not exists profiles create table if not exists profiles
(name text primary key, data text not null);""") (name text primary key, data text not null);""")
try: if new:
self.meta = self.loadProfile("_global")
except:
# create a default global profile # create a default global profile
self.meta = metaConf.copy() self.meta = metaConf.copy()
self.db.execute("insert into profiles values ('_global', ?)", self.db.execute("insert into profiles values ('_global', ?)",
cPickle.dumps(metaConf)) cPickle.dumps(metaConf))
# and save a default user profile for later (commits) # and save a default user profile for later (commits)
self.create("User 1") 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))

View file

@ -14,6 +14,8 @@ class LatestVersionFinder(QThread):
def __init__(self, main): def __init__(self, main):
QThread.__init__(self) QThread.__init__(self)
print "autoupdate"
return
self.main = main self.main = main
self.config = main.config self.config = main.config
plat=sys.platform plat=sys.platform
@ -27,6 +29,7 @@ class LatestVersionFinder(QThread):
self.stats = d self.stats = d
def run(self): def run(self):
return
if not self.config['checkForUpdates']: if not self.config['checkForUpdates']:
return return
d = self.stats d = self.stats

View file

@ -7,6 +7,14 @@ import aqt
from anki.sound import playFromText, stripSounds from anki.sound import playFromText, stripSounds
from anki.utils import call, isWin, isMac 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): def openLink(link):
QDesktopServices.openUrl(QUrl(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 assert not dir or not key
if not dir: if not dir:
dirkey = key+"Directory" dirkey = key+"Directory"
dir = aqt.mw.config.get(dirkey, "") dir = aqt.mw.pm.profile.get(dirkey, "")
else: else:
dirkey = None dirkey = None
d = QFileDialog(parent) d = QFileDialog(parent)
@ -226,7 +234,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None):
file = unicode(list(d.selectedFiles())[0]) file = unicode(list(d.selectedFiles())[0])
if dirkey: if dirkey:
dir = os.path.dirname(file) dir = os.path.dirname(file)
aqt.mw.config[dirkey] = dir aqt.mw.pm.profile[dirkey] = dir
cb(file) cb(file)
d.connect(d, SIGNAL("accepted()"), accept) 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." "Ask the user for a file to save. Use DIR as config variable."
dirkey = dir+"Directory" dirkey = dir+"Directory"
file = unicode(QFileDialog.getSaveFileName( file = unicode(QFileDialog.getSaveFileName(
parent, title, aqt.mw.config.get(dirkey, ""), key, parent, title, aqt.mw.pm.profile.get(dirkey, ""), key,
None, QFileDialog.DontConfirmOverwrite)) None, QFileDialog.DontConfirmOverwrite))
if file: if file:
# add extension # add extension
@ -242,7 +250,7 @@ def getSaveFile(parent, title, dir, key, ext):
file += ext file += ext
# save new default # save new default
dir = os.path.dirname(file) dir = os.path.dirname(file)
aqt.mw.config[dirkey] = dir aqt.mw.pm.profile[dirkey] = dir
# check if it exists # check if it exists
if os.path.exists(file): if os.path.exists(file):
if not askUser( if not askUser(
@ -253,12 +261,12 @@ def getSaveFile(parent, title, dir, key, ext):
def saveGeom(widget, key): def saveGeom(widget, key):
key += "Geom" key += "Geom"
aqt.mw.config[key] = widget.saveGeometry() aqt.mw.pm.profile[key] = widget.saveGeometry()
def restoreGeom(widget, key, offset=None): def restoreGeom(widget, key, offset=None):
key += "Geom" key += "Geom"
if aqt.mw.config.get(key): if aqt.mw.pm.profile.get(key):
widget.restoreGeometry(aqt.mw.config[key]) widget.restoreGeometry(aqt.mw.pm.profile[key])
if isMac and offset: if isMac and offset:
from aqt.main import QtConfig as q from aqt.main import QtConfig as q
minor = (q.qt_version & 0x00ff00) >> 8 minor = (q.qt_version & 0x00ff00) >> 8
@ -269,30 +277,30 @@ def restoreGeom(widget, key, offset=None):
def saveState(widget, key): def saveState(widget, key):
key += "State" key += "State"
aqt.mw.config[key] = widget.saveState() aqt.mw.pm.profile[key] = widget.saveState()
def restoreState(widget, key): def restoreState(widget, key):
key += "State" key += "State"
if aqt.mw.config.get(key): if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.config[key]) widget.restoreState(aqt.mw.pm.profile[key])
def saveSplitter(widget, key): def saveSplitter(widget, key):
key += "Splitter" key += "Splitter"
aqt.mw.config[key] = widget.saveState() aqt.mw.pm.profile[key] = widget.saveState()
def restoreSplitter(widget, key): def restoreSplitter(widget, key):
key += "Splitter" key += "Splitter"
if aqt.mw.config.get(key): if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.config[key]) widget.restoreState(aqt.mw.pm.profile[key])
def saveHeader(widget, key): def saveHeader(widget, key):
key += "Header" key += "Header"
aqt.mw.config[key] = widget.saveState() aqt.mw.pm.profile[key] = widget.saveState()
def restoreHeader(widget, key): def restoreHeader(widget, key):
key += "Header" key += "Header"
if aqt.mw.config.get(key): if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.config[key]) widget.restoreState(aqt.mw.pm.profile[key])
def mungeQA(txt): def mungeQA(txt):
txt = stripSounds(txt) txt = stripSounds(txt)
@ -302,7 +310,7 @@ def mungeQA(txt):
def applyStyles(widget): def applyStyles(widget):
try: try:
styleFile = open(os.path.join(aqt.mw.config.confDir, styleFile = open(os.path.join(aqt.mw.pm.profile.confDir,
"style.css")) "style.css"))
widget.setStyleSheet(styleFile.read()) widget.setStyleSheet(styleFile.read())
except (IOError, OSError): except (IOError, OSError):

View file

@ -1,5 +1,9 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>icons/blue.png</file>
<file>icons/both.png</file>
<file>icons/green.png</file>
<file>icons/none.png</file>
<file>icons/edit-find 2.png</file> <file>icons/edit-find 2.png</file>
<file>icons/edit-find-replace.png</file> <file>icons/edit-find-replace.png</file>
<file>icons/graphite_smooth_folder_noncommercial.png</file> <file>icons/graphite_smooth_folder_noncommercial.png</file>

BIN
designer/icons/none.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

98
designer/profiles.ui Normal file
View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>352</width>
<height>283</height>
</rect>
</property>
<property name="windowTitle">
<string>Profiles</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Profile:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QListWidget" name="profiles"/>
</item>
<item>
<widget class="QLabel" name="passLabel">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passEdit">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="login">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="add">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="delete_2">
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="quit">
<property name="text">
<string>Quit</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>