feat(hotkeys): three-column customize editor + larger keycap glyphs
The editable Customize Shortcuts modal now lays its categories across three columns (extracted KbEditGroup, reusing the cheatsheet's column models) so it matches the cheatsheet and avoids a tall scroll; panel widened to fit. Keycap text bumped to 15px in both modals so the arrow-key glyphs read clearly. cc
This commit is contained in:
2 files changed
+137
-120
No files matched your search
@@ -22,9 +22,124 @@ import {
|
|||||||
KeyboardShortcutsState,
|
KeyboardShortcutsState,
|
||||||
KeybindingsState,
|
KeybindingsState,
|
||||||
KeybindingsActions,
|
KeybindingsActions,
|
||||||
|
KeybindingCategoryGroup,
|
||||||
} from "../state.slint";
|
} from "../state.slint";
|
||||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||||
|
|
||||||
|
// One editable category block (header + capture rows). Extracted so the body
|
||||||
|
// can lay the categories out across three columns instead of one tall column.
|
||||||
|
component KbEditGroup inherits VerticalLayout {
|
||||||
|
in property <KeybindingCategoryGroup> group;
|
||||||
|
spacing: Spacing.sm;
|
||||||
|
|
||||||
|
// Category header.
|
||||||
|
Text {
|
||||||
|
text: group.label;
|
||||||
|
color: Theme.text-muted;
|
||||||
|
font-size: Typography.legal;
|
||||||
|
font-weight: Typography.semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in group.rows: HorizontalLayout {
|
||||||
|
spacing: 10px;
|
||||||
|
min-height: 36px;
|
||||||
|
|
||||||
|
// LEFT: action label.
|
||||||
|
Text {
|
||||||
|
text: entry.label;
|
||||||
|
color: Theme.text-primary;
|
||||||
|
font-size: Typography.body;
|
||||||
|
vertical-alignment: center;
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIGHT: keycap capture button + reset, conflict message below.
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: 4px;
|
||||||
|
alignment: center;
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
spacing: 8px;
|
||||||
|
alignment: end;
|
||||||
|
|
||||||
|
// Keycap button (Radius.sm rect, NOT a pill — ADR-008).
|
||||||
|
capture := Rectangle {
|
||||||
|
property <bool> recording: KeybindingsState.recording-id == entry.id;
|
||||||
|
property <bool> conflicting: self.recording && KeybindingsState.conflict-label != "";
|
||||||
|
width: Math.max(120px, cap-label.preferred-width + 24px);
|
||||||
|
height: 32px;
|
||||||
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
|
border-radius: Radius.sm;
|
||||||
|
background: capture.recording
|
||||||
|
? Theme.bg-hover
|
||||||
|
: (cap-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
|
||||||
|
border-width: (capture.recording || entry.modified) ? 2px : 1px;
|
||||||
|
border-color: capture.conflicting
|
||||||
|
? Theme.danger
|
||||||
|
: (capture.recording
|
||||||
|
? Theme.accent
|
||||||
|
: (entry.modified ? Theme.accent : Theme.border-subtle));
|
||||||
|
|
||||||
|
cap-label := Text {
|
||||||
|
text: capture.recording
|
||||||
|
? (KeybindingsState.pending-display != ""
|
||||||
|
? KeybindingsState.pending-display
|
||||||
|
: @tr("Press keys…"))
|
||||||
|
: entry.shortcut;
|
||||||
|
color: Theme.text-primary;
|
||||||
|
font-size: Typography.body;
|
||||||
|
font-weight: Typography.medium;
|
||||||
|
font-family: "monospace";
|
||||||
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||||
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
cap-ta := TouchArea {
|
||||||
|
mouse-cursor: pointer;
|
||||||
|
clicked => {
|
||||||
|
KeybindingsActions.start-record(entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-row reset — only when modified.
|
||||||
|
if entry.modified: reset-row := Rectangle {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
|
border-radius: Radius.sm;
|
||||||
|
background: reset-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: Theme.border-subtle;
|
||||||
|
QbzIcon {
|
||||||
|
source: @image-url("../assets/icons/rotate-ccw.svg");
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||||
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
|
tint: reset-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
||||||
|
}
|
||||||
|
reset-ta := TouchArea {
|
||||||
|
mouse-cursor: pointer;
|
||||||
|
clicked => {
|
||||||
|
KeybindingsActions.reset-one(entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflict message BELOW the keycap.
|
||||||
|
if KeybindingsState.recording-id == entry.id
|
||||||
|
&& KeybindingsState.conflict-label != "": Text {
|
||||||
|
text: @tr("Already used by") + " " + KeybindingsState.conflict-label;
|
||||||
|
color: Theme.danger;
|
||||||
|
font-size: Typography.legal;
|
||||||
|
horizontal-alignment: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export component CustomizeShortcutsModal inherits Rectangle {
|
export component CustomizeShortcutsModal inherits Rectangle {
|
||||||
visible: KeyboardShortcutsState.customize-open;
|
visible: KeyboardShortcutsState.customize-open;
|
||||||
|
|
||||||
@@ -45,7 +160,7 @@ export component CustomizeShortcutsModal inherits Rectangle {
|
|||||||
// The panel is a FocusScope so Escape cancels recording if recording,
|
// The panel is a FocusScope so Escape cancels recording if recording,
|
||||||
// else closes the modal (mirrors the Svelte handler).
|
// else closes the modal (mirrors the Svelte handler).
|
||||||
keys := FocusScope {
|
keys := FocusScope {
|
||||||
width: Math.min(root.width - 80px, 720px);
|
width: Math.min(root.width - 80px, 1080px);
|
||||||
height: Math.min(Math.max(420px, panel.preferred-height), root.height - 80px);
|
height: Math.min(Math.max(420px, panel.preferred-height), root.height - 80px);
|
||||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
@@ -157,130 +272,30 @@ export component CustomizeShortcutsModal inherits Rectangle {
|
|||||||
viewport-width: self.width;
|
viewport-width: self.width;
|
||||||
viewport-height: groups-col.preferred-height;
|
viewport-height: groups-col.preferred-height;
|
||||||
|
|
||||||
groups-col := VerticalLayout {
|
// Categories laid out across THREE columns (round-robin
|
||||||
|
// by index, same split as the read-only cheatsheet) so
|
||||||
|
// the editor fits without a tall scroll.
|
||||||
|
groups-col := HorizontalLayout {
|
||||||
padding-left: Spacing.lg;
|
padding-left: Spacing.lg;
|
||||||
padding-right: Spacing.lg;
|
padding-right: Spacing.lg;
|
||||||
padding-top: Spacing.md;
|
padding-top: Spacing.md;
|
||||||
padding-bottom: Spacing.md;
|
padding-bottom: Spacing.md;
|
||||||
spacing: Spacing.lg;
|
spacing: Spacing.lg;
|
||||||
|
|
||||||
for group in KeybindingsState.groups: VerticalLayout {
|
VerticalLayout {
|
||||||
spacing: Spacing.sm;
|
alignment: start;
|
||||||
|
spacing: Spacing.lg;
|
||||||
// Category header.
|
for group in KeybindingsState.groups-col1: KbEditGroup { group: group; }
|
||||||
Text {
|
}
|
||||||
text: group.label;
|
VerticalLayout {
|
||||||
color: Theme.text-muted;
|
alignment: start;
|
||||||
font-size: Typography.legal;
|
spacing: Spacing.lg;
|
||||||
font-weight: Typography.semibold;
|
for group in KeybindingsState.groups-col2: KbEditGroup { group: group; }
|
||||||
}
|
}
|
||||||
|
VerticalLayout {
|
||||||
for entry in group.rows: HorizontalLayout {
|
alignment: start;
|
||||||
spacing: 10px;
|
spacing: Spacing.lg;
|
||||||
min-height: 36px;
|
for group in KeybindingsState.groups-col3: KbEditGroup { group: group; }
|
||||||
|
|
||||||
// LEFT: action label.
|
|
||||||
Text {
|
|
||||||
text: entry.label;
|
|
||||||
color: Theme.text-primary;
|
|
||||||
font-size: Typography.body;
|
|
||||||
vertical-alignment: center;
|
|
||||||
horizontal-stretch: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RIGHT: keycap capture button + reset.
|
|
||||||
// A small column so the conflict message
|
|
||||||
// can sit BELOW the keycap while recording.
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: 4px;
|
|
||||||
alignment: center;
|
|
||||||
|
|
||||||
HorizontalLayout {
|
|
||||||
spacing: 8px;
|
|
||||||
alignment: end;
|
|
||||||
|
|
||||||
// Keycap button — Radius.sm rounded
|
|
||||||
// rect with a border (NOT a pill,
|
|
||||||
// ADR-008). Accent border while
|
|
||||||
// recording; danger border on
|
|
||||||
// conflict.
|
|
||||||
capture := Rectangle {
|
|
||||||
property <bool> recording: KeybindingsState.recording-id == entry.id;
|
|
||||||
property <bool> conflicting: self.recording && KeybindingsState.conflict-label != "";
|
|
||||||
width: Math.max(120px, cap-label.preferred-width + 24px);
|
|
||||||
height: 32px;
|
|
||||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
||||||
border-radius: Radius.sm;
|
|
||||||
background: capture.recording
|
|
||||||
? Theme.bg-hover
|
|
||||||
: (cap-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
|
|
||||||
border-width: (capture.recording || entry.modified) ? 2px : 1px;
|
|
||||||
border-color: capture.conflicting
|
|
||||||
? Theme.danger
|
|
||||||
: (capture.recording
|
|
||||||
? Theme.accent
|
|
||||||
: (entry.modified ? Theme.accent : Theme.border-subtle));
|
|
||||||
|
|
||||||
cap-label := Text {
|
|
||||||
text: capture.recording
|
|
||||||
? (KeybindingsState.pending-display != ""
|
|
||||||
? KeybindingsState.pending-display
|
|
||||||
: @tr("Press keys…"))
|
|
||||||
: entry.shortcut;
|
|
||||||
color: Theme.text-primary;
|
|
||||||
font-size: Typography.legal;
|
|
||||||
font-weight: Typography.medium;
|
|
||||||
font-family: "monospace";
|
|
||||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
||||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
cap-ta := TouchArea {
|
|
||||||
mouse-cursor: pointer;
|
|
||||||
clicked => {
|
|
||||||
KeybindingsActions.start-record(entry.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-row reset — only when modified.
|
|
||||||
if entry.modified: reset-row := Rectangle {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
||||||
border-radius: Radius.sm;
|
|
||||||
background: reset-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: Theme.border-subtle;
|
|
||||||
QbzIcon {
|
|
||||||
source: @image-url("../assets/icons/rotate-ccw.svg");
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
||||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
||||||
tint: reset-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
||||||
}
|
|
||||||
reset-ta := TouchArea {
|
|
||||||
mouse-cursor: pointer;
|
|
||||||
clicked => {
|
|
||||||
KeybindingsActions.reset-one(entry.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conflict message BELOW the keycap —
|
|
||||||
// recording stays active so the user can
|
|
||||||
// pick another combo.
|
|
||||||
if KeybindingsState.recording-id == entry.id
|
|
||||||
&& KeybindingsState.conflict-label != "": Text {
|
|
||||||
text: @tr("Already used by") + " " + KeybindingsState.conflict-label;
|
|
||||||
color: Theme.danger;
|
|
||||||
font-size: Typography.legal;
|
|
||||||
horizontal-alignment: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ component KbGroupBlock inherits VerticalLayout {
|
|||||||
keycap-label := Text {
|
keycap-label := Text {
|
||||||
text: entry.shortcut;
|
text: entry.shortcut;
|
||||||
color: Theme.text-secondary;
|
color: Theme.text-secondary;
|
||||||
font-size: Typography.legal;
|
// Bigger than the rest so the arrow-key glyphs (← → ↑ ↓)
|
||||||
|
// read clearly — their heads were faint at 13px.
|
||||||
|
font-size: Typography.body;
|
||||||
font-family: "monospace";
|
font-family: "monospace";
|
||||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
|
|||||||
Reference in new issue
Block a user