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
This commit is contained in:
Mani 2023-07-27 20:45:49 +08:00 committed by GitHub
parent 0a418e0612
commit 135de7f9ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 520 additions and 240 deletions

View file

@ -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

View file

@ -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,

View file

@ -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]

View file

@ -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

View file

@ -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()

View file

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

View file

@ -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());
</script>
<div class="field-container" on:mouseenter on:mouseleave>
<div class="field-container" class:hide={field.hidden} on:mouseenter on:mouseleave>
<slot name="field-label" />
<Collapsible collapse={collapsed} let:collapsed={hidden}>
@ -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 */

View file

@ -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.
</Absolute>
{/if}
<Fields>
{#each fieldsData as field, index}
{@const content = fieldStores[index]}
{#if imageOcclusionMode}
<div style="display: {$ioMaskEditorVisible ? 'block' : 'none'}">
<ImageOcclusionPage mode={imageOcclusionMode} />
</div>
{/if}
<EditorField
{field}
{content}
flipInputs={plainTextDefaults[index]}
api={fields[index]}
on:focusin={() => {
$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}
<div id="io-select-image-div" style="padding-top: 60px; text-align: center;">
<LabelButton
--border-left-radius="5px"
--border-right-radius="5px"
class="io-select-image-btn"
on:click={() => bridgeCommand("addImageForOcclusion")}
>
<svelte:fragment slot="field-label">
<LabelContainer
collapsed={fieldsCollapsed[index]}
on:toggle={() => toggleField(index)}
--icon-align="bottom"
>
<svelte:fragment slot="field-name">
<LabelName>
{field.name}
</LabelName>
</svelte:fragment>
<FieldState>
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
{#if plainTextDefaults[index]}
<RichTextBadge
show={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] === $focusedField)}
bind:off={richTextsHidden[index]}
on:toggle={() => toggleRichTextInput(index)}
{tr.notetypesIoSelectImage()}
</LabelButton>
</div>
{/if}
{#if !$ioMaskEditorVisible}
<Fields>
{#each fieldsData as field, index}
{@const content = fieldStores[index]}
<EditorField
{field}
{content}
flipInputs={plainTextDefaults[index]}
api={fields[index]}
on:focusin={() => {
$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}"`}
>
<svelte:fragment slot="field-label">
<LabelContainer
collapsed={fieldsCollapsed[index]}
on:toggle={() => toggleField(index)}
--icon-align="bottom"
>
<svelte:fragment slot="field-name">
<LabelName>
{field.name}
</LabelName>
</svelte:fragment>
<FieldState>
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
{#if plainTextDefaults[index]}
<RichTextBadge
show={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] === $focusedField)}
bind:off={richTextsHidden[index]}
on:toggle={() => toggleRichTextInput(index)}
/>
{:else}
<PlainTextBadge
show={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] === $focusedField)}
bind:off={plainTextsHidden[index]}
on:toggle={() => togglePlainTextInput(index)}
/>
{/if}
<slot
name="field-state"
{field}
{index}
show={fields[index] === $hoveredField ||
fields[index] === $focusedField}
/>
{:else}
<PlainTextBadge
show={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] === $focusedField)}
bind:off={plainTextsHidden[index]}
on:toggle={() => togglePlainTextInput(index)}
/>
{/if}
<slot
name="field-state"
{field}
{index}
show={fields[index] === $hoveredField ||
fields[index] === $focusedField}
</FieldState>
</LabelContainer>
</svelte:fragment>
<svelte:fragment slot="rich-text-input">
<Collapsible
collapse={richTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<RichTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={richTextInputs[index]}
/>
</FieldState>
</LabelContainer>
</svelte:fragment>
<svelte:fragment slot="rich-text-input">
<Collapsible
collapse={richTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<RichTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={richTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<PlainTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
</EditorField>
{/each}
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<PlainTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
</EditorField>
{/each}
<MathjaxOverlay />
<ImageOverlay maxWidth={250} maxHeight={125} />
</Fields>
<MathjaxOverlay />
<ImageOverlay maxWidth={250} maxHeight={125} />
</Fields>
<Shortcut
keyCombination="Control+Shift+T"
on:action={() => {
updateTagsCollapsed(false);
}}
/>
<CollapseLabel
collapsed={$tagsCollapsed}
tooltip={$tagsCollapsed ? tr.editingExpand() : tr.editingCollapse()}
on:toggle={() => updateTagsCollapsed(!$tagsCollapsed)}
>
{@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`}
</CollapseLabel>
<Collapsible toggleDisplay collapse={$tagsCollapsed}>
<TagEditor {tags} on:tagsupdate={saveTags} />
</Collapsible>
<Shortcut
keyCombination="Control+Shift+T"
on:action={() => {
updateTagsCollapsed(false);
}}
/>
<CollapseLabel
collapsed={$tagsCollapsed}
tooltip={$tagsCollapsed ? tr.editingExpand() : tr.editingCollapse()}
on:toggle={() => updateTagsCollapsed(!$tagsCollapsed)}
>
{@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`}
</CollapseLabel>
<Collapsible toggleDisplay collapse={$tagsCollapsed}>
<TagEditor {tags} on:tagsupdate={saveTags} />
</Collapsible>
{/if}
</div>
<style lang="scss">
@ -603,4 +697,32 @@ the AddCards dialog) should be implemented in the user of this component.
flex-direction: column;
height: 100%;
}
:global(.image-occlusion) {
position: fixed;
}
:global(.image-occlusion .tab-buttons) {
display: none !important;
}
:global(.image-occlusion .top-tool-bar-container) {
margin-left: 28px !important;
}
:global(.top-tool-bar-container .icon-button) {
height: 36px !important;
}
:global(.image-occlusion .tool-bar-container) {
top: unset !important;
margin-top: 2px !important;
}
:global(.io-select-image-btn) {
margin: auto;
padding: 0px 8px 0px 8px !important;
}
:global(.image-occlusion .sticky-footer) {
display: none;
}
</style>

View file

@ -38,6 +38,8 @@ export const editorModules = [
ModuleName.KEYBOARD,
ModuleName.ACTIONS,
ModuleName.BROWSING,
ModuleName.NOTETYPES,
ModuleName.IMPORTING,
];
export const components = {

View file

@ -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
<Item id="cloze">
<RichTextClozeButtons />
</Item>
<Item id="image-occlusion-button">
<ImageOcclusionButton />
</Item>
</DynamicallySlottable>
</ButtonToolbar>
</div>

View file

@ -0,0 +1,48 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import ButtonGroup from "components/ButtonGroup.svelte";
import DynamicallySlottable from "components/DynamicallySlottable.svelte";
import IconButton from "components/IconButton.svelte";
import { ioMaskEditorVisible } from "image-occlusion/store";
import ButtonGroupItem, {
createProps,
setSlotHostContext,
updatePropsList,
} from "../../components/ButtonGroupItem.svelte";
import { mdiViewDashboard } from "./icons";
export let api = {};
</script>
<ButtonGroup>
<DynamicallySlottable
slotHost={ButtonGroupItem}
{createProps}
{updatePropsList}
{setSlotHostContext}
{api}
>
<ButtonGroupItem>
<IconButton
id="io-mask-btn"
class={$ioMaskEditorVisible ? "active-io-btn" : ""}
on:click={() => {
$ioMaskEditorVisible = !$ioMaskEditorVisible;
}}
>
{@html mdiViewDashboard}
</IconButton>
</ButtonGroupItem>
</DynamicallySlottable>
</ButtonGroup>
<style>
:global(.active-io-btn) {
background: var(--button-primary-bg) !important;
color: white !important;
}
</style>

View file

@ -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";

View file

@ -15,6 +15,7 @@
{ "path": "../sveltelib" },
{ "path": "../editable" },
{ "path": "../html-filter" },
{ "path": "../tag-editor" }
{ "path": "../tag-editor" },
{ "path": "../image-occlusion" }
]
}

