mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
filter menu tweaks
- make the chunking and menu code more generic - decks and note types now chunk menus too - chunked menu now shows start and end prefix
This commit is contained in:
parent
1b53ad4555
commit
b558712976
2 changed files with 166 additions and 97 deletions
181
aqt/browser.py
181
aqt/browser.py
|
@ -18,7 +18,7 @@ from anki.utils import fmtTimeSpan, ids2str, stripHTMLMedia, htmlToTextLine, isW
|
||||||
from aqt.utils import saveGeom, restoreGeom, saveSplitter, restoreSplitter, \
|
from aqt.utils import saveGeom, restoreGeom, saveSplitter, restoreSplitter, \
|
||||||
saveHeader, restoreHeader, saveState, restoreState, applyStyles, getTag, \
|
saveHeader, restoreHeader, saveState, restoreState, applyStyles, getTag, \
|
||||||
showInfo, askUser, tooltip, openHelp, showWarning, shortcut, mungeQA, \
|
showInfo, askUser, tooltip, openHelp, showWarning, shortcut, mungeQA, \
|
||||||
getOnlyText
|
getOnlyText, MenuList, SubMenu
|
||||||
from anki.hooks import runHook, addHook, remHook
|
from anki.hooks import runHook, addHook, remHook
|
||||||
from aqt.webview import AnkiWebView
|
from aqt.webview import AnkiWebView
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
@ -856,24 +856,24 @@ by clicking on one on the left."""))
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onFilterButton(self):
|
def onFilterButton(self):
|
||||||
m = QMenu()
|
ml = MenuList()
|
||||||
|
|
||||||
self._addCommonFilters(m)
|
ml.addChild(self._commonFilters())
|
||||||
m.addSeparator()
|
ml.addSeparator()
|
||||||
|
|
||||||
self._addTodayFilters(m)
|
ml.addChild(self._todayFilters())
|
||||||
self._addCardStateFilters(m)
|
ml.addChild(self._cardStateFilters())
|
||||||
self._addDeckFilters(m)
|
ml.addChild(self._deckFilters())
|
||||||
self._addNoteTypeFilters(m)
|
ml.addChild(self._noteTypeFilters())
|
||||||
self._addTagFilters(m)
|
ml.addChild(self._tagFilters())
|
||||||
|
ml.addSeparator()
|
||||||
|
|
||||||
m.addSeparator()
|
ml.addChild(self.sidebarDockWidget.toggleViewAction())
|
||||||
m.addAction(self.sidebarDockWidget.toggleViewAction())
|
ml.addSeparator()
|
||||||
m.addSeparator()
|
|
||||||
|
|
||||||
self._addSavedSearches(m)
|
ml.addChild(self._savedSearches())
|
||||||
|
|
||||||
m.exec_(self.form.filter.mapToGlobal(QPoint(0,0)))
|
ml.popupOver(self.form.filter)
|
||||||
|
|
||||||
def setFilter(self, *args):
|
def setFilter(self, *args):
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
|
@ -904,32 +904,35 @@ by clicking on one on the left."""))
|
||||||
self.form.searchEdit.lineEdit().setText(txt)
|
self.form.searchEdit.lineEdit().setText(txt)
|
||||||
self.onSearchActivated()
|
self.onSearchActivated()
|
||||||
|
|
||||||
def _addSimpleFilters(self, m, items):
|
def _simpleFilters(self, items):
|
||||||
|
ml = MenuList()
|
||||||
for row in items:
|
for row in items:
|
||||||
if row is None:
|
if row is None:
|
||||||
m.addSeparator()
|
ml.addSeparator()
|
||||||
else:
|
else:
|
||||||
label, search = row
|
label, filter = row
|
||||||
a = m.addAction(label)
|
ml.addItem(label, self._filterFunc(filter))
|
||||||
a.triggered.connect(lambda *, f=search: self.setFilter(f))
|
return ml
|
||||||
|
|
||||||
def _addCommonFilters(self, m):
|
def _filterFunc(self, *args):
|
||||||
items = (
|
return lambda *, f=args: self.setFilter(*f)
|
||||||
|
|
||||||
|
def _commonFilters(self):
|
||||||
|
return self._simpleFilters((
|
||||||
(_("Whole Collection"), ""),
|
(_("Whole Collection"), ""),
|
||||||
(_("Current Deck"), "deck:current"))
|
(_("Current Deck"), "deck:current")))
|
||||||
self._addSimpleFilters(m, items)
|
|
||||||
|
|
||||||
def _addTodayFilters(self, m):
|
def _todayFilters(self):
|
||||||
m = m.addMenu(_("Today"))
|
subm = SubMenu(_("Today"))
|
||||||
items = (
|
subm.addChild(self._simpleFilters((
|
||||||
(_("Added Today"), "added:1"),
|
(_("Added Today"), "added:1"),
|
||||||
(_("Studied Today"), "rated:1"),
|
(_("Studied Today"), "rated:1"),
|
||||||
(_("Again Today"), "rated:1:1"))
|
(_("Again Today"), "rated:1:1"))))
|
||||||
self._addSimpleFilters(m, items)
|
return subm
|
||||||
|
|
||||||
def _addCardStateFilters(self, m):
|
def _cardStateFilters(self):
|
||||||
m = m.addMenu(_("Card State"))
|
subm = SubMenu(_("Card State"))
|
||||||
items = (
|
subm.addChild(self._simpleFilters((
|
||||||
(_("New"), "is:new"),
|
(_("New"), "is:new"),
|
||||||
(_("Learning"), "is:learn"),
|
(_("Learning"), "is:learn"),
|
||||||
(_("Review"), "is:review"),
|
(_("Review"), "is:review"),
|
||||||
|
@ -944,112 +947,96 @@ by clicking on one on the left."""))
|
||||||
(_("Blue Flag"), "flag:4"),
|
(_("Blue Flag"), "flag:4"),
|
||||||
(_("No Flag"), "flag:0"),
|
(_("No Flag"), "flag:0"),
|
||||||
(_("Any Flag"), "-flag:0"),
|
(_("Any Flag"), "-flag:0"),
|
||||||
)
|
)))
|
||||||
self._addSimpleFilters(m, items)
|
return subm
|
||||||
|
|
||||||
_tagsMenuSize = 30
|
def _tagFilters(self):
|
||||||
|
m = SubMenu(_("Tags"))
|
||||||
|
|
||||||
def _addTagFilters(self, m):
|
m.addItem(_("Clear Unused"), self.clearUnusedTags)
|
||||||
m = m.addMenu(_("Tags"))
|
|
||||||
|
|
||||||
a = m.addAction(_("Clear Unused"))
|
|
||||||
a.triggered.connect(self.clearUnusedTags)
|
|
||||||
m.addSeparator()
|
m.addSeparator()
|
||||||
|
|
||||||
tags = sorted(self.col.tags.all(), key=lambda s: s.lower())
|
tagList = MenuList()
|
||||||
|
for t in sorted(self.col.tags.all(), key=lambda s: s.lower()):
|
||||||
|
tagList.addItem(t, self._filterFunc("tag", t))
|
||||||
|
|
||||||
if len(tags) < self._tagsMenuSize:
|
m.addChild(tagList.chunked())
|
||||||
self._addTagFilterBlock(m, tags)
|
return m
|
||||||
else:
|
|
||||||
# split the list into a more manageable size
|
|
||||||
chunks = []
|
|
||||||
while tags:
|
|
||||||
chunk = tags[:self._tagsMenuSize]
|
|
||||||
chunks.append(chunk)
|
|
||||||
del tags[:self._tagsMenuSize]
|
|
||||||
# use separate menu for each chunk
|
|
||||||
for chunk in chunks:
|
|
||||||
name = chunk[0]+"..."
|
|
||||||
child = m.addMenu(name)
|
|
||||||
self._addTagFilterBlock(child, chunk)
|
|
||||||
|
|
||||||
def _addTagFilterBlock(self, m, tags):
|
def _deckFilters(self):
|
||||||
for t in tags:
|
|
||||||
a = m.addAction(t)
|
|
||||||
a.triggered.connect(lambda *, tag=t: self.setFilter("tag", tag))
|
|
||||||
|
|
||||||
def _addDeckFilters(self, m):
|
|
||||||
def addDecks(parent, decks):
|
def addDecks(parent, decks):
|
||||||
for head, did, rev, lrn, new, children in decks:
|
for head, did, rev, lrn, new, children in decks:
|
||||||
name = self.mw.col.decks.get(did)['name']
|
name = self.mw.col.decks.get(did)['name']
|
||||||
shortname = name.split("::")[-1]
|
shortname = name.split("::")[-1]
|
||||||
if children:
|
if children:
|
||||||
newparent = parent.addMenu(shortname)
|
subm = parent.addMenu(shortname)
|
||||||
a = newparent.addAction(_("Filter"))
|
subm.addItem(_("Filter"), self._filterFunc("deck", name))
|
||||||
a.triggered.connect(
|
subm.addSeparator()
|
||||||
lambda *, name=name: self.setFilter("deck", name))
|
addDecks(subm, children)
|
||||||
newparent.addSeparator()
|
|
||||||
addDecks(newparent, children)
|
|
||||||
else:
|
else:
|
||||||
a = parent.addAction(shortname)
|
parent.addItem(shortname, self._filterFunc("deck", name))
|
||||||
a.triggered.connect(
|
|
||||||
lambda *, name=name: self.setFilter("deck", name))
|
|
||||||
|
|
||||||
# fixme: could rewrite to avoid calculating due # in the future
|
# fixme: could rewrite to avoid calculating due # in the future
|
||||||
alldecks = self.col.sched.deckDueTree()
|
alldecks = self.col.sched.deckDueTree()
|
||||||
root = m.addMenu(_("Decks"))
|
ml = MenuList()
|
||||||
|
addDecks(ml, alldecks)
|
||||||
|
|
||||||
addDecks(root, alldecks)
|
root = SubMenu(_("Decks"))
|
||||||
|
root.addChild(ml.chunked())
|
||||||
|
|
||||||
def _addNoteTypeFilters(self, m):
|
return root
|
||||||
m = m.addMenu(_("Note Types"))
|
|
||||||
a = m.addAction(_("Manage..."))
|
def _noteTypeFilters(self):
|
||||||
a.triggered.connect(self.mw.onNoteTypes)
|
m = SubMenu(_("Note Types"))
|
||||||
|
|
||||||
|
m.addItem(_("Manage..."), self.mw.onNoteTypes)
|
||||||
m.addSeparator()
|
m.addSeparator()
|
||||||
for nt in sorted(self.col.models.all(), key=itemgetter("name")):
|
|
||||||
|
noteTypes = MenuList()
|
||||||
|
for nt in sorted(self.col.models.all(), key=lambda nt: nt['name'].lower()):
|
||||||
# no sub menu if it's a single template
|
# no sub menu if it's a single template
|
||||||
if len(nt['tmpls']) == 1:
|
if len(nt['tmpls']) == 1:
|
||||||
a = m.addAction(nt['name'])
|
noteTypes.addItem(nt['name'], self._filterFunc("mid", str(nt['id'])))
|
||||||
a.triggered.connect(lambda *, nt=nt: self.setFilter("mid", str(nt['id'])))
|
|
||||||
else:
|
else:
|
||||||
subm = m.addMenu(nt['name'])
|
subm = noteTypes.addMenu(nt['name'])
|
||||||
a = subm.addAction(_("All Card Types"))
|
|
||||||
a.triggered.connect(lambda *, nt=nt: self.setFilter("mid", str(nt['id'])))
|
subm.addItem(_("All Card Types"), self._filterFunc("mid", str(nt['id'])))
|
||||||
|
subm.addSeparator()
|
||||||
|
|
||||||
# add templates
|
# add templates
|
||||||
subm.addSeparator()
|
|
||||||
for c, tmpl in enumerate(nt['tmpls']):
|
for c, tmpl in enumerate(nt['tmpls']):
|
||||||
a = subm.addAction(_("%(n)d: %(name)s") % dict(
|
name = _("%(n)d: %(name)s") % dict(n=c+1, name=tmpl['name'])
|
||||||
n=c+1, name=tmpl['name']))
|
subm.addItem(name, self._filterFunc(
|
||||||
a.triggered.connect(lambda *, nt=nt, c=c: self.setFilter(
|
"mid", str(nt['id']), "card", str(c+1)))
|
||||||
"mid", str(nt['id']), "card", str(c+1)
|
|
||||||
))
|
m.addChild(noteTypes.chunked())
|
||||||
|
return m
|
||||||
|
|
||||||
# Favourites
|
# Favourites
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def _addSavedSearches(self, m):
|
def _savedSearches(self):
|
||||||
|
ml = MenuList()
|
||||||
# make sure exists
|
# make sure exists
|
||||||
if "savedFilters" not in self.col.conf:
|
if "savedFilters" not in self.col.conf:
|
||||||
self.col.conf['savedFilters'] = {}
|
self.col.conf['savedFilters'] = {}
|
||||||
|
|
||||||
m.addSeparator()
|
ml.addSeparator()
|
||||||
|
|
||||||
if self._currentFilterIsSaved():
|
if self._currentFilterIsSaved():
|
||||||
a = m.addAction(_("Remove Current Filter..."))
|
ml.addItem(_("Remove Current Filter..."), self._onRemoveFilter)
|
||||||
a.triggered.connect(self._onRemoveFilter)
|
|
||||||
else:
|
else:
|
||||||
a = m.addAction(_("Save Current Filter..."))
|
ml.addItem(_("Save Current Filter..."), self._onSaveFilter)
|
||||||
a.triggered.connect(self._onSaveFilter)
|
|
||||||
|
|
||||||
saved = self.col.conf['savedFilters']
|
saved = self.col.conf['savedFilters']
|
||||||
if not saved:
|
if not saved:
|
||||||
return
|
return ml
|
||||||
|
|
||||||
m.addSeparator()
|
ml.addSeparator()
|
||||||
for name, filt in sorted(saved.items()):
|
for name, filt in sorted(saved.items()):
|
||||||
a = m.addAction(name)
|
ml.addItem(name, self._filterFunc(filt))
|
||||||
a.triggered.connect(lambda *, f=filt: self.setFilter(f))
|
|
||||||
|
return ml
|
||||||
|
|
||||||
def _onSaveFilter(self):
|
def _onSaveFilter(self):
|
||||||
name = getOnlyText(_("Please give your filter a name:"))
|
name = getOnlyText(_("Please give your filter a name:"))
|
||||||
|
|
82
aqt/utils.py
82
aqt/utils.py
|
@ -438,3 +438,85 @@ def checkInvalidFilename(str, dirsep=True):
|
||||||
bad)
|
bad)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Menus
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
class MenuList:
|
||||||
|
def __init__(self):
|
||||||
|
self.children = []
|
||||||
|
|
||||||
|
def addItem(self, title, func):
|
||||||
|
item = MenuItem(title, func)
|
||||||
|
self.children.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def addSeparator(self):
|
||||||
|
self.children.append(None)
|
||||||
|
|
||||||
|
def addMenu(self, title):
|
||||||
|
submenu = SubMenu(title)
|
||||||
|
self.children.append(submenu)
|
||||||
|
return submenu
|
||||||
|
|
||||||
|
def addChild(self, child):
|
||||||
|
self.children.append(child)
|
||||||
|
|
||||||
|
def renderTo(self, qmenu):
|
||||||
|
for child in self.children:
|
||||||
|
if child is None:
|
||||||
|
qmenu.addSeparator()
|
||||||
|
elif isinstance(child, QAction):
|
||||||
|
qmenu.addAction(child)
|
||||||
|
else:
|
||||||
|
child.renderTo(qmenu)
|
||||||
|
|
||||||
|
def popupOver(self, widget):
|
||||||
|
qmenu = QMenu()
|
||||||
|
self.renderTo(qmenu)
|
||||||
|
qmenu.exec_(widget.mapToGlobal(QPoint(0,0)))
|
||||||
|
|
||||||
|
# Chunking
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
chunkSize = 30
|
||||||
|
|
||||||
|
def chunked(self):
|
||||||
|
if len(self.children) <= self.chunkSize:
|
||||||
|
return self
|
||||||
|
|
||||||
|
newList = MenuList()
|
||||||
|
oldItems = self.children[:]
|
||||||
|
while oldItems:
|
||||||
|
chunk = oldItems[:self.chunkSize]
|
||||||
|
del oldItems[:self.chunkSize]
|
||||||
|
label = self._chunkLabel(chunk)
|
||||||
|
menu = newList.addMenu(label)
|
||||||
|
menu.children = chunk
|
||||||
|
return newList
|
||||||
|
|
||||||
|
def _chunkLabel(self, items):
|
||||||
|
start = items[0].title
|
||||||
|
end = items[-1].title
|
||||||
|
prefix = os.path.commonprefix([start.upper(), end.upper()])
|
||||||
|
n = len(prefix)+1
|
||||||
|
return f"{start[:n].upper()}-{end[:n].upper()}"
|
||||||
|
|
||||||
|
class SubMenu(MenuList):
|
||||||
|
def __init__(self, title):
|
||||||
|
super().__init__()
|
||||||
|
self.title = title
|
||||||
|
|
||||||
|
def renderTo(self, menu):
|
||||||
|
submenu = menu.addMenu(self.title)
|
||||||
|
super().renderTo(submenu)
|
||||||
|
|
||||||
|
class MenuItem:
|
||||||
|
def __init__(self, title, func):
|
||||||
|
self.title = title
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def renderTo(self, qmenu):
|
||||||
|
a = qmenu.addAction(self.title)
|
||||||
|
a.triggered.connect(self.func)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue