diff --git a/ui/list_timers.go b/ui/list_timers.go index bb28e0c..04244f0 100644 --- a/ui/list_timers.go +++ b/ui/list_timers.go @@ -12,6 +12,13 @@ import ( "github.com/nsf/termbox-go" ) +const ( + activeToggleActive = iota + activeToggleInactive + activeToggleAll + activeToggleErr +) + type listTimersScreen struct { ui *Ui @@ -21,10 +28,12 @@ type listTimersScreen struct { cursor int - fullList *timertxt.TimerList - timerList *timertxt.TimerList - doneList *timertxt.TimerList + activeToggle int + fullList *timertxt.TimerList + timerList *timertxt.TimerList + doneList *timertxt.TimerList + fullFilterList *timertxt.TimerList timerFilterList *timertxt.TimerList doneFilterList *timertxt.TimerList @@ -33,7 +42,9 @@ type listTimersScreen struct { inputDialog *widdles.InputDialog filter string - partManager *PartManager + choiceMenu *widdles.MenuV + tagEditor *PromptForTagWiddle + //partManager *PartManager msg string err error @@ -49,7 +60,9 @@ func NewListTimersScreen(u *Ui) *listTimersScreen { scrollbar: widdles.NewScrollbar(w-2, 2, 1, h-2), selected: make(map[int]bool), inputDialog: widdles.NewInputDialog("", ""), - partManager: NewPartManager("", 0, 0, 0, 0), + + choiceMenu: widdles.NewMenuV(0, 0, 0, 0), + tagEditor: NewPromptForTagWiddle(0, 0, widdles.AUTO_SIZE, widdles.AUTO_SIZE, "", ""), } return &s } @@ -74,14 +87,23 @@ func (s *listTimersScreen) Init() wandle.Cmd { s.fullList = timertxt.NewTimerList() s.fullList.AddTimers(s.timerList.GetTimerSlice()) s.fullList.AddTimers(s.doneList.GetTimerSlice()) + s.fullFilterList = s.fullList s.timerFilterList, s.doneFilterList = s.timerList, s.doneList + s.fullFilterList.Sort(timertxt.SORT_START_DATE_DESC) s.timerFilterList.Sort(timertxt.SORT_START_DATE_DESC) s.doneFilterList.Sort(timertxt.SORT_START_DATE_DESC) w, h := termbox.Size() - s.partManager.SetX(w / 4) - s.partManager.SetY(h / 4) - s.partManager.SetWidth(w / 2) - s.partManager.SetHeight(h / 2) + s.choiceMenu.SetBorder(wandle.BRD_CSIMPLE) + s.choiceMenu.SetX((w / 2) - 7) + s.choiceMenu.SetY((h / 2) - 7) + s.choiceMenu.SetWidth(widdles.AUTO_SIZE) + s.choiceMenu.SetHeight(widdles.AUTO_SIZE) + s.choiceMenu.SetPadding(0, 1, 0, 1) + s.tagEditor.SetX(w / 4) + s.tagEditor.SetY(h / 4) + s.tagEditor.SetWidth(w / 2) + s.tagEditor.SetHeight(h / 2) + s.updateFullFilterList() return nil } @@ -98,44 +120,50 @@ func (s *listTimersScreen) Update(msg wandle.Msg) wandle.Cmd { func (s *listTimersScreen) View(style wandle.Style) { _, h := termbox.Size() y := 2 - wandle.Print(1, y, style.Bold(true), "Active Timers") - y++ - for idx, tmr := range s.timerFilterList.GetTimerSlice() { - if y > h-2 { - break - } - st := style - if s.cursor == idx { - st = st.Invert() - } - if s.selected[idx] { - wandle.Print(1, y, st, "[✔] ") - } else { - wandle.Print(1, y, st, "[ ] ") - } - s.ViewTimer(5, y, st, tmr) + printedTimers := 0 + if s.activeToggle == activeToggleAll || s.activeToggle == activeToggleActive { + wandle.Print(1, y, style.Bold(true), "Active Timers") y++ - } - y++ - wandle.Print(1, y, style.Bold(true), "Done Timers") - y++ - for idx, tmr := range s.doneFilterList.GetTimerSlice() { - if y > h-3 { - break + for idx, tmr := range s.timerFilterList.GetTimerSlice() { + if y > h-2 { + break + } + st := style + if s.cursor == idx { + st = st.Invert() + } + if s.selected[idx] { + wandle.Print(1, y, st, "[✔] ") + } else { + wandle.Print(1, y, st, "[ ] ") + } + s.ViewTimer(5, y, st, tmr) + y++ } - st := style - if s.cursor == s.timerFilterList.Size()+idx { - st = st.Invert() - } - if s.selected[s.timerFilterList.Size()+idx] { - wandle.Print(1, y, st, "[✔] ") - } else { - wandle.Print(1, y, st, "[ ] ") - } - s.ViewTimer(5, y, st, tmr) y++ + printedTimers = s.timerFilterList.Size() } - wandle.Print(1, h-1, style, "[p]roject(+), [c]ontext(@), [t]ags(:)") + if s.activeToggle == activeToggleAll || s.activeToggle == activeToggleInactive { + wandle.Print(1, y, style.Bold(true), "Done Timers") + y++ + for idx, tmr := range s.doneFilterList.GetTimerSlice() { + if y > h-3 { + break + } + st := style + if s.cursor == printedTimers+idx { + st = st.Invert() + } + if s.selected[printedTimers+idx] { + wandle.Print(1, y, st, "[✔] ") + } else { + wandle.Print(1, y, st, "[ ] ") + } + s.ViewTimer(5, y, st, tmr) + y++ + } + } + wandle.Print(1, h-1, style, "[A]ctive toggle, [p]roject(+), [c]ontext(@), [t]ags(:)") if len(s.selected) > 0 { wandle.Print(39, h-1, style, fmt.Sprintf("(%d selected)", len(s.selected))) } @@ -144,7 +172,13 @@ func (s *listTimersScreen) View(style wandle.Style) { if s.menu.IsActive() { s.menu.View(style) } - s.partManager.View(style) + if s.choiceMenu.IsActive() { + s.choiceMenu.View(style) + } + if s.tagEditor.IsActive() { + s.tagEditor.View(style) + } + wandle.Print(1, h-2, style, s.msg) } func (s *listTimersScreen) ViewTimer(x, y int, style wandle.Style, tmr *timertxt.Timer) { @@ -157,26 +191,19 @@ func (s *listTimersScreen) ViewTimer(x, y int, style wandle.Style, tmr *timertxt } func (s *listTimersScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd { - if pmRes := s.partManager.Update(msg); pmRes != nil { - return pmRes + if s.choiceMenu.IsActive() { + return s.choiceMenu.Update(msg) + } + if s.tagEditor.IsActive() { + return s.tagEditor.Update(msg) } if (msg.Type == termbox.EventKey && msg.Key == termbox.KeyEsc) || s.menu.IsActive() { return s.menu.Update(msg) } switch msg.Type { case termbox.EventKey: - top := s.timerFilterList.Size() + s.doneFilterList.Size() - 2 - selected := len(s.selected) - var selTimer *timertxt.Timer - if selected == 0 { - if s.cursor < s.timerFilterList.Size() { - selTimer, s.err = s.timerFilterList.GetTimer(s.cursor) - } else { - selTimer, s.err = s.doneFilterList.GetTimer(s.cursor) - } - } if msg.Key == termbox.KeyEnter { - // Edit the entry + // TODO: Edit the entry /* if s.cursor >= 0 && s.cursor < s.timerFilterList.Size()+s.doneFilterList.Size() { } else { @@ -189,6 +216,12 @@ func (s *listTimersScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd { } else { s.selected[s.cursor] = true } + if s.cursor < s.fullFilterList.Size()-1 { + s.cursor++ + } + } else if msg.Ch == 'A' { + s.activeToggle = (s.activeToggle + 1) % activeToggleErr + s.updateFullFilterList() } else if msg.Key == termbox.KeyArrowUp || msg.Ch == 'k' { if s.cursor > 0 { s.cursor-- @@ -197,60 +230,172 @@ func (s *listTimersScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd { } return nil } else if msg.Key == termbox.KeyArrowDown || msg.Ch == 'j' { - if s.cursor < top { + if s.cursor < s.fullFilterList.Size()-1 { s.cursor++ } else { - s.cursor = top + s.cursor = s.fullFilterList.Size() - 1 } return nil } else if msg.Ch == 'G' { - s.cursor = top + s.cursor = s.fullFilterList.Size() - 1 } else if msg.Ch == 't' { // Edit tag(s) - tags := s.fullList.GetTagKeys() - s.partManager.SetOptions(tags) - if selected > 0 { - // TODO - //s.partManager.SetLabel(fmt.Sprintf("Manage Tags on %d Timers", selected)) - } else { - s.partManager.SetLabel("Manage Tags for Timer") - s.partManager.SetType("Tag") - } - s.partManager.Show() + return s.showEditTagsChoice() } else if msg.Ch == 'p' { // Edit project(s) + // TODO: Prompt for Choice: Add/Edit/Remove projs := s.fullList.GetProjects() - s.partManager.SetOptions(projs) - if selected > 0 { - // TODO - //s.partManager.SetLabel(fmt.Sprintf("Manage Projects on %d Timers", selected)) - } else { - s.partManager.SetLabel("Manage Projects for Timer") - s.partManager.SetType("Project") - s.partManager.SetValue(strings.Join(selTimer.Projects, " ")) - } - s.partManager.Show() + _ = projs } else if msg.Ch == 'c' { // Edit context(s) + // TODO: Prompt for choice: Add/Edit/Remove ctxts := s.fullList.GetContexts() - s.partManager.SetOptions(ctxts) - if selected > 0 { - // TODO - //s.partManager.SetLabel(fmt.Sprintf("Manage Contexts on %d Timers", selected)) - } else { - s.partManager.SetLabel("Manage Contexts for Timer") - s.partManager.SetType("Context") - s.partManager.SetValue(strings.Join(selTimer.Contexts, " ")) - } - s.partManager.Show() + _ = ctxts } } return nil } +func (s *listTimersScreen) showEditTagsChoice() wandle.Cmd { + tags := s.getSelectedTimerTags() + var showTagEditor = func(key, val string) wandle.Cmd { + return func() wandle.Msg { + s.tagEditor.SetTag(key, val) + // TODO: Set Done & Cancel Commands + s.choiceMenu.SetActive(false) + s.tagEditor.SetActive(true) + return wandle.EmptyCmd + } + } + s.choiceMenu.SetTitle("") + s.choiceMenu.ClearOptions() + addTag := widdles.NewMenuItem("Add New Tag") + addTag.SetCommand(showTagEditor("", "")) + s.choiceMenu.AddOption(addTag) + editTag := widdles.NewMenuItem("Edit Tag") + editTag.SetEnabled(len(tags) > 0) + editTag.SetCommand(func() wandle.Msg { + s.choiceMenu.ClearOptions() + s.choiceMenu.SetTitle("Choose Tag to Edit") + for k, v := range tags { + var vals string + if len(v) == 1 { + vals = v[0] + } else { + vals = "..." + } + opt := widdles.NewMenuItem(fmt.Sprintf("%s (%s)", k, vals)) + opt.SetCommand(showTagEditor(k, vals)) + s.choiceMenu.AddOption(opt) + } + return wandle.EmptyCmd + }) + s.choiceMenu.AddOption(editTag) + removeTag := widdles.NewMenuItem("Remove Tag") + removeTag.SetCommand(func() wandle.Msg { + s.choiceMenu.ClearOptions() + s.choiceMenu.SetTitle("Choose Tag to Remove") + return wandle.EmptyCmd + }) + s.choiceMenu.AddOption(removeTag) + s.choiceMenu.SetActive(true) + //tags := s.fullList.GetTagKeys() + //_ = tags + return wandle.EmptyCmd +} + +func (s *listTimersScreen) updateFullFilterList() { + s.fullFilterList = timertxt.NewTimerList() + switch s.activeToggle { + case activeToggleAll: + s.fullFilterList.Combine(s.timerFilterList) + s.fullFilterList.Combine(s.doneFilterList) + case activeToggleActive: + s.fullFilterList.Combine(s.timerFilterList) + case activeToggleInactive: + s.fullFilterList.Combine(s.doneFilterList) + } + if s.cursor >= s.fullFilterList.Size() { + s.cursor = s.fullFilterList.Size() - 1 + } +} + func (s *listTimersScreen) gotoSettingsScreen() wandle.Msg { return ScreenMsg{ source: ListTimersId, command: CmdGotoSettings, } } + +func (s *listTimersScreen) getSelectedTimerTags() map[string][]string { + ret := make(map[string][]string) + selected := len(s.selected) + var selTimer *timertxt.Timer + if selected == 0 { + if s.cursor < s.fullFilterList.Size() { + selTimer, s.err = s.fullFilterList.GetTimer(s.cursor) + for k, v := range selTimer.AdditionalTags { + ret[k] = []string{v} + } + } + } else { + for i := range s.selected { + if tmr, err := s.fullFilterList.GetTimer(i); err == nil { + for k, v := range tmr.AdditionalTags { + if !util.StringSliceContains(ret[k], v) { + ret[k] = append(ret[k], v) + } + } + } + } + } + return ret +} +func (s *listTimersScreen) getSelectedTimerProjects() []string { + var ret []string + selected := len(s.selected) + var selTimer *timertxt.Timer + if selected == 0 { + if s.cursor < s.fullFilterList.Size() { + selTimer, s.err = s.fullFilterList.GetTimer(s.cursor) + for _, v := range selTimer.Projects { + ret = append(ret, v) + } + } + } else { + for i := range s.selected { + if tmr, err := s.fullFilterList.GetTimer(i); err == nil { + for _, v := range tmr.Projects { + if !util.StringSliceContains(ret, v) { + ret = append(ret, v) + } + } + } + } + } + return ret +} +func (s *listTimersScreen) getSelectedTimerContexts() []string { + var ret []string + selected := len(s.selected) + var selTimer *timertxt.Timer + if selected == 0 { + if s.cursor < s.fullFilterList.Size() { + selTimer, s.err = s.fullFilterList.GetTimer(s.cursor) + for _, v := range selTimer.Contexts { + ret = append(ret, v) + } + } + } else { + for i := range s.selected { + if tmr, err := s.fullFilterList.GetTimer(i); err == nil { + for _, v := range tmr.Contexts { + if !util.StringSliceContains(ret, v) { + ret = append(ret, v) + } + } + } + } + } + return ret +} diff --git a/ui/widdle_addtagtotimers.go b/ui/widdle_addtagtotimers.go new file mode 100644 index 0000000..7ab2b3f --- /dev/null +++ b/ui/widdle_addtagtotimers.go @@ -0,0 +1,195 @@ +package ui + +import ( + "git.bullercodeworks.com/brian/wandle" + "git.bullercodeworks.com/brian/widdles" + "github.com/nsf/termbox-go" +) + +/* + * A widdle to prompt the user for a tag key and value + */ +type PromptForTagWiddle struct { + active bool + x, y, w, h int + + origKey, origVal string + + keyInput *widdles.ToggleField + valInput *widdles.ToggleField + + cancelButton *widdles.Button + doneButton *widdles.Button + + msg string +} + +var _ widdles.Widdle = (*PromptForTagWiddle)(nil) + +func NewPromptForTagWiddle(x, y, w, h int, key, val string) *PromptForTagWiddle { + keyInp := widdles.NewToggleField("Key", key, 0, 0, 0, 0) + keyInp.SetActive(true) + return &PromptForTagWiddle{ + x: x, y: y, w: w, h: h, + origKey: key, origVal: val, + keyInput: keyInp, + valInput: widdles.NewToggleField("Value", val, 0, 0, 0, 0), + cancelButton: widdles.NewButton("Cancel", 0, 0, 0, 0), + doneButton: widdles.NewButton("Done", 0, 0, 0, 0), + } +} + +func (w *PromptForTagWiddle) Init() wandle.Cmd { + return func() wandle.Msg { + w.Measure() + return nil + } +} +func (w *PromptForTagWiddle) Update(msg wandle.Msg) wandle.Cmd { + if w.active { + // Make sure a widdle is active + var found bool + for _, wdl := range []widdles.Widdle{w.keyInput, w.valInput, w.cancelButton, w.doneButton} { + if wdl.IsActive() { + found = true + break + } + } + if !found { + w.keyInput.SetActive(true) + } + if msg, ok := msg.(termbox.Event); ok { + return w.handleTermboxEvent(msg) + } + } + return nil +} +func (w *PromptForTagWiddle) View(style wandle.Style) { + title := "Add Tag" + if w.origKey != "" { + title = "Edit Tag" + } + wandle.TitledBorderFilled(title, w.x, w.y, w.x+w.w, w.y+w.h, style, wandle.BRD_CSIMPLE) + w.keyInput.View(style) + w.valInput.View(style) + w.cancelButton.View(style) + w.doneButton.View(style) + wandle.Print(w.x+1, w.y+w.h-2, style, w.msg) +} + +func (w *PromptForTagWiddle) IsActive() bool { return w.active } +func (w *PromptForTagWiddle) SetActive(b bool) { w.active = b } +func (w *PromptForTagWiddle) Focusable() bool { return true } +func (w *PromptForTagWiddle) SetX(x int) { + w.x = x + w.Measure() +} +func (w *PromptForTagWiddle) GetX() int { return w.x } +func (w *PromptForTagWiddle) SetY(y int) { + w.y = y + w.Measure() +} +func (w *PromptForTagWiddle) GetY() int { return w.y } +func (w *PromptForTagWiddle) SetHeight(h int) { + w.h = h + w.Measure() +} +func (w *PromptForTagWiddle) GetHeight() int { + //if w.h == widdles.AUTO_SIZE { } + return w.h +} +func (w *PromptForTagWiddle) SetWidth(v int) { + w.w = v + w.Measure() +} +func (w *PromptForTagWiddle) GetWidth() int { + //if w.w == widdles.AUTO_SIZE { } + return w.w +} +func (w *PromptForTagWiddle) Measure() { + w.keyInput.SetX(w.x + 1) + w.keyInput.SetY(w.y + 1) + w.keyInput.SetWidth(w.w - 2) + w.keyInput.SetHeight(1) + + w.valInput.SetX(w.x + 1) + w.valInput.SetY(w.y + 2) + w.valInput.SetWidth(w.w - 2) + w.valInput.SetHeight(1) + + w.doneButton.SetX(w.x + w.w - 9) + w.doneButton.SetY(w.y + w.h - 1) + w.doneButton.SetWidth(8) + w.doneButton.SetHeight(1) + + w.cancelButton.SetX(w.doneButton.GetX() - 12) + w.cancelButton.SetY(w.y + w.h - 1) + w.cancelButton.SetWidth(10) + w.cancelButton.SetHeight(1) +} + +func (w *PromptForTagWiddle) handleTermboxEvent(msg termbox.Event) wandle.Cmd { + if msg.Key == termbox.KeyEnter { + if w.keyInput.IsEditable() { + w.keyInput.SetActive(false) + w.keyInput.SetEditable(false) + w.valInput.SetActive(true) + w.valInput.SetEditable(true) + return wandle.EmptyCmd + } else if w.valInput.IsEditable() { + w.valInput.SetActive(false) + w.valInput.SetEditable(false) + if w.keyInput.GetValue() != "" && w.valInput.GetValue() != "" { + w.doneButton.SetActive(true) + } else { + w.cancelButton.SetActive(true) + } + return wandle.EmptyCmd + } + } + widdles := []widdles.Widdle{w.keyInput, w.valInput, w.cancelButton, w.doneButton} + for _, wdl := range widdles { + if wdl.IsActive() { + if ret := wdl.Update(msg); ret != nil { + return ret + } + } + } + if msg.Ch == 'j' || msg.Key == termbox.KeyArrowDown || msg.Key == termbox.KeyArrowRight { + for i := range widdles { + if widdles[i].IsActive() { + return func() wandle.Msg { + widdles[i].SetActive(false) + next := ((i + 1) + len(widdles)) % len(widdles) + widdles[next].SetActive(true) + return nil + } + } + } + } else if msg.Ch == 'k' || msg.Key == termbox.KeyArrowUp || msg.Key == termbox.KeyArrowLeft { + for i := range widdles { + if widdles[i].IsActive() { + return func() wandle.Msg { + widdles[i].SetActive(false) + next := ((i - 1) + len(widdles)) % len(widdles) + widdles[next].SetActive(true) + return nil + } + } + } + } + return nil +} +func (w *PromptForTagWiddle) SetTag(key, val string) { + w.origKey, w.origVal = key, val + w.keyInput.SetValue(key) + if key == "" && val == "" { + w.keyInput.SetEditable(true) + } + w.valInput.SetValue(val) +} + +func (w *PromptForTagWiddle) ClearCancelCommand() { w.SetCancelCommand(wandle.EmptyCmd) } +func (w *PromptForTagWiddle) SetCancelCommand(cmd func() wandle.Msg) { w.cancelButton.SetCommand(cmd) } +func (w *PromptForTagWiddle) ClearDoneCommand() { w.SetDoneCommand(wandle.EmptyCmd) } +func (w *PromptForTagWiddle) SetDoneCommand(cmd func() wandle.Msg) { w.doneButton.SetCommand(cmd) } diff --git a/util/helpers.go b/util/helpers.go index b2dcafe..e9fb802 100644 --- a/util/helpers.go +++ b/util/helpers.go @@ -426,3 +426,12 @@ func SortedTagKeyList(m map[string]string) []string { sort.Strings(ret) return ret } + +func StringSliceContains(sl []string, val string) bool { + for i := range sl { + if sl[i] == val { + return true + } + } + return false +}