diff --git a/ftl/core/decks.ftl b/ftl/core/decks.ftl index 222a993c2..22175d3e7 100644 --- a/ftl/core/decks.ftl +++ b/ftl/core/decks.ftl @@ -31,6 +31,7 @@ decks-reschedule-cards-based-on-my-answers = Reschedule cards based on my answer decks-study = Study decks-study-deck = Study Deck decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it? +decks-unmovable-cards = Some cards may be excluded despite matching the search. decks-it-has-card = { $count -> [one] It has { $count } card. diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 8c88df964..d1c4e8e2e 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -1,6 +1,6 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Callable, List, Optional +from typing import Callable, List, Optional, Tuple import aqt from anki.collection import SearchNode @@ -40,12 +40,13 @@ class DeckConf(QDialog): QDialog.__init__(self, mw) self.mw = mw + self.col = self.mw.col self.did: Optional[int] = None self.form = aqt.forms.dyndconf.Ui_Dialog() self.form.setupUi(self) self.mw.checkpoint(tr(TR.ACTIONS_OPTIONS)) self.initialSetup() - self.old_deck = self.mw.col.decks.current() + self.old_deck = self.col.decks.current() if deck and deck["dyn"]: # modify existing dyn deck @@ -69,10 +70,14 @@ class DeckConf(QDialog): self.set_custom_searches(search, search_2) qconnect(self.form.search_button.clicked, self.on_search_button) qconnect(self.form.search_button_2.clicked, self.on_search_button_2) - color = theme_manager.color(colors.LINK) + qconnect(self.form.hint_button.clicked, self.on_hint_button) + blue = theme_manager.color(colors.LINK) + grey = theme_manager.color(colors.DISABLED) self.setStyleSheet( - f"""QPushButton[flat=true] {{ text-align: left; color: {color}; padding: 0; border: 0 }} - QPushButton[flat=true]:hover {{ text-decoration: underline }}""" + f"""QPushButton[label] {{ padding: 0; border: 0 }} + QPushButton[label]:hover {{ text-decoration: underline }} + QPushButton[label="search"] {{ text-align: left; color: {blue} }} + QPushButton[label="hint"] {{ text-align: right; color: {grey} }}""" ) disable_help_button(self) self.setWindowModality(Qt.WindowModal) @@ -83,7 +88,7 @@ class DeckConf(QDialog): without_unicode_isolation(tr(TR.ACTIONS_OPTIONS_FOR, val=self.deck["name"])) ) self.form.buttonBox.button(QDialogButtonBox.Ok).setText(label) - if self.mw.col.schedVer() == 1: + if self.col.schedVer() == 1: self.form.secondFilter.setVisible(False) restoreGeom(self, "dyndeckconf") @@ -100,23 +105,23 @@ class DeckConf(QDialog): def new_dyn_deck(self) -> None: suffix: int = 1 - while self.mw.col.decks.id_for_name( + while self.col.decks.id_for_name( without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=suffix)) ): suffix += 1 name: str = without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=suffix)) - self.did = self.mw.col.decks.new_filtered(name) - self.deck = self.mw.col.decks.current() + self.did = self.col.decks.new_filtered(name) + self.deck = self.col.decks.current() def set_default_searches(self, deck_name: str) -> None: self.form.search.setText( - self.mw.col.build_search_string( + self.col.build_search_string( SearchNode(deck=deck_name), SearchNode(card_state=SearchNode.CARD_STATE_DUE), ) ) self.form.search_2.setText( - self.mw.col.build_search_string( + self.col.build_search_string( SearchNode(deck=deck_name), SearchNode(card_state=SearchNode.CARD_STATE_NEW), ) @@ -152,7 +157,7 @@ class DeckConf(QDialog): def _on_search_button(self, line: QLineEdit) -> None: try: - search = self.mw.col.build_search_string(line.text()) + search = self.col.build_search_string(line.text()) except InvalidInput as err: line.setFocus() line.selectAll() @@ -160,9 +165,61 @@ class DeckConf(QDialog): else: aqt.dialogs.open("Browser", self.mw, search=(search,)) + def on_hint_button(self) -> None: + """Open the browser to show cards that match the typed-in filters but cannot be included + due to internal limitations. + """ + manual_filters = (self.form.search.text(), *self._second_filter()) + implicit_filters = ( + SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED), + SearchNode(card_state=SearchNode.CARD_STATE_BURIED), + *self._learning_search_node(), + *self._filtered_search_node(), + ) + manual_filter = self.col.group_searches(*manual_filters, joiner="OR") + implicit_filter = self.col.group_searches(*implicit_filters, joiner="OR") + try: + search = self.col.build_search_string(manual_filter, implicit_filter) + except InvalidInput as err: + show_invalid_search_error(err) + else: + aqt.dialogs.open("Browser", self.mw, search=(search,)) + + def _second_filter(self) -> Tuple[str, ...]: + if self.form.secondFilter.isChecked(): + return (self.form.search_2.text(),) + return () + + def _learning_search_node(self) -> Tuple[SearchNode, ...]: + """Return a search node that matches learning cards if the old scheduler is enabled. + If it's a rebuild, exclude cards from this filtered deck as those will be reset. + """ + if self.col.schedVer() == 1: + if self.did is None: + return ( + self.col.group_searches( + SearchNode(card_state=SearchNode.CARD_STATE_LEARN), + SearchNode(negated=SearchNode(deck=self.deck["name"])), + ), + ) + return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),) + return () + + def _filtered_search_node(self) -> Tuple[SearchNode]: + """Return a search node that matches cards in filtered decks, if applicable excluding those + in the deck being rebuild.""" + if self.did is None: + return ( + self.col.group_searches( + SearchNode(deck="filtered"), + SearchNode(negated=SearchNode(deck=self.deck["name"])), + ), + ) + return (SearchNode(deck="filtered"),) + def _onReschedToggled(self, _state: int) -> None: self.form.previewDelayWidget.setVisible( - not self.form.resched.isChecked() and self.mw.col.schedVer() > 1 + not self.form.resched.isChecked() and self.col.schedVer() > 1 ) def loadConf(self, deck: Optional[Deck] = None) -> None: @@ -175,7 +232,7 @@ class DeckConf(QDialog): search, limit, order = d["terms"][0] f.search.setText(search) - if self.mw.col.schedVer() == 1: + if self.col.schedVer() == 1: if d["delays"]: f.steps.setText(self.listToUser(d["delays"])) f.stepsOn.setChecked(True) @@ -205,35 +262,36 @@ class DeckConf(QDialog): d = self.deck if f.name.text() and d["name"] != f.name.text(): - self.mw.col.decks.rename(d, f.name.text()) + self.col.decks.rename(d, f.name.text()) gui_hooks.sidebar_should_refresh_decks() d["resched"] = f.resched.isChecked() d["delays"] = None - if self.mw.col.schedVer() == 1 and f.stepsOn.isChecked(): + if self.col.schedVer() == 1 and f.stepsOn.isChecked(): steps = self.userToList(f.steps) if steps: d["delays"] = steps else: d["delays"] = None - search = self.mw.col.build_search_string(f.search.text()) + search = self.col.build_search_string(f.search.text()) terms = [[search, f.limit.value(), f.order.currentIndex()]] if f.secondFilter.isChecked(): - search_2 = self.mw.col.build_search_string(f.search_2.text()) + search_2 = self.col.build_search_string(f.search_2.text()) terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()]) d["terms"] = terms d["previewDelay"] = f.previewDelay.value() - self.mw.col.decks.save(d) + self.col.decks.save(d) def reject(self) -> None: if self.did: - self.mw.col.decks.rem(self.did) - self.mw.col.decks.select(self.old_deck["id"]) + self.col.decks.rem(self.did) + self.col.decks.select(self.old_deck["id"]) + self.mw.reset() saveGeom(self, "dyndeckconf") QDialog.reject(self) aqt.dialogs.markClosed("DynDeckConfDialog") @@ -246,7 +304,7 @@ class DeckConf(QDialog): except DeckRenameError as err: showWarning(err.description) else: - if not self.mw.col.sched.rebuild_filtered_deck(self.deck["id"]): + if not self.col.sched.rebuild_filtered_deck(self.deck["id"]): if askUser(tr(TR.DECKS_THE_PROVIDED_SEARCH_DID_NOT_MATCH)): return saveGeom(self, "dyndeckconf") diff --git a/qt/aqt/forms/dyndconf.ui b/qt/aqt/forms/dyndconf.ui index 6e9eaa441..3e91c0a96 100644 --- a/qt/aqt/forms/dyndconf.ui +++ b/qt/aqt/forms/dyndconf.ui @@ -79,6 +79,9 @@ true + + search + @@ -143,6 +146,9 @@ true + + search + @@ -250,6 +256,28 @@ + + + + Qt::NoFocus + + + SEARCH_VIEW_IN_BROWSER + + + DECKS_UNMOVABLE_CARDS + + + false + + + true + + + hint + + +