diff --git a/plugin/__init__.py b/plugin/__init__.py index 3f81a74..6426f13 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1629,7 +1629,7 @@ class AnkiConnect: # when run inside Anki, `__name__` would be either numeric, # or, if installed via `link.sh`, `AnkiConnectDev` if __name__ != "plugin": - Edit.register_with_dialog_manager() + Edit.register_with_anki() ac = AnkiConnect() ac.initLogging() diff --git a/plugin/edit.py b/plugin/edit.py index e0a2bf6..1e063af 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -2,7 +2,6 @@ import aqt import aqt.editor from aqt import gui_hooks from aqt.qt import QDialog, Qt, QKeySequence, QShortcut -from aqt.browser.previewer import MultiCardPreviewer from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip from anki.errors import NotFoundError from anki.consts import QUEUE_TYPE_SUSPENDED @@ -11,13 +10,13 @@ from anki.utils import ids2str # Edit dialog. Like Edit Current, but: # * has a Preview button to preview the cards for the note -# * has a Browse button to open the browser with these cards # * has Previous/Back buttons to navigate the history of the dialog +# * has a Browse button to open the history in the Browser # * has no bar with the Close button # # To register in Anki's dialog system: # > from .edit import Edit -# > Edit.register_with_dialog_manager() +# > Edit.register_with_anki() # # To (re)open (note_id is an integer): # > Edit.open_dialog_and_show_note_with_id(note_id) @@ -87,7 +86,7 @@ class DecentPreviewer(aqt.browser.previewer.MultiCardPreviewer): self.adapter.can_select_next_card() def _render_scheduled(self): - super()._render_scheduled() + super()._render_scheduled() # noqa self._updateButtons() def showing_answer_and_can_show_question(self): @@ -157,6 +156,21 @@ class History: history = History() +# see method `find_cards` of `collection.py` +def trigger_search_for_dialog_history_notes(search_context, use_history_order): + search_context.search = " or ".join( + f"nid:{note_id}" for note_id in history.note_ids + ) + + if use_history_order: + search_context.order = f"""case c.nid { + " ".join( + f"when {note_id} then {n}" + for (n, note_id) in enumerate(reversed(history.note_ids)) + ) + } end asc""" + + ############################################################################## @@ -164,6 +178,7 @@ history = History() class Edit(aqt.editcurrent.EditCurrent): dialog_geometry_tag = DOMAIN_PREFIX + "edit" dialog_registry_tag = DOMAIN_PREFIX + "Edit" + dialog_search_tag = DOMAIN_PREFIX + "edit.history" # depending on whether the dialog already exists, # upon a request to open the dialog via `aqt.dialogs.open()`, @@ -245,13 +260,26 @@ class Edit(aqt.editcurrent.EditCurrent): ################################################################## actions + # search two times, one is to select the current note or its cards, + # and another to show the whole history, while keeping the above selection + # set sort column to our search tag, which: + # * prevents the column sort indicator from being shown + # * serves as a hint for us to show notes or cards in history order + # (user can then click on any of the column names + # to show history cards in the order of their choosing) def show_browser(self, *_): - def search_input_select_all(browser, *_): - browser.form.searchEdit.lineEdit().selectAll() + def search_input_select_all(hook_browser, *_): + hook_browser.form.searchEdit.lineEdit().selectAll() gui_hooks.browser_did_change_row.remove(search_input_select_all) - gui_hooks.browser_did_change_row.append(search_input_select_all) - aqt.dialogs.open("Browser", aqt.mw, search=(f"nid:{self.note.id}",)) + + browser = aqt.dialogs.open("Browser", aqt.mw) + browser.table._state.sort_column = self.dialog_search_tag # noqa + browser.table._set_sort_indicator() # noqa + + browser.search_for(f"nid:{self.note.id}") + browser.table.select_all() + browser.search_for(self.dialog_search_tag) def show_preview(self, *_): if cards := self.note.cards(): @@ -339,8 +367,19 @@ class Edit(aqt.editcurrent.EditCurrent): ########################################################################## @classmethod - def register_with_dialog_manager(cls): - aqt.dialogs.register_dialog(cls.dialog_registry_tag, cls) + def browser_will_search(cls, search_context): + if search_context.search == cls.dialog_search_tag: + trigger_search_for_dialog_history_notes( + search_context=search_context, + use_history_order=cls.dialog_search_tag == + search_context.browser.table._state.sort_column # noqa + ) + + @classmethod + def register_with_anki(cls): + if cls.dialog_registry_tag not in aqt.dialogs._dialogs: # noqa + aqt.dialogs.register_dialog(cls.dialog_registry_tag, cls) + gui_hooks.browser_will_search.append(cls.browser_will_search) @classmethod def open_dialog_and_show_note_with_id(cls, note_id): # raises NotFoundError diff --git a/tests/conftest.py b/tests/conftest.py index faad341..4905f7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,6 +44,10 @@ def close_all_dialogs_and_wait_for_them_to_run_closing_callbacks(): wait_until(aqt.dialogs.allClosed) +def get_dialog_instance(name): + return aqt.dialogs._dialogs[name][1] # noqa + + @contextmanager def empty_anki_session_started(): with anki_running( @@ -98,6 +102,8 @@ class Setup: deck_id: int note1_id: int note2_id: int + note1_card_ids: "list[int]" + note2_card_ids: "list[int]" card_ids: "list[int]" @@ -128,12 +134,16 @@ def set_up_test_deck_and_test_model_and_two_notes(): tags={"tag2"}, )) + note1_card_ids = ac.findCards(query=f"nid:{note1_id}") + note2_card_ids = ac.findCards(query=f"nid:{note2_id}") card_ids = ac.findCards(query="deck:test_deck") return Setup( deck_id=deck_id, note1_id=note1_id, note2_id=note2_id, + note1_card_ids=note1_card_ids, + note2_card_ids=note2_card_ids, card_ids=card_ids, ) @@ -239,6 +249,6 @@ def setup(session_with_profile_loaded): * Edit dialog is registered with dialog manager * Any dialogs, if open, are safely closed on exit """ - Edit.register_with_dialog_manager() + Edit.register_with_anki() yield set_up_test_deck_and_test_model_and_two_notes() close_all_dialogs_and_wait_for_them_to_run_closing_callbacks() diff --git a/tests/test_edit.py b/tests/test_edit.py index 2ecb5c2..6886fa8 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -1,5 +1,7 @@ -import pytest import aqt.operations.note +import pytest + +from conftest import get_dialog_instance from plugin.edit import Edit, DecentPreviewer, history @@ -18,9 +20,39 @@ def test_edit_dialog_fails_to_open_with_invalid_note(setup): Edit.open_dialog_and_show_note_with_id(123) -def test_browser_dialog_opens(setup): - dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id) - dialog.show_browser() +class TestBrowser: + @staticmethod + def get_selected_card_ids(): + return get_dialog_instance("Browser").table.get_selected_card_ids() + + def test_dialog_opens(self, setup): + dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id) + dialog.show_browser() + + def test_selects_cards_of_last_note(self, setup): + Edit.open_dialog_and_show_note_with_id(setup.note1_id) + Edit.open_dialog_and_show_note_with_id(setup.note2_id).show_browser() + + assert {*self.get_selected_card_ids()} == {*setup.note2_card_ids} + + def test_selects_cards_of_note_before_last_after_previous_button_pressed(self, setup): + Edit.open_dialog_and_show_note_with_id(setup.note1_id) + dialog = Edit.open_dialog_and_show_note_with_id(setup.note2_id) + + def verify_that_the_table_shows_note2_cards_then_note1_cards(): + get_dialog_instance("Browser").table.select_all() + assert {*self.get_selected_card_ids()[:2]} == {*setup.note2_card_ids} + assert {*self.get_selected_card_ids()[2:]} == {*setup.note1_card_ids} + + dialog.show_previous() + dialog.show_browser() + assert {*self.get_selected_card_ids()} == {*setup.note1_card_ids} + verify_that_the_table_shows_note2_cards_then_note1_cards() + + dialog.show_next() + dialog.show_browser() + assert {*self.get_selected_card_ids()} == {*setup.note2_card_ids} + verify_that_the_table_shows_note2_cards_then_note1_cards() class TestPreviewDialog: diff --git a/tests/test_graphical.py b/tests/test_graphical.py index eada1be..cabcfea 100755 --- a/tests/test_graphical.py +++ b/tests/test_graphical.py @@ -1,8 +1,8 @@ -import aqt import pytest -from conftest import ac, wait, wait_until, \ - close_all_dialogs_and_wait_for_them_to_run_closing_callbacks +from conftest import ac, wait_until, \ + close_all_dialogs_and_wait_for_them_to_run_closing_callbacks, \ + get_dialog_instance def test_guiBrowse(setup): @@ -52,7 +52,7 @@ class TestAddCards: @staticmethod def click_on_add_card_dialog_save_button(): - dialog = aqt.dialogs._dialogs["AddCards"][1] + dialog = get_dialog_instance("AddCards") dialog.addButton.click() # todo previously, these tests were verifying