From 135de7f9ed4d85cbd63d701a23a6d3b26d6da9da Mon Sep 17 00:00:00 2001 From: Mani <12841290+krmanik@users.noreply.github.com> Date: Thu, 27 Jul 2023 20:45:49 +0800 Subject: [PATCH] image occlusion button in note editor (#2485) * setup mask editor in note editor - add image on mask button click (only one time) - show hide add button for io on notetype change - hide field in io notetype - icon for toggle and replace image * add update io notes * Tidy up i/o notetype check and fix error - Make it a method on editor - Use .get(), because the setting doesn't exist on older notetypes - Pass the bool value into the ts code, instead of the enum * reset io page after adding * remove adjust function & add target for mask editor * handle browse mode & merged sidetoolbar and toptoolbar to toolbar * fix: shape, button click in browse, dropdown menu * add arrow to add button * store for handling visiblity of maskeditor - remove update button in edit mode, implement autoupdate * update var name * simplify store --- ftl/core/notetypes.ftl | 2 + pylib/anki/collection.py | 4 + pylib/anki/models.py | 1 + qt/aqt/addcards.py | 46 ++- qt/aqt/editcurrent.py | 6 +- qt/aqt/editor.py | 56 +++ ts/editor/EditorField.svelte | 7 +- ts/editor/NoteEditor.svelte | 372 ++++++++++++------ ts/editor/base.ts | 2 + ts/editor/editor-toolbar/EditorToolbar.svelte | 5 + .../ImageOcclusionButton.svelte | 48 +++ ts/editor/editor-toolbar/icons.ts | 2 + ts/editor/tsconfig.json | 3 +- ts/image-occlusion/MaskEditor.svelte | 6 +- ts/image-occlusion/Notes.svelte | 2 +- ts/image-occlusion/SideToolbar.svelte | 99 ----- .../{TopToolbar.svelte => Toolbar.svelte} | 79 +++- ts/image-occlusion/index.ts | 4 +- ts/image-occlusion/mask-editor.ts | 4 + ts/image-occlusion/shapes/floats.ts | 3 + ts/image-occlusion/shapes/from-cloze.ts | 4 +- ts/image-occlusion/shapes/to-cloze.ts | 3 + ts/image-occlusion/store.ts | 2 + 23 files changed, 520 insertions(+), 240 deletions(-) create mode 100644 ts/editor/editor-toolbar/ImageOcclusionButton.svelte delete mode 100644 ts/image-occlusion/SideToolbar.svelte rename ts/image-occlusion/{TopToolbar.svelte => Toolbar.svelte} (70%) diff --git a/ftl/core/notetypes.ftl b/ftl/core/notetypes.ftl index 59036f9f1..96f4087e3 100644 --- a/ftl/core/notetypes.ftl +++ b/ftl/core/notetypes.ftl @@ -51,3 +51,5 @@ notetypes-error-generating-cloze = An error occurred when generating an image oc notetypes-error-getting-imagecloze = An error occurred while fetching an image occlusion note notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date? notetype-error-no-image-to-show = No image to show. +notetypes-no-occlusion-created = You must make at least one occlusion. +notetypes-io-select-image = Select Image diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 4b924fe17..da9bf8134 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -466,6 +466,10 @@ class Collection(DeprecatedNamesMixin): def get_image_for_occlusion(self, path: str | None) -> GetImageForOcclusionResponse: return self._backend.get_image_for_occlusion(path=path) + def add_image_occlusion_notetype(self) -> None: + "Add notetype if missing." + self._backend.add_image_occlusion_notetype() + def add_image_occlusion_note( self, notetype_id: int, diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 7fe27dba3..fe9c9b5f2 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -27,6 +27,7 @@ NotetypeNameIdUseCount = notetypes_pb2.NotetypeNameIdUseCount NotetypeNames = notetypes_pb2.NotetypeNames ChangeNotetypeInfo = notetypes_pb2.ChangeNotetypeInfo ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest +StockNotetype = notetypes_pb2.StockNotetype # legacy types NotetypeDict = dict[str, Any] diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index b0dd4c6a5..933ab8379 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -48,9 +48,10 @@ class AddCards(QMainWindow): self.setMinimumWidth(400) self.setup_choosers() self.setupEditor() - self.setupButtons() add_close_shortcut(self) self._load_new_note() + self.setupButtons() + self.col.add_image_occlusion_notetype() self.history: list[NoteId] = [] self._last_added_note: Optional[Note] = None gui_hooks.operation_did_execute.append(self.on_operation_did_execute) @@ -112,6 +113,12 @@ class AddCards(QMainWindow): self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self) qconnect(self.compat_add_shorcut.activated, self.addButton.click) self.addButton.setToolTip(shortcut(tr.adding_add_shortcut_ctrlandenter())) + + # add io button + self.io_add_button = bb.addButton(f"{tr.actions_add()} {downArrow()}", ar) + qconnect(self.io_add_button.clicked, self.onAddIo) + self.io_add_button.setShortcut(QKeySequence("Ctrl+Shift+I")) + # close self.closeButton = QPushButton(tr.actions_close()) self.closeButton.setAutoDefault(False) @@ -133,6 +140,17 @@ class AddCards(QMainWindow): b.setEnabled(False) self.historyButton = b + # hide io buttons for note type other than image occlusion + self.show_hide_add_buttons() + + def show_hide_add_buttons(self) -> None: + if self.editor.current_notetype_is_image_occlusion(): + self.addButton.setVisible(False) + self.io_add_button.setVisible(True) + else: + self.addButton.setVisible(True) + self.io_add_button.setVisible(False) + def setAndFocusNote(self, note: Note) -> None: self.editor.set_note(note, focusTo=0) @@ -192,6 +210,9 @@ class AddCards(QMainWindow): self, old_note.note_type(), new_note.note_type() ) + # update buttons for image occlusion on note type change + self.show_hide_add_buttons() + def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None: note = self._new_note() if old_note := sticky_fields_from: @@ -283,7 +304,10 @@ class AddCards(QMainWindow): # no problem, duplicate, and confirmed cloze cases problem = None if result == NoteFieldsCheckResult.EMPTY: - problem = tr.adding_the_first_field_is_empty() + if self.editor.current_notetype_is_image_occlusion(): + problem = tr.notetypes_no_occlusion_created() + else: + problem = tr.adding_the_first_field_is_empty() elif result == NoteFieldsCheckResult.MISSING_CLOZE: if not askUser(tr.adding_you_have_a_cloze_deletion_note()): return False @@ -348,6 +372,24 @@ class AddCards(QMainWindow): self.ifCanClose(doClose) + def onAddIo(self) -> None: + m = QMenu(self) + a = m.addAction(tr.notetypes_hide_all_guess_one()) + qconnect(a.triggered, self.add_io_hide_all_note) + a = m.addAction(tr.notetypes_hide_one_guess_one()) + qconnect(a.triggered, self.add_io_hide_one_note) + m.popup(QCursor.pos()) + + def add_io_hide_all_note(self) -> None: + self.editor.web.eval("setOcclusionField(true)") + self.add_current_note() + self.editor.web.eval("resetIOImageLoaded()") + + def add_io_hide_one_note(self) -> None: + self.editor.web.eval("setOcclusionField(false)") + self.add_current_note() + self.editor.web.eval("resetIOImageLoaded()") + # legacy aliases @property diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index a9320bccc..71fc32992 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -21,9 +21,6 @@ class EditCurrent(QDialog): disable_help_button(self) self.setMinimumHeight(400) self.setMinimumWidth(250) - self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut( - QKeySequence("Ctrl+Return") - ) self.editor = aqt.editor.Editor( self.mw, self.form.fieldsArea, @@ -33,6 +30,9 @@ class EditCurrent(QDialog): self.editor.card = self.mw.reviewer.card self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0) restoreGeom(self, "editcurrent") + self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut( + QKeySequence("Ctrl+Return") + ) gui_hooks.operation_did_execute.append(self.on_operation_did_execute) self.show() diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 4bda62e0e..732e0cfc8 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -31,6 +31,7 @@ from anki.collection import Config, SearchNode from anki.consts import MODEL_CLOZE from anki.hooks import runFilter from anki.httpclient import HttpClient +from anki.models import StockNotetype from anki.notes import Note, NoteFieldsCheckResult from anki.utils import checksum, is_lin, is_win, namedtmp from aqt import AnkiQt, colors, gui_hooks @@ -549,17 +550,33 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too setShrinkImages({json.dumps(self.mw.col.get_config("shrinkEditorImages", True))}); setCloseHTMLTags({json.dumps(self.mw.col.get_config("closeHTMLTags", True))}); triggerChanges(); + setIsImageOcclusion({json.dumps(self.current_notetype_is_image_occlusion())}); + setIsEditMode({json.dumps(self.editorMode != EditorMode.ADD_CARDS)}); """ if self.addMode: sticky = [field["sticky"] for field in self.note.note_type()["flds"]] js += " setSticky(%s);" % json.dumps(sticky) + if ( + self.editorMode != EditorMode.ADD_CARDS + and self.current_notetype_is_image_occlusion() + ): + options = {"kind": "edit", "noteId": self.note.id} + options = {"mode": options} + js += " setupMaskEditor(%s);" % json.dumps(options) + js = gui_hooks.editor_will_load_note(js, self.note, self) self.web.evalWithCallback( f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback ) + def current_notetype_is_image_occlusion(self) -> bool: + return bool(self.note) and ( + self.note.note_type().get("originalStockKind", None) + == StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION + ) + def _save_current_note(self) -> None: "Call after note is updated with data from webview." update_note(parent=self.widget, note=self.note).run_in_background( @@ -1175,6 +1192,34 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too def setTagsCollapsed(self, collapsed: bool) -> None: aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed) + def onAddImageForOcclusion(self) -> None: + """Show a file selection screen, then get selected image path.""" + extension_filter = " ".join( + f"*.{extension}" for extension in sorted(itertools.chain(pics)) + ) + filter = f"{tr.editing_media()} ({extension_filter})" + + def accept(file: str) -> None: + try: + html = self._addMedia(file) + mode = {"kind": "add", "imagePath": file, "notetypeId": 0} + # pass both html and options + options = {"html": html, "mode": mode} + self.web.eval(f"setupMaskEditor({json.dumps(options)})") + except Exception as e: + showWarning(str(e)) + return + + file = getFile( + parent=self.widget, + title=tr.editing_add_media(), + cb=cast(Callable[[Any], None], accept), + filter=filter, + key="media", + ) + + self.parentWindow.activateWindow() + # Links from HTML ###################################################################### @@ -1204,6 +1249,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too toggleMathjax=Editor.toggleMathjax, toggleShrinkImages=Editor.toggleShrinkImages, toggleCloseHTMLTags=Editor.toggleCloseHTMLTags, + addImageForOcclusion=Editor.onAddImageForOcclusion, ) @@ -1452,4 +1498,14 @@ def set_cloze_button(editor: Editor) -> None: ) +def set_image_occlusion_button(editor: Editor) -> None: + action = "show" if editor.current_notetype_is_image_occlusion() else "hide" + editor.web.eval( + 'require("anki/ui").loaded.then(() =>' + f'require("anki/NoteEditor").instances[0].toolbar.toolbar.{action}("image-occlusion-button")' + "); " + ) + + gui_hooks.editor_did_load_note.append(set_cloze_button) +gui_hooks.editor_did_load_note.append(set_image_occlusion_button) diff --git a/ts/editor/EditorField.svelte b/ts/editor/EditorField.svelte index 9238250c0..a4849a290 100644 --- a/ts/editor/EditorField.svelte +++ b/ts/editor/EditorField.svelte @@ -15,6 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html plainText: boolean; description: string; collapsed: boolean; + hidden: boolean; } export interface EditorFieldAPI { @@ -87,7 +88,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html onDestroy(() => api?.destroy()); -
+
@@ -127,6 +128,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html overflow: hidden; } + .field-container.hide { + display: none; + } + .editor-field { overflow: hidden; /* make room for thicker focus border */ diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index d0f58fe3c..dd5b96237 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -237,6 +237,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html return noteId; } + let isImageOcclusion = false; + function setIsImageOcclusion(val: boolean) { + isImageOcclusion = val; + $ioMaskEditorVisible = val; + } + + let isEditMode = false; + function setIsEditMode(val: boolean) { + isEditMode = val; + } + let cols: ("dupe" | "")[] = []; export function setBackgrounds(cls: ("dupe" | "")[]): void { cols = cls; @@ -255,6 +266,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html fontSize: fonts[index][1], direction: fonts[index][2] ? "rtl" : "ltr", collapsed: fieldsCollapsed[index], + hidden: hideFieldInOcclusionType(index), })) as FieldData[]; function saveTags({ detail }: CustomEvent): void { @@ -286,6 +298,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } function saveNow(): void { + updateIONoteInEditMode(); closeMathjaxEditor?.(); $commitTagEdits(); saveFieldNow(); @@ -372,12 +385,68 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } import { wrapInternal } from "@tslib/wrap"; + import LabelButton from "components/LabelButton.svelte"; import Shortcut from "components/Shortcut.svelte"; + import ImageOcclusionPage from "image-occlusion/ImageOcclusionPage.svelte"; + import type { IOMode } from "image-occlusion/lib"; + import { exportShapesToClozeDeletions } from "image-occlusion/shapes/to-cloze"; + import { ioMaskEditorVisible } from "image-occlusion/store"; import { mathjaxConfig } from "../editable/mathjax-element"; import CollapseLabel from "./CollapseLabel.svelte"; import * as oldEditorAdapter from "./old-editor-adapter"; + let isIOImageLoaded = false; + let imageOcclusionMode: IOMode | undefined; + async function setupMaskEditor(options: { html: string; mode: IOMode }) { + imageOcclusionMode = options.mode; + if (options.mode.kind === "add") { + fieldStores[1].set(options.html); + } + isIOImageLoaded = true; + } + + // update cloze deletions and set occlusion fields, it call in saveNow to update cloze deletions + function updateIONoteInEditMode() { + if (isEditMode) { + const clozeNote = get(fieldStores[0]); + if (clozeNote.includes("oi=1")) { + setOcclusionField(true); + } else { + setOcclusionField(false); + } + } + } + + // reset for new occlusion in add mode + function resetIOImageLoaded() { + isIOImageLoaded = false; + globalThis.canvas.clear(); + const page = document.querySelector(".image-occlusion"); + if (page) { + page.remove(); + } + } + globalThis.resetIOImageLoaded = resetIOImageLoaded; + + function setOcclusionField(occludeInactive: boolean) { + // set fields data for occlusion and image fields for io notes type + if (isImageOcclusion) { + const occlusionsData = exportShapesToClozeDeletions(occludeInactive); + fieldStores[0].set(occlusionsData.clozes); + } + } + + // hide first two fields for occlusion type, first contains occlusion data and second contains image + function hideFieldInOcclusionType(index: number) { + if (isImageOcclusion) { + if (index == 0 || index == 1) { + return true; + } + } + return false; + } + onMount(() => { function wrap(before: string, after: string): void { if (!$focusedInput || !editingInputIsRichText($focusedInput)) { @@ -411,6 +480,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setShrinkImages, setCloseHTMLTags, triggerChanges, + setIsImageOcclusion, + setIsEditMode, + setupMaskEditor, + setOcclusionField, ...oldEditorAdapter, }); @@ -464,137 +537,158 @@ the AddCards dialog) should be implemented in the user of this component. {/if} - - {#each fieldsData as field, index} - {@const content = fieldStores[index]} + {#if imageOcclusionMode} +
+ +
+ {/if} - { - $focusedField = fields[index]; - setAddonButtonsDisabled(false); - bridgeCommand(`focus:${index}`); - }} - on:focusout={() => { - $focusedField = null; - setAddonButtonsDisabled(true); - bridgeCommand( - `blur:${index}:${getNoteId()}:${transformContentBeforeSave( - get(content), - )}`, - ); - }} - on:mouseenter={() => { - $hoveredField = fields[index]; - }} - on:mouseleave={() => { - $hoveredField = null; - }} - collapsed={fieldsCollapsed[index]} - dupe={cols[index] === "dupe"} - --description-font-size="{field.fontSize}px" - --description-content={`"${field.description}"`} + {#if $ioMaskEditorVisible && isImageOcclusion && !isIOImageLoaded} +
+ bridgeCommand("addImageForOcclusion")} > - - toggleField(index)} - --icon-align="bottom" - > - - - {field.name} - - - - {#if cols[index] === "dupe"} - - {/if} - {#if plainTextDefaults[index]} - toggleRichTextInput(index)} + {tr.notetypesIoSelectImage()} + +
+ {/if} + + {#if !$ioMaskEditorVisible} + + {#each fieldsData as field, index} + {@const content = fieldStores[index]} + + { + $focusedField = fields[index]; + setAddonButtonsDisabled(false); + bridgeCommand(`focus:${index}`); + }} + on:focusout={() => { + $focusedField = null; + setAddonButtonsDisabled(true); + bridgeCommand( + `blur:${index}:${getNoteId()}:${transformContentBeforeSave( + get(content), + )}`, + ); + }} + on:mouseenter={() => { + $hoveredField = fields[index]; + }} + on:mouseleave={() => { + $hoveredField = null; + }} + collapsed={fieldsCollapsed[index]} + dupe={cols[index] === "dupe"} + --description-font-size="{field.fontSize}px" + --description-content={`"${field.description}"`} + > + + toggleField(index)} + --icon-align="bottom" + > + + + {field.name} + + + + {#if cols[index] === "dupe"} + + {/if} + {#if plainTextDefaults[index]} + toggleRichTextInput(index)} + /> + {:else} + togglePlainTextInput(index)} + /> + {/if} + - {:else} - togglePlainTextInput(index)} - /> - {/if} - + + + + + { + saveFieldNow(); + $focusedInput = null; + }} + bind:this={richTextInputs[index]} /> - - - - - - { - saveFieldNow(); - $focusedInput = null; - }} - bind:this={richTextInputs[index]} - /> - - - - - { - saveFieldNow(); - $focusedInput = null; - }} - bind:this={plainTextInputs[index]} - /> - - - - {/each} +
+ + + + { + saveFieldNow(); + $focusedInput = null; + }} + bind:this={plainTextInputs[index]} + /> + + + + {/each} - - - + + + - { - updateTagsCollapsed(false); - }} - /> - updateTagsCollapsed(!$tagsCollapsed)} - > - {@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`} - - - - + { + updateTagsCollapsed(false); + }} + /> + updateTagsCollapsed(!$tagsCollapsed)} + > + {@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`} + + + + + {/if}
diff --git a/ts/editor/base.ts b/ts/editor/base.ts index 80fd82283..b4a120728 100644 --- a/ts/editor/base.ts +++ b/ts/editor/base.ts @@ -38,6 +38,8 @@ export const editorModules = [ ModuleName.KEYBOARD, ModuleName.ACTIONS, ModuleName.BROWSING, + ModuleName.NOTETYPES, + ModuleName.IMPORTING, ]; export const components = { diff --git a/ts/editor/editor-toolbar/EditorToolbar.svelte b/ts/editor/editor-toolbar/EditorToolbar.svelte index 00b0c01ae..db0874b85 100644 --- a/ts/editor/editor-toolbar/EditorToolbar.svelte +++ b/ts/editor/editor-toolbar/EditorToolbar.svelte @@ -55,6 +55,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import DynamicallySlottable from "../../components/DynamicallySlottable.svelte"; import Item from "../../components/Item.svelte"; import BlockButtons from "./BlockButtons.svelte"; + import ImageOcclusionButton from "./ImageOcclusionButton.svelte"; import InlineButtons from "./InlineButtons.svelte"; import NotetypeButtons from "./NotetypeButtons.svelte"; import OptionsButtons from "./OptionsButtons.svelte"; @@ -120,6 +121,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + + + +
diff --git a/ts/editor/editor-toolbar/ImageOcclusionButton.svelte b/ts/editor/editor-toolbar/ImageOcclusionButton.svelte new file mode 100644 index 000000000..3b17ad121 --- /dev/null +++ b/ts/editor/editor-toolbar/ImageOcclusionButton.svelte @@ -0,0 +1,48 @@ + + + + + + + { + $ioMaskEditorVisible = !$ioMaskEditorVisible; + }} + > + {@html mdiViewDashboard} + + + + + + diff --git a/ts/editor/editor-toolbar/icons.ts b/ts/editor/editor-toolbar/icons.ts index 159712b75..5c2749818 100644 --- a/ts/editor/editor-toolbar/icons.ts +++ b/ts/editor/editor-toolbar/icons.ts @@ -11,6 +11,8 @@ export { default as subscriptIcon } from "@mdi/svg/svg/format-subscript.svg"; export { default as superscriptIcon } from "@mdi/svg/svg/format-superscript.svg"; export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg"; export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg"; +export { default as mdiRefresh } from "@mdi/svg/svg/refresh.svg"; +export { default as mdiViewDashboard } from "@mdi/svg/svg/view-dashboard.svg"; export { default as eraserIcon } from "bootstrap-icons/icons/eraser.svg"; export { default as justifyFullIcon } from "bootstrap-icons/icons/justify.svg"; export { default as olIcon } from "bootstrap-icons/icons/list-ol.svg"; diff --git a/ts/editor/tsconfig.json b/ts/editor/tsconfig.json index b5d2538b8..5d34acd1e 100644 --- a/ts/editor/tsconfig.json +++ b/ts/editor/tsconfig.json @@ -15,6 +15,7 @@ { "path": "../sveltelib" }, { "path": "../editable" }, { "path": "../html-filter" }, - { "path": "../tag-editor" } + { "path": "../tag-editor" }, + { "path": "../image-occlusion" } ] } diff --git a/ts/image-occlusion/MaskEditor.svelte b/ts/image-occlusion/MaskEditor.svelte index b5e74267b..b0a26139a 100644 --- a/ts/image-occlusion/MaskEditor.svelte +++ b/ts/image-occlusion/MaskEditor.svelte @@ -8,10 +8,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { IOMode } from "./lib"; import { setupMaskEditor, setupMaskEditorForEdit } from "./mask-editor"; - import SideToolbar from "./SideToolbar.svelte"; + import Toolbar from "./Toolbar.svelte"; export let mode: IOMode; - + const iconSize = 80; let instance: PanZoom; let innerWidth = 0; const startingTool = mode.kind === "add" ? "draw-rectangle" : "cursor"; @@ -39,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } - +
diff --git a/ts/image-occlusion/Notes.svelte b/ts/image-occlusion/Notes.svelte index 9333348b1..bbe8fe9ae 100644 --- a/ts/image-occlusion/Notes.svelte +++ b/ts/image-occlusion/Notes.svelte @@ -85,7 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } .note-toolbar { - margin-left: 98px; + margin-left: 106px; margin-top: 2px; display: flex; overflow-x: auto; diff --git a/ts/image-occlusion/SideToolbar.svelte b/ts/image-occlusion/SideToolbar.svelte deleted file mode 100644 index 5a0b38c31..000000000 --- a/ts/image-occlusion/SideToolbar.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - - - - -
- {#each tools as tool} - { - activeTool = tool.id; - }} - > - {@html tool.icon} - - {/each} -
- - diff --git a/ts/image-occlusion/TopToolbar.svelte b/ts/image-occlusion/Toolbar.svelte similarity index 70% rename from ts/image-occlusion/TopToolbar.svelte rename to ts/image-occlusion/Toolbar.svelte index 0461915eb..da27783ab 100644 --- a/ts/image-occlusion/TopToolbar.svelte +++ b/ts/image-occlusion/Toolbar.svelte @@ -5,18 +5,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +
+ {#each tools as tool} + { + activeTool = tool.id; + }} + > + {@html tool.icon} + + {/each} +
+
@@ -141,7 +190,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html display: flex; overflow-y: scroll; z-index: 99; - margin-left: 98px; + margin-left: 106px; margin-top: 2px; } @@ -171,6 +220,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html height: 32px; margin: unset; padding: 6px !important; + font-size: 16px !important; } .dropdown-content { @@ -189,4 +239,31 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html width: 0.1em !important; height: 0.1em !important; } + + .tool-bar-container { + position: fixed; + top: 42px; + left: 2px; + height: 100%; + border-right: 1px solid var(--border); + overflow-y: auto; + width: 32px; + z-index: 99; + background: var(--canvas-elevated); + padding-bottom: 100px; + } + + :global(.tool-icon-button) { + border: unset; + display: block; + width: 32px; + height: 32px; + margin: unset; + padding: 6px !important; + } + + :global(.active-tool) { + color: white !important; + background: var(--button-primary-bg) !important; + } diff --git a/ts/image-occlusion/index.ts b/ts/image-occlusion/index.ts index 33803a8d1..d3d3e841b 100644 --- a/ts/image-occlusion/index.ts +++ b/ts/image-occlusion/index.ts @@ -20,12 +20,12 @@ const i18n = setupI18n({ ], }); -export async function setupImageOcclusion(mode: IOMode): Promise { +export async function setupImageOcclusion(mode: IOMode, target = document.body): Promise { checkNightMode(); await i18n; return new ImageOcclusionPage({ - target: document.body, + target: target, props: { mode, }, diff --git a/ts/image-occlusion/mask-editor.ts b/ts/image-occlusion/mask-editor.ts index ef98dbe94..ea9550fdd 100644 --- a/ts/image-occlusion/mask-editor.ts +++ b/ts/image-occlusion/mask-editor.ts @@ -54,6 +54,7 @@ export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom): // get image width and height const image = document.getElementById("image") as HTMLImageElement; + image.style.visibility = "hidden"; image.src = getImageData(clozeNote.imageData!); image.onload = function() { const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize()); @@ -66,6 +67,9 @@ export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom): addShapesToCanvasFromCloze(canvas, clozeNote.occlusions); enableSelectable(canvas, true); addClozeNotesToTextEditor(clozeNote.header, clozeNote.backExtra, clozeNote.tags); + window.requestAnimationFrame(() => { + image.style.visibility = "visible"; + }); }; return canvas; diff --git a/ts/image-occlusion/shapes/floats.ts b/ts/image-occlusion/shapes/floats.ts index e5a297e76..4521a1f1b 100644 --- a/ts/image-occlusion/shapes/floats.ts +++ b/ts/image-occlusion/shapes/floats.ts @@ -6,5 +6,8 @@ * for up to widths/heights of 10kpx. */ export function floatToDisplay(number: number): string { + if (Number.isNaN(number) || number == 0) { + return ".0000"; + } return number.toFixed(4).replace(/^0+|0+$/g, ""); } diff --git a/ts/image-occlusion/shapes/from-cloze.ts b/ts/image-occlusion/shapes/from-cloze.ts index c62300fdf..5445ef763 100644 --- a/ts/image-occlusion/shapes/from-cloze.ts +++ b/ts/image-occlusion/shapes/from-cloze.ts @@ -108,8 +108,8 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null { type ShapeType = "rect" | "ellipse" | "polygon"; function buildShape(type: ShapeType, props: Record): Shape { - props.left = parseFloat(props.left); - props.top = parseFloat(props.top); + props.left = parseFloat(Number.isNaN(Number(props.left)) ? ".0000" : props.left); + props.top = parseFloat(Number.isNaN(Number(props.top)) ? ".0000" : props.top); switch (type) { case "rect": { return new Rectangle({ ...props, width: parseFloat(props.width), height: parseFloat(props.height) }); diff --git a/ts/image-occlusion/shapes/to-cloze.ts b/ts/image-occlusion/shapes/to-cloze.ts index 59a544f24..29c37676c 100644 --- a/ts/image-occlusion/shapes/to-cloze.ts +++ b/ts/image-occlusion/shapes/to-cloze.ts @@ -81,6 +81,9 @@ function fabricObjectToBaseShapeOrShapes( function shapeOrShapesToCloze(shapeOrShapes: ShapeOrShapes, index: number): string { let text = ""; function addKeyValue(key: string, value: string) { + if (Number.isNaN(Number(value))) { + value = ".0000"; + } text += `:${key}=${value}`; } diff --git a/ts/image-occlusion/store.ts b/ts/image-occlusion/store.ts index ce9b3be7d..bf56b688f 100644 --- a/ts/image-occlusion/store.ts +++ b/ts/image-occlusion/store.ts @@ -9,3 +9,5 @@ export const notesDataStore = writable({ id: "", title: "", divValue: "", textar export const zoomResetValue = writable(1); // it stores the tags for the note in note editor export const tagsWritable = writable([""]); +// it stores the visibility of mask editor +export const ioMaskEditorVisible = writable(true);