Refactor search_string() and FilterToSearchIn

See #955.
This commit is contained in:
RumovZ 2021-01-29 18:27:33 +01:00
parent 349bd9d681
commit c299e271e8
16 changed files with 201 additions and 254 deletions

View File

@ -31,19 +31,19 @@ from anki.rsbackend import ( # pylint: disable=unused-import
ConcatSeparator,
DBError,
DupeIn,
FilterToSearchIn,
Flag,
FormatTimeSpanContext,
InvalidInput,
NamedFilter,
NoteIDs,
Progress,
RustBackend,
SearchTerm,
pb,
)
from anki.sched import Scheduler as V1Scheduler
from anki.schedv2 import Scheduler as V2Scheduler
from anki.tags import TagManager
from anki.utils import devMode, ids2str, intTime
from anki.utils import devMode, ids2str, intTime, splitFields, stripHTMLMedia
if TYPE_CHECKING:
from anki.rsbackend import FormatTimeSpanContextValue, TRValue
@ -460,8 +460,8 @@ class Collection:
)
return self.backend.search_cards(search=query, order=mode)
def find_notes(self, query: str) -> Sequence[int]:
return self.backend.search_notes(query)
def find_notes(self, *terms: Union[str, SearchTerm]) -> Sequence[int]:
return self.backend.search_notes(self.build_search_string(*terms))
def find_and_replace(
self,
@ -474,8 +474,39 @@ class Collection:
) -> int:
return anki.find.findReplace(self, nids, src, dst, regex, field, fold)
# returns array of ("dupestr", [nids])
def findDupes(self, fieldName: str, search: str = "") -> List[Tuple[Any, list]]:
return anki.find.findDupes(self, fieldName, search)
nids = self.findNotes(search, SearchTerm(field_name=fieldName))
# go through notes
vals: Dict[str, List[int]] = {}
dupes = []
fields: Dict[int, int] = {}
def ordForMid(mid):
if mid not in fields:
model = self.models.get(mid)
for c, f in enumerate(model["flds"]):
if f["name"].lower() == fieldName.lower():
fields[mid] = c
break
return fields[mid]
for nid, mid, flds in self.db.all(
"select id, mid, flds from notes where id in " + ids2str(nids)
):
flds = splitFields(flds)
ord = ordForMid(mid)
if ord is None:
continue
val = flds[ord]
val = stripHTMLMedia(val)
# empty does not count as duplicate
if not val:
continue
vals.setdefault(val, []).append(nid)
if len(vals[val]) == 2:
dupes.append((val, vals[val]))
return dupes
findCards = find_cards
findNotes = find_notes
@ -484,68 +515,35 @@ class Collection:
# Search Strings
##########################################################################
def search_string(
self,
*,
negate: bool = False,
concat_by_or: bool = False,
searches: Optional[List[str]] = None,
name: Optional["FilterToSearchIn.NamedFilterValue"] = None,
tag: Optional[str] = None,
deck: Optional[str] = None,
note: Optional[str] = None,
template: Optional[int] = None,
dupe: Optional[Tuple[int, str]] = None,
forgot_in_days: Optional[int] = None,
added_in_days: Optional[int] = None,
due_in_days: Optional[int] = None,
nids: Optional[List[int]] = None,
field_name: Optional[str] = None,
def build_search_string(
self, *terms: Union[str, SearchTerm], negate=False, match_any=False
) -> str:
"""Helper function for the backend's search string operations.
Pass search strings as 'search_strings' to normalize.
Pass multiple to concatenate (defaults to 'and').
Pass terms as strings to normalize.
Pass fields of backend.proto/FilterToSearchIn as valid SearchTerms.
Pass multiple terms to concatenate (defaults to 'and', 'or' when 'match_any=True').
Pass 'negate=True' to negate the end result.
May raise InvalidInput.
"""
def append_filter(filter_in):
filters.append(self.backend.filter_to_search(filter_in))
if name:
append_filter(FilterToSearchIn(name=name))
if tag:
append_filter(FilterToSearchIn(tag=tag))
if deck:
append_filter(FilterToSearchIn(deck=deck))
if note:
append_filter(FilterToSearchIn(note=note))
if template:
append_filter(FilterToSearchIn(template=template))
if dupe:
dupe_in = DupeIn(mid=BackendNoteTypeID(ntid=dupe[0]), text=dupe[1])
append_filter(FilterToSearchIn(dupe=dupe_in))
if forgot_in_days:
append_filter(FilterToSearchIn(forgot_in_days=forgot_in_days))
if added_in_days:
append_filter(FilterToSearchIn(added_in_days=added_in_days))
if due_in_days:
append_filter(FilterToSearchIn(due_in_days=due_in_days))
if nids:
append_filter(FilterToSearchIn(nids=NoteIDs(nids=nids)))
if field_name:
append_filter(FilterToSearchIn(field_name=field_name))
if concat_by_or:
searches = []
for term in terms:
if isinstance(term, SearchTerm):
term = self.backend.filter_to_search(term)
searches.append(term)
if match_any:
sep = ConcatSeparator.OR
else:
sep = ConcatSeparator.AND
search_string = self.backend.concatenate_searches(sep=sep, searches=filters)
search_string = self.backend.concatenate_searches(sep=sep, searches=searches)
if negate:
search_string = self.backend.negate_search(search_string)
return search_string
def replace_search_term(self, search: str, replacement: str) -> str:
"""Wrapper for the according backend function."""
return self.backend.replace_search_term(search=search, replacement=replacement)
# Config
@ -788,5 +786,18 @@ table.review-log {{ {revlog_style} }}
)
def dupe_search_term(mid: int, text: str) -> SearchTerm:
"""Helper function for building a DupeIn message."""
dupe_in = DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text)
return SearchTerm(dupe=dupe_in)
def nid_search_term(nids: List[int]) -> SearchTerm:
"""Helper function for building a NoteIDs message."""
return SearchTerm(nids=NoteIDs(nids=nids))
# legacy name
_Collection = Collection

