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:
parent
6bbd6d2dd5
commit
71101d041a
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
######################################################################
|
||||
|
@ -120,7 +120,7 @@ class Reviewer:
|
||||
def revHtml(self):
|
||||
extra = self.mw.col.conf.get("reviewExtra", "")
|
||||
return f"""
|
||||
<img src="/_anki/imgs/rating.png" id=star class=marked>
|
||||
<div id=_flag>⚑</div>
|
||||
<div id=qa></div>
|
||||
{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"))
|
||||
|
@ -268,6 +268,17 @@
|
||||
<property name="title">
|
||||
<string>&Cards</string>
|
||||
</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="separator"/>
|
||||
<addaction name="actionReschedule"/>
|
||||
@ -275,6 +286,8 @@
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionToggle_Suspend"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuFlag"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Info"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Notes">
|
||||
@ -291,8 +304,6 @@
|
||||
<addaction name="actionFindDuplicates"/>
|
||||
<addaction name="actionFindReplace"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionToggle_Mark"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionDelete"/>
|
||||
</widget>
|
||||
<addaction name="menuEdit"/>
|
||||
@ -502,12 +513,44 @@
|
||||
<string>Ctrl+D</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionToggle_Mark">
|
||||
<action name="actionClear_Flag">
|
||||
<property name="text">
|
||||
<string>Toggle Mark</string>
|
||||
<string>Clear Flag</string>
|
||||
</property>
|
||||
<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>
|
||||
</action>
|
||||
</widget>
|
||||
|
41
tests/test_flags.py
Normal file
41
tests/test_flags.py
Normal 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 |
@ -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; }
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user