mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-20 00:43:04 +00:00
feat: layout editing (sorta)
This commit is contained in:
@@ -1,167 +1,306 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
||||
import charaActions from "$lib/assets/keymaps/chara-chorder.yml"
|
||||
import mouseActions from "$lib/assets/keymaps/mouse.yml"
|
||||
import keyboardActions from "$lib/assets/keymaps/keyboard.yml"
|
||||
import asciiActions from "$lib/assets/keymaps/ascii.yml"
|
||||
import cp1252Actions from "$lib/assets/keymaps/cp-1252.yml"
|
||||
import FlexSearch from "flexsearch"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||
import Index from "flexsearch"
|
||||
import {createEventDispatcher} from "svelte"
|
||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||
import LL from "../../../i18n/i18n-svelte"
|
||||
|
||||
const index = new FlexSearch({tokenize: "full"})
|
||||
export let currentAction: number
|
||||
|
||||
for (const code in KEYMAP_CODES) {
|
||||
const key = KEYMAP_CODES[code]
|
||||
index.add(
|
||||
code,
|
||||
`${key.id || key.code} ${key.title || ""} ${key.variant || ""} ${key.description || ""}`.trim(),
|
||||
const index = new Index({tokenize: "full"})
|
||||
for (const action of Object.values(KEYMAP_CODES)) {
|
||||
index?.add(
|
||||
action.code,
|
||||
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
||||
action.description || ""
|
||||
}`,
|
||||
)
|
||||
}
|
||||
const exactIndex: Record<string, KeyInfo> = Object.fromEntries(
|
||||
Object.values(KEYMAP_CODES)
|
||||
.filter(it => !!it.id)
|
||||
.map(it => [it.id, it] as const),
|
||||
)
|
||||
|
||||
function search() {
|
||||
const query = searchInput.value
|
||||
customValue = query && !Number.isNaN(Number(query)) ? Number(query) : undefined
|
||||
results = query ? index.search(searchInput.value) : defaultActions
|
||||
results = index!.search(searchBox.value)
|
||||
exact = exactIndex[searchBox.value]?.code
|
||||
code = Number(searchBox.value)
|
||||
}
|
||||
|
||||
let customValue: number | undefined = undefined
|
||||
const defaultActions: string[] = [
|
||||
charaActions,
|
||||
mouseActions,
|
||||
keyboardActions,
|
||||
asciiActions,
|
||||
cp1252Actions,
|
||||
].flatMap(it => Object.keys(it.actions))
|
||||
let results: string[] = defaultActions
|
||||
let searchInput: HTMLInputElement
|
||||
function select(id?: number) {
|
||||
if (id !== undefined) {
|
||||
dispatch("select", id)
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardNavigation(event: KeyboardEvent) {
|
||||
if (event.shiftKey && event.key === "Enter") {
|
||||
dispatch("select", exact)
|
||||
} else if (event.shiftKey && event.key === "Escape") {
|
||||
dispatch("select", 0)
|
||||
} else if (event.key === "Escape") {
|
||||
dispatch("close")
|
||||
} else if (event.key === "ArrowDown") {
|
||||
const element =
|
||||
resultList.querySelector("li:focus-within")?.nextSibling ?? resultList.querySelector("li:not(.exact)")
|
||||
if (element instanceof HTMLLIElement) {
|
||||
element.querySelector("button")?.focus()
|
||||
}
|
||||
} else if (event.key === "ArrowUp") {
|
||||
const element =
|
||||
resultList.querySelector("li:focus-within")?.previousSibling ??
|
||||
resultList.querySelector("li:not(.exact)")
|
||||
if (element instanceof HTMLLIElement) {
|
||||
element.querySelector("button")?.focus()
|
||||
}
|
||||
} else {
|
||||
searchBox.focus()
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
let results: number[] = []
|
||||
let exact: number | undefined = undefined
|
||||
let code: number = Number.NaN
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let searchBox: HTMLInputElement
|
||||
let resultList: HTMLUListElement
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<input type="search" on:input={search} placeholder="Search Actions" bind:this={searchInput} />
|
||||
<svelte:window on:keydown={keyboardNavigation} />
|
||||
|
||||
<div class="results">
|
||||
{#if customValue !== undefined}
|
||||
<button class="custom">
|
||||
Custom ActionID
|
||||
<span class="key">0x{customValue.toString(16).toUpperCase()}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#each results as id}
|
||||
{@const key = KEYMAP_CODES[id]}
|
||||
<button title={key.description}>
|
||||
<div class="title">
|
||||
<b>
|
||||
{key.title || ""}
|
||||
{#if key.variant === "left"}
|
||||
(Left)
|
||||
{:else if key.variant === "right"}
|
||||
(Right)
|
||||
{/if}
|
||||
</b>
|
||||
{#if key.description}
|
||||
<i>{key.description}</i>
|
||||
{/if}
|
||||
</div>
|
||||
<span class:icon={!!key.icon} class="key">{key.icon || key.id || key.code}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<dialog open on:click|self={() => dispatch("close")}>
|
||||
<div class="content">
|
||||
<div class="search-row">
|
||||
<input
|
||||
type="search"
|
||||
bind:this={searchBox}
|
||||
autofocus
|
||||
on:input={search}
|
||||
on:keypress={event => {
|
||||
if (event.key === "Enter") {
|
||||
select(exact)
|
||||
}
|
||||
}}
|
||||
placeholder={$LL.actionSearch.PLACEHOLDER()}
|
||||
/>
|
||||
<button on:click={() => select(0)}
|
||||
><div><span class="icon key-hint">shift</span>+<span class="key-hint">ESC</span></div>
|
||||
{$LL.actionSearch.DELETE()}</button
|
||||
>
|
||||
<button title={$LL.modal.CLOSE()} class="icon" on:click={() => dispatch("close")}>close</button>
|
||||
</div>
|
||||
<aside>
|
||||
<h3>{$LL.actionSearch.CURRENT_ACTION()}</h3>
|
||||
<ActionListItem id={currentAction} />
|
||||
</aside>
|
||||
<ul bind:this={resultList}>
|
||||
{#if exact !== undefined}
|
||||
<li class="exact">
|
||||
<i
|
||||
>Exact match <span class="icon key-hint">shift</span>+<span class="icon key-hint"
|
||||
>keyboard_return</span
|
||||
></i
|
||||
>
|
||||
<ActionListItem id={exact} on:click={() => select(exact)} />
|
||||
</li>
|
||||
{/if}
|
||||
{#if !exact && code}
|
||||
{#if code >= 2 ** 5 && code < 2 ** 13}
|
||||
<li><button on:click={() => select(code)}>USE CODE</button></li>
|
||||
{:else}
|
||||
<li>Action code is out of range</li>
|
||||
{/if}
|
||||
{/if}
|
||||
{#each results as id (id)}
|
||||
<li><ActionListItem {id} on:click={() => select(id)} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</dialog>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
width: calc(min(100vw - 10px, 512px));
|
||||
height: calc(min(90vh, 600px));
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding-inline: 16px;
|
||||
|
||||
font-family: "Noto Sans Mono", monospace;
|
||||
font-size: 18px;
|
||||
color: var(--md-sys-color-on-primary);
|
||||
|
||||
background: var(--md-sys-color-primary);
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
|
||||
&::placeholder {
|
||||
color: inherit;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "plus";
|
||||
}
|
||||
}
|
||||
|
||||
.key {
|
||||
overflow: hidden;
|
||||
dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
|
||||
font-size: 18px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
|
||||
text-align: start;
|
||||
|
||||
> b {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-family: "Noto Sans Mono", monospace;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
|
||||
background: transparent;
|
||||
background: rgba(0 0 0 / 60%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.custom {
|
||||
padding: 8px;
|
||||
padding-inline-start: 16px;
|
||||
aside {
|
||||
pointer-events: none;
|
||||
|
||||
margin: 8px;
|
||||
|
||||
opacity: 0.4;
|
||||
border: 1px dashed var(--md-sys-color-outline);
|
||||
border-radius: 8px;
|
||||
|
||||
> h3 {
|
||||
width: fit-content;
|
||||
margin-block-start: -13px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 16px;
|
||||
padding-inline: 8px;
|
||||
|
||||
background: var(--md-sys-color-background);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-inline: 16px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
margin-inline: 16px;
|
||||
|
||||
> button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: fit-content;
|
||||
|
||||
color: currentcolor;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 100%;
|
||||
|
||||
&:not(.icon) {
|
||||
font-family: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
aspect-ratio: 1;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: calc(min(30cm, 90%));
|
||||
height: calc(min(100% - 128px, 90%));
|
||||
|
||||
color: var(--md-sys-color-on-background);
|
||||
|
||||
background: var(--md-sys-color-background);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.results {
|
||||
overflow-y: scroll;
|
||||
input[type="search"] {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin-block-end: 8px;
|
||||
padding-inline: 16px;
|
||||
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
color: currentcolor;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--md-sys-color-primary-container);
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 1px solid var(--md-sys-color-primary);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
--scrollbar-color: var(--md-sys-color-surface-variant);
|
||||
|
||||
scrollbar-gutter: both-edges stable;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
li {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.exact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-block-start: 8px;
|
||||
|
||||
border: 1px solid var(--md-sys-color-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
> i {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding-inline: 6px;
|
||||
|
||||
color: var(--md-sys-color-on-primary);
|
||||
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.key-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 20px;
|
||||
margin-block: 6px;
|
||||
padding: 2px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: currentcolor;
|
||||
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 4px;
|
||||
|
||||
&.icon {
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<select bind:value={device}>
|
||||
<option value="ONE">CC1</option>
|
||||
<option value="LITE">Lite</option>
|
||||
</select>
|
||||
|
||||
<fieldset>
|
||||
{#each layers as [title, icon, value]}
|
||||
<button
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<div class="col layout" style="gap: 0">
|
||||
<div class="row" style="gap: 156px">
|
||||
<div class="row">
|
||||
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} type="tertiary" />
|
||||
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} />
|
||||
<div class="col">
|
||||
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
|
||||
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} type="secondary" />
|
||||
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} />
|
||||
</div>
|
||||
<div class="col">
|
||||
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
|
||||
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} type="secondary" />
|
||||
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} />
|
||||
</div>
|
||||
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
||||
</div>
|
||||
@@ -41,8 +41,8 @@
|
||||
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
||||
</div>
|
||||
<div class="row" style="gap: 320px; margin-top: -12px">
|
||||
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} type="secondary" />
|
||||
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} type="secondary" />
|
||||
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} />
|
||||
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {highlightActions, layout} from "$lib/serial/connection"
|
||||
import {changes, highlightActions, layout} from "$lib/serial/connection"
|
||||
import type {Change} from "$lib/serial/connection"
|
||||
import type {CharaLayout} from "$lib/serialization/layout"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||
@@ -7,9 +8,6 @@
|
||||
|
||||
export let activeLayer = 0
|
||||
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
|
||||
export let type: "primary" | "secondary" | "tertiary" = "primary"
|
||||
|
||||
const layerNames = ["Primary Layer", "Number Layer", "Function Layer"]
|
||||
|
||||
const virtualLayerMap = [1, 0, 2]
|
||||
const characterOffset = 8
|
||||
@@ -20,26 +18,32 @@
|
||||
return 25 * quadrant + layerOffsetIndex * layerOffset
|
||||
}
|
||||
|
||||
function getActions(id: number, layout: CharaLayout): KeyInfo[] {
|
||||
function getActions(id: number, layout: CharaLayout, changes: Change[]): [KeyInfo, KeyInfo | undefined][] {
|
||||
return Array.from({length: 3}).map((_, i) => {
|
||||
const actionId = layout?.[i][id]
|
||||
return KEYMAP_CODES[actionId]
|
||||
const changedId = changes.findLast(it => it?.layout?.[i]?.[id] !== undefined)?.layout![i]![id]
|
||||
if (changedId !== undefined) {
|
||||
return [KEYMAP_CODES[changedId], KEYMAP_CODES[actionId]]
|
||||
} else {
|
||||
return [KEYMAP_CODES[actionId], undefined]
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="radial {type}">
|
||||
<div class="radial">
|
||||
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
||||
{@const actions = getActions(id, $layout)}
|
||||
{@const actions = getActions(id, $layout, $changes)}
|
||||
<button
|
||||
use:editableLayout={{id, quadrant}}
|
||||
class:active={actions.some(it => it && $highlightActions?.includes(it.code))}
|
||||
use:editableLayout={{activeLayer, id}}
|
||||
class:active={actions.some(([it]) => it && $highlightActions?.includes(it.code))}
|
||||
>
|
||||
{#each actions as keyInfo, layer}
|
||||
{#each actions as [keyInfo, old], layer}
|
||||
{#if keyInfo}
|
||||
<span
|
||||
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
||||
class:icon={!!keyInfo.icon}
|
||||
class:changed={!!old}
|
||||
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
||||
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
||||
>
|
||||
@@ -95,7 +99,9 @@
|
||||
|
||||
opacity: 0.2;
|
||||
|
||||
transition: scale $transition-time ease, opacity $transition-time ease,
|
||||
transition:
|
||||
scale $transition-time ease,
|
||||
opacity $transition-time ease,
|
||||
offset-distance $transition-time ease;
|
||||
|
||||
&.active {
|
||||
@@ -107,6 +113,11 @@
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
&.changed {
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -170,12 +181,4 @@
|
||||
mask-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary > button {
|
||||
filter: brightness(80%) contrast(120%);
|
||||
}
|
||||
|
||||
.tertiary > button {
|
||||
filter: brightness(80%) contrast(110%);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user