feat(설정 단축키): CAD 리본 묶음 · 찾기 · 다른 곳 = 실제 동작 등록부 표 · 키 바꾸기 · 겹침 막음 (PLAN 588-1~3 · 요구 445 C2 · C3)
- 새 key_registry.ts — 화면마다 받는 키 · 마우스 동작 한 벌 · keyIs(evt, id) · 바꾼 키만 input-prefs.keys - ☰ [설정] CAD 탭 = 찾기 칸 + 「키 · 마우스」 + 리본 탭 › 패널 접이 묶음(기타 여섯) · 날 input → 부품 입력 - 다른 곳 탭 = 손글 안내 지우고 등록부 표(B05 방향키 · D03 Delete 등 빠진 것 포함) · 찾기 - 키 기록 칸 · 줄마다 [기본값] · [모두 기본값] · 같은 화면 · 모든 화면 겹침 · 브라우저 몫 · CAD 글자 단독은 저장 막음 - 배선: B05 되돌림 · 자유지정 Enter · Backspace · D03 되돌림 · 그리기 · 지움(한글 자판도 자판 자리) · CAD F3 · F7 · F8 · F10 · Ctrl 명령 · Delete - 낡은 글 대조 시험 셋(215-1 · 345-3 · 346-4)은 새 배선 글로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,3 +28,33 @@
|
||||
top: 0;
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
/* 588 — 찾기 줄 · 묶음 사이 · 키 배지 줄 */
|
||||
.app-settings__findbar {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
align-items: flex-start;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding-bottom: var(--spacing-8);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.app-settings__findbar > .ui-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-settings__group + .app-settings__group {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.app-settings__group[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-settings__keys {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
+62
-30
@@ -1,9 +1,10 @@
|
||||
/* =============================================================================
|
||||
* app_settings.ts
|
||||
* ☰ [설정] 창(PLAN 332-2 · 요구 224-8) — 탭 셋: ① 마우스 ② CAD 단축키 ③ 다른 곳 단축키(읽기 전용).
|
||||
* ☰ [설정] 창(PLAN 332-2 · 588 · 요구 224-8 · 445) — 탭 셋: ① 마우스(휠 방향 · 화면 옮기기 단추) ② CAD 단축키 ③ 다른 곳 단축키.
|
||||
*
|
||||
* - 고침은 창 안 사본에만 · [저장] 때만 계정 pref(`input-prefs`)로 한 번 씀 — 읽기는 `input_prefs.ts`
|
||||
* - 저장 안 한 고침이 있으면 닫을 때 물음(askLeave) · 잘못 있는 별칭은 저장 안 함
|
||||
* - 저장 안 한 고침이 있으면 닫을 때 물음(askLeave) · 잘못 있는 별칭 · 겹친 키는 저장 안 함
|
||||
* - ② · ③ 표 = 단축키 등록부(`key_registry.ts`) — 바꾼 키만 `keys` 에
|
||||
* - 로그인 전엔 이 브라우저에만 남음(계정 올리기는 `b_page_state` 가 로그인 뒤에)
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -15,9 +16,11 @@ import { askLeave } from "@ui/ui_template_leave_dialog";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { writeState } from "./b_page_state";
|
||||
import { inputPrefs, type InputPrefs, type WheelZoom } from "./input_prefs";
|
||||
import { inputPrefs, type InputPrefs, type PanButton, type WheelZoom } from "./input_prefs";
|
||||
import { fillTabs } from "./b_page_tabs";
|
||||
import { buildCadAliasTab, checkCadAliases } from "./app_settings_cad";
|
||||
import { findPanel, keyRows } from "./app_settings_keys";
|
||||
import { KEY_ACTIONS, checkKeys, type KeyScope } from "./key_registry";
|
||||
import "./app_settings.css";
|
||||
|
||||
type LocaleKey = Parameters<typeof L>[0];
|
||||
@@ -34,6 +37,12 @@ const WHEEL: [WheelZoom, LocaleKey][] = [
|
||||
["push", "App_Settings_Wheel_Push"],
|
||||
];
|
||||
|
||||
const PAN: [PanButton, LocaleKey][] = [
|
||||
["left", "App_Settings_Pan_Left"],
|
||||
["middle", "App_Settings_Pan_Middle"],
|
||||
["right", "App_Settings_Pan_Right"],
|
||||
];
|
||||
|
||||
const MOUSE_RULES: LocaleKey[] = [
|
||||
"App_Settings_Rule_Pan",
|
||||
"App_Settings_Rule_Pick",
|
||||
@@ -41,17 +50,10 @@ const MOUSE_RULES: LocaleKey[] = [
|
||||
"App_Settings_Rule_Pinch",
|
||||
];
|
||||
|
||||
/** ③ 안내 — [자리, 키] */
|
||||
const KEY_ROWS: [LocaleKey, LocaleKey][] = [
|
||||
["App_Settings_Keys_All", "App_Settings_Keys_All_Keys"],
|
||||
["App_Settings_Keys_Profile", "App_Settings_Keys_Profile_Keys"],
|
||||
["App_Settings_Keys_RouteEdit", "App_Settings_Keys_RouteEdit_Keys"],
|
||||
["App_Settings_Keys_Plan", "App_Settings_Keys_Plan_Keys"],
|
||||
["App_Settings_Keys_Section", "App_Settings_Keys_Section_Keys"],
|
||||
["App_Settings_Keys_Cad", "App_Settings_Keys_Cad_Keys"],
|
||||
["App_Settings_Keys_Table", "App_Settings_Keys_Table_Keys"],
|
||||
["App_Settings_Keys_M02", "App_Settings_Keys_M02_Keys"],
|
||||
];
|
||||
/** ③ 자리 = CAD 뺀 등록부 자리(CAD 는 ② 탭 「키 · 마우스」) */
|
||||
const OTHER_SCOPES = [...new Set(KEY_ACTIONS.map((a) => a.scope))].filter(
|
||||
(scope): scope is KeyScope => scope !== "cad",
|
||||
);
|
||||
|
||||
/** 비교 · 저장 꼴 — 별칭 명령 차례를 맞춤 */
|
||||
const normalize = (p: InputPrefs): InputPrefs => ({
|
||||
@@ -76,46 +78,69 @@ function ruleList(keys: LocaleKey[]): HTMLElement {
|
||||
return createKv(rows, { rule: true });
|
||||
}
|
||||
|
||||
function mouseTab(draft: InputPrefs): HTMLElement {
|
||||
function radios<T extends string>(
|
||||
name: string,
|
||||
label: LocaleKey,
|
||||
options: [T, LocaleKey][],
|
||||
value: T,
|
||||
onChange: (value: T) => void,
|
||||
): HTMLElement {
|
||||
const group = el("div", {
|
||||
className: "app-settings__radios",
|
||||
attrs: { role: "radiogroup", "aria-label": L("App_Settings_Wheel") },
|
||||
attrs: { role: "radiogroup", "aria-label": L(label) },
|
||||
});
|
||||
for (const [mode, key] of WHEEL) {
|
||||
for (const [mode, key] of options) {
|
||||
group.append(
|
||||
createCheckbox(L(key), {
|
||||
radio: "app-settings-wheel",
|
||||
radio: name,
|
||||
value: mode,
|
||||
checked: draft.wheelZoom === mode,
|
||||
onChange: () => (draft.wheelZoom = mode),
|
||||
checked: value === mode,
|
||||
onChange: () => onChange(mode),
|
||||
}).root,
|
||||
);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
function mouseTab(draft: InputPrefs): HTMLElement {
|
||||
return el("div", {
|
||||
children: [
|
||||
el("h4", { text: L("App_Settings_Wheel") }),
|
||||
group,
|
||||
radios("app-settings-wheel", "App_Settings_Wheel", WHEEL, draft.wheelZoom, (mode) => {
|
||||
draft.wheelZoom = mode;
|
||||
}),
|
||||
el("h4", { text: L("App_Settings_Pan") }),
|
||||
radios("app-settings-pan", "App_Settings_Pan", PAN, draft.panButton, (mode) => {
|
||||
draft.panButton = mode;
|
||||
}),
|
||||
el("h4", { text: L("App_Settings_Rules") }),
|
||||
ruleList(MOUSE_RULES),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function keysTab(): HTMLElement {
|
||||
return el("div", {
|
||||
children: KEY_ROWS.flatMap(([place, keys]) => [el("h4", { text: L(place) }), ruleList([keys])]),
|
||||
});
|
||||
}
|
||||
|
||||
/** ☰ [설정] — 창을 열고 지금 설정을 사본으로 고침 */
|
||||
export function openAppSettings(): void {
|
||||
const draft = normalize(inputPrefs());
|
||||
let saved = snapshot(draft);
|
||||
const cad = buildCadAliasTab(draft.cadAliases);
|
||||
/** 키를 고칠 때마다 — 두 탭 칸 글 · 겹침 표시 다시 */
|
||||
const keysChanged = (): void => {
|
||||
const errors = checkKeys(draft.keys);
|
||||
for (const rows of [cad.keys, other]) {
|
||||
rows.refresh();
|
||||
rows.mark(errors);
|
||||
}
|
||||
};
|
||||
const cad = buildCadAliasTab(draft.cadAliases, draft.keys, keysChanged);
|
||||
const other = keyRows(OTHER_SCOPES, draft.keys, keysChanged);
|
||||
const keysPanel = findPanel(other.groups, [el("p", { text: L("App_Settings_Key_Help") })], () => {
|
||||
for (const id of Object.keys(draft.keys)) if (!id.startsWith("cad.")) delete draft.keys[id];
|
||||
keysChanged();
|
||||
});
|
||||
const panels: Record<TabId, HTMLElement> = {
|
||||
mouse: mouseTab(draft),
|
||||
cad: cad.root,
|
||||
keys: keysTab(),
|
||||
keys: keysPanel.root,
|
||||
};
|
||||
const tabs = el("div", { className: "app-settings__tabs" });
|
||||
const show = (id: TabId): void => {
|
||||
@@ -129,7 +154,7 @@ export function openAppSettings(): void {
|
||||
for (const [tab, panel] of Object.entries(panels)) panel.hidden = tab !== id;
|
||||
};
|
||||
|
||||
/** [저장] — 잘못 있는 별칭이면 CAD 탭을 보이고 안 씀 */
|
||||
/** [저장] — 잘못 있는 별칭 · 겹친 키면 그 탭을 보이고 안 씀 */
|
||||
const save = (): boolean => {
|
||||
const errors = checkCadAliases(draft.cadAliases);
|
||||
cad.mark(errors);
|
||||
@@ -138,6 +163,13 @@ export function openAppSettings(): void {
|
||||
showToast(L("App_Settings_Cad_Invalid"), "error");
|
||||
return false;
|
||||
}
|
||||
const keyErrors = Object.keys(checkKeys(draft.keys));
|
||||
if (keyErrors.length) {
|
||||
keysChanged();
|
||||
show(keyErrors.some((id) => id.startsWith("cad.")) ? "cad" : "keys");
|
||||
showToast(L("App_Settings_Key_Invalid"), "error");
|
||||
return false;
|
||||
}
|
||||
if (snapshot(draft) !== saved) writeState("input-prefs", normalize(draft));
|
||||
saved = snapshot(draft);
|
||||
showToast(L("App_Settings_Saved"), "success");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* app_settings_cad.ts
|
||||
* ☰ [설정] ② CAD 단축키 탭(PLAN 332-2) — 명령 표(명령 · 한글 이름 · 기본 별칭 · 내 별칭) · 검사.
|
||||
* ☰ [설정] ② CAD 단축키 탭(PLAN 332-2 · 588-1) — 찾기 · 키 · 마우스 묶음 · 리본 이름 묶음 명령 표(명령 · 한글 이름 · 기본 별칭 · 내 별칭) · 검사.
|
||||
*
|
||||
* - 명령 목록 = `openwebcad/src/commands/command-catalog.json`(registry 거울 · commands.*.ts 는 xstate 가 딸려 와서 JSON 한 벌)
|
||||
* - acad.pgp 식 덮어쓰기 — CAD 는 명령 이름 > 내 별칭 > 기본 별칭 순서로 찾음(배선은 332-3)
|
||||
@@ -8,11 +8,12 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import catalog from "../B07_DesignDetail/openwebcad/src/commands/command-catalog.json";
|
||||
import { setUiSize } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { cadCommandGroups } from "./app_settings_find";
|
||||
import { findGroup, findPanel, keyRows, type FindRow, type KeyRows } from "./app_settings_keys";
|
||||
|
||||
export interface CadCommandRow {
|
||||
id: string;
|
||||
@@ -65,20 +66,25 @@ export function checkCadAliases(
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** 탭 몸 — `aliases` 를 칸 입력대로 고쳐 씀 · `mark` 로 잘못 칸 표시 */
|
||||
export function buildCadAliasTab(aliases: Record<string, string[]>): {
|
||||
/** 탭 몸(588-1) — 찾기 · 「키 · 마우스」 묶음(등록부 CAD 줄) · 리본 이름 묶음 · `aliases` · `keys` 를 칸 입력대로 고쳐 씀 */
|
||||
export function buildCadAliasTab(
|
||||
aliases: Record<string, string[]>,
|
||||
keys: Record<string, string[]>,
|
||||
onKeysChange: () => void,
|
||||
): {
|
||||
root: HTMLElement;
|
||||
mark: (errors: Record<string, AliasError>) => void;
|
||||
keys: KeyRows;
|
||||
count: () => number;
|
||||
} {
|
||||
const message = createText("", "error");
|
||||
message.hidden = true;
|
||||
const inputs = new Map<string, HTMLInputElement>();
|
||||
const rows = CAD_COMMANDS.map((command) => {
|
||||
const input = el("input", {
|
||||
className: "ui-input",
|
||||
attrs: { type: "text", "aria-label": `${command.id} ${L("App_Settings_Cad_Mine")}` },
|
||||
});
|
||||
setUiSize(input, 24);
|
||||
const byId = new Map(CAD_COMMANDS.map((command) => [command.id, command]));
|
||||
const row = (command: CadCommandRow): FindRow => {
|
||||
const field = createInputField({ size: 24 });
|
||||
const input = field.input;
|
||||
input.setAttribute("aria-label", `${command.id} ${L("App_Settings_Cad_Mine")}`);
|
||||
input.value = (aliases[command.id] ?? []).join(", ");
|
||||
input.dataset.cadCommand = command.id;
|
||||
input.addEventListener("input", () => {
|
||||
@@ -87,21 +93,30 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
|
||||
else delete aliases[command.id];
|
||||
});
|
||||
inputs.set(command.id, input);
|
||||
return [command.id, command.label, command.aliases.join(", "), input];
|
||||
});
|
||||
const table = createTable(
|
||||
(
|
||||
[
|
||||
"App_Settings_Cad_Command",
|
||||
"App_Settings_Cad_Label",
|
||||
"App_Settings_Cad_Default",
|
||||
"App_Settings_Cad_Mine",
|
||||
] as const
|
||||
).map((key) => ({ label: L(key) })),
|
||||
rows,
|
||||
{ density: "data" },
|
||||
);
|
||||
table.classList.add("app-settings__table");
|
||||
return {
|
||||
cells: [command.id, command.label, command.aliases.join(", "), field.root],
|
||||
texts: () => [command.id, command.label, ...command.aliases, input.value],
|
||||
};
|
||||
};
|
||||
const columns = (
|
||||
[
|
||||
"App_Settings_Cad_Command",
|
||||
"App_Settings_Cad_Label",
|
||||
"App_Settings_Cad_Default",
|
||||
"App_Settings_Cad_Mine",
|
||||
] as const
|
||||
).map((key) => ({ label: L(key) }));
|
||||
const keyRowsCad = keyRows(["cad"], keys, onKeysChange, () => L("App_Settings_Cad_Keys"));
|
||||
const groups = [
|
||||
...keyRowsCad.groups,
|
||||
...cadCommandGroups(CAD_COMMANDS.map((c) => c.id)).map((group) =>
|
||||
findGroup(
|
||||
group.label || L("App_Settings_Cad_Other"),
|
||||
columns,
|
||||
group.ids.map((id) => row(byId.get(id)!)),
|
||||
),
|
||||
),
|
||||
];
|
||||
const mark = (errors: Record<string, AliasError>): void => {
|
||||
for (const [id, input] of inputs) {
|
||||
const error = errors[id];
|
||||
@@ -113,8 +128,18 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
|
||||
message.lastChild!.textContent = kinds.map((k) => L(ERROR_TEXT[k])).join(" · ");
|
||||
message.hidden = !kinds.length;
|
||||
};
|
||||
const root = el("div", {
|
||||
children: [el("p", { text: L("App_Settings_Cad_Help") }), message, table],
|
||||
});
|
||||
return { root, mark };
|
||||
/** [모두 기본값] — 내 별칭 · CAD 바꾼 키 비움 */
|
||||
const resetAll = (): void => {
|
||||
for (const id of Object.keys(aliases)) delete aliases[id];
|
||||
for (const input of inputs.values()) input.value = "";
|
||||
for (const id of Object.keys(keys)) if (id.startsWith("cad.")) delete keys[id];
|
||||
mark({});
|
||||
onKeysChange();
|
||||
};
|
||||
const panel = findPanel(
|
||||
groups,
|
||||
[el("p", { text: L("App_Settings_Cad_Help") }), message],
|
||||
resetAll,
|
||||
);
|
||||
return { root: panel.root, mark, keys: keyRowsCad, count: panel.count };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/* =============================================================================
|
||||
* app_settings_find.ts
|
||||
* ☰ [설정] 단축키 탭 셈(PLAN 588-1) — CAD 명령 묶음 · 찾기 판정. 화면 · 저장소 안 씀.
|
||||
*
|
||||
* - 묶음 = 리본 탭 › 패널 이름 그대로(`openwebcad/src/ribbon/ribbon.config.ts` · 순수 자료) · 두 패널에 있으면 앞 패널 · 리본에 없는 명령 = 기타
|
||||
* - 찾기 = 띄어 나눈 낱말이 모두 어느 글에든 들면 맞음 · 대소문자 무시
|
||||
* 시험 = `resources/tester/test_588_key_registry.py`
|
||||
* ========================================================================== */
|
||||
|
||||
import { RIBBON_TABS } from "../B07_DesignDetail/openwebcad/src/ribbon/ribbon.config";
|
||||
|
||||
export interface CadGroup {
|
||||
/** 「홈 › 그리기」 · 기타는 빈 글 */
|
||||
label: string;
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
/** 명령 id 들 → 리본 묶음(리본 차례) + 끝에 기타(`label` 빈 글) */
|
||||
export function cadCommandGroups(ids: string[]): CadGroup[] {
|
||||
const known = new Set(ids);
|
||||
const seen = new Set<string>();
|
||||
const groups: CadGroup[] = [];
|
||||
for (const tab of RIBBON_TABS) {
|
||||
for (const panel of tab.panels) {
|
||||
const mine = [...(panel.big ?? []), ...(panel.commands ?? []), ...(panel.overflow ?? [])]
|
||||
.filter((id) => known.has(id) && !seen.has(id))
|
||||
.filter((id) => seen.add(id));
|
||||
if (mine.length) groups.push({ label: `${tab.label} › ${panel.label}`, ids: mine });
|
||||
}
|
||||
}
|
||||
const rest = ids.filter((id) => !seen.has(id));
|
||||
if (rest.length) groups.push({ label: "", ids: rest });
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** 찾기 — 빈 찾기 = 맞음 · 낱말마다 어느 글에든 들어야 */
|
||||
export function matchesFind(query: string, texts: string[]): boolean {
|
||||
const hay = texts.join("\n").toLowerCase();
|
||||
return query
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.every((word) => hay.includes(word));
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/* =============================================================================
|
||||
* app_settings_keys.ts
|
||||
* ☰ [설정] 단축키 표(PLAN 588) — 접이 묶음 · 찾기 · 키 기록 칸 · 줄마다 [기본값]. CAD 탭 · 다른 곳 탭이 같이 씀.
|
||||
*
|
||||
* - 표 줄 = 등록부(`key_registry.ts`) — 손글 안내 없음 · 못 바꾸는 줄은 키 배지만
|
||||
* - 키 기록 = 칸을 누르고 새 키를 누름(Tab = 초점 넘김 · Esc = 그만) · 기본과 같으면 바꾼 키에서 뺌
|
||||
* - 고침은 창 안 사본(`keys`)에만 · [저장] 은 `app_settings.ts`
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { attachCollapsible, createCollapsibleHead } from "@ui/ui_template_collapsible";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { currentLanguageIndex, t as L } from "@ui/ui_template_locale";
|
||||
import { tableRow, tableShell, type Cell, type Column } from "@ui/ui_template_table";
|
||||
import { matchesFind } from "./app_settings_find";
|
||||
import {
|
||||
KEY_ACTIONS,
|
||||
KEY_SCOPES,
|
||||
actionKeys,
|
||||
chordOf,
|
||||
keyLabel,
|
||||
type KeyError,
|
||||
type KeyScope,
|
||||
} from "./key_registry";
|
||||
|
||||
type LocaleKey = Parameters<typeof L>[0];
|
||||
|
||||
const KEY_ERROR_TEXT: Record<KeyError, LocaleKey> = {
|
||||
dup: "App_Settings_Key_ErrDup",
|
||||
reserved: "App_Settings_Key_ErrReserved",
|
||||
letter: "App_Settings_Key_ErrLetter",
|
||||
};
|
||||
|
||||
export interface FindRow {
|
||||
cells: Cell[];
|
||||
/** 찾기에 쓰는 글 — 찾을 때마다 읽음(칸에 친 글도) */
|
||||
texts: () => string[];
|
||||
}
|
||||
|
||||
export interface FindGroup {
|
||||
root: HTMLElement;
|
||||
/** 맞는 줄 수 */
|
||||
filter: (query: string) => number;
|
||||
}
|
||||
|
||||
/** 접이 묶음 하나 — 머리(이름 · 줄 수 배지) + 표 · 처음엔 접힘 · 찾는 동안 맞는 줄만 펼침 */
|
||||
export function findGroup(title: string, columns: Column[], rows: FindRow[]): FindGroup {
|
||||
const badge = createBadge(String(rows.length), "neutral", { kind: "count" });
|
||||
const head = createCollapsibleHead({ title, actions: [badge], level: 4 });
|
||||
const { root: table, tbody } = tableShell(columns, { density: "data" });
|
||||
table.classList.add("app-settings__table");
|
||||
const trs = rows.map((row) => tableRow(columns, row.cells));
|
||||
tbody.append(...trs);
|
||||
const root = el("section", {
|
||||
className: "ui-collapsible is-collapsed app-settings__group",
|
||||
children: [head.root, table],
|
||||
});
|
||||
const filter = (query: string): number => {
|
||||
let shown = 0;
|
||||
rows.forEach((row, i) => {
|
||||
const hit = matchesFind(query, row.texts());
|
||||
trs[i].hidden = !hit;
|
||||
if (hit) shown += 1;
|
||||
});
|
||||
badge.textContent = String(shown);
|
||||
root.hidden = !shown;
|
||||
root.classList.toggle("is-collapsed", !query.trim());
|
||||
return shown;
|
||||
};
|
||||
return { root, filter };
|
||||
}
|
||||
|
||||
/** 찾기 칸 + [모두 기본값] + 묶음들 + 0건 안내 — `count` = 맞는 줄 수(시험 · 검증) */
|
||||
export function findPanel(
|
||||
groups: FindGroup[],
|
||||
head: HTMLElement[],
|
||||
onResetAll: () => void,
|
||||
): { root: HTMLElement; count: () => number } {
|
||||
const empty = createEmpty({ text: L("App_Settings_Find_Empty"), small: true });
|
||||
empty.hidden = true;
|
||||
let count = 0;
|
||||
const find = createInputField({
|
||||
type: "search",
|
||||
size: 28,
|
||||
placeholder: L("App_Settings_Find"),
|
||||
onInput: (query) => {
|
||||
count = groups.reduce((sum, group) => sum + group.filter(query), 0);
|
||||
empty.hidden = count > 0;
|
||||
},
|
||||
});
|
||||
find.input.setAttribute("aria-label", L("App_Settings_Find"));
|
||||
find.input.dataset.settingsFind = "";
|
||||
const reset = createButton({
|
||||
label: L("App_Settings_Reset_All"),
|
||||
variant: "ghost",
|
||||
size: 28,
|
||||
onClick: onResetAll,
|
||||
});
|
||||
const bar = el("div", { className: "app-settings__findbar", children: [find.root, reset] });
|
||||
const root = el("div", { children: [bar, ...head, ...groups.map((g) => g.root), empty] });
|
||||
attachCollapsible(root);
|
||||
count = groups.reduce((sum, group) => sum + group.filter(""), 0);
|
||||
return { root, count: () => count };
|
||||
}
|
||||
|
||||
const text = (pair: readonly [string, string]): string => pair[currentLanguageIndex] ?? pair[0];
|
||||
|
||||
const badges = (keys: readonly string[]): HTMLElement =>
|
||||
el("span", {
|
||||
className: "app-settings__keys",
|
||||
children: keys.map((key) => createBadge(keyLabel(key), "neutral")),
|
||||
});
|
||||
|
||||
export interface KeyRows {
|
||||
groups: FindGroup[];
|
||||
/** 잘못 표시 */
|
||||
mark: (errors: Record<string, KeyError>) => void;
|
||||
/** 사본이 바뀐 뒤 칸 글 · [기본값] 다시 맞춤 */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/** 등록부 자리들의 묶음 — `keys` 사본을 고쳐 씀 · `onChange` = 고칠 때마다(겹침 다시 셈) */
|
||||
export function keyRows(
|
||||
scopes: KeyScope[],
|
||||
keys: Record<string, string[]>,
|
||||
onChange: () => void,
|
||||
titleOf: (scope: KeyScope) => string = (scope) => text(KEY_SCOPES[scope].label),
|
||||
): KeyRows {
|
||||
const columns: Column[] = [
|
||||
{ label: L("App_Settings_Key_Action") },
|
||||
{ label: L("App_Settings_Key_Key"), width: "13rem" },
|
||||
{ label: "", width: "6rem" },
|
||||
];
|
||||
const fields = new Map<string, { set: () => void; setError: (msg?: string) => void }>();
|
||||
const groups = scopes.map((scope) => {
|
||||
const rows = KEY_ACTIONS.filter((a) => a.scope === scope).map((action): FindRow => {
|
||||
const label = text(action.label);
|
||||
const texts = [label, ...action.keys, ...action.keys.map(keyLabel)];
|
||||
if (action.fixed) return { cells: [label, badges(action.keys), ""], texts: () => texts };
|
||||
const field = createInputField({ size: 24 });
|
||||
const input = field.input;
|
||||
input.readOnly = true;
|
||||
input.dataset.keyAction = action.id;
|
||||
input.setAttribute("aria-label", `${label} ${L("App_Settings_Key_Key")}`);
|
||||
const defaults = action.keys.map(keyLabel).join(" · ");
|
||||
const reset = createButton({
|
||||
label: L("App_Settings_Reset"),
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () => {
|
||||
delete keys[action.id];
|
||||
onChange();
|
||||
},
|
||||
});
|
||||
reset.title = `${L("App_Settings_Reset")}: ${defaults}`;
|
||||
reset.dataset.keyReset = action.id;
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Tab") return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // 창 닫기 · 페이지 키로 안 샘
|
||||
if (event.key === "Escape") return input.blur();
|
||||
const chord = chordOf(event);
|
||||
if (!chord) return;
|
||||
if (action.keys.length === 1 && action.keys[0] === chord) delete keys[action.id];
|
||||
else keys[action.id] = [chord];
|
||||
onChange();
|
||||
});
|
||||
fields.set(action.id, {
|
||||
set: () => {
|
||||
input.value = actionKeys(action.id, keys).map(keyLabel).join(" · ");
|
||||
reset.disabled = !keys[action.id];
|
||||
},
|
||||
setError: field.setError,
|
||||
});
|
||||
return { cells: [label, field.root, reset], texts: () => [...texts, input.value] };
|
||||
});
|
||||
return findGroup(titleOf(scope), columns, rows);
|
||||
});
|
||||
const refresh = (): void => fields.forEach((field) => field.set());
|
||||
const mark = (errors: Record<string, KeyError>): void => {
|
||||
for (const [id, field] of fields) {
|
||||
const error = errors[id];
|
||||
field.setError(error ? L(KEY_ERROR_TEXT[error]) : undefined);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
return { groups, mark, refresh };
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/* =============================================================================
|
||||
* key_registry.ts
|
||||
* 단축키 등록부(PLAN 588-2 · 3) — 화면마다 실제로 받는 키 · 마우스 동작 한 벌 · ☰ [설정] 표가 여기서 그려짐.
|
||||
*
|
||||
* - 줄 = 동작 id · 자리 · 기본 키 · 이름 · `fixed`(못 바꿈 — Esc · 방향키 · 마우스 · 글 입력 · 시트 키)
|
||||
* - 판정 = `keyIs(evt, id)` — 사용자가 바꾼 키(`input-prefs` 의 `keys`) 있으면 그것만 · 없으면 기본 키
|
||||
* - 키 글 = 「Ctrl+Shift+Z」 꼴 — 글자 · 숫자는 자판 자리(`keyLetter` · 한글 자판 안전) · 차례 Ctrl · Alt · Shift · 키
|
||||
* - 겹침 = 같은 화면에서 함께 듣는 자리끼리 · 「모든 화면」과 · 브라우저 몫 · CAD 글자 단독(명령행 몫)
|
||||
* - 불러올 때 window 안 만짐(CAD 도 import) · 시험 = `resources/tester/test_588_key_registry.py`
|
||||
* ========================================================================== */
|
||||
|
||||
import { inputPrefs } from "./input_prefs";
|
||||
import { keyLetter } from "./key_shortcut";
|
||||
|
||||
export type KeyScope =
|
||||
| "all"
|
||||
| "b05"
|
||||
| "profile"
|
||||
| "routeEdit"
|
||||
| "drainage"
|
||||
| "section"
|
||||
| "plan"
|
||||
| "d02"
|
||||
| "b04"
|
||||
| "cad"
|
||||
| "b08"
|
||||
| "b09"
|
||||
| "m02"
|
||||
| "stab"
|
||||
| "etc";
|
||||
|
||||
type Text = readonly [ko: string, en: string];
|
||||
|
||||
export interface KeyAction {
|
||||
id: string;
|
||||
scope: KeyScope;
|
||||
/** 기본 키(키 글) · 못 바꾸는 줄은 보일 글(마우스 · 「글자」 등) */
|
||||
keys: readonly string[];
|
||||
label: Text;
|
||||
fixed?: true;
|
||||
}
|
||||
|
||||
/** 자리 이름 · 함께 듣는 화면(같은 화면이면 키가 겹치면 안 됨) — 표 차례도 이대로 */
|
||||
export const KEY_SCOPES: Record<KeyScope, { label: Text; page: string }> = {
|
||||
all: { label: ["모든 화면", "All views"], page: "all" },
|
||||
b05: { label: ["B05 · B06 공통", "B05 · B06 common"], page: "b05" },
|
||||
profile: { label: ["B05 종단", "B05 profile"], page: "b05" },
|
||||
routeEdit: { label: ["B05 노선 편집(평면)", "B05 route edit (plan)"], page: "b05" },
|
||||
drainage: { label: ["B05 배수 · 3D", "B05 drainage · 3D"], page: "b05" },
|
||||
section: { label: ["B06 횡단", "B06 cross section"], page: "b05" },
|
||||
plan: { label: ["D03 노선분석", "D03 route analysis"], page: "plan" },
|
||||
d02: { label: ["D02 기준지정", "D02 base points"], page: "d02" },
|
||||
b04: { label: ["B04 지도 · 3D", "B04 map · 3D"], page: "b04" },
|
||||
cad: { label: ["CAD", "CAD"], page: "cad" },
|
||||
b08: { label: ["B08 수량 · 이정표", "B08 quantity · milestones"], page: "b08" },
|
||||
b09: { label: ["B09 내역", "B09 estimate"], page: "b09" },
|
||||
m02: { label: ["M02 양식", "M02 templates"], page: "m02" },
|
||||
stab: { label: ["E03 · E04 · E07 안정성", "E03 · E04 · E07 stability"], page: "stab" },
|
||||
etc: { label: ["그 밖", "Other"], page: "etc" },
|
||||
};
|
||||
|
||||
const ARROWS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"] as const;
|
||||
const SHIFT_ARROWS = ARROWS.map((key) => `Shift+${key}`);
|
||||
/** 화면 옮기기 — ☰ [설정] 마우스 탭에서 고른 버튼(가운데는 늘) */
|
||||
const PAN = "옮기기 버튼 끌기";
|
||||
|
||||
const A = (
|
||||
id: string,
|
||||
scope: KeyScope,
|
||||
keys: readonly string[],
|
||||
label: Text,
|
||||
fixed?: true,
|
||||
): KeyAction => (fixed ? { id, scope, keys, label, fixed } : { id, scope, keys, label });
|
||||
const F = true as const;
|
||||
|
||||
/* prettier-ignore */
|
||||
export const KEY_ACTIONS: readonly KeyAction[] = [
|
||||
A("all.escape", "all", ["Escape"], ["☰ · 창 · 메뉴 닫기 · 고름 풀기", "☰ · close window · menu · clear selection"], F),
|
||||
A("all.loupe", "all", ["Z"], ["누른 채 = 돋보기", "Hold = loupe"], F),
|
||||
A("all.loupeRight", "all", ["오른쪽 0.4초 누름"], ["돋보기", "Loupe"], F),
|
||||
A("all.loupeWheel", "all", ["돋보기 중 휠"], ["돋보기 배율", "Loupe zoom"], F),
|
||||
A("all.zoomPan", "all", [PAN], ["그림 칸 화면 옮김(횡단 · 단면 · 미리 보기)", "Pan drawings (sections · previews)"], F),
|
||||
A("all.zoomReset", "all", ["두 번 누름"], ["그림 칸 배율 처음대로", "Reset drawing zoom"], F),
|
||||
A("all.resizer", "all", ["칸 경계 두 번 누름"], ["칸 크기 처음대로", "Reset panel size"], F),
|
||||
A("all.grip", "all", ["ArrowLeft", "ArrowRight", "Shift+ArrowLeft", "Shift+ArrowRight"], ["칸 경계 손잡이에서 크기 바꿈", "Resize at a panel grip"], F),
|
||||
A("all.touchLong", "all", ["손가락 길게 누름"], ["오른쪽 누름(메뉴)", "Right click (menu)"], F),
|
||||
A("all.touchDouble", "all", ["손가락 두 번 톡"], ["두 번 누름", "Double click"], F),
|
||||
A("all.touchPinch", "all", ["두 손가락 벌림"], ["확대", "Zoom in"], F),
|
||||
|
||||
A("b05.undo", "b05", ["Ctrl+Z"], ["되돌림(평면 · 종단 · 횡단 · 관 · 구조물)", "Undo (plan · profile · section · pipes · structures)"]),
|
||||
A("b05.redo", "b05", ["Ctrl+Shift+Z", "Ctrl+Y"], ["다시", "Redo"]),
|
||||
A("b05.escape", "b05", ["Escape"], ["모든 보기의 고름 풀기", "Clear selection in every view"], F),
|
||||
|
||||
A("profile.grade", "profile", ["ArrowUp", "ArrowDown"], ["계획고 0.1 m · [쉬프트] 중 구간 평행 옮김 · 누른 채 = 되풀이", "Grade 0.1 m · parallel shift · hold = repeat"], F),
|
||||
A("profile.station", "profile", ["ArrowLeft", "ArrowRight"], ["구조물 · 부정 측점 옮김(종단을 마지막에 누른 때)", "Move structure · irregular station"], F),
|
||||
A("profile.freeFinish", "profile", ["Enter"], ["자유지정 끝", "Finish free mode"]),
|
||||
A("profile.freeBack", "profile", ["Backspace"], ["자유지정 마지막 점 빼기", "Free mode: remove last point"]),
|
||||
A("profile.escape", "profile", ["Escape"], ["자유지정 취소 · 직선화 시작 측점 취소", "Cancel free mode · straighten start"], F),
|
||||
A("profile.wheel", "profile", ["휠"], ["가로 스크롤(그래프 · 유토곡선 · 측점 표)", "Horizontal scroll"], F),
|
||||
A("profile.drag", "profile", ["왼쪽 끌기", PAN], ["가로 옮김", "Horizontal pan"], F),
|
||||
A("profile.menu", "profile", ["오른쪽 누름"], ["구조물 메뉴(넣기 · 지움)", "Structure menu"], F),
|
||||
A("profile.pill", "profile", ["알약 끌기", "알약 오른쪽 누름"], ["구조물 옮김 · 지움", "Move · delete structure"], F),
|
||||
A("profile.marker", "profile", ["측점 표식 끌기"], ["측점 옮김", "Move station"], F),
|
||||
A("profile.markerPick", "profile", ["Enter", "Space"], ["초점 측점 표식 고름", "Pick focused station marker"], F),
|
||||
A("profile.balloon", "profile", ["풍선 두 번 누름"], ["유토곡선 풍선 자동 자리", "Balloon back to auto"], F),
|
||||
A("profile.hold", "profile", ["▲▼ 누른 채"], ["되풀이(0.5초 뒤 초당 10번)", "Repeat while held"], F),
|
||||
|
||||
A("routeEdit.nudge", "routeEdit", ARROWS, ["교각점 0.1 m(측점 맞춤이면 한 칸) · 고른 마커 옮김", "Move IP 0.1 m · picked marker"], F),
|
||||
A("routeEdit.nudgeBig", "routeEdit", SHIFT_ARROWS, ["교각점 1 m 옮김", "Move IP 1 m"], F),
|
||||
A("routeEdit.unmark", "routeEdit", ["Escape"], ["마커 고름 풀기", "Clear marker pick"], F),
|
||||
A("routeEdit.zoom", "routeEdit", ["휠"], ["커서 자리 확대 · 축소", "Zoom at cursor"], F),
|
||||
A("routeEdit.rotate", "routeEdit", ["Ctrl+휠"], ["지도 5° 돌림", "Rotate map 5°"], F),
|
||||
A("routeEdit.pan", "routeEdit", [PAN], ["화면 옮김", "Pan"], F),
|
||||
A("routeEdit.measure", "routeEdit", ["Shift+누름"], ["재기", "Measure"], F),
|
||||
A("routeEdit.drag", "routeEdit", ["노드 · 손잡이 끌기"], ["교각점 옮김 · 반지름 바꿈", "Move IP · change radius"], F),
|
||||
A("routeEdit.tick", "routeEdit", ["측점 눈금 누름"], ["그 측점 횡단 미리 보기", "Preview that station's section"], F),
|
||||
A("routeEdit.splice", "routeEdit", ["선 두 번 누름"], ["노드 넣기", "Add node"], F),
|
||||
A("routeEdit.unsplice", "routeEdit", ["노드 오른쪽 누름"], ["노드 지움", "Delete node"], F),
|
||||
A("routeEdit.menu", "routeEdit", ["노선 오른쪽 누름"], ["구조물 메뉴 · 마커 지움", "Structure menu · delete marker"], F),
|
||||
A("routeEdit.contour", "routeEdit", ["등고선 누름"], ["등고선 고정 단추", "Contour lock button"], F),
|
||||
|
||||
A("drainage.zoom", "drainage", ["휠"], ["배수유역도 확대 · 축소", "Drainage map zoom"], F),
|
||||
A("drainage.pan", "drainage", [PAN], ["배수유역도 옮김", "Drainage map pan"], F),
|
||||
A("drainage.pipe", "drainage", ["관 표식 끌기"], ["관 옮김", "Move pipe"], F),
|
||||
A("drainage.basin", "drainage", ["누름"], ["유역 고름 · 풂", "Pick · unpick basin"], F),
|
||||
A("drainage.menu", "drainage", ["오른쪽 누름"], ["배수 · 3D 노선 메뉴", "Drainage · 3D route menu"], F),
|
||||
A("drainage.orbit", "drainage", ["3D 왼쪽 끌기"], ["누른 점 둘레로 돌림", "Orbit around picked point"], F),
|
||||
A("drainage.pan3d", "drainage", [`3D ${PAN}`], ["3D 화면 옮김", "3D pan"], F),
|
||||
A("drainage.zoom3d", "drainage", ["3D 휠"], ["3D 커서 자리 확대", "3D zoom at cursor"], F),
|
||||
|
||||
A("section.pad", "section", ARROWS, ["열린 조정 판 ▲▼◀▶", "Open adjust pad ▲▼◀▶"], F),
|
||||
A("section.zoom", "section", ["Ctrl+휠"], ["카드 확대(휠만 = 카드 목록 스크롤)", "Card zoom (wheel alone scrolls)"], F),
|
||||
A("section.detailEnter", "section", ["Enter"], ["상세 창 고르기 칸에 넣음", "Detail: put the pick in the cell"], F),
|
||||
A("section.detailEscape", "section", ["Escape"], ["상세 창 고르기 취소", "Detail: cancel picking"], F),
|
||||
A("section.detailAdd", "section", ["Shift+누름", "Ctrl+누름"], ["상세 창 선 조각 · 면 더함", "Detail: add line pieces · faces"], F),
|
||||
A("section.detailMenu", "section", ["표 칸 오른쪽 누름"], ["상세 표 칸 메뉴", "Detail table cell menu"], F),
|
||||
A("section.hold", "section", ["▲▼ 누른 채"], ["선 칸 되풀이", "Line cell repeat"], F),
|
||||
|
||||
A("plan.undo", "plan", ["Ctrl+Z"], ["되돌림", "Undo"]),
|
||||
A("plan.redo", "plan", ["Ctrl+Shift+Z", "Ctrl+Y"], ["다시", "Redo"]),
|
||||
A("plan.drawFinish", "plan", ["Enter"], ["그리기 끝", "Finish drawing"]),
|
||||
A("plan.drawBack", "plan", ["Backspace"], ["그리기 마지막 점 빼기", "Drawing: remove last point"]),
|
||||
A("plan.delete", "plan", ["Delete"], ["고른 것 지움", "Delete selection"]),
|
||||
A("plan.escape", "plan", ["Escape"], ["그리기 취소 · 고름 풀기 · 재기 끝", "Cancel drawing · clear selection · end measure"], F),
|
||||
A("plan.zoom", "plan", ["휠"], ["커서 자리 확대 · 축소", "Zoom at cursor"], F),
|
||||
A("plan.rotate", "plan", ["Ctrl+휠"], ["지도 돌림", "Rotate map"], F),
|
||||
A("plan.pan", "plan", [PAN], ["화면 옮김", "Pan"], F),
|
||||
A("plan.vertex", "plan", ["고른 선 누름", "꼭짓점 끌기"], ["꼭짓점 넣고 끌기 · 옮김", "Insert · drag vertex"], F),
|
||||
A("plan.unvertex", "plan", ["꼭짓점 · 마커 오른쪽 누름"], ["지움", "Delete"], F),
|
||||
A("plan.measure", "plan", ["재기: 누름 · 누름"], ["시점 · 종점", "Start · end"], F),
|
||||
|
||||
A("d02.place", "d02", ["지도 누름"], ["지점 놓기", "Place point"], F),
|
||||
A("d02.drag", "d02", ["표식 끌기"], ["지점 옮김", "Move point"], F),
|
||||
|
||||
A("b04.zoom", "b04", ["휠"], ["지도 · 3D 확대 · 축소", "Map · 3D zoom"], F),
|
||||
A("b04.pan", "b04", [PAN], ["지도 · 3D 옮김", "Map · 3D pan"], F),
|
||||
A("b04.pick", "b04", ["누름"], ["유역 · 흐름 점 고름", "Pick basin · flow point"], F),
|
||||
A("b04.pipe", "b04", ["관 표식 끌기"], ["관 옮김", "Move pipe"], F),
|
||||
A("b04.menu", "b04", ["오른쪽 누름"], ["노선 · 관 메뉴", "Route · pipe menu"], F),
|
||||
A("b04.orbit", "b04", ["3D 왼쪽 끌기"], ["누른 점 둘레로 돌림", "Orbit around picked point"], F),
|
||||
|
||||
A("cad.snap", "cad", ["F3"], ["스냅", "Snap"]),
|
||||
A("cad.grid", "cad", ["F7"], ["그리드", "Grid"]),
|
||||
A("cad.ortho", "cad", ["F8"], ["직교", "Ortho"]),
|
||||
A("cad.polar45", "cad", ["F10"], ["45°", "45°"]),
|
||||
A("cad.undo", "cad", ["Ctrl+Z"], ["되돌림", "Undo"]),
|
||||
A("cad.redo", "cad", ["Ctrl+Y", "Ctrl+Shift+Z"], ["다시", "Redo"]),
|
||||
A("cad.save", "cad", ["Ctrl+S"], ["저장(M02)", "Save (M02)"]),
|
||||
A("cad.selectAll", "cad", ["Ctrl+A"], ["모두 고름", "Select all"]),
|
||||
A("cad.delete", "cad", ["Delete"], ["고른 것 지움", "Delete selection"]),
|
||||
A("cad.copy", "cad", ["Ctrl+C"], ["복사", "Copy"]),
|
||||
A("cad.copyBase", "cad", ["Ctrl+Shift+C"], ["기준점 복사", "Copy with base point"]),
|
||||
A("cad.cut", "cad", ["Ctrl+X"], ["잘라 냄", "Cut"]),
|
||||
A("cad.pasteBlock", "cad", ["Ctrl+Shift+V"], ["블록으로 붙여넣기", "Paste as block"]),
|
||||
A("cad.properties", "cad", ["Ctrl+1"], ["특성", "Properties"]),
|
||||
A("cad.paste", "cad", ["Ctrl+V"], ["붙여넣기", "Paste"], F),
|
||||
A("cad.enter", "cad", ["Enter", "Space", "오른쪽 누름"], ["입력 끝 · 쉴 때 명령 반복", "Submit · repeat command when idle"], F),
|
||||
A("cad.escape", "cad", ["Escape"], ["명령 취소 · 쉴 때 고름 풀기", "Cancel command · clear selection when idle"], F),
|
||||
A("cad.type", "cad", ["글자"], ["명령 · 값 입력", "Type command · value"], F),
|
||||
A("cad.backspace", "cad", ["Backspace"], ["입력 글 지움", "Erase typed text"], F),
|
||||
A("cad.tab", "cad", ["Tab"], ["명령 후보 돌림 · 동적 입력 칸 넘김", "Cycle candidates · next dynamic field"], F),
|
||||
A("cad.recall", "cad", ["ArrowUp", "ArrowDown"], ["기록 · 최근 값 · 후보", "History · recent values · candidates"], F),
|
||||
A("cad.arrowPan", "cad", ["Shift+ArrowUp", "Shift+ArrowDown", "ArrowLeft", "ArrowRight"], ["화면 옮김", "Pan"], F),
|
||||
A("cad.zoom", "cad", ["휠"], ["확대 · 축소", "Zoom"], F),
|
||||
A("cad.pan", "cad", [PAN], ["화면 옮김", "Pan"], F),
|
||||
A("cad.snapMenu", "cad", ["Shift+오른쪽 누름"], ["점 물음에서 스냅 메뉴", "Snap menu at a point prompt"], F),
|
||||
A("cad.extents", "cad", ["가운데 두 번 누름"], ["범위 줌", "Zoom extents"], F),
|
||||
A("cad.box", "cad", ["왼쪽 끌기"], ["고르기 상자(오른쪽으로 = 안 · 왼쪽으로 = 걸침)", "Selection window · crossing"], F),
|
||||
|
||||
A("b08.zoom", "b08", ["휠"], ["이정표 그림 확대 · 축소", "Milestone diagram zoom"], F),
|
||||
A("b08.pan", "b08", [PAN, "빈 곳 왼쪽 끌기"], ["이정표 그림 옮김", "Milestone diagram pan"], F),
|
||||
A("b08.reset", "b08", ["빈 곳 두 번 누름"], ["배율 처음대로", "Reset zoom"], F),
|
||||
A("b08.leg", "b08", ["구간 글 두 번 누름", "Enter", "Escape"], ["노선 종류 · km 고침 · 적용 · 취소", "Edit road type · km · apply · cancel"], F),
|
||||
A("b08.box", "b08", ["상자 · 잇는 점 끌기"], ["옮김 · 이음", "Move · link"], F),
|
||||
A("b08.rowMenu", "b08", ["이정표 표 줄 오른쪽 누름"], ["이음 메뉴", "Link menu"], F),
|
||||
A("b08.cell", "b08", ["두 번 누름", "Enter", "F2"], ["표 칸 입력 · Enter = 넣음", "Edit table cell · Enter = commit"], F),
|
||||
|
||||
A("b09.indent", "b09", ["Tab", "Shift+Tab"], ["내역 줄 들임 · 내어씀", "Indent · outdent row"], F),
|
||||
A("b09.level", "b09", ["ArrowLeft", "ArrowRight"], ["내역 줄 단 바꿈", "Change row level"], F),
|
||||
A("b09.move", "b09", ["ArrowUp", "ArrowDown", "Alt+ArrowUp", "Alt+ArrowDown"], ["내역 줄 위 · 아래로", "Move row up · down"], F),
|
||||
A("b09.edit", "b09", ["Enter", "F2", "두 번 누름"], ["고른 줄 · 칸 입력", "Edit picked row · cell"], F),
|
||||
A("b09.escape", "b09", ["Escape"], ["입력 끝 · 고름 풀기 · 빨강 표시 지움 · 뜬 창 닫기", "End edit · unpick · clear highlight · close popups"], F),
|
||||
A("b09.menu", "b09", ["칸 · 줄 오른쪽 누름"], ["칸 · 줄 메뉴", "Cell · row menu"], F),
|
||||
A("b09.drag", "b09", ["줄 끌기"], ["세로 = 자리 · 가로 = 단", "Vertical = position · horizontal = level"], F),
|
||||
|
||||
A("m02.save", "m02", ["Ctrl+S"], ["카드 저장", "Save card"], F),
|
||||
A("m02.treeMove", "m02", ARROWS, ["나무: ↑↓ 초점 · →← 펼침 · 접음", "Tree: ↑↓ focus · →← expand · collapse"], F),
|
||||
A("m02.rename", "m02", ["F2"], ["나무 이름 바꿈", "Tree rename"], F),
|
||||
A("m02.delete", "m02", ["Delete"], ["나무 · 흐름도 고른 것 지움", "Delete selected (tree · data flow)"], F),
|
||||
A("m02.copy", "m02", ["Ctrl+C", "Ctrl+V"], ["나무 복사 · 붙여넣기", "Tree copy · paste"], F),
|
||||
A("m02.multi", "m02", ["Ctrl+누름", "Shift+누름"], ["나무 여럿 고름", "Tree multi-select"], F),
|
||||
A("m02.menu", "m02", ["나무 빈 곳 오른쪽 누름"], ["맨 위 메뉴", "Top menu"], F),
|
||||
A("m02.zoom", "m02", ["휠", "Ctrl+휠"], ["흐름도 · 노임 그림 배율", "Data flow · labor diagram zoom"], F),
|
||||
A("m02.pan", "m02", [PAN, "Space+끌기"], ["흐름도 · 노임 그림 옮김", "Data flow · labor diagram pan"], F),
|
||||
A("m02.boxMove", "m02", ARROWS, ["흐름도 고른 상자 10 px(Shift 50)", "Move selected box 10 px (Shift 50)"], F),
|
||||
A("m02.undo", "m02", ["Ctrl+Z"], ["흐름도 되돌림", "Data flow undo"], F),
|
||||
A("m02.escape", "m02", ["Escape"], ["잇기 · 알약 취소 · 크게 보기 줄임 · 열린 창 닫기", "Cancel link · pill · shrink full view · close window"], F),
|
||||
A("m02.sheet", "m02", ["시트 안"], ["엑셀 키 그대로", "Excel keys"], F),
|
||||
|
||||
A("stab.zoom", "stab", ["휠"], ["지도 · 그림 확대 · 축소", "Map · drawing zoom"], F),
|
||||
A("stab.pan", "stab", ["왼쪽 끌기", PAN], ["E04 · E07 지도 · E03 그림 옮김", "Pan"], F),
|
||||
A("stab.fit", "stab", ["두 번 누름"], ["전체 보기", "Fit"], F),
|
||||
|
||||
A("etc.chat", "etc", ["Enter", "Shift+Enter"], ["B01 지식 물음 보냄 · 줄 바꿈", "B01 chat send · new line"], F),
|
||||
A("etc.help", "etc", ["두 번 누름", "Ctrl+Enter", "Escape"], ["도움말 고침(관리자) · 저장 · 취소", "Help edit (admin) · save · cancel"], F),
|
||||
A("etc.cadTab", "etc", ["CAD 탭 두 번 누름"], ["탭 이름 바꿈", "Rename tab"], F),
|
||||
A("etc.sheet", "etc", ["항목 고르기 표"], ["방향키 · Tab · Enter · F2 · Delete · 글자 = 입력", "Arrows · Tab · Enter · F2 · Delete · typing"], F),
|
||||
A("etc.toolbar", "etc", ["지도 도구줄 휠"], ["넘친 도구줄 가로 스크롤", "Scroll overflowing toolbar"], F),
|
||||
];
|
||||
|
||||
const BY_ID = new Map(KEY_ACTIONS.map((action) => [action.id, action]));
|
||||
|
||||
/** 브라우저 몫 — 못 씀 */
|
||||
export const RESERVED_KEYS = [
|
||||
"F5",
|
||||
"F11",
|
||||
"F12",
|
||||
"Ctrl+W",
|
||||
"Ctrl+T",
|
||||
"Ctrl+N",
|
||||
"Ctrl+R",
|
||||
"Ctrl+Shift+T",
|
||||
];
|
||||
|
||||
const MODIFIERS = new Set(["Control", "Shift", "Alt", "Meta", "AltGraph", "CapsLock", "Process"]);
|
||||
const NAMED: Record<string, string> = {
|
||||
" ": "Space",
|
||||
Spacebar: "Space",
|
||||
Esc: "Escape",
|
||||
Del: "Delete",
|
||||
};
|
||||
|
||||
/** 누른 키 → 키 글 · 수정키만 · 한글 조합 = 빈 글 */
|
||||
export function chordOf(
|
||||
evt: Pick<KeyboardEvent, "key" | "code" | "ctrlKey" | "altKey" | "shiftKey"> &
|
||||
Partial<Pick<KeyboardEvent, "metaKey">>,
|
||||
): string {
|
||||
const letter = keyLetter(evt);
|
||||
const raw = NAMED[evt.key] ?? evt.key;
|
||||
if (!letter && (!raw || MODIFIERS.has(raw))) return "";
|
||||
const key = letter ? letter.toUpperCase() : raw.length === 1 ? raw.toUpperCase() : raw;
|
||||
const parts = [
|
||||
evt.ctrlKey || evt.metaKey ? "Ctrl" : "",
|
||||
evt.altKey ? "Alt" : "",
|
||||
evt.shiftKey ? "Shift" : "",
|
||||
];
|
||||
return [...parts.filter(Boolean), key].join("+");
|
||||
}
|
||||
|
||||
/** 지금 쓰는 키 — 바꾼 것(`keys`) 있으면 그것 · 없으면 기본 */
|
||||
export function actionKeys(
|
||||
id: string,
|
||||
keys: Record<string, string[]> = inputPrefs().keys,
|
||||
): readonly string[] {
|
||||
const action = BY_ID.get(id);
|
||||
if (!action) return [];
|
||||
return (!action.fixed && keys[id]) || action.keys;
|
||||
}
|
||||
|
||||
/** 이 키가 그 동작인가 */
|
||||
export const keyIs = (
|
||||
evt: Parameters<typeof chordOf>[0],
|
||||
id: string,
|
||||
keys?: Record<string, string[]>,
|
||||
): boolean => {
|
||||
const chord = chordOf(evt);
|
||||
return !!chord && actionKeys(id, keys).includes(chord);
|
||||
};
|
||||
|
||||
/** 그 자리 바꿀 수 있는 동작 중 이 키에 맞는 첫 동작 id */
|
||||
export function keyAction(evt: Parameters<typeof chordOf>[0], scope: KeyScope): string | null {
|
||||
const chord = chordOf(evt);
|
||||
if (!chord) return null;
|
||||
const keys = inputPrefs().keys;
|
||||
return (
|
||||
KEY_ACTIONS.find((a) => a.scope === scope && !a.fixed && actionKeys(a.id, keys).includes(chord))
|
||||
?.id ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export type KeyError = "dup" | "reserved" | "letter";
|
||||
|
||||
/** 함께 듣는 자리인가 — 같은 화면 · 「모든 화면」 */
|
||||
const together = (a: KeyScope, b: KeyScope): boolean =>
|
||||
a === "all" || b === "all" || KEY_SCOPES[a].page === KEY_SCOPES[b].page;
|
||||
|
||||
/** 바꾼 키 검사 — `{동작 id: 첫 잘못}` · 비면 통과 · 겹침은 바꾼 줄에 붙임(상대가 못 바꾸는 줄이어도) */
|
||||
export function checkKeys(keys: Record<string, string[]>): Record<string, KeyError> {
|
||||
const errors: Record<string, KeyError> = {};
|
||||
const live = KEY_ACTIONS.map((a) => ({ a, list: actionKeys(a.id, keys) }));
|
||||
for (const { a, list } of live) {
|
||||
if (a.fixed || !keys[a.id]) continue;
|
||||
for (const key of list) {
|
||||
const error: KeyError | null = RESERVED_KEYS.includes(key)
|
||||
? "reserved"
|
||||
: a.scope === "cad" && /^(Shift\+)?.$/.test(key)
|
||||
? "letter"
|
||||
: live.some(
|
||||
({ a: b, list: other }) =>
|
||||
b !== a && together(a.scope, b.scope) && other.includes(key),
|
||||
)
|
||||
? "dup"
|
||||
: null;
|
||||
if (error) {
|
||||
errors[a.id] ??= error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
const LABEL: Record<string, string> = {
|
||||
ArrowUp: "↑",
|
||||
ArrowDown: "↓",
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
Escape: "Esc",
|
||||
};
|
||||
|
||||
/** 키 글 → 보일 글(↑ · Esc) */
|
||||
export const keyLabel = (chord: string): string =>
|
||||
chord
|
||||
.split("+")
|
||||
.map((part) => LABEL[part] ?? part)
|
||||
.join("+");
|
||||
@@ -9,7 +9,7 @@
|
||||
* 되돌리기 범위가 B05 조작 전부인 이유는 `B05_Profile_UI_Profile_History` 머리말 참조.
|
||||
* ========================================================================== */
|
||||
|
||||
import { isCtrlKey } from "../A00_Common/key_shortcut";
|
||||
import { keyIs } from "../A00_Common/key_registry";
|
||||
import { viewOf } from "../common_util/common_util_view_window";
|
||||
import type {
|
||||
AlignmentBase,
|
||||
@@ -341,24 +341,32 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
// 입력칸 안에서는 방향키가 값 조작이므로 손대지 않는다.
|
||||
if (target && target.closest("input, select, textarea")) return;
|
||||
const key = event.key;
|
||||
// [자유지정] 중 Enter 끝 · Esc 취소 · Backspace 점 빼기 — 단추 초점의 Enter 누름도 막는다.
|
||||
if (tools.mode() === "free" && free.key(key)) {
|
||||
// [자유지정] 중 Enter 끝 · Esc 취소 · Backspace 점 빼기 — 단추 초점의 Enter 누름도 막는다(키는 등록부 · 588-3).
|
||||
const freeKey = keyIs(event, "profile.freeFinish")
|
||||
? "Enter"
|
||||
: keyIs(event, "profile.freeBack")
|
||||
? "Backspace"
|
||||
: key === "Escape"
|
||||
? key
|
||||
: "";
|
||||
if (tools.mode() === "free" && freeKey && free.key(freeKey)) {
|
||||
event.preventDefault();
|
||||
freeView.redraw();
|
||||
return;
|
||||
}
|
||||
// 한 줄 되돌리기(346-4) — 평면 · 종단 · 횡단 · 관 · 구조물을 만진 차례 거꾸로.
|
||||
if (isCtrlKey(event, "z") || isCtrlKey(event, "y")) {
|
||||
const redo = keyIs(event, "b05.redo");
|
||||
if (redo || keyIs(event, "b05.undo")) {
|
||||
event.preventDefault();
|
||||
// 자유지정 동안은 자유지정 안만 — 되돌릴 것 없어도 큰 이력으로 안 감(367-3)
|
||||
if (tools.mode() === "free") {
|
||||
if (event.shiftKey || isCtrlKey(event, "y")) free.redo();
|
||||
if (redo) free.redo();
|
||||
else free.undo();
|
||||
freeView.redraw();
|
||||
return;
|
||||
}
|
||||
flushHold();
|
||||
if (event.shiftKey || isCtrlKey(event, "y")) history.redo();
|
||||
if (redo) history.redo();
|
||||
else history.undo();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { SelectState } from '../tools/select-tool.ts';
|
||||
import { ActorEvent, type MouseClickEvent } from '../tools/tool.types.ts';
|
||||
import { activeToolWantsFreeText, activeToolWantsIme, ImeSink, isImeKey, isPlainSpace } from './ime-sink.ts';
|
||||
import { drawInputField, dynamicInputLine } from './input-field-draw.ts';
|
||||
import { cadCtrlCommand, ctrlShortcut } from './input-shortcut.ts';
|
||||
import { CAD_KEY_COMMANDS, cadKeyAction } from './input-shortcut.ts';
|
||||
import { wheelZoom } from './input-wheel.ts';
|
||||
import { isObjectSnap, noteShift, snapAt } from './ortho.ts';
|
||||
import { isWaitingForSecondSelectPoint, selectingActor } from './selecting.ts';
|
||||
@@ -376,10 +376,11 @@ export class InputController {
|
||||
if (target !== this.ime.element && target?.closest('input, textarea, select')) {
|
||||
return;
|
||||
}
|
||||
const shortcut = ctrlShortcut(evt);
|
||||
const cadCommand = cadCtrlCommand(evt);
|
||||
// 한글 조합 중 — 단축키가 먹지 않게 IME 에 넘김(Ctrl 단축키는 조합 중에도 먹음)
|
||||
if (isImeKey(evt) && !shortcut && !cadCommand) return;
|
||||
// 키 동작 = ☰ [설정] 등록부(588-3) — 바꾼 키만 먹고 옛 키는 아래 글자 · 기본 처리로 감
|
||||
const action = cadKeyAction(evt);
|
||||
const cadCommand = action ? CAD_KEY_COMMANDS[action] : undefined;
|
||||
// 한글 조합 중 — 단축키가 먹지 않게 IME 에 넘김(키 동작은 조합 중에도 먹음)
|
||||
if (isImeKey(evt) && !action) return;
|
||||
if (!['Shift', 'Control', 'Alt', 'Meta'].includes(evt.key)) notice(null); // 알림 줄은 다음 키까지
|
||||
console.log(`key pressed: ${evt.key}`);
|
||||
if (evt.key === 'F12') {
|
||||
@@ -397,22 +398,22 @@ export class InputController {
|
||||
return;
|
||||
}
|
||||
// 제도 보조 토글 (AutoCAD 상태막대 기능키)
|
||||
if (evt.key === 'F3') {
|
||||
if (action === 'cad.snap') {
|
||||
evt.preventDefault();
|
||||
setSnapEnabled(!getSnapEnabled());
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'F7') {
|
||||
if (action === 'cad.grid') {
|
||||
evt.preventDefault();
|
||||
setGridEnabled(!getGridEnabled());
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'F8') {
|
||||
if (action === 'cad.ortho') {
|
||||
evt.preventDefault();
|
||||
setAngleStep(getAngleStep() === 90 ? 0 : 90);
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'F10') {
|
||||
if (action === 'cad.polar45') {
|
||||
evt.preventDefault();
|
||||
setAngleStep(getAngleStep() === 45 ? 0 : 45);
|
||||
return;
|
||||
@@ -444,16 +445,16 @@ export class InputController {
|
||||
}
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
if (shortcut === 'undo') {
|
||||
if (action === 'cad.undo') {
|
||||
// User wants to undo the last action
|
||||
this.handleUndo(evt);
|
||||
} else if (shortcut === 'redo') {
|
||||
} else if (action === 'cad.redo') {
|
||||
// User wants to redo the last action
|
||||
this.handleRedo(evt);
|
||||
} else if (shortcut === 'save') {
|
||||
} else if (action === 'cad.save') {
|
||||
// 부모 페이지가 저장을 맡은 화면(M02 양식)만 — B07 은 예전처럼 아무 일 없음
|
||||
if (isHostSave()) requestHostSave();
|
||||
} else if (shortcut === 'selectAll') {
|
||||
} else if (action === 'cad.selectAll') {
|
||||
// User wants to select everything
|
||||
setSelectedEntityIds(getPickableEntities().map((entity) => entity.id));
|
||||
} else if (evt.key === 'Backspace') {
|
||||
@@ -461,7 +462,7 @@ export class InputController {
|
||||
evt.preventDefault();
|
||||
this.text = this.text.slice(0, this.text.length - 1);
|
||||
this.browsing = null;
|
||||
} else if (evt.key === 'Delete') {
|
||||
} else if (action === 'cad.delete') {
|
||||
// User wants to delete the current selection
|
||||
evt.preventDefault();
|
||||
getActiveToolActor()?.send({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Ctrl 단축키 판정 — 공용 `A00_Common/key_shortcut.ts`(214-2)를 그대로 내보냄.
|
||||
* 한글 자판은 `key` 가 「ㅋ」·「ㅛ」로 와 글자로는 안 맞아 자판 자리(`code`)로 본다.
|
||||
*/
|
||||
import { keyLetter } from '../../../../A00_Common/key_shortcut';
|
||||
import { keyAction } from '../../../../A00_Common/key_registry';
|
||||
|
||||
export {
|
||||
ctrlShortcut,
|
||||
@@ -11,22 +11,14 @@ export {
|
||||
type CtrlShortcut,
|
||||
} from '../../../../A00_Common/key_shortcut';
|
||||
|
||||
/** CAD 만의 Ctrl 단축키 → 명령 이름(오토캐드) — Ctrl+V 는 브라우저 `paste` 사건 몫 */
|
||||
export function cadCtrlCommand(
|
||||
evt: Pick<KeyboardEvent, 'code' | 'ctrlKey' | 'altKey' | 'shiftKey'> &
|
||||
Partial<Pick<KeyboardEvent, 'key' | 'metaKey'>>
|
||||
): string | null {
|
||||
if (!(evt.ctrlKey || evt.metaKey) || evt.altKey) return null;
|
||||
switch (keyLetter(evt)) {
|
||||
case 'c':
|
||||
return evt.shiftKey ? 'COPYBASE' : 'COPYCLIP';
|
||||
case 'x':
|
||||
return evt.shiftKey ? null : 'CUTCLIP';
|
||||
case 'v':
|
||||
return evt.shiftKey ? 'PASTEBLOCK' : null;
|
||||
case '1':
|
||||
return evt.shiftKey ? null : 'PROPERTIES';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/** CAD 키 동작(☰ [설정] 단축키 등록부 · 588-3) — 사용자가 바꾼 키 있으면 그것만 · 없으면 기본(F3 · Ctrl+Z …) */
|
||||
export const cadKeyAction = (evt: KeyboardEvent): string | null => keyAction(evt, 'cad');
|
||||
|
||||
/** 명령으로 가는 CAD 키 동작 → 명령 이름(오토캐드) — Ctrl+V 는 브라우저 `paste` 사건 몫 */
|
||||
export const CAD_KEY_COMMANDS: Record<string, string> = {
|
||||
'cad.copy': 'COPYCLIP',
|
||||
'cad.copyBase': 'COPYBASE',
|
||||
'cad.cut': 'CUTCLIP',
|
||||
'cad.pasteBlock': 'PASTEBLOCK',
|
||||
'cad.properties': 'PROPERTIES',
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { keyIs } from "../A00_Common/key_registry";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
@@ -498,7 +499,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
panelState();
|
||||
},
|
||||
key(event) {
|
||||
if (mode === "draw" && event.key === "Enter") {
|
||||
if (mode === "draw" && keyIs(event, "plan.drawFinish")) {
|
||||
setMode("idle");
|
||||
return true;
|
||||
}
|
||||
@@ -509,7 +510,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
setMode("idle");
|
||||
return true;
|
||||
}
|
||||
if (mode === "draw" && active !== null && event.key === "Backspace") {
|
||||
if (mode === "draw" && active !== null && keyIs(event, "plan.drawBack")) {
|
||||
const network = params.network();
|
||||
const step =
|
||||
network.lines[active].vertices.length < 2
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { keyIs } from "../A00_Common/key_registry";
|
||||
import {
|
||||
distanceAlong,
|
||||
lineLength,
|
||||
@@ -306,7 +307,7 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
select,
|
||||
key(event) {
|
||||
if (!selected || !params.enabled()) return false;
|
||||
if (event.key === "Delete" && params.editable()) {
|
||||
if (keyIs(event, "plan.delete") && params.editable()) {
|
||||
apply((network, index) => removeLine(network, index));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { keyIs } from "../A00_Common/key_registry";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import { heightAt as gridHeightAt } from "../common_util/common_util_surface_sampler";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
@@ -596,16 +597,14 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
if (!root.isConnected) return window.removeEventListener("keydown", onKey);
|
||||
const typing = (event.target as HTMLElement | null)?.closest?.("input, textarea, select");
|
||||
if (typing || !editable) return;
|
||||
const key = event.key.toLowerCase();
|
||||
const ctrl = event.ctrlKey || event.metaKey;
|
||||
const handled =
|
||||
ctrl && key === "z" && !event.shiftKey
|
||||
? (undo(), true)
|
||||
: ctrl && (key === "y" || (key === "z" && event.shiftKey))
|
||||
? (redo(), true)
|
||||
: measuring && event.key === "Escape"
|
||||
? (setMeasure(false), true)
|
||||
: !ctrl && (draw.key(event) || edit.key(event));
|
||||
// 키 = 등록부(588-3 · 한글 자판도 자판 자리로)
|
||||
const handled = keyIs(event, "plan.undo")
|
||||
? (undo(), true)
|
||||
: keyIs(event, "plan.redo")
|
||||
? (redo(), true)
|
||||
: measuring && event.key === "Escape"
|
||||
? (setMeasure(false), true)
|
||||
: draw.key(event) || edit.key(event);
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
map.redraw();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* CAD 설정 배선(PLAN 332-3) — ☰ [설정] 휠 확대 방향 · 내 별칭(`A00_Common/input_prefs.ts`).
|
||||
* 휠 = 기본 밂(앞으로 굴리면 확대 · 오토캐드 · 337) · 당김 저장 뒤 뒤집힘 · 핀치는 늘 벌리면 확대.
|
||||
* 별칭 = 명령 이름 > 내 별칭 > 기본 별칭 · 저장 즉시 따름.
|
||||
* 바꾼 키(588-3) = 새 키만 먹음 · 옛 키는 안 먹음 · 한글 자판도 자판 자리로.
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad input-prefs-cad
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
matchCommandPrefixes,
|
||||
resolveCommandInput,
|
||||
} from "../../../B07_DesignDetail/openwebcad/src/commands/registry";
|
||||
import { getActiveToolActor } from "../../../B07_DesignDetail/openwebcad/src/state";
|
||||
import { getActiveToolActor, getSnapEnabled } from "../../../B07_DesignDetail/openwebcad/src/state";
|
||||
import { initApplication } from "../../../B07_DesignDetail/openwebcad/test/helpers/init-application";
|
||||
|
||||
const store = new Map<string, string>();
|
||||
@@ -96,3 +97,34 @@ describe("내 별칭", () => {
|
||||
expect(getActiveToolActor()?.getSnapshot()?.context.type).toBe("RECTANGLE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("바꾼 키(588-3)", () => {
|
||||
const key = (controller: ReturnType<typeof initApplication>, extra: Partial<KeyboardEvent>) =>
|
||||
controller.handleKeyStroke({
|
||||
key: "",
|
||||
code: "",
|
||||
keyCode: 0,
|
||||
isComposing: false,
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
target: null,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
...extra,
|
||||
} as unknown as KeyboardEvent);
|
||||
|
||||
it("스냅 F3 → Ctrl+Q — 새 키(한글 자판 「ㅂ」)만 켜고 끔 · F3 안 먹음", () => {
|
||||
const controller = initApplication();
|
||||
const before = getSnapEnabled();
|
||||
key(controller, { key: "F3", code: "F3" });
|
||||
expect(getSnapEnabled()).toBe(!before);
|
||||
savePrefs({ keys: { "cad.snap": ["Ctrl+Q"] } });
|
||||
key(controller, { key: "F3", code: "F3" });
|
||||
expect(getSnapEnabled()).toBe(!before);
|
||||
key(controller, { key: "ㅂ", code: "KeyQ", ctrlKey: true });
|
||||
expect(getSnapEnabled()).toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* PLAN 588 지킴 헬퍼 — 단축키 등록부(`A00_Common/key_registry.ts`) · CAD 묶음 · 찾기(`app_settings_find.ts`)
|
||||
* 를 그 자리에서 트랜스파일해 Node 로 돌림. 가짜 localStorage 로 바꾼 키를 넣어 봄.
|
||||
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_588_key_registry.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const mods = {};
|
||||
function load(rel) {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, rel), "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const req = (p) => mods[path.basename(p)] ?? {};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return (mods[path.basename(rel, ".ts")] = box.exports);
|
||||
}
|
||||
load("A00_Common/input_prefs.ts");
|
||||
load("A00_Common/key_shortcut.ts");
|
||||
load("B07_DesignDetail/openwebcad/src/ribbon/ribbon.config.ts");
|
||||
const reg = load("A00_Common/key_registry.ts");
|
||||
const find = load("A00_Common/app_settings_find.ts");
|
||||
const catalog = require(
|
||||
path.join(ROOT, "B07_DesignDetail/openwebcad/src/commands/command-catalog.json"),
|
||||
);
|
||||
|
||||
const store = new Map();
|
||||
globalThis.localStorage = { getItem: (k) => (store.has(k) ? store.get(k) : null) };
|
||||
const setKeys = (keys) => store.set(mods.input_prefs.INPUT_PREFS_KEY, JSON.stringify({ keys }));
|
||||
|
||||
const ev = (key, code, extra = {}) => ({
|
||||
key,
|
||||
code,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
...extra,
|
||||
});
|
||||
const out = {};
|
||||
|
||||
// ① CAD 묶음 · 찾기
|
||||
const groups = find.cadCommandGroups(catalog.map((c) => c.id));
|
||||
out.groups = groups.map((g) => ({ label: g.label, n: g.ids.length }));
|
||||
out.groupIds = groups.flatMap((g) => g.ids);
|
||||
out.other = groups.find((g) => !g.label)?.ids ?? [];
|
||||
const hits = (q) =>
|
||||
catalog.filter((c) => find.matchesFind(q, [c.id, c.label, ...c.aliases])).map((c) => c.id);
|
||||
out.find = {
|
||||
sun: hits("선"),
|
||||
L: hits("L"),
|
||||
sunPl: hits("선 pl"),
|
||||
blank: hits(" ").length,
|
||||
none: hits("zzqq"),
|
||||
};
|
||||
|
||||
// ② 키 글
|
||||
out.chord = {
|
||||
hangulCtrlZ: reg.chordOf(ev("ㅋ", "KeyZ", { ctrlKey: true })),
|
||||
ctrlShiftZ: reg.chordOf(ev("Z", "KeyZ", { ctrlKey: true, shiftKey: true })),
|
||||
meta: reg.chordOf(ev("z", "KeyZ", { metaKey: true })),
|
||||
space: reg.chordOf(ev(" ", "Space")),
|
||||
shiftOnly: reg.chordOf(ev("Shift", "ShiftLeft", { shiftKey: true })),
|
||||
process: reg.chordOf(ev("Process", "")),
|
||||
f3: reg.chordOf(ev("F3", "F3")),
|
||||
shiftDigit: reg.chordOf(ev("!", "Digit1", { ctrlKey: true, shiftKey: true })),
|
||||
arrow: reg.chordOf(ev("ArrowUp", "ArrowUp", { shiftKey: true })),
|
||||
label: reg.keyLabel("Shift+ArrowUp"),
|
||||
};
|
||||
|
||||
// ③ 판정 — 기본 · 바꾼 키 · 못 바꾸는 줄은 바꾼 값 무시
|
||||
const del = ev("Delete", "Delete");
|
||||
out.keyIs = {
|
||||
delDefault: reg.keyIs(del, "plan.delete"),
|
||||
hangulUndo: reg.keyIs(ev("ㅋ", "KeyZ", { ctrlKey: true }), "plan.undo"),
|
||||
};
|
||||
setKeys({ "plan.delete": ["Ctrl+D"], "all.escape": ["Q"] });
|
||||
out.keyIs.delAfter = reg.keyIs(del, "plan.delete");
|
||||
out.keyIs.ctrlDAfter = reg.keyIs(ev("ㅇ", "KeyD", { ctrlKey: true }), "plan.delete");
|
||||
out.keyIs.fixedIgnored = reg.actionKeys("all.escape");
|
||||
out.keyIs.cadAction = reg.keyAction(ev("F7", "F7"), "cad");
|
||||
store.clear();
|
||||
|
||||
// ④ 겹침 검사
|
||||
const chk = (keys) => reg.checkKeys(keys);
|
||||
out.check = {
|
||||
none: chk({}),
|
||||
sameScope: chk({ "plan.delete": ["Ctrl+Z"] }),
|
||||
withAll: chk({ "plan.delete": ["Escape"] }),
|
||||
withLoupe: chk({ "plan.delete": ["Z"] }),
|
||||
otherPage: chk({ "plan.delete": ["F3"] }),
|
||||
reserved: chk({ "plan.delete": ["F5"] }),
|
||||
cadLetter: chk({ "cad.snap": ["Q"] }),
|
||||
cadShiftLetter: chk({ "cad.snap": ["Shift+Q"] }),
|
||||
cadCtrl: chk({ "cad.snap": ["Ctrl+Q"] }),
|
||||
samePage: chk({ "b05.undo": ["Enter"] }),
|
||||
swap: chk({ "plan.undo": ["Ctrl+Y"], "plan.redo": ["Ctrl+Z"] }),
|
||||
both: chk({ "plan.drawFinish": ["F2"], "plan.drawBack": ["F2"] }),
|
||||
};
|
||||
|
||||
// ⑤ 등록부 꼴
|
||||
out.actions = reg.KEY_ACTIONS.map((a) => ({
|
||||
id: a.id,
|
||||
scope: a.scope,
|
||||
keys: a.keys,
|
||||
fixed: !!a.fixed,
|
||||
}));
|
||||
out.scopes = Object.keys(reg.KEY_SCOPES);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -137,7 +137,7 @@ def test_되돌리기_배선_자유지정_먼저():
|
||||
panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert 'tools.mode() === "free" ? (free.undo(), freeView.redraw()) : history.undo()' in panel
|
||||
assert 'tools.mode() === "free" ? free.canUndo() : history.canUndo()' in panel
|
||||
keys = panel[panel.index('isCtrlKey(event, "z") || isCtrlKey(event, "y")') :]
|
||||
keys = panel[panel.index('if (redo || keyIs(event, "b05.undo")) {') :] # 588-3 등록부
|
||||
free_at = keys.index('if (tools.mode() === "free") {')
|
||||
assert (
|
||||
free_at < keys.index("history.undo()")
|
||||
@@ -159,7 +159,7 @@ def test_도구줄_직선화_오른쪽_자유지정():
|
||||
def test_적용은_직선화와_같은_길_키_배선():
|
||||
panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert "apply: (next) => ctx.applyEdits(next)" in panel
|
||||
assert 'tools.mode() === "free" && free.key(key)' in panel
|
||||
assert 'tools.mode() === "free" && freeKey && free.key(freeKey)' in panel
|
||||
profile = _read("A00_Common/design/design_profile.ts")
|
||||
# applyEdits = 캐시(store.replace) + 되돌리기 기록 — 자동저장 없음
|
||||
assert "store.replace(next);\n history.record();" in profile
|
||||
|
||||
@@ -47,7 +47,7 @@ def test_Ctrl_Z_는_사진첩_한_줄(out):
|
||||
|
||||
def test_종단도_Ctrl_Z_줄이_사진첩_하나():
|
||||
tools = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
at = tools.index('if (isCtrlKey(event, "z") || isCtrlKey(event, "y")) {')
|
||||
at = tools.index('if (redo || keyIs(event, "b05.undo")) {') # 588-3 등록부
|
||||
assert (
|
||||
"undoOwner" not in tools and "history.undo()" in tools[at : at + 600]
|
||||
) # 367-3 자유지정 분기 앞에 섬
|
||||
|
||||
@@ -57,7 +57,7 @@ def test_다시하기는_앞으로(out) -> None:
|
||||
def test_단축키는_사진첩_한_곳():
|
||||
tools = (ROOT / "B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts").read_text(encoding="utf-8")
|
||||
assert "undoOwner" not in tools
|
||||
assert 'isCtrlKey(event, "y")' in tools and "setPageHistory(history)" in tools
|
||||
assert 'keyIs(event, "b05.redo")' in tools and "setPageHistory(history)" in tools # 588-3
|
||||
plan = (ROOT / "B05_Profile/B05_Profile_UI_RouteEdit_History.ts").read_text(encoding="utf-8")
|
||||
assert '"keydown"' not in plan
|
||||
assert not (ROOT / "B05_Profile/B05_Profile_UI_RouteEdit_UndoOwner.ts").exists()
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""PLAN 588 — 단축키 등록부 · 겹침 판정 · CAD 리본 묶음 · 찾기 (요구 445 C2 · C3).
|
||||
|
||||
· 등록부 = `A00_Common/key_registry.ts` — ☰ [설정] 표가 여기서 그려짐 · 바꿀 수 있는 줄은 코드가 부름
|
||||
· 키 글 = 한글 자판에서도 자판 자리로(Ctrl+ㅋ = Ctrl+Z) · 바꾼 키만 먹음 · 못 바꾸는 줄은 바꾼 값 무시
|
||||
· 겹침 = 같은 화면 자리끼리 · 「모든 화면」과 · 브라우저 몫 · CAD 글자 단독 — 다른 화면끼리는 같은 키 됨
|
||||
· CAD 묶음 = 리본 탭 › 패널 · 154 명령이 꼭 한 묶음 · 기타 여섯(리본에 없음)
|
||||
짝: `helper_588_key_registry.cjs`(실제 TS 를 Node 로 돌림).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
with open(os.path.join(ROOT, rel), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_588_key_registry.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_CAD_154_명령_꼭_한_묶음_기타_여섯(r):
|
||||
ids = r["groupIds"]
|
||||
assert len(ids) == 154 and len(set(ids)) == 154
|
||||
assert sorted(r["other"]) == ["LAYER", "PLACETABLE", "RECOVER", "REDO", "TOAREA", "UNDO"]
|
||||
assert r["groups"][0]["label"] == "홈 › 그리기"
|
||||
assert r["groups"][-1]["label"] == "" # 기타 이름은 화면 글(locale)
|
||||
|
||||
|
||||
def test_찾기_낱말_모두_대소문자_무시(r):
|
||||
f = r["find"]
|
||||
assert {"LINE", "PLINE"} <= set(f["sun"]) and {"LINE", "PLINE"} <= set(f["L"])
|
||||
assert "PLINE" in f["sunPl"] and "LINE" not in f["sunPl"] # 낱말 모두
|
||||
assert f["blank"] == 154 and f["none"] == []
|
||||
|
||||
|
||||
def test_키_글_한글_자판_안전(r):
|
||||
c = r["chord"]
|
||||
assert c["hangulCtrlZ"] == "Ctrl+Z"
|
||||
assert c["ctrlShiftZ"] == "Ctrl+Shift+Z"
|
||||
assert c["meta"] == "Ctrl+Z"
|
||||
assert c["space"] == "Space"
|
||||
assert c["shiftOnly"] == "" and c["process"] == ""
|
||||
assert c["f3"] == "F3"
|
||||
assert c["shiftDigit"] == "Ctrl+Shift+1"
|
||||
assert c["arrow"] == "Shift+ArrowUp" and c["label"] == "Shift+↑"
|
||||
|
||||
|
||||
def test_바꾼_키만_먹음_못_바꾸는_줄은_무시(r):
|
||||
k = r["keyIs"]
|
||||
assert k["delDefault"] is True and k["hangulUndo"] is True
|
||||
assert k["delAfter"] is False and k["ctrlDAfter"] is True
|
||||
assert k["fixedIgnored"] == ["Escape"]
|
||||
assert k["cadAction"] == "cad.grid"
|
||||
|
||||
|
||||
def test_겹침_판정(r):
|
||||
c = r["check"]
|
||||
assert c["none"] == {}
|
||||
assert c["sameScope"] == {"plan.delete": "dup"}
|
||||
assert c["withAll"] == {"plan.delete": "dup"}
|
||||
assert c["withLoupe"] == {"plan.delete": "dup"}
|
||||
assert c["otherPage"] == {} # D03 과 CAD 는 다른 화면
|
||||
assert c["reserved"] == {"plan.delete": "reserved"}
|
||||
assert c["cadLetter"] == {"cad.snap": "letter"}
|
||||
assert c["cadShiftLetter"] == {"cad.snap": "letter"}
|
||||
assert c["cadCtrl"] == {}
|
||||
assert c["samePage"] == {"b05.undo": "dup"} # B05 · B06 한 페이지 — 횡단 Enter 와 겹침
|
||||
assert c["swap"] == {} # 서로 바꿈은 됨
|
||||
assert c["both"] == {"plan.drawFinish": "dup", "plan.drawBack": "dup"}
|
||||
|
||||
|
||||
def test_등록부_꼴(r):
|
||||
ids = [a["id"] for a in r["actions"]]
|
||||
assert len(ids) == len(set(ids))
|
||||
for a in r["actions"]:
|
||||
assert a["scope"] in r["scopes"] and a["keys"]
|
||||
assert a["id"].startswith(a["scope"] + ".")
|
||||
|
||||
|
||||
# 바꿀 수 있는 줄 = 코드가 실제로 부름(등록부에만 있고 안 먹는 키 없음)
|
||||
WIRED = {
|
||||
"b05": "B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts",
|
||||
"profile": "B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts",
|
||||
"plan": "D03_PlanAnalysis",
|
||||
"cad": "B07_DesignDetail/openwebcad/src/inputController",
|
||||
}
|
||||
|
||||
|
||||
def _sources(rel: str) -> str:
|
||||
full = os.path.join(ROOT, rel)
|
||||
if os.path.isfile(full):
|
||||
return _read(rel)
|
||||
names = sorted(n for n in os.listdir(full) if n.endswith(".ts"))
|
||||
return "\n".join(_read(os.path.join(rel, n)) for n in names)
|
||||
|
||||
|
||||
def test_바꿀_수_있는_줄은_배선됨(r):
|
||||
for a in r["actions"]:
|
||||
if a["fixed"]:
|
||||
continue
|
||||
src = _sources(WIRED[a["scope"]])
|
||||
assert f'"{a["id"]}"' in src or f"'{a['id']}'" in src, a["id"]
|
||||
|
||||
|
||||
def test_손글_안내_지움_표는_등록부():
|
||||
assert "App_Settings_Keys_" not in _read("ui_template/ui_template_locale_common.ts")
|
||||
assert "App_Settings_Keys_" not in _read("A00_Common/app_settings.ts")
|
||||
@@ -110,7 +110,7 @@ export const ui_locales_common = {
|
||||
"그림 위를 길게 누르면 메뉴 · 두 손가락으로 확대",
|
||||
"Long-press the drawing for the menu · pinch to zoom",
|
||||
],
|
||||
/* ☰ [설정] 창(332) — 마우스 · CAD 단축키 · 다른 곳 단축키 */
|
||||
/* ☰ [설정] 창(332 · 588) — 마우스 · CAD 단축키 · 다른 곳 단축키(표 = `A00_Common/key_registry.ts`) */
|
||||
App_Settings: ["설정", "Settings"],
|
||||
App_Settings_Tab_Mouse: ["마우스", "Mouse"],
|
||||
App_Settings_Tab_Cad: ["CAD 단축키", "CAD shortcuts"],
|
||||
@@ -118,9 +118,35 @@ export const ui_locales_common = {
|
||||
App_Settings_Wheel: ["휠 확대 방향(모든 화면)", "Wheel zoom-in direction (all views)"],
|
||||
App_Settings_Wheel_Pull: ["당기면 확대(휠을 몸 쪽으로)", "Pull to zoom in (wheel toward you)"],
|
||||
App_Settings_Wheel_Push: ["밀면 확대(휠을 앞으로)", "Push to zoom in (wheel away)"],
|
||||
App_Settings_Pan: ["화면 옮기기 버튼(모든 화면)", "Pan button (all views)"],
|
||||
App_Settings_Pan_Left: ["왼쪽", "Left"],
|
||||
App_Settings_Pan_Middle: ["가운데(휠)", "Middle (wheel)"],
|
||||
App_Settings_Pan_Right: ["오른쪽", "Right"],
|
||||
App_Settings_Find: ["찾기 — 이름 · 키 · 별칭", "Find — name · key · alias"],
|
||||
App_Settings_Find_Empty: ["맞는 것 없음", "No match"],
|
||||
App_Settings_Reset: ["기본값", "Default"],
|
||||
App_Settings_Reset_All: ["모두 기본값", "All defaults"],
|
||||
App_Settings_Key_Action: ["동작", "Action"],
|
||||
App_Settings_Key_Key: ["키", "Key"],
|
||||
App_Settings_Key_Help: [
|
||||
"칸을 누르고 새 키를 누름 · 배지 줄은 못 바꿈 · [저장] 때 계정에 남음",
|
||||
"Click a box and press the new key · badge rows are fixed · saved to your account on [Save]",
|
||||
],
|
||||
App_Settings_Key_ErrDup: ["같은 화면에서 이미 쓰는 키", "Key already used on this screen"],
|
||||
App_Settings_Key_ErrReserved: ["브라우저가 쓰는 키", "Reserved by the browser"],
|
||||
App_Settings_Key_ErrLetter: [
|
||||
"CAD 는 글자 하나만으로 못 씀(명령 입력 몫)",
|
||||
"CAD cannot use a single letter (command line)",
|
||||
],
|
||||
App_Settings_Key_Invalid: ["키가 겹치거나 못 쓰는 키라 저장 안 함", "Not saved — fix the keys"],
|
||||
App_Settings_Cad_Keys: ["키 · 마우스", "Keys · mouse"],
|
||||
App_Settings_Cad_Other: ["기타", "Other"],
|
||||
/* 아래 규칙 값 = 한 줄에 「입력 = 동작」 · 줄바꿈(\n)으로 줄 나눔 · ko · en 줄 수 같게 */
|
||||
App_Settings_Rules: ["마우스 버튼", "Mouse buttons"],
|
||||
App_Settings_Rule_Pan: ["가운데 버튼 끌기 = 화면 옮김", "Middle button drag = pan"],
|
||||
App_Settings_Rule_Pan: [
|
||||
"고른 버튼 끌기 = 화면 옮김(가운데는 늘)\n고른 버튼 안 끌고 뗌 = 그 버튼 제 몫",
|
||||
"Drag with the chosen button = pan (middle always)\nClick without drag = the button's own role",
|
||||
],
|
||||
App_Settings_Rule_Pick: ["왼쪽 버튼 = 고르기", "Left button = select"],
|
||||
App_Settings_Rule_Right: [
|
||||
"오른쪽 버튼 = 메뉴\n오른쪽 버튼(CAD 명령 중) = Enter\n오른쪽 버튼(CAD 쉴 때) = 반복\nShift+오른쪽 버튼(CAD) = 스냅 메뉴\n휠 두 번 누름(CAD) = 범위",
|
||||
@@ -144,46 +170,6 @@ export const ui_locales_common = {
|
||||
App_Settings_Cad_Invalid: ["별칭에 잘못이 있어 저장 안 함", "Not saved — fix the aliases"],
|
||||
App_Settings_Saved: ["설정 저장함", "Settings saved"],
|
||||
App_Settings_Leave: ["저장 안 한 설정이 있음", "You have unsaved settings"],
|
||||
App_Settings_Keys_All: ["모든 화면", "All views"],
|
||||
App_Settings_Keys_All_Keys: [
|
||||
"Esc = ☰ · 창 닫기 · 고름 풀기\nZ 누른 채 = 돋보기\n오른쪽 버튼 0.4초 누름 = 돋보기\n돋보기 중 휠 = 배율",
|
||||
"Esc = ☰ · close window · clear selection\nHold Z = loupe\nRight button 0.4 s = loupe\nWheel in loupe = zoom",
|
||||
],
|
||||
App_Settings_Keys_Profile: ["B05 종단", "B05 profile"],
|
||||
App_Settings_Keys_Profile_Keys: [
|
||||
"Ctrl+Z = 되돌림\nCtrl+Shift+Z = 다시\n방향키 = 옮김\nEnter(자유지정) = 끝\nEsc(자유지정) = 취소\nBackspace(자유지정) = 마지막 점 빼기",
|
||||
"Ctrl+Z = undo\nCtrl+Shift+Z = redo\nArrows = move\nEnter (free mode) = finish\nEsc (free mode) = cancel\nBackspace (free mode) = remove last point",
|
||||
],
|
||||
App_Settings_Keys_RouteEdit: ["B05 노선 편집", "B05 route edit"],
|
||||
App_Settings_Keys_RouteEdit_Keys: [
|
||||
"Ctrl+Z = 되돌림\nCtrl+Y = 다시\nCtrl+Shift+Z = 다시",
|
||||
"Ctrl+Z = undo\nCtrl+Y = redo\nCtrl+Shift+Z = redo",
|
||||
],
|
||||
App_Settings_Keys_Plan: ["D03 노선분석", "D03 route analysis"],
|
||||
App_Settings_Keys_Plan_Keys: [
|
||||
"Enter(그리기) = 끝\nEsc(그리기) = 취소\nBackspace(그리기) = 마지막 점 빼기\nCtrl+Z = 되돌림\nCtrl+Y = 다시\nCtrl+Shift+Z = 다시",
|
||||
"Enter (drawing) = finish\nEsc (drawing) = cancel\nBackspace (drawing) = remove last point\nCtrl+Z = undo\nCtrl+Y = redo\nCtrl+Shift+Z = redo",
|
||||
],
|
||||
App_Settings_Keys_Section: ["B06 횡단", "B06 cross section"],
|
||||
App_Settings_Keys_Section_Keys: [
|
||||
"방향키 = 측점 옮김\n▲▼ = 측점 옮김\nEnter = 고름\nSpace = 고름\nEsc = 고름 풀기\nEsc(상세 창 고르기 중) = 고르기 취소",
|
||||
"Arrows = move station\n▲▼ = move station\nEnter = pick\nSpace = pick\nEsc = clear selection\nEsc (detail window, while picking) = cancel picking",
|
||||
],
|
||||
App_Settings_Keys_Cad: ["CAD", "CAD"],
|
||||
App_Settings_Keys_Cad_Keys: [
|
||||
"F3 = 스냅\nF7 = 그리드\nF8 = 직교\nF10 = 45°\nCtrl+Z = 되돌림\nCtrl+Y = 다시\nCtrl+S(M02) = 저장\nSpace = Enter\n↑↓ = 기록 · 최근 값\nShift+↑↓ = 화면 옮김\n글자 = 명령\nCtrl+A = 모두 고름\nDelete = 고른 것 지움\nEsc = 명령 취소 · 쉴 때 고름 풀기\nEnter = 입력 끝\nCtrl+C = 복사\nCtrl+X = 잘라 냄\nCtrl+V = 붙여넣기\nCtrl+1 = 특성\n← → = 화면 옮김",
|
||||
"F3 = snap\nF7 = grid\nF8 = ortho\nF10 = 45°\nCtrl+Z = undo\nCtrl+Y = redo\nCtrl+S (M02) = save\nSpace = Enter\n↑↓ = history · recent values\nShift+↑↓ = pan\nLetters = command\nCtrl+A = select all\nDelete = delete selection\nEsc = cancel command · clear selection when idle\nEnter = submit input\nCtrl+C = copy\nCtrl+X = cut\nCtrl+V = paste\nCtrl+1 = properties\n← → = pan",
|
||||
],
|
||||
App_Settings_Keys_Table: ["B08 · B09 표", "B08 · B09 tables"],
|
||||
App_Settings_Keys_Table_Keys: [
|
||||
"Enter = 입력\nF2 = 입력\n방향키 = 칸 옮김\nEsc = 그만",
|
||||
"Enter = edit\nF2 = edit\nArrows = move cell\nEsc = cancel",
|
||||
],
|
||||
App_Settings_Keys_M02: ["M02 양식", "M02 templates"],
|
||||
App_Settings_Keys_M02_Keys: [
|
||||
"Ctrl+S = 저장\nSpace+끌기 = 화면 옮김\nCtrl+휠 = 배율\n시트 안 = 엑셀 키 그대로\nCtrl+C(구조물 나무) = 복사\nCtrl+V(구조물 나무) = 붙여넣기\nDelete = 고른 항목 지움\nEsc = 연결 · 열린 창 취소",
|
||||
"Ctrl+S = save\nSpace+drag = pan\nCtrl+wheel = zoom\nInside a sheet = Excel keys\nCtrl+C (structure tree) = copy\nCtrl+V (structure tree) = paste\nDelete = delete selected item\nEsc = cancel link · close open window",
|
||||
],
|
||||
App_Footer_Copyright: [
|
||||
"© 2026 임도설계 및 견적 자동화. All rights reserved.",
|
||||
"© 2026 Forest Road Design & Estimation. All rights reserved.",
|
||||
|
||||
Reference in New Issue
Block a user