Add ContextMenu

Initially supported by List
pull/434/head
Trevor Slocum 5 years ago
parent 8e06c826b3
commit c111597668

@ -13,7 +13,7 @@ Among these components are:
- Navigable multi-color __text views__ - Navigable multi-color __text views__
- Sophisticated navigable __table views__ - Sophisticated navigable __table views__
- Flexible __tree views__ - Flexible __tree views__
- Selectable __lists__ - Selectable __lists__ with __context menus__
- __Grid__, __Flexbox__ and __page layouts__ - __Grid__, __Flexbox__ and __page layouts__
- Modal __message windows__ - Modal __message windows__
- An __application__ wrapper - An __application__ wrapper

@ -52,6 +52,9 @@ type Box struct {
// Whether or not this box has focus. // Whether or not this box has focus.
hasFocus bool hasFocus bool
// Whether or not this box shows its focus.
showFocus bool
// An optional capture function which receives a key event and returns the // 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 // event to be forwarded to the primitive's default input handler (nil if
// nothing should be forwarded). // nothing should be forwarded).
@ -76,6 +79,7 @@ func NewBox() *Box {
borderColor: Styles.BorderColor, borderColor: Styles.BorderColor,
titleColor: Styles.TitleColor, titleColor: Styles.TitleColor,
titleAlign: AlignCenter, titleAlign: AlignCenter,
showFocus: true,
} }
b.focus = b b.focus = b
return b return b
@ -342,7 +346,13 @@ func (b *Box) Draw(screen tcell.Screen) {
if b.border && b.width >= 2 && b.height >= 2 { if b.border && b.width >= 2 && b.height >= 2 {
border := background.Foreground(b.borderColor) | tcell.Style(b.borderAttributes) border := background.Foreground(b.borderColor) | tcell.Style(b.borderAttributes)
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
if b.focus.HasFocus() { var hasFocus bool
if b.focus == b {
hasFocus = b.hasFocus
} else {
hasFocus = b.focus.HasFocus()
}
if hasFocus && b.showFocus {
horizontal = Borders.HorizontalFocus horizontal = Borders.HorizontalFocus
vertical = Borders.VerticalFocus vertical = Borders.VerticalFocus
topLeft = Borders.TopLeftFocus topLeft = Borders.TopLeftFocus

@ -0,0 +1,171 @@
package tview
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 {
return &ContextMenu{
parent: parent,
}
}
func (c *ContextMenu) initializeList() {
if c.list != nil {
return
}
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
}
// ContextMenuList returns the underlying List of the context menu.
func (c *ContextMenu) ContextMenuList() *List {
c.l.Lock()
defer c.l.Unlock()
c.initializeList()
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()
c.initializeList()
c.list.AddItem(text, "", shortcut, c.wrap(selected))
if text == "" && shortcut == 0 {
index := len(c.list.items) - 1
c.list.items[index].Enabled = false
}
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()
c.initializeList()
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)
}
// HideContextMenu hides the context menu.
func (c *ContextMenu) HideContextMenu(setFocus func(Primitive)) {
c.l.Lock()
defer c.l.Unlock()
c.hide(setFocus)
}
// ContextMenuVisible returns whether or not the context menu is visible.
func (c *ContextMenu) ContextMenuVisible() bool {
c.l.Lock()
defer c.l.Unlock()
return c.open
}
func (c *ContextMenu) show(item int, x int, y int, setFocus func(Primitive)) {
c.initializeList()
if len(c.list.items) == 0 {
return
}
c.open = true
c.item = item
c.x, c.y = x, y
for i, item := range c.list.items {
if item.Enabled {
c.list.currentItem = i
break
}
}
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.initializeList()
c.open = false
if c.list.HasFocus() {
setFocus(c.parent)
}
}

@ -7,7 +7,11 @@ import (
func main() { func main() {
app := tview.NewApplication() app := tview.NewApplication()
list := tview.NewList(). list := tview.NewList()
reset := func() {
list.
Clear().
AddItem("List item 1", "Some explanatory text", 'a', nil). AddItem("List item 1", "Some explanatory text", 'a', nil).
AddItem("List item 2", "Some explanatory text", 'b', nil). AddItem("List item 2", "Some explanatory text", 'b', nil).
AddItem("List item 3", "Some explanatory text", 'c', nil). AddItem("List item 3", "Some explanatory text", 'c', nil).
@ -15,6 +19,39 @@ func main() {
AddItem("Quit", "Press to exit", 'q', func() { AddItem("Quit", "Press to exit", 'q', func() {
app.Stop() 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 { if err := app.SetRoot(list, true).EnableMouse(true).Run(); err != nil {
panic(err) panic(err)
} }

@ -4,11 +4,49 @@ import "github.com/rivo/tview"
// Introduction returns a tview.List with the highlights of the tview package. // Introduction returns a tview.List with the highlights of the tview package.
func Introduction(nextSlide func()) (title string, content tview.Primitive) { func Introduction(nextSlide func()) (title string, content tview.Primitive) {
list := tview.NewList(). list := tview.NewList()
reset := func() {
list.
Clear().
AddItem("A Go package for terminal based UIs", "with a special focus on rich interactive widgets", '1', nextSlide). 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("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("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("Good for data entry", `For charts, use "termui" - for low-level views, use "gocui" - ...`, '4', nextSlide).
AddItem("Extensive documentation", "Everything is documented, examples in GitHub wiki, demo code for each widget", '5', nextSlide) AddItem("Supports context menus", "Right click on one of these items or press Alt+Enter", '5', nextSlide).
return "Introduction", Center(80, 10, list) AddItem("Extensive documentation", "Everything is documented, examples in GitHub wiki, demo code 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)
} }

@ -9,6 +9,7 @@ import (
// listItem represents one item in a List. // listItem represents one item in a List.
type listItem struct { type listItem struct {
Enabled bool // Whether or not the list item is selectable.
MainText string // The main text of the list item. MainText string // The main text of the list item.
SecondaryText string // A secondary text to be shown underneath the main text. 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. Shortcut rune // The key to select the list item directly, 0 if there is no shortcut.
@ -20,6 +21,7 @@ type listItem struct {
// See https://github.com/rivo/tview/wiki/List for an example. // See https://github.com/rivo/tview/wiki/List for an example.
type List struct { type List struct {
*Box *Box
*ContextMenu
// The items of the list. // The items of the list.
items []*listItem items []*listItem
@ -54,6 +56,9 @@ type List struct {
// Whether or not navigating the list will wrap around. // Whether or not navigating the list will wrap around.
wrapAround bool 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. // The number of list items skipped at the top before the first item is drawn.
offset int offset int
@ -71,7 +76,7 @@ type List struct {
// NewList returns a new form. // NewList returns a new form.
func NewList() *List { func NewList() *List {
return &List{ l := &List{
Box: NewBox(), Box: NewBox(),
showSecondaryText: true, showSecondaryText: true,
wrapAround: true, wrapAround: true,
@ -81,6 +86,11 @@ func NewList() *List {
selectedTextColor: Styles.PrimitiveBackgroundColor, selectedTextColor: Styles.PrimitiveBackgroundColor,
selectedBackgroundColor: Styles.PrimaryTextColor, selectedBackgroundColor: Styles.PrimaryTextColor,
} }
l.ContextMenu = NewContextMenu(l)
l.focus = l
return l
} }
// SetCurrentItem sets the currently selected item by its index, starting at 0 // SetCurrentItem sets the currently selected item by its index, starting at 0
@ -150,7 +160,7 @@ func (l *List) RemoveItem(index int) *List {
// Shift current item. // Shift current item.
previousCurrentItem := l.currentItem previousCurrentItem := l.currentItem
if l.currentItem >= index { if l.currentItem >= index && l.currentItem > 0 {
l.currentItem-- l.currentItem--
} }
@ -216,6 +226,13 @@ func (l *List) ShowSecondaryText(show bool) *List {
return l 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.hover = hover
return l
}
// SetWrapAround sets the flag that determines whether navigating the list will // SetWrapAround sets the flag that determines whether navigating the list will
// wrap around. That is, navigating downwards on the last item will move the // 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 // selection to the first item (similarly in the other direction). If set to
@ -283,6 +300,7 @@ func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected f
// selected. // selected.
func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut rune, selected func()) *List { func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut rune, selected func()) *List {
item := &listItem{ item := &listItem{
Enabled: true,
MainText: mainText, MainText: mainText,
SecondaryText: secondaryText, SecondaryText: secondaryText,
Shortcut: shortcut, Shortcut: shortcut,
@ -340,6 +358,14 @@ func (l *List) SetItemText(index int, main, secondary string) *List {
return l 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 {
item := l.items[index]
item.Enabled = enabled
return l
}
// FindItems searches the main and secondary texts for the given strings and // 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 // 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 // two search strings may be empty, it will then be ignored. Indices are always
@ -387,9 +413,26 @@ func (l *List) Clear() *List {
return l 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 {
if l.ContextMenu.open {
return l.ContextMenu.list.HasFocus()
}
return l.hasFocus
}
// Draw draws this primitive onto the screen. // Draw draws this primitive onto the screen.
func (l *List) Draw(screen tcell.Screen) { func (l *List) Draw(screen tcell.Screen) {
l.Box.Draw(screen) l.Box.Draw(screen)
hasFocus := l.GetFocusable().HasFocus()
// Determine the dimensions. // Determine the dimensions.
x, y, width, height := l.GetInnerRect() x, y, width, height := l.GetInnerRect()
@ -433,6 +476,26 @@ func (l *List) Draw(screen tcell.Screen) {
break 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)
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)
y++
continue
}
// Shortcuts. // Shortcuts.
if showShortcuts && item.Shortcut != 0 { if showShortcuts && item.Shortcut != 0 {
Print(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 4, AlignRight, l.shortcutColor) Print(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 4, AlignRight, l.shortcutColor)
@ -473,17 +536,71 @@ func (l *List) Draw(screen tcell.Screen) {
y++ y++
} }
} }
// Draw context menu.
if hasFocus && l.ContextMenu.open {
ctx := l.ContextMenu.list
x, y, width, height = l.GetInnerRect()
// What's the longest option text?
maxWidth := 0
for _, option := range ctx.items {
strWidth := TaggedStringWidth(option.MainText)
if option.Shortcut != 0 {
strWidth += 4
}
if strWidth > maxWidth {
maxWidth = strWidth
}
}
lheight := len(ctx.items)
lwidth := maxWidth
// Add space for borders
lwidth += 2
lheight += 2
lwidth += ctx.paddingLeft + ctx.paddingRight
lheight += ctx.paddingTop + ctx.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
}
ctx.SetRect(cx, cy, lwidth, lheight)
ctx.Draw(screen)
}
} }
// InputHandler returns the handler for this primitive. // InputHandler returns the handler for this primitive.
func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
if event.Key() == tcell.KeyEscape { if event.Key() == tcell.KeyEscape {
if l.ContextMenu.open {
l.ContextMenu.hide(setFocus)
return
}
if l.done != nil { if l.done != nil {
l.done() l.done()
} }
return return
} else if len(l.items) == 0 { } else if len(l.items) == 0 && (event.Key() != tcell.KeyEnter || event.Modifiers()&tcell.ModAlt == 0) {
return return
} }
@ -505,8 +622,30 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
_, _, _, height := l.GetInnerRect() _, _, _, height := l.GetInnerRect()
l.currentItem -= height l.currentItem -= height
case tcell.KeyEnter: case tcell.KeyEnter:
if l.currentItem >= 0 && l.currentItem < len(l.items) { 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
}
}
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] item := l.items[l.currentItem]
if item.Enabled {
if item.Selected != nil { if item.Selected != nil {
item.Selected() item.Selected()
} }
@ -514,13 +653,14 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
} }
} }
}
case tcell.KeyRune: case tcell.KeyRune:
ch := event.Rune() ch := event.Rune()
if ch != ' ' { if ch != ' ' {
// It's not a space bar. Is it a shortcut? // It's not a space bar. Is it a shortcut?
var found bool var found bool
for index, item := range l.items { for index, item := range l.items {
if item.Shortcut == ch { if item.Enabled && item.Shortcut == ch {
// We have a shortcut. // We have a shortcut.
found = true found = true
l.currentItem = index l.currentItem = index
@ -540,6 +680,8 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
} }
} }
decreasing := l.currentItem < previousItem
for i := 0; i < len(l.items); i++ {
if l.currentItem < 0 { if l.currentItem < 0 {
if l.wrapAround { if l.wrapAround {
l.currentItem = len(l.items) - 1 l.currentItem = len(l.items) - 1
@ -554,6 +696,18 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
} }
} }
item := l.items[l.currentItem]
if item.Enabled {
break
}
if decreasing {
l.currentItem--
} else {
l.currentItem++
}
}
if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil { if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
item := l.items[l.currentItem] item := l.items[l.currentItem]
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
@ -561,11 +715,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 // indexAtPoint returns the index of the list item found at the given position
// or a negative value if there is no such list item. // or a negative value if there is no such list item.
func (l *List) indexAtPoint(x, y int) int { func (l *List) indexAtPoint(x, y int) int {
rectX, rectY, width, height := l.GetInnerRect() 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 return -1
} }
@ -584,6 +758,13 @@ func (l *List) indexAtPoint(x, y int) int {
// MouseHandler returns the mouse handler for this primitive. // 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) { 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) { return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
// Pass events to context menu.
if l.ContextMenuVisible() && l.ContextMenuList().InRect(event.Position()) {
defer l.ContextMenuList().MouseHandler()(action, event, setFocus)
consumed = true
return
}
if !l.InRect(event.Position()) { if !l.InRect(event.Position()) {
return false, nil return false, nil
} }
@ -591,10 +772,17 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
// Process mouse event. // Process mouse event.
switch action { switch action {
case MouseLeftClick: case MouseLeftClick:
if l.ContextMenuVisible() {
defer l.ContextMenu.hide(setFocus)
consumed = true
return
}
setFocus(l) setFocus(l)
index := l.indexAtPoint(event.Position()) index := l.indexAtPoint(event.Position())
if index != -1 { if index != -1 {
item := l.items[index] item := l.items[index]
if item.Enabled {
if item.Selected != nil { if item.Selected != nil {
item.Selected() item.Selected()
} }
@ -606,7 +794,48 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
} }
l.currentItem = index l.currentItem = index
} }
}
consumed = true consumed = true
case MouseMiddleClick:
if l.ContextMenuVisible() {
defer l.ContextMenu.hide(setFocus)
consumed = true
return
}
case MouseRightDown:
if len(l.ContextMenuList().items) == 0 {
return
}
x, y := event.Position()
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
if item.Enabled {
l.currentItem = index
if index != l.currentItem && l.changed != nil {
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
}
}
}
defer l.ContextMenu.show(l.currentItem, x, y, setFocus)
l.ContextMenu.drag = true
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: case MouseScrollUp:
if l.offset > 0 { if l.offset > 0 {
l.offset-- l.offset--

@ -4,32 +4,50 @@ import "github.com/gdamore/tcell"
// Theme defines the colors used when primitives are initialized. // Theme defines the colors used when primitives are initialized.
type Theme struct { type Theme struct {
PrimitiveBackgroundColor tcell.Color // Main background color for primitives. // Title, border and other lines
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. TitleColor tcell.Color // Box titles.
BorderColor tcell.Color // Box borders.
GraphicsColor tcell.Color // Graphics. GraphicsColor tcell.Color // Graphics.
// Text
PrimaryTextColor tcell.Color // Primary text. PrimaryTextColor tcell.Color // Primary text.
SecondaryTextColor tcell.Color // Secondary text (e.g. labels). SecondaryTextColor tcell.Color // Secondary text (e.g. labels).
TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes). TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes).
InverseTextColor tcell.Color // Text on primary-colored backgrounds. InverseTextColor tcell.Color // Text on primary-colored backgrounds.
ContrastSecondaryTextColor tcell.Color // Secondary text on ContrastBackgroundColor-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.
// Context menu
ContextMenuPaddingTop int // Top padding.
ContextMenuPaddingBottom int // Bottom padding.
ContextMenuPaddingLeft int // Left padding.
ContextMenuPaddingRight int // Right padding.
} }
// Styles defines the theme for applications. The default is for a black // Styles defines the theme for applications. The default is for a black
// background and some basic colors: black, white, yellow, green, cyan, and // background and some basic colors: black, white, yellow, green, cyan, and
// blue. // blue.
var Styles = Theme{ var Styles = Theme{
PrimitiveBackgroundColor: tcell.ColorBlack,
ContrastBackgroundColor: tcell.ColorBlue,
MoreContrastBackgroundColor: tcell.ColorGreen,
BorderColor: tcell.ColorWhite,
TitleColor: tcell.ColorWhite, TitleColor: tcell.ColorWhite,
BorderColor: tcell.ColorWhite,
GraphicsColor: tcell.ColorWhite, GraphicsColor: tcell.ColorWhite,
PrimaryTextColor: tcell.ColorWhite, PrimaryTextColor: tcell.ColorWhite,
SecondaryTextColor: tcell.ColorYellow, SecondaryTextColor: tcell.ColorYellow,
TertiaryTextColor: tcell.ColorGreen, TertiaryTextColor: tcell.ColorGreen,
InverseTextColor: tcell.ColorBlue, InverseTextColor: tcell.ColorBlue,
ContrastSecondaryTextColor: tcell.ColorDarkCyan, ContrastSecondaryTextColor: tcell.ColorDarkCyan,
PrimitiveBackgroundColor: tcell.ColorBlack,
ContrastBackgroundColor: tcell.ColorBlue,
MoreContrastBackgroundColor: tcell.ColorGreen,
ContextMenuPaddingTop: 0,
ContextMenuPaddingBottom: 0,
ContextMenuPaddingLeft: 1,
ContextMenuPaddingRight: 1,
} }

Loading…
Cancel
Save