diff --git a/absolute_layout.go b/absolute_layout.go index 4b52853..fe863ce 100644 --- a/absolute_layout.go +++ b/absolute_layout.go @@ -71,6 +71,7 @@ func (w *AbsoluteLayout) Init(id string, s tcell.Style) { w.visible = true w.defAnchor = AnchorTL w.wCoords = make(map[Widget]Coord) + w.wAnchor = make(map[Widget]AbsoluteAnchor) } func (w *AbsoluteLayout) Id() string { return w.id } diff --git a/alert.go b/alert.go new file mode 100644 index 0000000..a3fe3d9 --- /dev/null +++ b/alert.go @@ -0,0 +1,116 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import ( + "fmt" + "strings" + + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +type Alert struct { + id string + style tcell.Style + + x, y int + w, h int + active bool + visible bool + + title string + message *Text + btnOk, btnCancel *Button +} + +var _ Widget = (*Alert)(nil) + +func NewAlert(id string, style tcell.Style) *Alert { + ret := &Alert{} + ret.Init(id, style) + return ret +} + +func (w *Alert) Init(id string, style tcell.Style) { + w.id = id + w.style = style + w.message = NewText(fmt.Sprintf("%s-text", id), style) + w.btnOk = NewButton(fmt.Sprintf("%s-select", id), style) + w.btnOk.SetLabel("Ok") + w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style) + w.btnCancel.SetLabel("Cancel") +} +func (w *Alert) Id() string { return w.id } +func (w *Alert) HandleResize(ev *tcell.EventResize) { + w.btnOk.SetPos(Coord{X: w.x + w.w - w.btnOk.WantW(), Y: w.y + w.h - 1}) + w.btnCancel.SetPos(Coord{X: w.x + 1, Y: w.y + w.h - 1}) +} + +func (w *Alert) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + return false +} +func (w *Alert) HandleTime(ev *tcell.EventTime) {} +func (w *Alert) Draw(screen tcell.Screen) { + if !w.visible { + return + } + dS := w.style + if !w.active { + dS = dS.Dim(true) + } + wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, wh.BRD_SIMPLE, w.style, screen) + w.message.Draw(screen) + w.btnOk.Draw(screen) + w.btnCancel.Draw(screen) +} +func (w *Alert) Active() bool { return w.active } +func (w *Alert) SetActive(a bool) { w.active = a } +func (w *Alert) Visible() bool { return w.visible } +func (w *Alert) SetVisible(a bool) { w.visible = a } +func (w *Alert) SetX(x int) { w.x = x } +func (w *Alert) SetY(y int) { w.y = y } +func (w *Alert) GetX() int { return w.x } +func (w *Alert) GetY() int { return w.y } +func (w *Alert) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *Alert) SetW(x int) { w.w = x } +func (w *Alert) SetH(y int) { w.h = y } +func (w *Alert) GetW() int { return w.w } +func (w *Alert) GetH() int { return w.y } +func (w *Alert) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *Alert) Focusable() bool { return true } +func (w *Alert) WantW() int { + return w.btnOk.WantW() + w.btnCancel.WantW() + 4 +} + +func (w *Alert) WantH() int { + msg := len(strings.Split(wh.WrapText(w.message.GetText(), w.WantW()), "\n")) + return 2 + w.btnOk.WantH() + msg +} + +func (w *Alert) SetTitle(ttl string) { w.title = ttl } +func (w *Alert) SetMessage(msg string) { + w.message.SetText(msg) +} diff --git a/cli.go b/cli.go index e0e743a..94a9561 100644 --- a/cli.go +++ b/cli.go @@ -53,6 +53,8 @@ type Cli struct { commands []string commandMap map[string]cliCommand commandGuessMap map[string]func(args ...string) string + + keyMap KeyMap } func NewCli(id string, s tcell.Style) *Cli { @@ -64,6 +66,7 @@ func NewCli(id string, s tcell.Style) *Cli { func (w *Cli) Init(id string, s tcell.Style) { w.id, w.style = id, s w.visible = true + w.initKeyMap() } func (w *Cli) Id() string { return w.id } @@ -84,80 +87,10 @@ func (w *Cli) HandleKey(ev *tcell.EventKey) bool { w.value = w.value[:w.cursor] + w.value[w.cursor+1:] } } - if done := h.HandleKeys(*ev, map[tcell.Key]func() bool{ - tcell.KeyEsc: func() bool { - w.SetActive(false) - return true - }, - tcell.KeyUp: func() bool { - if w.historyPosition < len(w.history)-1 { - w.historyPosition++ - w.value = w.history[w.historyPosition] - w.cursor = len(w.value) - return true - } - return false - }, - tcell.KeyDown: func() bool { - if w.historyPosition > -1 { - w.historyPosition-- - if w.historyPosition == -1 { - w.value = "" - } else { - w.value = w.history[w.historyPosition] - } - w.cursor = len(w.value) - return true - } - return false - }, - tcell.KeyLeft: func() bool { - if w.cursor > 0 { - w.cursor-- - return true - } - return false - }, - tcell.KeyRight: func() bool { - if w.cursor < len(w.value) { - w.cursor++ - return true - } - return false - }, - tcell.KeyCtrlU: func() bool { - w.value = w.value[w.cursor:] - w.cursor = 0 - return true - }, - tcell.KeyTab: func() bool { - // Auto-complete - guess := w.findBestGuess() - if guess != "" { - w.value = guess - w.cursor = len(w.value) - return true - } - return false - }, - tcell.KeyEnter: func() bool { - cmds := strings.Split(w.value, " ") - if v, ok := w.commandMap[cmds[0]]; ok { - w.history = append(w.history, w.value) - w.historyPosition = -1 - w.value = "" - w.cursor = 0 - if len(cmds) > 1 { - return v(cmds[1:]...) - } else { - return v() - } - } - return true - }, - }); done { - return done + if ok := w.keyMap.Handle(ev); ok { + return true } + var ch string if h.KeyIsDisplayable(*ev) { ch = string(ev.Rune()) @@ -237,6 +170,80 @@ func (w *Cli) SetSize(c Coord) { w.w, w.h = c.X, c.Y } func (w *Cli) WantW() int { return w.w } func (w *Cli) WantH() int { return w.h } +func (w *Cli) initKeyMap() { + w.keyMap = NewKeyMap(map[tcell.Key]func() bool{ + tcell.KeyEsc: func() bool { + w.SetActive(false) + return true + }, + tcell.KeyUp: func() bool { + if w.historyPosition < len(w.history)-1 { + w.historyPosition++ + w.value = w.history[w.historyPosition] + w.cursor = len(w.value) + return true + } + return false + }, + tcell.KeyDown: func() bool { + if w.historyPosition > -1 { + w.historyPosition-- + if w.historyPosition == -1 { + w.value = "" + } else { + w.value = w.history[w.historyPosition] + } + w.cursor = len(w.value) + return true + } + return false + }, + tcell.KeyLeft: func() bool { + if w.cursor > 0 { + w.cursor-- + return true + } + return false + }, + tcell.KeyRight: func() bool { + if w.cursor < len(w.value) { + w.cursor++ + return true + } + return false + }, + tcell.KeyCtrlU: func() bool { + w.value = w.value[w.cursor:] + w.cursor = 0 + return true + }, + tcell.KeyTab: func() bool { + // Auto-complete + guess := w.findBestGuess() + if guess != "" { + w.value = guess + w.cursor = len(w.value) + return true + } + return false + }, + tcell.KeyEnter: func() bool { + cmds := strings.Split(w.value, " ") + if v, ok := w.commandMap[cmds[0]]; ok { + w.history = append(w.history, w.value) + w.historyPosition = -1 + w.value = "" + w.cursor = 0 + if len(cmds) > 1 { + return v(cmds[1:]...) + } else { + return v() + } + } + return true + }, + }) +} func (w *Cli) SetTitle(ttl string) { w.title = ttl } func (w *Cli) Title() string { return w.title } func (w *Cli) SetValue(val string) { diff --git a/field.go b/field.go index a8556c4..345eb4b 100644 --- a/field.go +++ b/field.go @@ -41,9 +41,10 @@ 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 } var _ Widget = (*Field)(nil) @@ -62,6 +63,13 @@ func (w *Field) Init(id string, style tcell.Style) { return h.IsBS(*ev) || h.KeyIsDisplayable(*ev) } + w.keyMap = NewKeyMap(map[tcell.Key]func() bool{ + tcell.KeyLeft: w.handleLeft, + tcell.KeyRight: w.handleRight, + tcell.KeyHome: w.handleHome, + tcell.KeyEnd: w.handleEnd, + tcell.KeyCtrlU: w.clearValueBeforeCursor, + }) } func (w *Field) Id() string { return w.id } @@ -70,17 +78,10 @@ func (w *Field) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false } - if h.IsBS(*ev) { return w.handleBackspace() } - if h.HandleKeys(*ev, map[tcell.Key]func() bool{ - tcell.KeyLeft: w.handleLeft, - tcell.KeyRight: w.handleRight, - tcell.KeyHome: w.handleHome, - tcell.KeyEnd: w.handleEnd, - tcell.KeyCtrlU: w.clearValueBeforeCursor, - }) { + if ok := w.keyMap.Handle(ev); ok { return true } if w.filter != nil && !w.filter(ev) { diff --git a/filepicker.go b/filepicker.go new file mode 100644 index 0000000..467bfe1 --- /dev/null +++ b/filepicker.go @@ -0,0 +1,142 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import ( + "fmt" + "os" + + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +type FilePicker struct { + id string + title string + style tcell.Style + active bool + visible bool + focusable bool + + x, y int + w, h int + wantW, wantH int + + path string + wrkDir *os.File + + fileList *List + btnSelect, btnCancel *Button +} + +var _ Widget = (*FilePicker)(nil) + +func NewFilePicker(id string, style tcell.Style) *FilePicker { + ret := &FilePicker{style: style} + ret.Init(id, style) + return ret +} + +func (w *FilePicker) Init(id string, style tcell.Style) { + w.id = id + w.style = style + w.btnSelect = NewButton(fmt.Sprintf("%s-select", id), style) + w.btnSelect.SetLabel("Select") + w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style) + w.btnCancel.SetLabel("Cancel") +} +func (w *FilePicker) Id() string { return w.id } +func (w *FilePicker) HandleResize(ev *tcell.EventResize) { + w.btnSelect.SetPos(Coord{X: w.x + w.w - w.btnSelect.WantW(), Y: w.y + w.h - 1}) + w.btnCancel.SetPos(Coord{X: w.x + 1, Y: w.y + w.h - 1}) +} + +func (w *FilePicker) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + return false +} +func (w *FilePicker) HandleTime(ev *tcell.EventTime) {} +func (w *FilePicker) Draw(screen tcell.Screen) { + if !w.visible { + return + } + ds := w.style + if !w.active { + ds = ds.Dim(true) + } + wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, wh.BRD_SIMPLE, ds, screen) + // TODO: Draw the file picker + wh.DrawText(w.x+1, w.y+1, "TODO: Draw Filepicker", ds, screen) + w.btnSelect.Draw(screen) + w.btnCancel.Draw(screen) +} +func (w *FilePicker) Active() bool { return w.active } +func (w *FilePicker) SetActive(a bool) { w.active = a } +func (w *FilePicker) Visible() bool { return w.visible } +func (w *FilePicker) SetVisible(a bool) { w.visible = a } +func (w *FilePicker) SetX(x int) { w.x = x } +func (w *FilePicker) SetY(y int) { w.y = y } +func (w *FilePicker) GetX() int { return w.x } +func (w *FilePicker) GetY() int { return w.y } +func (w *FilePicker) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *FilePicker) SetW(x int) { w.w = x } +func (w *FilePicker) SetH(y int) { w.h = y } +func (w *FilePicker) GetW() int { return w.w } +func (w *FilePicker) GetH() int { return w.y } +func (w *FilePicker) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *FilePicker) Focusable() bool { return w.focusable } +func (w *FilePicker) WantW() int { + // borders + the greater of the buttons next to each other or the list width + return wh.Max((w.btnSelect.WantW()+w.btnCancel.WantW()), w.fileList.WantW()) + 2 +} + +func (w *FilePicker) WantH() int { + // borders + list + buttons + return 2 + w.fileList.WantH() + w.btnSelect.WantH() +} + +func (w *FilePicker) SetTitle(ttl string) { w.title = ttl } +func (w *FilePicker) SetPath(path string) error { + var err error + var fl *os.File + if fl, err = os.Open(path); err != nil { + return err + } + if fs, err := fl.Stat(); err != nil { + return err + } else if !fs.IsDir() { + return fmt.Errorf("path must be a directory") + } + w.wrkDir = fl + w.path = path + return nil +} + +func (w *FilePicker) SetOnSelect(sel func() bool) { + w.btnSelect.SetOnPressed(sel) +} + +func (w *FilePicker) SetOnCancel(cnc func() bool) { + w.btnCancel.SetOnPressed(cnc) +} diff --git a/helpers/event_helpers.go b/helpers/event_helpers.go index 64c2c9f..9c7c2c6 100644 --- a/helpers/event_helpers.go +++ b/helpers/event_helpers.go @@ -85,13 +85,4 @@ func KeyIsSymbol(ev tcell.EventKey) bool { return false } -func HandleKeys(ev tcell.EventKey, keys map[tcell.Key]func() bool) bool { - for k, v := range keys { - if ev.Key() == k { - return v() - } - } - return false -} - func HasCtrl(ev *tcell.EventKey) bool { return ev.Modifiers()&tcell.ModCtrl == tcell.ModCtrl } diff --git a/keymap.go b/keymap.go new file mode 100644 index 0000000..0f75cb2 --- /dev/null +++ b/keymap.go @@ -0,0 +1,50 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import "github.com/gdamore/tcell" + +type KeyMap map[tcell.Key]func() bool + +func BlankKeyMap() KeyMap { + return KeyMap(make(map[tcell.Key]func() bool)) +} + +func NewKeyMap(m map[tcell.Key]func() bool) KeyMap { + return KeyMap(m) +} + +func (m KeyMap) Add(k tcell.Key, do func() bool) { m[k] = do } +func (m KeyMap) Remove(k tcell.Key) { + if _, ok := m[k]; ok { + delete(m, k) + } +} + +func (m KeyMap) Handle(ev *tcell.EventKey) bool { + for k, v := range m { + if ev.Key() == k { + return v() + } + } + return false +} diff --git a/list.go b/list.go new file mode 100644 index 0000000..a19d2a0 --- /dev/null +++ b/list.go @@ -0,0 +1,191 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import ( + h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +type List struct { + id string + title string + style tcell.Style + active bool + visible bool + focusable bool + + x, y int + w, h int + wantW, wantH int + + border []rune + cursor int + cursorWrap bool + list []string + + onSelect func(string) bool + keyMap KeyMap +} + +var _ Widget = (*List)(nil) + +func NewList(id string, style tcell.Style) *List { + ret := &List{style: style} + ret.Init(id, style) + return ret +} + +func (w *List) Init(id string, style tcell.Style) { + w.id = id + w.style = style + w.keyMap = NewKeyMap(map[tcell.Key]func() bool{ + tcell.KeyUp: w.MoveUp, + tcell.KeyDown: w.MoveDown, + tcell.KeyEnter: func() bool { + if w.onSelect != nil && w.cursor < len(w.list) { + return w.onSelect(w.list[w.cursor]) + } + return false + }, + }) +} +func (w *List) Id() string { return w.id } +func (w *List) HandleResize(ev *tcell.EventResize) {} + +func (w *List) HandleKey(ev *tcell.EventKey) bool { + if !w.active || !w.focusable { + return false + } + return false +} +func (w *List) HandleTime(ev *tcell.EventTime) {} +func (w *List) Draw(screen tcell.Screen) { + dS := w.style + if !w.active { + dS = dS.Dim(true) + } + x, y := w.x, w.y + brdSz := 0 + if len(w.border) > 0 { + brdSz = 2 + if len(w.title) > 0 { + h.TitledBorderFilled(x, y, x+w.w, y+w.h, w.title, w.border, dS, screen) + } else { + h.BorderFilled(x, y, x+w.w, y+w.h, w.border, dS, screen) + } + } + x, y = x+1, y+1 + for i := range w.list { + if i == w.cursor { + dS = dS.Reverse(true) + } + txt := w.list[i] + if len(txt) > w.w-brdSz { + txt = txt[:(w.w - brdSz)] + h.DrawText(x, y, txt, dS, screen) + } + } +} +func (w *List) Active() bool { return w.active } +func (w *List) SetActive(a bool) { w.active = a } +func (w *List) Visible() bool { return w.visible } +func (w *List) SetVisible(a bool) { w.visible = a } +func (w *List) SetX(x int) { w.x = x } +func (w *List) SetY(y int) { w.y = y } +func (w *List) GetX() int { return w.x } +func (w *List) GetY() int { return w.y } +func (w *List) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *List) SetW(x int) { w.w = x } +func (w *List) SetH(y int) { w.h = y } +func (w *List) GetW() int { return w.w } +func (w *List) GetH() int { return w.y } +func (w *List) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *List) Focusable() bool { return w.focusable } +func (w *List) WantW() int { + lng := h.Longest(w.list) + if len(w.border) > 0 { + return lng + 2 + } + return lng +} + +func (w *List) WantH() int { + lng := len(w.list) + if len(w.border) > 0 { + return lng + 2 + } + return lng +} + +func (w *List) SetFocusable(f bool) { w.focusable = f } + +func (w *List) SetCursorWrap(b bool) { w.cursorWrap = b } + +func (w *List) MoveUp() bool { + if w.cursor > 0 { + w.cursor-- + return true + } else if w.cursorWrap { + w.cursor = len(w.list) - 1 + return true + } + return false +} + +func (w *List) MoveDown() bool { + if w.cursor < len(w.list) { + w.cursor++ + return true + } else if w.cursorWrap { + w.cursor = 0 + return true + } + return false +} + +func (w *List) SetTitle(ttl string) { w.title = ttl } +func (w *List) SetList(l []string) { w.list = l } +func (w *List) Add(l string) { w.list = append(w.list, l) } +func (w *List) Remove(l string) { + var idx int + var found bool + for idx = range w.list { + if w.list[idx] == l { + found = true + break + } + } + if found { + w.list = append(w.list[:idx], w.list[idx+1:]...) + } +} + +func (w *List) SetBorder(brd []rune) { + if len(brd) == 0 { + w.border = h.BRD_SIMPLE + } else { + w.border = h.ValidateBorder(brd) + } +} + +func (w *List) ClearBorder() { w.border = []rune{} } diff --git a/menu.go b/menu.go index 6d244d9..8ce6cbd 100644 --- a/menu.go +++ b/menu.go @@ -41,6 +41,8 @@ type Menu struct { onPressed func() bool manualExpand bool expanded bool + + keyMap KeyMap } type MenuType int @@ -60,19 +62,7 @@ func (w *Menu) Init(id string, style tcell.Style) { w.id = id w.style = style w.visible = true -} -func (w *Menu) Id() string { return w.id } -func (w *Menu) HandleResize(ev *tcell.EventResize) {} -func (w *Menu) HandleKey(ev *tcell.EventKey) bool { - if !w.active { - return false - } - // See if the active menuitem consumes this event - if w.items[w.cursor].HandleKey(ev) { - return true - } - // Otherwise see if we handle it - if h.HandleKeys(*ev, map[tcell.Key]func() bool{ + w.keyMap = NewKeyMap(map[tcell.Key]func() bool{ tcell.KeyRight: w.MoveRight, tcell.KeyLeft: w.MoveLeft, tcell.KeyUp: w.MoveUp, @@ -83,7 +73,18 @@ func (w *Menu) HandleKey(ev *tcell.EventKey) bool { } return false }, - }) { + }) +} +func (w *Menu) Id() string { return w.id } +func (w *Menu) HandleResize(ev *tcell.EventResize) {} +func (w *Menu) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } else if w.items[w.cursor].HandleKey(ev) { + // See if the active menuitem consumes this event + return true + } else if ok := w.keyMap.Handle(ev); ok { + // Otherwise see if we handle it return true } // See if we can find an item that matches the key pressed @@ -109,13 +110,14 @@ func (w *Menu) drawHMenu(screen tcell.Screen) { h.DrawText(x, y, w.label, st, screen) x = x + len(w.label) + 1 } - h.DrawText(x, y, "-", w.style, screen) x += 2 - for i := range w.items { - w.items[i].SetActive(w.active && w.cursor == i) - w.items[i].SetPos(Coord{X: x, Y: y}) - w.items[i].Draw(screen) - x += w.items[i].WantW() + 1 + if w.expanded || (w.active && !w.manualExpand) { + for i := range w.items { + w.items[i].SetActive(w.active && w.cursor == i) + w.items[i].SetPos(Coord{X: x, Y: y}) + w.items[i].Draw(screen) + x += w.items[i].WantW() + 1 + } } } diff --git a/prompt.go b/prompt.go new file mode 100644 index 0000000..c6ef34f --- /dev/null +++ b/prompt.go @@ -0,0 +1,123 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import ( + "fmt" + "strings" + + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +type Prompt struct { + id string + style tcell.Style + + x, y int + w, h int + active bool + visible bool + + title string + message *Text + field *Field + btnOk, btnCancel *Button + onOk func(string) bool +} + +var _ Widget = (*Prompt)(nil) + +func NewPrompt(id string, style tcell.Style) *Prompt { + ret := &Prompt{} + ret.Init(id, style) + return ret +} + +func (w *Prompt) Init(id string, style tcell.Style) { + w.id = id + w.style = style + w.message = NewText(fmt.Sprintf("%s-text", id), style) + w.field = NewField(fmt.Sprintf("%s-field", id), style) + w.btnOk = NewButton(fmt.Sprintf("%s-select", id), style) + w.btnOk.SetLabel("Ok") + w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style) + w.btnCancel.SetLabel("Cancel") +} +func (w *Prompt) Id() string { return w.id } +func (w *Prompt) HandleResize(ev *tcell.EventResize) { + w.message.SetPos(Coord{X: w.x + 1, Y: w.y + 1}) + w.field.SetPos(Coord{X: w.x + 1, Y: WidgetBottom(w.message)}) + w.btnOk.SetPos(Coord{X: w.x + w.w - w.btnOk.WantW(), Y: w.y + w.h - 1}) + w.btnCancel.SetPos(Coord{X: w.x + 1, Y: w.y + w.h - 1}) +} + +func (w *Prompt) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + return false +} +func (w *Prompt) HandleTime(ev *tcell.EventTime) {} +func (w *Prompt) Draw(screen tcell.Screen) { + if !w.visible { + return + } + dS := w.style + if !w.active { + dS = dS.Dim(true) + } + wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, wh.BRD_SIMPLE, w.style, screen) + w.message.Draw(screen) + w.btnOk.Draw(screen) + w.btnCancel.Draw(screen) +} +func (w *Prompt) Active() bool { return w.active } +func (w *Prompt) SetActive(a bool) { w.active = a } +func (w *Prompt) Visible() bool { return w.visible } +func (w *Prompt) SetVisible(a bool) { w.visible = a } +func (w *Prompt) SetX(x int) { w.x = x } +func (w *Prompt) SetY(y int) { w.y = y } +func (w *Prompt) GetX() int { return w.x } +func (w *Prompt) GetY() int { return w.y } +func (w *Prompt) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *Prompt) SetW(x int) { w.w = x } +func (w *Prompt) SetH(y int) { w.h = y } +func (w *Prompt) GetW() int { return w.w } +func (w *Prompt) GetH() int { return w.y } +func (w *Prompt) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *Prompt) Focusable() bool { return true } +func (w *Prompt) WantW() int { + return w.btnOk.WantW() + w.btnCancel.WantW() + 4 +} + +func (w *Prompt) WantH() int { + msg := len(strings.Split(wh.WrapText(w.message.GetText(), w.WantW()), "\n")) + return 2 + w.field.WantH() + w.btnOk.WantH() + msg +} + +func (w *Prompt) SetTitle(ttl string) { w.title = ttl } +func (w *Prompt) SetMessage(msg string) { + w.message.SetText(msg) +} + +func (w *Prompt) SetOnOk(f func(string) bool) { w.onOk = f } diff --git a/searcher.go b/searcher.go index b0f1b28..2506db5 100644 --- a/searcher.go +++ b/searcher.go @@ -47,6 +47,8 @@ type Searcher struct { selectFunc func(idx int, s string) bool hideOnSelect bool logger func(string) + + keyMap KeyMap } var _ Widget = (*Searcher)(nil) @@ -65,7 +67,15 @@ func (w *Searcher) Init(id string, style tcell.Style) { w.visible = true w.search.SetOnChange(func(prev, curr string) { w.updateFilter() - w.Log("Updated Search:" + curr) + }) + w.keyMap = NewKeyMap(map[tcell.Key]func() bool{ + tcell.KeyUp: w.handleKeyUp, + tcell.KeyDown: w.handleKeyDown, + tcell.KeyHome: w.handleKeyHome, + tcell.KeyEnd: w.handleKeyEnd, + tcell.KeyPgUp: w.handleKeyPgUp, + tcell.KeyPgDn: w.handleKeyPgDn, + tcell.KeyEnter: w.handleKeyEnter, }) } @@ -74,17 +84,7 @@ func (w *Searcher) HandleResize(ev *tcell.EventResize) {} func (w *Searcher) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false - } - - if h.HandleKeys(*ev, map[tcell.Key]func() bool{ - tcell.KeyUp: w.handleKeyUp, - tcell.KeyDown: w.handleKeyDown, - tcell.KeyHome: w.handleKeyHome, - tcell.KeyEnd: w.handleKeyEnd, - tcell.KeyPgUp: w.handleKeyPgUp, - tcell.KeyPgDn: w.handleKeyPgDn, - tcell.KeyEnter: w.handleKeyEnter, - }) { + } else if ok := w.keyMap.Handle(ev); ok { return true } return w.search.HandleKey(ev) diff --git a/text.go b/text.go index b35a32c..ae2b912 100644 --- a/text.go +++ b/text.go @@ -77,3 +77,4 @@ func (w *Text) SetSize(c Coord) { w.w, w.h = c.X, c.Y } func (w *Text) Focusable() bool { return false } func (w *Text) SetText(txt string) { w.text = txt } +func (w *Text) GetText() string { return w.text } diff --git a/widget.go b/widget.go index be17ad0..e8ca151 100644 --- a/widget.go +++ b/widget.go @@ -26,6 +26,8 @@ import "github.com/gdamore/tcell" type Widget interface { Init(string, tcell.Style) Id() string + // HandleResize sets things up to be drawn based on the width and height + // given to it through SetW & SetH HandleResize(*tcell.EventResize) HandleKey(*tcell.EventKey) bool HandleTime(*tcell.EventTime) @@ -40,15 +42,27 @@ type Widget interface { GetX() int GetY() int SetPos(Coord) + // Whatever is managing this widget (parent widget, screen, etc) should + // tell it exactly what the Width & Height are. + // It _should_ try to base this off of WantW & WantH SetW(int) SetH(int) GetW() int GetH() int + // Given infinite space, WantW & WantH are what this widget wants WantW() int WantH() int SetSize(Coord) } +func WidgetBottom(w Widget) int { + return w.GetY() + w.GetH() +} + +func WidgetRight(w Widget) int { + return w.GetX() + w.GetW() +} + type Coord struct { X, Y int }