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"