follow-up pr for io button in note editor (#2600)

* follow-up pr for io button in note editor

* Expose change event in Svelte instead of relying on timeout (dae)
This commit is contained in:
Mani 2023-07-31 12:24:26 +08:00 committed by GitHub
parent a35c1a058d
commit b9da61f993
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 186 additions and 56 deletions

View file

@ -115,8 +115,8 @@ class AddCards(QMainWindow):
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 = bb.addButton(f"{tr.actions_add()}", ar)
qconnect(self.io_add_button.clicked, self.add_io_note)
self.io_add_button.setShortcut(QKeySequence("Ctrl+Shift+I"))
# close
@ -372,21 +372,8 @@ 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)")
def add_io_note(self) -> None:
self.editor.web.eval("setOcclusionFieldInner()")
self.add_current_note()
self.editor.web.eval("resetIOImageLoaded()")

View file

@ -562,8 +562,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self.editorMode != EditorMode.ADD_CARDS
and self.current_notetype_is_image_occlusion()
):
options = {"kind": "edit", "noteId": self.note.id}
options = {"mode": options}
mode = {"kind": "edit", "noteId": self.note.id}
options = {"mode": mode}
js += " setupMaskEditor(%s);" % json.dumps(options)
js = gui_hooks.editor_will_load_note(js, self.note, self)
@ -1202,10 +1202,16 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def accept(file: str) -> None:
try:
html = self._addMedia(file)
if self.editorMode == EditorMode.ADD_CARDS:
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)})")
else:
mode = {"kind": "edit", "notetypeId": self.note.id}
options = {"html": html, "mode": mode}
self.web.eval(f"resetIOImage({json.dumps(file)})")
self.web.eval(f"setImageField({json.dumps(html)})")
self.web.eval(f"setupMaskEditor({json.dumps(options)})")
except Exception as e:
showWarning(str(e))
return

View file

