start refactoring main window

- moved progress handling into separate progress.py
- moved deck browser code into separate deckbrowser.py
- started reworking the state code; views will be rolled into this in the
  future
- the main window has been stripped of the study options, inline editor,
  congrats screen and so on, and now consists of a single main widget which
  has a webview placed inside it. The stripped features will be implemented
  either in separate windows, or as part of the web view
This commit is contained in:
Damien Elmes 2011-03-15 00:00:45 +09:00
parent 798b0af128
commit d948b00c54
16 changed files with 986 additions and 4099 deletions

7
anki
View file

@ -13,15 +13,14 @@ if __name__ == "__main__":
runningDir=os.path.dirname(sys.argv[0])
modDir=runningDir
# set up paths for local development
sys.path.insert(0, os.path.join(modDir, "libanki"))
sys.path.insert(0, os.path.join(os.path.join(modDir, ".."), "libanki"))
import ankiqt
import aqt
try:
import ankiqt.forms
import aqt.forms
except ImportError:
raise Exception("You need to run tools/build_ui.sh in order for anki to work.")
ankiqt.run()
aqt.run()

View file

@ -94,7 +94,7 @@ class AddCards(QDialog):
self.onLink)
def onLink(self, url):
browser = ui.dialogs.get("CardList", self.parent)
browser = ui.dialogs.open("CardList", self.parent)
browser.dialog.filterEdit.setText("fid:" + url.toString())
browser.updateSearch()
browser.onFact()

View file

@ -1038,10 +1038,13 @@ where id in %s""" % ids2str(sf))
######################################################################
def setupHooks(self):
print "setupHooks()"
return
addHook("postUndoRedo", self.postUndoRedo)
addHook("currentCardDeleted", self.updateSearch)
def teardownHooks(self):
return
removeHook("postUndoRedo", self.postUndoRedo)
removeHook("currentCardDeleted", self.updateSearch)

View file

@ -298,7 +298,7 @@ order by n""", id=card.id)
def reject(self):
modified = False
self.deck.startProgress()
self.mw.startProgress()
self.deck.updateProgress(_("Applying changes..."))
reset=True
if self.needFormatRebuild:

View file

