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);