anki/ts/image-occlusion/Toolbar.svelte
Abdo cc81db0f9d
Fix undo handling of group and some other IO tools (#2931)
* Fix undo handling of group and some other IO tools

* Emit change signal inside onObjectModified

* Fix group lost after moving group then undoing

* Skip undo entry if canvas has not changed

The onObjectModified() call I added in a previous commit to deleteDuplicateTools results in a duplicate undo entry for the delete tool. Checking for duplicate entries seems simpler than having to think about where onObjectModified() should be called exactly

* Fix extra undo entry added after ungroup
2024-01-09 11:19:46 +10:00

502 lines
16 KiB
Svelte

<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { directionKey } from "@tslib/context-keys";
import * as tr from "@tslib/ftl";
import { getPlatformString } from "@tslib/shortcuts";
import DropdownItem from "components/DropdownItem.svelte";
import IconButton from "components/IconButton.svelte";
import Popover from "components/Popover.svelte";
import Shortcut from "components/Shortcut.svelte";
import WithFloating from "components/WithFloating.svelte";
import { getContext, onMount } from "svelte";
import type { Readable } from "svelte/store";
import { mdiEye, mdiFormatAlignCenter, mdiSquare, mdiViewDashboard } from "./icons";
import { emitChangeSignal } from "./MaskEditor.svelte";
import { hideAllGuessOne, ioMaskEditorVisible, textEditingState } from "./store";
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
import { makeMaskTransparent } from "./tools/lib";
import { enableSelectable, stopDraw } from "./tools/lib";
import {
alignTools,
deleteDuplicateTools,
groupUngroupTools,
zoomTools,
} from "./tools/more-tools";
import { toggleTranslucentKeyCombination } from "./tools/shortcuts";
import { tools } from "./tools/tool-buttons";
import { removeUnfinishedPolygon } from "./tools/tool-polygon";
import { undoRedoTools, undoStack } 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;
let showFloating = false;
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
document.addEventListener("click", (event) => {
const upperCanvas = document.querySelector(".upper-canvas");
if (event.target == upperCanvas) {
showAlignTools = false;
}
});
// handle zoom event when mouse scroll and ctrl key are hold for panzoom
let clicked = false;
let dbclicked = false;
let move = false;
let wheel = false;
onMount(() => {
window.addEventListener("mousedown", (event) => {
if (event.ctrlKey) {
clicked = true;
}
});
window.addEventListener("mouseup", (event) => {
if (event.ctrlKey) {
clicked = false;
}
});
window.addEventListener("mousemove", (event) => {
if (event.ctrlKey) {
move = true;
}
});
window.addEventListener("wheel", (event) => {
if (event.ctrlKey) {
wheel = true;
}
});
window.addEventListener("dblclick", (event) => {
if (event.ctrlKey) {
dbclicked = true;
}
});
window.addEventListener("keyup", (event) => {
if (event.key == "Control") {
clicked = false;
move = false;
wheel = false;
dbclicked = false;
}
});
window.addEventListener("keydown", (event) => {
if (event.key == "Control") {
clicked = false;
move = false;
wheel = false;
dbclicked = false;
}
});
window.addEventListener("keydown", (event) => {
if (event.key == "Control" && activeTool != "magnify") {
instance.resume();
}
});
window.addEventListener("keyup", (event) => {
if (event.key == "Control" && activeTool != "magnify") {
instance.pause();
}
});
window.addEventListener("wheel", () => {
if (clicked && move && wheel && !dbclicked) {
enableMagnify();
}
});
});
// handle tool changes after initialization
$: if (instance && canvas) {
disableFunctions();
enableSelectable(canvas, true);
// remove unfinished polygon when switching to other tools
removeUnfinishedPolygon(canvas);
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;
case "draw-text":
drawText(canvas);
break;
default:
break;
}
}
const disableFunctions = () => {
instance.pause();
stopDraw(canvas);
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
};
function changeOcclusionType(occlusionType: "all" | "one"): void {
$hideAllGuessOne = occlusionType === "all";
emitChangeSignal();
}
const enableMagnify = () => {
disableFunctions();
enableSelectable(canvas, false);
instance.resume();
activeTool = "magnify";
};
</script>
<div class="tool-bar-container">
{#each tools as tool}
<IconButton
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
{iconSize}
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
active={activeTool === tool.id}
on:click={() => {
activeTool = tool.id;
}}
>
{@html tool.icon}
</IconButton>
{#if $ioMaskEditorVisible && !$textEditingState}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
activeTool = tool.id;
}}
/>
{/if}
{/each}
</div>
<div dir={$direction}>
<div class="top-tool-bar-container">
<WithFloating
show={showFloating}
closeOnInsideClick
inline
on:close={() => (showFloating = false)}
>
<IconButton
class="top-tool-icon-button border-radius dropdown-tool-mode"
slot="reference"
tooltip={tr.editingImageOcclusionMode()}
{iconSize}
on:click={() => (showFloating = !showFloating)}
>
{@html $hideAllGuessOne ? mdiViewDashboard : mdiSquare}
</IconButton>
<Popover slot="floating">
<DropdownItem
active={$hideAllGuessOne}
on:click={() => changeOcclusionType("all")}
>
<span>{tr.notetypesHideAllGuessOne()}</span>
</DropdownItem>
<DropdownItem
active={!$hideAllGuessOne}
on:click={() => changeOcclusionType("one")}
>
<span>{tr.notetypesHideOneGuessOne()}</span>
</DropdownItem>
</Popover>
</WithFloating>
<!-- undo & redo tools -->
<div class="undo-redo-button">
{#each undoRedoTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'undo'
? 'left-border-radius'
: 'right-border-radius'}"
{iconSize}
on:click={tool.action}
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
disabled={tool.name === "undo"
? !$undoStack.undoable
: !$undoStack.redoable}
>
{@html tool.icon}
</IconButton>
{#if $ioMaskEditorVisible && !$textEditingState}
<Shortcut keyCombination={tool.shortcut} on:action={tool.action} />
{/if}
{/each}
</div>
<!-- zoom tools -->
<div class="tool-button-container">
{#each zoomTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'zoomOut'
? 'left-border-radius'
: ''} {tool.name === 'zoomReset' ? 'right-border-radius' : ''}"
{iconSize}
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
on:click={() => {
tool.action(instance);
}}
>
{@html tool.icon}
</IconButton>
{#if $ioMaskEditorVisible && !$textEditingState}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
tool.action(instance);
}}
/>
{/if}
{/each}
</div>
<div class="tool-button-container">
<!-- opacity tools -->
<IconButton
class="top-tool-icon-button left-border-radius"
{iconSize}
tooltip="{tr.editingImageOcclusionToggleTranslucent()} ({getPlatformString(
toggleTranslucentKeyCombination,
)})"
on:click={() => {
maksOpacity = !maksOpacity;
makeMaskTransparent(canvas, maksOpacity);
}}
>
{@html mdiEye}
</IconButton>
{#if $ioMaskEditorVisible && !$textEditingState}
<Shortcut
keyCombination={toggleTranslucentKeyCombination}
on:action={() => {
maksOpacity = !maksOpacity;
makeMaskTransparent(canvas, maksOpacity);
}}
/>
{/if}
<!-- cursor tools -->
{#each deleteDuplicateTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'duplicate'
? 'right-border-radius'
: ''}"
{iconSize}
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
on:click={() => {
tool.action(canvas);
undoStack.onObjectModified();
}}
>
{@html tool.icon}
</IconButton>
{#if $ioMaskEditorVisible && !$textEditingState}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
tool.action(canvas);
emitChangeSignal();
}}
/>
{/if}
{/each}
</div>
<div class="tool-button-container">
<!-- group & ungroup tools -->
{#each groupUngroupTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'group'
? 'left-border-radius'
: ''}"
{iconSize}
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
on:click={() => {
tool.action(canvas);
undoStack.onObjectModified();
}}
>
{@html tool.icon}
</IconButton>
{#if $ioMaskEditorVisible && !$textEditingState}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
tool.action(canvas);
emitChangeSignal();
}}
/>
{/if}
{/each}
<IconButton
class="top-tool-icon-button dropdown-tool right-border-radius"
{iconSize}
tooltip={tr.editingImageOcclusionAlignment()}
on:click={(e) => {
showAlignTools = !showAlignTools;
leftPos = e.pageX - 100;
}}
>
{@html mdiFormatAlignCenter}
</IconButton>
</div>
</div>
<div class:show={showAlignTools} class="dropdown-content" style="left:{leftPos}px;">
{#each alignTools as alignTool}
<IconButton
class="top-tool-icon-button"
{iconSize}
tooltip="{alignTool.tooltip()} ({getPlatformString(
alignTool.shortcut,
)})"
on:click={() => {
alignTool.action(canvas);
undoStack.onObjectModified();
}}
>
{@html alignTool.icon}
</IconButton>
{#if $ioMaskEditorVisible && !$textEditingState}
<Shortcut
keyCombination={alignTool.shortcut}
on:action={() => {
alignTool.action(canvas);
}}
/>
{/if}
{/each}
</div>
</div>
<style>
.top-tool-bar-container {
display: flex;
overflow-y: scroll;
z-index: 99;
margin-left: 106px;
margin-top: 2px;
}
:global([dir="rtl"] .top-tool-bar-container) {
margin-left: unset;
margin-right: 28px;
}
.undo-redo-button {
margin-right: 2px;
display: flex;
}
.tool-button-container {
margin-left: 2px;
margin-right: 2px;
display: flex;
}
:global(.left-border-radius) {
border-radius: 5px 0 0 5px !important;
}
:global([dir="rtl"] .left-border-radius) {
border-radius: 0 5px 5px 0 !important;
}
:global(.right-border-radius) {
border-radius: 0 5px 5px 0 !important;
}
:global([dir="rtl"] .right-border-radius) {
border-radius: 5px 0 0 5px !important;
}
:global(.border-radius) {
border-radius: 5px !important;
}
:global(.top-tool-icon-button) {
border: unset;
display: inline;
width: 32px;
height: 32px;
margin: unset;
padding: 6px !important;
font-size: 16px !important;
}
.dropdown-content {
display: none;
position: absolute;
z-index: 100;
top: 40px;
margin-top: 1px;
}
.show {
display: flex;
}
::-webkit-scrollbar {
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([dir="rtl"] .tool-bar-container) {
left: unset;
right: 2px;
}
: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;
}
:global(.icon-border-radius) {
border-radius: 5px !important;
}
:global(.dropdown-tool-mode) {
height: 38px !important;
display: inline;
}
</style>