diff --git a/input_group.go b/input_group.go index 4ec5f31..885654c 100644 --- a/input_group.go +++ b/input_group.go @@ -172,14 +172,27 @@ func InputGroup(c *ui.Context, value *string, opts InputGroupOptions) InputGroup return parts } -// inputGroupEditor builds the input; a disabled one edits a copy, so queued input cannot reach value. +// inputGroupLock remembers locks so an unlocked input starts afresh. +type inputGroupLock struct { + locked bool + gen int +} + +// inputGroupEditor builds the input, or plain selectable text while locked, so no edit command runs. func inputGroupEditor(c *ui.Context, box *ui.Element, value *string, locked bool) *ui.Element { - if !locked { - return ui.TextInputBase(c, value) + lk := ui.Local(box, "lock", func() inputGroupLock { return inputGroupLock{} }) + if lk.locked && !locked { + // A new identity drops the edits queued in the old editor while locked. + lk.gen++ } - shown := ui.Local(box, "locked", func() string { return *value }) - *shown = *value - in := ui.TextInputBase(c, shown) - *shown = *value + lk.locked = locked + var in *ui.Element + ui.Row(c).Key(lk.gen).FillWidth().MinWidth(0).Children(func() { + if locked { + in = ui.Text(c, *value).SingleLine().Selectable() + } else { + in = ui.TextInputBase(c, value) + } + }) return in } diff --git a/input_group_test.go b/input_group_test.go index 34a7e3e..91bb733 100644 --- a/input_group_test.go +++ b/input_group_test.go @@ -141,3 +141,33 @@ func TestInputGroupHitAreasCompact(t *testing.T) { t.Fatalf("button %v", r) } } + +// Cut and paste after disabling a field with a selection change neither value nor clipboard. +func TestInputGroupDisabledEditCommands(t *testing.T) { + value, disabled, changes := "secret", false, 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if InputGroup(c, &value, InputGroupOptions{Label: "Code", Disabled: disabled}).Changed() { + changes++ + } + }, 400, 100) + _ = tt.Click("Code") + tt.Key(ui.Cmd, ui.KeyA) + tt.SetClipboard("clip") + disabled = true + tt.Frame() + tt.Key(ui.Cmd, ui.KeyX) + tt.Command("cut") + tt.Command("paste") + tt.Command("undo") + tt.Frame() + if value != "secret" || tt.Clipboard() != "clip" || changes != 0 { + t.Fatalf("value %q clipboard %q changes %d", value, tt.Clipboard(), changes) + } + disabled = false + tt.Frame() + tt.Frame() + if value != "secret" || tt.Clipboard() != "clip" || changes != 0 { + t.Fatalf("after enabling: value %q clipboard %q", value, tt.Clipboard()) + } +} diff --git a/tags_input_test.go b/tags_input_test.go index fb1be8d..3028c5d 100644 --- a/tags_input_test.go +++ b/tags_input_test.go @@ -144,3 +144,35 @@ func TestTagsInputDisabledAfterFocus(t *testing.T) { t.Fatalf("tags %q changes %d", tags, n) } } + +// Cut and paste after disabling with a selected draft change neither draft nor clipboard. +func TestTagsInputDisabledEditCommands(t *testing.T) { + tags, n, seen := []string{}, 0, "" + opts := TagsInputOptions{Label: "Tags", Validate: func(s string) string { seen = s; return "" }} + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if TagsInput(c, &tags, opts).Changed() { + n++ + } + }, 520, 200) + _ = tt.Click("Tags") + tt.Type("ivy") + tt.Key(ui.Cmd, ui.KeyA) + tt.SetClipboard("clip") + opts.Disabled = true + tt.Frame() + tt.Key(ui.Cmd, ui.KeyX) + tt.Command("cut") + tt.Command("paste") + tt.Frame() + if tt.Clipboard() != "clip" || n != 0 { + t.Fatalf("clipboard %q changes %d", tt.Clipboard(), n) + } + opts.Disabled = false + tt.Frame() + _ = tt.Click("Tags") + tt.Key(0, ui.KeyEnter) + if seen != "ivy" || tt.Clipboard() != "clip" || len(tags) != 1 { + t.Fatalf("draft %q clipboard %q tags %q", seen, tt.Clipboard(), tags) + } +}