View file

@ -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
}
</script>
<SideToolbar {instance} {canvas} activeTool={startingTool} />
<Toolbar {canvas} {instance} {iconSize} activeTool={startingTool} />
<div class="editor-main" bind:clientWidth={innerWidth}>
<div class="editor-container" use:init>
<!-- svelte-ignore a11y-missing-attribute -->

View file

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

View file

@ -1,99 +0,0 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import IconButton from "../components/IconButton.svelte";
import { drawEllipse, drawPolygon, drawRectangle } from "./tools/index";
import { enableSelectable, stopDraw } from "./tools/lib";
import { tools } from "./tools/tool-buttons";
import TopToolbar from "./TopToolbar.svelte";
export let instance;
export let canvas;
const iconSize = 80;
export let activeTool = "cursor";
// handle tool changes after initialization
$: if (instance && canvas) {
disableFunctions();
enableSelectable(canvas, true);
switch (activeTool) {
case "magnify":
enableSelectable(canvas, false);
instance.resume();
break;
case "draw-rectangle":
drawRectangle(canvas);
break;
case "draw-ellipse":
drawEllipse(canvas);
break;
case "draw-polygon":
drawPolygon(canvas, instance);
break;
default:
break;
}
}
const disableFunctions = () => {
instance.pause();
stopDraw(canvas);
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
};
</script>
<TopToolbar {canvas} {instance} {iconSize} />
<div class="tool-bar-container">
{#each tools as tool}
<IconButton
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
{iconSize}
active={activeTool === tool.id}
on:click={() => {
activeTool = tool.id;
}}
>
{@html tool.icon}
</IconButton>
{/each}
</div>
<style>
.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;
}
::-webkit-scrollbar {
width: 0.2em !important;
height: 0.2em !important;
}
</style>