View File

@ -3,10 +3,9 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Set, Tuple
from typing import TYPE_CHECKING, Optional, Set
from anki.hooks import *
from anki.utils import ids2str, splitFields, stripHTMLMedia
if TYPE_CHECKING:
from anki.collection import Collection
@ -64,41 +63,3 @@ def fieldNames(col, downcase=True) -> List:
if name not in fields: # slower w/o
fields.add(name)
return list(fields)
# returns array of ("dupestr", [nids])
def findDupes(
col: Collection, fieldName: str, search: str = ""
) -> List[Tuple[Any, List]]:
# limit search to notes with applicable field name
search = col.search_string(searches=[search], field_name=fieldName)
# go through notes
vals: Dict[str, List[int]] = {}
dupes = []
fields: Dict[int, int] = {}
def ordForMid(mid):
if mid not in fields:
model = col.models.get(mid)
for c, f in enumerate(model["flds"]):
if f["name"].lower() == fieldName.lower():
fields[mid] = c
break
return fields[mid]
for nid, mid, flds in col.db.all(
"select id, mid, flds from notes where id in " + ids2str(col.findNotes(search))
):
flds = splitFields(flds)
ord = ordForMid(mid)
if ord is None:
continue
val = flds[ord]
val = stripHTMLMedia(val)
# empty does not count as duplicate
if not val:
continue
vals.setdefault(val, []).append(nid)
if len(vals[val]) == 2:
dupes.append((val, vals[val]))
return dupes

View File

@ -47,8 +47,8 @@ TagTreeNode = pb.TagTreeNode
NoteType = pb.NoteType
DeckTreeNode = pb.DeckTreeNode
StockNoteType = pb.StockNoteType
FilterToSearchIn = pb.FilterToSearchIn
NamedFilter = pb.FilterToSearchIn.NamedFilter
SearchTerm = pb.FilterToSearchIn
Flag = pb.FilterToSearchIn.Flag
DupeIn = pb.FilterToSearchIn.DupeIn
NoteIDs = pb.NoteIDs
BackendNoteTypeID = pb.NoteTypeID

View File

@ -16,7 +16,7 @@ import re
from typing import Collection, List, Optional, Sequence, Tuple
import anki # pylint: disable=unused-import
from anki.rsbackend import FilterToSearchIn
from anki.collection import SearchTerm
from anki.utils import ids2str
@ -87,8 +87,7 @@ class TagManager:
def rename_tag(self, old: str, new: str) -> int:
"Rename provided tag, returning number of changed notes."
search = self.col.backend.filter_to_search(FilterToSearchIn(tag=old))
nids = self.col.find_notes(search)
nids = self.col.find_notes(SearchTerm(tag=old))
if not nids:
return 0
escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old)

View File

@ -7,6 +7,7 @@ import aqt.deckchooser
import aqt.editor
import aqt.forms
import aqt.modelchooser
from anki.collection import nid_search_term
from anki.consts import MODEL_CLOZE
from anki.notes import Note
from anki.utils import htmlToTextLine, isMac
@ -144,7 +145,7 @@ class AddCards(QDialog):
def onHistory(self) -> None:
m = QMenu(self)
for nid in self.history:
if self.mw.col.findNotes(self.mw.col.search_string(nids=[nid])):
if self.mw.col.findNotes(nid_search_term([nid])):
note = self.mw.col.getNote(nid)
fields = note.fields
txt = htmlToTextLine(", ".join(fields))
@ -161,7 +162,7 @@ class AddCards(QDialog):
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
def editHistory(self, nid):
self.mw.browser_search(nids=[nid])
self.mw.browser_search(nid_search_term([nid]))
def addNote(self, note) -> Optional[Note]:
note.model()["did"] = self.deckChooser.selectedId()

View File

