mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
profile gui, new deck browser
This commit is contained in:
parent
f4150a5df4
commit
7c68b58d44
10 changed files with 379 additions and 396 deletions
|
@ -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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = ""
|
self.web.stdHtml(self._body%dict(
|
||||||
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(
|
|
||||||
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")+'▼'))
|
|
||||||
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")+'▼')
|
||||||
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)
|
|
||||||
|
|
269
aqt/main.py
269
aqt/main.py
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
self.name = name
|
prof = cPickle.loads(
|
||||||
self.idself.profile = cPickle.loads(
|
self.db.scalar("select data from profiles where name = ?", name))
|
||||||
self.db.scalar("select oid, 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):
|
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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
42
aqt/utils.py
42
aqt/utils.py
|
@ -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):
|
||||||
|
|
|
@ -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
BIN
designer/icons/none.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 358 B |
98
designer/profiles.ui
Normal file
98
designer/profiles.ui
Normal 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>
|
Loading…
Reference in a new issue