forked from tslocum/cview
Add ContextMenu
This commit is contained in:
parent
ae4c7b398c
commit
57ef7437e3
10 changed files with 573 additions and 75 deletions
|
@ -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)
|
||||
|
|
|
@ -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__
|
||||
|
|
6
box.go
6
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
|
||||
|
|
151
contextmenu.go
Normal file
151
contextmenu.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
2
go.mod
2
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
|
||||
)
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
320
list.go
320
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--
|
||||
|
|
57
styles.go
57
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,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue