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
This commit is contained in:
Damien Elmes 2017-08-12 16:08:10 +10:00
parent 6bbd6d2dd5
commit 71101d041a
10 changed files with 199 additions and 57 deletions

View File

@ -187,3 +187,10 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
del d['col'] del d['col']
del d['timerStarted'] del d['timerStarted']
return pprint.pformat(d, width=300) 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

View File

@ -850,3 +850,11 @@ and queue = 0""", intTime(), self.usn())
def _closeLog(self): def _closeLog(self):
self._logHnd = None 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)

View File

@ -29,6 +29,7 @@ class Finder:
rated=self._findRated, rated=self._findRated,
tag=self._findTag, tag=self._findTag,
dupe=self._findDupes, dupe=self._findDupes,
flag=self._findFlag,
) )
self.search['is'] = self._findCardState self.search['is'] = self._findCardState
runHook("search", self.search) 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)""" % ( (c.queue = 1 and c.due <= %d)""" % (
self.col.sched.today, self.col.sched.dayCutoff) 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): def _findRated(self, args):
# days(:optional_ease) # days(:optional_ease)
(val, args) = args (val, args) = args

View File

@ -24,11 +24,6 @@ from aqt.webview import AnkiWebView
from anki.consts import * from anki.consts import *
from anki.sound import playFromText, clearAudioQueue from anki.sound import playFromText, clearAudioQueue
COLOUR_SUSPENDED = "#FFFFB2"
COLOUR_MARKED = "#D9B2E9"
# fixme: need to refresh after undo
# Data model # Data model
########################################################################## ##########################################################################
@ -318,6 +313,15 @@ class DataModel(QAbstractTableModel):
# Line painter # Line painter
###################################################################### ######################################################################
COLOUR_SUSPENDED = "#FFFFB2"
flagColours = {
1: "#F5B7B1",
2: "#BB8FCE",
3: "#82E0AA",
4: "#85C1E9",
}
class StatusDelegate(QItemDelegate): class StatusDelegate(QItemDelegate):
def __init__(self, browser, model): def __init__(self, browser, model):
@ -335,16 +339,19 @@ class StatusDelegate(QItemDelegate):
return return
finally: finally:
self.browser.mw.progress.blockUpdates = True self.browser.mw.progress.blockUpdates = True
col = None col = None
if c.note().hasTag("Marked"): if c.queue == -1:
col = COLOUR_MARKED
elif c.queue == -1:
col = COLOUR_SUSPENDED col = COLOUR_SUSPENDED
elif c.userFlag() > 0:
col = flagColours[c.userFlag()]
if col: if col:
brush = QBrush(QColor(col)) brush = QBrush(QColor(col))
painter.save() painter.save()
painter.fillRect(option.rect, brush) painter.fillRect(option.rect, brush)
painter.restore() painter.restore()
return QItemDelegate.paint(self, painter, option, index) return QItemDelegate.paint(self, painter, option, index)
# Browser window # Browser window
@ -403,7 +410,6 @@ class Browser(QMainWindow):
f.actionChangeModel.triggered.connect(self.onChangeModel) f.actionChangeModel.triggered.connect(self.onChangeModel)
f.actionFindDuplicates.triggered.connect(self.onFindDupes) f.actionFindDuplicates.triggered.connect(self.onFindDupes)
f.actionFindReplace.triggered.connect(self.onFindReplace) f.actionFindReplace.triggered.connect(self.onFindReplace)
f.actionToggle_Mark.triggered.connect(lambda: self.onMark())
f.actionDelete.triggered.connect(self.deleteNotes) f.actionDelete.triggered.connect(self.deleteNotes)
# cards # cards
f.actionChange_Deck.triggered.connect(self.setDeck) f.actionChange_Deck.triggered.connect(self.setDeck)
@ -411,6 +417,11 @@ class Browser(QMainWindow):
f.actionReposition.triggered.connect(self.reposition) f.actionReposition.triggered.connect(self.reposition)
f.actionReschedule.triggered.connect(self.reschedule) f.actionReschedule.triggered.connect(self.reschedule)
f.actionToggle_Suspend.triggered.connect(self.onSuspend) 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 # jumps
f.actionPreviousCard.triggered.connect(self.onPreviousCard) f.actionPreviousCard.triggered.connect(self.onPreviousCard)
f.actionNextCard.triggered.connect(self.onNextCard) f.actionNextCard.triggered.connect(self.onNextCard)
@ -743,11 +754,10 @@ by clicking on one on the left."""))
self._addTodayFilters(m) self._addTodayFilters(m)
self._addCardStateFilters(m) self._addCardStateFilters(m)
m.addSeparator()
self._addDeckFilters(m) self._addDeckFilters(m)
self._addNoteTypeFilters(m) self._addNoteTypeFilters(m)
self._addTagFilters(m) self._addTagFilters(m)
self._addSavedSearches(m) self._addSavedSearches(m)
m.exec_(self.form.filter.mapToGlobal(QPoint(0,0))) m.exec_(self.form.filter.mapToGlobal(QPoint(0,0)))
@ -793,10 +803,7 @@ by clicking on one on the left."""))
def _addCommonFilters(self, m): def _addCommonFilters(self, m):
items = ( items = (
(_("Whole Collection"), ""), (_("Whole Collection"), ""),
(_("Current Deck"), "deck:current"), (_("Current Deck"), "deck:current"))
None,
(_("Marked"), "tag:marked"),
(_("Leech"), "tag:leech"))
self._addSimpleFilters(m, items) self._addSimpleFilters(m, items)
def _addTodayFilters(self, m): def _addTodayFilters(self, m):
@ -816,7 +823,15 @@ by clicking on one on the left."""))
(_("Due"), "is:due"), (_("Due"), "is:due"),
None, None,
(_("Suspended"), "is:suspended"), (_("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) self._addSimpleFilters(m, items)
_tagsMenuSize = 30 _tagsMenuSize = 30
@ -847,8 +862,6 @@ by clicking on one on the left."""))
def _addTagFilterBlock(self, m, tags): def _addTagFilterBlock(self, m, tags):
for t in tags: for t in tags:
if t.lower() == "marked" or t.lower() == "leech":
continue
a = m.addAction(t) a = m.addAction(t)
a.triggered.connect(lambda *, tag=t: self.setFilter("tag", tag)) 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): def _clearUnusedTags(self):
self.col.tags.registerNotes() self.col.tags.registerNotes()
# Suspending and marking # Suspending
###################################################################### ######################################################################
def isSuspended(self): def isSuspended(self):
@ -1357,16 +1370,12 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
self.model.reset() self.model.reset()
self.mw.requireReset() self.mw.requireReset()
def isMarked(self): # Flags
return not not (self.card and self.card.note().hasTag("Marked")) ######################################################################
def onMark(self, mark=None): def onSetFlag(self, n):
if mark is None: self.col.setUserFlag(n, self.selectedCards())
mark = not self.isMarked() self.model.reset()
if mark:
self.addTags(tags="marked", label=False)
else:
self.deleteTags(tags="marked", label=False)
# Repositioning # Repositioning
###################################################################### ######################################################################

View File

@ -120,7 +120,7 @@ class Reviewer:
def revHtml(self): def revHtml(self):
extra = self.mw.col.conf.get("reviewExtra", "") extra = self.mw.col.conf.get("reviewExtra", "")
return f""" return f"""
<img src="/_anki/imgs/rating.png" id=star class=marked> <div id=_flag>&#x2691;</div>
<div id=qa></div> <div id=qa></div>
{extra} {extra}
""" """
@ -170,7 +170,7 @@ The front of this card is empty. Please run Tools>Empty Cards.""")
bodyclass = "card card%d" % (c.ord+1) bodyclass = "card card%d" % (c.ord+1)
self.web.eval("_showQuestion(%s,'%s');" % (json.dumps(q), bodyclass)) self.web.eval("_showQuestion(%s,'%s');" % (json.dumps(q), bodyclass))
self._toggleStar() self._drawFlag()
self._showAnswerButton() self._showAnswerButton()
# if we have a type answer field, focus main web # if we have a type answer field, focus main web
if self.typeCorrect: 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( return s.mw.col.decks.confForDid(
s.card.odid or s.card.did).get('replayq', True) s.card.odid or s.card.did).get('replayq', True)
def _toggleStar(self): def _drawFlag(self):
self.web.eval("_toggleStar(%s);" % json.dumps( self.web.eval("_drawFlag(%s);" % self.card.userFlag())
self.card.note().hasTag("marked")))
# Showing the answer # Showing the answer
########################################################################## ##########################################################################
@ -239,7 +238,11 @@ The front of this card is empty. Please run Tools>Empty Cards.""")
(Qt.Key_Enter, self.onEnterKey), (Qt.Key_Enter, self.onEnterKey),
("r", self.replayAudio), ("r", self.replayAudio),
(Qt.Key_F5, 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.onBuryNote),
("-", self.onBuryCard), ("-", self.onBuryCard),
("!", self.onSuspend), ("!", self.onSuspend),
@ -564,7 +567,13 @@ time = %(time)d;
# note the shortcuts listed here also need to be defined above # note the shortcuts listed here also need to be defined above
def showContextMenu(self): def showContextMenu(self):
opts = [ 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 Card"), "-", self.onBuryCard],
[_("Bury Note"), "=", self.onBuryNote], [_("Bury Note"), "=", self.onBuryNote],
[_("Suspend Card"), "@", self.onSuspendCard], [_("Suspend Card"), "@", self.onSuspendCard],
@ -577,30 +586,35 @@ time = %(time)d;
[_("Replay Own Voice"), "V", self.onReplayRecorded], [_("Replay Own Voice"), "V", self.onReplayRecorded],
] ]
m = QMenu(self.mw) 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: if not row:
m.addSeparator() m.addSeparator()
continue continue
if len(row) == 2:
subm = m.addMenu(row[0])
self._addMenuItems(subm, row[1])
continue
label, scut, func = row label, scut, func = row
a = m.addAction(label) a = m.addAction(label)
if scut: if scut:
a.setShortcut(QKeySequence(scut)) a.setShortcut(QKeySequence(scut))
a.triggered.connect(func) a.triggered.connect(func)
runHook("Reviewer.contextMenuEvent",self,m)
m.exec_(QCursor.pos())
def onOptions(self): def onOptions(self):
self.mw.onDeckConf(self.mw.col.decks.get( self.mw.onDeckConf(self.mw.col.decks.get(
self.card.odid or self.card.did)) self.card.odid or self.card.did))
def onMark(self): def setFlag(self, flag):
f = self.card.note() self.card.setUserFlag(flag)
if f.hasTag("marked"): self.card.flush()
f.delTag("marked") self._drawFlag()
else:
f.addTag("marked")
f.flush()
self._toggleStar()
def onSuspend(self): def onSuspend(self):
self.mw.checkpoint(_("Suspend")) self.mw.checkpoint(_("Suspend"))

View File

@ -268,6 +268,17 @@
<property name="title"> <property name="title">
<string>&amp;Cards</string> <string>&amp;Cards</string>
</property> </property>
<widget class="QMenu" name="menuFlag">
<property name="title">
<string>Flag</string>
</property>
<addaction name="actionRed_Flag"/>
<addaction name="actionPurple_Flag"/>
<addaction name="actionGreen_Flag"/>
<addaction name="actionBlue_Flag"/>
<addaction name="separator"/>
<addaction name="actionClear_Flag"/>
</widget>
<addaction name="actionChange_Deck"/> <addaction name="actionChange_Deck"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionReschedule"/> <addaction name="actionReschedule"/>
@ -275,6 +286,8 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionToggle_Suspend"/> <addaction name="actionToggle_Suspend"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="menuFlag"/>
<addaction name="separator"/>
<addaction name="action_Info"/> <addaction name="action_Info"/>
</widget> </widget>
<widget class="QMenu" name="menu_Notes"> <widget class="QMenu" name="menu_Notes">
@ -291,8 +304,6 @@
<addaction name="actionFindDuplicates"/> <addaction name="actionFindDuplicates"/>
<addaction name="actionFindReplace"/> <addaction name="actionFindReplace"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionToggle_Mark"/>
<addaction name="separator"/>
<addaction name="actionDelete"/> <addaction name="actionDelete"/>
</widget> </widget>
<addaction name="menuEdit"/> <addaction name="menuEdit"/>
@ -502,12 +513,44 @@
<string>Ctrl+D</string> <string>Ctrl+D</string>
</property> </property>
</action> </action>
<action name="actionToggle_Mark"> <action name="actionClear_Flag">
<property name="text"> <property name="text">
<string>Toggle Mark</string> <string>Clear Flag</string>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string>Ctrl+K</string> <string>Ctrl+0</string>
</property>
</action>
<action name="actionRed_Flag">
<property name="text">
<string>Red Flag</string>
</property>
<property name="shortcut">
<string>Ctrl+1</string>
</property>
</action>
<action name="actionPurple_Flag">
<property name="text">
<string>Purple Flag</string>
</property>
<property name="shortcut">
<string>Ctrl+2</string>
</property>
</action>
<action name="actionGreen_Flag">
<property name="text">
<string>Green Flag</string>
</property>
<property name="shortcut">
<string>Ctrl+3</string>
</property>
</action>
<action name="actionBlue_Flag">
<property name="text">
<string>Blue Flag</string>
</property>
<property name="shortcut">
<string>Ctrl+4</string>
</property> </property>
</action> </action>
</widget> </widget>

41
tests/test_flags.py Normal file
View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,7 +1,9 @@
hr { background-color:#ccc; margin: 1em; } hr { background-color:#ccc; margin: 1em; }
body { margin:1.5em; } body { margin:1.5em; }
img { max-width: 95%; max-height: 95%; } 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%; } #typeans { width: 100%; }
.typeGood { background: #0f0; } .typeGood { background: #0f0; }
.typeBad { background: #f00; } .typeBad { background: #f00; }

View File

@ -59,12 +59,21 @@ function _showAnswer(a, bodyclass) {
}); });
} }
function _toggleStar(show) { _flagColours = {
if (show) { 1: "red",
$(".marked").show(); 2: "purple",
} else { 3: "green",
$(".marked").hide(); 4: "blue"
};
function _drawFlag(flag) {
var elem = $("#_flag");
if (flag === 0) {
elem.hide();
return;
} }
elem.show();
elem.css("color", _flagColours[flag]);
} }
function _typeAnsPress() { function _typeAnsPress() {