import type { Ref } from "vue"; import type { ProjectFile } from "../../domain/project"; import type { KeyNode, Node } from "../../domain/phy"; import { moveNode, resizeKey, rotateNode } from "../../domain/phyCommands"; import { nextGrid, resolveKey, tabOrder } from "./logic"; type Options = { project: () => ProjectFile; mismatch: () => boolean; pane: Ref; selected: Ref; focus: Ref; grid: Ref; zoom: Ref; showHelp: Ref; popover: Ref; confirm: Ref; spaceHeld: Ref; variants: Ref>; priorChild: Map; node: (id: string) => Node | undefined; allNodes: (roots?: Node[]) => Node[]; parentId: (id: string) => string | null; selectedContainer: (id: string) => string | null; insertionParent: () => string | null; nodesInContainer: (container: string | null) => Node[]; directChildren: (id: string) => Node[]; tabItem: (node: Node) => { id: string; x: number; y: number }; dispatch: (label: string, update: (project: ProjectFile) => ProjectFile) => void; fit: () => void; fitSelected: () => void; focusIn: (id: string) => void; makeCluster: () => void; ungroup: () => void; duplicate: () => void; keyAdd: () => void; askDelete: () => void; deleteConfirmed: () => void; chooseVariant: (group: Node, id: string) => void; }; export function usePhyKeyboard(options: Options) { return function keyHandler(event: KeyboardEvent) { const target = event.target as HTMLElement | null; if ( !options.pane.value?.contains(document.activeElement) || target?.closest("input,select,textarea,[contenteditable=true]") ) { return; } if (event.code === "Space") { options.spaceHeld.value = true; event.preventDefault(); return; } const action = resolveKey(event); const control = event.ctrlKey || event.metaKey; if (["undo", "redo", "save"].includes(action || "")) return; if (action === "?" || event.key === "?") { options.showHelp.value = true; event.preventDefault(); return; } if (event.key === "Escape") { if (options.showHelp.value || options.popover.value || options.confirm.value) { options.showHelp.value = false; options.popover.value = null; options.confirm.value = null; } else if (options.selected.value.length) { options.selected.value = []; } else if (options.focus.value) { options.focus.value = options.parentId(options.focus.value); } event.preventDefault(); return; } if (event.key === "Enter" && options.confirm.value) { options.deleteConfirmed(); event.preventDefault(); return; } if ( event.key === "Enter" && options.selected.value.length === 1 && ["cluster", "variantGroup"].includes(options.node(options.selected.value[0]!)?.type || "") ) { options.focusIn(options.selected.value[0]!); return; } if (action === "fit") { options.fit(); return; } if (action === "selection-fit") { options.fitSelected(); return; } if (options.mismatch()) return; if (action === "group" && control) { event.preventDefault(); options.makeCluster(); return; } if (action === "ungroup" && control) { event.preventDefault(); options.ungroup(); return; } if (action === "d" && control) { event.preventDefault(); options.duplicate(); return; } if (action === "a" && control) { event.preventDefault(); const container = options.selected.value.length ? options.selectedContainer(options.selected.value[0]!) : options.insertionParent(); options.selected.value = options.nodesInContainer(container).map((node) => node.id); return; } if (event.key === "Insert") { event.preventDefault(); options.keyAdd(); return; } if (event.key === "Delete" || event.key === "Backspace") { event.preventDefault(); options.askDelete(); return; } if (event.key === "g" || event.key === "G") { options.grid.value = nextGrid(options.grid.value, event.shiftKey ? -1 : 1); return; } if (event.key === "+" || event.key === "=") { options.zoom.value *= 1.15; return; } if (event.key === "-") { options.zoom.value /= 1.15; return; } if (event.key === "Tab") { const container = options.selected.value.length ? options.selectedContainer(options.selected.value[0]!) : options.insertionParent(); const siblings = options.nodesInContainer(container); const ordered = tabOrder(siblings.map(options.tabItem)); const index = ordered.findIndex((node) => node.id === options.selected.value[0]); const next = (index + (event.shiftKey ? -1 : 1) + ordered.length) % ordered.length; if (ordered[next]) options.selected.value = [ordered[next]!.id]; event.preventDefault(); return; } if (event.altKey && event.key === "ArrowUp") { const id = options.selected.value[0]; const parent = id ? options.parentId(id) : null; if (id && parent) { options.priorChild.set(parent, id); options.selected.value = [parent]; } event.preventDefault(); return; } if (event.altKey && event.key === "ArrowDown") { const id = options.selected.value[0]; const children = id ? options.directChildren(id) : []; const back = id ? options.priorChild.get(id) : undefined; const next = children.some((child) => child.id === back) ? back : tabOrder(children.map(options.tabItem))[0]?.id; if (next) options.selected.value = [next]; event.preventDefault(); return; } if (event.key.startsWith("Arrow")) { const ids = options.selected.value; if (!ids.length) return; event.preventDefault(); let phy = options.project().phy; for (const id of ids) { const node = options.node(id); if (!node) continue; if (event.shiftKey) { if (node.type !== "key") continue; const size = { ...node.size }; if (event.key === "ArrowRight") size.w = Math.max(0.25, size.w + 0.25); if (event.key === "ArrowDown") size.h = Math.max(0.25, size.h + 0.25); if (event.key === "ArrowLeft") size.w = Math.max(0.25, size.w - 0.25); if (event.key === "ArrowUp") size.h = Math.max(0.25, size.h - 0.25); phy = resizeKey(phy, id, size); } else { const delta = event.key === "ArrowLeft" ? [-options.grid.value, 0] : event.key === "ArrowRight" ? [options.grid.value, 0] : event.key === "ArrowUp" ? [0, -options.grid.value] : [0, options.grid.value]; phy = moveNode(phy, id, delta[0]!, delta[1]!); } } options.dispatch(event.shiftKey ? "サイズ" : "移動", (project) => ({ ...project, phy })); return; } if (event.key === "PageUp" || event.key === "PageDown") { const ids = options.selected.value; if (ids.length) { const delta = event.key === "PageUp" ? 15 : -15; options.dispatch("回転", (project) => ({ ...project, phy: ids.reduce((phy, id) => rotateNode(phy, id, delta), project.phy), })); } return; } if (event.key.toLowerCase() === "r") { const ids = options.selected.value.filter((id) => options.node(id)?.type === "key"); if (ids.length) { const delta = event.shiftKey ? -90 : 90; options.dispatch("向き変更", (project) => { const copy = structuredClone(project); for (const id of ids) { const node = options.allNodes(copy.phy.root).find((item) => item.id === id); if (node?.type === "key") { node.orientation = (((node.orientation || 0) + delta + 360) % 360) as KeyNode["orientation"]; } } return copy; }); } return; } if (event.key.toLowerCase() === "v" && options.selected.value.length === 1) { const group = options.node(options.selected.value[0]!); if (group?.type === "variantGroup") { const index = group.children.findIndex( (variant) => variant.id === (options.variants.value[group.id] || group.default), ); const variant = group.children[(index + (event.shiftKey ? -1 : 1) + group.children.length) % group.children.length]; if (variant) options.chooseVariant(group, variant.id); } return; } if (event.key === "F2") { (document.querySelector(".properties input") as HTMLInputElement | null)?.focus(); event.preventDefault(); } }; }