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 *
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
##########################################################################

View file

@ -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)

View file

@ -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 = """
<center>
<h1>%(title)s</h1>
%(tb)s
<p>
<table cellspacing=0 cellpadding=3 width=100%%>
%(rows)s
<table cellspacing=0 cellpading=3 width=100%%>
%(tree)s
</table>
<div class="extra">
%(extra)s
</div>
</center>
"""
def _renderPage(self):
css = self.mw.sharedCSS + self._css
if self._decks:
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(
tree = self._renderDeckTree(self.mw.col.sched.deckDueTree())
self.web.stdHtml(self._body%dict(
title=_("Decks"),
rows=buf,
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)
tree=tree), css=css)
def _deckRow(self, c, max, deck):
buf = "<tr>"
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 += "<td>%s<b>%s</b></td>" % (
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>"
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 <b>%(reps)d card</b> in <b>%(time)s</b> today.",
"Studied <b>%(reps)d cards</b> in <b>%(time)s</b> today.",
reps) % {
'reps': reps,
'time': fmtTimeSpan(mins, point=2),
}
rev = ngettext(
"<b><font color=#0000ff>%d</font></b> review",
"<b><font color=#0000ff>%d</font></b> reviews",
revC) % revC
new = ngettext("<b>%d</b> new card", "<b>%d</b> new cards", newC) % newC
line2 = _("Due: %(rev)s, %(new)s") % {
'rev': rev, 'new': new}
return line1+'<br>'+line2
def _deckRow(self, node, depth):
name, did, due, new, children = node
# due image
buf = "<tr><td colspan=5>" + self._dueImg(due, new)
# deck link
buf += " <a class=deck href='open:%d'>%s</a></td>"% (did, name)
# options
buf += "<td align=right class=opts>%s</td></tr>" % self.mw.button(
link="opts:%d"%did, name=_("Options")+'&#9660')
# 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 '<img valign=bottom src="qrc:/icons/%s.png">' % 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)

View file

@ -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</button>''' % (
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</button>''' % (
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)

View file

@ -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))

View file

@ -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

View file

@ -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):

View file

@ -1,5 +1,9 @@
<RCC>
<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-replace.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>