Files
MujicaUI/account/shortcut_recorder.go
T

220 lines
6.5 KiB
Go

package account
import (
"fmt"
"log/slog"
"runtime"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/display"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// Shortcut combines modifiers and a key; zero means unassigned.
type Shortcut struct {
Mods ui.Modifiers
Key ui.Key
}
// IsZero reports an empty shortcut.
func (s Shortcut) IsZero() bool { return s == Shortcut{} }
// keys returns platform-ordered key names.
func (s Shortcut) keys() []string {
var out []string
mac := runtime.GOOS == "darwin"
add := func(m ui.Modifiers, name string) {
if s.Mods&m != 0 {
out = append(out, name)
}
}
if mac {
add(ui.Ctrl, "ctrl")
add(ui.Alt, "alt")
add(ui.Shift, "shift")
add(ui.Super, "mod")
} else {
add(ui.Ctrl, "ctrl")
add(ui.Super, "Win")
add(ui.Alt, "alt")
add(ui.Shift, "shift")
}
return append(out, shortcutKeyName(s.Key))
}
// String spells s as keys joined by "+", as "ctrl+shift+K".
func (s Shortcut) String() string {
if s.IsZero() {
return ""
}
return strings.Join(s.keys(), "+")
}
// shortcutKeyName is the Kbd name of key.
func shortcutKeyName(k ui.Key) string {
switch {
case k >= ui.KeyA && k <= ui.KeyZ:
return string(rune('A' + k - ui.KeyA))
case k >= ui.Key0 && k <= ui.Key9:
return string(rune('0' + k - ui.Key0))
case k >= ui.KeyF1 && k <= ui.KeyF12:
return fmt.Sprintf("F%d", k-ui.KeyF1+1)
}
if n, ok := shortcutNames[k]; ok {
return n
}
panic(fmt.Sprintf("mujica: Shortcut key %d has no name", k))
}
var shortcutNames = map[ui.Key]string{
ui.KeyEnter: "enter", ui.KeyEscape: "esc", ui.KeyBackspace: "backspace", ui.KeyTab: "tab", ui.KeySpace: "Space",
ui.KeyDelete: "delete", ui.KeyHome: "Home", ui.KeyEnd: "End", ui.KeyPageUp: "PgUp", ui.KeyPageDown: "PgDn",
ui.KeyLeft: "left", ui.KeyRight: "right", ui.KeyUp: "up", ui.KeyDown: "down", ui.KeyMinus: "-", ui.KeyEqual: "=",
ui.KeyComma: ",", ui.KeyPeriod: ".", ui.KeySlash: "/", ui.KeySemicolon: ";", ui.KeyQuote: "'",
ui.KeyBracketLeft: "[", ui.KeyBracketRight: "]", ui.KeyBackslash: "\\", ui.KeyBackquote: "`",
ui.KeyInsert: "Insert", ui.KeyContextMenu: "Menu", ui.KeyBack: "Back", ui.KeyForward: "Forward",
}
// shortcutKbd displays keycaps or a muted unassigned mark.
func shortcutKbd(c *ui.Context, s Shortcut) {
if s.IsZero() {
ui.Text(c, "—").TextColor(core.Tokens(c).TextMuted)
return
}
display.Kbd(c, s.keys()...)
}
// ShortcutBinding names an action that already owns a shortcut.
type ShortcutBinding struct {
Action string
Shortcut Shortcut
}
// ShortcutRecorderOptions configure a ShortcutRecorder.
type ShortcutRecorderOptions struct {
// Taken lists occupied shortcuts; conflicts preserve the previous value.
Taken []ShortcutBinding
// Label names the recorder, as the action it sets.
Label string
Disabled bool
}
type shortcutRecorderState struct {
recording, needMod bool
caught *Shortcut
conflict string
}
// ShortcutRecorder records key combinations; Escape cancels.
func ShortcutRecorder(c *ui.Context, value *Shortcut, opts ShortcutRecorderOptions) core.ChoiceResult {
if opts.Label == "" {
panic("mujica: ShortcutRecorder needs a Label")
}
d := core.Density(c)
var r core.ChoiceResult
r.Element = ui.Column(c).Gap(d.Unit()).AlignItems(ui.Start).Disabled(opts.Disabled)
r.Element.Children(func() {
b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).MinWidth(160).Padding(0, d.Unit()*3).Gap(d.Unit() * 2).
Radius(theme.ControlRadius).Justify(ui.SpaceBetween)
st := ui.Local(b, "rec", func() shortcutRecorderState { return shortcutRecorderState{} })
disabled := b.IsDisabled()
if disabled || !b.Focused() {
st.recording, st.caught = false, nil
}
if st.caught != nil {
s := *st.caught
st.caught, st.recording, st.conflict = nil, false, ""
for _, t := range opts.Taken {
if t.Shortcut == s && s != *value {
st.conflict = fmt.Sprintf(core.Msg(c, "shortcut_recorder.conflict"), s.String(), t.Action)
}
}
if st.conflict == "" && s != *value {
*value, r = s, core.NewChoiceResult(r.Element, true)
slog.Debug("mujica: shortcut_recorder set", "shortcut", s.String())
}
}
if b.Clicked() && !disabled {
st.recording, st.conflict, st.needMod = true, "", false
slog.Debug("mujica: shortcut_recorder recording")
}
if st.recording {
b.HandleInput(func(ev ui.InputEvent) bool {
if b.IsDisabled() {
return false
}
return shortcutRecorderKey(st, ev)
})
}
shortcutRecorderLook(c, b, st, *value, opts.Label)
shortcutRecorderNote(c, st)
})
return r
}
// shortcutRecorderKey captures keys during recording.
func shortcutRecorderKey(st *shortcutRecorderState, ev ui.InputEvent) bool {
switch {
case ev.Kind == ui.InputText:
return true
case ev.Kind != ui.InputKeyDown || ev.Key == ui.KeyUnknown:
return false
case ev.Key == ui.KeyTab && ev.Mods&^ui.Shift == 0:
return false
case ev.Key == ui.KeyEscape && ev.Mods == 0:
st.recording = false
case ev.Mods&^ui.Shift == 0 && !(ev.Key >= ui.KeyF1 && ev.Key <= ui.KeyF12):
st.needMod = true
default:
s := Shortcut{ev.Mods, ev.Key}
st.caught = &s
}
return true
}
// shortcutRecorderLook displays the shortcut or recording prompt.
func shortcutRecorderLook(c *ui.Context, b *ui.Element, st *shortcutRecorderState, value Shortcut, name string) {
k := core.Tokens(c)
internal.Ring(b, k.Focus, theme.ControlRadius)
label := name + ": " + value.String()
if value.IsZero() {
label = name + ": " + core.Msg(c, "shortcut_recorder.none")
}
b.Label(label)
bg, border := k.Surface, k.ControlBorder
if st.recording {
bg, border = k.Selection, k.AccentText
} else if b.Hovered() {
bg = k.SurfaceHover
}
b.Background(bg).Border(theme.BorderWidth, border)
b.Children(func() {
if st.recording {
ui.Text(c, core.Msg(c, "shortcut_recorder.press")).TextColor(k.AccentText).SingleLine()
} else {
shortcutKbd(c, value)
}
ui.Icon(c, icons.Must("command")).Size(14, 14).TextColor(k.TextMuted)
})
}
// shortcutRecorderNote displays conflict and modifier guidance.
func shortcutRecorderNote(c *ui.Context, st *shortcutRecorderState) {
text := st.conflict
if st.needMod && st.recording {
text = core.Msg(c, "shortcut_recorder.needs_modifier")
}
if text == "" {
return
}
ui.Row(c).Gap(4).AlignItems(ui.Center).TextColor(core.Tokens(c).Warning).Role(ui.RoleStatus).Children(func() {
ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14)
ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize))
})
}