From 9ef691c06f77bd78465d1ed25f00b63521134bc0 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 9 Jan 2021 10:50:08 +0100 Subject: [PATCH 01/28] Provide filter searches through backend --- rslib/backend.proto | 30 +++++++++++++++++++++++ rslib/src/backend/mod.rs | 50 +++++++++++++++++++++++++++++++++++--- rslib/src/search/mod.rs | 4 ++- rslib/src/search/parser.rs | 10 ++++---- rslib/src/search/writer.rs | 2 +- rslib/src/text.rs | 8 ++++++ 6 files changed, 94 insertions(+), 10 deletions(-) diff --git a/rslib/backend.proto b/rslib/backend.proto index a3966e47a..820b0a832 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -84,6 +84,7 @@ service BackendService { // searching + rpc FilterToSearch (FilterToSearchIn) returns (String); rpc NormalizeSearch (String) returns (String); rpc SearchCards (SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut); @@ -756,6 +757,35 @@ message BuiltinSearchOrder { bool reverse = 2; } +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; + } + oneof filter { + NamedFilter name = 1; + string tag = 2; + string deck = 3; + string note = 4; + uint32 template = 5; + } +} + message ConcatenateSearchesIn { enum Separator { AND = 0; diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 22868773f..18ee036bf 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -36,8 +36,8 @@ use crate::{ sched::new::NewCardSortOrder, sched::timespan::{answer_button_time, time_span}, search::{ - concatenate_searches, negate_search, normalize_search, replace_search_term, BoolSeparator, - SortMode, + concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes, + BoolSeparator, Node, SearchNode, SortMode, StateKind, TemplateKind, }, stats::studied_today, sync::{ @@ -45,7 +45,7 @@ use crate::{ SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage, }, template::RenderedNode, - text::{extract_av_tags, strip_av_tags, AVTag}, + text::{escape_anki_wildcards, extract_av_tags, strip_av_tags, AVTag}, timestamp::TimestampSecs, types::Usn, }; @@ -277,6 +277,46 @@ impl From for DeckConfID { } } +impl From for Node<'_> { + fn from(msg: pb::FilterToSearchIn) -> Self { + use pb::filter_to_search_in::Filter as F; + use pb::filter_to_search_in::NamedFilter as NF; + use Node as N; + use SearchNode as SN; + match msg.filter.unwrap_or(F::Name(NF::WholeCollection as i32)) { + F::Name(name) => match NF::from_i32(name).unwrap_or(NF::WholeCollection) { + NF::WholeCollection => N::Search(SN::WholeCollection), + NF::CurrentDeck => N::Search(SN::Deck("current".into())), + NF::AddedToday => N::Search(SN::AddedInDays(1)), + NF::StudiedToday => N::Search(SN::Rated { + days: 1, + ease: None, + }), + NF::AgainToday => N::Search(SN::Rated { + days: 1, + ease: Some(1), + }), + NF::New => N::Search(SN::State(StateKind::New)), + NF::Learn => N::Search(SN::State(StateKind::Learning)), + NF::Review => N::Search(SN::State(StateKind::Review)), + NF::Due => N::Search(SN::State(StateKind::Due)), + NF::Suspended => N::Search(SN::State(StateKind::Suspended)), + NF::Buried => N::Search(SN::State(StateKind::Buried)), + NF::RedFlag => N::Search(SN::Flag(1)), + NF::OrangeFlag => N::Search(SN::Flag(2)), + NF::GreenFlag => N::Search(SN::Flag(3)), + NF::BlueFlag => N::Search(SN::Flag(4)), + NF::NoFlag => N::Search(SN::Flag(0)), + NF::AnyFlag => N::Not(Box::new(N::Search(SN::Flag(0)))), + }, + F::Tag(s) => N::Search(SN::Tag(escape_anki_wildcards(&s).into_owned().into())), + F::Deck(s) => N::Search(SN::Deck(escape_anki_wildcards(&s).into_owned().into())), + F::Note(s) => N::Search(SN::NoteType(escape_anki_wildcards(&s).into_owned().into())), + F::Template(u) => N::Search(SN::CardTemplate(TemplateKind::Ordinal(u as u16))), + } + } +} + impl From for BoolSeparator { fn from(sep: BoolSeparatorProto) -> Self { match sep { @@ -408,6 +448,10 @@ impl BackendService for Backend { // searching //----------------------------------------------- + fn filter_to_search(&self, input: pb::FilterToSearchIn) -> Result { + Ok(write_nodes(&[input.into()]).into()) + } + fn normalize_search(&self, input: pb::String) -> Result { Ok(normalize_search(&input.val)?.into()) } diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index 607b9d767..5eef7e86e 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -5,6 +5,8 @@ mod sqlwriter; mod writer; pub use cards::SortMode; +pub use parser::{Node, PropertyKind, SearchNode, StateKind, TemplateKind}; pub use writer::{ - concatenate_searches, negate_search, normalize_search, replace_search_term, BoolSeparator, + concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes, + BoolSeparator, }; diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 9e03abcc5..7763c965f 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -42,7 +42,7 @@ impl From> for ParseError { type ParseResult = std::result::Result; #[derive(Debug, PartialEq)] -pub(super) enum Node<'a> { +pub enum Node<'a> { And, Or, Not(Box>), @@ -51,7 +51,7 @@ pub(super) enum Node<'a> { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum SearchNode<'a> { +pub enum SearchNode<'a> { // text without a colon UnqualifiedText(Cow<'a, str>), // foo:bar, where foo doesn't match a term below @@ -91,7 +91,7 @@ pub(super) enum SearchNode<'a> { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum PropertyKind { +pub enum PropertyKind { Due(i32), Interval(u32), Reps(u32), @@ -101,7 +101,7 @@ pub(super) enum PropertyKind { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum StateKind { +pub enum StateKind { New, Review, Learning, @@ -113,7 +113,7 @@ pub(super) enum StateKind { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum TemplateKind<'a> { +pub enum TemplateKind<'a> { Ordinal(u16), Name(Cow<'a, str>), } diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index ec7051893..e112ebcf5 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -87,7 +87,7 @@ pub fn replace_search_term(search: &str, replacement: &str) -> Result { Ok(write_nodes(&nodes)) } -fn write_nodes<'a, I>(nodes: I) -> String +pub fn write_nodes<'a, I>(nodes: I) -> String where I: IntoIterator>, { diff --git a/rslib/src/text.rs b/rslib/src/text.rs index 0f892ecd0..10391334e 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -331,6 +331,14 @@ pub(crate) fn escape_sql(txt: &str) -> Cow { RE.replace_all(&txt, r"\$0") } +/// Escape Anki wildcards and the backslash for escaping them: \*_ +pub(crate) fn escape_anki_wildcards(txt: &str) -> Cow { + lazy_static! { + static ref RE: Regex = Regex::new(r"[\\*_]").unwrap(); + } + RE.replace_all(&txt, r"\$0") +} + /// Compare text with a possible glob, folding case. pub(crate) fn matches_glob(text: &str, search: &str) -> bool { if is_glob(search) { From fda2bfdb4e77721bde478dda21812067da024cb8 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 9 Jan 2021 10:51:15 +0100 Subject: [PATCH 02/28] Use backend filters instead of literal searches --- pylib/anki/rsbackend.py | 7 +++ qt/aqt/browser.py | 113 ++++++++++++++++++---------------------- 2 files changed, 59 insertions(+), 61 deletions(-) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 1653bfe88..26bbc87ec 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -46,6 +46,7 @@ TagUsnTuple = pb.TagUsnTuple NoteType = pb.NoteType DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType +NamedFilter = pb.FilterToSearchIn.NamedFilter ConcatSeparator = pb.ConcatenateSearchesIn.Separator SyncAuth = pb.SyncAuth SyncOutput = pb.SyncCollectionOut @@ -261,6 +262,12 @@ class RustBackend(RustBackendGenerated): err.ParseFromString(err_bytes) raise proto_exception_to_native(err) + def filters_to_searches(self, **kwargs) -> List[str]: + return [ + self.filter_to_search(pb.FilterToSearchIn(**dict([f]))) + for f in kwargs.items() + ] + def translate_string_in( key: TRValue, **kwargs: Union[str, int, float] diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 3c448f89b..bb5a5be55 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -4,7 +4,6 @@ from __future__ import annotations import html -import re import time from concurrent.futures import Future from dataclasses import dataclass @@ -21,7 +20,7 @@ from anki.consts import * from anki.lang import without_unicode_isolation from anki.models import NoteType from anki.notes import Note -from anki.rsbackend import ConcatSeparator, DeckTreeNode, InvalidInput +from anki.rsbackend import ConcatSeparator, DeckTreeNode, InvalidInput, NamedFilter from anki.stats import CardStats from anki.utils import htmlToTextLine, ids2str, isMac, isWin from aqt import AnkiQt, gui_hooks @@ -1110,14 +1109,14 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( tr(TR.BROWSING_WHOLE_COLLECTION), ":/icons/collection.svg", - self._filterFunc(""), + self._filterFunc(name=NamedFilter.WHOLE_COLLECTION), item_type=SidebarItemType.COLLECTION, ) root.addChild(item) item = SidebarItem( tr(TR.BROWSING_CURRENT_DECK), ":/icons/deck.svg", - self._filterFunc("deck:current"), + self._filterFunc(name=NamedFilter.CURRENT_DECK), item_type=SidebarItemType.CURRENT_DECK, ) root.addChild(item) @@ -1140,7 +1139,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( t, ":/icons/tag.svg", - lambda t=t: self.setFilter("tag", t), # type: ignore + lambda t=t: self.setFilter(tag=t), # type: ignore item_type=SidebarItemType.TAG, ) root.addChild(item) @@ -1153,7 +1152,7 @@ QTableView {{ gridline-color: {grid} }} def set_filter(): full_name = head + node.name # pylint: disable=cell-var-from-loop - return lambda: self.setFilter("deck", full_name) + return lambda: self.setFilter(deck=full_name) def toggle_expand(): did = node.deck_id # pylint: disable=cell-var-from-loop @@ -1180,7 +1179,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( m.name, ":/icons/notetype.svg", - lambda m=m: self.setFilter("note", m.name), # type: ignore + lambda m=m: self.setFilter(note=m.name), # type: ignore item_type=SidebarItemType.NOTETYPE, ) root.addChild(item) @@ -1208,47 +1207,38 @@ QTableView {{ gridline-color: {grid} }} ml.popupOver(self.form.filter) - def setFilter(self, *args): - if len(args) == 1: - txt = args[0] - else: - txt = "" - items = [] - for i, a in enumerate(args): - if i % 2 == 0: - txt += a + ":" - else: - txt += re.sub(r'["*_\\]', r"\\\g<0>", a) - txt = '"{}"'.format(txt.replace('"', '\\"')) - items.append(txt) - txt = "" - txt = " AND ".join(items) + def setFilter(self, *args, **kwargs): + # args are literal searches, kwargs are searches provided by the backend try: - if self.mw.app.keyboardModifiers() & Qt.AltModifier: - txt = self.col.backend.negate_search(txt) + filters = self.col.backend.filters_to_searches(**kwargs) + search = self.col.backend.concatenate_searches( + sep=ConcatSeparator.AND, searches=list(args) + filters + ) + mods = self.mw.app.keyboardModifiers() + if mods & Qt.AltModifier: + search = self.col.backend.negate_search(search) cur = str(self.form.searchEdit.lineEdit().text()) if cur != self._searchPrompt: - mods = self.mw.app.keyboardModifiers() if mods & Qt.ControlModifier and mods & Qt.ShiftModifier: - txt = self.col.backend.replace_search_term( - search=cur, replacement=txt + search = self.col.backend.replace_search_term( + search=cur, replacement=search ) elif mods & Qt.ControlModifier: - txt = self.col.backend.concatenate_searches( + search = self.col.backend.concatenate_searches( # pylint: disable=no-member sep=ConcatSeparator.AND, - searches=[cur, txt], + searches=[cur, search], ) elif mods & Qt.ShiftModifier: - txt = self.col.backend.concatenate_searches( + search = self.col.backend.concatenate_searches( # pylint: disable=no-member sep=ConcatSeparator.OR, - searches=[cur, txt], + searches=[cur, search], ) except InvalidInput as e: showWarning(str(e)) else: - self.form.searchEdit.lineEdit().setText(txt) + self.form.searchEdit.lineEdit().setText(search) self.onSearchActivated() def _simpleFilters(self, items): @@ -1257,18 +1247,21 @@ QTableView {{ gridline-color: {grid} }} if row is None: ml.addSeparator() else: - label, filter = row - ml.addItem(label, self._filterFunc(filter)) + label, filter_name = row + ml.addItem(label, self._filterFunc(name=filter_name)) return ml - def _filterFunc(self, *args): - return lambda *, f=args: self.setFilter(*f) + def _filterFunc(self, *args, **kwargs): + return lambda: self.setFilter(*args, **kwargs) def _commonFilters(self): return self._simpleFilters( ( - (tr(TR.BROWSING_WHOLE_COLLECTION), ""), - (tr(TR.BROWSING_CURRENT_DECK), '"deck:current"'), + (tr(TR.BROWSING_WHOLE_COLLECTION), NamedFilter.WHOLE_COLLECTION), + ( + tr(TR.BROWSING_CURRENT_DECK), + NamedFilter.CURRENT_DECK, + ), ) ) @@ -1277,9 +1270,9 @@ QTableView {{ gridline-color: {grid} }} subm.addChild( self._simpleFilters( ( - (tr(TR.BROWSING_ADDED_TODAY), '"added:1"'), - (tr(TR.BROWSING_STUDIED_TODAY), '"rated:1"'), - (tr(TR.BROWSING_AGAIN_TODAY), '"rated:1:1"'), + (tr(TR.BROWSING_ADDED_TODAY), NamedFilter.ADDED_TODAY), + (tr(TR.BROWSING_STUDIED_TODAY), NamedFilter.STUDIED_TODAY), + (tr(TR.BROWSING_AGAIN_TODAY), NamedFilter.AGAIN_TODAY), ) ) ) @@ -1290,20 +1283,20 @@ QTableView {{ gridline-color: {grid} }} subm.addChild( self._simpleFilters( ( - (tr(TR.ACTIONS_NEW), '"is:new"'), - (tr(TR.SCHEDULING_LEARNING), '"is:learn"'), - (tr(TR.SCHEDULING_REVIEW), '"is:review"'), - (tr(TR.FILTERING_IS_DUE), '"is:due"'), + (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), None, - (tr(TR.BROWSING_SUSPENDED), '"is:suspended"'), - (tr(TR.BROWSING_BURIED), '"is:buried"'), + (tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED), + (tr(TR.BROWSING_BURIED), NamedFilter.BURIED), None, - (tr(TR.ACTIONS_RED_FLAG), '"flag:1"'), - (tr(TR.ACTIONS_ORANGE_FLAG), '"flag:2"'), - (tr(TR.ACTIONS_GREEN_FLAG), '"flag:3"'), - (tr(TR.ACTIONS_BLUE_FLAG), '"flag:4"'), - (tr(TR.BROWSING_NO_FLAG), '"flag:0"'), - (tr(TR.BROWSING_ANY_FLAG), '"-flag:0"'), + (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), ) ) ) @@ -1320,7 +1313,7 @@ QTableView {{ gridline-color: {grid} }} tagList = MenuList() for t in sorted(self.col.tags.all(), key=lambda s: s.lower()): - tagList.addItem(self._escapeMenuItem(t), self._filterFunc("tag", t)) + tagList.addItem(self._escapeMenuItem(t), self._filterFunc(tag=t)) m.addChild(tagList.chunked()) return m @@ -1333,13 +1326,11 @@ QTableView {{ gridline-color: {grid} }} fullname = parent_prefix + node.name if node.children: subm = parent.addMenu(escaped_name) - subm.addItem( - tr(TR.ACTIONS_FILTER), self._filterFunc("deck", fullname) - ) + subm.addItem(tr(TR.ACTIONS_FILTER), self._filterFunc(deck=fullname)) subm.addSeparator() addDecks(subm, node.children, fullname + "::") else: - parent.addItem(escaped_name, self._filterFunc("deck", fullname)) + parent.addItem(escaped_name, self._filterFunc(deck=fullname)) alldecks = self.col.decks.deck_tree() ml = MenuList() @@ -1361,12 +1352,12 @@ QTableView {{ gridline-color: {grid} }} escaped_nt_name = self._escapeMenuItem(nt["name"]) # no sub menu if it's a single template if len(nt["tmpls"]) == 1: - noteTypes.addItem(escaped_nt_name, self._filterFunc("note", nt["name"])) + noteTypes.addItem(escaped_nt_name, self._filterFunc(note=nt["name"])) else: subm = noteTypes.addMenu(escaped_nt_name) subm.addItem( - tr(TR.BROWSING_ALL_CARD_TYPES), self._filterFunc("note", nt["name"]) + tr(TR.BROWSING_ALL_CARD_TYPES), self._filterFunc(note=nt["name"]) ) subm.addSeparator() @@ -1380,7 +1371,7 @@ QTableView {{ gridline-color: {grid} }} name=self._escapeMenuItem(tmpl["name"]), ) subm.addItem( - name, self._filterFunc("note", nt["name"], "card", str(c + 1)) + name, self._filterFunc(note=nt["name"], template=c + 1) ) m.addChild(noteTypes.chunked()) From b99d9cda7421a0868a9962d1816f3a4533078607 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 9 Jan 2021 12:34:46 +0100 Subject: [PATCH 03/28] Prettify frontend filter code --- pylib/anki/rsbackend.py | 6 +++--- qt/aqt/browser.py | 18 +++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 26bbc87ec..935381739 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -262,10 +262,10 @@ class RustBackend(RustBackendGenerated): err.ParseFromString(err_bytes) raise proto_exception_to_native(err) - def filters_to_searches(self, **kwargs) -> List[str]: + def filters_to_searches(self, filters: dict) -> List[str]: return [ - self.filter_to_search(pb.FilterToSearchIn(**dict([f]))) - for f in kwargs.items() + self.filter_to_search(pb.FilterToSearchIn(**{key: val})) + for key, val in filters.items() ] diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index bb5a5be55..b4a37422b 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1128,7 +1128,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( name, ":/icons/heart.svg", - lambda s=filt: self.setFilter(s), # type: ignore + self._filterFunc(filt), item_type=SidebarItemType.FILTER, ) root.addChild(item) @@ -1139,7 +1139,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( t, ":/icons/tag.svg", - lambda t=t: self.setFilter(tag=t), # type: ignore + self._filterFunc(tag=t), item_type=SidebarItemType.TAG, ) root.addChild(item) @@ -1179,7 +1179,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( m.name, ":/icons/notetype.svg", - lambda m=m: self.setFilter(note=m.name), # type: ignore + self._filterFunc(note=m.name), item_type=SidebarItemType.NOTETYPE, ) root.addChild(item) @@ -1207,12 +1207,11 @@ QTableView {{ gridline-color: {grid} }} ml.popupOver(self.form.filter) - def setFilter(self, *args, **kwargs): - # args are literal searches, kwargs are searches provided by the backend + def setFilter(self, *search_strings, **filters): try: - filters = self.col.backend.filters_to_searches(**kwargs) + filter_searches = self.col.backend.filters_to_searches(filters) search = self.col.backend.concatenate_searches( - sep=ConcatSeparator.AND, searches=list(args) + filters + sep=ConcatSeparator.AND, searches=list(search_strings) + filter_searches ) mods = self.mw.app.keyboardModifiers() if mods & Qt.AltModifier: @@ -1258,10 +1257,7 @@ QTableView {{ gridline-color: {grid} }} return self._simpleFilters( ( (tr(TR.BROWSING_WHOLE_COLLECTION), NamedFilter.WHOLE_COLLECTION), - ( - tr(TR.BROWSING_CURRENT_DECK), - NamedFilter.CURRENT_DECK, - ), + (tr(TR.BROWSING_CURRENT_DECK), NamedFilter.CURRENT_DECK), ) ) From b763fc5b2ab2524e9c70252ed8f67629c761e59e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 9 Jan 2021 16:48:47 +0100 Subject: [PATCH 04/28] Use explicit wrapper functions to get filters --- pylib/anki/rsbackend.py | 7 +--- qt/aqt/browser.py | 77 +++++++++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 935381739..7b4454ca0 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -46,6 +46,7 @@ TagUsnTuple = pb.TagUsnTuple NoteType = pb.NoteType DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType +FilterToSearchIn = pb.FilterToSearchIn NamedFilter = pb.FilterToSearchIn.NamedFilter ConcatSeparator = pb.ConcatenateSearchesIn.Separator SyncAuth = pb.SyncAuth @@ -262,12 +263,6 @@ class RustBackend(RustBackendGenerated): err.ParseFromString(err_bytes) raise proto_exception_to_native(err) - def filters_to_searches(self, filters: dict) -> List[str]: - return [ - self.filter_to_search(pb.FilterToSearchIn(**{key: val})) - for key, val in filters.items() - ] - def translate_string_in( key: TRValue, **kwargs: Union[str, int, float] diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index b4a37422b..be56be53f 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -20,7 +20,13 @@ from anki.consts import * from anki.lang import without_unicode_isolation from anki.models import NoteType from anki.notes import Note -from anki.rsbackend import ConcatSeparator, DeckTreeNode, InvalidInput, NamedFilter +from anki.rsbackend import ( + ConcatSeparator, + DeckTreeNode, + FilterToSearchIn, + InvalidInput, + NamedFilter, +) from anki.stats import CardStats from anki.utils import htmlToTextLine, ids2str, isMac, isWin from aqt import AnkiQt, gui_hooks @@ -1109,14 +1115,14 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( tr(TR.BROWSING_WHOLE_COLLECTION), ":/icons/collection.svg", - self._filterFunc(name=NamedFilter.WHOLE_COLLECTION), + self._named_filter(NamedFilter.WHOLE_COLLECTION), item_type=SidebarItemType.COLLECTION, ) root.addChild(item) item = SidebarItem( tr(TR.BROWSING_CURRENT_DECK), ":/icons/deck.svg", - self._filterFunc(name=NamedFilter.CURRENT_DECK), + self._named_filter(NamedFilter.CURRENT_DECK), item_type=SidebarItemType.CURRENT_DECK, ) root.addChild(item) @@ -1128,7 +1134,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( name, ":/icons/heart.svg", - self._filterFunc(filt), + self._saved_filter(filt), item_type=SidebarItemType.FILTER, ) root.addChild(item) @@ -1139,7 +1145,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( t, ":/icons/tag.svg", - self._filterFunc(tag=t), + self._tag_filter(t), item_type=SidebarItemType.TAG, ) root.addChild(item) @@ -1150,10 +1156,6 @@ QTableView {{ gridline-color: {grid} }} def fillGroups(root, nodes: Sequence[DeckTreeNode], head=""): for node in nodes: - def set_filter(): - full_name = head + node.name # pylint: disable=cell-var-from-loop - return lambda: self.setFilter(deck=full_name) - def toggle_expand(): did = node.deck_id # pylint: disable=cell-var-from-loop return lambda _: self.mw.col.decks.collapseBrowser(did) @@ -1161,7 +1163,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( node.name, ":/icons/deck.svg", - set_filter(), + self._deck_filter(head + node.name), toggle_expand(), not node.collapsed, item_type=SidebarItemType.DECK, @@ -1179,7 +1181,7 @@ QTableView {{ gridline-color: {grid} }} item = SidebarItem( m.name, ":/icons/notetype.svg", - self._filterFunc(note=m.name), + self._note_filter(m.name), item_type=SidebarItemType.NOTETYPE, ) root.addChild(item) @@ -1207,11 +1209,10 @@ QTableView {{ gridline-color: {grid} }} ml.popupOver(self.form.filter) - def setFilter(self, *search_strings, **filters): + def setFilter(self, *searches): try: - filter_searches = self.col.backend.filters_to_searches(filters) search = self.col.backend.concatenate_searches( - sep=ConcatSeparator.AND, searches=list(search_strings) + filter_searches + sep=ConcatSeparator.AND, searches=searches ) mods = self.mw.app.keyboardModifiers() if mods & Qt.AltModifier: @@ -1247,11 +1248,37 @@ QTableView {{ gridline-color: {grid} }} ml.addSeparator() else: label, filter_name = row - ml.addItem(label, self._filterFunc(name=filter_name)) + ml.addItem(label, self._named_filter(filter_name)) return ml - def _filterFunc(self, *args, **kwargs): - return lambda: self.setFilter(*args, **kwargs) + def _named_filter(self, name: Any) -> Callable: + return lambda: self.setFilter( + self.col.backend.filter_to_search(FilterToSearchIn(name=name)) + ) + + def _tag_filter(self, tag: str) -> Callable: + return lambda: self.setFilter( + self.col.backend.filter_to_search(FilterToSearchIn(tag=tag)) + ) + + def _deck_filter(self, deck: str) -> Callable: + return lambda: self.setFilter( + self.col.backend.filter_to_search(FilterToSearchIn(deck=deck)) + ) + + def _note_filter(self, note: str) -> Callable: + return lambda: self.setFilter( + self.col.backend.filter_to_search(FilterToSearchIn(note=note)) + ) + + def _template_filter(self, note: str, template: int) -> Callable: + return lambda: self.setFilter( + self.col.backend.filter_to_search(FilterToSearchIn(note=note)), + self.col.backend.filter_to_search(FilterToSearchIn(template=template)), + ) + + def _saved_filter(self, saved: str) -> Callable: + return lambda: self.setFilter(saved) def _commonFilters(self): return self._simpleFilters( @@ -1309,7 +1336,7 @@ QTableView {{ gridline-color: {grid} }} tagList = MenuList() for t in sorted(self.col.tags.all(), key=lambda s: s.lower()): - tagList.addItem(self._escapeMenuItem(t), self._filterFunc(tag=t)) + tagList.addItem(self._escapeMenuItem(t), self._tag_filter(t)) m.addChild(tagList.chunked()) return m @@ -1322,11 +1349,11 @@ QTableView {{ gridline-color: {grid} }} fullname = parent_prefix + node.name if node.children: subm = parent.addMenu(escaped_name) - subm.addItem(tr(TR.ACTIONS_FILTER), self._filterFunc(deck=fullname)) + subm.addItem(tr(TR.ACTIONS_FILTER), self._deck_filter(fullname)) subm.addSeparator() addDecks(subm, node.children, fullname + "::") else: - parent.addItem(escaped_name, self._filterFunc(deck=fullname)) + parent.addItem(escaped_name, self._deck_filter(fullname)) alldecks = self.col.decks.deck_tree() ml = MenuList() @@ -1348,12 +1375,12 @@ QTableView {{ gridline-color: {grid} }} escaped_nt_name = self._escapeMenuItem(nt["name"]) # no sub menu if it's a single template if len(nt["tmpls"]) == 1: - noteTypes.addItem(escaped_nt_name, self._filterFunc(note=nt["name"])) + noteTypes.addItem(escaped_nt_name, self._note_filter(nt["name"])) else: subm = noteTypes.addMenu(escaped_nt_name) subm.addItem( - tr(TR.BROWSING_ALL_CARD_TYPES), self._filterFunc(note=nt["name"]) + tr(TR.BROWSING_ALL_CARD_TYPES), self._note_filter(nt["name"]) ) subm.addSeparator() @@ -1366,9 +1393,7 @@ QTableView {{ gridline-color: {grid} }} num=c + 1, name=self._escapeMenuItem(tmpl["name"]), ) - subm.addItem( - name, self._filterFunc(note=nt["name"], template=c + 1) - ) + subm.addItem(name, self._template_filter(nt["name"], c + 1)) m.addChild(noteTypes.chunked()) return m @@ -1395,7 +1420,7 @@ QTableView {{ gridline-color: {grid} }} ml.addSeparator() for name, filt in sorted(saved.items()): - ml.addItem(self._escapeMenuItem(name), self._filterFunc(filt)) + ml.addItem(self._escapeMenuItem(name), self._saved_filter(filt)) return ml From ca62f3ef8098b2f86ce6c9241ba9d537a451f474 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 9 Jan 2021 17:30:12 +0100 Subject: [PATCH 05/28] Fix ordinal case in write_template Internal card ordinals start at 0, so add 1 again when writing a template search string from a parsed ordinal. --- rslib/src/search/writer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index ec7051893..a591f2881 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -149,7 +149,7 @@ fn write_single_field(field: &str, text: &str, is_re: bool) -> String { fn write_template(template: &TemplateKind) -> String { match template { - TemplateKind::Ordinal(u) => format!("\"card:{}\"", u), + TemplateKind::Ordinal(u) => format!("\"card:{}\"", u + 1), TemplateKind::Name(s) => format!("\"card:{}\"", s), } } From 0629f80aeb1845cb84faa7a07a658770f9e6bbed Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 9 Jan 2021 20:09:47 +0100 Subject: [PATCH 06/28] Format backend.proto --- rslib/backend.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/backend.proto b/rslib/backend.proto index 10597806f..d4e3df1e1 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -84,7 +84,7 @@ service BackendService { // searching - rpc FilterToSearch (FilterToSearchIn) returns (String); + rpc FilterToSearch(FilterToSearchIn) returns (String); rpc NormalizeSearch(String) returns (String); rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); From f5d429a5ca0a0c7f4229ef1cad8ee479c7f70295 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 9 Jan 2021 22:34:53 +0100 Subject: [PATCH 07/28] Put Preview button into editor inside browser --- qt/aqt/browser.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 3c448f89b..24b8bd925 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -872,7 +872,20 @@ QTableView {{ gridline-color: {grid} }} self.singleCard = False def setupEditor(self): + def add_preview_button(leftbuttons, editor): + leftbuttons.insert(0, editor.addButton( + None, + "preview", + lambda _editor: self.onTogglePreview(), + "Preview Selected Card", + "Preview", + disables=False, + rightside=False, + )) + + gui_hooks.editor_did_init_left_buttons.append(add_preview_button) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) + gui_hooks.editor_did_init_left_buttons.remove(add_preview_button) def onRowChanged(self, current, previous): "Update current note and hide/show editor." From ece753991e88bf5aa5424a649a6bea52d7def899 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 9 Jan 2021 23:16:01 +0100 Subject: [PATCH 08/28] Make button show its toggle state --- qt/aqt/browser.py | 3 +++ qt/aqt/data/web/css/editor.scss | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 24b8bd925..b1f712a40 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -879,8 +879,10 @@ QTableView {{ gridline-color: {grid} }} lambda _editor: self.onTogglePreview(), "Preview Selected Card", "Preview", + id="previewButton", disables=False, rightside=False, + toggleable=True, )) gui_hooks.editor_did_init_left_buttons.append(add_preview_button) @@ -1579,6 +1581,7 @@ where id in %s""" self._previewer.close() def _on_preview_closed(self): + self.editor.web.eval("$('#previewButton').removeClass('highlighted')") self._previewer = None # Card deletion diff --git a/qt/aqt/data/web/css/editor.scss b/qt/aqt/data/web/css/editor.scss index 8f768e96c..8957a8101 100644 --- a/qt/aqt/data/web/css/editor.scss +++ b/qt/aqt/data/web/css/editor.scss @@ -73,7 +73,13 @@ button.linkb:disabled { } button.highlighted { - border-bottom: 3px solid #000; + #topbutsleft & { + box-shadow: inset 0px 0px 5px #222; + } + + #topbutsright & { + border-bottom: 3px solid #000; + } } #fields { From 28278fcc40936f8254d9892914b210500187c490 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 9 Jan 2021 23:25:56 +0100 Subject: [PATCH 09/28] Remove preview button from browser.py top bar --- qt/aqt/browser.py | 10 ++-------- qt/aqt/forms/browser.ui | 13 ------------- qt/aqt/previewer.py | 1 - 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index b1f712a40..8c8e28c66 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -622,12 +622,6 @@ class Browser(QMainWindow): # pylint: disable=unnecessary-lambda # actions f = self.form - qconnect(f.previewButton.clicked, self.onTogglePreview) - f.previewButton.setToolTip( - tr(TR.BROWSING_PREVIEW_SELECTED_CARD, val=shortcut("Ctrl+Shift+P")) - ) - f.previewButton.setShortcut("Ctrl+Shift+P") - qconnect(f.filter.clicked, self.onFilterButton) # edit qconnect(f.actionUndo.triggered, self.mw.onUndo) @@ -877,8 +871,8 @@ QTableView {{ gridline-color: {grid} }} None, "preview", lambda _editor: self.onTogglePreview(), - "Preview Selected Card", - "Preview", + tr(TR.BROWSING_PREVIEW_SELECTED_CARD, val=shortcut("Ctrl+Shift+P")), + tr(TR.ACTIONS_PREVIEW), id="previewButton", disables=False, rightside=False, diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui index 575371cb1..8d8b005a8 100644 --- a/qt/aqt/forms/browser.ui +++ b/qt/aqt/forms/browser.ui @@ -114,19 +114,6 @@ - - - - ACTIONS_PREVIEW - - - Ctrl+Shift+P - - - true - - - diff --git a/qt/aqt/previewer.py b/qt/aqt/previewer.py index 29e63251d..71fdbe2c8 100644 --- a/qt/aqt/previewer.py +++ b/qt/aqt/previewer.py @@ -307,7 +307,6 @@ class BrowserPreviewer(MultiCardPreviewer): def _on_finished(self, ok): super()._on_finished(ok) - self._parent.form.previewButton.setChecked(False) def _on_prev_card(self): self._parent.editor.saveNow( From 4d471612ec573e6efba1fabfbb3beb19ce0b8688 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 10 Jan 2021 08:48:20 +1000 Subject: [PATCH 10/28] fix protobuf formatting adding carriage returns on Windows --- rslib/proto_format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/proto_format.py b/rslib/proto_format.py index 306146a5d..d170d9ee1 100755 --- a/rslib/proto_format.py +++ b/rslib/proto_format.py @@ -14,7 +14,7 @@ for path in sys.argv[2:]: ).decode("utf-8") if orig != new: if want_fix: - with open(os.path.join(workspace, path), "w") as file: + with open(os.path.join(workspace, path), "w", newline="\n") as file: file.write(new) print("fixed", path) else: From fe118197a33f829490c4f41b384e113c72c3bf93 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 9 Jan 2021 23:55:39 +0100 Subject: [PATCH 11/28] Improve styling of new preview button on light and nightMode --- qt/aqt/data/web/css/editor.scss | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/qt/aqt/data/web/css/editor.scss b/qt/aqt/data/web/css/editor.scss index 8957a8101..c1bff5cf0 100644 --- a/qt/aqt/data/web/css/editor.scss +++ b/qt/aqt/data/web/css/editor.scss @@ -65,16 +65,28 @@ button.linkb { box-shadow: none; padding: 0px 2px; background: transparent; + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + .nightMode & > img { + filter: invert(180); + } } -button.linkb:disabled { - opacity: 0.3; - cursor: not-allowed; +button:focus { + outline: none; } button.highlighted { + .nightMode #topbutsleft & { + background: linear-gradient(0deg, #333333 0%, #434343 100%); + } + #topbutsleft & { - box-shadow: inset 0px 0px 5px #222; + background-color: lightgrey; } #topbutsright & { @@ -94,12 +106,6 @@ button.highlighted { color: var(--link); } -.nightMode { - button.linkb > img { - filter: invert(180); - } -} - .drawing { zoom: 50%; } From 59d0e8f03695a90a482c286f47ee4f47ad64c47b Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 00:00:19 +0100 Subject: [PATCH 12/28] Add shortcut to new preview button --- qt/aqt/browser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 8c8e28c66..7ea3393be 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -867,13 +867,15 @@ QTableView {{ gridline-color: {grid} }} def setupEditor(self): def add_preview_button(leftbuttons, editor): + preview_shortcut = "Ctrl+Shift+P" leftbuttons.insert(0, editor.addButton( None, "preview", lambda _editor: self.onTogglePreview(), - tr(TR.BROWSING_PREVIEW_SELECTED_CARD, val=shortcut("Ctrl+Shift+P")), + tr(TR.BROWSING_PREVIEW_SELECTED_CARD, val=preview_shortcut), tr(TR.ACTIONS_PREVIEW), id="previewButton", + keys=preview_shortcut, disables=False, rightside=False, toggleable=True, From 8b877f0a08250ec1b25fdc4e76ce276f54a3f16b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 10 Jan 2021 09:02:01 +1000 Subject: [PATCH 13/28] add type to _named_filter() The ...Value types generated by mypy-protobuf are only available at typechecking time, and pylint chokes on them despite the use of annotations from __future__ - so we either need to quote them, or use # pylint: disable=no-member --- qt/aqt/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 10761a36f..10b684524 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1248,7 +1248,7 @@ QTableView {{ gridline-color: {grid} }} ml.addItem(label, self._named_filter(filter_name)) return ml - def _named_filter(self, name: Any) -> Callable: + def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable: return lambda: self.setFilter( self.col.backend.filter_to_search(FilterToSearchIn(name=name)) ) From 5f70d718b85ea63cb0af2b84e72b84ce02104d4e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 10 Jan 2021 09:19:33 +1000 Subject: [PATCH 14/28] favour readability over brevity in filter conversion --- rslib/src/backend/mod.rs | 79 +++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 18ee036bf..f67bb77b8 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -279,40 +279,51 @@ impl From for DeckConfID { impl From for Node<'_> { fn from(msg: pb::FilterToSearchIn) -> Self { - use pb::filter_to_search_in::Filter as F; - use pb::filter_to_search_in::NamedFilter as NF; - use Node as N; - use SearchNode as SN; - match msg.filter.unwrap_or(F::Name(NF::WholeCollection as i32)) { - F::Name(name) => match NF::from_i32(name).unwrap_or(NF::WholeCollection) { - NF::WholeCollection => N::Search(SN::WholeCollection), - NF::CurrentDeck => N::Search(SN::Deck("current".into())), - NF::AddedToday => N::Search(SN::AddedInDays(1)), - NF::StudiedToday => N::Search(SN::Rated { - days: 1, - ease: None, - }), - NF::AgainToday => N::Search(SN::Rated { - days: 1, - ease: Some(1), - }), - NF::New => N::Search(SN::State(StateKind::New)), - NF::Learn => N::Search(SN::State(StateKind::Learning)), - NF::Review => N::Search(SN::State(StateKind::Review)), - NF::Due => N::Search(SN::State(StateKind::Due)), - NF::Suspended => N::Search(SN::State(StateKind::Suspended)), - NF::Buried => N::Search(SN::State(StateKind::Buried)), - NF::RedFlag => N::Search(SN::Flag(1)), - NF::OrangeFlag => N::Search(SN::Flag(2)), - NF::GreenFlag => N::Search(SN::Flag(3)), - NF::BlueFlag => N::Search(SN::Flag(4)), - NF::NoFlag => N::Search(SN::Flag(0)), - NF::AnyFlag => N::Not(Box::new(N::Search(SN::Flag(0)))), - }, - F::Tag(s) => N::Search(SN::Tag(escape_anki_wildcards(&s).into_owned().into())), - F::Deck(s) => N::Search(SN::Deck(escape_anki_wildcards(&s).into_owned().into())), - F::Note(s) => N::Search(SN::NoteType(escape_anki_wildcards(&s).into_owned().into())), - F::Template(u) => N::Search(SN::CardTemplate(TemplateKind::Ordinal(u as u16))), + 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: None, + }), + NamedFilter::AgainToday => Node::Search(SearchNode::Rated { + days: 1, + ease: Some(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)))), + } + } + Filter::Tag(s) => Node::Search(SearchNode::Tag( + escape_anki_wildcards(&s).into_owned().into(), + )), + Filter::Deck(s) => Node::Search(SearchNode::Deck( + escape_anki_wildcards(&s).into_owned().into(), + )), + Filter::Note(s) => Node::Search(SearchNode::NoteType( + escape_anki_wildcards(&s).into_owned().into(), + )), + Filter::Template(u) => { + Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16))) + } } } } From 6afc49503557d98b043602e3b973bf446de916b8 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 01:10:23 +0100 Subject: [PATCH 15/28] Activate toggle on hotkey invocation --- qt/aqt/editor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 64c3ebbde..03626ab6e 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -11,6 +11,7 @@ import urllib.error import urllib.parse import urllib.request import warnings +from random import randrange from typing import Callable, List, Optional, Tuple import bs4 @@ -252,10 +253,23 @@ class Editor: if func: self._links[cmd] = func if keys: + def on_activated(): + func(self) + + if toggleable: + # generate a random id for triggering toggle + id = id or str(randrange(1_000_000)) + + def on_hotkey(): + on_activated() + self.web.eval(f'toggleEditorButton("#{id}");') + else: + on_hotkey = on_activated + QShortcut( # type: ignore QKeySequence(keys), self.widget, - activated=lambda s=self: func(s), + activated=on_hotkey, ) btn = self._addButton( icon, From 6431f0b6b1bd483d4cc1cca2e06dd46c622dccbc Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 01:21:52 +0100 Subject: [PATCH 16/28] Prevent error when browser is closed with previewer open --- qt/aqt/browser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 7ea3393be..0fc9530fc 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1577,7 +1577,8 @@ where id in %s""" self._previewer.close() def _on_preview_closed(self): - self.editor.web.eval("$('#previewButton').removeClass('highlighted')") + if self.editor.web: + self.editor.web.eval("$('#previewButton').removeClass('highlighted')") self._previewer = None # Card deletion From 097fa16783a331a9b381a2c9dd986c87b91faa7b Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 01:34:59 +0100 Subject: [PATCH 17/28] Allow closing the Preview Dialog with Ctrl+Shift+P --- qt/aqt/previewer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qt/aqt/previewer.py b/qt/aqt/previewer.py index 71fdbe2c8..8d8f01bd3 100644 --- a/qt/aqt/previewer.py +++ b/qt/aqt/previewer.py @@ -16,6 +16,7 @@ from aqt.qt import ( QIcon, QKeySequence, QPixmap, + QShortcut, Qt, QVBoxLayout, QWidget, @@ -63,6 +64,9 @@ class Previewer(QDialog): def _create_gui(self): self.setWindowTitle(tr(TR.ACTIONS_PREVIEW)) + self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self) + qconnect(self.close_shortcut.activated, self.close) + qconnect(self.finished, self._on_finished) self.silentlyClose = True self.vbox = QVBoxLayout() From ace61835de704ad68d9c651c1a732665c13f67d2 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 01:44:08 +0100 Subject: [PATCH 18/28] Fix formatting, use shortcut on preview before displaying --- qt/aqt/browser.py | 30 ++++++++++++++++++------------ qt/aqt/editor.py | 2 ++ qt/aqt/previewer.py | 3 --- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 0fc9530fc..8bb09f14f 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -868,18 +868,24 @@ QTableView {{ gridline-color: {grid} }} def setupEditor(self): def add_preview_button(leftbuttons, editor): preview_shortcut = "Ctrl+Shift+P" - leftbuttons.insert(0, editor.addButton( - None, - "preview", - lambda _editor: self.onTogglePreview(), - tr(TR.BROWSING_PREVIEW_SELECTED_CARD, val=preview_shortcut), - tr(TR.ACTIONS_PREVIEW), - id="previewButton", - keys=preview_shortcut, - disables=False, - rightside=False, - toggleable=True, - )) + leftbuttons.insert( + 0, + editor.addButton( + None, + "preview", + lambda _editor: self.onTogglePreview(), + tr( + TR.BROWSING_PREVIEW_SELECTED_CARD, + val=shortcut(preview_shortcut), + ), + tr(TR.ACTIONS_PREVIEW), + id="previewButton", + keys=preview_shortcut, + disables=False, + rightside=False, + toggleable=True, + ), + ) gui_hooks.editor_did_init_left_buttons.append(add_preview_button) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 03626ab6e..8294e90a3 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -253,6 +253,7 @@ class Editor: if func: self._links[cmd] = func if keys: + def on_activated(): func(self) @@ -263,6 +264,7 @@ class Editor: def on_hotkey(): on_activated() self.web.eval(f'toggleEditorButton("#{id}");') + else: on_hotkey = on_activated diff --git a/qt/aqt/previewer.py b/qt/aqt/previewer.py index 8d8f01bd3..6a1164bbf 100644 --- a/qt/aqt/previewer.py +++ b/qt/aqt/previewer.py @@ -309,9 +309,6 @@ class BrowserPreviewer(MultiCardPreviewer): self._last_card_id = c.id return changed - def _on_finished(self, ok): - super()._on_finished(ok) - def _on_prev_card(self): self._parent.editor.saveNow( lambda: self._parent._moveCur(QAbstractItemView.MoveUp) From fce536f20541a4dc27e9aa322b6026b861d29c2b Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 02:01:24 +0100 Subject: [PATCH 19/28] Close previewer if there is no card to render --- qt/aqt/browser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 8bb09f14f..1b4608f08 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1575,7 +1575,10 @@ where id in %s""" def _renderPreview(self): if self._previewer: - self._previewer.render_card() + if self.singleCard: + self._previewer.render_card() + else: + self.onTogglePreview() def _cleanup_preview(self): if self._previewer: From 388e958f395cfab585533d9ad5f7c714437b70dd Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 10 Jan 2021 11:30:14 +0100 Subject: [PATCH 20/28] Don't add 1 when calling _template_filter() See #913. --- qt/aqt/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 10b684524..c673cc93c 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1390,7 +1390,7 @@ QTableView {{ gridline-color: {grid} }} num=c + 1, name=self._escapeMenuItem(tmpl["name"]), ) - subm.addItem(name, self._template_filter(nt["name"], c + 1)) + subm.addItem(name, self._template_filter(nt["name"], c)) m.addChild(noteTypes.chunked()) return m From 942632d57997c457039b54864122fe543ee7cbb1 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 10 Jan 2021 11:31:00 +0100 Subject: [PATCH 21/28] Also add FilterToSearch to want_release_gil() --- pylib/rsbridge/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pylib/rsbridge/lib.rs b/pylib/rsbridge/lib.rs index f3dedb826..53f324f23 100644 --- a/pylib/rsbridge/lib.rs +++ b/pylib/rsbridge/lib.rs @@ -57,6 +57,7 @@ fn want_release_gil(method: u32) -> bool { | BackendMethod::NegateSearch | BackendMethod::ConcatenateSearches | BackendMethod::ReplaceSearchTerm + | BackendMethod::FilterToSearch ) } else { false From cf1240bb18bec783d2e474283bd4354deb5f47b9 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 13:38:20 +0100 Subject: [PATCH 22/28] Make keys parameter requiring func parameter in _addButton explicit --- qt/aqt/editor.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 8294e90a3..3f69f068e 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -252,27 +252,29 @@ class Editor: """Assign func to bridge cmd, register shortcut, return button""" if func: self._links[cmd] = func - if keys: - def on_activated(): - func(self) + if keys: - if toggleable: - # generate a random id for triggering toggle - id = id or str(randrange(1_000_000)) + def on_activated(): + func(self) - def on_hotkey(): - on_activated() - self.web.eval(f'toggleEditorButton("#{id}");') + if toggleable: + # generate a random id for triggering toggle + id = id or str(randrange(1_000_000)) - else: - on_hotkey = on_activated + def on_hotkey(): + on_activated() + self.web.eval(f'toggleEditorButton("#{id}");') + + else: + on_hotkey = on_activated + + QShortcut( # type: ignore + QKeySequence(keys), + self.widget, + activated=on_hotkey, + ) - QShortcut( # type: ignore - QKeySequence(keys), - self.widget, - activated=on_hotkey, - ) btn = self._addButton( icon, cmd, From 87bc1e69b0c637fc4427364ea1ca4c8d56c1130b Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 16:16:17 +0100 Subject: [PATCH 23/28] Coerce added/edited:0 to 1, constrain rated:n to 1 <= 365 --- rslib/src/search/parser.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 9e03abcc5..ea0ff441b 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -273,8 +273,8 @@ fn search_node_for_text_with_argument<'a>( val: &'a str, ) -> ParseResult> { Ok(match key.to_ascii_lowercase().as_str() { - "added" => SearchNode::AddedInDays(val.parse()?), - "edited" => SearchNode::EditedInDays(val.parse()?), + "added" => parse_added(val)?, + "edited" => parse_edited(val)?, "deck" => SearchNode::Deck(unescape(val)?), "note" => SearchNode::NoteType(unescape(val)?), "tag" => SearchNode::Tag(unescape(val)?), @@ -309,6 +309,20 @@ fn check_id_list(s: &str) -> ParseResult<&str> { } } +/// eg added:1 +fn parse_added(s: &str) -> ParseResult> { + let n: u32 = s.parse()?; + let days = n.max(1); + Ok(SearchNode::AddedInDays(days)) +} + +/// eg edited:1 +fn parse_edited(s: &str) -> ParseResult> { + let n: u32 = s.parse()?; + let days = n.max(1); + Ok(SearchNode::EditedInDays(n)) +} + /// eg is:due fn parse_state(s: &str) -> ParseResult> { use StateKind::*; @@ -339,7 +353,10 @@ fn parse_flag(s: &str) -> ParseResult> { /// second arg must be between 0-4 fn parse_rated(val: &str) -> ParseResult> { let mut it = val.splitn(2, ':'); - let days = it.next().unwrap().parse()?; + + let n: u32 = it.next().unwrap().parse()?; + let days = n.max(1).min(365); + let ease = match it.next() { Some(v) => { let n: u8 = v.parse()?; From a0dc33c0e8c02208fa3c18790e33222017f01039 Mon Sep 17 00:00:00 2001 From: Kyle Mills Date: Sun, 10 Jan 2021 07:18:08 -0800 Subject: [PATCH 24/28] typo --- qt/aqt/sound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index a9cd6d4f9..19e5fe26a 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -136,7 +136,7 @@ class VideoPlayer(Player): # pylint: disable=abstract-method class AVPlayer: players: List[Player] = [] - # when a new batch of audio is played, shoud the currently playing + # when a new batch of audio is played, should the currently playing # audio be stopped? interrupt_current_audio = True From 8f01887fe78bbfb1dcc5c00a8b52c92ec52f48dc Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 16:23:53 +0100 Subject: [PATCH 25/28] Remove coercion in write_rated --- rslib/src/search/parser.rs | 2 +- rslib/src/search/sqlwriter.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index ea0ff441b..0e9d343df 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -320,7 +320,7 @@ fn parse_added(s: &str) -> ParseResult> { fn parse_edited(s: &str) -> ParseResult> { let n: u32 = s.parse()?; let days = n.max(1); - Ok(SearchNode::EditedInDays(n)) + Ok(SearchNode::EditedInDays(days)) } /// eg is:due diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index a29502d63..553039271 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -216,8 +216,7 @@ impl SqlWriter<'_> { fn write_rated(&mut self, days: u32, ease: Option) -> Result<()> { let today_cutoff = self.col.timing_today()?.next_day_at; - let days = days.min(365) as i64; - let target_cutoff_ms = (today_cutoff - 86_400 * days) * 1_000; + let target_cutoff_ms = (today_cutoff - 86_400 * i64::from(days)) * 1_000; write!( self.sql, "c.id in (select cid from revlog where id>{}", From 250b89be6048d3137575c07fa51331e03270a8d6 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 16:25:52 +0100 Subject: [PATCH 26/28] Adjust pyblib test_find --- pylib/tests/test_find.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pylib/tests/test_find.py b/pylib/tests/test_find.py index e7adfafec..e82163b48 100644 --- a/pylib/tests/test_find.py +++ b/pylib/tests/test_find.py @@ -193,6 +193,7 @@ def test_findCards(): assert len(col.findCards("-prop:ease>2")) > 1 # recently failed if not isNearCutoff(): + # rated assert len(col.findCards("rated:1:1")) == 0 assert len(col.findCards("rated:1:2")) == 0 c = col.sched.getCard() @@ -204,13 +205,14 @@ def test_findCards(): assert len(col.findCards("rated:1:1")) == 1 assert len(col.findCards("rated:1:2")) == 1 assert len(col.findCards("rated:1")) == 2 - assert len(col.findCards("rated:0:2")) == 0 assert len(col.findCards("rated:2:2")) == 1 + assert len(col.findCards("rated:0")) == len(col.findCards("rated:1")) + # added - assert len(col.findCards("added:0")) == 0 col.db.execute("update cards set id = id - 86400*1000 where id = ?", id) assert len(col.findCards("added:1")) == col.cardCount() - 1 assert len(col.findCards("added:2")) == col.cardCount() + assert len(col.findCards("added:0")) == len(col.findCards("added:1")) else: print("some find tests disabled near cutoff") # empty field From adf969d37f36cb6975304a1aa68507d112db7c14 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 16:29:10 +0100 Subject: [PATCH 27/28] Add a few rslib unit tests --- rslib/src/search/sqlwriter.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 553039271..41b2c02d0 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -637,6 +637,10 @@ mod test { s(ctx, "added:3").0, format!("(c.id > {})", (timing.next_day_at - (86_400 * 3)) * 1_000) ); + assert_eq!( + s(ctx, "added:0").0, + s(ctx, "added:1").0, + ); // deck assert_eq!( @@ -727,6 +731,10 @@ mod test { (timing.next_day_at - (86_400 * 365)) * 1_000 ) ); + assert_eq!( + s(ctx, "rated:0").0, + s(ctx, "rated:1").0 + ); // props assert_eq!(s(ctx, "prop:lapses=3").0, "(lapses = 3)".to_string()); From 0b955c369958222d5554a5da6ae73ef37a7a3f72 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 10 Jan 2021 16:38:20 +0100 Subject: [PATCH 28/28] Fix formatting --- rslib/src/search/sqlwriter.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 41b2c02d0..caac139e9 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -637,10 +637,7 @@ mod test { s(ctx, "added:3").0, format!("(c.id > {})", (timing.next_day_at - (86_400 * 3)) * 1_000) ); - assert_eq!( - s(ctx, "added:0").0, - s(ctx, "added:1").0, - ); + assert_eq!(s(ctx, "added:0").0, s(ctx, "added:1").0,); // deck assert_eq!( @@ -731,10 +728,7 @@ mod test { (timing.next_day_at - (86_400 * 365)) * 1_000 ) ); - assert_eq!( - s(ctx, "rated:0").0, - s(ctx, "rated:1").0 - ); + assert_eq!(s(ctx, "rated:0").0, s(ctx, "rated:1").0); // props assert_eq!(s(ctx, "prop:lapses=3").0, "(lapses = 3)".to_string());