@ -13,7 +13,7 @@ from typing import List, Optional, Sequence, Tuple, cast
import aqt
import aqt.forms
from anki.cards import Card
from anki.collection import Collection, InvalidInput, NamedFilter
from anki.collection import Collection, Flag, InvalidInput, SearchTerm, nid_search_term
from anki.consts import *
from anki.lang import without_unicode_isolation
from anki.models import NoteType
@ -612,7 +612,9 @@ class Browser(QMainWindow):
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None)
self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH)
self._searchPromptFilter = self.col.search_string(name=NamedFilter.CURRENT_DECK)
self._searchPromptFilter = self.col.build_search_string(
SearchTerm(current_deck=True)
)
self.form.searchEdit.addItems(
[self._searchPrompt] + self.mw.pm.profile["searchHistory"]
)
@ -659,7 +661,7 @@ class Browser(QMainWindow):
c = self.card = self.mw.reviewer.card
nid = c and c.nid or 0
if nid:
search = self.col.search_string(nids=[nid])
search = self.col.build_search_string(nid_search_term([nid]))
search = gui_hooks.default_search(search, c)
self.model.search(search)
self.focusCid(c.id)
@ -671,7 +673,7 @@ class Browser(QMainWindow):
self._onRowChanged(None, None)
def normalize_search(self, search: str) -> str:
normed = self.col.search_string(searches=[search])
normed = self.col.build_search_string(search)
self._lastSearchTxt = normed
self.form.searchEdit.lineEdit().setText(normed)
return normed
@ -951,23 +953,21 @@ QTableView {{ gridline-color: {grid} }}
ml.popupOver(self.form.filter)
def update_search(self, *terms: str):
def update_search(self, *terms: Union[str, SearchTerm]):
"Modify the current search string based on modified keys, then refresh."
try:
search = self.col.search_string(searches=list(terms))
search = self.col.build_search_string(*terms)
mods = self.mw.app.keyboardModifiers()
if mods & Qt.AltModifier:
search = self.col.search_string(negate=True, searches=[search])
search = self.col.build_search_string(search, negate=True)
cur = str(self.form.searchEdit.lineEdit().text())
if cur != self._searchPrompt:
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
search = self.col.replace_search_term(cur, search)
elif mods & Qt.ControlModifier:
search = self.col.search_string(searches=[cur, search])
search = self.col.build_search_string(cur, search)
elif mods & Qt.ShiftModifier:
search = self.col.search_string(
concat_by_or=True, searches=[cur, search]
)
search = self.col.build_search_string(cur, search, match_any=True)
except InvalidInput as e:
show_invalid_search_error(e)
else:
@ -993,9 +993,9 @@ QTableView {{ gridline-color: {grid} }}
subm.addChild(
self._simpleFilters(
(
(tr(TR.BROWSING_ADDED_TODAY), NamedFilter.ADDED_TODAY),
(tr(TR.BROWSING_STUDIED_TODAY), NamedFilter.STUDIED_TODAY),
(tr(TR.BROWSING_AGAIN_TODAY), NamedFilter.AGAIN_TODAY),
(tr(TR.BROWSING_ADDED_TODAY), SearchTerm(added_in_days=1)),
(tr(TR.BROWSING_STUDIED_TODAY), SearchTerm(studied_today=True)),
(tr(TR.BROWSING_AGAIN_TODAY), SearchTerm(forgot_in_days=1)),
)
)
)
@ -1006,20 +1006,20 @@ QTableView {{ gridline-color: {grid} }}
subm.addChild(
self._simpleFilters(
(
(tr(TR.ACTIONS_NEW), NamedFilter.NEW),
(tr(TR.SCHEDULING_LEARNING), NamedFilter.LEARN),
(tr(TR.SCHEDULING_REVIEW), NamedFilter.REVIEW),
(tr(TR.FILTERING_IS_DUE), NamedFilter.DUE),
(tr(TR.ACTIONS_NEW), SearchTerm(new=True)),
(tr(TR.SCHEDULING_LEARNING), SearchTerm(learn=True)),
(tr(TR.SCHEDULING_REVIEW), SearchTerm(review=True)),
(tr(TR.FILTERING_IS_DUE), SearchTerm(due=True)),
None,
(tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED),
(tr(TR.BROWSING_BURIED), NamedFilter.BURIED),
(tr(TR.BROWSING_SUSPENDED), SearchTerm(suspended=True)),
(tr(TR.BROWSING_BURIED), SearchTerm(buried=True)),
None,
(tr(TR.ACTIONS_RED_FLAG), NamedFilter.RED_FLAG),
(tr(TR.ACTIONS_ORANGE_FLAG), NamedFilter.ORANGE_FLAG),
(tr(TR.ACTIONS_GREEN_FLAG), NamedFilter.GREEN_FLAG),
(tr(TR.ACTIONS_BLUE_FLAG), NamedFilter.BLUE_FLAG),
(tr(TR.BROWSING_NO_FLAG), NamedFilter.NO_FLAG),
(tr(TR.BROWSING_ANY_FLAG), NamedFilter.ANY_FLAG),
(tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=Flag.RED)),
(tr(TR.ACTIONS_ORANGE_FLAG), SearchTerm(flag=Flag.ORANGE)),
(tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=Flag.GREEN)),
(tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=Flag.BLUE)),
(tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=Flag.WITHOUT)),
(tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=Flag.ANY)),
)
)
)
@ -1045,9 +1045,7 @@ QTableView {{ gridline-color: {grid} }}
def _onSaveFilter(self) -> None:
try:
filt = self.col.search_string(
searches=[self.form.searchEdit.lineEdit().text()]
)
filt = self.col.build_search_string(self.form.searchEdit.lineEdit().text())
except InvalidInput as e:
show_invalid_search_error(e)
else:
@ -1088,12 +1086,12 @@ QTableView {{ gridline-color: {grid} }}
def _currentFilterIsSaved(self) -> Optional[str]:
filt = self.form.searchEdit.lineEdit().text()
try:
filt = self.col.search_string(searches=[filt])
filt = self.col.build_search_string(filt)
except InvalidInput:
pass
for k, v in self.col.get_config("savedFilters").items():
try:
v = self.col.search_string(searches=[v])
v = self.col.build_search_string(v)
except InvalidInput:
pass
if filt == v:
@ -1494,7 +1492,7 @@ where id in %s"""
tv = self.form.tableView
tv.selectionModel().clear()
search = self.col.search_string(nids=nids)
search = self.col.build_search_string(nid_search_term(nids))
self.search_for(search)
tv.selectAll()
@ -1703,7 +1701,7 @@ where id in %s"""
t += (
"""<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>"""
% (
self.col.search_string(nids=nids).replace('"', "&quot;"),
html.escape(self.col.build_search_string(nid_search_term(nids))),
tr(TR.BROWSING_NOTE_COUNT, count=len(nids)),
html.escape(val),
)

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt
from anki.collection import NamedFilter
from anki.collection import SearchTerm
from anki.consts import *
from aqt.qt import *
from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr
@ -160,29 +160,33 @@ class CustomStudy(QDialog):
dyn = self.mw.col.decks.get(did)
# and then set various options
if i == RADIO_FORGOT:
search = self.mw.col.search_string(forgot_in_days=spin)
search = self.mw.col.build_search_string(SearchTerm(forgot_in_days=spin))
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM]
dyn["resched"] = False
elif i == RADIO_AHEAD:
search = self.mw.col.search_string(due_in_days=spin)
search = self.mw.col.build_search_string(SearchTerm(due_in_days=spin))
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_DUE]
dyn["resched"] = True
elif i == RADIO_PREVIEW:
search = self.mw.col.search_string(name=NamedFilter.NEW, added_in_days=spin)
search = self.mw.col.build_search_string(
SearchTerm(new=True), SearchTerm(added_in_days=spin)
)
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST]
dyn["resched"] = False
elif i == RADIO_CRAM:
type = f.cardType.currentRow()
if type == TYPE_NEW:
terms = self.mw.col.search_string(name=NamedFilter.NEW)
terms = self.mw.col.build_search_string(SearchTerm(new=True))
ord = DYN_ADDED
dyn["resched"] = True
elif type == TYPE_DUE:
terms = self.mw.col.search_string(name=NamedFilter.DUE)
terms = self.mw.col.build_search_string(SearchTerm(due=True))
ord = DYN_DUE
dyn["resched"] = True
elif type == TYPE_REVIEW:
terms = self.mw.col.search_string(negate=True, name=NamedFilter.NEW)
terms = self.mw.col.build_search_string(
SearchTerm(new=True), negate=True
)
ord = DYN_RANDOM
dyn["resched"] = True
else:
@ -191,8 +195,8 @@ class CustomStudy(QDialog):
dyn["resched"] = False
dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
# add deck limit
dyn["terms"][0][0] = self.mw.col.search_string(
deck=self.deck["name"], searches=[dyn["terms"][0][0]]
dyn["terms"][0][0] = self.mw.col.build_search_string(
dyn["terms"][0][0], SearchTerm(deck=self.deck["name"])
)
self.mw.col.decks.save(dyn)
# generate cards

View File

@ -4,7 +4,7 @@
from typing import List, Optional
import aqt
from anki.collection import InvalidInput, NamedFilter
from anki.collection import InvalidInput, SearchTerm
from anki.lang import without_unicode_isolation
from aqt.qt import *
from aqt.utils import (
@ -47,11 +47,9 @@ class DeckConf(QDialog):
self.initialSetup()
self.loadConf()
if search:
search = self.mw.col.search_string(searches=[search], name=NamedFilter.DUE)
search = self.mw.col.build_search_string(search, SearchTerm(due=True))
self.form.search.setText(search)
search_2 = self.mw.col.search_string(
searches=[search], name=NamedFilter.NEW
)
search_2 = self.mw.col.build_search_string(search, SearchTerm(new=True))
self.form.search_2.setText(search_2)
self.form.search.selectAll()
@ -123,11 +121,11 @@ class DeckConf(QDialog):
else:
d["delays"] = None
search = self.mw.col.search_string(searches=[f.search.text()])
search = self.mw.col.build_search_string(f.search.text())
terms = [[search, f.limit.value(), f.order.currentIndex()]]
if f.secondFilter.isChecked():
search_2 = self.mw.col.search_string(searches=[f.search_2.text()])
search_2 = self.mw.col.build_search_string(f.search_2.text())
terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()])
d["terms"] = terms

