diff --git a/helpers/text_helpers.go b/helpers/text_helpers.go index faa2abd..91ec737 100644 --- a/helpers/text_helpers.go +++ b/helpers/text_helpers.go @@ -36,6 +36,11 @@ const ( AlignRight ) +// Return if r1 and r2 are the same, ignoring case +func RuneEqualsNC(r1, r2 rune) bool { + return unicode.ToLower(r1) == unicode.ToLower(r2) +} + func Center(txt string, width int) string { if len(txt) >= width { // Trim from beg & end until width diff --git a/layout_flags.go b/layout_flags.go index 49bcb1b..a088860 100644 --- a/layout_flags.go +++ b/layout_flags.go @@ -58,6 +58,11 @@ func (f LayoutFlag) SetTopLeft() { f = LFAlignHLeft | LFAlignVTop } +func (f LayoutFlag) SetBottomCenter() { + f.ClearAll() + f = LFAlignHCenter | LFAlignVBottom +} + func (f LayoutFlag) ClearAlignH() { f = f &^ LFAlignH f.Add(LFAlignHCenter) diff --git a/wdgt_blank.go b/wdgt_blank.go index a8dc3f3..b87d0ab 100644 --- a/wdgt_blank.go +++ b/wdgt_blank.go @@ -28,6 +28,8 @@ type BlankWidget struct { id string x, y int keyMap KeyMap + + wantH, wantW int } var _ Widget = (*BlankWidget)(nil) @@ -71,7 +73,10 @@ func (w *BlankWidget) SetW(wd int) {} func (w *BlankWidget) SetH(h int) {} func (w *BlankWidget) GetW() int { return 0 } func (w *BlankWidget) GetH() int { return 0 } -func (w *BlankWidget) WantW() int { return 0 } -func (w *BlankWidget) WantH() int { return 0 } +func (w *BlankWidget) WantW() int { return w.wantW } +func (w *BlankWidget) WantH() int { return w.wantH } func (w *BlankWidget) MinW() int { return 0 } func (w *BlankWidget) MinH() int { return 0 } + +func (w *BlankWidget) SetWantH(v int) { w.wantH = v } +func (w *BlankWidget) SetWantW(v int) { w.wantW = v } diff --git a/wdgt_cli.go b/wdgt_cli.go index 595f6c0..fcc6e0d 100644 --- a/wdgt_cli.go +++ b/wdgt_cli.go @@ -40,6 +40,7 @@ type Cli struct { active bool visible bool focusable bool + minimized bool title string rawLog []string @@ -77,6 +78,9 @@ func (w *Cli) Init(id string, s tcell.Style) { func (w *Cli) Id() string { return w.id } func (w *Cli) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() + if w.minimized { + w.h = 3 + } } func (w *Cli) SetKeyMap(km KeyMap) { w.keyMap = km } @@ -174,8 +178,6 @@ func (w *Cli) Draw(screen tcell.Screen) { wh.DrawText(x, y, cursor, dStyle.Reverse(w.active), screen) x += 1 wh.DrawText(x, y, wh.PadR(post, w.w-x-2), dStyle, screen) - // wh.DrawText(w.x, y+1, fmt.Sprintf("Index: %d", w.historyPosition), dStyle, screen) - // x += len(post) - 1 } func (w *Cli) Active() bool { return w.active } @@ -196,9 +198,15 @@ func (w *Cli) SetW(wd int) { w.w = wd } func (w *Cli) SetH(h int) { w.h = h } func (w *Cli) SetSize(c Coord) { w.w, w.h = c.X, c.Y } func (w *Cli) WantW() int { return wh.MaxInt } -func (w *Cli) WantH() int { return wh.MaxInt } -func (w *Cli) MinW() int { return 20 } -func (w *Cli) MinH() int { return 6 } +func (w *Cli) WantH() int { + if w.minimized { + return 3 + } else { + return wh.MaxInt + } +} +func (w *Cli) MinW() int { return 20 } +func (w *Cli) MinH() int { return 6 } func (w *Cli) initKeyMap() { w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{ @@ -341,6 +349,9 @@ func (w *Cli) Clear() { w.log = []string{} } +func (w *Cli) SetMinimized(m bool) { w.minimized = m } +func (w *Cli) IsMinimized() bool { return w.minimized } + type cliCommand func(args ...string) bool // TODO diff --git a/wdgt_field.go b/wdgt_field.go index 4f2863b..8af727a 100644 --- a/wdgt_field.go +++ b/wdgt_field.go @@ -42,7 +42,7 @@ type Field struct { x, y int w, h int - filter func(*tcell.EventKey) bool + filter func(tcell.EventKey) bool onChange func(prev, curr string) keyMap KeyMap @@ -60,9 +60,9 @@ func (w *Field) Init(id string, style tcell.Style) { w.id = id w.style = style w.visible = true - w.filter = func(ev *tcell.EventKey) bool { - return wh.IsBS(*ev) || - wh.KeyIsDisplayable(*ev) + w.filter = func(ev tcell.EventKey) bool { + return wh.IsBS(ev) || + wh.KeyIsDisplayable(ev) } w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{ tcell.KeyLeft: w.handleLeft, @@ -98,7 +98,7 @@ func (w *Field) HandleKey(ev *tcell.EventKey) bool { if ok := w.keyMap.Handle(ev); ok { return true } - if w.filter != nil && !w.filter(ev) { + if w.filter != nil && !w.filter(*ev) { return false } if ev.Key() == tcell.KeyRune { @@ -238,3 +238,17 @@ func (w *Field) doOnChange(prev, curr string) { w.onChange(prev, curr) } } + +func (w *Field) SetFilter(f func(ev tcell.EventKey) bool) { + w.filter = func(ev tcell.EventKey) bool { + // We always want to make sure we allow backspace. + if wh.IsBS(ev) { + return true + } + // We also always want to make sure it's displayable + if !wh.KeyIsDisplayable(ev) { + return false + } + return f(ev) + } +} diff --git a/wdgt_linear_layout.go b/wdgt_linear_layout.go index a05736b..387335c 100644 --- a/wdgt_linear_layout.go +++ b/wdgt_linear_layout.go @@ -275,6 +275,12 @@ func (w *LinearLayout) findActiveOrFirst() Widget { return nil } +func (w *LinearLayout) ActivateWidget(n Widget) { + for i := range w.widgets { + w.widgets[i].SetActive(w.widgets[i] == n) + } +} + func (w *LinearLayout) ActivateNext() bool { var found bool for i := range w.widgets { diff --git a/wdgt_menu.go b/wdgt_menu.go index 19885e4..743a05b 100644 --- a/wdgt_menu.go +++ b/wdgt_menu.go @@ -140,6 +140,7 @@ func (w *Menu) HandleKey(ev *tcell.EventKey) bool { } else if w.items[w.cursor].HandleKey(ev) { // Active menuitem consumes this event if ev.Key() == tcell.KeyEnter { + w.cursor = 0 w.SetActive(false) } return true @@ -148,6 +149,21 @@ func (w *Menu) HandleKey(ev *tcell.EventKey) bool { return true } // See if we can find an item that matches the key pressed + for i := range w.items { + if wh.RuneEqualsNC(ev.Rune(), w.items[i].GetHotKey()) { + if w.items[i].Active() { + if w.items[i].HandleKey(ev) { + // Reset the cursor + w.cursor = 0 + w.SetActive(false) + return true + } + } + w.cursor = i + // w.items[i].SetActive(true) + return true + } + } return false } @@ -178,9 +194,11 @@ func (w *Menu) drawHMenu(screen tcell.Screen) { } x += 2 if w.expanded || (w.active && !w.manualExpand) { + // pos := w.GetPos() // TODO: Use DrawOffset? for i := range w.items { w.items[i].SetActive(w.active && w.cursor == i) + // pos.DrawOffset(w.items[i], screen) w.items[i].SetPos(Coord{X: x, Y: y}) w.items[i].Draw(screen) x += len(w.items[i].Label()) + 2 @@ -371,8 +389,9 @@ func (w *Menu) MoveDown(ev *tcell.EventKey) bool { return true } -func (w *Menu) CreateMenuItem(lbl string, do func() bool, subItems ...*MenuItem) *MenuItem { +func (w *Menu) CreateMenuItem(lbl string, do func() bool, hotKey rune, subItems ...*MenuItem) *MenuItem { d := NewMenuItem(fmt.Sprintf("menuitem-%s", lbl), tcell.StyleDefault) + d.SetHotKey(hotKey) d.SetLabel(lbl) d.SetOnPressed(do) if len(subItems) > 0 { diff --git a/wdgt_menu_item.go b/wdgt_menu_item.go index 969a607..fb10a14 100644 --- a/wdgt_menu_item.go +++ b/wdgt_menu_item.go @@ -29,6 +29,7 @@ import ( "github.com/gdamore/tcell" ) +// TODO: Sub-Menus type MenuItem struct { id string label string @@ -44,6 +45,8 @@ type MenuItem struct { items []*MenuItem onPressed func() bool + hotKey rune + manualExpand bool expanded bool disabled bool @@ -101,6 +104,20 @@ func (w *MenuItem) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false } + // If the user hits the hotkey of this active item, call 'Enter' on it + if wh.RuneEqualsNC(ev.Rune(), w.hotKey) { + w.HandleKey(tcell.NewEventKey(tcell.KeyEnter, 0, 0)) + } + for i := range w.items { + if wh.RuneEqualsNC(ev.Rune(), w.items[i].GetHotKey()) { + if w.items[i].Active() { + return w.items[i].HandleKey(ev) + } + w.cursor = i + w.updateActive() + return true + } + } // Look for a sub-item that's selected return w.keyMap.Handle(ev) } @@ -116,17 +133,29 @@ func (w *MenuItem) Draw(screen tcell.Screen) { return } st := w.style.Dim(w.disabled).Italic(w.disabled) - // st := w.style.Reverse(w.active).Dim(w.disabled).Italic(w.disabled) x, y := w.x, w.y - // wd := w.w + fndHotKey := w.GetHotKey() != 0 if w.expanded { if len(w.items) > 0 { - wh.DrawText(x, y, fmt.Sprintf("╭%s╮", w.label), st, screen) - wh.DrawText(x+1, y, fmt.Sprintf("%s", w.label), st.Reverse(true), screen) - y += 1 - if len(w.items) > 0 { - wh.TitledBorderFilled(w.x-1, y, w.x+w.WantW(), y+w.WantH(), fmt.Sprintf("╯%s╰", strings.Repeat(" ", len(w.label))), wh.BRD_CSIMPLE, w.style, screen) + screen.SetContent(x, y, '╭', nil, st) + x += 1 + for i := range w.label { + if fndHotKey && wh.RuneEqualsNC(rune(w.label[i]), w.hotKey) { + screen.SetContent(x, y, rune(w.label[i]), nil, st.Reverse(true).Underline(true)) + fndHotKey = false + } else { + screen.SetContent(x, y, rune(w.label[i]), nil, st.Reverse(true)) + } + x += 1 } + screen.SetContent(x, y, '╮', nil, st) + x = w.x + y += 1 + if len(w.items) == 0 { + return + } + wh.BorderFilled(w.x-1, y, w.x+w.WantW(), y+w.WantH(), wh.BRD_CSIMPLE, w.style, screen) + wh.DrawText(w.x, y, fmt.Sprintf("╯%s╰", strings.Repeat(" ", len(w.label))), w.style, screen) x += 1 y += 1 // pos := w.GetPos() @@ -138,10 +167,32 @@ func (w *MenuItem) Draw(screen tcell.Screen) { y++ } } else { - wh.DrawText(x, y, fmt.Sprintf(" %s ", w.label), st.Reverse(true), screen) + screen.SetContent(x, y, ' ', nil, st) + x += 1 + for i := range w.label { + if fndHotKey && wh.RuneEqualsNC(rune(w.label[i]), w.hotKey) { + screen.SetContent(x, y, rune(w.label[i]), nil, st.Reverse(true).Underline(true)) + fndHotKey = false + } else { + screen.SetContent(x, y, rune(w.label[i]), nil, st.Reverse(true)) + } + x += 1 + } + screen.SetContent(x, y, ' ', nil, st) } } else { - wh.DrawText(x, y, fmt.Sprintf(" %s ", w.label), st, screen) + screen.SetContent(x, y, ' ', nil, st) + x += 1 + for i := range w.label { + if fndHotKey && wh.RuneEqualsNC(rune(w.label[i]), w.hotKey) { + screen.SetContent(x, y, rune(w.label[i]), nil, st.Underline(true)) + fndHotKey = false + } else { + screen.SetContent(x, y, rune(w.label[i]), nil, st) + } + x += 1 + } + screen.SetContent(x, y, ' ', nil, st) } } @@ -167,7 +218,7 @@ func (w *MenuItem) GetH() int { return w.y } func (w *MenuItem) MinW() int { return w.w } func (w *MenuItem) MinH() int { return w.y } func (w *MenuItem) WantW() int { - ret := len(w.label) + 2 + ret := len(w.label) + 4 if len(w.items) > 0 { for i := range w.items { if w.items[i].WantW() > ret { @@ -263,3 +314,6 @@ func (w *MenuItem) FindItem(id string) *MenuItem { } return nil } + +func (w *MenuItem) SetHotKey(r rune) { w.hotKey = r } +func (w *MenuItem) GetHotKey() rune { return w.hotKey } diff --git a/wdgt_top_menu_layout.go b/wdgt_top_menu_layout.go index aab0d5a..d2fa21d 100644 --- a/wdgt_top_menu_layout.go +++ b/wdgt_top_menu_layout.go @@ -220,10 +220,11 @@ func (w *TopMenuLayout) Log(txt string, args ...any) { } } -func (w *TopMenuLayout) CreateMenuItem(id, lbl string, do func() bool, subItems ...*MenuItem) *MenuItem { +func (w *TopMenuLayout) CreateMenuItem(id, lbl string, do func() bool, hotKey rune, subItems ...*MenuItem) *MenuItem { d := NewMenuItem(id, tcell.StyleDefault) d.SetLabel(lbl) d.SetOnPressed(do) + d.SetHotKey(hotKey) if len(subItems) > 0 { d.AddItems(subItems...) } @@ -247,3 +248,9 @@ func (w *TopMenuLayout) SetItemDisabled(id string, d bool) bool { } func (w *TopMenuLayout) FindItem(id string) *MenuItem { return w.menu.FindItem(id) } +func (w *TopMenuLayout) CloseMenu() { + if w.menu != nil && w.menu.Active() { + w.menu.SetActive(false) + w.widget.SetActive(true) + } +}