From b15bb4289e5fe606d39d020f52f62e5987d8f273 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 11:11:32 +0100 Subject: [PATCH 01/38] Add forgot, due and added filters on backend --- rslib/backend.proto | 6 ++++++ rslib/src/backend/mod.rs | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/rslib/backend.proto b/rslib/backend.proto index 93c1a3990..62648e10f 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -791,6 +791,12 @@ message FilterToSearchIn { string note = 4; uint32 template = 5; DupeIn dupe = 6; + // rated Again in the last x days + uint32 forgot_in = 7; + // added in the last x days + uint32 added_in = 8; + // will be due in the next x days + int32 due_in = 9; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 71e1595de..9cf765f8a 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -36,7 +36,7 @@ use crate::{ sched::timespan::{answer_button_time, time_span}, search::{ concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes, - BoolSeparator, EaseKind, Node, SearchNode, SortMode, StateKind, TemplateKind, + BoolSeparator, EaseKind, Node, PropertyKind, SearchNode, SortMode, StateKind, TemplateKind, }, stats::studied_today, sync::{ @@ -331,6 +331,15 @@ impl From for Node<'_> { note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(), text: dupe.text.into(), }), + Filter::ForgotIn(u) => Node::Search(SearchNode::Rated { + days: u, + ease: EaseKind::AnswerButton(1), + }), + Filter::AddedIn(u) => Node::Search(SearchNode::AddedInDays(u)), + Filter::DueIn(i) => Node::Search(SearchNode::Property { + operator: "<=".to_string(), + kind: PropertyKind::Due(i), + }), } } } From 51e1e82a9a55ead66cd5f5065b48ff4fa6b7094b Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 11:13:57 +0100 Subject: [PATCH 02/38] Add helper functions for search strings in col --- pylib/anki/collection.py | 75 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 94ec8124a..b16bfcbef 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -25,7 +25,20 @@ from anki.errors import AnkiError from anki.media import MediaManager, media_paths_from_col_path from anki.models import ModelManager from anki.notes import Note -from anki.rsbackend import TR, DBError, FormatTimeSpanContext, Progress, RustBackend, pb +from anki.rsbackend import ( # pylint: disable=unused-import + TR, + BackendNoteTypeID, + ConcatSeparator, + DBError, + DupeIn, + FilterToSearchIn, + FormatTimeSpanContext, + InvalidInput, + NamedFilter, + Progress, + RustBackend, + pb, +) from anki.sched import Scheduler as V1Scheduler from anki.schedv2 import Scheduler as V2Scheduler from anki.tags import TagManager @@ -467,6 +480,66 @@ class Collection: findNotes = find_notes findReplace = find_and_replace + # Search Strings + ########################################################################## + + # Helper function for the backend's search string operations. + # Pass search strings as 'searches' to normalize. + # Pass multiple to concatenate (defaults to 'and'). + # Pass 'negate=True' to negate the end result. + # May raise InvalidInput. + 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: Optional[int] = None, + added_in: Optional[int] = None, + due_in: Optional[int] = None, + ) -> str: + filters = searches or [] + + 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: + append_filter(FilterToSearchIn(forgot_in=forgot_in)) + if added_in: + append_filter(FilterToSearchIn(added_in=added_in)) + if due_in: + append_filter(FilterToSearchIn(due_in=due_in)) + if concat_by_or: + sep = ConcatSeparator.OR + else: + sep = ConcatSeparator.AND + search_string = self.backend.concatenate_searches(sep=sep, searches=filters) + if negate: + search_string = self.backend.negate_search(search_string) + return search_string + + def replace_search_term(self, search: str, replacement: str) -> str: + return self.backend.replace_search_term(search=search, replacement=replacement) + # Config ########################################################################## From ea46e2466274a1b5ad3e18cf92171b2a984a0e97 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 11:17:24 +0100 Subject: [PATCH 03/38] Use col instead of backend in aqt for search strs --- qt/aqt/browser.py | 48 ++++++++++++------------------------------- qt/aqt/dyndeckconf.py | 4 ++-- qt/aqt/sidebar.py | 25 +++++++++------------- 3 files changed, 25 insertions(+), 52 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index ee1576108..91d53d4f8 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -13,19 +13,11 @@ from typing import List, Optional, Sequence, Tuple, cast import aqt import aqt.forms from anki.cards import Card -from anki.collection import Collection +from anki.collection import Collection, InvalidInput, NamedFilter 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 ( - BackendNoteTypeID, - ConcatSeparator, - DupeIn, - FilterToSearchIn, - InvalidInput, - NamedFilter, -) from anki.stats import CardStats from anki.utils import htmlToTextLine, ids2str, isMac, isWin from aqt import AnkiQt, gui_hooks @@ -678,7 +670,7 @@ class Browser(QMainWindow): self._onRowChanged(None, None) def normalize_search(self, search: str) -> str: - normed = self.col.backend.normalize_search(search) + normed = self.col.search_string(searches=[search]) self._lastSearchTxt = normed self.form.searchEdit.lineEdit().setText(normed) return normed @@ -961,29 +953,19 @@ QTableView {{ gridline-color: {grid} }} def update_search(self, *terms: str): "Modify the current search string based on modified keys, then refresh." try: - search = self.col.backend.concatenate_searches( - sep=ConcatSeparator.AND, searches=terms - ) + search = self.col.search_string(searches=list(terms)) mods = self.mw.app.keyboardModifiers() if mods & Qt.AltModifier: - search = self.col.backend.negate_search(search) + search = self.col.search_string(negate=True, searches=[search]) cur = str(self.form.searchEdit.lineEdit().text()) if cur != self._searchPrompt: if mods & Qt.ControlModifier and mods & Qt.ShiftModifier: - search = self.col.backend.replace_search_term( - search=cur, replacement=search - ) + search = self.col.replace_search_term(cur, search) elif mods & Qt.ControlModifier: - search = self.col.backend.concatenate_searches( - # pylint: disable=no-member - sep=ConcatSeparator.AND, - searches=[cur, search], - ) + search = self.col.search_string(searches=[cur, search]) elif mods & Qt.ShiftModifier: - search = self.col.backend.concatenate_searches( - # pylint: disable=no-member - sep=ConcatSeparator.OR, - searches=[cur, search], + search = self.col.search_string( + concat_by_or=True, searches=[cur, search] ) except InvalidInput as e: show_invalid_search_error(e) @@ -1062,8 +1044,8 @@ QTableView {{ gridline-color: {grid} }} def _onSaveFilter(self) -> None: try: - filt = self.col.backend.normalize_search( - self.form.searchEdit.lineEdit().text() + filt = self.col.search_string( + searches=[self.form.searchEdit.lineEdit().text()] ) except InvalidInput as e: show_invalid_search_error(e) @@ -1105,12 +1087,12 @@ QTableView {{ gridline-color: {grid} }} def _currentFilterIsSaved(self) -> Optional[str]: filt = self.form.searchEdit.lineEdit().text() try: - filt = self.col.backend.normalize_search(filt) + filt = self.col.search_string(searches=[filt]) except InvalidInput: pass for k, v in self.col.get_config("savedFilters").items(): try: - v = self.col.backend.normalize_search(v) + v = self.col.search_string(searches=[v]) except InvalidInput: pass if filt == v: @@ -1657,11 +1639,7 @@ where id in %s""" # filter called by the editor def search_dupe(self, mid: int, text: str): self.form.searchEdit.lineEdit().setText( - self.col.backend.filter_to_search( - FilterToSearchIn( - dupe=DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text) - ) - ) + self.col.search_string(dupe=(mid, text)) ) self.onSearchActivated() diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 6c47528a1..34e68a395 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -119,11 +119,11 @@ class DeckConf(QDialog): else: d["delays"] = None - search = self.mw.col.backend.normalize_search(f.search.text()) + search = self.mw.col.search_string(searches=[f.search.text()]) terms = [[search, f.limit.value(), f.order.currentIndex()]] if f.secondFilter.isChecked(): - search_2 = self.mw.col.backend.normalize_search(f.search_2.text()) + search_2 = self.mw.col.search_string(searches=[f.search_2.text()]) terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()]) d["terms"] = terms diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 0c68570e7..f9732241c 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -9,8 +9,12 @@ from enum import Enum from typing import Iterable, List, Optional import aqt +from anki.collection import ( # pylint: disable=unused-import + FilterToSearchIn, + NamedFilter, +) from anki.errors import DeckRenameError -from anki.rsbackend import DeckTreeNode, FilterToSearchIn, NamedFilter, TagTreeNode +from anki.rsbackend import DeckTreeNode, TagTreeNode from aqt import gui_hooks from aqt.main import ResetReason from aqt.models import Models @@ -390,29 +394,20 @@ class SidebarTreeView(QTreeView): root.addChild(item) def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable: - return lambda: self.browser.update_search( - self.col.backend.filter_to_search(FilterToSearchIn(name=name)) - ) + 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.backend.filter_to_search(FilterToSearchIn(tag=tag)) - ) + 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.backend.filter_to_search(FilterToSearchIn(deck=deck)) - ) + 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.backend.filter_to_search(FilterToSearchIn(note=note)) - ) + 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.backend.filter_to_search(FilterToSearchIn(note=note)), - self.col.backend.filter_to_search(FilterToSearchIn(template=template)), + self.col.search_string(note=note), self.col.search_string(template=template) ) def _saved_filter(self, saved: str) -> Callable: From 77765d4896156972cf349147a59f2a39397db569 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 11:19:07 +0100 Subject: [PATCH 04/38] Replace remaining literal searches in aqt --- qt/aqt/customstudy.py | 20 +++++++++++++------- qt/aqt/dyndeckconf.py | 10 +++++++--- qt/aqt/main.py | 2 +- qt/aqt/overview.py | 4 ++-- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index 416f01bfe..8113438fd 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -2,6 +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.consts import * from aqt.qt import * from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr @@ -159,26 +160,29 @@ class CustomStudy(QDialog): dyn = self.mw.col.decks.get(did) # and then set various options if i == RADIO_FORGOT: - dyn["terms"][0] = ["rated:%d:1" % spin, DYN_MAX_SIZE, DYN_RANDOM] + search = self.mw.col.search_string(forgot_in=spin) + dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM] dyn["resched"] = False elif i == RADIO_AHEAD: - dyn["terms"][0] = ["prop:due<=%d" % spin, DYN_MAX_SIZE, DYN_DUE] + search = self.mw.col.search_string(due_in=spin) + dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_DUE] dyn["resched"] = True elif i == RADIO_PREVIEW: - dyn["terms"][0] = ["is:new added:%s" % spin, DYN_MAX_SIZE, DYN_OLDEST] + search = self.mw.col.search_string(name=NamedFilter.NEW, added_in=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 = "is:new " + terms = self.mw.col.search_string(name=NamedFilter.NEW) ord = DYN_ADDED dyn["resched"] = True elif type == TYPE_DUE: - terms = "is:due " + terms = self.mw.col.search_string(name=NamedFilter.DUE) ord = DYN_DUE dyn["resched"] = True elif type == TYPE_REVIEW: - terms = "-is:new " + terms = self.mw.col.search_string(negate=True, name=NamedFilter.NEW) ord = DYN_RANDOM dyn["resched"] = True else: @@ -187,7 +191,9 @@ class CustomStudy(QDialog): dyn["resched"] = False dyn["terms"][0] = [(terms + tags).strip(), spin, ord] # add deck limit - dyn["terms"][0][0] = 'deck:"%s" %s ' % (self.deck["name"], dyn["terms"][0][0]) + dyn["terms"][0][0] = self.mw.col.search_string( + deck=self.deck["name"], searches=[dyn["terms"][0][0]] + ) self.mw.col.decks.save(dyn) # generate cards self.created_custom_study = True diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 34e68a395..9ae506431 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -4,8 +4,8 @@ from typing import List, Optional import aqt +from anki.collection import InvalidInput, NamedFilter from anki.lang import without_unicode_isolation -from anki.rsbackend import InvalidInput from aqt.qt import * from aqt.utils import ( TR, @@ -47,8 +47,12 @@ class DeckConf(QDialog): self.initialSetup() self.loadConf() if search: - self.form.search.setText(search + " is:due") - self.form.search_2.setText(search + " is:new") + search = self.mw.col.search_string(searches=[search], name=NamedFilter.DUE) + self.form.search.setText(search) + search_2 = self.mw.col.search_string( + searches=[search], name=NamedFilter.NEW + ) + self.form.search_2.setText(search_2) self.form.search.selectAll() if self.mw.col.schedVer() == 1: diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 8812a4cde..2f43864b6 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1141,7 +1141,7 @@ title="%s" %s>%s""" % ( deck = self.col.decks.current() if not search: if not deck["dyn"]: - search = 'deck:"%s" ' % deck["name"] + search = self.col.search_string(deck=deck["name"]) while self.col.decks.id_for_name( without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n)) ): diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 52c3ad52b..7f0eef43e 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -71,8 +71,8 @@ class Overview: elif url == "opts": self.mw.onDeckConf() elif url == "cram": - deck = self.mw.col.decks.current() - self.mw.onCram("'deck:%s'" % deck["name"]) + deck = self.mw.col.decks.current()["name"] + self.mw.onCram(self.mw.col.search_string(deck=deck)) elif url == "refresh": self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected()) self.mw.reset() From 99cb68513cdfbe0ecfdd6bb189170c6249a999a3 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 13:26:52 +0100 Subject: [PATCH 05/38] Use backend filter for search prompt --- 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 91d53d4f8..146452b07 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -612,6 +612,7 @@ 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.form.searchEdit.addItems( [self._searchPrompt] + self.mw.pm.profile["searchHistory"] ) @@ -629,7 +630,7 @@ class Browser(QMainWindow): prompt = self.form.searchEdit.lineEdit().text() # convert guide text before we save history - txt = "deck:current " if prompt == self._searchPrompt else prompt + txt = self._searchPromptFilter if prompt == self._searchPrompt else prompt self.update_history(txt) # keep track of search string so that we reuse identical search when From 1635f20af3f2d671fe070916fa74564c0c5a4df6 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 13:31:08 +0100 Subject: [PATCH 06/38] Prevent search prompt from being saved as a filter Normalisation would render it useless. --- 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 146452b07..1cb65fc86 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1038,7 +1038,7 @@ QTableView {{ gridline-color: {grid} }} if self._currentFilterIsSaved(): ml.addItem(tr(TR.BROWSING_REMOVE_CURRENT_FILTER), self._onRemoveFilter) - else: + elif self._searchPrompt != self.form.searchEdit.lineEdit().text(): ml.addItem(tr(TR.BROWSING_SAVE_CURRENT_FILTER), self._onSaveFilter) return ml From bc52a54dfc7bb1f7c5d0d864cd23f3c4ec1a182d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 16:19:55 +0100 Subject: [PATCH 07/38] Add nid filter on backend --- rslib/backend.proto | 5 +++++ rslib/src/backend/mod.rs | 7 +++++++ rslib/src/search/parser.rs | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rslib/backend.proto b/rslib/backend.proto index 62648e10f..899d74a0b 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -50,6 +50,10 @@ message NoteID { int64 nid = 1; } +message NoteIDs { + repeated int64 nids = 1; +} + message CardID { int64 cid = 1; } @@ -797,6 +801,7 @@ message FilterToSearchIn { uint32 added_in = 8; // will be due in the next x days int32 due_in = 9; + NoteIDs nids = 10; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 9cf765f8a..6d33d6df0 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -262,6 +262,12 @@ impl From for NoteID { } } +impl pb::NoteIDs { + fn into_id_string(self) -> String { + self.nids.iter().map(|i| i.to_string()).collect::>().join(",") + } +} + impl From for NoteTypeID { fn from(ntid: pb::NoteTypeId) -> Self { NoteTypeID(ntid.ntid) @@ -340,6 +346,7 @@ impl From for Node<'_> { operator: "<=".to_string(), kind: PropertyKind::Due(i), }), + Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())), } } } diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index e9b6a377b..399ce177f 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -67,7 +67,7 @@ pub enum SearchNode<'a> { }, State(StateKind), Flag(u8), - NoteIDs(&'a str), + NoteIDs(Cow<'a, str>), CardIDs(&'a str), Property { operator: String, @@ -318,7 +318,7 @@ fn search_node_for_text_with_argument<'a>( "is" => parse_state(val)?, "did" => parse_did(val)?, "mid" => parse_mid(val)?, - "nid" => SearchNode::NoteIDs(check_id_list(val, key)?), + "nid" => SearchNode::NoteIDs(check_id_list(val, key)?.into()), "cid" => SearchNode::CardIDs(check_id_list(val, key)?), "re" => SearchNode::Regex(unescape_quotes(val)), "nc" => SearchNode::NoCombining(unescape(val)?), From 407358ab686be83df03bd34008ff93cf797fb089 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 16:21:56 +0100 Subject: [PATCH 08/38] Use backend nid filter in browser --- pylib/anki/collection.py | 4 ++++ pylib/anki/rsbackend.py | 1 + qt/aqt/browser.py | 6 +++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index b16bfcbef..4eec7758f 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -35,6 +35,7 @@ from anki.rsbackend import ( # pylint: disable=unused-import FormatTimeSpanContext, InvalidInput, NamedFilter, + NoteIDs, Progress, RustBackend, pb, @@ -503,6 +504,7 @@ class Collection: forgot_in: Optional[int] = None, added_in: Optional[int] = None, due_in: Optional[int] = None, + nids: Optional[List[int]] = None, ) -> str: filters = searches or [] @@ -528,6 +530,8 @@ class Collection: append_filter(FilterToSearchIn(added_in=added_in)) if due_in: append_filter(FilterToSearchIn(due_in=due_in)) + if nids: + append_filter(FilterToSearchIn(nids=NoteIDs(nids=nids))) if concat_by_or: sep = ConcatSeparator.OR else: diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 0a4c20b03..d4b16fe71 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -50,6 +50,7 @@ StockNoteType = pb.StockNoteType FilterToSearchIn = pb.FilterToSearchIn NamedFilter = pb.FilterToSearchIn.NamedFilter DupeIn = pb.FilterToSearchIn.DupeIn +NoteIDs = pb.NoteIDs BackendNoteTypeID = pb.NoteTypeID ConcatSeparator = pb.ConcatenateSearchesIn.Separator SyncAuth = pb.SyncAuth diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 1cb65fc86..e1e474e38 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -659,7 +659,7 @@ class Browser(QMainWindow): c = self.card = self.mw.reviewer.card nid = c and c.nid or 0 if nid: - search = "nid:%d" % nid + search = self.col.search_string(nids=[nid]) search = gui_hooks.default_search(search, c) self.model.search(search) self.focusCid(c.id) @@ -1494,7 +1494,7 @@ where id in %s""" tv = self.form.tableView tv.selectionModel().clear() - search = "nid:" + ",".join([str(x) for x in nids]) + search = self.col.search_string(nids=nids) self.search_for(search) tv.selectAll() @@ -1705,7 +1705,7 @@ where id in %s""" t += ( """
  • %s: %s""" % ( - "nid:" + ",".join(str(id) for id in nids), + self.col.search_string(nids=nids).replace('"', '"'), tr(TR.BROWSING_NOTE_COUNT, count=len(nids)), html.escape(val), ) From da6f3b7e76ca124b09dd50c0c584778962991237 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 16:29:34 +0100 Subject: [PATCH 09/38] Fix nid search test --- rslib/src/backend/mod.rs | 6 +++++- rslib/src/search/parser.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 6d33d6df0..5d5c2e9cf 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -264,7 +264,11 @@ impl From for NoteID { impl pb::NoteIDs { fn into_id_string(self) -> String { - self.nids.iter().map(|i| i.to_string()).collect::>().join(",") + self.nids + .iter() + .map(|i| i.to_string()) + .collect::>() + .join(",") } } diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 399ce177f..903540a4f 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -813,7 +813,7 @@ mod test { assert_eq!(parse("tag:hard")?, vec![Search(Tag("hard".into()))]); assert_eq!( parse("nid:1237123712,2,3")?, - vec![Search(NoteIDs("1237123712,2,3"))] + vec![Search(NoteIDs("1237123712,2,3".into()))] ); assert_eq!(parse("is:due")?, vec![Search(State(StateKind::Due))]); assert_eq!(parse("flag:3")?, vec![Search(Flag(3))]); From a930aa41f0c2a6512ed15b73da90bbefaf3182ac Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 18:59:20 +0100 Subject: [PATCH 10/38] Use backend nid filter in addcards and mediacheck --- qt/aqt/addcards.py | 4 ++-- qt/aqt/browser.py | 2 +- qt/aqt/mediacheck.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 067290342..7def6df53 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -144,7 +144,7 @@ class AddCards(QDialog): def onHistory(self) -> None: m = QMenu(self) for nid in self.history: - if self.mw.col.findNotes("nid:%s" % nid): + if self.mw.col.findNotes(self.col.search_string(nids=[nid])): note = self.mw.col.getNote(nid) fields = note.fields txt = htmlToTextLine(", ".join(fields)) @@ -162,7 +162,7 @@ class AddCards(QDialog): def editHistory(self, nid): browser = aqt.dialogs.open("Browser", self.mw) - browser.form.searchEdit.lineEdit().setText("nid:%d" % nid) + browser.form.searchEdit.lineEdit().setText(self.col.search_string(nids=[nid])) browser.onSearchActivated() def addNote(self, note) -> Optional[Note]: diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index e1e474e38..603c47d31 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1705,7 +1705,7 @@ where id in %s""" t += ( """
  • %s: %s""" % ( - self.col.search_string(nids=nids).replace('"', '"'), + self.col.search_string(nids=nids).replace('"', """), tr(TR.BROWSING_NOTE_COUNT, count=len(nids)), html.escape(val), ) diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 092689a8d..76456a860 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -146,7 +146,8 @@ class MediaChecker: if out is not None: nid, err = out browser = aqt.dialogs.open("Browser", self.mw) - browser.form.searchEdit.lineEdit().setText("nid:%d" % nid) + search = self.mw.col.search_string(nids=[nid]) + browser.form.searchEdit.lineEdit().setText(search) browser.onSearchActivated() showText(err, type="html") else: From d33442f90175eb5bca5722960b4e1547f1b1b1a9 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 19:48:01 +0100 Subject: [PATCH 11/38] Add backend filter for field name --- pylib/anki/collection.py | 3 +++ rslib/backend.proto | 1 + rslib/src/backend/mod.rs | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 4eec7758f..4f45e1145 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -505,6 +505,7 @@ class Collection: added_in: Optional[int] = None, due_in: Optional[int] = None, nids: Optional[List[int]] = None, + field_name: Optional[str] = None, ) -> str: filters = searches or [] @@ -532,6 +533,8 @@ class Collection: append_filter(FilterToSearchIn(due_in=due_in)) if nids: append_filter(FilterToSearchIn(nids=NoteIDs(nids=nids))) + if field_name: + append_filter(FilterToSearchIn(field_name=field_name)) if concat_by_or: sep = ConcatSeparator.OR else: diff --git a/rslib/backend.proto b/rslib/backend.proto index 899d74a0b..e984b1f97 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -802,6 +802,7 @@ message FilterToSearchIn { // will be due in the next x days int32 due_in = 9; NoteIDs nids = 10; + string field_name = 11; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 5d5c2e9cf..48d9065dd 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -351,6 +351,11 @@ impl From for Node<'_> { 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, + }), } } } From 423d7e5098a39e1cc4ee410e225a7a6fe5552922 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 19:49:16 +0100 Subject: [PATCH 12/38] Use backend filter for findDupes and handle excep. --- pylib/anki/find.py | 6 ++---- qt/aqt/browser.py | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pylib/anki/find.py b/pylib/anki/find.py index 33c3ca838..c7bf14cdb 100644 --- a/pylib/anki/find.py +++ b/pylib/anki/find.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Set +from typing import TYPE_CHECKING, Optional, Set, Tuple from anki.hooks import * from anki.utils import ids2str, splitFields, stripHTMLMedia @@ -71,9 +71,7 @@ def findDupes( col: Collection, fieldName: str, search: str = "" ) -> List[Tuple[Any, List]]: # limit search to notes with applicable field name - if search: - search = "(" + search + ") " - search += '"%s:*"' % fieldName.replace('"', '"') + search = col.search_string(searches=[search], field_name=fieldName) # go through notes vals: Dict[str, List[int]] = {} dupes = [] diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 603c47d31..01b9dcbcf 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1688,7 +1688,12 @@ where id in %s""" def duplicatesReport(self, web, fname, search, frm, web_context): self.mw.progress.start() - res = self.mw.col.findDupes(fname, search) + try: + res = self.mw.col.findDupes(fname, search) + except InvalidInput as e: + self.mw.progress.finish() + show_invalid_search_error(e) + return if not self._dupesButton: self._dupesButton = b = frm.buttonBox.addButton( tr(TR.BROWSING_TAG_DUPLICATES), QDialogButtonBox.ActionRole From 251fe50660f5364fb5c52a9e4f6d1c53ec5027fe Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 20:01:20 +0100 Subject: [PATCH 13/38] Fix search calls in addcards --- qt/aqt/addcards.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 7def6df53..7d01cf472 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -144,7 +144,7 @@ class AddCards(QDialog): def onHistory(self) -> None: m = QMenu(self) for nid in self.history: - if self.mw.col.findNotes(self.col.search_string(nids=[nid])): + if self.mw.col.findNotes(self.mw.col.search_string(nids=[nid])): note = self.mw.col.getNote(nid) fields = note.fields txt = htmlToTextLine(", ".join(fields)) @@ -162,7 +162,7 @@ class AddCards(QDialog): def editHistory(self, nid): browser = aqt.dialogs.open("Browser", self.mw) - browser.form.searchEdit.lineEdit().setText(self.col.search_string(nids=[nid])) + browser.form.searchEdit.lineEdit().setText(self.mw.col.search_string(nids=[nid])) browser.onSearchActivated() def addNote(self, note) -> Optional[Note]: From f04228990ddc04d28940930bff87382928af7fb8 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 28 Jan 2021 20:51:32 +0100 Subject: [PATCH 14/38] Add browser_search helper in mw --- qt/aqt/addcards.py | 4 +--- qt/aqt/browser.py | 7 ------- qt/aqt/editor.py | 3 +-- qt/aqt/emptycards.py | 4 +--- qt/aqt/main.py | 10 ++++++++++ qt/aqt/mediacheck.py | 5 +---- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 7d01cf472..3cfdde261 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -161,9 +161,7 @@ class AddCards(QDialog): m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) def editHistory(self, nid): - browser = aqt.dialogs.open("Browser", self.mw) - browser.form.searchEdit.lineEdit().setText(self.mw.col.search_string(nids=[nid])) - browser.onSearchActivated() + self.mw.browser_search(nids=[nid]) def addNote(self, note) -> Optional[Note]: note.model()["did"] = self.deckChooser.selectedId() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 01b9dcbcf..317652c5a 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1637,13 +1637,6 @@ where id in %s""" # Edit: finding dupes ###################################################################### - # filter called by the editor - def search_dupe(self, mid: int, text: str): - self.form.searchEdit.lineEdit().setText( - self.col.search_string(dupe=(mid, text)) - ) - self.onSearchActivated() - def onFindDupes(self): self.editor.saveNow(self._onFindDupes) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 5e276f1fe..cff937d44 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -539,8 +539,7 @@ class Editor: self.web.eval("setBackgrounds(%s);" % json.dumps(cols)) def showDupes(self): - browser = aqt.dialogs.open("Browser", self.mw) - browser.search_dupe(self.note.model()["id"], self.note.fields[0]) + self.mw.browser_search(dupe=(self.note.model()["id"], self.note.fields[0])) def fieldsAreBlank(self, previousNote=None): if not self.note: diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index d7d0d0801..49df85598 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -66,9 +66,7 @@ class EmptyCardsDialog(QDialog): self._delete_button.clicked.connect(self._on_delete) def _on_note_link_clicked(self, link): - browser = aqt.dialogs.open("Browser", self.mw) - browser.form.searchEdit.lineEdit().setText(link) - browser.onSearchActivated() + self.mw.browser_search(searches=[link]) def _on_delete(self): self.mw.progress.start() diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 2f43864b6..1f1dc79aa 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1617,3 +1617,13 @@ title="%s" %s>%s""" % ( def serverURL(self) -> str: return "http://127.0.0.1:%d/" % self.mediaServer.getPort() + + # Helpers for all windows + ########################################################################## + + # Wrapper for col.search_string() to look up the result in the browser. + def browser_search(self, **kwargs) -> None: + search = self.col.search_string(**kwargs) + browser = aqt.dialogs.open("Browser", self) + browser.form.searchEdit.lineEdit().setText(search) + browser.onSearchActivated() diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 76456a860..86616cec1 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -145,10 +145,7 @@ class MediaChecker: if out is not None: nid, err = out - browser = aqt.dialogs.open("Browser", self.mw) - search = self.mw.col.search_string(nids=[nid]) - browser.form.searchEdit.lineEdit().setText(search) - browser.onSearchActivated() + self.mw.browser_search(nids=[nid]) showText(err, type="html") else: tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED)) From 1fb60244549922d7cd83e8740daef5a22f771a08 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 29 Jan 2021 09:38:13 +0100 Subject: [PATCH 15/38] Rename filters added_in etc. to added_in_days --- pylib/anki/collection.py | 18 +++++++++--------- qt/aqt/customstudy.py | 6 +++--- rslib/backend.proto | 9 +++------ rslib/src/backend/mod.rs | 6 +++--- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 4f45e1145..076c40186 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -501,9 +501,9 @@ class Collection: note: Optional[str] = None, template: Optional[int] = None, dupe: Optional[Tuple[int, str]] = None, - forgot_in: Optional[int] = None, - added_in: Optional[int] = None, - due_in: Optional[int] = 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, ) -> str: @@ -525,12 +525,12 @@ class Collection: if dupe: dupe_in = DupeIn(mid=BackendNoteTypeID(ntid=dupe[0]), text=dupe[1]) append_filter(FilterToSearchIn(dupe=dupe_in)) - if forgot_in: - append_filter(FilterToSearchIn(forgot_in=forgot_in)) - if added_in: - append_filter(FilterToSearchIn(added_in=added_in)) - if due_in: - append_filter(FilterToSearchIn(due_in=due_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: diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index 8113438fd..ca5c5c3c6 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -160,15 +160,15 @@ 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=spin) + search = self.mw.col.search_string(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=spin) + search = self.mw.col.search_string(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=spin) + search = self.mw.col.search_string(name=NamedFilter.NEW, added_in_days=spin) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST] dyn["resched"] = False elif i == RADIO_CRAM: diff --git a/rslib/backend.proto b/rslib/backend.proto index e984b1f97..fed42f957 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -795,12 +795,9 @@ message FilterToSearchIn { string note = 4; uint32 template = 5; DupeIn dupe = 6; - // rated Again in the last x days - uint32 forgot_in = 7; - // added in the last x days - uint32 added_in = 8; - // will be due in the next x days - int32 due_in = 9; + uint32 forgot_in_days = 7; + uint32 added_in_days = 8; + int32 due_in_days = 9; NoteIDs nids = 10; string field_name = 11; } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 48d9065dd..5a6f6aba2 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -341,12 +341,12 @@ impl From for Node<'_> { note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(), text: dupe.text.into(), }), - Filter::ForgotIn(u) => Node::Search(SearchNode::Rated { + Filter::ForgotInDays(u) => Node::Search(SearchNode::Rated { days: u, ease: EaseKind::AnswerButton(1), }), - Filter::AddedIn(u) => Node::Search(SearchNode::AddedInDays(u)), - Filter::DueIn(i) => Node::Search(SearchNode::Property { + Filter::AddedInDays(u) => Node::Search(SearchNode::AddedInDays(u)), + Filter::DueInDays(i) => Node::Search(SearchNode::Property { operator: "<=".to_string(), kind: PropertyKind::Due(i), }), From 349bd9d6818a283bd735e41ade8db68c327e21b0 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 29 Jan 2021 09:40:21 +0100 Subject: [PATCH 16/38] Use proper docstrings --- pylib/anki/collection.py | 13 +++++++------ qt/aqt/main.py | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 076c40186..379f50e1e 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -484,11 +484,6 @@ class Collection: # Search Strings ########################################################################## - # Helper function for the backend's search string operations. - # Pass search strings as 'searches' to normalize. - # Pass multiple to concatenate (defaults to 'and'). - # Pass 'negate=True' to negate the end result. - # May raise InvalidInput. def search_string( self, *, @@ -507,7 +502,13 @@ class Collection: nids: Optional[List[int]] = None, field_name: Optional[str] = None, ) -> str: - filters = searches or [] + """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 'negate=True' to negate the end result. + May raise InvalidInput. + """ def append_filter(filter_in): filters.append(self.backend.filter_to_search(filter_in)) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 1f1dc79aa..b0e46f6c3 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1621,8 +1621,9 @@ title="%s" %s>%s""" % ( # Helpers for all windows ########################################################################## - # Wrapper for col.search_string() to look up the result in the browser. def browser_search(self, **kwargs) -> None: + """Wrapper for col.search_string() to look up the result in the browser.""" + search = self.col.search_string(**kwargs) browser = aqt.dialogs.open("Browser", self) browser.form.searchEdit.lineEdit().setText(search) From c299e271e8858be7abe53d858fc630dee92725cf Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 29 Jan 2021 18:27:33 +0100 Subject: [PATCH 17/38] Refactor search_string() and FilterToSearchIn See #955. --- pylib/anki/collection.py | 117 +++++++++++++++++++++------------------ pylib/anki/find.py | 41 +------------- pylib/anki/rsbackend.py | 4 +- pylib/anki/tags.py | 5 +- qt/aqt/addcards.py | 5 +- qt/aqt/browser.py | 64 +++++++++++---------- qt/aqt/customstudy.py | 22 +++++--- qt/aqt/dyndeckconf.py | 12 ++-- qt/aqt/editor.py | 5 +- qt/aqt/emptycards.py | 2 +- qt/aqt/main.py | 10 ++-- qt/aqt/mediacheck.py | 3 +- qt/aqt/overview.py | 3 +- qt/aqt/sidebar.py | 42 ++++---------- rslib/backend.proto | 56 +++++++++---------- rslib/src/backend/mod.rs | 64 +++++++++------------ 16 files changed, 201 insertions(+), 254 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 379f50e1e..639f8cb88 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -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 diff --git a/pylib/anki/find.py b/pylib/anki/find.py index c7bf14cdb..ff83d5e2e 100644 --- a/pylib/anki/find.py +++ b/pylib/anki/find.py @@ -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 diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index d4b16fe71..90ed6a815 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -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 diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 5ced38aa2..c8e969568 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -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) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 3cfdde261..7de8b3f24 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -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() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 317652c5a..a91834d01 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -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 += ( """
  • %s: %s""" % ( - self.col.search_string(nids=nids).replace('"', """), + html.escape(self.col.build_search_string(nid_search_term(nids))), tr(TR.BROWSING_NOTE_COUNT, count=len(nids)), html.escape(val), ) diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index ca5c5c3c6..e65f4040b 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -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 diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 9ae506431..1a0242835 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -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 diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index cff937d44..9899390c5 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -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: diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index 49df85598..e399ce1da 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -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() diff --git a/qt/aqt/main.py b/qt/aqt/main.py index b0e46f6c3..c98973da9 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -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""" % ( 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""" % ( # 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() diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 86616cec1..3b128975d 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -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)) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 7f0eef43e..088065c95 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -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() diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f9732241c..d90c2804d 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -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 ########################### diff --git a/rslib/backend.proto b/rslib/backend.proto index fed42f957..44b028845 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -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; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 5a6f6aba2..8d6b19dc7 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -293,38 +293,8 @@ impl From for DeckConfID { impl From 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 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 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)), + }, } } } From 671c6a7b3e50518ae99a3d050b42e9fb788d21fa Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 29 Jan 2021 21:07:42 +0100 Subject: [PATCH 18/38] Rework search initialisation - Remove _searchPrompt. - Add placeholder prompt. - Move search for current card from browser to caller. (Thus, support current card search even with opened browser.) --- ftl/core/browsing.ftl | 1 + qt/aqt/browser.py | 65 ++++++++++++++++++++----------------------- qt/aqt/main.py | 3 +- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index eea2c7fb5..bd2a372b8 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -83,6 +83,7 @@ browsing-reposition = Reposition... browsing-reposition-new-cards = Reposition New Cards browsing-reschedule = Reschedule browsing-save-current-filter = Save Current Filter... +browsing-search-bar-hint = Type here and press Enter to search. Leave empty to show whole collection. browsing-search-in = Search in: browsing-search-within-formatting-slow = Search within formatting (slow) browsing-shift-position-of-existing-cards = Shift position of existing cards diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index a91834d01..375269bf1 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -611,15 +611,14 @@ class Browser(QMainWindow): qconnect(self.form.searchButton.clicked, self.onSearchActivated) 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.build_search_string( - SearchTerm(current_deck=True) + self.form.searchEdit.lineEdit().setPlaceholderText( + tr(TR.BROWSING_SEARCH_BAR_HINT) ) self.form.searchEdit.addItems( - [self._searchPrompt] + self.mw.pm.profile["searchHistory"] + [self.col.build_search_string(SearchTerm(current_deck=True))] + + self.mw.pm.profile["searchHistory"] ) - self.search_for("is:current", self._searchPrompt) - # then replace text for easily showing the deck + self._onRowChanged(None, None) self.form.searchEdit.lineEdit().selectAll() self.form.searchEdit.setFocus() @@ -629,15 +628,12 @@ class Browser(QMainWindow): def _onSearchActivated(self): # grab search text and normalize - prompt = self.form.searchEdit.lineEdit().text() - - # convert guide text before we save history - txt = self._searchPromptFilter if prompt == self._searchPrompt else prompt - self.update_history(txt) + text = self.form.searchEdit.lineEdit().text() + self.update_history(text) # keep track of search string so that we reuse identical search when # refreshing, rather than whatever is currently in the search field - self.search_for(txt) + self.search_for(text) def update_history(self, search: str) -> None: sh = self.mw.pm.profile["searchHistory"] @@ -649,25 +645,14 @@ class Browser(QMainWindow): self.form.searchEdit.addItems(sh) self.mw.pm.profile["searchHistory"] = sh - def search_for(self, search: str, prompt: Optional[str] = None) -> None: + def search_for(self, search: str) -> None: self._lastSearchTxt = search - self.form.searchEdit.lineEdit().setText(prompt or search) + self.form.searchEdit.lineEdit().setText(search) self.search() # search triggered programmatically. caller must have saved note first. def search(self) -> None: - if "is:current" in self._lastSearchTxt: - # show current card if there is one - c = self.card = self.mw.reviewer.card - nid = c and c.nid or 0 - if 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) - else: - self.model.search(self._lastSearchTxt) - + self.model.search(self._lastSearchTxt) if not self.model.cards: # no row change will fire self._onRowChanged(None, None) @@ -688,6 +673,17 @@ class Browser(QMainWindow): ) return selected + def show_single_card(self, card: Optional[Card]) -> None: + nid = card and card.nid + if nid: + self.card = card + search = self.col.build_search_string(nid_search_term([nid])) + search = gui_hooks.default_search(search, card) + self.form.searchEdit.lineEdit().setText(search) + self.onSearchActivated() + self.form.tableView.clearSelection() + self.focusCid(card.id) + def onReset(self): self.sidebar.refresh() self.editor.setNote(None) @@ -954,20 +950,19 @@ QTableView {{ gridline-color: {grid} }} ml.popupOver(self.form.filter) def update_search(self, *terms: Union[str, SearchTerm]): - "Modify the current search string based on modified keys, then refresh." + """Modify the current search string based on modified keys, then refresh.""" try: search = self.col.build_search_string(*terms) mods = self.mw.app.keyboardModifiers() if mods & Qt.AltModifier: 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.build_search_string(cur, search) - elif mods & Qt.ShiftModifier: - search = self.col.build_search_string(cur, search, match_any=True) + if mods & Qt.ControlModifier and mods & Qt.ShiftModifier: + search = self.col.replace_search_term(cur, search) + elif mods & Qt.ControlModifier: + search = self.col.build_search_string(cur, search) + elif mods & Qt.ShiftModifier: + search = self.col.build_search_string(cur, search, match_any=True) except InvalidInput as e: show_invalid_search_error(e) else: @@ -1038,7 +1033,7 @@ QTableView {{ gridline-color: {grid} }} if self._currentFilterIsSaved(): ml.addItem(tr(TR.BROWSING_REMOVE_CURRENT_FILTER), self._onRemoveFilter) - elif self._searchPrompt != self.form.searchEdit.lineEdit().text(): + else: ml.addItem(tr(TR.BROWSING_SAVE_CURRENT_FILTER), self._onSaveFilter) return ml diff --git a/qt/aqt/main.py b/qt/aqt/main.py index c98973da9..2653e11de 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1045,7 +1045,8 @@ title="%s" %s>%s""" % ( aqt.dialogs.open("AddCards", self) def onBrowse(self) -> None: - aqt.dialogs.open("Browser", self) + browser = aqt.dialogs.open("Browser", self) + browser.show_single_card(self.reviewer.card) def onEditCurrent(self): aqt.dialogs.open("EditCurrent", self) From 2ca4f31fc80a251b450e05a4faf296c055bcb316 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 29 Jan 2021 23:05:51 +0100 Subject: [PATCH 19/38] Update search history only after successful search Ergo, don't save invalid searches, but also save searches normalised so equivalent searches get saved only once. --- qt/aqt/browser.py | 54 +++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 375269bf1..70f214678 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -186,7 +186,6 @@ class DataModel(QAbstractTableModel): def search(self, txt: str) -> None: self.beginReset() self.cards = [] - exception: Optional[Exception] = None try: ctx = SearchContext(search=txt, browser=self.browser) gui_hooks.browser_will_search(ctx) @@ -195,14 +194,11 @@ class DataModel(QAbstractTableModel): ctx.card_ids = self.col.find_cards(ctx.search, order=ctx.order) gui_hooks.browser_did_search(ctx) self.cards = ctx.card_ids - except Exception as e: - exception = e + except Exception as err: + raise err finally: self.endReset() - if exception: - show_invalid_search_error(exception) - def reset(self): self.beginReset() self.endReset() @@ -627,36 +623,44 @@ class Browser(QMainWindow): self.editor.saveNow(self._onSearchActivated) def _onSearchActivated(self): - # grab search text and normalize text = self.form.searchEdit.lineEdit().text() - self.update_history(text) + if self.search_for(text): + # Only save successful searches. + self.update_history() + def search_for(self, search: str) -> bool: # keep track of search string so that we reuse identical search when # refreshing, rather than whatever is currently in the search field - self.search_for(text) + self._lastSearchTxt = search + self.form.searchEdit.lineEdit().setText(search) + return self.search() - def update_history(self, search: str) -> None: + def search(self) -> bool: + """Search triggered programmatically. Caller must have saved note first. + Return bool indicating success. + """ + + try: + self.model.search(self._lastSearchTxt) + except InvalidInput as err: + show_invalid_search_error(err) + return False + else: + if not self.model.cards: + # no row change will fire + self._onRowChanged(None, None) + return True + + def update_history(self) -> None: sh = self.mw.pm.profile["searchHistory"] - if search in sh: - sh.remove(search) - sh.insert(0, search) + if self._lastSearchTxt in sh: + sh.remove(self._lastSearchTxt) + sh.insert(0, self._lastSearchTxt) sh = sh[:30] self.form.searchEdit.clear() self.form.searchEdit.addItems(sh) self.mw.pm.profile["searchHistory"] = sh - def search_for(self, search: str) -> None: - self._lastSearchTxt = search - self.form.searchEdit.lineEdit().setText(search) - self.search() - - # search triggered programmatically. caller must have saved note first. - def search(self) -> None: - self.model.search(self._lastSearchTxt) - if not self.model.cards: - # no row change will fire - self._onRowChanged(None, None) - def normalize_search(self, search: str) -> str: normed = self.col.build_search_string(search) self._lastSearchTxt = normed From 22b80c2dd5ee49190425348bad4f2b4d06b22232 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 10:26:23 +1000 Subject: [PATCH 20/38] combine forgot_in_days and studied_today into a more general 'rated' --- qt/aqt/browser.py | 14 ++++++++++++-- qt/aqt/customstudy.py | 8 +++++++- rslib/backend.proto | 17 ++++++++++++++--- rslib/src/backend/mod.rs | 23 ++++++++++++++++------- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 70f214678..8de44a1c0 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -993,8 +993,18 @@ QTableView {{ gridline-color: {grid} }} self._simpleFilters( ( (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)), + ( + tr(TR.BROWSING_STUDIED_TODAY), + SearchTerm(rated=SearchTerm.Rated(days=1)), + ), + ( + tr(TR.BROWSING_AGAIN_TODAY), + SearchTerm( + rated=SearchTerm.Rated( + days=1, rating=SearchTerm.Rated.Rating.ANSWER_BUTTON_1 + ) + ), + ), ) ) ) diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index e65f4040b..58198bf77 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -160,7 +160,13 @@ class CustomStudy(QDialog): dyn = self.mw.col.decks.get(did) # and then set various options if i == RADIO_FORGOT: - search = self.mw.col.build_search_string(SearchTerm(forgot_in_days=spin)) + search = self.mw.col.build_search_string( + SearchTerm( + rated=SearchTerm.Rated( + days=spin, rating=SearchTerm.Rated.Rating.ANSWER_BUTTON_1 + ) + ) + ) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM] dyn["resched"] = False elif i == RADIO_AHEAD: diff --git a/rslib/backend.proto b/rslib/backend.proto index 44b028845..90b11dded 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -777,6 +777,18 @@ message FilterToSearchIn { GREEN = 4; BLUE = 5; } + message Rated { + enum Rating { + ANY_ANSWER_BUTTON = 0; + ANSWER_BUTTON_1 = 1; + ANSWER_BUTTON_2 = 2; + ANSWER_BUTTON_3 = 3; + ANSWER_BUTTON_4 = 4; + MANUAL_RESCHEDULE = 5; + } + uint32 days = 1; + Rating rating = 2; + } oneof filter { string tag = 1; string deck = 2; @@ -785,19 +797,18 @@ message FilterToSearchIn { NoteIDs nids = 5; DupeIn dupe = 6; string field_name = 7; - uint32 forgot_in_days = 8; + Rated rated = 8; uint32 added_in_days = 9; int32 due_in_days = 10; bool whole_collection = 11; bool current_deck = 12; - bool studied_today = 13; + Flag flag = 13; bool new = 14; bool learn = 15; bool review = 16; bool due = 17; bool suspended = 18; bool buried = 19; - Flag flag = 20; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 8d6b19dc7..297c649c6 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -317,9 +317,9 @@ impl From for Node<'_> { text: "*".to_string().into(), is_re: false, }), - Filter::ForgotInDays(u) => Node::Search(SearchNode::Rated { - days: u, - ease: EaseKind::AnswerButton(1), + Filter::Rated(rated) => Node::Search(SearchNode::Rated { + days: rated.days, + ease: rated.rating().into(), }), Filter::AddedInDays(u) => Node::Search(SearchNode::AddedInDays(u)), Filter::DueInDays(i) => Node::Search(SearchNode::Property { @@ -328,10 +328,6 @@ impl From for Node<'_> { }), 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)), @@ -359,6 +355,19 @@ impl From for BoolSeparator { } } +impl From for EaseKind { + fn from(r: pb::filter_to_search_in::rated::Rating) -> Self { + match r { + pb::filter_to_search_in::rated::Rating::AnswerButton1 => EaseKind::AnswerButton(1), + pb::filter_to_search_in::rated::Rating::AnswerButton2 => EaseKind::AnswerButton(2), + pb::filter_to_search_in::rated::Rating::AnswerButton3 => EaseKind::AnswerButton(3), + pb::filter_to_search_in::rated::Rating::AnswerButton4 => EaseKind::AnswerButton(4), + pb::filter_to_search_in::rated::Rating::AnyAnswerButton => EaseKind::AnyAnswerButton, + pb::filter_to_search_in::rated::Rating::ManualReschedule => EaseKind::ManualReschedule, + } + } +} + impl BackendService for Backend { fn latest_progress(&self, _input: Empty) -> BackendResult { let progress = self.progress_state.lock().unwrap().last_progress; From 52bac7a7a19045e0201633594cb136689e0a20f4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 10:49:00 +1000 Subject: [PATCH 21/38] use a separate enum for the is:* searches --- qt/.pylintrc | 1 + qt/aqt/browser.py | 30 ++++++++++++++++++++++++------ qt/aqt/customstudy.py | 13 +++++++++---- qt/aqt/dyndeckconf.py | 8 ++++++-- rslib/backend.proto | 15 +++++++++------ rslib/src/backend/mod.rs | 24 ++++++++++++++++++------ 6 files changed, 67 insertions(+), 24 deletions(-) diff --git a/qt/.pylintrc b/qt/.pylintrc index 608b97226..557c113be 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -5,6 +5,7 @@ ignore = forms,hooks_gen.py [TYPECHECK] ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio +ignored-classes=FilterToSearchIn [REPORTS] output-format=colorized diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 8de44a1c0..9d5f434cf 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1015,13 +1015,31 @@ QTableView {{ gridline-color: {grid} }} subm.addChild( self._simpleFilters( ( - (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)), + ( + tr(TR.ACTIONS_NEW), + SearchTerm(card_state=SearchTerm.CardState.NEW), + ), + ( + tr(TR.SCHEDULING_LEARNING), + SearchTerm(card_state=SearchTerm.CardState.LEARN), + ), + ( + tr(TR.SCHEDULING_REVIEW), + SearchTerm(card_state=SearchTerm.CardState.REVIEW), + ), + ( + tr(TR.FILTERING_IS_DUE), + SearchTerm(card_state=SearchTerm.CardState.DUE), + ), None, - (tr(TR.BROWSING_SUSPENDED), SearchTerm(suspended=True)), - (tr(TR.BROWSING_BURIED), SearchTerm(buried=True)), + ( + tr(TR.BROWSING_SUSPENDED), + SearchTerm(card_state=SearchTerm.CardState.SUSPENDED), + ), + ( + tr(TR.BROWSING_BURIED), + SearchTerm(card_state=SearchTerm.CardState.BURIED), + ), None, (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=Flag.RED)), (tr(TR.ACTIONS_ORANGE_FLAG), SearchTerm(flag=Flag.ORANGE)), diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index 58198bf77..4610f9910 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -175,23 +175,28 @@ class CustomStudy(QDialog): dyn["resched"] = True elif i == RADIO_PREVIEW: search = self.mw.col.build_search_string( - SearchTerm(new=True), SearchTerm(added_in_days=spin) + SearchTerm(card_state=SearchTerm.CardState.NEW), + 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.build_search_string(SearchTerm(new=True)) + terms = self.mw.col.build_search_string( + SearchTerm(card_state=SearchTerm.CardState.NEW) + ) ord = DYN_ADDED dyn["resched"] = True elif type == TYPE_DUE: - terms = self.mw.col.build_search_string(SearchTerm(due=True)) + terms = self.mw.col.build_search_string( + SearchTerm(card_state=SearchTerm.CardState.DUE) + ) ord = DYN_DUE dyn["resched"] = True elif type == TYPE_REVIEW: terms = self.mw.col.build_search_string( - SearchTerm(new=True), negate=True + SearchTerm(card_state=SearchTerm.CardState.NEW), negate=True ) ord = DYN_RANDOM dyn["resched"] = True diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 1a0242835..1ea32670f 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -47,9 +47,13 @@ class DeckConf(QDialog): self.initialSetup() self.loadConf() if search: - search = self.mw.col.build_search_string(search, SearchTerm(due=True)) + search = self.mw.col.build_search_string( + search, SearchTerm(card_state=SearchTerm.CardState.DUE) + ) self.form.search.setText(search) - search_2 = self.mw.col.build_search_string(search, SearchTerm(new=True)) + search_2 = self.mw.col.build_search_string( + search, SearchTerm(card_state=SearchTerm.CardState.NEW) + ) self.form.search_2.setText(search_2) self.form.search.selectAll() diff --git a/rslib/backend.proto b/rslib/backend.proto index 90b11dded..0bb4b5d97 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -789,6 +789,14 @@ message FilterToSearchIn { uint32 days = 1; Rating rating = 2; } + enum CardState { + NEW = 0; + LEARN = 1; + REVIEW = 2; + DUE = 3; + SUSPENDED = 4; + BURIED = 5; + } oneof filter { string tag = 1; string deck = 2; @@ -803,12 +811,7 @@ message FilterToSearchIn { bool whole_collection = 11; bool current_deck = 12; Flag flag = 13; - bool new = 14; - bool learn = 15; - bool review = 16; - bool due = 17; - bool suspended = 18; - bool buried = 19; + CardState card_state = 14; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 297c649c6..f536f431e 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -328,12 +328,11 @@ impl From for Node<'_> { }), Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection), Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())), - 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::CardState(state) => Node::Search(SearchNode::State( + pb::filter_to_search_in::CardState::from_i32(state) + .unwrap_or_default() + .into(), + )), 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)))), @@ -368,6 +367,19 @@ impl From for EaseKind { } } +impl From for StateKind { + fn from(k: pb::filter_to_search_in::CardState) -> Self { + match k { + pb::filter_to_search_in::CardState::New => StateKind::New, + pb::filter_to_search_in::CardState::Learn => StateKind::Learning, + pb::filter_to_search_in::CardState::Review => StateKind::Review, + pb::filter_to_search_in::CardState::Due => StateKind::Due, + pb::filter_to_search_in::CardState::Suspended => StateKind::Suspended, + pb::filter_to_search_in::CardState::Buried => StateKind::Buried, + } + } +} + impl BackendService for Backend { fn latest_progress(&self, _input: Empty) -> BackendResult { let progress = self.progress_state.lock().unwrap().last_progress; From 73b897c7548b8853fd771bac8a0ea79224d96f4a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 10:54:21 +1000 Subject: [PATCH 22/38] rename FilterToSearchIn in backend to match frontend --- pylib/anki/rsbackend.py | 6 +++--- qt/.pylintrc | 2 +- rslib/backend.proto | 4 ++-- rslib/src/backend/mod.rs | 44 ++++++++++++++++++++-------------------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 90ed6a815..994285484 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -47,9 +47,9 @@ TagTreeNode = pb.TagTreeNode NoteType = pb.NoteType DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType -SearchTerm = pb.FilterToSearchIn -Flag = pb.FilterToSearchIn.Flag -DupeIn = pb.FilterToSearchIn.DupeIn +SearchTerm = pb.SearchTerm +Flag = pb.SearchTerm.Flag +DupeIn = pb.SearchTerm.DupeIn NoteIDs = pb.NoteIDs BackendNoteTypeID = pb.NoteTypeID ConcatSeparator = pb.ConcatenateSearchesIn.Separator diff --git a/qt/.pylintrc b/qt/.pylintrc index 557c113be..33e90dfdc 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -5,7 +5,7 @@ ignore = forms,hooks_gen.py [TYPECHECK] ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio -ignored-classes=FilterToSearchIn +ignored-classes=SearchTerm [REPORTS] output-format=colorized diff --git a/rslib/backend.proto b/rslib/backend.proto index 0bb4b5d97..658e2952c 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -88,7 +88,7 @@ service BackendService { // searching - rpc FilterToSearch(FilterToSearchIn) returns (String); + rpc FilterToSearch(SearchTerm) returns (String); rpc NormalizeSearch(String) returns (String); rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); @@ -764,7 +764,7 @@ message BuiltinSearchOrder { bool reverse = 2; } -message FilterToSearchIn { +message SearchTerm { message DupeIn { NoteTypeID mid = 1; string text = 2; diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index f536f431e..c86f57063 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -290,10 +290,10 @@ impl From for DeckConfID { } } -impl From for Node<'_> { - fn from(msg: pb::FilterToSearchIn) -> Self { - use pb::filter_to_search_in::Filter; - use pb::filter_to_search_in::Flag; +impl From for Node<'_> { + fn from(msg: pb::SearchTerm) -> Self { + use pb::search_term::Filter; + use pb::search_term::Flag; match msg.filter.unwrap_or(Filter::WholeCollection(true)) { Filter::Tag(s) => Node::Search(SearchNode::Tag( escape_anki_wildcards(&s).into_owned().into(), @@ -329,7 +329,7 @@ impl From for Node<'_> { Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection), Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())), Filter::CardState(state) => Node::Search(SearchNode::State( - pb::filter_to_search_in::CardState::from_i32(state) + pb::search_term::CardState::from_i32(state) .unwrap_or_default() .into(), )), @@ -354,28 +354,28 @@ impl From for BoolSeparator { } } -impl From for EaseKind { - fn from(r: pb::filter_to_search_in::rated::Rating) -> Self { +impl From for EaseKind { + fn from(r: pb::search_term::rated::Rating) -> Self { match r { - pb::filter_to_search_in::rated::Rating::AnswerButton1 => EaseKind::AnswerButton(1), - pb::filter_to_search_in::rated::Rating::AnswerButton2 => EaseKind::AnswerButton(2), - pb::filter_to_search_in::rated::Rating::AnswerButton3 => EaseKind::AnswerButton(3), - pb::filter_to_search_in::rated::Rating::AnswerButton4 => EaseKind::AnswerButton(4), - pb::filter_to_search_in::rated::Rating::AnyAnswerButton => EaseKind::AnyAnswerButton, - pb::filter_to_search_in::rated::Rating::ManualReschedule => EaseKind::ManualReschedule, + pb::search_term::rated::Rating::AnswerButton1 => EaseKind::AnswerButton(1), + pb::search_term::rated::Rating::AnswerButton2 => EaseKind::AnswerButton(2), + pb::search_term::rated::Rating::AnswerButton3 => EaseKind::AnswerButton(3), + pb::search_term::rated::Rating::AnswerButton4 => EaseKind::AnswerButton(4), + pb::search_term::rated::Rating::AnyAnswerButton => EaseKind::AnyAnswerButton, + pb::search_term::rated::Rating::ManualReschedule => EaseKind::ManualReschedule, } } } -impl From for StateKind { - fn from(k: pb::filter_to_search_in::CardState) -> Self { +impl From for StateKind { + fn from(k: pb::search_term::CardState) -> Self { match k { - pb::filter_to_search_in::CardState::New => StateKind::New, - pb::filter_to_search_in::CardState::Learn => StateKind::Learning, - pb::filter_to_search_in::CardState::Review => StateKind::Review, - pb::filter_to_search_in::CardState::Due => StateKind::Due, - pb::filter_to_search_in::CardState::Suspended => StateKind::Suspended, - pb::filter_to_search_in::CardState::Buried => StateKind::Buried, + pb::search_term::CardState::New => StateKind::New, + pb::search_term::CardState::Learn => StateKind::Learning, + pb::search_term::CardState::Review => StateKind::Review, + pb::search_term::CardState::Due => StateKind::Due, + pb::search_term::CardState::Suspended => StateKind::Suspended, + pb::search_term::CardState::Buried => StateKind::Buried, } } } @@ -502,7 +502,7 @@ impl BackendService for Backend { // searching //----------------------------------------------- - fn filter_to_search(&self, input: pb::FilterToSearchIn) -> Result { + fn filter_to_search(&self, input: pb::SearchTerm) -> Result { Ok(write_nodes(&[input.into()]).into()) } From 5e6dd54c8e44689ecbea1336dbe51827d91c9cd3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 11:01:11 +1000 Subject: [PATCH 23/38] export SearchTerm from collection.py, and avoid exporting embedded items --- pylib/anki/collection.py | 6 ++---- pylib/anki/rsbackend.py | 3 --- qt/aqt/browser.py | 17 ++++++++++------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 639f8cb88..6c3a1434a 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -16,6 +16,7 @@ import anki.find import anki.latex # sets up hook import anki.template from anki import hooks +from anki.backend_pb2 import SearchTerm from anki.cards import Card from anki.config import ConfigManager from anki.consts import * @@ -30,14 +31,11 @@ from anki.rsbackend import ( # pylint: disable=unused-import BackendNoteTypeID, ConcatSeparator, DBError, - DupeIn, - Flag, FormatTimeSpanContext, InvalidInput, NoteIDs, Progress, RustBackend, - SearchTerm, pb, ) from anki.sched import Scheduler as V1Scheduler @@ -789,7 +787,7 @@ 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) + dupe_in = SearchTerm.DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text) return SearchTerm(dupe=dupe_in) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 994285484..15fbde7f5 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -47,9 +47,6 @@ TagTreeNode = pb.TagTreeNode NoteType = pb.NoteType DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType -SearchTerm = pb.SearchTerm -Flag = pb.SearchTerm.Flag -DupeIn = pb.SearchTerm.DupeIn NoteIDs = pb.NoteIDs BackendNoteTypeID = pb.NoteTypeID ConcatSeparator = pb.ConcatenateSearchesIn.Separator diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 9d5f434cf..b4cd6f202 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -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, Flag, InvalidInput, SearchTerm, nid_search_term +from anki.collection import Collection, InvalidInput, SearchTerm, nid_search_term from anki.consts import * from anki.lang import without_unicode_isolation from anki.models import NoteType @@ -1041,12 +1041,15 @@ QTableView {{ gridline-color: {grid} }} SearchTerm(card_state=SearchTerm.CardState.BURIED), ), None, - (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)), + (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=SearchTerm.Flag.RED)), + ( + tr(TR.ACTIONS_ORANGE_FLAG), + SearchTerm(flag=SearchTerm.Flag.ORANGE), + ), + (tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=SearchTerm.Flag.GREEN)), + (tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=SearchTerm.Flag.BLUE)), + (tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=SearchTerm.Flag.WITHOUT)), + (tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=SearchTerm.Flag.ANY)), ) ) ) From 1adc9952f4988bbc85f23a60636108a9cf861609 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 11:10:26 +1000 Subject: [PATCH 24/38] simplify Dupe message and ditch helper function Calling code doesn't need to know about the existence of such helpers; it can just rely on code completion to discover the required arguments. --- pylib/anki/collection.py | 7 ------- qt/aqt/editor.py | 8 ++++++-- rslib/backend.proto | 8 ++++---- rslib/src/backend/mod.rs | 4 ++-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 6c3a1434a..d6d2525f3 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -784,13 +784,6 @@ table.review-log {{ {revlog_style} }} ) -def dupe_search_term(mid: int, text: str) -> SearchTerm: - """Helper function for building a DupeIn message.""" - - dupe_in = SearchTerm.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.""" diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 9899390c5..be48c8a24 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -20,8 +20,8 @@ from bs4 import BeautifulSoup import aqt import aqt.sound +from anki.backend_pb2 import SearchTerm 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 @@ -541,7 +541,11 @@ class Editor: def showDupes(self): self.mw.browser_search( - dupe_search_term(self.note.model()["id"], self.note.fields[0]) + SearchTerm( + dupe=SearchTerm.Dupe( + notetype_id=self.note.model()["id"], first_field=self.note.fields[0] + ) + ) ) def fieldsAreBlank(self, previousNote=None): diff --git a/rslib/backend.proto b/rslib/backend.proto index 658e2952c..70d8a9269 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -765,9 +765,9 @@ message BuiltinSearchOrder { } message SearchTerm { - message DupeIn { - NoteTypeID mid = 1; - string text = 2; + message Dupe { + int64 notetype_id = 1; + string first_field = 2; } enum Flag { WITHOUT = 0; @@ -803,7 +803,7 @@ message SearchTerm { string note = 3; uint32 template = 4; NoteIDs nids = 5; - DupeIn dupe = 6; + Dupe dupe = 6; string field_name = 7; Rated rated = 8; uint32 added_in_days = 9; diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index c86f57063..4936e0554 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -309,8 +309,8 @@ impl From for Node<'_> { } 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(), + note_type_id: dupe.notetype_id.into(), + text: dupe.first_field.into(), }), Filter::FieldName(s) => Node::Search(SearchNode::SingleField { field: escape_anki_wildcards(&s).into_owned().into(), From cb6b88da0fbedc328e3f9ce7ed51262a25f9bb3f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 11:23:32 +1000 Subject: [PATCH 25/38] simplify nid/nids searches, and ditch helper function - IdList could be re-used for a cids: search in the future if required. - Embedding the message means it's easy to access from Python as an attribute of SearchTerm. --- pylib/anki/collection.py | 6 ------ qt/aqt/addcards.py | 6 +++--- qt/aqt/browser.py | 14 ++++++++++---- qt/aqt/mediacheck.py | 4 ++-- rslib/backend.proto | 6 +++++- rslib/src/backend/mod.rs | 5 +++-- 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index d6d2525f3..87cb80d43 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -784,11 +784,5 @@ table.review-log {{ {revlog_style} }} ) -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 diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 7de8b3f24..121ff145e 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -7,7 +7,7 @@ import aqt.deckchooser import aqt.editor import aqt.forms import aqt.modelchooser -from anki.collection import nid_search_term +from anki.backend_pb2 import SearchTerm from anki.consts import MODEL_CLOZE from anki.notes import Note from anki.utils import htmlToTextLine, isMac @@ -145,7 +145,7 @@ class AddCards(QDialog): def onHistory(self) -> None: m = QMenu(self) for nid in self.history: - if self.mw.col.findNotes(nid_search_term([nid])): + if self.mw.col.findNotes(SearchTerm(nid=nid)): note = self.mw.col.getNote(nid) fields = note.fields txt = htmlToTextLine(", ".join(fields)) @@ -162,7 +162,7 @@ class AddCards(QDialog): m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) def editHistory(self, nid): - self.mw.browser_search(nid_search_term([nid])) + self.mw.browser_search(SearchTerm(nid=nid)) def addNote(self, note) -> Optional[Note]: note.model()["did"] = self.deckChooser.selectedId() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index b4cd6f202..77bf9def9 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -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, SearchTerm, nid_search_term +from anki.collection import Collection, InvalidInput, SearchTerm from anki.consts import * from anki.lang import without_unicode_isolation from anki.models import NoteType @@ -681,7 +681,7 @@ class Browser(QMainWindow): nid = card and card.nid if nid: self.card = card - search = self.col.build_search_string(nid_search_term([nid])) + search = self.col.build_search_string(SearchTerm(nid=nid)) search = gui_hooks.default_search(search, card) self.form.searchEdit.lineEdit().setText(search) self.onSearchActivated() @@ -1522,7 +1522,9 @@ where id in %s""" tv = self.form.tableView tv.selectionModel().clear() - search = self.col.build_search_string(nid_search_term(nids)) + search = self.col.build_search_string( + SearchTerm(nids=SearchTerm.IdList(ids=nids)) + ) self.search_for(search) tv.selectAll() @@ -1731,7 +1733,11 @@ where id in %s""" t += ( """
  • %s: %s""" % ( - html.escape(self.col.build_search_string(nid_search_term(nids))), + html.escape( + self.col.build_search_string( + SearchTerm(nids=SearchTerm.IdList(ids=nids)) + ) + ), tr(TR.BROWSING_NOTE_COUNT, count=len(nids)), html.escape(val), ) diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 3b128975d..d6129e5ce 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -9,7 +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.backend_pb2 import SearchTerm from anki.rsbackend import TR, Interrupted, ProgressKind, pb from aqt.qt import * from aqt.utils import ( @@ -146,7 +146,7 @@ class MediaChecker: if out is not None: nid, err = out - self.mw.browser_search(nid_search_term([nid])) + self.mw.browser_search(SearchTerm(nid=nid)) showText(err, type="html") else: tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED)) diff --git a/rslib/backend.proto b/rslib/backend.proto index 70d8a9269..238a71de6 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -797,12 +797,15 @@ message SearchTerm { SUSPENDED = 4; BURIED = 5; } + message IdList { + repeated int64 ids = 1; + } oneof filter { string tag = 1; string deck = 2; string note = 3; uint32 template = 4; - NoteIDs nids = 5; + int64 nid = 5; Dupe dupe = 6; string field_name = 7; Rated rated = 8; @@ -812,6 +815,7 @@ message SearchTerm { bool current_deck = 12; Flag flag = 13; CardState card_state = 14; + IdList nids = 15; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 4936e0554..52e0e6be9 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -262,9 +262,9 @@ impl From for NoteID { } } -impl pb::NoteIDs { +impl pb::search_term::IdList { fn into_id_string(self) -> String { - self.nids + self.ids .iter() .map(|i| i.to_string()) .collect::>() @@ -307,6 +307,7 @@ impl From for Node<'_> { Filter::Template(u) => { Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16))) } + Filter::Nid(nid) => Node::Search(SearchNode::NoteIDs(nid.to_string().into())), Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())), Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates { note_type_id: dupe.notetype_id.into(), From 082b807c2e6d2da4c11632f85dc2415fdb9aa7b7 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 11:29:46 +1000 Subject: [PATCH 26/38] shorten search bar hint There's a 'whole collection' link in the top left, so the latter part is probably not required, and shortening this will bring it into line with the sidebar placeholder. Open to suggestions on the wording, just think it's probably best to keep it short. Also remove unused string. --- ftl/core/browsing.ftl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index bd2a372b8..a3053301a 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -83,7 +83,7 @@ browsing-reposition = Reposition... browsing-reposition-new-cards = Reposition New Cards browsing-reschedule = Reschedule browsing-save-current-filter = Save Current Filter... -browsing-search-bar-hint = Type here and press Enter to search. Leave empty to show whole collection. +browsing-search-bar-hint = Search cards/notes browsing-search-in = Search in: browsing-search-within-formatting-slow = Search within formatting (slow) browsing-shift-position-of-existing-cards = Shift position of existing cards @@ -101,7 +101,6 @@ browsing-today = Today browsing-toggle-mark = Toggle Mark browsing-toggle-suspend = Toggle Suspend browsing-treat-input-as-regular-expression = Treat input as regular expression -browsing-type-here-to-search = browsing-whole-collection = Whole Collection browsing-you-must-have-at-least-one = You must have at least one column. browsing-group = From 705012164b2751792c27acb8398a9b915f1a7787 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 30 Jan 2021 11:54:39 +1000 Subject: [PATCH 27/38] move Rating up a level More ergonomic, and will allow reuse if we expose prop:rated in the future. --- qt/aqt/browser.py | 2 +- qt/aqt/customstudy.py | 2 +- rslib/backend.proto | 16 ++++++++-------- rslib/src/backend/mod.rs | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 77bf9def9..9a1a1a34a 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1001,7 +1001,7 @@ QTableView {{ gridline-color: {grid} }} tr(TR.BROWSING_AGAIN_TODAY), SearchTerm( rated=SearchTerm.Rated( - days=1, rating=SearchTerm.Rated.Rating.ANSWER_BUTTON_1 + days=1, rating=SearchTerm.Rating.ANSWER_BUTTON_1 ) ), ), diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index 4610f9910..335689506 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -163,7 +163,7 @@ class CustomStudy(QDialog): search = self.mw.col.build_search_string( SearchTerm( rated=SearchTerm.Rated( - days=spin, rating=SearchTerm.Rated.Rating.ANSWER_BUTTON_1 + days=spin, rating=SearchTerm.Rating.ANSWER_BUTTON_1 ) ) ) diff --git a/rslib/backend.proto b/rslib/backend.proto index 238a71de6..cdb4aa988 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -777,15 +777,15 @@ message SearchTerm { GREEN = 4; BLUE = 5; } + enum Rating { + ANY_ANSWER_BUTTON = 0; + ANSWER_BUTTON_1 = 1; + ANSWER_BUTTON_2 = 2; + ANSWER_BUTTON_3 = 3; + ANSWER_BUTTON_4 = 4; + MANUAL_RESCHEDULE = 5; + } message Rated { - enum Rating { - ANY_ANSWER_BUTTON = 0; - ANSWER_BUTTON_1 = 1; - ANSWER_BUTTON_2 = 2; - ANSWER_BUTTON_3 = 3; - ANSWER_BUTTON_4 = 4; - MANUAL_RESCHEDULE = 5; - } uint32 days = 1; Rating rating = 2; } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 52e0e6be9..2bd5f5a96 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -355,15 +355,15 @@ impl From for BoolSeparator { } } -impl From for EaseKind { - fn from(r: pb::search_term::rated::Rating) -> Self { +impl From for EaseKind { + fn from(r: pb::search_term::Rating) -> Self { match r { - pb::search_term::rated::Rating::AnswerButton1 => EaseKind::AnswerButton(1), - pb::search_term::rated::Rating::AnswerButton2 => EaseKind::AnswerButton(2), - pb::search_term::rated::Rating::AnswerButton3 => EaseKind::AnswerButton(3), - pb::search_term::rated::Rating::AnswerButton4 => EaseKind::AnswerButton(4), - pb::search_term::rated::Rating::AnyAnswerButton => EaseKind::AnyAnswerButton, - pb::search_term::rated::Rating::ManualReschedule => EaseKind::ManualReschedule, + pb::search_term::Rating::AnswerButton1 => EaseKind::AnswerButton(1), + pb::search_term::Rating::AnswerButton2 => EaseKind::AnswerButton(2), + pb::search_term::Rating::AnswerButton3 => EaseKind::AnswerButton(3), + pb::search_term::Rating::AnswerButton4 => EaseKind::AnswerButton(4), + pb::search_term::Rating::AnyAnswerButton => EaseKind::AnyAnswerButton, + pb::search_term::Rating::ManualReschedule => EaseKind::ManualReschedule, } } } From 80a4a855101de7a7b8de9ba021d83144f2a460a6 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 10:37:46 +0100 Subject: [PATCH 28/38] Remove redundant docstring --- pylib/anki/collection.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 87cb80d43..994ef93b1 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -540,8 +540,6 @@ class Collection: 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 From 9c4cc88b470474fe2d82b30ebf36f4132b87347c Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 10:51:31 +0100 Subject: [PATCH 29/38] Auto search and check input before model search - Search for current deck automatically on browser setup. - Hide current deck and current card searches. - Check user search input before passing it on to the model, so invalid searches don't change TableView. --- qt/aqt/browser.py | 66 +++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 9a1a1a34a..c7731d31a 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -190,8 +190,7 @@ class DataModel(QAbstractTableModel): ctx = SearchContext(search=txt, browser=self.browser) gui_hooks.browser_will_search(ctx) if ctx.card_ids is None: - ctx.search = self.browser.normalize_search(ctx.search) - ctx.card_ids = self.col.find_cards(ctx.search, order=ctx.order) + ctx.card_ids = list(self.col.find_cards(ctx.search, order=ctx.order)) gui_hooks.browser_did_search(ctx) self.cards = ctx.card_ids except Exception as err: @@ -610,12 +609,8 @@ class Browser(QMainWindow): self.form.searchEdit.lineEdit().setPlaceholderText( tr(TR.BROWSING_SEARCH_BAR_HINT) ) - self.form.searchEdit.addItems( - [self.col.build_search_string(SearchTerm(current_deck=True))] - + self.mw.pm.profile["searchHistory"] - ) - self._onRowChanged(None, None) - self.form.searchEdit.lineEdit().selectAll() + self.form.searchEdit.addItems(self.mw.pm.profile["searchHistory"]) + self.search_for(self.col.build_search_string(SearchTerm(current_deck=True)), "") self.form.searchEdit.setFocus() # search triggered by user @@ -624,32 +619,32 @@ class Browser(QMainWindow): def _onSearchActivated(self): text = self.form.searchEdit.lineEdit().text() - if self.search_for(text): - # Only save successful searches. + try: + normed = self.col.build_search_string(text) + except InvalidInput as err: + show_invalid_search_error(err) + else: + self.search_for(normed) self.update_history() - def search_for(self, search: str) -> bool: + def search_for(self, search: str, prompt: Optional[str] = None) -> bool: # keep track of search string so that we reuse identical search when # refreshing, rather than whatever is currently in the search field self._lastSearchTxt = search - self.form.searchEdit.lineEdit().setText(search) - return self.search() + prompt = search if prompt == None else prompt + self.form.searchEdit.lineEdit().setText(prompt) + self.search() - def search(self) -> bool: - """Search triggered programmatically. Caller must have saved note first. - Return bool indicating success. - """ + def search(self): + """Search triggered programmatically. Caller must have saved note first.""" try: self.model.search(self._lastSearchTxt) - except InvalidInput as err: + except Exception as err: show_invalid_search_error(err) - return False - else: - if not self.model.cards: - # no row change will fire - self._onRowChanged(None, None) - return True + if not self.model.cards: + # no row change will fire + self._onRowChanged(None, None) def update_history(self) -> None: sh = self.mw.pm.profile["searchHistory"] @@ -661,12 +656,6 @@ class Browser(QMainWindow): self.form.searchEdit.addItems(sh) self.mw.pm.profile["searchHistory"] = sh - def normalize_search(self, search: str) -> str: - normed = self.col.build_search_string(search) - self._lastSearchTxt = normed - self.form.searchEdit.lineEdit().setText(normed) - return normed - def updateTitle(self): selected = len(self.form.tableView.selectionModel().selectedRows()) cur = len(self.model.cards) @@ -680,13 +669,15 @@ class Browser(QMainWindow): def show_single_card(self, card: Optional[Card]) -> None: nid = card and card.nid if nid: - self.card = card - search = self.col.build_search_string(SearchTerm(nid=nid)) - search = gui_hooks.default_search(search, card) - self.form.searchEdit.lineEdit().setText(search) - self.onSearchActivated() - self.form.tableView.clearSelection() - self.focusCid(card.id) + + def on_show_single_card(): + self.card = card + search = self.col.build_search_string(SearchTerm(nid=nid)) + search = gui_hooks.default_search(search, card) + self.search_for(search, "") + self.focusCid(card.id) + + self.editor.saveNow(on_show_single_card) def onReset(self): self.sidebar.refresh() @@ -1836,6 +1827,7 @@ where id in %s""" row = self.model.cards.index(cid) except: return + self.form.tableView.clearSelection() self.form.tableView.selectRow(row) From 63e8b4462993089f1067197f72bb55bec4ed14ef Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 11:05:48 +0100 Subject: [PATCH 30/38] Update docstrings for browser search --- qt/aqt/browser.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index c7731d31a..40fd7695a 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -628,8 +628,11 @@ class Browser(QMainWindow): self.update_history() def search_for(self, search: str, prompt: Optional[str] = None) -> bool: - # keep track of search string so that we reuse identical search when - # refreshing, rather than whatever is currently in the search field + """Keep track of search string so that we reuse identical search when + refreshing, rather than whatever is currently in the search field. + Optionally set the search bar to a different text than the actual search. + """ + self._lastSearchTxt = search prompt = search if prompt == None else prompt self.form.searchEdit.lineEdit().setText(prompt) @@ -667,6 +670,8 @@ class Browser(QMainWindow): return selected def show_single_card(self, card: Optional[Card]) -> None: + """Try to search for the according note and select the given card.""" + nid = card and card.nid if nid: From 26e52358469e60104954791f24b78d6f8a870b6b Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 11:24:33 +0100 Subject: [PATCH 31/38] Fix type annotations in browser search --- qt/aqt/browser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 40fd7695a..7f32d2fd8 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -627,7 +627,7 @@ class Browser(QMainWindow): self.search_for(normed) self.update_history() - def search_for(self, search: str, prompt: Optional[str] = None) -> bool: + def search_for(self, search: str, prompt: Optional[str] = None): """Keep track of search string so that we reuse identical search when refreshing, rather than whatever is currently in the search field. Optionally set the search bar to a different text than the actual search. @@ -649,7 +649,7 @@ class Browser(QMainWindow): # no row change will fire self._onRowChanged(None, None) - def update_history(self) -> None: + def update_history(self): sh = self.mw.pm.profile["searchHistory"] if self._lastSearchTxt in sh: sh.remove(self._lastSearchTxt) @@ -659,7 +659,7 @@ class Browser(QMainWindow): self.form.searchEdit.addItems(sh) self.mw.pm.profile["searchHistory"] = sh - def updateTitle(self): + def updateTitle(self) -> int: selected = len(self.form.tableView.selectionModel().selectedRows()) cur = len(self.model.cards) self.setWindowTitle( @@ -669,10 +669,10 @@ class Browser(QMainWindow): ) return selected - def show_single_card(self, card: Optional[Card]) -> None: + def show_single_card(self, card: Optional[Card]): """Try to search for the according note and select the given card.""" - nid = card and card.nid + nid: Optional[int] = card and card.nid or 0 if nid: def on_show_single_card(): From 5425b00d2b4d3d0e825ec1552d9de620df4bd3a3 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 12:51:24 +0100 Subject: [PATCH 32/38] Import SearchTerm from collection in aqt --- qt/aqt/addcards.py | 2 +- qt/aqt/editor.py | 2 +- qt/aqt/mediacheck.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 121ff145e..5a8d07607 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -7,7 +7,7 @@ import aqt.deckchooser import aqt.editor import aqt.forms import aqt.modelchooser -from anki.backend_pb2 import SearchTerm +from anki.collection import SearchTerm from anki.consts import MODEL_CLOZE from anki.notes import Note from anki.utils import htmlToTextLine, isMac diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index be48c8a24..15bf68d93 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -20,8 +20,8 @@ from bs4 import BeautifulSoup import aqt import aqt.sound -from anki.backend_pb2 import SearchTerm from anki.cards import Card +from anki.collection import SearchTerm from anki.hooks import runFilter from anki.httpclient import HttpClient from anki.notes import Note diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index d6129e5ce..50bf66f0e 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -9,7 +9,7 @@ from concurrent.futures import Future from typing import Iterable, List, Optional, Sequence, TypeVar import aqt -from anki.backend_pb2 import SearchTerm +from anki.collection import SearchTerm from anki.rsbackend import TR, Interrupted, ProgressKind, pb from aqt.qt import * from aqt.utils import ( From 4745b55d27b4fb016ac2c558aed3c9d76be16d7a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 12:59:18 +0100 Subject: [PATCH 33/38] Revert addition of pb.NoteIDs --- pylib/anki/collection.py | 1 - pylib/anki/rsbackend.py | 1 - rslib/backend.proto | 4 ---- 3 files changed, 6 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 994ef93b1..55c3f5cf9 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -33,7 +33,6 @@ from anki.rsbackend import ( # pylint: disable=unused-import DBError, FormatTimeSpanContext, InvalidInput, - NoteIDs, Progress, RustBackend, pb, diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 15fbde7f5..5ccaa2b1a 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -47,7 +47,6 @@ TagTreeNode = pb.TagTreeNode NoteType = pb.NoteType DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType -NoteIDs = pb.NoteIDs BackendNoteTypeID = pb.NoteTypeID ConcatSeparator = pb.ConcatenateSearchesIn.Separator SyncAuth = pb.SyncAuth diff --git a/rslib/backend.proto b/rslib/backend.proto index cdb4aa988..d3d023f7a 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -50,10 +50,6 @@ message NoteID { int64 nid = 1; } -message NoteIDs { - repeated int64 nids = 1; -} - message CardID { int64 cid = 1; } From 9e9d8b9c7acab31e8440396ac00207a14b8449b0 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 13:15:46 +0100 Subject: [PATCH 34/38] Build list in focusCid and specify exception model.cards may be a protobuf sequence but focusCid needs list's index method, so convert to list, but only if needed. --- qt/aqt/browser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 7f32d2fd8..414336539 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -190,7 +190,7 @@ class DataModel(QAbstractTableModel): ctx = SearchContext(search=txt, browser=self.browser) gui_hooks.browser_will_search(ctx) if ctx.card_ids is None: - ctx.card_ids = list(self.col.find_cards(ctx.search, order=ctx.order)) + ctx.card_ids = self.col.find_cards(ctx.search, order=ctx.order) gui_hooks.browser_did_search(ctx) self.cards = ctx.card_ids except Exception as err: @@ -1829,8 +1829,8 @@ where id in %s""" def focusCid(self, cid): try: - row = self.model.cards.index(cid) - except: + row = list(self.model.cards).index(cid) + except ValueError: return self.form.tableView.clearSelection() self.form.tableView.selectRow(row) From 375794893f2dbe9942dcfd60f74a5af82b5738c1 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 17:39:21 +0100 Subject: [PATCH 35/38] Replace leftover _named_filter with _filter_func --- 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 414336539..32c556e9f 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -980,7 +980,7 @@ QTableView {{ gridline-color: {grid} }} ml.addSeparator() else: label, filter_name = row - ml.addItem(label, self.sidebar._named_filter(filter_name)) + ml.addItem(label, self.sidebar._filter_func(filter_name)) return ml def _todayFilters(self): From b0890b0e47da48510e4041d784ff9c048ef2ba73 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 17:56:29 +0100 Subject: [PATCH 36/38] Manually namespace enum variants in SearchTerm In protobuf "...enum values use C++ scoping rules, meaning that enum values are siblings of their type, not children of it. Therefore, [an enum variant] must be unique within [a message], not just within [the enum.]" So we must prefix enum variants with their enum's name, but can also call them directly from the message namespace. The protobuf crate is smart, though, and strips the prefixes. (Simultaneously change some SearchTerm variant names.) --- qt/aqt/browser.py | 26 +++++++++++++------------- qt/aqt/customstudy.py | 10 +++++----- qt/aqt/dyndeckconf.py | 4 ++-- rslib/backend.proto | 36 ++++++++++++++++++------------------ rslib/src/backend/mod.rs | 14 +++++++------- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 32c556e9f..52a4f2246 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -997,7 +997,7 @@ QTableView {{ gridline-color: {grid} }} tr(TR.BROWSING_AGAIN_TODAY), SearchTerm( rated=SearchTerm.Rated( - days=1, rating=SearchTerm.Rating.ANSWER_BUTTON_1 + days=1, rating=SearchTerm.RATING_AGAIN ) ), ), @@ -1013,39 +1013,39 @@ QTableView {{ gridline-color: {grid} }} ( ( tr(TR.ACTIONS_NEW), - SearchTerm(card_state=SearchTerm.CardState.NEW), + SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), ), ( tr(TR.SCHEDULING_LEARNING), - SearchTerm(card_state=SearchTerm.CardState.LEARN), + SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN), ), ( tr(TR.SCHEDULING_REVIEW), - SearchTerm(card_state=SearchTerm.CardState.REVIEW), + SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW), ), ( tr(TR.FILTERING_IS_DUE), - SearchTerm(card_state=SearchTerm.CardState.DUE), + SearchTerm(card_state=SearchTerm.CARD_STATE_DUE), ), None, ( tr(TR.BROWSING_SUSPENDED), - SearchTerm(card_state=SearchTerm.CardState.SUSPENDED), + SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED), ), ( tr(TR.BROWSING_BURIED), - SearchTerm(card_state=SearchTerm.CardState.BURIED), + SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED), ), None, - (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=SearchTerm.Flag.RED)), + (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=SearchTerm.FLAG_RED)), ( tr(TR.ACTIONS_ORANGE_FLAG), - SearchTerm(flag=SearchTerm.Flag.ORANGE), + SearchTerm(flag=SearchTerm.FLAG_ORANGE), ), - (tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=SearchTerm.Flag.GREEN)), - (tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=SearchTerm.Flag.BLUE)), - (tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=SearchTerm.Flag.WITHOUT)), - (tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=SearchTerm.Flag.ANY)), + (tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=SearchTerm.FLAG_GREEN)), + (tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=SearchTerm.FLAG_BLUE)), + (tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=SearchTerm.FLAG_NONE)), + (tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=SearchTerm.FLAG_ANY)), ) ) ) diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index 335689506..c752f24fc 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -163,7 +163,7 @@ class CustomStudy(QDialog): search = self.mw.col.build_search_string( SearchTerm( rated=SearchTerm.Rated( - days=spin, rating=SearchTerm.Rating.ANSWER_BUTTON_1 + days=spin, rating=SearchTerm.RATING_AGAIN ) ) ) @@ -175,7 +175,7 @@ class CustomStudy(QDialog): dyn["resched"] = True elif i == RADIO_PREVIEW: search = self.mw.col.build_search_string( - SearchTerm(card_state=SearchTerm.CardState.NEW), + SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), SearchTerm(added_in_days=spin), ) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST] @@ -184,19 +184,19 @@ class CustomStudy(QDialog): type = f.cardType.currentRow() if type == TYPE_NEW: terms = self.mw.col.build_search_string( - SearchTerm(card_state=SearchTerm.CardState.NEW) + SearchTerm(card_state=SearchTerm.CARD_STATE_NEW) ) ord = DYN_ADDED dyn["resched"] = True elif type == TYPE_DUE: terms = self.mw.col.build_search_string( - SearchTerm(card_state=SearchTerm.CardState.DUE) + SearchTerm(card_state=SearchTerm.CARD_STATE_DUE) ) ord = DYN_DUE dyn["resched"] = True elif type == TYPE_REVIEW: terms = self.mw.col.build_search_string( - SearchTerm(card_state=SearchTerm.CardState.NEW), negate=True + SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), negate=True ) ord = DYN_RANDOM dyn["resched"] = True diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 1ea32670f..c9d0d6338 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -48,11 +48,11 @@ class DeckConf(QDialog): self.loadConf() if search: search = self.mw.col.build_search_string( - search, SearchTerm(card_state=SearchTerm.CardState.DUE) + search, SearchTerm(card_state=SearchTerm.CARD_STATE_DUE) ) self.form.search.setText(search) search_2 = self.mw.col.build_search_string( - search, SearchTerm(card_state=SearchTerm.CardState.NEW) + search, SearchTerm(card_state=SearchTerm.CARD_STATE_NEW) ) self.form.search_2.setText(search_2) self.form.search.selectAll() diff --git a/rslib/backend.proto b/rslib/backend.proto index d3d023f7a..19d1bab13 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -766,32 +766,32 @@ message SearchTerm { string first_field = 2; } enum Flag { - WITHOUT = 0; - ANY = 1; - RED = 2; - ORANGE = 3; - GREEN = 4; - BLUE = 5; + FLAG_NONE = 0; + FLAG_ANY = 1; + FLAG_RED = 2; + FLAG_ORANGE = 3; + FLAG_GREEN = 4; + FLAG_BLUE = 5; } enum Rating { - ANY_ANSWER_BUTTON = 0; - ANSWER_BUTTON_1 = 1; - ANSWER_BUTTON_2 = 2; - ANSWER_BUTTON_3 = 3; - ANSWER_BUTTON_4 = 4; - MANUAL_RESCHEDULE = 5; + RATING_ANY = 0; + RATING_AGAIN = 1; + RATING_HARD = 2; + RATING_GOOD = 3; + RATING_EASY = 4; + RATING_BY_RESCHEDULE = 5; } message Rated { uint32 days = 1; Rating rating = 2; } enum CardState { - NEW = 0; - LEARN = 1; - REVIEW = 2; - DUE = 3; - SUSPENDED = 4; - BURIED = 5; + CARD_STATE_NEW = 0; + CARD_STATE_LEARN = 1; + CARD_STATE_REVIEW = 2; + CARD_STATE_DUE = 3; + CARD_STATE_SUSPENDED = 4; + CARD_STATE_BURIED = 5; } message IdList { repeated int64 ids = 1; diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 2bd5f5a96..62bf0f341 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -335,7 +335,7 @@ impl From for Node<'_> { .into(), )), Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) { - Flag::Without => Node::Search(SearchNode::Flag(0)), + Flag::None => 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)), @@ -358,12 +358,12 @@ impl From for BoolSeparator { impl From for EaseKind { fn from(r: pb::search_term::Rating) -> Self { match r { - pb::search_term::Rating::AnswerButton1 => EaseKind::AnswerButton(1), - pb::search_term::Rating::AnswerButton2 => EaseKind::AnswerButton(2), - pb::search_term::Rating::AnswerButton3 => EaseKind::AnswerButton(3), - pb::search_term::Rating::AnswerButton4 => EaseKind::AnswerButton(4), - pb::search_term::Rating::AnyAnswerButton => EaseKind::AnyAnswerButton, - pb::search_term::Rating::ManualReschedule => EaseKind::ManualReschedule, + pb::search_term::Rating::Again => EaseKind::AnswerButton(1), + pb::search_term::Rating::Hard => EaseKind::AnswerButton(2), + pb::search_term::Rating::Good => EaseKind::AnswerButton(3), + pb::search_term::Rating::Easy => EaseKind::AnswerButton(4), + pb::search_term::Rating::Any => EaseKind::AnyAnswerButton, + pb::search_term::Rating::ByReschedule => EaseKind::ManualReschedule, } } } From 692aa16f6beae7d0c13ef913bf849342431c557d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 19:03:50 +0100 Subject: [PATCH 37/38] Rename EaseKind to RatingKind --- rslib/src/backend/mod.rs | 17 +++++++++-------- rslib/src/search/mod.rs | 2 +- rslib/src/search/parser.rs | 16 ++++++++-------- rslib/src/search/sqlwriter.rs | 10 +++++----- rslib/src/search/writer.rs | 12 ++++++------ 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 62bf0f341..099aa1f87 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -36,7 +36,8 @@ use crate::{ sched::timespan::{answer_button_time, time_span}, search::{ concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes, - BoolSeparator, EaseKind, Node, PropertyKind, SearchNode, SortMode, StateKind, TemplateKind, + BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind, + TemplateKind, }, stats::studied_today, sync::{ @@ -355,15 +356,15 @@ impl From for BoolSeparator { } } -impl From for EaseKind { +impl From for RatingKind { fn from(r: pb::search_term::Rating) -> Self { match r { - pb::search_term::Rating::Again => EaseKind::AnswerButton(1), - pb::search_term::Rating::Hard => EaseKind::AnswerButton(2), - pb::search_term::Rating::Good => EaseKind::AnswerButton(3), - pb::search_term::Rating::Easy => EaseKind::AnswerButton(4), - pb::search_term::Rating::Any => EaseKind::AnyAnswerButton, - pb::search_term::Rating::ByReschedule => EaseKind::ManualReschedule, + pb::search_term::Rating::Again => RatingKind::AnswerButton(1), + pb::search_term::Rating::Hard => RatingKind::AnswerButton(2), + pb::search_term::Rating::Good => RatingKind::AnswerButton(3), + pb::search_term::Rating::Easy => RatingKind::AnswerButton(4), + pb::search_term::Rating::Any => RatingKind::AnyAnswerButton, + pb::search_term::Rating::ByReschedule => RatingKind::ManualReschedule, } } } diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index ef107a94a..ba396242c 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -5,7 +5,7 @@ mod sqlwriter; mod writer; pub use cards::SortMode; -pub use parser::{EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind}; +pub use parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind}; pub use writer::{ 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 903540a4f..ee7e2909f 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -58,7 +58,7 @@ pub enum SearchNode<'a> { NoteType(Cow<'a, str>), Rated { days: u32, - ease: EaseKind, + ease: RatingKind, }, Tag(Cow<'a, str>), Duplicates { @@ -87,7 +87,7 @@ pub enum PropertyKind { Lapses(u32), Ease(f32), Position(u32), - Rated(i32, EaseKind), + Rated(i32, RatingKind), } #[derive(Debug, PartialEq, Clone)] @@ -109,7 +109,7 @@ pub enum TemplateKind<'a> { } #[derive(Debug, PartialEq, Clone)] -pub enum EaseKind { +pub enum RatingKind { AnswerButton(u8), AnyAnswerButton, ManualReschedule, @@ -353,7 +353,7 @@ fn parse_flag(s: &str) -> ParseResult { fn parse_resched(s: &str) -> ParseResult { parse_u32(s, "resched:").map(|days| SearchNode::Rated { days, - ease: EaseKind::ManualReschedule, + ease: RatingKind::ManualReschedule, }) } @@ -392,7 +392,7 @@ fn parse_prop(prop_clause: &str) -> ParseResult { "rated" => parse_prop_rated(num, prop_clause)?, "resched" => PropertyKind::Rated( parse_negative_i32(num, prop_clause)?, - EaseKind::ManualReschedule, + RatingKind::ManualReschedule, ), "ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?), "reps" => PropertyKind::Reps(parse_u32(num, prop_clause)?), @@ -470,9 +470,9 @@ fn parse_i64<'a>(num: &str, context: &'a str) -> ParseResult<'a, i64> { }) } -fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, EaseKind> { +fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, RatingKind> { Ok(if let Some(num) = num { - EaseKind::AnswerButton( + RatingKind::AnswerButton( num.parse() .map_err(|_| ()) .and_then(|n| if matches!(n, 1..=4) { Ok(n) } else { Err(()) }) @@ -487,7 +487,7 @@ fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<' })?, ) } else { - EaseKind::AnyAnswerButton + RatingKind::AnyAnswerButton }) } diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 6921794c6..a3e009d5e 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::parser::{EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind}; +use super::parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind}; use crate::{ card::{CardQueue, CardType}, collection::Collection, @@ -211,7 +211,7 @@ impl SqlWriter<'_> { Ok(()) } - fn write_rated(&mut self, op: &str, days: i64, ease: &EaseKind) -> Result<()> { + fn write_rated(&mut self, op: &str, days: i64, ease: &RatingKind) -> Result<()> { let today_cutoff = self.col.timing_today()?.next_day_at; let target_cutoff_ms = (today_cutoff + 86_400 * days) * 1_000; let day_before_cutoff_ms = (today_cutoff + 86_400 * (days - 1)) * 1_000; @@ -240,9 +240,9 @@ impl SqlWriter<'_> { .unwrap(); match ease { - EaseKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u), - EaseKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"), - EaseKind::ManualReschedule => write!(self.sql, " and ease = 0)"), + RatingKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u), + RatingKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"), + RatingKind::ManualReschedule => write!(self.sql, " and ease = 0)"), } .unwrap(); diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 1715c11e3..ac4490e9e 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -5,7 +5,7 @@ use crate::{ decks::DeckID as DeckIDType, err::Result, notetype::NoteTypeID as NoteTypeIDType, - search::parser::{parse, EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind}, + search::parser::{parse, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind}, }; use itertools::Itertools; use std::mem; @@ -154,8 +154,8 @@ fn write_template(template: &TemplateKind) -> String { } } -fn write_rated(days: &u32, ease: &EaseKind) -> String { - use EaseKind::*; +fn write_rated(days: &u32, ease: &RatingKind) -> String { + use RatingKind::*; match ease { AnswerButton(n) => format!("\"rated:{}:{}\"", days, n), AnyAnswerButton => format!("\"rated:{}\"", days), @@ -196,9 +196,9 @@ fn write_property(operator: &str, kind: &PropertyKind) -> String { Ease(f) => format!("\"prop:ease{}{}\"", operator, f), Position(u) => format!("\"prop:pos{}{}\"", operator, u), Rated(u, ease) => match ease { - EaseKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val), - EaseKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u), - EaseKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u), + RatingKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val), + RatingKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u), + RatingKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u), }, } } From 9bfe8e3840f04195375946dbe055b14c60c5f42d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 30 Jan 2021 19:23:40 +0100 Subject: [PATCH 38/38] Remove search button and reword search bar hint --- ftl/core/browsing.ftl | 2 +- qt/aqt/browser.py | 1 - qt/aqt/customstudy.py | 4 +--- qt/aqt/forms/browser.ui | 15 ++++----------- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index a3053301a..1db3be976 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -83,7 +83,7 @@ browsing-reposition = Reposition... browsing-reposition-new-cards = Reposition New Cards browsing-reschedule = Reschedule browsing-save-current-filter = Save Current Filter... -browsing-search-bar-hint = Search cards/notes +browsing-search-bar-hint = Type here and press Enter to search for cards and notes. browsing-search-in = Search in: browsing-search-within-formatting-slow = Search within formatting (slow) browsing-shift-position-of-existing-cards = Shift position of existing cards diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 52a4f2246..8bb54cf72 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -603,7 +603,6 @@ class Browser(QMainWindow): ###################################################################### def setupSearch(self): - qconnect(self.form.searchButton.clicked, self.onSearchActivated) qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) self.form.searchEdit.lineEdit().setPlaceholderText( diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index c752f24fc..baa022243 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -162,9 +162,7 @@ class CustomStudy(QDialog): if i == RADIO_FORGOT: search = self.mw.col.build_search_string( SearchTerm( - rated=SearchTerm.Rated( - days=spin, rating=SearchTerm.RATING_AGAIN - ) + rated=SearchTerm.Rated(days=spin, rating=SearchTerm.RATING_AGAIN) ) ) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM] diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui index 87335617a..ba6a48980 100644 --- a/qt/aqt/forms/browser.ui +++ b/qt/aqt/forms/browser.ui @@ -107,13 +107,6 @@ - - - - ACTIONS_SEARCH - - - @@ -158,12 +151,12 @@ false - - 20 - false + + 20 + true @@ -223,7 +216,7 @@ 0 0 750 - 22 + 21