mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-18 16:02:57 +00:00
improve cv2
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.2.17",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.2.7",
|
||||
"@lezer/common": "^1.4.0",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.5",
|
||||
|
||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
||||
'@fontsource-variable/noto-sans-mono':
|
||||
specifier: ^5.2.7
|
||||
version: 5.2.7
|
||||
'@lezer/common':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
'@lezer/generator':
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
@@ -1084,8 +1087,8 @@ packages:
|
||||
'@keyv/serialize@1.1.0':
|
||||
resolution: {integrity: sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==}
|
||||
|
||||
'@lezer/common@1.2.1':
|
||||
resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==}
|
||||
'@lezer/common@1.4.0':
|
||||
resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==}
|
||||
|
||||
'@lezer/generator@1.8.0':
|
||||
resolution: {integrity: sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==}
|
||||
@@ -5148,14 +5151,14 @@ snapshots:
|
||||
'@codemirror/language': 6.11.2
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.1
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
|
||||
'@codemirror/commands@6.8.1':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.2
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.1
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
|
||||
'@codemirror/lang-javascript@6.2.4':
|
||||
dependencies:
|
||||
@@ -5164,14 +5167,14 @@ snapshots:
|
||||
'@codemirror/lint': 6.8.1
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.1
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/javascript': 1.4.17
|
||||
|
||||
'@codemirror/language@6.11.2':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.1
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.5
|
||||
style-mod: 4.1.2
|
||||
@@ -5407,26 +5410,26 @@ snapshots:
|
||||
|
||||
'@keyv/serialize@1.1.0': {}
|
||||
|
||||
'@lezer/common@1.2.1': {}
|
||||
'@lezer/common@1.4.0': {}
|
||||
|
||||
'@lezer/generator@1.8.0':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/lr': 1.4.5
|
||||
|
||||
'@lezer/highlight@1.2.1':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
|
||||
'@lezer/javascript@1.4.17':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.5
|
||||
|
||||
'@lezer/lr@1.4.5':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/common': 1.4.0
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
|
||||
54
src/lib/chord-editor/AutospaceSelector.svelte
Normal file
54
src/lib/chord-editor/AutospaceSelector.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { actionTooltip } from "$lib/title";
|
||||
|
||||
let {
|
||||
onchange,
|
||||
value,
|
||||
variant,
|
||||
}: {
|
||||
value: boolean;
|
||||
variant: "start" | "end";
|
||||
onchange: (
|
||||
event: Event & { currentTarget: EventTarget & HTMLInputElement },
|
||||
) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet tooltip()}
|
||||
{#if value}
|
||||
{#if variant === "start"}
|
||||
<b>Remove</b> preceding space
|
||||
{:else}
|
||||
<b>Add</b> trailing space
|
||||
{/if}
|
||||
{:else if variant === "start"}
|
||||
<b>Keep</b> preceding space
|
||||
{:else}
|
||||
<b>Add</b> trailing space
|
||||
{/if}
|
||||
{/snippet}
|
||||
<label class="autospace" {@attach actionTooltip(tooltip)}
|
||||
><span class="icon">space_bar</span><input
|
||||
checked={!value}
|
||||
{onchange}
|
||||
type="checkbox"
|
||||
/></label
|
||||
>
|
||||
|
||||
<style lang="scss">
|
||||
label.autospace {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
margin-inline: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--md-sys-color-tertiary-container);
|
||||
padding-inline: 0;
|
||||
height: 1em;
|
||||
color: var(--md-sys-color-on-tertiary-container);
|
||||
font-size: 1.3em;
|
||||
|
||||
&:has(:checked) {
|
||||
opacity: var(--auto-space-show, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,19 +11,12 @@ import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
|
||||
export class ActionWidget extends WidgetType {
|
||||
component: {};
|
||||
element: HTMLElement;
|
||||
component?: {};
|
||||
element?: HTMLElement;
|
||||
|
||||
constructor(readonly id: string | number) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.element = document.createElement("span");
|
||||
this.element.style.paddingInline = "2px";
|
||||
|
||||
this.component = mount(Action, {
|
||||
target: this.element,
|
||||
props: { action: id, display: "keys" },
|
||||
});
|
||||
}
|
||||
|
||||
override eq(other: ActionWidget) {
|
||||
@@ -31,6 +24,15 @@ export class ActionWidget extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
if (!this.element) {
|
||||
this.element = document.createElement("span");
|
||||
this.element.style.paddingInline = "2px";
|
||||
|
||||
this.component = mount(Action, {
|
||||
target: this.element,
|
||||
props: { action: this.id, display: "keys", inText: true },
|
||||
});
|
||||
}
|
||||
return this.element;
|
||||
}
|
||||
|
||||
@@ -39,7 +41,9 @@ export class ActionWidget extends WidgetType {
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
unmount(this.component);
|
||||
if (this.component) {
|
||||
unmount(this.component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,29 +7,71 @@ import {
|
||||
} from "@codemirror/view";
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
import { mount, unmount } from "svelte";
|
||||
import Action from "../components/Action.svelte";
|
||||
import type { SyntaxNodeRef } from "@lezer/common";
|
||||
import classNames from "./concatenator-button.module.scss";
|
||||
|
||||
export class DelimWidget extends WidgetType {
|
||||
constructor() {
|
||||
component?: {};
|
||||
element?: HTMLElement;
|
||||
|
||||
constructor(readonly hasConcatenator: boolean) {
|
||||
super();
|
||||
}
|
||||
|
||||
override eq(other: DelimWidget) {
|
||||
return true;
|
||||
return this.hasConcatenator == other.hasConcatenator;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const element = document.createElement("span");
|
||||
element.innerHTML = " ⇛ ";
|
||||
element.style.scale = "1.8";
|
||||
element.style.opacity = "0.5";
|
||||
return element;
|
||||
if (!this.element) {
|
||||
this.element = document.createElement("span");
|
||||
this.element.innerHTML =
|
||||
" ⇛" + (this.hasConcatenator ? "" : " ");
|
||||
this.element.style.scale = "1.8";
|
||||
this.element.style.color =
|
||||
"color-mix(in srgb, currentColor 50%, transparent)";
|
||||
|
||||
if (this.hasConcatenator) {
|
||||
const button = document.createElement("button");
|
||||
button.className = classNames["concatenator-button"]!;
|
||||
this.component = mount(Action, {
|
||||
target: button,
|
||||
props: { action: 574, display: "keys", inText: true, ghost: true },
|
||||
});
|
||||
this.element.appendChild(button);
|
||||
}
|
||||
}
|
||||
return this.element;
|
||||
}
|
||||
|
||||
override ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
override destroy() {}
|
||||
override destroy() {
|
||||
if (this.component) {
|
||||
unmount(this.component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getJoinNode(
|
||||
view: EditorView,
|
||||
phraseDelimNode: SyntaxNodeRef,
|
||||
): SyntaxNodeRef | null | undefined {
|
||||
const firstPhraseAction = phraseDelimNode.node.nextSibling
|
||||
?.getChild("ActionString")
|
||||
?.node.firstChild?.node.getChild("ExplicitAction");
|
||||
const idNode = firstPhraseAction?.node.getChild("ActionId");
|
||||
const actionId = idNode
|
||||
? view.state.doc.sliceString(idNode.from, idNode.to)
|
||||
: null;
|
||||
const isJoinAction =
|
||||
actionId === "JOIN" &&
|
||||
!!firstPhraseAction!.node.getChild("ExplicitDelimEnd");
|
||||
return isJoinAction ? firstPhraseAction : null;
|
||||
}
|
||||
|
||||
function actionWidgets(view: EditorView) {
|
||||
@@ -40,8 +82,10 @@ function actionWidgets(view: EditorView) {
|
||||
to,
|
||||
enter: (node) => {
|
||||
if (node.name !== "PhraseDelim") return;
|
||||
const joinNode = getJoinNode(view, node);
|
||||
|
||||
let deco = Decoration.replace({
|
||||
widget: new DelimWidget(),
|
||||
widget: new DelimWidget(!joinNode),
|
||||
});
|
||||
widgets.push(deco.range(node.from, node.to));
|
||||
},
|
||||
@@ -76,5 +120,38 @@ export const delimPlugin = ViewPlugin.fromClass(
|
||||
(view) => view.plugin(plugin)?.decorations ?? Decoration.none,
|
||||
);
|
||||
},
|
||||
eventHandlers: {
|
||||
click: (event, view) => {
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
if (
|
||||
!(
|
||||
event.target instanceof HTMLButtonElement ||
|
||||
(event.target as HTMLElement).parentElement instanceof
|
||||
HTMLButtonElement
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
const chordNode = syntaxTree(view.state).resolve(
|
||||
view.posAtDOM(event.target),
|
||||
);
|
||||
const delimNode = (
|
||||
chordNode.name === "ActionString"
|
||||
? chordNode.parent?.parent
|
||||
: chordNode
|
||||
)?.getChild("PhraseDelim");
|
||||
if (!delimNode) return;
|
||||
const joinNode = getJoinNode(view, delimNode);
|
||||
if (!event.target.checked && !joinNode) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: delimNode.to,
|
||||
insert: "<JOIN>",
|
||||
},
|
||||
selection: { anchor: delimNode.to + "<JOIN>".length },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
13
src/lib/chord-editor/concatenator-button.module.scss
Normal file
13
src/lib/chord-editor/concatenator-button.module.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.concatenator-button {
|
||||
display: inline;
|
||||
opacity: calc(var(--auto-space-show, 0) * 0.7);
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
height: auto;
|
||||
|
||||
> :global(kbd) {
|
||||
outline: 1px dashed var(--md-sys-color-outline);
|
||||
outline-offset: -1px;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,17 @@
|
||||
import { KEYMAP_CODES, KEYMAP_IDS } from "$lib/serial/keymap-codes";
|
||||
import type { KeyInfo } from "$lib/serial/keymap-codes";
|
||||
import { osLayout } from "$lib/os-layout";
|
||||
import { tooltip } from "$lib/hover-popover";
|
||||
import { isVerbose } from "./verbose-action";
|
||||
import { actionTooltip } from "$lib/title";
|
||||
|
||||
let {
|
||||
action,
|
||||
display,
|
||||
inText = false,
|
||||
}: {
|
||||
action: string | number | KeyInfo;
|
||||
display: "inline-text" | "inline-keys" | "keys" | "verbose";
|
||||
display: "inline-keys" | "keys" | "verbose";
|
||||
inText?: boolean;
|
||||
} = $props();
|
||||
|
||||
let retrievedInfo = $derived(
|
||||
@@ -69,6 +70,7 @@
|
||||
{/snippet}
|
||||
{#snippet kbdSnippet(withPopover = true)}
|
||||
<kbd
|
||||
class:in-text={inText}
|
||||
class:icon={!!info.icon}
|
||||
class:left={info.variant === "left"}
|
||||
class:right={info.variant === "right"}
|
||||
@@ -83,7 +85,7 @@
|
||||
{#if !info.icon && dynamicMapping?.length === 1}
|
||||
<span
|
||||
{@attach hasPopover ? actionTooltip(popover) : null}
|
||||
class:in-text={display === "inline-text"}
|
||||
class:in-text={inText}
|
||||
class:error={info.code > 1023}
|
||||
class:warn={!retrievedInfo}
|
||||
class:left={info.variant === "left"}
|
||||
@@ -92,7 +94,7 @@
|
||||
{:else if !info.icon && info.id?.length === 1}
|
||||
<span
|
||||
{@attach hasPopover ? actionTooltip(popover) : null}
|
||||
class:in-text={display === "inline-text"}
|
||||
class:in-text={inText}
|
||||
class:error={info.code > 1023}
|
||||
class:warn={!retrievedInfo}
|
||||
class:left={info.variant === "left"}
|
||||
@@ -101,7 +103,7 @@
|
||||
{:else}
|
||||
<kbd
|
||||
class="inline-kbd"
|
||||
class:in-text={display === "inline-text"}
|
||||
class:in-text={inText}
|
||||
class:left={info.variant === "left"}
|
||||
class:right={info.variant === "right"}
|
||||
class:icon={!!info.icon}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { action, actionTooltip } from "$lib/title";
|
||||
import semverGte from "semver/functions/gte";
|
||||
import Action from "$lib/components/Action.svelte";
|
||||
import AutospaceSelector from "$lib/chord-editor/AutospaceSelector.svelte";
|
||||
|
||||
let { chord }: { chord: ChordInfo } = $props();
|
||||
|
||||
@@ -208,36 +209,27 @@
|
||||
}}
|
||||
>
|
||||
{#if supportsAutospace}
|
||||
{#snippet tooltip()}
|
||||
{#if chord.phrase[0] === JOIN_ACTION}
|
||||
<b>Remove</b> preceding space
|
||||
{:else}
|
||||
<b>Keep</b> preceding space
|
||||
{/if}
|
||||
{/snippet}
|
||||
<label class="auto-space-edit" {@attach actionTooltip(tooltip)}
|
||||
><span class="icon">space_bar</span><input
|
||||
checked={chord.phrase[0] !== JOIN_ACTION}
|
||||
onchange={async (event) => {
|
||||
const autospace = hasAutospace;
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
if (chord.phrase[0] === JOIN_ACTION) {
|
||||
deleteAction(0, 1);
|
||||
await tick();
|
||||
moveCursor(cursorPosition - 1, true);
|
||||
}
|
||||
} else {
|
||||
if (chord.phrase[0] !== JOIN_ACTION) {
|
||||
insertAction(0, JOIN_ACTION);
|
||||
moveCursor(cursorPosition + 1, true);
|
||||
}
|
||||
<AutospaceSelector
|
||||
variant="start"
|
||||
value={chord.phrase[0] === JOIN_ACTION}
|
||||
onchange={async (event) => {
|
||||
const autospace = hasAutospace;
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
if (chord.phrase[0] === JOIN_ACTION) {
|
||||
deleteAction(0, 1);
|
||||
await tick();
|
||||
moveCursor(cursorPosition - 1, true);
|
||||
}
|
||||
await tick();
|
||||
resolveAutospace(autospace);
|
||||
}}
|
||||
type="checkbox"
|
||||
/></label
|
||||
>
|
||||
} else {
|
||||
if (chord.phrase[0] !== JOIN_ACTION) {
|
||||
insertAction(0, JOIN_ACTION);
|
||||
moveCursor(cursorPosition + 1, true);
|
||||
}
|
||||
}
|
||||
await tick();
|
||||
resolveAutospace(autospace);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div
|
||||
onkeydown={keypress}
|
||||
@@ -268,21 +260,12 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#if supportsAutospace}
|
||||
{#snippet tooltip()}
|
||||
{#if hasAutospace}
|
||||
<b>Add</b> trailing space
|
||||
{:else}
|
||||
<b>Don't add</b> trailing space
|
||||
{/if}
|
||||
{/snippet}
|
||||
<label class="auto-space-edit" {@attach actionTooltip(tooltip)}
|
||||
><span class="icon">space_bar</span><input
|
||||
checked={hasAutospace}
|
||||
onchange={(event) =>
|
||||
resolveAutospace((event.target as HTMLInputElement).checked)}
|
||||
type="checkbox"
|
||||
/></label
|
||||
>
|
||||
<AutospaceSelector
|
||||
variant="end"
|
||||
value={!hasAutospace}
|
||||
onchange={(event) =>
|
||||
resolveAutospace((event.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
{/if}
|
||||
<sup>•</sup>
|
||||
</div>
|
||||
@@ -330,24 +313,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.auto-space-edit {
|
||||
margin-inline: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--md-sys-color-tertiary-container);
|
||||
padding-inline: 0;
|
||||
height: 1em;
|
||||
color: var(--md-sys-color-on-tertiary-container);
|
||||
font-size: 1.3em;
|
||||
|
||||
&:has(:checked) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper:hover .auto-space-edit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
|
||||
@@ -380,8 +345,12 @@
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 0.3;
|
||||
&:hover {
|
||||
--auto-space-show: 1;
|
||||
|
||||
&::before {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(> :focus-within)::after {
|
||||
|
||||
@@ -26,15 +26,16 @@
|
||||
const showEdits = persistentWritable("chord-editor-show-edits", true);
|
||||
let originalDoc = $derived(
|
||||
$chords
|
||||
.map(
|
||||
(chord) =>
|
||||
.map((chord) => {
|
||||
return (
|
||||
chord.actions
|
||||
.filter((it) => it !== 0)
|
||||
.map((it) => actionToValue(it))
|
||||
.join("") +
|
||||
"=>" +
|
||||
chord.phrase.map((it) => actionToValue(it)).join(""),
|
||||
)
|
||||
chord.phrase.map((it) => actionToValue(it)).join("")
|
||||
);
|
||||
})
|
||||
.join("\n"),
|
||||
);
|
||||
let editor: HTMLDivElement | undefined = $state(undefined);
|
||||
@@ -175,6 +176,11 @@
|
||||
) !important;
|
||||
}
|
||||
|
||||
:global(.cm-activeLine),
|
||||
:global(.cm-line:hover) {
|
||||
--auto-space-show: 1;
|
||||
}
|
||||
|
||||
:global(.cm-activeLine) {
|
||||
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
||||
/*background-color: color-mix(
|
||||
|
||||
Reference in New Issue
Block a user