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,14 +7,51 @@ import (
func main() { func main() {
app := tview.NewApplication() app := tview.NewApplication()
list := tview.NewList(). list := tview.NewList()
AddItem("List item 1", "Some explanatory text", 'a', nil).
AddItem("List item 2", "Some explanatory text", 'b', nil). reset := func() {
AddItem("List item 3", "Some explanatory text", 'c', nil). list.
AddItem("List item 4", "Some explanatory text", 'd', nil). Clear().
AddItem("Quit", "Press to exit", 'q', func() { AddItem("List item 1", "Some explanatory text", 'a', nil).
app.Stop() 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 { 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()
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). reset := func() {
AddItem("Designed to be simple", `"Hello world" is 5 lines of code`, '3', nextSlide). list.
AddItem("Good for data entry", `For charts, use "termui" - for low-level views, use "gocui" - ...`, '4', nextSlide). Clear().
AddItem("Extensive documentation", "Everything is documented, examples in GitHub wiki, demo code for each widget", '5', nextSlide) AddItem("A Go package for terminal based UIs", "with a special focus on rich interactive widgets", '1', nextSlide).
return "Introduction", Center(80, 10, list) 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", "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,13 +622,36 @@ 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 {
item := l.items[l.currentItem] // Do we show any shortcuts?
if item.Selected != nil { var showShortcuts bool
item.Selected() 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
} }
if l.selected != nil {
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) 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 {
item.Selected()
}
if l.selected != nil {
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
}
} }
} }
case tcell.KeyRune: case tcell.KeyRune:
@ -520,7 +660,7 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
// 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,17 +680,31 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
} }
} }
if l.currentItem < 0 { decreasing := l.currentItem < previousItem
if l.wrapAround { for i := 0; i < len(l.items); i++ {
l.currentItem = len(l.items) - 1 if l.currentItem < 0 {
} else { if l.wrapAround {
l.currentItem = 0 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 { item := l.items[l.currentItem]
l.currentItem = 0 if item.Enabled {
break
}
if decreasing {
l.currentItem--
} else { } else {
l.currentItem = len(l.items) - 1 l.currentItem++
} }
} }
@ -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,22 +772,70 @@ 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.Selected != nil { if item.Enabled {
item.Selected() if item.Selected != nil {
} item.Selected()
if l.selected != nil { }
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut) 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 index != l.currentItem && l.changed != nil { }
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut) 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)
}
} }
l.currentItem = index
} }
defer l.ContextMenu.show(l.currentItem, x, y, setFocus)
l.ContextMenu.drag = true
consumed = 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 {
// 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. PrimitiveBackgroundColor tcell.Color // Main background color for primitives.
ContrastBackgroundColor tcell.Color // Background color for contrasting elements. ContrastBackgroundColor tcell.Color // Background color for contrasting elements.
MoreContrastBackgroundColor tcell.Color // Background color for even more contrasting elements. MoreContrastBackgroundColor tcell.Color // Background color for even more contrasting elements.
BorderColor tcell.Color // Box borders.
TitleColor tcell.Color // Box titles. // Context menu
GraphicsColor tcell.Color // Graphics. ContextMenuPaddingTop int // Top padding.
PrimaryTextColor tcell.Color // Primary text. ContextMenuPaddingBottom int // Bottom padding.
SecondaryTextColor tcell.Color // Secondary text (e.g. labels). ContextMenuPaddingLeft int // Left padding.
TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes). ContextMenuPaddingRight int // Right padding.
InverseTextColor tcell.Color // Text on primary-colored backgrounds.
ContrastSecondaryTextColor tcell.Color // Secondary text on ContrastBackgroundColor-colored backgrounds.
} }
// 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{
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, PrimitiveBackgroundColor: tcell.ColorBlack,
ContrastBackgroundColor: tcell.ColorBlue, ContrastBackgroundColor: tcell.ColorBlue,
MoreContrastBackgroundColor: tcell.ColorGreen, MoreContrastBackgroundColor: tcell.ColorGreen,
BorderColor: tcell.ColorWhite,
TitleColor: tcell.ColorWhite, ContextMenuPaddingTop: 0,
GraphicsColor: tcell.ColorWhite, ContextMenuPaddingBottom: 0,
PrimaryTextColor: tcell.ColorWhite, ContextMenuPaddingLeft: 1,
SecondaryTextColor: tcell.ColorYellow, ContextMenuPaddingRight: 1,
TertiaryTextColor: tcell.ColorGreen,
InverseTextColor: tcell.ColorBlue,
ContrastSecondaryTextColor: tcell.ColorDarkCyan,
} }

Loading…
Cancel
Save