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]) runningDir=os.path.dirname(sys.argv[0])
modDir=runningDir modDir=runningDir
# set up paths for local development
sys.path.insert(0, os.path.join(modDir, "libanki")) sys.path.insert(0, os.path.join(modDir, "libanki"))
sys.path.insert(0, os.path.join(os.path.join(modDir, ".."), "libanki")) sys.path.insert(0, os.path.join(os.path.join(modDir, ".."), "libanki"))
import ankiqt import aqt
try: try:
import ankiqt.forms import aqt.forms
except ImportError: except ImportError:
raise Exception("You need to run tools/build_ui.sh in order for anki to work.") 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) self.onLink)
def onLink(self, url): 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.dialog.filterEdit.setText("fid:" + url.toString())
browser.updateSearch() browser.updateSearch()
browser.onFact() browser.onFact()

View file

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

View file

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

View file

@ -14,8 +14,8 @@ defaultConf = {
'confVer': 3, 'confVer': 3,
# remove? # remove?
'colourTimes': True, 'colourTimes': True,
'deckBrowserNameLength': 30,
'deckBrowserOrder': 0, 'deckBrowserOrder': 0,
# too long?
'deckBrowserRefreshPeriod': 3600, 'deckBrowserRefreshPeriod': 3600,
'factEditorAdvanced': False, 'factEditorAdvanced': False,
'showStudyScreen': True, 'showStudyScreen': True,
@ -96,7 +96,7 @@ class Config(object):
path = self._dbPath() path = self._dbPath()
self.db = DB(path, level=None, text=str) self.db = DB(path, level=None, text=str)
self.db.executescript(""" 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); create table if not exists config (conf text not null);
insert or ignore into config values ('');""") insert or ignore into config values ('');""")
conf = self.db.scalar("select conf from config") conf = self.db.scalar("select conf from config")
@ -111,6 +111,24 @@ insert or ignore into config values ('');""")
cPickle.dumps(self._conf)) cPickle.dumps(self._conf))
self.db.commit() 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): def _addDefaults(self):
if self.get('confVer') >= defaultConf['confVer']: if self.get('confVer') >= defaultConf['confVer']:
return 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): def reject(self):
n = _("Deck Properties") n = _("Deck Properties")
self.d.startProgress() self.mw.startProgress()
self.d.setUndoStart(n) self.d.setUndoStart(n)
needSync = False needSync = False
# syncing # syncing

View file

@ -250,7 +250,7 @@ class GraphWindow(object):
self.hbox.addWidget(buttonBox) self.hbox.addWidget(buttonBox)
def showHideAll(self): def showHideAll(self):
self.deck.startProgress(len(self.widgets)) self.mw.startProgress(len(self.widgets))
for w in self.widgets: for w in self.widgets:
self.deck.updateProgress(_("Processing...")) self.deck.updateProgress(_("Processing..."))
w.showHide() w.showHide()
@ -280,7 +280,7 @@ class GraphWindow(object):
QDesktopServices.openUrl(QUrl(aqt.appWiki + "Graphs")) QDesktopServices.openUrl(QUrl(aqt.appWiki + "Graphs"))
def onRefresh(self): def onRefresh(self):
self.deck.startProgress(len(self.widgets)) self.mw.startProgress(len(self.widgets))
self.dg.stats = None self.dg.stats = None
for w in self.widgets: for w in self.widgets:
self.deck.updateProgress(_("Processing...")) 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): def reject(self):
"Save user settings on close." "Save user settings on close."
# update properties # update properties
self.deck.startProgress() self.mw.startProgress()
mname = unicode(self.dialog.name.text()) mname = unicode(self.dialog.name.text())
if not mname: if not mname:
mname = _("Model") 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 warnTime = 10
def __init__(self, parent): def __init__(self, parent):
return
self.main = parent self.main = parent
self.statusbar = parent.mainWin.statusbar self.statusbar = parent.mainWin.statusbar
self.shown = [] self.shown = []

View file

@ -285,53 +285,3 @@ def getBase(deck, card):
return '<base href="%s">' % base return '<base href="%s">' % base
else: else:
return "" 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 import unicodedata as ucd
from aqt.utils import mungeQA, getBase from aqt.utils import mungeQA, getBase
from anki.utils import fmtTimeSpan from anki.utils import fmtTimeSpan
from PyQt4.QtWebKit import QWebPage, QWebView
failedCharColour = "#FF0000" failedCharColour = "#FF0000"
passedCharColour = "#00FF00" passedCharColour = "#00FF00"
@ -330,21 +329,3 @@ class View(object):
"Tell the user the deck is finished." "Tell the user the deck is finished."
self.main.mainWin.congratsLabel.setText( self.main.mainWin.congratsLabel.setText(
self.main.deck.deckFinishedMsg()) 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