From b08e454f57331af35cef9c52cfff96ba4161e006 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sun, 22 Sep 2024 10:07:24 +0200 Subject: [PATCH] If deck options are modified, ask before closing (#3410) * If deck options are modified, ask before closing This imitates the way the note editor behaves. If a user assumes by error that chanhges are automatically saved, it ensures they won't lose them. Also, this will eventually allows to have the same feature on AnkiDroid. While, currently, we always ask the user whether they want to close the deck options, even when there are no modification, which seems to regularly frustate users (including myself). I'm new to Svelte, please let me know whether there is a better way to obtain the information from Svelte state that I missed. Note that I ensured that only a boolean can be obtained. I didn't cause the whole state to be accessible. May be useful for some add-ons, I guess, but risks breaking too much things. Regarding the deckoptions.py, I tried to imitate addcards.py way to check whether the add card view can be closed. Reusing the same function and variable name when possible. * Update qt/aqt/deckoptions.py (dae) --- qt/aqt/deckoptions.py | 48 ++++++++++++++++++++ ts/routes/deck-options/[deckId]/+page.svelte | 4 ++ ts/routes/deck-options/lib.ts | 4 ++ 3 files changed, 56 insertions(+) diff --git a/qt/aqt/deckoptions.py b/qt/aqt/deckoptions.py index 28600545c..634e58752 100644 --- a/qt/aqt/deckoptions.py +++ b/qt/aqt/deckoptions.py @@ -14,6 +14,7 @@ from aqt.qt import * from aqt.utils import ( KeyboardModifiersPressed, addCloseShortcut, + ask_user_dialog, disable_help_button, restoreGeom, saveGeom, @@ -33,6 +34,8 @@ class DeckOptionsDialog(QDialog): self.mw = mw self._deck = deck self._setup_ui() + self._close_event_has_cleaned_up = False + self._ready = False def _setup_ui(self) -> None: self.setWindowModality(Qt.WindowModality.ApplicationModal) @@ -57,8 +60,44 @@ class DeckOptionsDialog(QDialog): def _on_bridge_cmd(self, cmd: str) -> None: if cmd == "deckOptionsReady": + self.ready = True gui_hooks.deck_options_did_load(self) + def closeEvent(self, evt: QCloseEvent) -> None: + if self._close_event_has_cleaned_up: + evt.accept() + return + evt.ignore() + self.if_can_close() + + def _close(self): + """Close. Ensure the closeEvent is not ignored.""" + self._close_event_has_cleaned_up = True + self.close() + + def if_can_close(self): + """Close if there was no modification. Otherwise ask for confirmation first.""" + + def callbackWithUserChoice(choice: int): + if choice == 0: + # The user accepted to discard current input. + self._close() + + def if_can_close_callback_with_data_information(has_modified_dataData: bool): + if has_modified_dataData: + ask_user_dialog( + tr.card_templates_discard_changes(), + callback=callbackWithUserChoice, + buttons=[ + QMessageBox.StandardButton.Discard, + (tr.adding_keep_editing(), QMessageBox.ButtonRole.RejectRole), + ], + ) + else: + self._close() + + self.has_modified_data(if_can_close_callback_with_data_information) + def reject(self) -> None: self.mw.col.set_wants_abort() self.web.cleanup() @@ -66,6 +105,15 @@ class DeckOptionsDialog(QDialog): saveGeom(self, self.TITLE) QDialog.reject(self) + def has_modified_data(self, callback: Callable[[bool], None]): + """Calls `callback` with the information of whether any deck options are modified.""" + if self.ready: + self.web.evalWithCallback( + "anki.deckOptionsPendingChanges()", callback + ) + else: + callback(False) + def confirm_deck_then_display_options(active_card: Card | None = None) -> None: decks = [aqt.mw.col.decks.current()] diff --git a/ts/routes/deck-options/[deckId]/+page.svelte b/ts/routes/deck-options/[deckId]/+page.svelte index 11af70f71..e3a5f6432 100644 --- a/ts/routes/deck-options/[deckId]/+page.svelte +++ b/ts/routes/deck-options/[deckId]/+page.svelte @@ -11,6 +11,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let data: PageData; let page: DeckOptionsPage; + globalThis.anki ||= {}; + globalThis.anki.deckOptionsPendingChanges = () => { + return data.state.isModified(); + }; onMount(() => { globalThis.$deckOptions = new Promise((resolve, _reject) => { resolve(page); diff --git a/ts/routes/deck-options/lib.ts b/ts/routes/deck-options/lib.ts index d95b1e1aa..5df94ecfb 100644 --- a/ts/routes/deck-options/lib.ts +++ b/ts/routes/deck-options/lib.ts @@ -292,6 +292,10 @@ export class DeckOptionsState { }); return list; } + + isModified(): boolean { + return this.removedConfigs.length > 0 || this.modifiedConfigs.size > 0; + } } function bytesToObject(bytes: Uint8Array): Record {