View File

@ -21,6 +21,7 @@ from bs4 import BeautifulSoup
import aqt
import aqt.sound
from anki.cards import Card
from anki.collection import dupe_search_term
from anki.hooks import runFilter
from anki.httpclient import HttpClient
from anki.notes import Note
@ -539,7 +540,9 @@ class Editor:
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
def showDupes(self):
self.mw.browser_search(dupe=(self.note.model()["id"], self.note.fields[0]))
self.mw.browser_search(
dupe_search_term(self.note.model()["id"], self.note.fields[0])
)
def fieldsAreBlank(self, previousNote=None):
if not self.note:

View File

@ -66,7 +66,7 @@ class EmptyCardsDialog(QDialog):
self._delete_button.clicked.connect(self._on_delete)
def _on_note_link_clicked(self, link):
self.mw.browser_search(searches=[link])
self.mw.browser_search(link)
def _on_delete(self):
self.mw.progress.start()

View File

@ -26,7 +26,7 @@ import aqt.stats
import aqt.toolbar
import aqt.webview
from anki import hooks
from anki.collection import Collection
from anki.collection import Collection, SearchTerm
from anki.decks import Deck
from anki.hooks import runHook
from anki.lang import without_unicode_isolation
@ -1141,7 +1141,7 @@ title="%s" %s>%s</button>""" % (
deck = self.col.decks.current()
if not search:
if not deck["dyn"]:
search = self.col.search_string(deck=deck["name"])
search = self.col.build_search_string(SearchTerm(deck=deck["name"]))
while self.col.decks.id_for_name(
without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n))
):
@ -1621,10 +1621,10 @@ title="%s" %s>%s</button>""" % (
# Helpers for all windows
##########################################################################
def browser_search(self, **kwargs) -> None:
"""Wrapper for col.search_string() to look up the result in the browser."""
def browser_search(self, *terms: Union[str, SearchTerm]) -> None:
"""Wrapper for col.build_search_string() to look up the result in the browser."""
search = self.col.search_string(**kwargs)
search = self.col.build_search_string(*terms)
browser = aqt.dialogs.open("Browser", self)
browser.form.searchEdit.lineEdit().setText(search)
browser.onSearchActivated()

View File

@ -9,6 +9,7 @@ from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar
import aqt
from anki.collection import nid_search_term
from anki.rsbackend import TR, Interrupted, ProgressKind, pb
from aqt.qt import *
from aqt.utils import (
@ -145,7 +146,7 @@ class MediaChecker:
if out is not None:
nid, err = out
self.mw.browser_search(nids=[nid])
self.mw.browser_search(nid_search_term([nid]))
showText(err, type="html")
else:
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))

