From efaaae8ce4139ee9fd4b7d547748600b65e9beab Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 24 Apr 2025 10:37:41 +0200 Subject: [PATCH] Cloze button get disabled outside of cloze field (#3879) * NF: replace `disabled` by `enabled` This allows to remove the negations and, in my opinion, make the code easier to understand and edit. * Cloze button get disabled outside of cloze field More specifically, if the user focus in a field that is not a cloze field, the button are still there but appear as disabled. The shortcut instead of adding the cloze context shows an alert explaining why this can't be done. While this message is already displayed when the user tries to add a note with cloze in non-cloze field, I suspect it will save time to stop the user as soon as possible from making mistake. This should make very clear what is authorized and what is not. It'll also be a reminder of whether the current field is a cloze or not. In order to do this, I added a back-end method (that I expect we may reuse in ankidroid) to get the index of the fields used in cloze. This set is sent to the note editor, which propagates it where needed. In mathjax, the cloze symbol is removed when the selected field is not a cloze field. --- proto/anki/notetypes.proto | 5 +++++ pylib/anki/models.py | 4 ++++ qt/aqt/editor.py | 3 +++ rslib/src/notetype/service.rs | 15 +++++++++++++++ ts/editor/ClozeButtons.svelte | 8 ++++++-- ts/editor/EditorField.svelte | 2 ++ ts/editor/NoteEditor.svelte | 7 +++++++ .../editor-toolbar/RichTextClozeButtons.svelte | 3 +++ ts/editor/mathjax-overlay/MathjaxButtons.svelte | 5 ++++- ts/editor/mathjax-overlay/MathjaxOverlay.svelte | 4 ++++ ts/editor/rich-text-input/RichTextInput.svelte | 7 +++++++ 11 files changed, 60 insertions(+), 3 deletions(-) diff --git a/proto/anki/notetypes.proto b/proto/anki/notetypes.proto index d6dadc03f..06bf463bf 100644 --- a/proto/anki/notetypes.proto +++ b/proto/anki/notetypes.proto @@ -33,6 +33,7 @@ service NotetypesService { rpc GetFieldNames(NotetypeId) returns (generic.StringList); rpc RestoreNotetypeToStock(RestoreNotetypeToStockRequest) returns (collection.OpChanges); + rpc GetClozeFieldOrds(NotetypeId) returns (GetClozeFieldOrdsResponse); } // Implicitly includes any of the above methods that are not listed in the @@ -242,3 +243,7 @@ enum ClozeField { CLOZE_FIELD_TEXT = 0; CLOZE_FIELD_BACK_EXTRA = 1; } + +message GetClozeFieldOrdsResponse { + repeated uint32 ords = 1; +} \ No newline at end of file diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 4157bef16..230084359 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -281,6 +281,10 @@ class ModelManager(DeprecatedNamesMixin): def sort_idx(self, notetype: NotetypeDict) -> int: return notetype["sortf"] + def cloze_fields(self, mid: NotetypeId) -> Sequence[int]: + """The list of index of fields that are used by cloze deletion in the note type with id `mid`.""" + return self.col._backend.get_cloze_field_ords(mid) + # Adding & changing fields ################################################## diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index b3530a19e..c1eb14b18 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -555,6 +555,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too note_type = self.note_type() flds = note_type["flds"] collapsed = [fld["collapsed"] for fld in flds] + cloze_fields_ords = self.mw.col.models.cloze_fields(self.note.mid) + cloze_fields = [ord in cloze_fields_ords for ord in range(len(flds))] plain_texts = [fld.get("plainText", False) for fld in flds] descriptions = [fld.get("description", "") for fld in flds] notetype_meta = {"id": self.note.mid, "modTime": note_type["mod"]} @@ -584,6 +586,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too setIsImageOcclusion({json.dumps(self.current_notetype_is_image_occlusion())}); setNotetypeMeta({json.dumps(notetype_meta)}); setCollapsed({json.dumps(collapsed)}); + setClozeFields({json.dumps(cloze_fields)}); setPlainTexts({json.dumps(plain_texts)}); setDescriptions({json.dumps(descriptions)}); setFonts({json.dumps(self.fonts())}); diff --git a/rslib/src/notetype/service.rs b/rslib/src/notetype/service.rs index fe899b035..3e0add71e 100644 --- a/rslib/src/notetype/service.rs +++ b/rslib/src/notetype/service.rs @@ -212,6 +212,21 @@ impl crate::services::NotetypesService for Collection { ) .map(Into::into) } + + fn get_cloze_field_ords( + &mut self, + input: anki_proto::notetypes::NotetypeId, + ) -> error::Result { + Ok(anki_proto::notetypes::GetClozeFieldOrdsResponse { + ords: self + .get_notetype(input.into())? + .unwrap() + .cloze_fields() + .iter() + .map(|ord| (*ord) as u32) + .collect(), + }) + } } impl From for Notetype { diff --git a/ts/editor/ClozeButtons.svelte b/ts/editor/ClozeButtons.svelte index 03d346012..e9faae540 100644 --- a/ts/editor/ClozeButtons.svelte +++ b/ts/editor/ClozeButtons.svelte @@ -71,8 +71,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }); } - $: disabled = - !alwaysEnabled && (!$focusedInput || !editingInputIsRichText($focusedInput)); + $: enabled = + alwaysEnabled || + ($focusedInput && + editingInputIsRichText($focusedInput) && + $focusedInput.isClozeField); + $: disabled = !enabled; const incrementKeyCombination = "Control+Shift+C"; const sameKeyCombination = "Control+Alt+Shift+C"; diff --git a/ts/editor/EditorField.svelte b/ts/editor/EditorField.svelte index 89e797a29..4fc80e0c1 100644 --- a/ts/editor/EditorField.svelte +++ b/ts/editor/EditorField.svelte @@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html description: string; collapsed: boolean; hidden: boolean; + isClozeField: boolean; } export interface EditorFieldAPI { @@ -82,6 +83,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html element, direction: directionStore, editingArea: editingArea as EditingAreaAPI, + isClozeField: field.isClozeField, }); setContextProperty(api); diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index 9bf66d858..17ced575b 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -144,6 +144,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html fieldsCollapsed = sessionOptions[notetypeMeta?.id]?.fieldsCollapsed ?? defaultCollapsed; } + let clozeFields: boolean[] = []; + export function setClozeFields(defaultClozeFields: boolean[]): void { + clozeFields = defaultClozeFields; + } let richTextsHidden: boolean[] = []; let plainTextsHidden: boolean[] = []; @@ -276,6 +280,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html direction: fonts[index][2] ? "rtl" : "ltr", collapsed: fieldsCollapsed[index], hidden: hideFieldInOcclusionType(index, ioFields), + isClozeField: clozeFields[index], })) as FieldData[]; let lastSavedTags: string[] | null = null; @@ -573,6 +578,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html saveSession, setFields, setCollapsed, + setClozeFields, setPlainTexts, setDescriptions, setFonts, @@ -762,6 +768,7 @@ the AddCards dialog) should be implemented in the user of this component. $focusedInput = null; }} bind:this={richTextInputs[index]} + isClozeField={field.isClozeField} /> diff --git a/ts/editor/editor-toolbar/RichTextClozeButtons.svelte b/ts/editor/editor-toolbar/RichTextClozeButtons.svelte index 04aed2581..74d7b8db8 100644 --- a/ts/editor/editor-toolbar/RichTextClozeButtons.svelte +++ b/ts/editor/editor-toolbar/RichTextClozeButtons.svelte @@ -14,6 +14,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: richTextAPI = $focusedInput as RichTextInputAPI; async function onSurround({ detail }): Promise { + if (!richTextAPI.isClozeField) { + return; + } const richText = await richTextAPI.element; const { prefix, suffix } = detail; diff --git a/ts/editor/mathjax-overlay/MathjaxButtons.svelte b/ts/editor/mathjax-overlay/MathjaxButtons.svelte index 03098c96a..06f620114 100644 --- a/ts/editor/mathjax-overlay/MathjaxButtons.svelte +++ b/ts/editor/mathjax-overlay/MathjaxButtons.svelte @@ -15,6 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import ClozeButtons from "../ClozeButtons.svelte"; export let isBlock: boolean; + export let isClozeField: boolean; const dispatch = createEventDispatcher(); @@ -40,7 +41,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - + {#if isClozeField} + + {/if} { cleanup?.(); @@ -50,6 +52,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html on(container, "movecaretafter" as any, showOnAutofocus), on(container, "selectall" as any, showSelectAll), ); + isClozeField = input.isClozeField; } // Wait if the mathjax overlay is still active @@ -242,6 +245,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html { isBlock = false; await updateBlockAttribute(); diff --git a/ts/editor/rich-text-input/RichTextInput.svelte b/ts/editor/rich-text-input/RichTextInput.svelte index 74abed4e9..88549aefc 100644 --- a/ts/editor/rich-text-input/RichTextInput.svelte +++ b/ts/editor/rich-text-input/RichTextInput.svelte @@ -22,6 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html /** The API exposed by the editable component */ editable: ContentEditableAPI; customStyles: Promise>; + isClozeField: boolean; } function editingInputIsRichText( @@ -84,6 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let hidden = false; export const focusFlag = new Flag(); + export let isClozeField: boolean; const { focusedInput } = noteEditorContext.get(); const { content, editingInputs } = editingAreaContext.get(); @@ -156,6 +158,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html inputHandler, editable: {} as ContentEditableAPI, customStyles, + isClozeField, }; const allContexts = getAllContexts(); @@ -204,6 +207,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + $: { + api.isClozeField = isClozeField; + } + onMount(() => { $editingInputs.push(api); $editingInputs = $editingInputs;