From 71101d041a61a4c8f4ba1609f222b09ec33febaf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 12 Aug 2017 16:08:10 +1000 Subject: [PATCH] ditch marked tag in favour of card flags Users can now mark individual cards with one of four different coloured flags, instead of relying on a tag that applied to the whole note. - replaced marking functionality in reviewer and browser with new flag options - added flag:x search - marked and leech tags now show in normal tag list in filter screen, instead of being treated specially - the other clients will need updating to set and shown the flags, but flags set in the beta should be preserved by the other clients --- anki/cards.py | 7 +++++ anki/collection.py | 8 ++++++ anki/find.py | 9 ++++++ aqt/browser.py | 65 +++++++++++++++++++++++++------------------- aqt/reviewer.py | 50 ++++++++++++++++++++++------------ designer/browser.ui | 53 ++++++++++++++++++++++++++++++++---- tests/test_flags.py | 41 ++++++++++++++++++++++++++++ web/imgs/rating.png | Bin 2508 -> 0 bytes web/reviewer.css | 4 ++- web/reviewer.js | 19 +++++++++---- 10 files changed, 199 insertions(+), 57 deletions(-) create mode 100644 tests/test_flags.py delete mode 100644 web/imgs/rating.png diff --git a/anki/cards.py b/anki/cards.py index 90f80fa59..a0c5c5d54 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -187,3 +187,10 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""", del d['col'] del d['timerStarted'] return pprint.pformat(d, width=300) + + def userFlag(self): + return self.flags & 0b111 + + def setUserFlag(self, flag): + assert 0 <= flag <= 7 + self.flags = (self.flags & ~0b111) | flag diff --git a/anki/collection.py b/anki/collection.py index 7d644b6e6..6cec228db 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -850,3 +850,11 @@ and queue = 0""", intTime(), self.usn()) def _closeLog(self): self._logHnd = None + + # Card Flags + ########################################################################## + + def setUserFlag(self, flag, cids): + assert 0 <= flag <= 7 + self.db.execute("update cards set flags = (flags & ~?) | ? where id in %s" % + ids2str(cids), 0b111, flag) diff --git a/anki/find.py b/anki/find.py index b86319840..214f11644 100644 --- a/anki/find.py +++ b/anki/find.py @@ -29,6 +29,7 @@ class Finder: rated=self._findRated, tag=self._findTag, dupe=self._findDupes, + flag=self._findFlag, ) self.search['is'] = self._findCardState runHook("search", self.search) @@ -271,6 +272,14 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds (c.queue = 1 and c.due <= %d)""" % ( self.col.sched.today, self.col.sched.dayCutoff) + def _findFlag(self, args): + (val, args) = args + if not val or val not in "01234": + return + val = int(val) + mask = 2**3 - 1 + return "(c.flags & %d) == %d" % (mask, val) + def _findRated(self, args): # days(:optional_ease) (val, args) = args diff --git a/aqt/browser.py b/aqt/browser.py index f875873d4..f8f306b50 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -24,11 +24,6 @@ from aqt.webview import AnkiWebView from anki.consts import * from anki.sound import playFromText, clearAudioQueue -COLOUR_SUSPENDED = "#FFFFB2" -COLOUR_MARKED = "#D9B2E9" - -# fixme: need to refresh after undo - # Data model ########################################################################## @@ -318,6 +313,15 @@ class DataModel(QAbstractTableModel): # Line painter ###################################################################### +COLOUR_SUSPENDED = "#FFFFB2" + +flagColours = { + 1: "#F5B7B1", + 2: "#BB8FCE", + 3: "#82E0AA", + 4: "#85C1E9", +} + class StatusDelegate(QItemDelegate): def __init__(self, browser, model): @@ -335,16 +339,19 @@ class StatusDelegate(QItemDelegate): return finally: self.browser.mw.progress.blockUpdates = True + col = None - if c.note().hasTag("Marked"): - col = COLOUR_MARKED - elif c.queue == -1: + if c.queue == -1: col = COLOUR_SUSPENDED + elif c.userFlag() > 0: + col = flagColours[c.userFlag()] + if col: brush = QBrush(QColor(col)) painter.save() painter.fillRect(option.rect, brush) painter.restore() + return QItemDelegate.paint(self, painter, option, index) # Browser window @@ -403,7 +410,6 @@ class Browser(QMainWindow): f.actionChangeModel.triggered.connect(self.onChangeModel) f.actionFindDuplicates.triggered.connect(self.onFindDupes) f.actionFindReplace.triggered.connect(self.onFindReplace) - f.actionToggle_Mark.triggered.connect(lambda: self.onMark()) f.actionDelete.triggered.connect(self.deleteNotes) # cards f.actionChange_Deck.triggered.connect(self.setDeck) @@ -411,6 +417,11 @@ class Browser(QMainWindow): f.actionReposition.triggered.connect(self.reposition) f.actionReschedule.triggered.connect(self.reschedule) f.actionToggle_Suspend.triggered.connect(self.onSuspend) + f.actionRed_Flag.triggered.connect(lambda: self.onSetFlag(1)) + f.actionPurple_Flag.triggered.connect(lambda: self.onSetFlag(2)) + f.actionGreen_Flag.triggered.connect(lambda: self.onSetFlag(3)) + f.actionBlue_Flag.triggered.connect(lambda: self.onSetFlag(4)) + f.actionClear_Flag.triggered.connect(lambda: self.onSetFlag(0)) # jumps f.actionPreviousCard.triggered.connect(self.onPreviousCard) f.actionNextCard.triggered.connect(self.onNextCard) @@ -743,11 +754,10 @@ by clicking on one on the left.""")) self._addTodayFilters(m) self._addCardStateFilters(m) - m.addSeparator() - self._addDeckFilters(m) self._addNoteTypeFilters(m) self._addTagFilters(m) + self._addSavedSearches(m) m.exec_(self.form.filter.mapToGlobal(QPoint(0,0))) @@ -793,10 +803,7 @@ by clicking on one on the left.""")) def _addCommonFilters(self, m): items = ( (_("Whole Collection"), ""), - (_("Current Deck"), "deck:current"), - None, - (_("Marked"), "tag:marked"), - (_("Leech"), "tag:leech")) + (_("Current Deck"), "deck:current")) self._addSimpleFilters(m, items) def _addTodayFilters(self, m): @@ -816,7 +823,15 @@ by clicking on one on the left.""")) (_("Due"), "is:due"), None, (_("Suspended"), "is:suspended"), - (_("Buried"), "is:buried")) + (_("Buried"), "is:buried"), + None, + (_("Red Flag"), "flag:1"), + (_("Purple Flag"), "flag:2"), + (_("Green Flag"), "flag:3"), + (_("Blue Flag"), "flag:4"), + (_("No Flag"), "flag:0"), + (_("Any Flag"), "-flag:0"), + ) self._addSimpleFilters(m, items) _tagsMenuSize = 30 @@ -847,8 +862,6 @@ by clicking on one on the left.""")) def _addTagFilterBlock(self, m, tags): for t in tags: - if t.lower() == "marked" or t.lower() == "leech": - continue a = m.addAction(t) a.triggered.connect(lambda *, tag=t: self.setFilter("tag", tag)) @@ -1338,7 +1351,7 @@ update cards set usn=?, mod=?, did=? where id in """ + scids, def _clearUnusedTags(self): self.col.tags.registerNotes() - # Suspending and marking + # Suspending ###################################################################### def isSuspended(self): @@ -1357,16 +1370,12 @@ update cards set usn=?, mod=?, did=? where id in """ + scids, self.model.reset() self.mw.requireReset() - def isMarked(self): - return not not (self.card and self.card.note().hasTag("Marked")) + # Flags + ###################################################################### - def onMark(self, mark=None): - if mark is None: - mark = not self.isMarked() - if mark: - self.addTags(tags="marked", label=False) - else: - self.deleteTags(tags="marked", label=False) + def onSetFlag(self, n): + self.col.setUserFlag(n, self.selectedCards()) + self.model.reset() # Repositioning ###################################################################### diff --git a/aqt/reviewer.py b/aqt/reviewer.py index 79e64def8..452da6972 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -120,7 +120,7 @@ class Reviewer: def revHtml(self): extra = self.mw.col.conf.get("reviewExtra", "") return f""" - +
{extra} """ @@ -170,7 +170,7 @@ The front of this card is empty. Please run Tools>Empty Cards.""") bodyclass = "card card%d" % (c.ord+1) self.web.eval("_showQuestion(%s,'%s');" % (json.dumps(q), bodyclass)) - self._toggleStar() + self._drawFlag() self._showAnswerButton() # if we have a type answer field, focus main web if self.typeCorrect: @@ -187,9 +187,8 @@ The front of this card is empty. Please run Tools>Empty Cards.""") 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( - self.card.note().hasTag("marked"))) + def _drawFlag(self): + self.web.eval("_drawFlag(%s);" % self.card.userFlag()) # Showing the answer ########################################################################## @@ -239,7 +238,11 @@ The front of this card is empty. Please run Tools>Empty Cards.""") (Qt.Key_Enter, self.onEnterKey), ("r", self.replayAudio), (Qt.Key_F5, self.replayAudio), - ("*", self.onMark), + ("Ctrl+1", lambda: self.setFlag(1)), + ("Ctrl+2", lambda: self.setFlag(2)), + ("Ctrl+3", lambda: self.setFlag(3)), + ("Ctrl+4", lambda: self.setFlag(4)), + ("Ctrl+0", lambda: self.setFlag(0)), ("=", self.onBuryNote), ("-", self.onBuryCard), ("!", self.onSuspend), @@ -564,7 +567,13 @@ time = %(time)d; # note the shortcuts listed here also need to be defined above def showContextMenu(self): opts = [ - [_("Mark Note"), "*", self.onMark], + [_("Flag Card"), [ + [_("Red Flag"), "Ctrl+1", lambda: self.setFlag(1)], + [_("Purple Flag"), "Ctrl+2", lambda: self.setFlag(2)], + [_("Green Flag"), "Ctrl+3", lambda: self.setFlag(3)], + [_("Blue Flag"), "Ctrl+4", lambda: self.setFlag(4)], + [_("No Flag"), "Ctrl+5", lambda: self.setFlag(0)], + ]], [_("Bury Card"), "-", self.onBuryCard], [_("Bury Note"), "=", self.onBuryNote], [_("Suspend Card"), "@", self.onSuspendCard], @@ -577,30 +586,35 @@ time = %(time)d; [_("Replay Own Voice"), "V", self.onReplayRecorded], ] m = QMenu(self.mw) - for row in opts: + self._addMenuItems(m, opts) + + runHook("Reviewer.contextMenuEvent",self,m) + m.exec_(QCursor.pos()) + + def _addMenuItems(self, m, rows): + for row in rows: if not row: m.addSeparator() continue + if len(row) == 2: + subm = m.addMenu(row[0]) + self._addMenuItems(subm, row[1]) + continue label, scut, func = row a = m.addAction(label) if scut: a.setShortcut(QKeySequence(scut)) a.triggered.connect(func) - runHook("Reviewer.contextMenuEvent",self,m) - m.exec_(QCursor.pos()) + def onOptions(self): self.mw.onDeckConf(self.mw.col.decks.get( self.card.odid or self.card.did)) - def onMark(self): - f = self.card.note() - if f.hasTag("marked"): - f.delTag("marked") - else: - f.addTag("marked") - f.flush() - self._toggleStar() + def setFlag(self, flag): + self.card.setUserFlag(flag) + self.card.flush() + self._drawFlag() def onSuspend(self): self.mw.checkpoint(_("Suspend")) diff --git a/designer/browser.ui b/designer/browser.ui index 905f3f442..8c0af36bc 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -268,6 +268,17 @@ &Cards + + + Flag + + + + + + + + @@ -275,6 +286,8 @@ + + @@ -291,8 +304,6 @@ - - @@ -502,12 +513,44 @@ Ctrl+D - + - Toggle Mark + Clear Flag - Ctrl+K + Ctrl+0 + + + + + Red Flag + + + Ctrl+1 + + + + + Purple Flag + + + Ctrl+2 + + + + + Green Flag + + + Ctrl+3 + + + + + Blue Flag + + + Ctrl+4 diff --git a/tests/test_flags.py b/tests/test_flags.py new file mode 100644 index 000000000..8bfb38dee --- /dev/null +++ b/tests/test_flags.py @@ -0,0 +1,41 @@ +from tests.shared import assertException, getEmptyCol + +def test_flags(): + col = getEmptyCol() + n = col.newNote() + n['Front'] = "one"; n['Back'] = "two" + cnt = col.addNote(n) + c = n.cards()[0] + # make sure higher bits are preserved + origBits = 0b101 << 3 + c.flags = origBits + c.flush() + # no flags to start with + assert c.userFlag() == 0 + assert len(col.findCards("flag:0")) == 1 + assert len(col.findCards("flag:1")) == 0 + # set flag 2 + col.setUserFlag(2, [c.id]) + c.load() + print("db is", col.db.all("select id, flags from cards")) + assert c.userFlag() == 2 + assert c.flags & origBits == origBits + assert len(col.findCards("flag:0")) == 0 + assert len(col.findCards("flag:2")) == 1 + assert len(col.findCards("flag:3")) == 0 + # change to 3 + col.setUserFlag(3, [c.id]) + c.load() + assert c.userFlag() == 3 + # unset + col.setUserFlag(0, [c.id]) + c.load() + assert c.userFlag() == 0 + + # should work with Cards method as well + c.setUserFlag(2) + assert c.userFlag() == 2 + c.setUserFlag(3) + assert c.userFlag() == 3 + c.setUserFlag(0) + assert c.userFlag() == 0 diff --git a/web/imgs/rating.png b/web/imgs/rating.png deleted file mode 100644 index 4613cfae6494ffe9f734a62839c36ee0a2e7e238..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2508 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 T=5fw?1_0PvI#@KAp>F;UOjeML diff --git a/web/reviewer.css b/web/reviewer.css index ad40c1c52..b13f1ad48 100644 --- a/web/reviewer.css +++ b/web/reviewer.css @@ -1,7 +1,9 @@ hr { background-color:#ccc; margin: 1em; } body { margin:1.5em; } img { max-width: 95%; max-height: 95%; } -.marked { position:fixed; right: 7px; top: 7px; width: 24px; height: 24px; display: none; } +#_flag { + position:fixed; right: 7px; top: 0px; font-size: 30px; display: none; +} #typeans { width: 100%; } .typeGood { background: #0f0; } .typeBad { background: #f00; } diff --git a/web/reviewer.js b/web/reviewer.js index a225d8c61..5b195c134 100644 --- a/web/reviewer.js +++ b/web/reviewer.js @@ -59,12 +59,21 @@ function _showAnswer(a, bodyclass) { }); } -function _toggleStar(show) { - if (show) { - $(".marked").show(); - } else { - $(".marked").hide(); +_flagColours = { + 1: "red", + 2: "purple", + 3: "green", + 4: "blue" +}; + +function _drawFlag(flag) { + var elem = $("#_flag"); + if (flag === 0) { + elem.hide(); + return; } + elem.show(); + elem.css("color", _flagColours[flag]); } function _typeAnsPress() {