diff --git a/CHANGELOG b/CHANGELOG index df2b9c8..7807240 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ v1.4.5 (WIP) - Add multithreading support +- Add ContextMenu (initially supported by List) - Merge upstream mouse support v1.4.4 (2020-02-24) diff --git a/README.md b/README.md index 26e7c6f..bc01c7f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Available widgets: - Navigable multi-color __text views__ - Sophisticated navigable __table views__ - Flexible __tree views__ -- Selectable __lists__ +- Selectable __lists__ with __context menus__ - __Grid__, __Flexbox__ and __page layouts__ - Modal __message windows__ - Horizontal and vertical __progress bars__ diff --git a/box.go b/box.go index 07f868f..d2acbb2 100644 --- a/box.go +++ b/box.go @@ -50,6 +50,9 @@ type Box struct { // Whether or not this box has focus. hasFocus bool + // Whether or not this box shows its focus. + showFocus bool + // An optional capture function which receives a key event and returns the // event to be forwarded to the primitive's default input handler (nil if // nothing should be forwarded). @@ -76,6 +79,7 @@ func NewBox() *Box { borderColor: Styles.BorderColor, titleColor: Styles.TitleColor, titleAlign: AlignCenter, + showFocus: true, } b.focus = b return b @@ -389,7 +393,7 @@ func (b *Box) Draw(screen tcell.Screen) { } else { hasFocus = b.focus.HasFocus() } - if hasFocus { + if hasFocus && b.showFocus { horizontal = Borders.HorizontalFocus vertical = Borders.VerticalFocus topLeft = Borders.TopLeftFocus diff --git a/contextmenu.go b/contextmenu.go new file mode 100644 index 0000000..ad085a3 --- /dev/null +++ b/contextmenu.go @@ -0,0 +1,151 @@ +package cview + +import "sync" + +// ContextMenu is a menu that appears upon user interaction, such as right +// clicking or pressing Alt+Enter. +type ContextMenu struct { + parent Primitive + item int + open bool + drag bool + list *List + x, y int + selected func(int, string, rune) + + l sync.Mutex +} + +// NewContextMenu returns a new context menu. +func NewContextMenu(parent Primitive) *ContextMenu { + c := &ContextMenu{parent: parent} + + return c +} + +// ContextMenuList returns the underlying List of the context menu. +func (c *ContextMenu) ContextMenuList() *List { + c.l.Lock() + defer c.l.Unlock() + + return c.list +} + +// AddContextItem adds an item to the context menu. Adding an item with no text +// or shortcut will add a divider. +func (c *ContextMenu) AddContextItem(text string, shortcut rune, selected func(index int)) *ContextMenu { + c.l.Lock() + defer c.l.Unlock() + + if c.list == nil { + c.list = NewList(). + ShowSecondaryText(false). + SetHover(true). + SetWrapAround(true) + c.list. + SetBorder(true). + SetBorderPadding( + Styles.ContextMenuPaddingTop, + Styles.ContextMenuPaddingBottom, + Styles.ContextMenuPaddingLeft, + Styles.ContextMenuPaddingRight) + c.list.showFocus = false + } + + c.list.AddItem(text, "", shortcut, c.wrap(selected)) + if text == "" && shortcut == 0 { + c.list.Lock() + index := len(c.list.items) - 1 + c.list.items[index].Enabled = false + c.list.Unlock() + } + + return c +} + +func (c *ContextMenu) wrap(f func(index int)) func() { + return func() { + f(c.item) + } +} + +// ClearContextMenu removes all items from the context menu. +func (c *ContextMenu) ClearContextMenu() *ContextMenu { + c.l.Lock() + defer c.l.Unlock() + + if c.list != nil { + c.list.Clear() + } + + return c +} + +// SetContextSelectedFunc sets the function which is called when the user +// selects a context menu item. The function receives the item's index in the +// menu (starting with 0), its text and its shortcut rune. SetSelectedFunc must +// be called before the context menu is shown. +func (c *ContextMenu) SetContextSelectedFunc(handler func(index int, text string, shortcut rune)) *ContextMenu { + c.l.Lock() + defer c.l.Unlock() + + c.selected = handler + return c +} + +// ShowContextMenu shows the context menu. +func (c *ContextMenu) ShowContextMenu(item int, x int, y int, setFocus func(Primitive)) { + c.l.Lock() + defer c.l.Unlock() + + c.show(item, x, y, setFocus) +} + +func (c *ContextMenu) show(item int, x int, y int, setFocus func(Primitive)) { + if c.list == nil || len(c.list.items) == 0 { + return + } + + c.open = true + c.item = item + c.x, c.y = x, y + + c.list.Lock() + for i, item := range c.list.items { + if item.Enabled { + c.list.currentItem = i + break + } + } + c.list.Unlock() + + c.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) { + c.l.Lock() + + // A context item was selected. Close the menu. + c.hide(setFocus) + + if c.selected != nil { + c.l.Unlock() + c.selected(index, mainText, shortcut) + } else { + c.l.Unlock() + } + }).SetDoneFunc(func() { + c.hide(setFocus) + }) + + setFocus(c.list) +} + +func (c *ContextMenu) hide(setFocus func(Primitive)) { + c.open = false + + if c.list == nil { + return + } + + if c.list.HasFocus() { + setFocus(c.parent) + } +} diff --git a/demos/list/main.go b/demos/list/main.go index 1460c7d..e194116 100644 --- a/demos/list/main.go +++ b/demos/list/main.go @@ -7,14 +7,51 @@ import ( func main() { app := cview.NewApplication() - list := cview.NewList(). - AddItem("List item 1", "Some explanatory text", 'a', nil). - AddItem("List item 2", "Some explanatory text", 'b', nil). - AddItem("List item 3", "Some explanatory text", 'c', nil). - AddItem("List item 4", "Some explanatory text", 'd', nil). - AddItem("Quit", "Press to exit", 'q', func() { - app.Stop() - }) + list := cview.NewList() + + reset := func() { + list. + Clear(). + AddItem("List item 1", "Some explanatory text", 'a', nil). + AddItem("List item 2", "Some explanatory text", 'b', nil). + AddItem("List item 3", "Some explanatory text", 'c', nil). + AddItem("List item 4", "Some explanatory text", 'd', nil). + AddItem("Quit", "Press to exit", 'q', func() { + app.Stop() + }) + + list.ContextMenuList().SetItemEnabled(3, false) + } + + list.AddContextItem("Delete item", 'i', func(index int) { + list.RemoveItem(index) + + if list.GetItemCount() == 0 { + list.ContextMenuList().SetItemEnabled(0, false) + list.ContextMenuList().SetItemEnabled(1, false) + } + list.ContextMenuList().SetItemEnabled(3, true) + }) + + list.AddContextItem("Delete all", 'a', func(index int) { + list.Clear() + + list.ContextMenuList().SetItemEnabled(0, false) + list.ContextMenuList().SetItemEnabled(1, false) + list.ContextMenuList().SetItemEnabled(3, true) + }) + + list.AddContextItem("", 0, nil) + + list.AddContextItem("Reset", 'r', func(index int) { + reset() + + list.ContextMenuList().SetItemEnabled(0, true) + list.ContextMenuList().SetItemEnabled(1, true) + list.ContextMenuList().SetItemEnabled(3, false) + }) + + reset() if err := app.SetRoot(list, true).EnableMouse(true).Run(); err != nil { panic(err) } diff --git a/demos/presentation/introduction.go b/demos/presentation/introduction.go index f22b651..2a0bd03 100644 --- a/demos/presentation/introduction.go +++ b/demos/presentation/introduction.go @@ -4,11 +4,49 @@ import "gitlab.com/tslocum/cview" // Introduction returns a cview.List with the highlights of the cview package. func Introduction(nextSlide func()) (title string, content cview.Primitive) { - list := cview.NewList(). - AddItem("A Go package for terminal based UIs", "with a special focus on rich interactive widgets", '1', nextSlide). - AddItem("Based on github.com/gdamore/tcell", "Like termbox but better (see tcell docs)", '2', nextSlide). - AddItem("Designed to be simple", `"Hello world" is 5 lines of code`, '3', nextSlide). - AddItem("Good for data entry", `For charts, use "termui" - for low-level views, use "gocui" - ...`, '4', nextSlide). - AddItem("Extensive documentation", "Demo code is available for each widget", '5', nextSlide) - return "Introduction", Center(80, 10, list) + list := cview.NewList() + + reset := func() { + list. + Clear(). + AddItem("A Go package for terminal based UIs", "with a special focus on rich interactive widgets", '1', nextSlide). + AddItem("Based on github.com/gdamore/tcell", "Like termbox but better (see tcell docs)", '2', nextSlide). + AddItem("Designed to be simple", `"Hello world" is 5 lines of code`, '3', nextSlide). + AddItem("Good for data entry", `For charts, use "termui" - for low-level views, use "gocui" - ...`, '4', nextSlide). + AddItem("Supports context menus", "Right click on one of these items or press Alt+Enter", '5', nextSlide). + AddItem("Extensive documentation", "Demo code is available for each widget", '6', nextSlide) + + list.ContextMenuList().SetItemEnabled(3, false) + } + + list.AddContextItem("Delete item", 'i', func(index int) { + list.RemoveItem(index) + + if list.GetItemCount() == 0 { + list.ContextMenuList().SetItemEnabled(0, false) + list.ContextMenuList().SetItemEnabled(1, false) + } + list.ContextMenuList().SetItemEnabled(3, true) + }) + + list.AddContextItem("Delete all", 'a', func(index int) { + list.Clear() + + list.ContextMenuList().SetItemEnabled(0, false) + list.ContextMenuList().SetItemEnabled(1, false) + list.ContextMenuList().SetItemEnabled(3, true) + }) + + list.AddContextItem("", 0, nil) + + list.AddContextItem("Reset", 'r', func(index int) { + reset() + + list.ContextMenuList().SetItemEnabled(0, true) + list.ContextMenuList().SetItemEnabled(1, true) + list.ContextMenuList().SetItemEnabled(3, false) + }) + + reset() + return "Introduction", Center(80, 12, list) } diff --git a/go.mod b/go.mod index ef99982..d8bcd0a 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.0.3 github.com/mattn/go-runewidth v0.0.9 github.com/rivo/uniseg v0.1.0 - golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 // indirect + golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect golang.org/x/text v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index 8de7444..00ee510 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE= -golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= diff --git a/list.go b/list.go index 9a447f2..5fdd1ff 100644 --- a/list.go +++ b/list.go @@ -10,6 +10,7 @@ import ( // listItem represents one item in a List. type listItem struct { + Enabled bool // Whether or not the list item is selectable. MainText string // The main text of the list item. SecondaryText string // A secondary text to be shown underneath the main text. Shortcut rune // The key to select the list item directly, 0 if there is no shortcut. @@ -21,6 +22,7 @@ type listItem struct { // See https://gitlab.com/tslocum/cview/wiki/List for an example. type List struct { *Box + *ContextMenu // The items of the list. items []*listItem @@ -61,6 +63,9 @@ type List struct { // Whether or not navigating the list will wrap around. wrapAround bool + // Whether or not hovering over an item will highlight it. + hover bool + // The number of list items skipped at the top before the first item is drawn. offset int @@ -80,7 +85,7 @@ type List struct { // NewList returns a new form. func NewList() *List { - return &List{ + l := &List{ Box: NewBox(), showSecondaryText: true, scrollBarVisibility: ScrollBarAuto, @@ -91,6 +96,11 @@ func NewList() *List { scrollBarColor: Styles.ScrollBarColor, selectedBackgroundColor: Styles.PrimaryTextColor, } + + l.ContextMenu = NewContextMenu(l) + l.focus = l + + return l } // SetCurrentItem sets the currently selected item by its index, starting at 0 @@ -172,7 +182,7 @@ func (l *List) RemoveItem(index int) *List { // Shift current item. previousCurrentItem := l.currentItem - if l.currentItem >= index { + if l.currentItem >= index && l.currentItem > 0 { l.currentItem-- } @@ -283,6 +293,16 @@ func (l *List) SetScrollBarColor(color tcell.Color) *List { return l } +// SetHover sets the flag that determines whether hovering over an item will +// highlight it (without triggering callbacks set with SetSelectedFunc). +func (l *List) SetHover(hover bool) *List { + l.Lock() + defer l.Unlock() + + l.hover = hover + return l +} + // SetWrapAround sets the flag that determines whether navigating the list will // wrap around. That is, navigating downwards on the last item will move the // selection to the first item (similarly in the other direction). If set to @@ -364,6 +384,7 @@ func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut ru l.Lock() item := &listItem{ + Enabled: true, MainText: mainText, SecondaryText: secondaryText, Shortcut: shortcut, @@ -433,6 +454,17 @@ func (l *List) SetItemText(index int, main, secondary string) *List { return l } +// SetItemEnabled sets whether an item is selectable. Panics if the index is +// out of range. +func (l *List) SetItemEnabled(index int, enabled bool) *List { + l.Lock() + defer l.Unlock() + + item := l.items[index] + item.Enabled = enabled + return l +} + // FindItems searches the main and secondary texts for the given strings and // returns a list of item indices in which those strings are found. One of the // two search strings may be empty, it will then be ignored. Indices are always @@ -486,9 +518,29 @@ func (l *List) Clear() *List { return l } +// Focus is called by the application when the primitive receives focus. +func (l *List) Focus(delegate func(p Primitive)) { + l.Box.Focus(delegate) + if l.ContextMenu.open { + delegate(l.ContextMenu.list) + } +} + +// HasFocus returns whether or not this primitive has focus. +func (l *List) HasFocus() bool { + l.RLock() + defer l.RUnlock() + + if l.ContextMenu.open { + return l.ContextMenu.list.HasFocus() + } + return l.hasFocus +} + // Draw draws this primitive onto the screen. func (l *List) Draw(screen tcell.Screen) { l.Box.Draw(screen) + hasFocus := l.GetFocusable().HasFocus() l.Lock() defer l.Unlock() @@ -499,7 +551,7 @@ func (l *List) Draw(screen tcell.Screen) { screenWidth, _ := screen.Size() scrollBarHeight := height - scrollBarX := x + (width - 1) + scrollBarX := x + (width - 1) + l.paddingLeft + l.paddingRight if scrollBarX > screenWidth-1 { scrollBarX = screenWidth - 1 } @@ -543,6 +595,28 @@ func (l *List) Draw(screen tcell.Screen) { break } + if item.MainText == "" && item.SecondaryText == "" && item.Shortcut == 0 { // Divider + Print(screen, string(tcell.RuneLTee), (x-5)-l.paddingLeft, y, 1, AlignLeft, l.mainTextColor) + Print(screen, strings.Repeat(string(tcell.RuneHLine), width+4+l.paddingLeft+l.paddingRight), (x-4)-l.paddingLeft, y, width+4+l.paddingLeft+l.paddingRight, AlignLeft, l.mainTextColor) + Print(screen, string(tcell.RuneRTee), (x-5)+width+5+l.paddingRight, y, 1, AlignLeft, l.mainTextColor) + + RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), l.currentItem, index-l.offset, l.hasFocus, l.scrollBarColor) + y++ + continue + } else if !item.Enabled { // Disabled item + // Shortcuts. + if showShortcuts && item.Shortcut != 0 { + Print(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray) + } + + // Main text. + Print(screen, item.MainText, x, y, width, AlignLeft, tcell.ColorGray) + + RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), l.currentItem, index-l.offset, l.hasFocus, l.scrollBarColor) + y++ + continue + } + // Shortcuts. if showShortcuts && item.Shortcut != 0 { Print(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 4, AlignRight, l.shortcutColor) @@ -595,6 +669,59 @@ func (l *List) Draw(screen tcell.Screen) { y++ } + + // Draw context menu. + if hasFocus && l.ContextMenu.open { + list := l.ContextMenu.list + + x, y, width, height = l.GetInnerRect() + + // What's the longest option text? + maxWidth := 0 + for _, option := range list.items { + strWidth := TaggedStringWidth(option.MainText) + if option.Shortcut != 0 { + strWidth += 4 + } + if strWidth > maxWidth { + maxWidth = strWidth + } + } + + lheight := len(list.items) + lwidth := maxWidth + + // Add space for borders + lwidth += 2 + lheight += 2 + + lwidth += l.list.paddingLeft + l.list.paddingRight + lheight += l.list.paddingTop + l.list.paddingBottom + + cx, cy := l.ContextMenu.x, l.ContextMenu.y + if cx < 0 || cy < 0 { + cx = x + (width / 2) + cy = y + (height / 2) + } + + _, sheight := screen.Size() + if cy+lheight >= sheight && cy-2 > lheight-cy { + cy = y - lheight + if cy < 0 { + cy = 0 + } + } + if cy+lheight >= sheight { + lheight = sheight - cy + } + + if list.scrollBarVisibility == ScrollBarAlways || (list.scrollBarVisibility == ScrollBarAuto && len(list.items) > lheight) { + lwidth++ // Add space for scroll bar + } + + list.SetRect(cx, cy, lwidth, lheight) + list.Draw(screen) + } } // InputHandler returns the handler for this primitive. @@ -603,12 +730,19 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit l.Lock() if event.Key() == tcell.KeyEscape { - if l.done != nil { - l.done() + if l.ContextMenu.open { + l.ContextMenu.hide(setFocus) + return + } + + if l.done != nil { + l.Unlock() + l.done() + } else { + l.Unlock() } - l.Unlock() return - } else if len(l.items) == 0 { + } else if len(l.items) == 0 && (event.Key() != tcell.KeyEnter || event.Modifiers()&tcell.ModAlt == 0) { l.Unlock() return } @@ -629,17 +763,40 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit case tcell.KeyPgUp: l.currentItem -= 5 case tcell.KeyEnter: - if l.currentItem >= 0 && l.currentItem < len(l.items) { - item := l.items[l.currentItem] - if item.Selected != nil { - l.Unlock() - item.Selected() - l.Lock() + if event.Modifiers()&tcell.ModAlt != 0 { + // Do we show any shortcuts? + var showShortcuts bool + for _, item := range l.items { + if item.Shortcut != 0 { + showShortcuts = true + break + } } - if l.selected != nil { - l.Unlock() - l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) - l.Lock() + + offsetX := 7 + if showShortcuts { + offsetX += 4 + } + offsetY := l.currentItem + if l.showSecondaryText { + offsetY *= 2 + } + + x, y, _, _ := l.GetInnerRect() + l.ContextMenu.show(l.currentItem, x+offsetX, y+offsetY, setFocus) + } else if l.currentItem >= 0 && l.currentItem < len(l.items) { + item := l.items[l.currentItem] + if item.Enabled { + if item.Selected != nil { + l.Unlock() + item.Selected() + l.Lock() + } + if l.selected != nil { + l.Unlock() + l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) + l.Lock() + } } } case tcell.KeyRune: @@ -648,7 +805,7 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit // It's not a space bar. Is it a shortcut? var found bool for index, item := range l.items { - if item.Shortcut == ch { + if item.Enabled && item.Shortcut == ch { // We have a shortcut. found = true l.currentItem = index @@ -672,17 +829,31 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit } } - if l.currentItem < 0 { - if l.wrapAround { - l.currentItem = len(l.items) - 1 - } else { - l.currentItem = 0 + decreasing := l.currentItem < previousItem + for i := 0; i < len(l.items); i++ { + if l.currentItem < 0 { + if l.wrapAround { + l.currentItem = len(l.items) - 1 + } else { + l.currentItem = 0 + } + } else if l.currentItem >= len(l.items) { + if l.wrapAround { + l.currentItem = 0 + } else { + l.currentItem = len(l.items) - 1 + } } - } else if l.currentItem >= len(l.items) { - if l.wrapAround { - l.currentItem = 0 + + item := l.items[l.currentItem] + if item.Enabled { + break + } + + if decreasing { + l.currentItem-- } else { - l.currentItem = len(l.items) - 1 + l.currentItem++ } } @@ -696,11 +867,31 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit }) } +// indexAtY returns the index of the list item found at the given Y position +// or a negative value if there is no such list item. +func (l *List) indexAtY(y int) int { + _, rectY, _, height := l.GetInnerRect() + if y < rectY || y >= rectY+height { + return -1 + } + + index := y - rectY + if l.showSecondaryText { + index /= 2 + } + index += l.offset + + if index >= len(l.items) { + return -1 + } + return index +} + // indexAtPoint returns the index of the list item found at the given position // or a negative value if there is no such list item. func (l *List) indexAtPoint(x, y int) int { rectX, rectY, width, height := l.GetInnerRect() - if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height { + if x < rectX || x >= rectX+width || y < rectY || y >= rectY+height { return -1 } @@ -719,6 +910,13 @@ func (l *List) indexAtPoint(x, y int) int { // MouseHandler returns the mouse handler for this primitive. func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + // Pass events to context menu. + if l.ContextMenu.open && l.ContextMenu.list != nil && l.ContextMenu.list.InRect(event.Position()) { + l.ContextMenu.list.MouseHandler()(action, event, setFocus) + consumed = true + return + } + if !l.InRect(event.Position()) { return false, nil } @@ -726,22 +924,70 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, // Process mouse event. switch action { case MouseLeftClick: + if l.ContextMenu.open { + l.ContextMenu.hide(setFocus) + consumed = true + return + } + setFocus(l) index := l.indexAtPoint(event.Position()) if index != -1 { item := l.items[index] - if item.Selected != nil { - item.Selected() + if item.Enabled { + if item.Selected != nil { + item.Selected() + } + if l.selected != nil { + l.selected(index, item.MainText, item.SecondaryText, item.Shortcut) + } + if index != l.currentItem && l.changed != nil { + l.changed(index, item.MainText, item.SecondaryText, item.Shortcut) + } + l.currentItem = index } - if l.selected != nil { - l.selected(index, item.MainText, item.SecondaryText, item.Shortcut) - } - if index != l.currentItem && l.changed != nil { - l.changed(index, item.MainText, item.SecondaryText, item.Shortcut) - } - l.currentItem = index } consumed = true + case MouseMiddleClick: + if l.ContextMenu.open { + l.ContextMenu.hide(setFocus) + consumed = true + return + } + case MouseRightDown: + x, y := event.Position() + + index := l.indexAtPoint(event.Position()) + if index != -1 { + item := l.items[index] + if item.Enabled { + if index != l.currentItem && l.changed != nil { + l.changed(index, item.MainText, item.SecondaryText, item.Shortcut) + } + l.currentItem = index + } + } + + if l.ContextMenu.list != nil && len(l.ContextMenu.list.items) > 0 { + l.ContextMenu.show(l.currentItem, x, y, setFocus) + l.ContextMenu.drag = true + } else { + defer l.MouseHandler()(MouseLeftClick, event, setFocus) + } + consumed = true + case MouseMove: + if l.hover { + _, y := event.Position() + index := l.indexAtY(y) + if index >= 0 { + item := l.items[index] + if item.Enabled { + l.currentItem = index + } + } + + consumed = true + } case MouseScrollUp: if l.offset > 0 { l.offset-- diff --git a/styles.go b/styles.go index e740b92..eb16257 100644 --- a/styles.go +++ b/styles.go @@ -4,34 +4,55 @@ import "github.com/gdamore/tcell" // Theme defines the colors used when primitives are initialized. type Theme struct { + // Title, border and other lines + TitleColor tcell.Color // Box titles. + BorderColor tcell.Color // Box borders. + GraphicsColor tcell.Color // Graphics. + + // Text + PrimaryTextColor tcell.Color // Primary text. + SecondaryTextColor tcell.Color // Secondary text (e.g. labels). + TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes). + InverseTextColor tcell.Color // Text on primary-colored backgrounds. + ContrastSecondaryTextColor tcell.Color // Secondary text on ContrastBackgroundColor-colored backgrounds. + + // Background PrimitiveBackgroundColor tcell.Color // Main background color for primitives. ContrastBackgroundColor tcell.Color // Background color for contrasting elements. MoreContrastBackgroundColor tcell.Color // Background color for even more contrasting elements. - BorderColor tcell.Color // Box borders. - TitleColor tcell.Color // Box titles. - GraphicsColor tcell.Color // Graphics. - PrimaryTextColor tcell.Color // Primary text. - SecondaryTextColor tcell.Color // Secondary text (e.g. labels). - TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes). - InverseTextColor tcell.Color // Text on primary-colored backgrounds. - ContrastSecondaryTextColor tcell.Color // Secondary text on ContrastBackgroundColor-colored backgrounds. - ScrollBarColor tcell.Color // Scroll bar. + + // Context menu + ContextMenuPaddingTop int // Top padding. + ContextMenuPaddingBottom int // Bottom padding. + ContextMenuPaddingLeft int // Left padding. + ContextMenuPaddingRight int // Right padding. + + // Scroll bar + ScrollBarColor tcell.Color // Scroll bar color. } // Styles defines the theme for applications. The default is for a black // background and some basic colors: black, white, yellow, green, cyan, and // blue. var Styles = Theme{ + TitleColor: tcell.ColorWhite, + BorderColor: tcell.ColorWhite, + GraphicsColor: tcell.ColorWhite, + + PrimaryTextColor: tcell.ColorWhite, + SecondaryTextColor: tcell.ColorYellow, + TertiaryTextColor: tcell.ColorGreen, + InverseTextColor: tcell.ColorBlue, + ContrastSecondaryTextColor: tcell.ColorDarkCyan, + PrimitiveBackgroundColor: tcell.ColorBlack, ContrastBackgroundColor: tcell.ColorBlue, MoreContrastBackgroundColor: tcell.ColorGreen, - BorderColor: tcell.ColorWhite, - TitleColor: tcell.ColorWhite, - GraphicsColor: tcell.ColorWhite, - PrimaryTextColor: tcell.ColorWhite, - SecondaryTextColor: tcell.ColorYellow, - TertiaryTextColor: tcell.ColorGreen, - InverseTextColor: tcell.ColorBlue, - ContrastSecondaryTextColor: tcell.ColorDarkCyan, - ScrollBarColor: tcell.ColorWhite, + + ContextMenuPaddingTop: 0, + ContextMenuPaddingBottom: 0, + ContextMenuPaddingLeft: 1, + ContextMenuPaddingRight: 1, + + ScrollBarColor: tcell.ColorWhite, }