@ -298,10 +298,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
function saveNow(): void {
updateIONoteInEditMode();
closeMathjaxEditor?.();
$commitTagEdits();
saveFieldNow();
imageOcclusionMode = undefined;
}
export function saveOnPageHide() {
@ -390,7 +390,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
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 { hideAllGuessOne, ioMaskEditorVisible } from "image-occlusion/store";
import { mathjaxConfig } from "../editable/mathjax-element";
import CollapseLabel from "./CollapseLabel.svelte";
@ -402,10 +402,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
imageOcclusionMode = options.mode;
if (options.mode.kind === "add") {
fieldStores[1].set(options.html);
} else {
const clozeNote = get(fieldStores[0]);
if (clozeNote.includes("oi=1")) {
$hideAllGuessOne = true;
} else {
$hideAllGuessOne = false;
}
}
isIOImageLoaded = true;
}
function setImageField(html) {
fieldStores[1].set(html);
}
globalThis.setImageField = setImageField;
// update cloze deletions and set occlusion fields, it call in saveNow to update cloze deletions
function updateIONoteInEditMode() {
if (isEditMode) {
@ -418,6 +431,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}
function setOcclusionFieldInner() {
if (isImageOcclusion) {
const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne);
fieldStores[0].set(occlusionsData.clozes);
}
}
// global for calling this method in desktop note editor
globalThis.setOcclusionFieldInner = setOcclusionFieldInner;
// reset for new occlusion in add mode
function resetIOImageLoaded() {
isIOImageLoaded = false;
@ -484,6 +506,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setIsEditMode,
setupMaskEditor,
setOcclusionField,
setOcclusionFieldInner,
...oldEditorAdapter,
});
@ -539,7 +562,10 @@ the AddCards dialog) should be implemented in the user of this component.
{#if imageOcclusionMode}
<div style="display: {$ioMaskEditorVisible ? 'block' : 'none'}">
<ImageOcclusionPage mode={imageOcclusionMode} />
<ImageOcclusionPage
mode={imageOcclusionMode}
on:change={updateIONoteInEditMode}
/>
</div>
{/if}

View file

@ -11,15 +11,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import MasksEditor from "./MaskEditor.svelte";
import Notes from "./Notes.svelte";
import StickyFooter from "./StickyFooter.svelte";
import { hideAllGuessOne } from "./store";
export let mode: IOMode;
async function hideAllGuessOne(): Promise<void> {
addOrUpdateNote(mode, true);
}
async function hideOneGuessOne(): Promise<void> {
addOrUpdateNote(mode, false);
async function addNote(): Promise<void> {
addOrUpdateNote(mode, $hideAllGuessOne);
}
const items = [
@ -45,14 +42,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</div>
<div hidden={activeTabValue != 1}>
<MasksEditor {mode} />
<MasksEditor {mode} on:change />
</div>
<div hidden={activeTabValue != 2}>
<Notes />
</div>
<StickyFooter {hideAllGuessOne} {hideOneGuessOne} />
<StickyFooter {addNote} />
</Container>
<style lang="scss">

View file

@ -5,6 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts">
import type { PanZoom } from "panzoom";
import panzoom from "panzoom";
import { createEventDispatcher } from "svelte";
import type { IOMode } from "./lib";
import { setupMaskEditor, setupMaskEditorForEdit } from "./mask-editor";
@ -17,6 +18,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const startingTool = mode.kind === "add" ? "draw-rectangle" : "cursor";
$: canvas = null;
const dispatch = createEventDispatcher();
function onChange() {
dispatch("change", { canvas });
}
function init(node) {
instance = panzoom(node, {
bounds: true,
@ -28,11 +35,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
instance.pause();
if (mode.kind == "add") {
setupMaskEditor(mode.imagePath, instance).then((canvas1) => {
setupMaskEditor(mode.imagePath, instance, onChange).then((canvas1) => {
canvas = canvas1;
});
} else {
setupMaskEditorForEdit(mode.noteId, instance).then((canvas1) => {
setupMaskEditorForEdit(mode.noteId, instance, onChange).then((canvas1) => {
canvas = canvas1;
});
}

View file

@ -8,8 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ButtonGroup from "../components/ButtonGroup.svelte";
import LabelButton from "../components/LabelButton.svelte";
export let hideAllGuessOne: () => void;
export let hideOneGuessOne: () => void;
export let addNote: () => void;
</script>
<div style:flex-grow="1" />
@ -18,18 +17,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<LabelButton
--border-left-radius="5px"
--border-right-radius="5px"
on:click={hideAllGuessOne}
on:click={addNote}
class=" bottom-btn"
>
{tr.notetypesHideAllGuessOne()}
</LabelButton>
<LabelButton
--border-left-radius="5px"
--border-right-radius="5px"
on:click={hideOneGuessOne}
class=" bottom-btn"
>
{tr.notetypesHideOneGuessOne()}
{tr.actionsAdd()}
</LabelButton>
</ButtonGroup>
</div>

View file

@ -3,8 +3,23 @@ Copyright: Ankitects Pty Ltd and contributors
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 { bridgeCommand } from "@tslib/bridgecommand";
import * as tr from "@tslib/ftl";
import DropdownItem from "components/DropdownItem.svelte";
import IconButton from "components/IconButton.svelte";
import Popover from "components/Popover.svelte";
import WithFloating from "components/WithFloating.svelte";
import {
mdiChevronDown,
mdiEye,
mdiFormatAlignCenter,
mdiRefresh,
mdiSquare,
mdiViewDashboard,
} from "./icons";
import { setupMaskEditor } from "./mask-editor";
import { hideAllGuessOne } from "./store";
import { drawEllipse, drawPolygon, drawRectangle } from "./tools/index";
import { makeMaskTransparent } from "./tools/lib";
import { enableSelectable, stopDraw } from "./tools/lib";
@ -24,6 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let showAlignTools = false;
let leftPos = 82;
let maksOpacity = false;
let showFloating = false;
document.addEventListener("click", (event) => {
const upperCanvas = document.querySelector(".upper-canvas");
@ -61,6 +77,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
stopDraw(canvas);
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
};
const resetIOImage = (path) => {
setupMaskEditor(path, instance);
};
globalThis.resetIOImage = resetIOImage;
const setOcclusionFieldForDesktop = () => {
const clist = document.body.classList;
if (
clist.contains("isLin") ||
clist.contains("isMac") ||
clist.contains("isWin")
) {
globalThis.setOcclusionFieldInner();
}
};
</script>
<div class="tool-bar-container">
@ -79,6 +111,65 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</div>
<div class="top-tool-bar-container">
<div class="undo-redo-button" on:click={() => (showFloating = !showFloating)}>
{#if $hideAllGuessOne}
<IconButton class="top-tool-icon-button left-border-radius" {iconSize}>
{@html mdiViewDashboard}
</IconButton>
{:else}
<IconButton class="top-tool-icon-button left-border-radius" {iconSize}>
{@html mdiSquare}
</IconButton>
{/if}
<WithFloating
show={showFloating}
closeOnInsideClick
inline
style="line-height: unset !important"
on:close={() => (showFloating = false)}
>
<IconButton
class="top-tool-icon-button right-border-radius dropdown-tool-mode"
slot="reference"
>
{@html mdiChevronDown}
</IconButton>
<Popover slot="floating" --popover-padding-inline="0">
<DropdownItem
on:click={() => {
$hideAllGuessOne = true;
setOcclusionFieldForDesktop();
}}
>
<span>{tr.notetypesHideAllGuessOne()}</span>
</DropdownItem>
<DropdownItem
on:click={() => {
$hideAllGuessOne = false;
setOcclusionFieldForDesktop();
}}
>
<span>{tr.notetypesHideOneGuessOne()}</span>
</DropdownItem>
</Popover>
</WithFloating>
</div>
<!-- refresh for changing image -->
<div class="undo-redo-button">
<IconButton
class="top-tool-icon-button icon-border-radius"
{iconSize}
on:click={() => {
bridgeCommand("addImageForOcclusion");
}}
>
{@html mdiRefresh}
</IconButton>
</div>
<!-- undo & redo tools -->
<div class="undo-redo-button">
{#each undoRedoTools as tool}
@ -266,4 +357,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
color: white !important;
background: var(--button-primary-bg) !important;
}
:global(.icon-border-radius) {
border-radius: 5px !important;
}
:global(.dropdown-tool-mode) {
height: 38px !important;
display: inline;
}
</style>

View file

@ -9,6 +9,7 @@ export { default as mdiAlignHorizontalRight } from "@mdi/svg/svg/align-horizonta
export { default as mdiAlignVerticalBottom } from "@mdi/svg/svg/align-vertical-bottom.svg";
export { default as mdiAlignVerticalCenter } from "@mdi/svg/svg/align-vertical-center.svg";
export { default as mdiAlignVerticalTop } from "@mdi/svg/svg/align-vertical-top.svg";
export { default as mdiChevronDown } from "@mdi/svg/svg/chevron-down.svg";
export { default as mdiClose } from "@mdi/svg/svg/close.svg";
export { default as mdiCodeTags } from "@mdi/svg/svg/code-tags.svg";
export { default as mdiCopy } from "@mdi/svg/svg/content-copy.svg";
@ -27,7 +28,10 @@ export { default as mdiZoomIn } from "@mdi/svg/svg/magnify-plus-outline.svg";
export { default as mdiMagnifyScan } from "@mdi/svg/svg/magnify-scan.svg";
export { default as mdiRectangleOutline } from "@mdi/svg/svg/rectangle-outline.svg";
export { default as mdiRedo } from "@mdi/svg/svg/redo.svg";
export { default as mdiRefresh } from "@mdi/svg/svg/refresh.svg";
export { default as mdiSquare } from "@mdi/svg/svg/square.svg";
export { default as mdiUndo } from "@mdi/svg/svg/undo.svg";
export { default as mdiUnfoldMoreHorizontal } from "@mdi/svg/svg/unfold-more-horizontal.svg";
export { default as mdiUngroup } from "@mdi/svg/svg/ungroup.svg";
export { default as mdiVectorPolygonVariant } from "@mdi/svg/svg/vector-polygon-variant.svg";
export { default as mdiViewDashboard } from "@mdi/svg/svg/view-dashboard.svg";

View file

@ -16,9 +16,13 @@ import { enableSelectable, moveShapeToCanvasBoundaries } from "./tools/lib";
import { undoRedoInit } from "./tools/tool-undo-redo";
import type { Size } from "./types";
export const setupMaskEditor = async (path: string, instance: PanZoom): Promise<fabric.Canvas> => {
export const setupMaskEditor = async (
path: string,
instance: PanZoom,
onChange: () => void,
): Promise<fabric.Canvas> => {
const imageData = await getImageForOcclusion({ path });
const canvas = initCanvas();
const canvas = initCanvas(onChange);
// get image width and height
const image = document.getElementById("image") as HTMLImageElement;
@ -35,7 +39,11 @@ export const setupMaskEditor = async (path: string, instance: PanZoom): Promise<
return canvas;
};
export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom): Promise<fabric.Canvas> => {
export const setupMaskEditorForEdit = async (
noteId: number,
instance: PanZoom,
onChange: () => void,
): Promise<fabric.Canvas> => {
const clozeNoteResponse = await getImageOcclusionNote({ noteId: BigInt(noteId) });
const kind = clozeNoteResponse.value?.case;
if (!kind || kind === "error") {
@ -50,7 +58,7 @@ export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom):
}
const clozeNote = clozeNoteResponse.value.value;
const canvas = initCanvas();
const canvas = initCanvas(onChange);
// get image width and height
const image = document.getElementById("image") as HTMLImageElement;
@ -75,7 +83,7 @@ export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom):
return canvas;
};
const initCanvas = (): fabric.Canvas => {
function initCanvas(onChange: () => void): fabric.Canvas {
const canvas = new fabric.Canvas("canvas");
tagsWritable.set([]);
globalThis.canvas = canvas;
@ -84,8 +92,10 @@ const initCanvas = (): fabric.Canvas => {
canvas.uniScaleKey = "none";
moveShapeToCanvasBoundaries(canvas);
undoRedoInit(canvas);
canvas.on("object:modified", onChange);
canvas.on("object:removed", onChange);
return canvas;
};
}
const getImageData = (imageData): string => {
const b64encoded = protoBase64.enc(imageData);

View file

@ -11,3 +11,5 @@ export const zoomResetValue = writable(1);
export const tagsWritable = writable([""]);
// it stores the visibility of mask editor
export const ioMaskEditorVisible = writable(true);
// it store hide all or hide one mode
export const hideAllGuessOne = writable(true);