View File

@ -7,6 +7,7 @@ from dataclasses import dataclass
from typing import Optional
import aqt
from anki.collection import SearchTerm
from aqt import gui_hooks
from aqt.sound import av_player
from aqt.toolbar import BottomBar
@ -72,7 +73,7 @@ class Overview:
self.mw.onDeckConf()
elif url == "cram":
deck = self.mw.col.decks.current()["name"]
self.mw.onCram(self.mw.col.search_string(deck=deck))
self.mw.onCram(self.mw.col.build_search_string(SearchTerm(deck=deck)))
elif url == "refresh":
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()

View File

@ -9,10 +9,7 @@ from enum import Enum
from typing import Iterable, List, Optional
import aqt
from anki.collection import ( # pylint: disable=unused-import
FilterToSearchIn,
NamedFilter,
)
from anki.collection import SearchTerm
from anki.errors import DeckRenameError
from anki.rsbackend import DeckTreeNode, TagTreeNode
from aqt import gui_hooks
@ -294,14 +291,14 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
tr(TR.BROWSING_WHOLE_COLLECTION),
":/icons/collection.svg",
self._named_filter(NamedFilter.WHOLE_COLLECTION),
self._filter_func(SearchTerm(whole_collection=True)),
item_type=SidebarItemType.COLLECTION,
)
root.addChild(item)
item = SidebarItem(
tr(TR.BROWSING_CURRENT_DECK),
":/icons/deck.svg",
self._named_filter(NamedFilter.CURRENT_DECK),
self._filter_func(SearchTerm(current_deck=True)),
item_type=SidebarItemType.CURRENT_DECK,
)
root.addChild(item)
@ -313,7 +310,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
name,
":/icons/heart.svg",
self._saved_filter(filt),
self._filter_func(filt),
item_type=SidebarItemType.FILTER,
)
root.addChild(item)
@ -333,7 +330,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
":/icons/tag.svg",
self._tag_filter(head + node.name),
self._filter_func(SearchTerm(tag=head + node.name)),
toggle_expand(),
not node.collapsed,
item_type=SidebarItemType.TAG,
@ -358,7 +355,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
":/icons/deck.svg",
self._deck_filter(head + node.name),
self._filter_func(SearchTerm(deck=head + node.name)),
toggle_expand(),
not node.collapsed,
item_type=SidebarItemType.DECK,
@ -377,7 +374,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
nt["name"],
":/icons/notetype.svg",
self._note_filter(nt["name"]),
self._filter_func(SearchTerm(note=nt["name"])),
item_type=SidebarItemType.NOTETYPE,
id=nt["id"],
)
@ -386,32 +383,17 @@ class SidebarTreeView(QTreeView):
child = SidebarItem(
tmpl["name"],
":/icons/notetype.svg",
self._template_filter(nt["name"], c),
self._filter_func(
SearchTerm(note=nt["name"]), SearchTerm(template=c)
),
item_type=SidebarItemType.TEMPLATE,
)
item.addChild(child)
root.addChild(item)
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
return lambda: self.browser.update_search(self.col.search_string(name=name))
def _tag_filter(self, tag: str) -> Callable:
return lambda: self.browser.update_search(self.col.search_string(tag=tag))
def _deck_filter(self, deck: str) -> Callable:
return lambda: self.browser.update_search(self.col.search_string(deck=deck))
def _note_filter(self, note: str) -> Callable:
return lambda: self.browser.update_search(self.col.search_string(note=note))
def _template_filter(self, note: str, template: int) -> Callable:
return lambda: self.browser.update_search(
self.col.search_string(note=note), self.col.search_string(template=template)
)
def _saved_filter(self, saved: str) -> Callable:
return lambda: self.browser.update_search(saved)
def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
return lambda: self.browser.update_search(self.col.build_search_string(*terms))
# Context menu actions
###########################

