diff --git a/anki/decks.py b/anki/decks.py
index 180eb4200..b7940abda 100644
--- a/anki/decks.py
+++ b/anki/decks.py
@@ -262,9 +262,14 @@ class DeckManager(object):
self.rename(draggedDeck, ontoDeckName + "::" + self._basename(draggedDeckName))
def _canDragAndDrop(self, draggedDeckName, ontoDeckName):
- return draggedDeckName <> ontoDeckName \
- and not self._isParent(ontoDeckName, draggedDeckName) \
- and not self._isAncestor(draggedDeckName, ontoDeckName)
+ if draggedDeckName == ontoDeckName \
+ or self._isParent(ontoDeckName, draggedDeckName) \
+ or self._isAncestor(draggedDeckName, ontoDeckName):
+ return False
+ elif self.byName(ontoDeckName)['dyn']:
+ raise DeckRenameError(_("A filtered deck cannot have subdecks."))
+ else:
+ return True
def _isParent(self, parentDeckName, childDeckName):
return self._path(childDeckName) == self._path(parentDeckName) + [ self._basename(childDeckName) ]
diff --git a/anki/importing/csvfile.py b/anki/importing/csvfile.py
index bd0b1ff4b..612cdd27a 100644
--- a/anki/importing/csvfile.py
+++ b/anki/importing/csvfile.py
@@ -132,6 +132,6 @@ class TextImporter(NoteImporter):
def noteFromFields(self, fields):
note = ForeignNote()
- note.fields.extend([x.strip().replace("\n", "
") for x in fields])
+ note.fields.extend([x for x in fields])
note.tags.extend(self.tagsToAdd)
return note
diff --git a/anki/importing/noteimp.py b/anki/importing/noteimp.py
index 462fb662f..e9e435d67 100644
--- a/anki/importing/noteimp.py
+++ b/anki/importing/noteimp.py
@@ -117,10 +117,13 @@ class NoteImporter(Importer):
self._ids = []
self._cards = []
self._emptyNotes = False
+ dupeCount = 0
+ dupes = []
for n in notes:
- if not self.allowHTML:
- for c in range(len(n.fields)):
+ for c in range(len(n.fields)):
+ if not self.allowHTML:
n.fields[c] = cgi.escape(n.fields[c])
+ n.fields[c] = n.fields[c].strip().replace("\n", "
")
fld0 = n.fields[fld0idx]
csum = fieldChecksum(fld0)
# first field must exist
@@ -151,11 +154,18 @@ class NoteImporter(Importer):
if data:
updates.append(data)
updateLog.append(updateLogTxt % fld0)
+ dupeCount += 1
found = True
break
+ elif self.importMode == 1:
+ dupeCount += 1
elif self.importMode == 2:
# allow duplicates in this case
- updateLog.append(dupeLogTxt % fld0)
+ if fld0 not in dupes:
+ # only show message once, no matter how many
+ # duplicates are in the collection already
+ updateLog.append(dupeLogTxt % fld0)
+ dupes.append(fld0)
found = False
# newly add
if not found:
@@ -183,9 +193,19 @@ class NoteImporter(Importer):
self.col.sched.randomizeCards(did)
else:
self.col.sched.orderCards(did)
+
part1 = ngettext("%d note added", "%d notes added", len(new)) % len(new)
- part2 = ngettext("%d note updated", "%d notes updated", self.updateCount) % self.updateCount
- self.log.append("%s, %s." % (part1, part2))
+ part2 = ngettext("%d note updated", "%d notes updated",
+ self.updateCount) % self.updateCount
+ if self.importMode == 0:
+ unchanged = dupeCount - self.updateCount
+ elif self.importMode == 1:
+ unchanged = dupeCount
+ else:
+ unchanged = 0
+ part3 = ngettext("%d note unchanged", "%d notes unchanged",
+ unchanged) % unchanged
+ self.log.append("%s, %s, %s." % (part1, part2, part3))
self.log.extend(updateLog)
if self._emptyNotes:
self.log.append(_("""\
@@ -213,7 +233,6 @@ content in the text file to the correct fields."""))
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
rows)
- # need to document that deck is ignored in this case
def updateData(self, n, id, sflds):
self._ids.append(id)
if not self.processFields(n, sflds):
diff --git a/anki/stats.py b/anki/stats.py
index 7a93dbadd..681157785 100644
--- a/anki/stats.py
+++ b/anki/stats.py
@@ -655,7 +655,7 @@ group by hour having count() > 30 order by hour""" % lim,
(_("Mature"), colMature),
(_("Young+Learn"), colYoung),
(_("Unseen"), colUnseen),
- (_("Suspended"), colSusp))):
+ (_("Suspended+Buried"), colSusp))):
d.append(dict(data=div[c], label="%s: %s" % (t, div[c]), color=col))
# text data
i = []
diff --git a/aqt/addons.py b/aqt/addons.py
index 4e8f6cd7a..47279c503 100644
--- a/aqt/addons.py
+++ b/aqt/addons.py
@@ -6,7 +6,7 @@ import sys, os, traceback
from cStringIO import StringIO
from aqt.qt import *
from aqt.utils import showInfo, openFolder, isWin, openLink, \
- askUser
+ askUser, restoreGeom, saveGeom
from zipfile import ZipFile
import aqt.forms
import aqt
@@ -141,7 +141,9 @@ class GetAddons(QDialog):
b = self.form.buttonBox.addButton(
_("Browse"), QDialogButtonBox.ActionRole)
self.connect(b, SIGNAL("clicked()"), self.onBrowse)
+ restoreGeom(self, "getaddons", adjustSize=True)
self.exec_()
+ saveGeom(self, "getaddons")
def onBrowse(self):
openLink(aqt.appShared + "addons/")
diff --git a/aqt/browser.py b/aqt/browser.py
index 8a4efd980..e43a58a60 100644
--- a/aqt/browser.py
+++ b/aqt/browser.py
@@ -1015,6 +1015,10 @@ where id in %s""" % ids2str(sf))
self._previewWeb = AnkiWebView(True)
vbox.addWidget(self._previewWeb)
bbox = QDialogButtonBox()
+ self._previewReplay = bbox.addButton(_("Replay Audio"), QDialogButtonBox.ActionRole)
+ self._previewReplay.setAutoDefault(False)
+ self._previewReplay.setShortcut(QKeySequence("R"))
+ self._previewReplay.setToolTip(_("Shortcut key: %s" % "R"))
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
self._previewPrev.setAutoDefault(False)
self._previewPrev.setShortcut(QKeySequence("Left"))
@@ -1023,6 +1027,7 @@ where id in %s""" % ids2str(sf))
self._previewNext.setShortcut(QKeySequence("Right"))
c(self._previewPrev, SIGNAL("clicked()"), self._onPreviewPrev)
c(self._previewNext, SIGNAL("clicked()"), self._onPreviewNext)
+ c(self._previewReplay, SIGNAL("clicked()"), self._onReplayAudio)
vbox.addWidget(bbox)
self._previewWindow.setLayout(vbox)
restoreGeom(self._previewWindow, "preview")
@@ -1050,6 +1055,9 @@ where id in %s""" % ids2str(sf))
self.onNextCard()
self._updatePreviewButtons()
+ def _onReplayAudio(self):
+ self.mw.reviewer.replayAudio(self)
+
def _updatePreviewButtons(self):
if not self._previewWindow:
return
@@ -1321,7 +1329,10 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
frm.field.addItems([_("All Fields")] + fields)
self.connect(frm.buttonBox, SIGNAL("helpRequested()"),
self.onFindReplaceHelp)
- if not d.exec_():
+ restoreGeom(d, "findreplace")
+ r = d.exec_()
+ saveGeom(d, "findreplace")
+ if not r:
return
if frm.field.currentIndex() == 0:
field = None
diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py
index 74ad1b0a3..ca13c78b3 100644
--- a/aqt/deckbrowser.py
+++ b/aqt/deckbrowser.py
@@ -268,10 +268,15 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000)
a.connect(a, SIGNAL("triggered()"), lambda did=did: self._rename(did))
a = m.addAction(_("Options"))
a.connect(a, SIGNAL("triggered()"), lambda did=did: self._options(did))
+ a = m.addAction(_("Export"))
+ a.connect(a, SIGNAL("triggered()"), lambda did=did: self._export(did))
a = m.addAction(_("Delete"))
a.connect(a, SIGNAL("triggered()"), lambda did=did: self._delete(did))
m.exec_(QCursor.pos())
+ def _export(self, did):
+ self.mw.onExport(did=did)
+
def _rename(self, did):
self.mw.checkpoint(_("Rename Deck"))
deck = self.mw.col.decks.get(did)
diff --git a/aqt/deckconf.py b/aqt/deckconf.py
index 3404bc703..0e71b6b07 100644
--- a/aqt/deckconf.py
+++ b/aqt/deckconf.py
@@ -7,7 +7,7 @@ from anki.consts import NEW_CARDS_RANDOM
from aqt.qt import *
import aqt
from aqt.utils import showInfo, showWarning, openHelp, getOnlyText, askUser, \
- tooltip
+ tooltip, saveGeom, restoreGeom
class DeckConf(QDialog):
def __init__(self, mw, deck):
@@ -33,9 +33,10 @@ class DeckConf(QDialog):
self.onRestore)
self.setWindowTitle(_("Options for %s") % self.deck['name'])
# qt doesn't size properly with altered fonts otherwise
+ restoreGeom(self, "deckconf", adjustSize=True)
self.show()
- self.adjustSize()
self.exec_()
+ saveGeom(self, "deckconf")
def setupCombos(self):
import anki.consts as cs
diff --git a/aqt/dyndeckconf.py b/aqt/dyndeckconf.py
index 1f3f0f838..1b782e44a 100644
--- a/aqt/dyndeckconf.py
+++ b/aqt/dyndeckconf.py
@@ -4,7 +4,7 @@
from aqt.qt import *
import aqt
-from aqt.utils import showWarning, openHelp, askUser
+from aqt.utils import showWarning, openHelp, askUser, saveGeom, restoreGeom
class DeckConf(QDialog):
def __init__(self, mw, first=False, search="", deck=None):
@@ -26,6 +26,7 @@ class DeckConf(QDialog):
SIGNAL("helpRequested()"),
lambda: openHelp("filtered"))
self.setWindowTitle(_("Options for %s") % self.deck['name'])
+ restoreGeom(self, "dyndeckconf")
self.setupOrder()
self.loadConf()
if search:
@@ -33,6 +34,7 @@ class DeckConf(QDialog):
self.form.search.selectAll()
self.show()
self.exec_()
+ saveGeom(self, "dyndeckconf")
def setupOrder(self):
import anki.consts as cs
diff --git a/aqt/exporting.py b/aqt/exporting.py
index 7f4ee90c4..cb5bdd1ae 100644
--- a/aqt/exporting.py
+++ b/aqt/exporting.py
@@ -12,17 +12,17 @@ from anki.exporting import exporters
class ExportDialog(QDialog):
- def __init__(self, mw):
+ def __init__(self, mw, did=None):
QDialog.__init__(self, mw, Qt.Window)
self.mw = mw
self.col = mw.col
self.frm = aqt.forms.exporting.Ui_ExportDialog()
self.frm.setupUi(self)
self.exporter = None
- self.setup()
+ self.setup(did)
self.exec_()
- def setup(self):
+ def setup(self, did):
self.frm.format.insertItems(0, list(zip(*exporters())[0]))
self.connect(self.frm.format, SIGNAL("activated(int)"),
self.exporterChanged)
@@ -32,6 +32,11 @@ class ExportDialog(QDialog):
# save button
b = QPushButton(_("Export..."))
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
+ # set default option if accessed through deck button
+ if did:
+ name = self.mw.col.decks.get(did)['name']
+ index = self.frm.deck.findText(name)
+ self.frm.deck.setCurrentIndex(index)
def exporterChanged(self, idx):
self.exporter = exporters()[idx][1](self.col)
diff --git a/aqt/importing.py b/aqt/importing.py
index ef99958c8..5e7d90511 100644
--- a/aqt/importing.py
+++ b/aqt/importing.py
@@ -295,7 +295,7 @@ def importFile(mw, file):
return
except Exception, e:
msg = repr(str(e))
- if msg == "unknownFormat":
+ if msg == "'unknownFormat'":
if file.endswith(".anki2"):
showWarning(_("""\
.anki2 files are not designed for importing. If you're trying to restore from a \
@@ -379,7 +379,11 @@ def replaceWithApkg(mw, file, backup):
mw.unloadCollection()
# overwrite collection
z = zipfile.ZipFile(file)
- z.extract("collection.anki2", mw.pm.profileFolder())
+ try:
+ z.extract("collection.anki2", mw.pm.profileFolder())
+ except:
+ showWarning(_("The provided file is not a valid .apkg file."))
+ return
# because users don't have a backup of media, it's safer to import new
# data and rely on them running a media db check to get rid of any
# unwanted media. in the future we might also want to deduplicate this
diff --git a/aqt/main.py b/aqt/main.py
index 2bdce16b2..1b9e440ed 100644
--- a/aqt/main.py
+++ b/aqt/main.py
@@ -16,7 +16,7 @@ import aqt.progress
import aqt.webview
import aqt.toolbar
import aqt.stats
-from aqt.utils import restoreGeom, showInfo, showWarning,\
+from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \
openHelp, openLink, checkInvalidFilename
import anki.db
@@ -764,9 +764,9 @@ title="%s">%s''' % (
import aqt.importing
aqt.importing.onImport(self)
- def onExport(self):
+ def onExport(self, did=None):
import aqt.exporting
- aqt.exporting.ExportDialog(self)
+ aqt.exporting.ExportDialog(self, did=did)
# Cramming
##########################################################################
@@ -971,7 +971,9 @@ will be lost. Continue?"""))
diag.connect(box, SIGNAL("rejected()"), diag, SLOT("reject()"))
diag.setMinimumHeight(400)
diag.setMinimumWidth(500)
+ restoreGeom(diag, "checkmediadb")
diag.exec_()
+ saveGeom(diag, "checkmediadb")
def deleteUnused(self, unused, diag):
if not askUser(
@@ -980,7 +982,8 @@ will be lost. Continue?"""))
mdir = self.col.media.dir()
for f in unused:
path = os.path.join(mdir, f)
- send2trash(path)
+ if os.path.exists(path):
+ send2trash(path)
tooltip(_("Deleted."))
diag.close()
@@ -1003,10 +1006,12 @@ will be lost. Continue?"""))
self.progress.finish()
part1 = ngettext("%d card", "%d cards", len(cids)) % len(cids)
part1 = _("%s to delete:") % part1
- diag, box = showText(part1 + "\n\n" + report, run=False)
+ diag, box = showText(part1 + "\n\n" + report, run=False,
+ geomKey="emptyCards")
box.addButton(_("Delete Cards"), QDialogButtonBox.AcceptRole)
box.button(QDialogButtonBox.Close).setDefault(True)
def onDelete():
+ saveGeom(diag, "emptyCards")
QDialog.accept(diag)
self.checkpoint(_("Delete Empty"))
self.col.remCards(cids)
diff --git a/aqt/models.py b/aqt/models.py
index e09729922..a3c71afa9 100644
--- a/aqt/models.py
+++ b/aqt/models.py
@@ -115,7 +115,9 @@ class Models(QDialog):
self.connect(
frm.buttonBox, SIGNAL("helpRequested()"),
lambda: openHelp("latex"))
+ restoreGeom(d, "modelopts")
d.exec_()
+ saveGeom(d, "modelopts")
self.model['latexPre'] = unicode(frm.latexHeader.toPlainText())
self.model['latexPost'] = unicode(frm.latexFooter.toPlainText())
diff --git a/aqt/profiles.py b/aqt/profiles.py
index 4503240d6..a7472947c 100644
--- a/aqt/profiles.py
+++ b/aqt/profiles.py
@@ -88,8 +88,10 @@ class ProfileManager(object):
# can't translate, as lang not initialized
QMessageBox.critical(
None, "Error", """\
-Anki can't write to the harddisk. Please see the \
-documentation for information on using a flash drive.""")
+Anki could not create the folder %s. Please ensure that location is not \
+read-only and you have permission to write to it. If you cannot fix this \
+issue, please see the documentation for information on running Anki from \
+a flash drive.""" % self.base)
raise
# Profile load/save
diff --git a/aqt/reviewer.py b/aqt/reviewer.py
index c00bbf76c..40a5dcdc9 100644
--- a/aqt/reviewer.py
+++ b/aqt/reviewer.py
@@ -106,14 +106,19 @@ class Reviewer(object):
# Audio
##########################################################################
- def replayAudio(self):
+ def replayAudio(self, previewer=None):
+ if previewer:
+ state = previewer._previewState
+ c = previewer.card
+ else:
+ state = self.state
+ c = self.card
clearAudioQueue()
- c = self.card
- if self.state == "question":
+ if state == "question":
playFromText(c.q())
- elif self.state == "answer":
+ elif state == "answer":
txt = ""
- if self._replayq(c):
+ if self._replayq(c, previewer):
txt = c.q()
txt += c.a()
playFromText(txt)
@@ -218,9 +223,10 @@ The front of this card is empty. Please run Tools>Empty Cards.""")
return self.mw.col.decks.confForDid(
card.odid or card.did)['autoplay']
- def _replayq(self, card):
- return self.mw.col.decks.confForDid(
- self.card.odid or self.card.did).get('replayq', True)
+ def _replayq(self, card, previewer=None):
+ s = previewer if previewer else self
+ return s.mw.col.decks.confForDid(
+ s.card.odid or s.card.did).get('replayq', True)
def _toggleStar(self):
self.web.eval("_toggleStar(%s);" % json.dumps(
diff --git a/aqt/stats.py b/aqt/stats.py
index 93bf6601a..40f062f21 100644
--- a/aqt/stats.py
+++ b/aqt/stats.py
@@ -61,9 +61,14 @@ class DeckStats(QDialog):
painter = QPainter(image)
p.mainFrame().render(painter)
painter.end()
- image.save(path, "png")
+ isOK = image.save(path, "png")
+ if isOK:
+ showInfo(_("An image was saved to your desktop."))
+ else:
+ showInfo(_("""\
+Anki could not save the image. Please check that you have permission to write \
+to your desktop."""))
p.setViewportSize(oldsize)
- showInfo(_("An image was saved to your desktop."))
def changePeriod(self, n):
self.period = n
diff --git a/aqt/studydeck.py b/aqt/studydeck.py
index 81a96e477..636d6c4e1 100644
--- a/aqt/studydeck.py
+++ b/aqt/studydeck.py
@@ -118,6 +118,7 @@ class StudyDeck(QDialog):
QDialog.accept(self)
def reject(self):
+ saveGeom(self, self.geomKey)
remHook('reset', self.onReset)
if not self.cancel:
return self.accept()
diff --git a/aqt/utils.py b/aqt/utils.py
index 88ada7067..21312b232 100644
--- a/aqt/utils.py
+++ b/aqt/utils.py
@@ -47,7 +47,7 @@ def showInfo(text, parent=False, help="", type="info"):
b.setAutoDefault(False)
return mb.exec_()
-def showText(txt, parent=None, type="text", run=True):
+def showText(txt, parent=None, type="text", run=True, geomKey=None):
if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw
diag = QDialog(parent)
@@ -63,9 +63,15 @@ def showText(txt, parent=None, type="text", run=True):
layout.addWidget(text)
box = QDialogButtonBox(QDialogButtonBox.Close)
layout.addWidget(box)
- diag.connect(box, SIGNAL("rejected()"), diag, SLOT("reject()"))
+ def onReject():
+ if geomKey:
+ saveGeom(diag, geomKey)
+ QDialog.reject(diag)
+ diag.connect(box, SIGNAL("rejected()"), onReject)
diag.setMinimumHeight(400)
diag.setMinimumWidth(500)
+ if geomKey:
+ restoreGeom(diag, geomKey)
if run:
diag.exec_()
else:
@@ -280,7 +286,7 @@ def saveGeom(widget, key):
key += "Geom"
aqt.mw.pm.profile[key] = widget.saveGeometry()
-def restoreGeom(widget, key, offset=None):
+def restoreGeom(widget, key, offset=None, adjustSize=False):
key += "Geom"
if aqt.mw.pm.profile.get(key):
widget.restoreGeometry(aqt.mw.pm.profile[key])
@@ -289,6 +295,9 @@ def restoreGeom(widget, key, offset=None):
# bug in osx toolkit
s = widget.size()
widget.resize(s.width(), s.height()+offset*2)
+ else:
+ if adjustSize:
+ widget.adjustSize()
def saveState(widget, key):
key += "State"