diff --git a/TabTitleMenu/README.md b/TabTitleMenu/README.md new file mode 100644 index 0000000..3f3d8d6 --- /dev/null +++ b/TabTitleMenu/README.md @@ -0,0 +1,10 @@ +Tabs Instead of Title for TUI Example +=============== +see central box's tabs where the other box's titles are + +![screenshot](/screenshot.png?raw=true "screenshot") + + +Thanks +=============== +A MASSIVE thanks to @LoreleiAurora for helping with this! diff --git a/TabTitleMenu/README.md b/TabTitleMenu/README.md new file mode 100644 index 0000000..3f3d8d6 --- /dev/null +++ b/TabTitleMenu/README.md @@ -0,0 +1,10 @@ +Tabs Instead of Title for TUI Example +=============== +see central box's tabs where the other box's titles are + +![screenshot](/screenshot.png?raw=true "screenshot") + + +Thanks +=============== +A MASSIVE thanks to @LoreleiAurora for helping with this! diff --git a/TabTitleMenu/main.go b/TabTitleMenu/main.go new file mode 100755 index 0000000..5919b14 --- /dev/null +++ b/TabTitleMenu/main.go @@ -0,0 +1,88 @@ +// Demo code for the TabbedPanels primitive. +package main + +import ( + "fmt" + + //"github.com/rivo/tview" + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +const panelCount = 5 + + +func demoBox(title string) *cview.Box { + b := cview.NewBox() + b.SetBorder(true) + b.SetTitle(title) + return b + } + + +func main() { + + app := cview.NewApplication() + defer app.HandlePanic() + app.EnableMouse(true) + + panels := NewTabbedPanels() + + for panel := 0; panel < panelCount; panel++ { + func(panel int) { + form := cview.NewForm() + form.SetBorder(true) + form.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { + //fix corners + screen.SetContent(x, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + screen.SetContent(x+width-1, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + // Draw nothing across the top of the box. + for cx := x + 1; cx < x+width-1; cx++ { + screen.SetContent(cx, y, ' ', nil, tcell.StyleDefault.Background(tcell.Color16)) + } + // Space for other content. + return x + 1, y, width - 2, height - y +1 + }) + //form.SetTitle(fmt.Sprintf("This is tab %d. Choose another tab.", panel+1)) + form.AddButton("Next", func() { + panels.SetCurrentTab(fmt.Sprintf("panel-%d", (panel+1)%panelCount)) + }) + form.AddButton("Quit", func() { + app.Stop() + }) + form.SetCancelFunc(func() { + app.Stop() + }) + + panels.AddTab(fmt.Sprintf("panel-%d", panel), fmt.Sprintf("Panel #%d", panel), form) + }(panel) + } + + subFlex := cview.NewFlex() + subFlex.SetDirection(cview.FlexRow) + subFlex.AddItem(demoBox("Top"), 0, 1, false) + subFlex.AddItem(panels, 0, 3, false) + subFlex.AddItem(demoBox("Bottom (5 rows)"), 5, 1, false) + + flex := cview.NewFlex() + flex.AddItem(demoBox("Left (1/2 x width of Top)"), 0, 1, false) + flex.AddItem(subFlex, 0, 2, false) + flex.AddItem(demoBox("Right (20 cols)"), 20, 1, false) + + app.SetRoot(flex, true) + if err := app.Run(); err != nil { + panic(err) + } + + //app := cview.NewApplication() + //defer app.HandlePanic() + + + + + + //app.SetRoot(panels, true) + //if err := app.Run(); err != nil { + // panic(err) + //} +} \ No newline at end of file diff --git a/TabTitleMenu/README.md b/TabTitleMenu/README.md new file mode 100644 index 0000000..3f3d8d6 --- /dev/null +++ b/TabTitleMenu/README.md @@ -0,0 +1,10 @@ +Tabs Instead of Title for TUI Example +=============== +see central box's tabs where the other box's titles are + +![screenshot](/screenshot.png?raw=true "screenshot") + + +Thanks +=============== +A MASSIVE thanks to @LoreleiAurora for helping with this! diff --git a/TabTitleMenu/main.go b/TabTitleMenu/main.go new file mode 100755 index 0000000..5919b14 --- /dev/null +++ b/TabTitleMenu/main.go @@ -0,0 +1,88 @@ +// Demo code for the TabbedPanels primitive. +package main + +import ( + "fmt" + + //"github.com/rivo/tview" + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +const panelCount = 5 + + +func demoBox(title string) *cview.Box { + b := cview.NewBox() + b.SetBorder(true) + b.SetTitle(title) + return b + } + + +func main() { + + app := cview.NewApplication() + defer app.HandlePanic() + app.EnableMouse(true) + + panels := NewTabbedPanels() + + for panel := 0; panel < panelCount; panel++ { + func(panel int) { + form := cview.NewForm() + form.SetBorder(true) + form.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { + //fix corners + screen.SetContent(x, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + screen.SetContent(x+width-1, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + // Draw nothing across the top of the box. + for cx := x + 1; cx < x+width-1; cx++ { + screen.SetContent(cx, y, ' ', nil, tcell.StyleDefault.Background(tcell.Color16)) + } + // Space for other content. + return x + 1, y, width - 2, height - y +1 + }) + //form.SetTitle(fmt.Sprintf("This is tab %d. Choose another tab.", panel+1)) + form.AddButton("Next", func() { + panels.SetCurrentTab(fmt.Sprintf("panel-%d", (panel+1)%panelCount)) + }) + form.AddButton("Quit", func() { + app.Stop() + }) + form.SetCancelFunc(func() { + app.Stop() + }) + + panels.AddTab(fmt.Sprintf("panel-%d", panel), fmt.Sprintf("Panel #%d", panel), form) + }(panel) + } + + subFlex := cview.NewFlex() + subFlex.SetDirection(cview.FlexRow) + subFlex.AddItem(demoBox("Top"), 0, 1, false) + subFlex.AddItem(panels, 0, 3, false) + subFlex.AddItem(demoBox("Bottom (5 rows)"), 5, 1, false) + + flex := cview.NewFlex() + flex.AddItem(demoBox("Left (1/2 x width of Top)"), 0, 1, false) + flex.AddItem(subFlex, 0, 2, false) + flex.AddItem(demoBox("Right (20 cols)"), 20, 1, false) + + app.SetRoot(flex, true) + if err := app.Run(); err != nil { + panic(err) + } + + //app := cview.NewApplication() + //defer app.HandlePanic() + + + + + + //app.SetRoot(panels, true) + //if err := app.Run(); err != nil { + // panic(err) + //} +} \ No newline at end of file diff --git a/TabTitleMenu/panels.go b/TabTitleMenu/panels.go new file mode 100755 index 0000000..a3187f8 --- /dev/null +++ b/TabTitleMenu/panels.go @@ -0,0 +1,449 @@ +package main + +import ( + "sync" + + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +// panel represents a single panel of a Panels object. +type panel struct { + Name string // The panel's name. + Item cview.Primitive // The panel's cview.Primitive. + Resize bool // Whether or not to resize the panel when it is drawn. + Visible bool // Whether or not this panel is visible. +} + +// Panels is a container for other cview.Primitives often used as the application's +// root cview.Primitive. It allows to easily switch the visibility of the contained +// cview.Primitives. +type Panels struct { + *cview.Box + + // The contained panels. (Visible) panels are drawn from back to front. + panels []*panel + + // We keep a reference to the function which allows us to set the focus to + // a newly visible panel. + setFocus func(p cview.Primitive) + + // An optional handler which is called whenever the visibility or the order of + // panels changes. + changed func() + + sync.RWMutex +} + +// NewPanels returns a new Panels object. +func NewPanels() *Panels { + p := &Panels{ + Box: cview.NewBox(), + } + //p.focus = p + return p +} + +// SetChangedFunc sets a handler which is called whenever the visibility or the +// order of any visible panels changes. This can be used to redraw the panels. +func (p *Panels) SetChangedFunc(handler func()) { + p.Lock() + defer p.Unlock() + + p.changed = handler +} + +// GetPanelCount returns the number of panels currently stored in this object. +func (p *Panels) GetPanelCount() int { + p.RLock() + defer p.RUnlock() + + return len(p.panels) +} + +// AddPanel adds a new panel with the given name and cview.Primitive. If there was +// previously a panel with the same name, it is overwritten. Leaving the name +// empty may cause conflicts in other functions so always specify a non-empty +// name. +// +// Visible panels will be drawn in the order they were added (unless that order +// was changed in one of the other functions). If "resize" is set to true, the +// cview.Primitive will be set to the size available to the Panels cview.Primitive whenever +// the panels are drawn. +func (p *Panels) AddPanel(name string, item cview.Primitive, resize, visible bool) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + var added bool + for i, pg := range p.panels { + if pg.Name == name { + p.panels[i] = &panel{Item: item, Name: name, Resize: resize, Visible: visible} + added = true + break + } + } + if !added { + p.panels = append(p.panels, &panel{Item: item, Name: name, Resize: resize, Visible: visible}) + } + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// RemovePanel removes the panel with the given name. If that panel was the only +// visible panel, visibility is assigned to the last panel. +func (p *Panels) RemovePanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + var isVisible bool + for index, panel := range p.panels { + if panel.Name == name { + isVisible = panel.Visible + p.panels = append(p.panels[:index], p.panels[index+1:]...) + if panel.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if isVisible { + for index, panel := range p.panels { + if index < len(p.panels)-1 { + if panel.Visible { + break // There is a remaining visible panel. + } + } else { + panel.Visible = true // We need at least one visible panel. + } + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// HasPanel returns true if a panel with the given name exists in this object. +func (p *Panels) HasPanel(name string) bool { + p.RLock() + defer p.RUnlock() + + for _, panel := range p.panels { + if panel.Name == name { + return true + } + } + return false +} + +// ShowPanel sets a panel's visibility to "true" (in addition to any other panels +// which are already visible). +func (p *Panels) ShowPanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = true + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// HidePanel sets a panel's visibility to "false". +func (p *Panels) HidePanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = false + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SetCurrentPanel sets a panel's visibility to "true" and all other panels' +// visibility to "false". +func (p *Panels) SetCurrentPanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = true + } else { + panel.Visible = false + } + } + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SendToFront changes the order of the panels such that the panel with the given +// name comes last, causing it to be drawn last with the next update (if +// visible). +func (p *Panels) SendToFront(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for index, panel := range p.panels { + if panel.Name == name { + if index < len(p.panels)-1 { + p.panels = append(append(p.panels[:index], p.panels[index+1:]...), panel) + } + if panel.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SendToBack changes the order of the panels such that the panel with the given +// name comes first, causing it to be drawn first with the next update (if +// visible). +func (p *Panels) SendToBack(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for index, pg := range p.panels { + if pg.Name == name { + if index > 0 { + p.panels = append(append([]*panel{pg}, p.panels[:index]...), p.panels[index+1:]...) + } + if pg.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// GetFrontPanel returns the front-most visible panel. If there are no visible +// panels, ("", nil) is returned. +func (p *Panels) GetFrontPanel() (name string, item cview.Primitive) { + p.RLock() + defer p.RUnlock() + + for index := len(p.panels) - 1; index >= 0; index-- { + if p.panels[index].Visible { + return p.panels[index].Name, p.panels[index].Item + } + } + return +} + +// HasFocus returns whether or not this cview.Primitive has focus. +func (p *Panels) HasFocus() bool { + p.RLock() + defer p.RUnlock() + + for _, panel := range p.panels { + if panel.Item.GetFocusable().HasFocus() { + return true + } + } + return false +} + +// Focus is called by the application when the cview.Primitive receives focus. +func (p *Panels) Focus(delegate func(p cview.Primitive)) { + p.Lock() + defer p.Unlock() + + if delegate == nil { + return // We cannot delegate so we cannot focus. + } + p.setFocus = delegate + var topItem cview.Primitive + for _, panel := range p.panels { + if panel.Visible { + topItem = panel.Item + } + } + if topItem != nil { + p.Unlock() + delegate(topItem) + p.Lock() + } +} + +// Draw draws this cview.Primitive onto the screen. +func (p *Panels) Draw(screen tcell.Screen) { + if !p.GetVisible() { + return + } + + p.Box.Draw(screen) + + p.Lock() + defer p.Unlock() + + x, y, width, height := p.GetInnerRect() + + for _, panel := range p.panels { + if !panel.Visible { + continue + } + if panel.Resize { + panel.Item.SetRect(x, y, width, height) + } + panel.Item.Draw(screen) + } +} + +// MouseHandler returns the mouse handler for this cview.Primitive. +func (p *Panels) MouseHandler() func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + return p.WrapMouseHandler(func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + if !p.InRect(event.Position()) { + return false, nil + } + + // Pass mouse events along to the last visible panel item that takes it. + for index := len(p.panels) - 1; index >= 0; index-- { + panel := p.panels[index] + if panel.Visible { + consumed, capture = panel.Item.MouseHandler()(action, event, setFocus) + if consumed { + return + } + } + } + + return + }) +} + +// Support backwards compatibility with Pages. +type page = panel + +// Pages is a wrapper around Panels. +// +// Deprecated: This type is provided for backwards compatibility. +// Developers should use Panels instead. +type Pages struct { + *Panels +} + +// NewPages returns a new Panels object. +// +// Deprecated: This function is provided for backwards compatibility. +// Developers should use NewPanels instead. +func NewPages() *Pages { + return &Pages{NewPanels()} +} + +// GetPageCount returns the number of panels currently stored in this object. +func (p *Pages) GetPageCount() int { + return p.GetPanelCount() +} + +// AddPage adds a new panel with the given name and cview.Primitive. +func (p *Pages) AddPage(name string, item cview.Primitive, resize, visible bool) { + p.AddPanel(name, item, resize, visible) +} + +// AddAndSwitchToPage calls Add(), then SwitchTo() on that newly added panel. +func (p *Pages) AddAndSwitchToPage(name string, item cview.Primitive, resize bool) { + p.AddPanel(name, item, resize, true) + p.SetCurrentPanel(name) +} + +// RemovePage removes the panel with the given name. +func (p *Pages) RemovePage(name string) { + p.RemovePanel(name) +} + +// HasPage returns true if a panel with the given name exists in this object. +func (p *Pages) HasPage(name string) bool { + return p.HasPanel(name) +} + +// ShowPage sets a panel's visibility to "true". +func (p *Pages) ShowPage(name string) { + p.ShowPanel(name) +} + +// HidePage sets a panel's visibility to "false". +func (p *Pages) HidePage(name string) { + p.HidePanel(name) +} + +// SwitchToPage sets a panel's visibility to "true" and all other panels' +// visibility to "false". +func (p *Pages) SwitchToPage(name string) { + p.SetCurrentPanel(name) +} + +// GetFrontPage returns the front-most visible panel. +func (p *Pages) GetFrontPage() (name string, item cview.Primitive) { + return p.GetFrontPanel() +} diff --git a/TabTitleMenu/README.md b/TabTitleMenu/README.md new file mode 100644 index 0000000..3f3d8d6 --- /dev/null +++ b/TabTitleMenu/README.md @@ -0,0 +1,10 @@ +Tabs Instead of Title for TUI Example +=============== +see central box's tabs where the other box's titles are + +![screenshot](/screenshot.png?raw=true "screenshot") + + +Thanks +=============== +A MASSIVE thanks to @LoreleiAurora for helping with this! diff --git a/TabTitleMenu/main.go b/TabTitleMenu/main.go new file mode 100755 index 0000000..5919b14 --- /dev/null +++ b/TabTitleMenu/main.go @@ -0,0 +1,88 @@ +// Demo code for the TabbedPanels primitive. +package main + +import ( + "fmt" + + //"github.com/rivo/tview" + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +const panelCount = 5 + + +func demoBox(title string) *cview.Box { + b := cview.NewBox() + b.SetBorder(true) + b.SetTitle(title) + return b + } + + +func main() { + + app := cview.NewApplication() + defer app.HandlePanic() + app.EnableMouse(true) + + panels := NewTabbedPanels() + + for panel := 0; panel < panelCount; panel++ { + func(panel int) { + form := cview.NewForm() + form.SetBorder(true) + form.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { + //fix corners + screen.SetContent(x, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + screen.SetContent(x+width-1, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + // Draw nothing across the top of the box. + for cx := x + 1; cx < x+width-1; cx++ { + screen.SetContent(cx, y, ' ', nil, tcell.StyleDefault.Background(tcell.Color16)) + } + // Space for other content. + return x + 1, y, width - 2, height - y +1 + }) + //form.SetTitle(fmt.Sprintf("This is tab %d. Choose another tab.", panel+1)) + form.AddButton("Next", func() { + panels.SetCurrentTab(fmt.Sprintf("panel-%d", (panel+1)%panelCount)) + }) + form.AddButton("Quit", func() { + app.Stop() + }) + form.SetCancelFunc(func() { + app.Stop() + }) + + panels.AddTab(fmt.Sprintf("panel-%d", panel), fmt.Sprintf("Panel #%d", panel), form) + }(panel) + } + + subFlex := cview.NewFlex() + subFlex.SetDirection(cview.FlexRow) + subFlex.AddItem(demoBox("Top"), 0, 1, false) + subFlex.AddItem(panels, 0, 3, false) + subFlex.AddItem(demoBox("Bottom (5 rows)"), 5, 1, false) + + flex := cview.NewFlex() + flex.AddItem(demoBox("Left (1/2 x width of Top)"), 0, 1, false) + flex.AddItem(subFlex, 0, 2, false) + flex.AddItem(demoBox("Right (20 cols)"), 20, 1, false) + + app.SetRoot(flex, true) + if err := app.Run(); err != nil { + panic(err) + } + + //app := cview.NewApplication() + //defer app.HandlePanic() + + + + + + //app.SetRoot(panels, true) + //if err := app.Run(); err != nil { + // panic(err) + //} +} \ No newline at end of file diff --git a/TabTitleMenu/panels.go b/TabTitleMenu/panels.go new file mode 100755 index 0000000..a3187f8 --- /dev/null +++ b/TabTitleMenu/panels.go @@ -0,0 +1,449 @@ +package main + +import ( + "sync" + + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +// panel represents a single panel of a Panels object. +type panel struct { + Name string // The panel's name. + Item cview.Primitive // The panel's cview.Primitive. + Resize bool // Whether or not to resize the panel when it is drawn. + Visible bool // Whether or not this panel is visible. +} + +// Panels is a container for other cview.Primitives often used as the application's +// root cview.Primitive. It allows to easily switch the visibility of the contained +// cview.Primitives. +type Panels struct { + *cview.Box + + // The contained panels. (Visible) panels are drawn from back to front. + panels []*panel + + // We keep a reference to the function which allows us to set the focus to + // a newly visible panel. + setFocus func(p cview.Primitive) + + // An optional handler which is called whenever the visibility or the order of + // panels changes. + changed func() + + sync.RWMutex +} + +// NewPanels returns a new Panels object. +func NewPanels() *Panels { + p := &Panels{ + Box: cview.NewBox(), + } + //p.focus = p + return p +} + +// SetChangedFunc sets a handler which is called whenever the visibility or the +// order of any visible panels changes. This can be used to redraw the panels. +func (p *Panels) SetChangedFunc(handler func()) { + p.Lock() + defer p.Unlock() + + p.changed = handler +} + +// GetPanelCount returns the number of panels currently stored in this object. +func (p *Panels) GetPanelCount() int { + p.RLock() + defer p.RUnlock() + + return len(p.panels) +} + +// AddPanel adds a new panel with the given name and cview.Primitive. If there was +// previously a panel with the same name, it is overwritten. Leaving the name +// empty may cause conflicts in other functions so always specify a non-empty +// name. +// +// Visible panels will be drawn in the order they were added (unless that order +// was changed in one of the other functions). If "resize" is set to true, the +// cview.Primitive will be set to the size available to the Panels cview.Primitive whenever +// the panels are drawn. +func (p *Panels) AddPanel(name string, item cview.Primitive, resize, visible bool) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + var added bool + for i, pg := range p.panels { + if pg.Name == name { + p.panels[i] = &panel{Item: item, Name: name, Resize: resize, Visible: visible} + added = true + break + } + } + if !added { + p.panels = append(p.panels, &panel{Item: item, Name: name, Resize: resize, Visible: visible}) + } + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// RemovePanel removes the panel with the given name. If that panel was the only +// visible panel, visibility is assigned to the last panel. +func (p *Panels) RemovePanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + var isVisible bool + for index, panel := range p.panels { + if panel.Name == name { + isVisible = panel.Visible + p.panels = append(p.panels[:index], p.panels[index+1:]...) + if panel.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if isVisible { + for index, panel := range p.panels { + if index < len(p.panels)-1 { + if panel.Visible { + break // There is a remaining visible panel. + } + } else { + panel.Visible = true // We need at least one visible panel. + } + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// HasPanel returns true if a panel with the given name exists in this object. +func (p *Panels) HasPanel(name string) bool { + p.RLock() + defer p.RUnlock() + + for _, panel := range p.panels { + if panel.Name == name { + return true + } + } + return false +} + +// ShowPanel sets a panel's visibility to "true" (in addition to any other panels +// which are already visible). +func (p *Panels) ShowPanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = true + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// HidePanel sets a panel's visibility to "false". +func (p *Panels) HidePanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = false + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SetCurrentPanel sets a panel's visibility to "true" and all other panels' +// visibility to "false". +func (p *Panels) SetCurrentPanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = true + } else { + panel.Visible = false + } + } + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SendToFront changes the order of the panels such that the panel with the given +// name comes last, causing it to be drawn last with the next update (if +// visible). +func (p *Panels) SendToFront(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for index, panel := range p.panels { + if panel.Name == name { + if index < len(p.panels)-1 { + p.panels = append(append(p.panels[:index], p.panels[index+1:]...), panel) + } + if panel.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SendToBack changes the order of the panels such that the panel with the given +// name comes first, causing it to be drawn first with the next update (if +// visible). +func (p *Panels) SendToBack(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for index, pg := range p.panels { + if pg.Name == name { + if index > 0 { + p.panels = append(append([]*panel{pg}, p.panels[:index]...), p.panels[index+1:]...) + } + if pg.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// GetFrontPanel returns the front-most visible panel. If there are no visible +// panels, ("", nil) is returned. +func (p *Panels) GetFrontPanel() (name string, item cview.Primitive) { + p.RLock() + defer p.RUnlock() + + for index := len(p.panels) - 1; index >= 0; index-- { + if p.panels[index].Visible { + return p.panels[index].Name, p.panels[index].Item + } + } + return +} + +// HasFocus returns whether or not this cview.Primitive has focus. +func (p *Panels) HasFocus() bool { + p.RLock() + defer p.RUnlock() + + for _, panel := range p.panels { + if panel.Item.GetFocusable().HasFocus() { + return true + } + } + return false +} + +// Focus is called by the application when the cview.Primitive receives focus. +func (p *Panels) Focus(delegate func(p cview.Primitive)) { + p.Lock() + defer p.Unlock() + + if delegate == nil { + return // We cannot delegate so we cannot focus. + } + p.setFocus = delegate + var topItem cview.Primitive + for _, panel := range p.panels { + if panel.Visible { + topItem = panel.Item + } + } + if topItem != nil { + p.Unlock() + delegate(topItem) + p.Lock() + } +} + +// Draw draws this cview.Primitive onto the screen. +func (p *Panels) Draw(screen tcell.Screen) { + if !p.GetVisible() { + return + } + + p.Box.Draw(screen) + + p.Lock() + defer p.Unlock() + + x, y, width, height := p.GetInnerRect() + + for _, panel := range p.panels { + if !panel.Visible { + continue + } + if panel.Resize { + panel.Item.SetRect(x, y, width, height) + } + panel.Item.Draw(screen) + } +} + +// MouseHandler returns the mouse handler for this cview.Primitive. +func (p *Panels) MouseHandler() func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + return p.WrapMouseHandler(func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + if !p.InRect(event.Position()) { + return false, nil + } + + // Pass mouse events along to the last visible panel item that takes it. + for index := len(p.panels) - 1; index >= 0; index-- { + panel := p.panels[index] + if panel.Visible { + consumed, capture = panel.Item.MouseHandler()(action, event, setFocus) + if consumed { + return + } + } + } + + return + }) +} + +// Support backwards compatibility with Pages. +type page = panel + +// Pages is a wrapper around Panels. +// +// Deprecated: This type is provided for backwards compatibility. +// Developers should use Panels instead. +type Pages struct { + *Panels +} + +// NewPages returns a new Panels object. +// +// Deprecated: This function is provided for backwards compatibility. +// Developers should use NewPanels instead. +func NewPages() *Pages { + return &Pages{NewPanels()} +} + +// GetPageCount returns the number of panels currently stored in this object. +func (p *Pages) GetPageCount() int { + return p.GetPanelCount() +} + +// AddPage adds a new panel with the given name and cview.Primitive. +func (p *Pages) AddPage(name string, item cview.Primitive, resize, visible bool) { + p.AddPanel(name, item, resize, visible) +} + +// AddAndSwitchToPage calls Add(), then SwitchTo() on that newly added panel. +func (p *Pages) AddAndSwitchToPage(name string, item cview.Primitive, resize bool) { + p.AddPanel(name, item, resize, true) + p.SetCurrentPanel(name) +} + +// RemovePage removes the panel with the given name. +func (p *Pages) RemovePage(name string) { + p.RemovePanel(name) +} + +// HasPage returns true if a panel with the given name exists in this object. +func (p *Pages) HasPage(name string) bool { + return p.HasPanel(name) +} + +// ShowPage sets a panel's visibility to "true". +func (p *Pages) ShowPage(name string) { + p.ShowPanel(name) +} + +// HidePage sets a panel's visibility to "false". +func (p *Pages) HidePage(name string) { + p.HidePanel(name) +} + +// SwitchToPage sets a panel's visibility to "true" and all other panels' +// visibility to "false". +func (p *Pages) SwitchToPage(name string) { + p.SetCurrentPanel(name) +} + +// GetFrontPage returns the front-most visible panel. +func (p *Pages) GetFrontPage() (name string, item cview.Primitive) { + return p.GetFrontPanel() +} diff --git a/TabTitleMenu/screenshot.png b/TabTitleMenu/screenshot.png new file mode 100755 index 0000000..a852a03 --- /dev/null +++ b/TabTitleMenu/screenshot.png Binary files differ diff --git a/TabTitleMenu/README.md b/TabTitleMenu/README.md new file mode 100644 index 0000000..3f3d8d6 --- /dev/null +++ b/TabTitleMenu/README.md @@ -0,0 +1,10 @@ +Tabs Instead of Title for TUI Example +=============== +see central box's tabs where the other box's titles are + +![screenshot](/screenshot.png?raw=true "screenshot") + + +Thanks +=============== +A MASSIVE thanks to @LoreleiAurora for helping with this! diff --git a/TabTitleMenu/main.go b/TabTitleMenu/main.go new file mode 100755 index 0000000..5919b14 --- /dev/null +++ b/TabTitleMenu/main.go @@ -0,0 +1,88 @@ +// Demo code for the TabbedPanels primitive. +package main + +import ( + "fmt" + + //"github.com/rivo/tview" + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +const panelCount = 5 + + +func demoBox(title string) *cview.Box { + b := cview.NewBox() + b.SetBorder(true) + b.SetTitle(title) + return b + } + + +func main() { + + app := cview.NewApplication() + defer app.HandlePanic() + app.EnableMouse(true) + + panels := NewTabbedPanels() + + for panel := 0; panel < panelCount; panel++ { + func(panel int) { + form := cview.NewForm() + form.SetBorder(true) + form.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { + //fix corners + screen.SetContent(x, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + screen.SetContent(x+width-1, y, cview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Background(tcell.Color16)) + // Draw nothing across the top of the box. + for cx := x + 1; cx < x+width-1; cx++ { + screen.SetContent(cx, y, ' ', nil, tcell.StyleDefault.Background(tcell.Color16)) + } + // Space for other content. + return x + 1, y, width - 2, height - y +1 + }) + //form.SetTitle(fmt.Sprintf("This is tab %d. Choose another tab.", panel+1)) + form.AddButton("Next", func() { + panels.SetCurrentTab(fmt.Sprintf("panel-%d", (panel+1)%panelCount)) + }) + form.AddButton("Quit", func() { + app.Stop() + }) + form.SetCancelFunc(func() { + app.Stop() + }) + + panels.AddTab(fmt.Sprintf("panel-%d", panel), fmt.Sprintf("Panel #%d", panel), form) + }(panel) + } + + subFlex := cview.NewFlex() + subFlex.SetDirection(cview.FlexRow) + subFlex.AddItem(demoBox("Top"), 0, 1, false) + subFlex.AddItem(panels, 0, 3, false) + subFlex.AddItem(demoBox("Bottom (5 rows)"), 5, 1, false) + + flex := cview.NewFlex() + flex.AddItem(demoBox("Left (1/2 x width of Top)"), 0, 1, false) + flex.AddItem(subFlex, 0, 2, false) + flex.AddItem(demoBox("Right (20 cols)"), 20, 1, false) + + app.SetRoot(flex, true) + if err := app.Run(); err != nil { + panic(err) + } + + //app := cview.NewApplication() + //defer app.HandlePanic() + + + + + + //app.SetRoot(panels, true) + //if err := app.Run(); err != nil { + // panic(err) + //} +} \ No newline at end of file diff --git a/TabTitleMenu/panels.go b/TabTitleMenu/panels.go new file mode 100755 index 0000000..a3187f8 --- /dev/null +++ b/TabTitleMenu/panels.go @@ -0,0 +1,449 @@ +package main + +import ( + "sync" + + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +// panel represents a single panel of a Panels object. +type panel struct { + Name string // The panel's name. + Item cview.Primitive // The panel's cview.Primitive. + Resize bool // Whether or not to resize the panel when it is drawn. + Visible bool // Whether or not this panel is visible. +} + +// Panels is a container for other cview.Primitives often used as the application's +// root cview.Primitive. It allows to easily switch the visibility of the contained +// cview.Primitives. +type Panels struct { + *cview.Box + + // The contained panels. (Visible) panels are drawn from back to front. + panels []*panel + + // We keep a reference to the function which allows us to set the focus to + // a newly visible panel. + setFocus func(p cview.Primitive) + + // An optional handler which is called whenever the visibility or the order of + // panels changes. + changed func() + + sync.RWMutex +} + +// NewPanels returns a new Panels object. +func NewPanels() *Panels { + p := &Panels{ + Box: cview.NewBox(), + } + //p.focus = p + return p +} + +// SetChangedFunc sets a handler which is called whenever the visibility or the +// order of any visible panels changes. This can be used to redraw the panels. +func (p *Panels) SetChangedFunc(handler func()) { + p.Lock() + defer p.Unlock() + + p.changed = handler +} + +// GetPanelCount returns the number of panels currently stored in this object. +func (p *Panels) GetPanelCount() int { + p.RLock() + defer p.RUnlock() + + return len(p.panels) +} + +// AddPanel adds a new panel with the given name and cview.Primitive. If there was +// previously a panel with the same name, it is overwritten. Leaving the name +// empty may cause conflicts in other functions so always specify a non-empty +// name. +// +// Visible panels will be drawn in the order they were added (unless that order +// was changed in one of the other functions). If "resize" is set to true, the +// cview.Primitive will be set to the size available to the Panels cview.Primitive whenever +// the panels are drawn. +func (p *Panels) AddPanel(name string, item cview.Primitive, resize, visible bool) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + var added bool + for i, pg := range p.panels { + if pg.Name == name { + p.panels[i] = &panel{Item: item, Name: name, Resize: resize, Visible: visible} + added = true + break + } + } + if !added { + p.panels = append(p.panels, &panel{Item: item, Name: name, Resize: resize, Visible: visible}) + } + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// RemovePanel removes the panel with the given name. If that panel was the only +// visible panel, visibility is assigned to the last panel. +func (p *Panels) RemovePanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + var isVisible bool + for index, panel := range p.panels { + if panel.Name == name { + isVisible = panel.Visible + p.panels = append(p.panels[:index], p.panels[index+1:]...) + if panel.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if isVisible { + for index, panel := range p.panels { + if index < len(p.panels)-1 { + if panel.Visible { + break // There is a remaining visible panel. + } + } else { + panel.Visible = true // We need at least one visible panel. + } + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// HasPanel returns true if a panel with the given name exists in this object. +func (p *Panels) HasPanel(name string) bool { + p.RLock() + defer p.RUnlock() + + for _, panel := range p.panels { + if panel.Name == name { + return true + } + } + return false +} + +// ShowPanel sets a panel's visibility to "true" (in addition to any other panels +// which are already visible). +func (p *Panels) ShowPanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = true + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// HidePanel sets a panel's visibility to "false". +func (p *Panels) HidePanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = false + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SetCurrentPanel sets a panel's visibility to "true" and all other panels' +// visibility to "false". +func (p *Panels) SetCurrentPanel(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for _, panel := range p.panels { + if panel.Name == name { + panel.Visible = true + } else { + panel.Visible = false + } + } + if p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SendToFront changes the order of the panels such that the panel with the given +// name comes last, causing it to be drawn last with the next update (if +// visible). +func (p *Panels) SendToFront(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for index, panel := range p.panels { + if panel.Name == name { + if index < len(p.panels)-1 { + p.panels = append(append(p.panels[:index], p.panels[index+1:]...), panel) + } + if panel.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// SendToBack changes the order of the panels such that the panel with the given +// name comes first, causing it to be drawn first with the next update (if +// visible). +func (p *Panels) SendToBack(name string) { + hasFocus := p.HasFocus() + + p.Lock() + defer p.Unlock() + + for index, pg := range p.panels { + if pg.Name == name { + if index > 0 { + p.panels = append(append([]*panel{pg}, p.panels[:index]...), p.panels[index+1:]...) + } + if pg.Visible && p.changed != nil { + p.Unlock() + p.changed() + p.Lock() + } + break + } + } + if hasFocus { + p.Unlock() + p.Focus(p.setFocus) + p.Lock() + } +} + +// GetFrontPanel returns the front-most visible panel. If there are no visible +// panels, ("", nil) is returned. +func (p *Panels) GetFrontPanel() (name string, item cview.Primitive) { + p.RLock() + defer p.RUnlock() + + for index := len(p.panels) - 1; index >= 0; index-- { + if p.panels[index].Visible { + return p.panels[index].Name, p.panels[index].Item + } + } + return +} + +// HasFocus returns whether or not this cview.Primitive has focus. +func (p *Panels) HasFocus() bool { + p.RLock() + defer p.RUnlock() + + for _, panel := range p.panels { + if panel.Item.GetFocusable().HasFocus() { + return true + } + } + return false +} + +// Focus is called by the application when the cview.Primitive receives focus. +func (p *Panels) Focus(delegate func(p cview.Primitive)) { + p.Lock() + defer p.Unlock() + + if delegate == nil { + return // We cannot delegate so we cannot focus. + } + p.setFocus = delegate + var topItem cview.Primitive + for _, panel := range p.panels { + if panel.Visible { + topItem = panel.Item + } + } + if topItem != nil { + p.Unlock() + delegate(topItem) + p.Lock() + } +} + +// Draw draws this cview.Primitive onto the screen. +func (p *Panels) Draw(screen tcell.Screen) { + if !p.GetVisible() { + return + } + + p.Box.Draw(screen) + + p.Lock() + defer p.Unlock() + + x, y, width, height := p.GetInnerRect() + + for _, panel := range p.panels { + if !panel.Visible { + continue + } + if panel.Resize { + panel.Item.SetRect(x, y, width, height) + } + panel.Item.Draw(screen) + } +} + +// MouseHandler returns the mouse handler for this cview.Primitive. +func (p *Panels) MouseHandler() func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + return p.WrapMouseHandler(func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + if !p.InRect(event.Position()) { + return false, nil + } + + // Pass mouse events along to the last visible panel item that takes it. + for index := len(p.panels) - 1; index >= 0; index-- { + panel := p.panels[index] + if panel.Visible { + consumed, capture = panel.Item.MouseHandler()(action, event, setFocus) + if consumed { + return + } + } + } + + return + }) +} + +// Support backwards compatibility with Pages. +type page = panel + +// Pages is a wrapper around Panels. +// +// Deprecated: This type is provided for backwards compatibility. +// Developers should use Panels instead. +type Pages struct { + *Panels +} + +// NewPages returns a new Panels object. +// +// Deprecated: This function is provided for backwards compatibility. +// Developers should use NewPanels instead. +func NewPages() *Pages { + return &Pages{NewPanels()} +} + +// GetPageCount returns the number of panels currently stored in this object. +func (p *Pages) GetPageCount() int { + return p.GetPanelCount() +} + +// AddPage adds a new panel with the given name and cview.Primitive. +func (p *Pages) AddPage(name string, item cview.Primitive, resize, visible bool) { + p.AddPanel(name, item, resize, visible) +} + +// AddAndSwitchToPage calls Add(), then SwitchTo() on that newly added panel. +func (p *Pages) AddAndSwitchToPage(name string, item cview.Primitive, resize bool) { + p.AddPanel(name, item, resize, true) + p.SetCurrentPanel(name) +} + +// RemovePage removes the panel with the given name. +func (p *Pages) RemovePage(name string) { + p.RemovePanel(name) +} + +// HasPage returns true if a panel with the given name exists in this object. +func (p *Pages) HasPage(name string) bool { + return p.HasPanel(name) +} + +// ShowPage sets a panel's visibility to "true". +func (p *Pages) ShowPage(name string) { + p.ShowPanel(name) +} + +// HidePage sets a panel's visibility to "false". +func (p *Pages) HidePage(name string) { + p.HidePanel(name) +} + +// SwitchToPage sets a panel's visibility to "true" and all other panels' +// visibility to "false". +func (p *Pages) SwitchToPage(name string) { + p.SetCurrentPanel(name) +} + +// GetFrontPage returns the front-most visible panel. +func (p *Pages) GetFrontPage() (name string, item cview.Primitive) { + return p.GetFrontPanel() +} diff --git a/TabTitleMenu/screenshot.png b/TabTitleMenu/screenshot.png new file mode 100755 index 0000000..a852a03 --- /dev/null +++ b/TabTitleMenu/screenshot.png Binary files differ diff --git a/TabTitleMenu/tabbedpanels.go b/TabTitleMenu/tabbedpanels.go new file mode 100755 index 0000000..2b35f82 --- /dev/null +++ b/TabTitleMenu/tabbedpanels.go @@ -0,0 +1,433 @@ +package main + +import ( + "bytes" + "fmt" + "sync" + "strings" + + "github.com/gdamore/tcell/v2" + "code.rocketnine.space/tslocum/cview" +) + +// TabbedPanels is a tabbed container for other cview.Primitives. The tab switcher +// may be positioned vertically or horizontally, before or after the content. +type TabbedPanels struct { + *cview.Flex + Switcher *cview.TextView + panels *Panels + + tabLabels map[string]string + currentTab string + + dividerStart string + dividerMid string + dividerEnd string + + switcherVertical bool + switcherAfterContent bool + switcherHeight int + + width, lastWidth int + + setFocus func(cview.Primitive) + + sync.RWMutex +} + +// NewTabbedPanels returns a new TabbedPanels object. +func NewTabbedPanels() *TabbedPanels { + t := &TabbedPanels{ + Flex: cview.NewFlex(), + Switcher: cview.NewTextView(), + panels: NewPanels(), + dividerStart: string(cview.BoxDrawingsLightDownAndRight), + dividerMid: string("-"), + dividerEnd: string(cview.BoxDrawingsLightDownAndLeft), + tabLabels: make(map[string]string), + } + + s := t.Switcher + s.SetDynamicColors(true) + s.SetHighlightForegroundColor(tcell.Color226) // yellow + s.SetHighlightBackgroundColor(tcell.Color16) // black + s.SetRegions(true) + s.SetScrollable(true) + s.SetWrap(true) + s.SetWordWrap(true) + s.SetHighlightedFunc(func(added, removed, remaining []string) { + if len(added) == 0 { + return + } + + s.ScrollToHighlight() + t.SetCurrentTab(added[0]) + if t.setFocus != nil { + t.setFocus(t.panels) + } + }) + + t.rebuild() + + return t +} + +// SetChangedFunc sets a handler which is called whenever a tab is added, +// selected, reordered or removed. +func (t *TabbedPanels) SetChangedFunc(handler func()) { + t.panels.SetChangedFunc(handler) +} + +// AddTab adds a new tab. Tab names should consist only of letters, numbers +// and spaces. +func (t *TabbedPanels) AddTab(name, label string, item cview.Primitive) { + t.Lock() + t.tabLabels[name] = label + t.Unlock() + + t.panels.AddPanel(name, item, true, false) + + t.updateAll() +} + +// RemoveTab removes a tab. +func (t *TabbedPanels) RemoveTab(name string) { + t.panels.RemovePanel(name) + + t.updateAll() +} + +// HasTab returns true if a tab with the given name exists in this object. +func (t *TabbedPanels) HasTab(name string) bool { + t.RLock() + defer t.RUnlock() + + for _, panel := range t.panels.panels { + if panel.Name == name { + return true + } + } + return false +} + +// SetCurrentTab sets the currently visible tab. +func (t *TabbedPanels) SetCurrentTab(name string) { + t.Lock() + + if t.currentTab == name { + t.Unlock() + return + } + + t.currentTab = name + + t.updateAll() + + t.Unlock() + + h := t.Switcher.GetHighlights() + var found bool + for _, hl := range h { + if hl == name { + found = true + break + } + } + if !found { + t.Switcher.Highlight(t.currentTab) + } + t.Switcher.ScrollToHighlight() +} + +// GetCurrentTab returns the currently visible tab. +func (t *TabbedPanels) GetCurrentTab() string { + t.RLock() + defer t.RUnlock() + return t.currentTab +} + +// SetTabLabel sets the label of a tab. +func (t *TabbedPanels) SetTabLabel(name, label string) { + t.Lock() + defer t.Unlock() + + if t.tabLabels[name] == label { + return + } + + t.tabLabels[name] = label + t.updateTabLabels() +} + +// SetTabTextColor sets the color of the tab text. +func (t *TabbedPanels) SetTabTextColor(color tcell.Color) { + t.Switcher.SetTextColor(color) +} + +// SetTabTextColorFocused sets the color of the tab text when the tab is in focus. +func (t *TabbedPanels) SetTabTextColorFocused(color tcell.Color) { + t.Switcher.SetHighlightForegroundColor(color) +} + +// SetTabBackgroundColor sets the background color of the tab. +func (t *TabbedPanels) SetTabBackgroundColor(color tcell.Color) { + t.Switcher.SetBackgroundColor(color) +} + +// SetTabBackgroundColorFocused sets the background color of the tab when the +// tab is in focus. +func (t *TabbedPanels) SetTabBackgroundColorFocused(color tcell.Color) { + t.Switcher.SetHighlightBackgroundColor(color) +} + +// SetTabSwitcherDivider sets the tab switcher divider text. Color tags are supported. +func (t *TabbedPanels) SetTabSwitcherDivider(start, mid, end string) { + t.Lock() + defer t.Unlock() + t.dividerStart, t.dividerMid, t.dividerEnd = start, mid, end +} + +// SetTabSwitcherHeight sets the tab switcher height. This setting only applies +// when rendering horizontally. A value of 0 (the default) indicates the height +// should automatically adjust to fit all of the tab labels. +func (t *TabbedPanels) SetTabSwitcherHeight(height int) { + t.Lock() + defer t.Unlock() + + t.switcherHeight = height + t.rebuild() +} + +// SetTabSwitcherVertical sets the orientation of the tab switcher. +func (t *TabbedPanels) SetTabSwitcherVertical(vertical bool) { + t.Lock() + defer t.Unlock() + if t.switcherVertical == vertical { + return + } + + t.switcherVertical = vertical + t.rebuild() +} + +// SetTabSwitcherAfterContent sets whether the tab switcher is positioned after content. +func (t *TabbedPanels) SetTabSwitcherAfterContent(after bool) { + t.Lock() + defer t.Unlock() + + if t.switcherAfterContent == after { + return + } + + t.switcherAfterContent = after + t.rebuild() +} + +func (t *TabbedPanels) rebuild() { + f := t.Flex + if t.switcherVertical { + f.SetDirection(cview.FlexColumn) + } else { + f.SetDirection(cview.FlexRow) + } + f.RemoveItem(t.panels) + f.RemoveItem(t.Switcher) + if t.switcherAfterContent { + f.AddItem(t.panels, 0, 1, true) + f.AddItem(t.Switcher, 1, 1, false) + } else { + f.AddItem(t.Switcher, 1, 1, false) + f.AddItem(t.panels, 0, 1, true) + } + + t.updateTabLabels() + + t.Switcher.SetMaxLines(t.switcherHeight) +} + +func (t *TabbedPanels) updateTabLabels() { + if len(t.panels.panels) == 0 { + t.Switcher.SetText("") + t.Flex.ResizeItem(t.Switcher, 0, 1) + return + } + + maxWidth := 0 + for _, panel := range t.panels.panels { + label := t.tabLabels[panel.Name] + if len(label) > maxWidth { + maxWidth = len(label) + } + } + + var b bytes.Buffer + if !t.switcherVertical { + b.WriteString(t.dividerStart) + } + l := len(t.panels.panels) + spacer := []byte(" ") + for i, panel := range t.panels.panels { + if i > 0 && t.switcherVertical { + b.WriteRune('\n') + } + + if t.switcherVertical && t.switcherAfterContent { + b.WriteString(t.dividerMid) + b.WriteRune(' ') + } + + label := t.tabLabels[panel.Name] + if !t.switcherVertical { + label = " " + label + } + + if t.switcherVertical { + spacer = bytes.Repeat([]byte(" "), maxWidth-len(label)+1) + } + b.WriteString(fmt.Sprintf(`["%s"]%s%s[""]`, panel.Name, label, spacer)) + + if i == l-1 && !t.switcherVertical { + //fmt.Println("t.width", t.width, "maxwidth", maxWidth, "label", label, "l", l) + /*** + * This did not work! (trying to make when panel highlighted borders change) + */ + /* + spacer_char := "" + div_end := "" + if t.setFocus != nil { // not in focus! + spacer_char = string(cview.BoxDrawingsLightHorizontal) + div_end = t.dividerEnd + }else{ + spacer_char = "[::b]"+string(cview.BoxDrawingsHeavyHorizontal)+"[::-]" + div_end = string(cview.BoxDrawingsHeavyDownAndLeft) + } + */ + spacer_char := string(cview.BoxDrawingsLightHorizontal) + div_end := t.dividerEnd + + spacer_str := "" + if t.width > 0 { + spacer_len := 0 + for _, panel_test := range t.panels.panels{ + spacer_len += len(t.tabLabels[panel_test.Name])+2 + } + + spacer_str = strings.Repeat(spacer_char, t.width-maxWidth-spacer_len+2) + + }else{ + spacer_str = strings.Repeat("─", maxWidth+1) + } + b.WriteString(spacer_str + div_end) + } else if !t.switcherAfterContent { + b.WriteString(t.dividerMid) + } + } + t.Switcher.SetText(b.String()) + + var reqLines int + if t.switcherVertical { + reqLines = maxWidth + 2 + } else { + if t.switcherHeight > 0 { + reqLines = t.switcherHeight + } else { + reqLines = len(cview.WordWrap(t.Switcher.GetText(true), t.width)) + if reqLines < 1 { + reqLines = 1 + } + } + } + t.Flex.ResizeItem(t.Switcher, reqLines, 1) +} + +func (t *TabbedPanels) updateVisibleTabs() { + allPanels := t.panels.panels + + var newTab string + + var foundCurrent bool + for _, panel := range allPanels { + if panel.Name == t.currentTab { + newTab = panel.Name + foundCurrent = true + break + } + } + if !foundCurrent { + for _, panel := range allPanels { + if panel.Name != "" { + newTab = panel.Name + break + } + } + } + + if t.currentTab != newTab { + t.SetCurrentTab(newTab) + return + } + + for _, panel := range allPanels { + if panel.Name == t.currentTab { + t.panels.ShowPanel(panel.Name) + } else { + t.panels.HidePanel(panel.Name) + } + } +} + +func (t *TabbedPanels) updateAll() { + t.updateTabLabels() + t.updateVisibleTabs() +} + +// Draw draws this cview.Primitive onto the screen. +func (t *TabbedPanels) Draw(screen tcell.Screen) { + if !t.GetVisible() { + return + } + + t.Box.Draw(screen) + + _, _, t.width, _ = t.GetInnerRect() + if t.width != t.lastWidth { + t.updateTabLabels() + } + t.lastWidth = t.width + + t.Flex.Draw(screen) +} + +// InputHandler returns the handler for this cview.Primitive. +func (t *TabbedPanels) InputHandler() func(event *tcell.EventKey, setFocus func(p cview.Primitive)) { + return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p cview.Primitive)) { + if t.setFocus == nil { + t.setFocus = setFocus + } + t.Flex.InputHandler()(event, setFocus) + }) +} + +// MouseHandler returns the mouse handler for this cview.Primitive. +func (t *TabbedPanels) MouseHandler() func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + return t.WrapMouseHandler(func(action cview.MouseAction, event *tcell.EventMouse, setFocus func(p cview.Primitive)) (consumed bool, capture cview.Primitive) { + if t.setFocus == nil { + t.setFocus = setFocus + } + + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + if t.Switcher.InRect(x, y) { + if t.setFocus != nil { + defer t.setFocus(t.panels) + } + defer t.Switcher.MouseHandler()(action, event, setFocus) + return true, nil + } + + return t.Flex.MouseHandler()(action, event, setFocus) + }) +}