View File

@ -765,41 +765,39 @@ message BuiltinSearchOrder {
}
message FilterToSearchIn {
enum NamedFilter {
WHOLE_COLLECTION = 0;
CURRENT_DECK = 1;
ADDED_TODAY = 2;
STUDIED_TODAY = 3;
AGAIN_TODAY = 4;
NEW = 5;
LEARN = 6;
REVIEW = 7;
DUE = 8;
SUSPENDED = 9;
BURIED = 10;
RED_FLAG = 11;
ORANGE_FLAG = 12;
GREEN_FLAG = 13;
BLUE_FLAG = 14;
NO_FLAG = 15;
ANY_FLAG = 16;
}
message DupeIn {
NoteTypeID mid = 1;
string text = 2;
}
enum Flag {
WITHOUT = 0;
ANY = 1;
RED = 2;
ORANGE = 3;
GREEN = 4;
BLUE = 5;
}
oneof filter {
NamedFilter name = 1;
string tag = 2;
string deck = 3;
string note = 4;
uint32 template = 5;
string tag = 1;
string deck = 2;
string note = 3;
uint32 template = 4;
NoteIDs nids = 5;
DupeIn dupe = 6;
uint32 forgot_in_days = 7;
uint32 added_in_days = 8;
int32 due_in_days = 9;
NoteIDs nids = 10;
string field_name = 11;
string field_name = 7;
uint32 forgot_in_days = 8;
uint32 added_in_days = 9;
int32 due_in_days = 10;
bool whole_collection = 11;
bool current_deck = 12;
bool studied_today = 13;
bool new = 14;
bool learn = 15;
bool review = 16;
bool due = 17;
bool suspended = 18;
bool buried = 19;
Flag flag = 20;
}
}

View File