@ -14,8 +14,8 @@ defaultConf = {
'confVer': 3,
# remove?
'colourTimes': True,
'deckBrowserNameLength': 30,
'deckBrowserOrder': 0,
# too long?
'deckBrowserRefreshPeriod': 3600,
'factEditorAdvanced': False,
'showStudyScreen': True,
@ -96,7 +96,7 @@ class Config(object):
path = self._dbPath()
self.db = DB(path, level=None, text=str)
self.db.executescript("""
create table if not exists recentDecks (path not null);
create table if not exists decks (path text primary key);
create table if not exists config (conf text not null);
insert or ignore into config values ('');""")
conf = self.db.scalar("select conf from config")
@ -111,6 +111,24 @@ insert or ignore into config values ('');""")
cPickle.dumps(self._conf))
self.db.commit()
# recent deck support
def recentDecks(self):
"Return a list of paths to remembered decks."
# have to convert to unicode manually because of the text factory
return [unicode(d[0]) for d in
self.db.execute("select path from decks")]
def addRecentDeck(self, path):
"Add PATH to the list of recent decks if not already. Must be unicode."
self.db.execute("insert or ignore into decks values (?)",
path.encode("utf-8"))
def delRecentDeck(self, path):
"Remove PATH from the list if it exists. Must be unicode."
self.db.execute("delete from decks where path = ?",
path.encode("utf-8"))
# helpers
def _addDefaults(self):
if self.get('confVer') >= defaultConf['confVer']:
return

289
aqt/deckbrowser.py Normal file
View file

@ -0,0 +1,289 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# -*- coding: utf-8 -*-
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import time, os, stat
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from anki import Deck
from anki.utils import fmtTimeSpan
from anki.hooks import addHook
class DeckBrowser(object):
def __init__(self, mw):
self.mw = mw
self._browserLastRefreshed = 0
self._decks = []
addHook("deckClosing", self.onClose)
def show(self):
if (time.time() - self._browserLastRefreshed >
self.mw.config['deckBrowserRefreshPeriod']):
t = time.time()
self._checkDecks()
print "check decks", time.time() - t
else:
self._reorderDecks()
if self._decks:
buf = self._header()
buf += "<center><h1>Decks</h1><table cellspacing=0 width=90%>"
t = time.time
for c, deck in enumerate(self._decks):
buf += self._deckRow(c, deck)
buf += "</table>"
buf += self._buttons()
buf += self._summary()
else:
buf = ("""\
<br>
<font size=+1>
Welcome to Anki! Click <b>'Download'</b> to get started. You can return here
later by using File>Close.
</font>
<br>
""")
# FIXME: ensure deck open button is focused
self.mw.web.setHtml(buf)
def onClose(self, deck):
print "onClose"
return
if deck.finishScheduler:
self.deck.finishScheduler()
self.deck.reset()
# update counts
for d in self.browserDecks:
if d['path'] == self.deck.path:
d['due'] = self.deck.failedSoonCount + self.deck.revCount
d['new'] = self.deck.newCount
d['mod'] = self.deck.modified
d['time'] = self.deck._dailyStats.reviewTime
d['reps'] = self.deck._dailyStats.reps
def _header(self):
return "<html><head><style>td { border-bottom: 1px solid #000; margin:0px; padding:0px;} </style></head><body>"
def _footer(self):
return "</body></html>"
def _deckRow(self, c, deck):
buf = "<tr>"
# name and status
ok = deck['state'] == 'ok'
if ok:
sub = _("%s ago") % fmtTimeSpan(
time.time() - deck['mod'])
elif deck['state'] == 'missing':
sub = _("(moved or removed)")
elif deck['state'] == 'corrupt':
sub = _("(corrupt)")
elif deck['state'] == 'in use':
sub = _("(already open)")
sub = "<font size=-1>%s</font>" % sub
buf += "<td>%s<br>%s</td>" % (deck['name'], sub)
if ok:
# due
col = '<td><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>%s</td>" % s
else:
buf += "<td></td><td></td>"
# open
# openButton = QPushButton(_("Open"))
# if c < 9:
# if sys.platform.startswith("darwin"):
# extra = ""
# # appears to be broken on osx
# #extra = _(" (Command+Option+%d)") % (c+1)
# #openButton.setShortcut(_("Ctrl+Alt+%d" % (c+1)))
# else:
# extra = _(" (Alt+%d)") % (c+1)
# openButton.setShortcut(_("Alt+%d" % (c+1)))
# else:
# extra = ""
# openButton.setToolTip(_("Open this deck%s") % extra)
# self.connect(openButton, SIGNAL("clicked()"),
# lambda d=deck['path']: self.loadDeck(d))
# layout.addWidget(openButton, c+1, 5)
# if c == 0:
# focusButton = openButton
# more
# moreButton = QPushButton(_("More"))
# moreMenu = QMenu()
# a = moreMenu.addAction(QIcon(":/icons/edit-undo.png"),
# _("Hide From List"))
# a.connect(a, SIGNAL("triggered()"),
# lambda c=c: self.onDeckBrowserForget(c))
# a = moreMenu.addAction(QIcon(":/icons/editdelete.png"),
# _("Delete"))
# a.connect(a, SIGNAL("triggered()"),
# lambda c=c: self.onDeckBrowserDelete(c))
# moreButton.setMenu(moreMenu)
# self.moreMenus.append(moreMenu)
# layout.addWidget(moreButton, c+1, 6)
buf += "</tr>"
return buf
def _buttons(self):
# refresh = QPushButton(_("Refresh"))
# refresh.setToolTip(_("Check due counts again (F5)"))
# refresh.setShortcut(_("F5"))
# self.connect(refresh, SIGNAL("clicked()"),
# self.refresh)
# layout.addItem(QSpacerItem(1,20, QSizePolicy.Preferred,
# QSizePolicy.Preferred), c+2, 5)
# layout.addWidget(refresh, c+3, 5)
# more = QPushButton(_("More"))
# moreMenu = QMenu()
# a = moreMenu.addAction(QIcon(":/icons/edit-undo.png"),
# _("Forget Inaccessible Decks"))
# a.connect(a, SIGNAL("triggered()"),
# self.onDeckBrowserForgetInaccessible)
# more.setMenu(moreMenu)
# layout.addWidget(more, c+3, 6)
# self.moreMenus.append(moreMenu)
return ""
def _summary(self):
return ""
# summarize
reps = 0
mins = 0
revC = 0
newC = 0
for d in self._decks:
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': anki.utils.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 ""
def _checkDecks(self, forget=False):
self._decks = []
decks = self.mw.config.recentDecks()
if not decks:
return
missingDecks = []
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):
missingDecks.append(d)
self._decks.append({'name': base, 'state': 'missing'})
continue
try:
mod = os.stat(d)[stat.ST_MTIME]
t = time.time()
deck = Deck(d)
counts = deck.sched.counts()
self._decks.append({
'path': d,
'state': 'ok',
'name': deck.name(),
'due': counts[0],
'new': counts[1],
'mod': deck.mod,
# these multiple deck check time by a factor of 6
'time': 0, #deck.sched.timeToday(),
'reps': 0, #deck.sched.repsToday()
})
deck.close()
# 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 "File is in use" in unicode(e):
state = "in use"
else:
raise
state = "corrupt"
self._decks.append({'name': base, 'state':state})
if forget:
for d in missingDecks:
self.mw.config.delRecentDeck(d)
self.mw.progress.finish()
self._browserLastRefreshed = time.time()
self._reorderDecks()
def _reorderDecks(self):
print "reorder decks"
return
if self.mw.config['deckBrowserOrder'] == 0:
self._decks.sort(key=itemgetter('mod'),
reverse=True)
else:
def custcmp(a, b):
x = cmp(not not b['due'], not not a['due'])
if x:
return x
x = cmp(not not b['new'], not not a['new'])
if x:
return x
return cmp(a['mod'], b['mod'])
self._decks.sort(cmp=custcmp)
def refresh(self):
self._browserLastRefreshed = 0
self.show()
def onDeckBrowserForget(self, c):
if aqt.utils.askUser(_("""\
Hide %s from the list? You can File>Open it again later.""") %
self._decks[c]['name']):
self.mw.config['recentDeckPaths'].remove(self._decks[c]['path'])
del self._decks[c]
self.doLater(100, self.showDeckBrowser)
def onDeckBrowserDelete(self, c):
deck = self._decks[c]['path']
if aqt.utils.askUser(_("""\
Delete %s? If this deck is synchronized the online version will \
not be touched.""") %
self._decks[c]['name']):
del self._decks[c]
os.unlink(deck)
try:
shutil.rmtree(re.sub(".anki$", ".media", deck))
except OSError:
pass
#self.config['recentDeckPaths'].remove(deck)
self.doLater(100, self.showDeckBrowser)
def onDeckBrowserForgetInaccessible(self):
self._checkDecks(forget=True)
def doLater(self, msecs, func):
timer = QTimer(self)
timer.setSingleShot(True)
timer.start(msecs)
self.connect(timer, SIGNAL("timeout()"), func)

View file

@ -152,7 +152,7 @@ class DeckProperties(QDialog):
def reject(self):
n = _("Deck Properties")
self.d.startProgress()
self.mw.startProgress()
self.d.setUndoStart(n)
needSync = False
# syncing

View file

@ -250,7 +250,7 @@ class GraphWindow(object):
self.hbox.addWidget(buttonBox)
def showHideAll(self):
self.deck.startProgress(len(self.widgets))
self.mw.startProgress(len(self.widgets))
for w in self.widgets:
self.deck.updateProgress(_("Processing..."))
w.showHide()
@ -280,7 +280,7 @@ class GraphWindow(object):
QDesktopServices.openUrl(QUrl(aqt.appWiki + "Graphs"))
def onRefresh(self):
self.deck.startProgress(len(self.widgets))
self.mw.startProgress(len(self.widgets))
self.dg.stats = None
for w in self.widgets:
self.deck.updateProgress(_("Processing..."))

File diff suppressed because it is too large Load diff

View file

@ -261,7 +261,7 @@ class ModelProperties(QDialog):
def reject(self):
"Save user settings on close."
# update properties
self.deck.startProgress()
self.mw.startProgress()
mname = unicode(self.dialog.name.text())
if not mname:
mname = _("Model")

129
aqt/progress.py Normal file
View file

@ -0,0 +1,129 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# -*- coding: utf-8 -*-
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import time
from PyQt4.QtGui import *
from PyQt4.QtCore import *
# Progress info
##########################################################################
class ProgressManager(object):
def __init__(self, mw):
self.mw = mw
self.app = QApplication.instance()
self._win = None
self._levels = 0
self._mainThread = QThread.currentThread()
# SQLite progress handler
##########################################################################
def setupDB(self):
"Install a handler in the current deck."
self.lastDbProgress = 0
self.inDB = False
self.mw.deck.db.set_progress_handler(self._dbProgress, 100000)
def _dbProgress(self):
"Called from SQLite."
# do nothing if we don't have a progress window
if not self._progressWin:
return
# make sure we're not executing too frequently
if (time.time() - self.lastDbProgress) < 0.2:
return
self.lastDbProgress = time.time()
# and we're in the main thread
if self._mainThread != QThread.currentThread():
return
# ensure timers don't fire
self.inDB = True
# handle GUI events
self._maybeShow()
self.app.processEvents(QEventLoop.ExcludeUserInputEvents)
self.inDB = False
# DB-safe timers
##########################################################################
# QTimer may fire in processEvents(). We provide a custom timer which
# automatically defers until the DB is not busy.
def timer(self, ms, func, repeat):
def handler():
if self.inDB:
# retry in 100ms
self.timer(100, func, repeat)
else:
func()
t = QTimer(self.mw)
if not repeat:
t.setSingleShot(True)
t.connect(t, SIGNAL("timeout()"), handler)
t.start(ms)
# Creating progress dialogs
##########################################################################
def start(self, max=0, min=0, label=None, parent=None, immediate=False):
self._levels += 1
if self._levels > 1:
return
# disable UI
self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor))
self.mw.setEnabled(False)
# setup window
parent = parent or self.app.activeWindow() or self.mw
label = label or _("Processing...")
self._win = QProgressDialog(label, "", min, max, parent)
self._win.setWindowTitle("Anki")
self._win.setCancelButton(None)
self._win.setAutoClose(False)
self._win.setAutoReset(False)
# we need to manually manage minimum time to show, as qt gets confused
# by the db handler
self._win.setMinimumDuration(100000)
if immediate:
self._shown = True
print "show"
self._win.show()
else:
self._shown = False
self._counter = min
self._min = min
self._max = max
self._firstTime = time.time()
self._lastTime = time.time()
def update(self, label=None, value=None, process=True):
#print self._min, self._counter, self._max, label, time.time() - self._lastTime
self._maybeShow()
self._lastTime = time.time()
if label:
self._win.setLabelText(label)
if self._max and self._shown:
self._counter = value or (self._counter+1)
self._win.setValue(self._counter)
if process:
self.app.processEvents()
def finish(self):
self._levels -= 1
if self._levels == 0:
self._win.cancel()
self.app.restoreOverrideCursor()
self.mw.setEnabled(True)
def clear(self):
"Restore the interface after an error."
if self._levels:
self._levels = 1
self.finishProgress()
def _maybeShow(self):
if not self._shown and (time.time() - self._firstTime) > 2:
print "show2"
self._shown = True
self._win.show()

View file

@ -26,6 +26,7 @@ class StatusView(object):
warnTime = 10
def __init__(self, parent):
return
self.main = parent
self.statusbar = parent.mainWin.statusbar
self.shown = []

View file

@ -285,53 +285,3 @@ def getBase(deck, card):
return '<base href="%s">' % base
else:
return ""
class ProgressWin(object):
def __init__(self, parent, max=0, min=0, title=None, immediate=False):
if not title:
title = "Anki"
self.diag = QProgressDialog("", "", min, max, parent)
self.diag.setWindowTitle(title)
self.diag.setCancelButton(None)
self.diag.setAutoClose(False)
self.diag.setAutoReset(False)
# qt doesn't seem to honour this consistently, and it's not triggered
# by the db progress handler, so we set it high and use maybeShow() below
if immediate:
self.diag.show()
else:
self.diag.setMinimumDuration(100000)
self.counter = min
self.min = min
self.max = max
self.firstTime = time.time()
self.lastTime = time.time()
self.app = QApplication.instance()
self.shown = False
if max == 0:
self.diag.setLabelText(_("Processing..."))
def maybeShow(self):
if time.time() - self.firstTime > 2 and not self.shown:
self.shown = True
self.diag.show()
def update(self, label=None, value=None, process=True):
#print self.min, self.counter, self.max, label, time.time() - self.lastTime
self.maybeShow()
self.lastTime = time.time()
if label:
self.diag.setLabelText(label)
if value is None:
value = self.counter
self.counter += 1
else:
self.counter = value + 1
if self.max:
self.diag.setValue(value)
if process:
self.app.processEvents()
def finish(self):
self.diag.cancel()

View file

@ -12,7 +12,6 @@ import types, time, re, os, urllib, sys, difflib
import unicodedata as ucd
from aqt.utils import mungeQA, getBase
from anki.utils import fmtTimeSpan
from PyQt4.QtWebKit import QWebPage, QWebView
failedCharColour = "#FF0000"
passedCharColour = "#00FF00"
@ -330,21 +329,3 @@ class View(object):
"Tell the user the deck is finished."
self.main.mainWin.congratsLabel.setText(
self.main.deck.deckFinishedMsg())
class AnkiWebView(QWebView):
def __init__(self, *args):
QWebView.__init__(self, *args)
self.setObjectName("mainText")
def keyPressEvent(self, evt):
if evt.matches(QKeySequence.Copy):
self.triggerPageAction(QWebPage.Copy)
evt.accept()
QWebView.keyPressEvent(self, evt)
def contextMenuEvent(self, evt):
QWebView.contextMenuEvent(self, evt)
def dropEvent(self, evt):
pass

File diff suppressed because it is too large Load diff

1
icons/.gitignore vendored
View file

@ -1 +0,0 @@
/.directory