mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 12:47:11 -05:00
This PR adds the "hide all but one" occlusion mode. An example use case is a note containing a collection of pairs of selection, where each selection is the prompt for the other in its pair. For example, given a table like | small | big | |-------+-----| | a | A | | b | B | | c | C | in each card, five letters are occluded, and one is shown. The user is prompted to state the occluded symbol that is adjacent to the shown symbol.
641 lines
20 KiB
Svelte
641 lines
20 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 * as tr from "@generated/ftl";
|
|
import { directionKey } from "@tslib/context-keys";
|
|
import { on } from "@tslib/events";
|
|
import { getPlatformString } from "@tslib/shortcuts";
|
|
import type { Callback } from "@tslib/typing";
|
|
import { singleCallback } from "@tslib/typing";
|
|
import { getContext, onDestroy, onMount } from "svelte";
|
|
import { writable, type Readable } from "svelte/store";
|
|
|
|
import DropdownItem from "$lib/components/DropdownItem.svelte";
|
|
import Icon from "$lib/components/Icon.svelte";
|
|
import IconButton from "$lib/components/IconButton.svelte";
|
|
import {
|
|
mdiCheckboxBlankOutline,
|
|
mdiEye,
|
|
mdiFormatAlignCenter,
|
|
mdiSquare,
|
|
mdiViewDashboard,
|
|
} from "$lib/components/icons";
|
|
import Popover from "$lib/components/Popover.svelte";
|
|
import Shortcut from "$lib/components/Shortcut.svelte";
|
|
import WithFloating from "$lib/components/WithFloating.svelte";
|
|
|
|
import {
|
|
ioMaskEditorVisible,
|
|
OcclusionMode,
|
|
occlusionMode,
|
|
opacityStateStore,
|
|
saveNeededStore,
|
|
textEditingState,
|
|
} from "./store";
|
|
import { get } from "svelte/store";
|
|
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
|
|
import { makeMaskTransparent, SHAPE_MASK_COLOR } 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, type ActiveTool } from "./tools/tool-buttons";
|
|
import { drawCursor } from "./tools/tool-cursor";
|
|
import { removeUnfinishedPolygon } from "./tools/tool-polygon";
|
|
import { undoRedoTools, undoStack } from "./tools/tool-undo-redo";
|
|
import {
|
|
disablePan,
|
|
disableZoom,
|
|
enablePan,
|
|
enableZoom,
|
|
onWheelDrag,
|
|
onWheelDragX,
|
|
} from "./tools/tool-zoom";
|
|
import { fillMask } from "./tools/tool-fill";
|
|
import { getCustomColours, saveCustomColours } from "@generated/backend";
|
|
|
|
export let canvas;
|
|
export let iconSize;
|
|
export let activeTool: ActiveTool = "cursor";
|
|
let showAlignTools = false;
|
|
let leftPos = 82;
|
|
let maskOpacity = false;
|
|
let showFloating = false;
|
|
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
|
// handle zoom event when mouse scroll and ctrl key are hold for panzoom
|
|
let spaceClicked = false;
|
|
let controlClicked = false;
|
|
let shiftClicked = false;
|
|
let move = false;
|
|
const spaceKey = " ";
|
|
const controlKey = "Control";
|
|
const shiftKey = "Shift";
|
|
let removeHandlers: Callback;
|
|
let colourRef: HTMLInputElement | undefined;
|
|
const colour = writable(SHAPE_MASK_COLOR);
|
|
|
|
const customColorPickerPalette = writable<string[]>([]);
|
|
|
|
async function loadCustomColours() {
|
|
customColorPickerPalette.set(
|
|
(await getCustomColours({})).colours.filter(
|
|
(hex) => !hex.startsWith("#ffffff"),
|
|
),
|
|
);
|
|
}
|
|
|
|
function onClick(event: MouseEvent) {
|
|
const upperCanvas = document.querySelector(".upper-canvas");
|
|
if (event.target == upperCanvas) {
|
|
showAlignTools = false;
|
|
}
|
|
}
|
|
|
|
function onMousemove() {
|
|
if (spaceClicked || move) {
|
|
disableFunctions();
|
|
enablePan(canvas);
|
|
}
|
|
}
|
|
|
|
function onMouseup() {
|
|
if (spaceClicked) {
|
|
spaceClicked = false;
|
|
}
|
|
if (move) {
|
|
move = false;
|
|
}
|
|
}
|
|
|
|
function onKeyup(event: KeyboardEvent) {
|
|
if (
|
|
event.key === spaceKey ||
|
|
event.key === controlKey ||
|
|
event.key === shiftKey
|
|
) {
|
|
spaceClicked = false;
|
|
controlClicked = false;
|
|
shiftClicked = false;
|
|
move = false;
|
|
|
|
disableFunctions();
|
|
handleToolChanges(activeTool);
|
|
}
|
|
}
|
|
|
|
function onKeydown(event: KeyboardEvent) {
|
|
if (event.key === spaceKey) {
|
|
spaceClicked = true;
|
|
}
|
|
if (event.key === controlKey) {
|
|
controlClicked = true;
|
|
}
|
|
if (event.key === shiftKey) {
|
|
shiftClicked = true;
|
|
}
|
|
}
|
|
|
|
function onWheel(event: WheelEvent) {
|
|
// allow scroll in fields, when mask editor hidden
|
|
if (!$ioMaskEditorVisible) {
|
|
return;
|
|
}
|
|
|
|
if (event.ctrlKey) {
|
|
controlClicked = true;
|
|
}
|
|
if (event.shiftKey) {
|
|
shiftClicked = true;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
if (controlClicked) {
|
|
disableFunctions();
|
|
enableZoom(canvas);
|
|
return;
|
|
}
|
|
|
|
if (shiftClicked) {
|
|
onWheelDragX(canvas, event);
|
|
return;
|
|
}
|
|
|
|
onWheelDrag(canvas, event);
|
|
}
|
|
|
|
// initializes lastPosX and lastPosY because it is undefined in touchmove event
|
|
function onTouchstart(event: TouchEvent) {
|
|
const canvas = globalThis.canvas;
|
|
canvas.lastPosX = event.touches[0].clientX;
|
|
canvas.lastPosY = event.touches[0].clientY;
|
|
}
|
|
|
|
// initializes lastPosX and lastPosY because it is undefined before mousemove event
|
|
function onMousemoveDocument(event: MouseEvent) {
|
|
if (spaceClicked) {
|
|
canvas.lastPosX = event.clientX;
|
|
canvas.lastPosY = event.clientY;
|
|
}
|
|
}
|
|
|
|
const handleToolChanges = (newActiveTool: ActiveTool, clicked: boolean = false) => {
|
|
disableFunctions();
|
|
enableSelectable(canvas, true);
|
|
// remove unfinished polygon when switching to other tools
|
|
removeUnfinishedPolygon(canvas);
|
|
|
|
switch (newActiveTool) {
|
|
case "cursor":
|
|
drawCursor(canvas);
|
|
break;
|
|
case "draw-rectangle":
|
|
drawRectangle(canvas);
|
|
break;
|
|
case "draw-ellipse":
|
|
drawEllipse(canvas);
|
|
break;
|
|
case "draw-polygon":
|
|
drawPolygon(canvas);
|
|
break;
|
|
case "draw-text":
|
|
drawText(canvas, () => {
|
|
activeTool = "cursor";
|
|
handleToolChanges(activeTool);
|
|
});
|
|
break;
|
|
case "fill-mask":
|
|
if (clicked) {
|
|
colourRef?.click();
|
|
}
|
|
fillMask(canvas, colour);
|
|
break;
|
|
}
|
|
};
|
|
|
|
// handle tool changes after initialization
|
|
$: if (canvas) {
|
|
handleToolChanges(activeTool);
|
|
}
|
|
|
|
const disableFunctions = () => {
|
|
stopDraw(canvas);
|
|
disableZoom(canvas);
|
|
disablePan(canvas);
|
|
};
|
|
|
|
function changeOcclusionType(mode: OcclusionMode): void {
|
|
$occlusionMode = mode;
|
|
saveNeededStore.set(true);
|
|
}
|
|
|
|
onMount(() => {
|
|
maskOpacity = get(opacityStateStore);
|
|
removeHandlers = singleCallback(
|
|
on(document, "click", onClick),
|
|
on(window, "mousemove", onMousemove),
|
|
on(window, "mouseup", onMouseup),
|
|
on(window, "keyup", onKeyup),
|
|
on(window, "keydown", onKeydown),
|
|
on(window, "wheel", onWheel, { passive: false }),
|
|
on(document, "touchstart", onTouchstart),
|
|
on(document, "mousemove", onMousemoveDocument),
|
|
);
|
|
loadCustomColours();
|
|
});
|
|
|
|
onDestroy(() => {
|
|
removeHandlers();
|
|
});
|
|
</script>
|
|
|
|
<datalist id="colour-palette">
|
|
<option>{SHAPE_MASK_COLOR}</option>
|
|
{#each $customColorPickerPalette as colour}
|
|
<option>{colour}</option>
|
|
{/each}
|
|
</datalist>
|
|
|
|
<input
|
|
type="color"
|
|
bind:this={colourRef}
|
|
style:display="none"
|
|
list="colour-palette"
|
|
value={SHAPE_MASK_COLOR}
|
|
on:input={(e) => ($colour = e.currentTarget!.value)}
|
|
on:change={() => saveCustomColours({})}
|
|
/>
|
|
|
|
<div class="tool-bar-container" style:--fill-tool-colour={$colour}>
|
|
{#each tools as tool}
|
|
{@const active = activeTool == tool.id}
|
|
<IconButton
|
|
class="tool-icon-button {active ? 'active-tool' : ''} {tool.id}"
|
|
iconSize={iconSize * (tool["iconSizeMult"] ?? 1)}
|
|
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
|
{active}
|
|
on:click={() => {
|
|
activeTool = tool.id;
|
|
handleToolChanges(activeTool, true);
|
|
}}
|
|
>
|
|
<Icon icon={tool.icon} />
|
|
</IconButton>
|
|
{#if $ioMaskEditorVisible && !$textEditingState}
|
|
<Shortcut
|
|
keyCombination={tool.shortcut}
|
|
on:action={() => {
|
|
activeTool = tool.id;
|
|
handleToolChanges(activeTool, true);
|
|
}}
|
|
/>
|
|
{/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)}
|
|
>
|
|
<Icon
|
|
icon={$occlusionMode === OcclusionMode.HideAll
|
|
? mdiViewDashboard
|
|
: $occlusionMode === OcclusionMode.HideAllButOne
|
|
? mdiCheckboxBlankOutline
|
|
: mdiSquare}
|
|
/>
|
|
</IconButton>
|
|
|
|
<Popover slot="floating">
|
|
<DropdownItem
|
|
active={$occlusionMode === OcclusionMode.HideAll}
|
|
on:click={() => changeOcclusionType(OcclusionMode.HideAll)}
|
|
>
|
|
<span>{tr.notetypesHideAllGuessOne()}</span>
|
|
</DropdownItem>
|
|
<DropdownItem
|
|
active={$occlusionMode === OcclusionMode.HideOne}
|
|
on:click={() => changeOcclusionType(OcclusionMode.HideOne)}
|
|
>
|
|
<span>{tr.notetypesHideOneGuessOne()}</span>
|
|
</DropdownItem>
|
|
<DropdownItem
|
|
active={$occlusionMode === OcclusionMode.HideAllButOne}
|
|
on:click={() => changeOcclusionType(OcclusionMode.HideAllButOne)}
|
|
>
|
|
<span>{tr.notetypesHideAllButOne()}</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();
|
|
handleToolChanges(activeTool);
|
|
}}
|
|
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
|
disabled={tool.name === "undo"
|
|
? !$undoStack.undoable
|
|
: !$undoStack.redoable}
|
|
>
|
|
<Icon icon={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(canvas);
|
|
}}
|
|
>
|
|
<Icon icon={tool.icon} />
|
|
</IconButton>
|
|
{#if $ioMaskEditorVisible && !$textEditingState}
|
|
<Shortcut
|
|
keyCombination={tool.shortcut}
|
|
on:action={() => {
|
|
tool.action(canvas);
|
|
}}
|
|
/>
|
|
{/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={() => {
|
|
maskOpacity = !maskOpacity;
|
|
makeMaskTransparent(canvas, maskOpacity);
|
|
}}
|
|
>
|
|
<Icon icon={mdiEye} />
|
|
</IconButton>
|
|
{#if $ioMaskEditorVisible && !$textEditingState}
|
|
<Shortcut
|
|
keyCombination={toggleTranslucentKeyCombination}
|
|
on:action={() => {
|
|
maskOpacity = !maskOpacity;
|
|
makeMaskTransparent(canvas, maskOpacity);
|
|
}}
|
|
/>
|
|
{/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();
|
|
}}
|
|
>
|
|
<Icon icon={tool.icon} />
|
|
</IconButton>
|
|
{#if $ioMaskEditorVisible && !$textEditingState}
|
|
<Shortcut
|
|
keyCombination={tool.shortcut}
|
|
on:action={() => {
|
|
tool.action(canvas);
|
|
saveNeededStore.set(true);
|
|
}}
|
|
/>
|
|
{/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();
|
|
}}
|
|
>
|
|
<Icon icon={tool.icon} />
|
|
</IconButton>
|
|
{#if $ioMaskEditorVisible && !$textEditingState}
|
|
<Shortcut
|
|
keyCombination={tool.shortcut}
|
|
on:action={() => {
|
|
tool.action(canvas);
|
|
saveNeededStore.set(true);
|
|
}}
|
|
/>
|
|
{/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;
|
|
}}
|
|
>
|
|
<Icon icon={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();
|
|
}}
|
|
>
|
|
<Icon icon={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;
|
|
}
|
|
|
|
:global(.top-tool-icon-button:active) {
|
|
background: var(--highlight-bg) !important;
|
|
}
|
|
|
|
.dropdown-content {
|
|
display: none;
|
|
position: absolute;
|
|
z-index: 100;
|
|
top: 40px;
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.show {
|
|
display: table;
|
|
}
|
|
|
|
::-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(.fill-mask svg) {
|
|
fill: var(--fill-tool-colour) !important;
|
|
stroke: black;
|
|
stroke-width: 1px;
|
|
}
|
|
|
|
: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>
|