@ -293,38 +293,8 @@ impl From<pb::DeckConfigId> for DeckConfID {
impl From<pb::FilterToSearchIn> for Node<'_> {
fn from(msg: pb::FilterToSearchIn) -> Self {
use pb::filter_to_search_in::Filter;
use pb::filter_to_search_in::NamedFilter;
match msg
.filter
.unwrap_or(Filter::Name(NamedFilter::WholeCollection as i32))
{
Filter::Name(name) => {
match NamedFilter::from_i32(name).unwrap_or(NamedFilter::WholeCollection) {
NamedFilter::WholeCollection => Node::Search(SearchNode::WholeCollection),
NamedFilter::CurrentDeck => Node::Search(SearchNode::Deck("current".into())),
NamedFilter::AddedToday => Node::Search(SearchNode::AddedInDays(1)),
NamedFilter::StudiedToday => Node::Search(SearchNode::Rated {
days: 1,
ease: EaseKind::AnyAnswerButton,
}),
NamedFilter::AgainToday => Node::Search(SearchNode::Rated {
days: 1,
ease: EaseKind::AnswerButton(1),
}),
NamedFilter::New => Node::Search(SearchNode::State(StateKind::New)),
NamedFilter::Learn => Node::Search(SearchNode::State(StateKind::Learning)),
NamedFilter::Review => Node::Search(SearchNode::State(StateKind::Review)),
NamedFilter::Due => Node::Search(SearchNode::State(StateKind::Due)),
NamedFilter::Suspended => Node::Search(SearchNode::State(StateKind::Suspended)),
NamedFilter::Buried => Node::Search(SearchNode::State(StateKind::Buried)),
NamedFilter::RedFlag => Node::Search(SearchNode::Flag(1)),
NamedFilter::OrangeFlag => Node::Search(SearchNode::Flag(2)),
NamedFilter::GreenFlag => Node::Search(SearchNode::Flag(3)),
NamedFilter::BlueFlag => Node::Search(SearchNode::Flag(4)),
NamedFilter::NoFlag => Node::Search(SearchNode::Flag(0)),
NamedFilter::AnyFlag => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))),
}
}
use pb::filter_to_search_in::Flag;
match msg.filter.unwrap_or(Filter::WholeCollection(true)) {
Filter::Tag(s) => Node::Search(SearchNode::Tag(
escape_anki_wildcards(&s).into_owned().into(),
)),
@ -337,10 +307,16 @@ impl From<pb::FilterToSearchIn> for Node<'_> {
Filter::Template(u) => {
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16)))
}
Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())),
Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates {
note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(),
text: dupe.text.into(),
}),
Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
field: escape_anki_wildcards(&s).into_owned().into(),
text: "*".to_string().into(),
is_re: false,
}),
Filter::ForgotInDays(u) => Node::Search(SearchNode::Rated {
days: u,
ease: EaseKind::AnswerButton(1),
@ -350,12 +326,26 @@ impl From<pb::FilterToSearchIn> for Node<'_> {
operator: "<=".to_string(),
kind: PropertyKind::Due(i),
}),
Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())),
Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
field: escape_anki_wildcards(&s).into_owned().into(),
text: "*".to_string().into(),
is_re: false,
Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection),
Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())),
Filter::StudiedToday(_) => Node::Search(SearchNode::Rated {
days: 1,
ease: EaseKind::AnyAnswerButton,
}),
Filter::New(_) => Node::Search(SearchNode::State(StateKind::New)),
Filter::Learn(_) => Node::Search(SearchNode::State(StateKind::Learning)),
Filter::Review(_) => Node::Search(SearchNode::State(StateKind::Review)),
Filter::Due(_) => Node::Search(SearchNode::State(StateKind::Due)),
Filter::Suspended(_) => Node::Search(SearchNode::State(StateKind::Suspended)),
Filter::Buried(_) => Node::Search(SearchNode::State(StateKind::Buried)),
Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) {
Flag::Without => Node::Search(SearchNode::Flag(0)),
Flag::Any => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))),
Flag::Red => Node::Search(SearchNode::Flag(1)),
Flag::Orange => Node::Search(SearchNode::Flag(2)),
Flag::Green => Node::Search(SearchNode::Flag(3)),
Flag::Blue => Node::Search(SearchNode::Flag(4)),
},
}
}
}