diff --git a/internal/ui/form.go b/internal/ui/form.go index 07811fa..4c8194a 100644 --- a/internal/ui/form.go +++ b/internal/ui/form.go @@ -60,3 +60,33 @@ func focusedDropDown(f *tview.Form, item int) (*tview.DropDown, bool) { dd, ok := f.GetFormItem(item).(*tview.DropDown) return dd, ok } + +// bindSearchNow keeps a "Search now" checkbox consistent with whether a search +// can find anything. While searchable() is false the box is unchecked, greyed +// out and cannot be ticked (blocked is called instead); it returns to the +// user's earlier choice afterwards. The returned function must be called +// whenever the answer of searchable() may have changed. +// +// tview's SetDisabled is avoided on purpose: on a form item it re-triggers +// the form's focus handling and leaves two fields highlighted. +func bindSearchNow(box *tview.Checkbox, searchable func() bool, blocked func()) (sync func()) { + want := true + box.SetChangedFunc(func(checked bool) { + switch { + case searchable(): + want = checked + case checked: + box.SetChecked(false) + blocked() + } + }) + return func() { + if searchable() { + box.SetLabelColor(tview.Styles.SecondaryTextColor) + box.SetChecked(want) + return + } + box.SetLabelColor(tcell.ColorGray) + box.SetChecked(false) + } +} diff --git a/internal/ui/form_test.go b/internal/ui/form_test.go index b63008b..324e941 100644 --- a/internal/ui/form_test.go +++ b/internal/ui/form_test.go @@ -48,3 +48,63 @@ func TestArrowNavigation(t *testing.T) { t.Fatalf("Up from first button: item %d, want 1", item) } } + +func TestSearchNowFollowsMonitored(t *testing.T) { + f := tview.NewForm(). + AddDropDown("P", []string{"a"}, 0, nil). + AddDropDown("R", []string{"a"}, 0, nil). + AddCheckbox("M", true, nil). + AddCheckbox("S", true, nil). + AddButton("Add", nil) + arrowNavigation(f) + var focus func(p tview.Primitive) + focus = func(p tview.Primitive) { p.Focus(focus) } + f.Focus(focus) + + mon := f.GetFormItem(2).(*tview.Checkbox) + search := f.GetFormItem(3).(*tview.Checkbox) + blocked := 0 + sync := bindSearchNow(search, mon.IsChecked, func() { blocked++ }) + mon.SetChangedFunc(func(bool) { sync() }) + + cap := f.GetInputCapture() + key := func(k tcell.Key) { cap(tcell.NewEventKey(k, 0, 0)) } + space := func(item tview.FormItem) { + item.InputHandler()(tcell.NewEventKey(tcell.KeyRune, ' ', 0), func(tview.Primitive) {}) + } + focused := func() (n int) { + for i := 0; i < f.GetFormItemCount(); i++ { + if f.GetFormItem(i).HasFocus() { + n++ + } + } + return n + } + + key(tcell.KeyDown) + key(tcell.KeyDown) // on "Monitored" + space(mon) // unmonitor + if search.IsChecked() { + t.Error("Search now must be unchecked when not monitored") + } + key(tcell.KeyUp) + if n := focused(); n != 1 { + t.Fatalf("%d fields have focus after moving up, want 1", n) + } + if item, _ := f.GetFocusedItemIndex(); item != 1 { + t.Errorf("focus on item %d, want 1", item) + } + + key(tcell.KeyDown) + key(tcell.KeyDown) // on "Search now" + space(search) + if search.IsChecked() || blocked != 1 { + t.Errorf("ticking while blocked: checked=%v blocked=%d", search.IsChecked(), blocked) + } + + key(tcell.KeyUp) + space(mon) // monitor again + if !search.IsChecked() { + t.Error("Search now should return to its previous choice") + } +} diff --git a/internal/ui/search.go b/internal/ui/search.go index 022e7a0..b0811e3 100644 --- a/internal/ui/search.go +++ b/internal/ui/search.go @@ -315,26 +315,14 @@ func (v *searchView) add(i int) { // "Search now" only makes sense when something will be monitored. monitored := form.GetFormItemByLabel(lMonitor).(*tview.Checkbox) searchNow := form.GetFormItemByLabel(lSearch).(*tview.Checkbox) - wantSearch := true searchable := func() bool { if !monitored.IsChecked() { return false } return len(modes) == 0 || modes[selected(form, lEpisode)].value != sonarr.MonitorNone } - syncSearch := func() { - if searchable() { - searchNow.SetDisabled(false) - searchNow.SetChecked(wantSearch) - return - } - searchNow.SetDisabled(true) - searchNow.SetChecked(false) - } - searchNow.SetChangedFunc(func(checked bool) { - if searchable() { - wantSearch = checked - } + syncSearch := bindSearchNow(searchNow, searchable, func() { + v.app.Notify(levelWarn, "Search now needs something to be monitored") }) monitored.SetChangedFunc(func(bool) { syncSearch() }) if len(modes) > 0 {