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 4613cfae6..000000000 Binary files a/web/imgs/rating.png and /dev/null differ 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() {