View file

@ -5,18 +5,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script>
import IconButton from "../components/IconButton.svelte";
import { mdiEye, mdiFormatAlignCenter } from "./icons";
import { drawEllipse, drawPolygon, drawRectangle } from "./tools/index";
import { makeMaskTransparent } from "./tools/lib";
import { enableSelectable, stopDraw } from "./tools/lib";
import {
alignTools,
deleteDuplicateTools,
groupUngroupTools,
zoomTools,
} from "./tools/more-tools";
import { tools } from "./tools/tool-buttons";
import { undoRedoTools } from "./tools/tool-undo-redo";
export let canvas;
export let instance;
export let iconSize;
export let activeTool = "cursor";
let showAlignTools = false;
let leftPos = 82;
let maksOpacity = false;
@ -27,8 +31,53 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
showAlignTools = false;
}
});
// handle tool changes after initialization
$: if (instance && canvas) {
disableFunctions();
enableSelectable(canvas, true);
switch (activeTool) {
case "magnify":
enableSelectable(canvas, false);
instance.resume();
break;
case "draw-rectangle":
drawRectangle(canvas);
break;
case "draw-ellipse":
drawEllipse(canvas);
break;
case "draw-polygon":
drawPolygon(canvas, instance);
break;
default:
break;
}
}
const disableFunctions = () => {
instance.pause();
stopDraw(canvas);
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
};
</script>
<div class="tool-bar-container">
{#each tools as tool}
<IconButton
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
{iconSize}
active={activeTool === tool.id}
on:click={() => {
activeTool = tool.id;
}}
>
{@html tool.icon}
</IconButton>
{/each}
</div>
<div class="top-tool-bar-container">
<!-- undo & redo tools -->
<div class="undo-redo-button">
@ -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;
}
</style>

View file

@ -20,12 +20,12 @@ const i18n = setupI18n({
],
});
export async function setupImageOcclusion(mode: IOMode): Promise<ImageOcclusionPage> {
export async function setupImageOcclusion(mode: IOMode, target = document.body): Promise<ImageOcclusionPage> {
checkNightMode();
await i18n;
return new ImageOcclusionPage({
target: document.body,
target: target,
props: {
mode,
},

View file

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

View file

@ -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, "");
}

View file

@ -108,8 +108,8 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
type ShapeType = "rect" | "ellipse" | "polygon";
function buildShape(type: ShapeType, props: Record<string, any>): 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) });

View file

@ -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}`;
}

View file

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