From 50b3201606e8376d7e84b9d36c6714b4df987f91 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Thu, 15 Oct 2020 18:50:10 -0700 Subject: [PATCH] Add TabbedPanels --- CHANGELOG | 1 + demos/{pages => panels}/README.md | 0 demos/{pages => panels}/main.go | 4 +- demos/{pages => panels}/screenshot.png | Bin demos/presentation/flex.go | 8 +- demos/presentation/grid.go | 8 +- demos/presentation/main.go | 34 +--- demos/tabbedpanels/main.go | 40 +++++ demos/unicode/main.go | 8 +- pages.go | 60 ------- panels.go | 94 +++++++--- tabbedpanels.go | 240 +++++++++++++++++++++++++ 12 files changed, 376 insertions(+), 121 deletions(-) rename demos/{pages => panels}/README.md (100%) rename demos/{pages => panels}/main.go (81%) rename demos/{pages => panels}/screenshot.png (100%) create mode 100644 demos/tabbedpanels/main.go delete mode 100644 pages.go create mode 100644 tabbedpanels.go diff --git a/CHANGELOG b/CHANGELOG index 40beae7..be486ce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ v1.5.1 (WIP) - Add Slider +- Add TabbedPanels - Add Application.GetScreen and Application.GetScreenSize - Add TextView.SetBytes and TextView.GetBytes - Add TableCell.SetBytes, TableCell.GetBytes and TableCell.GetText diff --git a/demos/pages/README.md b/demos/panels/README.md similarity index 100% rename from demos/pages/README.md rename to demos/panels/README.md diff --git a/demos/pages/main.go b/demos/panels/main.go similarity index 81% rename from demos/pages/main.go rename to demos/panels/main.go index 38529aa..dee77bb 100644 --- a/demos/pages/main.go +++ b/demos/panels/main.go @@ -21,13 +21,13 @@ func main() { modal.AddButtons([]string{"Next", "Quit"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { if buttonIndex == 0 { - panels.SwitchTo(fmt.Sprintf("panel-%d", (panel+1)%panelCount)) + panels.SetCurrentPanel(fmt.Sprintf("panel-%d", (panel+1)%panelCount)) } else { app.Stop() } }) - panels.Add(fmt.Sprintf("panel-%d", panel), modal, false, panel == 0) + panels.AddPanel(fmt.Sprintf("panel-%d", panel), modal, false, panel == 0) }(panel) } diff --git a/demos/pages/screenshot.png b/demos/panels/screenshot.png similarity index 100% rename from demos/pages/screenshot.png rename to demos/panels/screenshot.png diff --git a/demos/presentation/flex.go b/demos/presentation/flex.go index f85a6b5..e63587c 100644 --- a/demos/presentation/flex.go +++ b/demos/presentation/flex.go @@ -25,7 +25,7 @@ func Flex(nextSlide func()) (title string, content cview.Primitive) { nextSlide() modalShown = false } else { - panels.Show("modal") + panels.ShowPanel("modal") modalShown = true } }) @@ -45,10 +45,10 @@ func Flex(nextSlide func()) (title string, content cview.Primitive) { modal.SetText("Resize the window to see the effect of the flexbox parameters") modal.AddButtons([]string{"Ok"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - panels.Hide("modal") + panels.HidePanel("modal") }) - panels.Add("flex", flex, true, true) - panels.Add("modal", modal, false, false) + panels.AddPanel("flex", flex, true, true) + panels.AddPanel("modal", modal, false, false) return "Flex", panels } diff --git a/demos/presentation/grid.go b/demos/presentation/grid.go index 3e54815..e7f5438 100644 --- a/demos/presentation/grid.go +++ b/demos/presentation/grid.go @@ -19,7 +19,7 @@ func Grid(nextSlide func()) (title string, content cview.Primitive) { nextSlide() modalShown = false } else { - panels.Show("modal") + panels.ShowPanel("modal") modalShown = true } }) @@ -51,11 +51,11 @@ func Grid(nextSlide func()) (title string, content cview.Primitive) { modal.SetText("Resize the window to see how the grid layout adapts") modal.AddButtons([]string{"Ok"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - panels.Hide("modal") + panels.HidePanel("modal") }) - panels.Add("grid", grid, true, true) - panels.Add("modal", modal, false, false) + panels.AddPanel("grid", grid, true, true) + panels.AddPanel("modal", modal, false, false) return "Grid", panels } diff --git a/demos/presentation/main.go b/demos/presentation/main.go index 59106a5..5ff7398 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -64,29 +64,18 @@ func main() { End, } - panels := cview.NewPanels() - - // The bottom row has some info on where we are. - info := cview.NewTextView() - info.SetDynamicColors(true) - info.SetRegions(true) - info.SetWrap(false) - info.SetHighlightedFunc(func(added, removed, remaining []string) { - panels.SwitchTo(added[0]) - }) + panels := cview.NewTabbedPanels() // Create the pages for all slides. previousSlide := func() { - slide, _ := strconv.Atoi(info.GetHighlights()[0]) + slide, _ := strconv.Atoi(panels.GetCurrentTab()) slide = (slide - 1 + len(slides)) % len(slides) - info.Highlight(strconv.Itoa(slide)) - info.ScrollToHighlight() + panels.SetCurrentTab(strconv.Itoa(slide)) } nextSlide := func() { - slide, _ := strconv.Atoi(info.GetHighlights()[0]) + slide, _ := strconv.Atoi(panels.GetCurrentTab()) slide = (slide + 1) % len(slides) - info.Highlight(strconv.Itoa(slide)) - info.ScrollToHighlight() + panels.SetCurrentTab(strconv.Itoa(slide)) } cursor := 0 @@ -95,18 +84,11 @@ func main() { slideRegions = append(slideRegions, cursor) title, primitive := slide(nextSlide) - panels.Add(strconv.Itoa(index), primitive, true, index == 0) - fmt.Fprintf(info, `["%d"][darkcyan] %s [white][""]|`, index, title) + panels.AddTab(strconv.Itoa(index), title, primitive) cursor += len(title) + 4 } - info.Highlight("0") - - // Create the main layout. - layout := cview.NewFlex() - layout.SetDirection(cview.FlexRow) - layout.AddItem(panels, 0, 1, true) - layout.AddItem(info, 1, 1, false) + panels.SetCurrentTab("0") // Shortcuts to navigate the slides. app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { @@ -119,7 +101,7 @@ func main() { }) // Start the application. - app.SetRoot(layout, true) + app.SetRoot(panels, true) if err := app.Run(); err != nil { panic(err) } diff --git a/demos/tabbedpanels/main.go b/demos/tabbedpanels/main.go new file mode 100644 index 0000000..15ba26a --- /dev/null +++ b/demos/tabbedpanels/main.go @@ -0,0 +1,40 @@ +// Demo code for the TabbedPanels primitive. +package main + +import ( + "fmt" + + "gitlab.com/tslocum/cview" +) + +const panelCount = 5 + +func main() { + app := cview.NewApplication() + app.EnableMouse(true) + + panels := cview.NewTabbedPanels() + for panel := 0; panel < panelCount; panel++ { + func(panel int) { + form := cview.NewForm() + form.SetBorder(true) + 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) + } + + app.SetRoot(panels, true) + if err := app.Run(); err != nil { + panic(err) + } +} diff --git a/demos/unicode/main.go b/demos/unicode/main.go index 6fe91e1..91a7b44 100644 --- a/demos/unicode/main.go +++ b/demos/unicode/main.go @@ -28,7 +28,7 @@ func main() { form.SetBorder(true) form.SetTitle("输入一些内容") form.SetTitleAlign(cview.AlignLeft) - panels.Add("base", form, true, true) + panels.AddPanel("base", form, true, true) app.SetRoot(panels, true) if err := app.Run(); err != nil { @@ -42,9 +42,9 @@ func alert(panels *cview.Panels, id string, message string) { modal.SetText(message) modal.AddButtons([]string{"确定"}) modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - panels.Hide(id) - panels.Remove(id) + panels.HidePanel(id) + panels.RemovePanel(id) }) - panels.Add(id, modal, false, true) + panels.AddPanel(id, modal, false, true) } diff --git a/pages.go b/pages.go deleted file mode 100644 index 6b6857a..0000000 --- a/pages.go +++ /dev/null @@ -1,60 +0,0 @@ -package cview - -type page = panel - -// Pages is a wrapper around Panels. It is provided for backwards compatibility. -// Application developers should use Panels instead. -type Pages struct { - *Panels -} - -// NewPages returns a new Panels object. -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 primitive. -func (p *Pages) AddPage(name string, item Primitive, resize, visible bool) { - p.Add(name, item, resize, visible) -} - -// AddAndSwitchToPage calls Add(), then SwitchTo() on that newly added panel. -func (p *Pages) AddAndSwitchToPage(name string, item Primitive, resize bool) { - p.AddAndSwitchTo(name, item, resize) -} - -// RemovePage removes the panel with the given name. -func (p *Pages) RemovePage(name string) { - p.Remove(name) -} - -// HasPage returns true if a panel with the given name exists in this object. -func (p *Pages) HasPage(name string) bool { - return p.Has(name) -} - -// ShowPage sets a panel's visibility to "true". -func (p *Pages) ShowPage(name string) { - p.Show(name) -} - -// HidePage sets a panel's visibility to "false". -func (p *Pages) HidePage(name string) { - p.Hide(name) -} - -// SwitchToPage sets a panel's visibility to "true" and all other panels' -// visibility to "false". -func (p *Pages) SwitchToPage(name string) { - p.SwitchTo(name) -} - -// GetFrontPage returns the front-most visible panel. -func (p *Pages) GetFrontPage() (name string, item Primitive) { - return p.GetFrontPanel() -} diff --git a/panels.go b/panels.go index 72c083a..812117c 100644 --- a/panels.go +++ b/panels.go @@ -31,9 +31,6 @@ type Panels struct { // panels changes. changed func() - // TODO enable tabs - tabs *TextView - sync.RWMutex } @@ -63,7 +60,7 @@ func (p *Panels) GetPanelCount() int { return len(p.panels) } -// Add adds a new panel with the given name and primitive. If there was +// AddPanel adds a new panel with the given name and 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. @@ -72,7 +69,7 @@ func (p *Panels) GetPanelCount() int { // was changed in one of the other functions). If "resize" is set to true, the // primitive will be set to the size available to the Panels primitive whenever // the panels are drawn. -func (p *Panels) Add(name string, item Primitive, resize, visible bool) { +func (p *Panels) AddPanel(name string, item Primitive, resize, visible bool) { hasFocus := p.HasFocus() p.Lock() @@ -97,15 +94,9 @@ func (p *Panels) Add(name string, item Primitive, resize, visible bool) { } } -// AddAndSwitchTo calls Add(), then SwitchTo() on the newly added panel. -func (p *Panels) AddAndSwitchTo(name string, item Primitive, resize bool) { - p.Add(name, item, resize, true) - p.SwitchTo(name) -} - -// Remove removes the panel with the given name. If that panel was the only +// 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) Remove(name string) { +func (p *Panels) RemovePanel(name string) { hasFocus := p.HasFocus() p.Lock() @@ -142,8 +133,8 @@ func (p *Panels) Remove(name string) { } } -// Has returns true if a panel with the given name exists in this object. -func (p *Panels) Has(name string) bool { +// 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() @@ -155,9 +146,9 @@ func (p *Panels) Has(name string) bool { return false } -// Show sets a panel's visibility to "true" (in addition to any other panels +// ShowPanel sets a panel's visibility to "true" (in addition to any other panels // which are already visible). -func (p *Panels) Show(name string) { +func (p *Panels) ShowPanel(name string) { hasFocus := p.HasFocus() p.Lock() @@ -181,8 +172,8 @@ func (p *Panels) Show(name string) { } } -// Hide sets a panel's visibility to "false". -func (p *Panels) Hide(name string) { +// HidePanel sets a panel's visibility to "false". +func (p *Panels) HidePanel(name string) { hasFocus := p.HasFocus() p.Lock() @@ -206,9 +197,9 @@ func (p *Panels) Hide(name string) { } } -// SwitchTo sets a panel's visibility to "true" and all other panels' +// SetCurrentPanel sets a panel's visibility to "true" and all other panels' // visibility to "false". -func (p *Panels) SwitchTo(name string) { +func (p *Panels) SetCurrentPanel(name string) { hasFocus := p.HasFocus() p.Lock() @@ -381,3 +372,64 @@ func (p *Panels) MouseHandler() func(action MouseAction, event *tcell.EventMouse return }) } + +// Support backwards compatibility with Pages. +type page = panel + +// Pages is a wrapper around Panels. It is provided for backwards compatibility. +// Application developers should use Panels instead. +type Pages struct { + *Panels +} + +// NewPages returns a new Panels object. +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 primitive. +func (p *Pages) AddPage(name string, item 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 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 Primitive) { + return p.GetFrontPanel() +} diff --git a/tabbedpanels.go b/tabbedpanels.go new file mode 100644 index 0000000..db8e142 --- /dev/null +++ b/tabbedpanels.go @@ -0,0 +1,240 @@ +package cview + +import ( + "bytes" + "fmt" + "sync" + + "github.com/gdamore/tcell/v2" +) + +// TabbedPanels is a tabbed container for other primitives. The tab switcher +// may be displayed at the top or bottom of the container. +type TabbedPanels struct { + *Flex + panels *Panels + tabs *TextView + + tabLabels map[string]string + currentTab string + + bottomTabSwitcher bool + + width, lastWidth int + + setFocus func(Primitive) + + sync.RWMutex +} + +// NewTabbedPanels returns a new TabbedPanels object. +func NewTabbedPanels() *TabbedPanels { + t := &TabbedPanels{ + Flex: NewFlex(), + panels: NewPanels(), + tabs: NewTextView(), + tabLabels: make(map[string]string), + } + + t.tabs.SetDynamicColors(true) + t.tabs.SetRegions(true) + t.tabs.SetWrap(true) + t.tabs.SetWordWrap(true) + t.tabs.SetHighlightedFunc(func(added, removed, remaining []string) { + t.SetCurrentTab(added[0]) + if t.setFocus != nil { + t.setFocus(t.panels) + } + }) + + f := t.Flex + f.SetDirection(FlexRow) + f.AddItem(t.tabs, 1, 1, false) + f.AddItem(t.panels, 0, 1, true) + + return t +} + +// AddTab adds a new tab. Tab names should consist only of letters, numbers +// and spaces. +func (t *TabbedPanels) AddTab(name, label string, item 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, label string, item Primitive) { + t.panels.RemovePanel(name) + + t.updateAll() +} + +// 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() + + t.tabs.Highlight(t.currentTab) +} + +// 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() +} + +// SetTabSwitcherPosition sets the position of the tab switcher. +func (t *TabbedPanels) SetTabSwitcherPosition(bottom bool) { + t.Lock() + defer t.Unlock() + + if t.bottomTabSwitcher == bottom { + return + } + + t.bottomTabSwitcher = bottom + + f := t.Flex + f.RemoveItem(t.panels) + f.RemoveItem(t.tabs) + if t.bottomTabSwitcher { + f.AddItem(t.panels, 0, 1, true) + f.AddItem(t.tabs, 1, 1, false) + } else { + f.AddItem(t.tabs, 1, 1, false) + f.AddItem(t.panels, 0, 1, true) + } + + t.updateTabLabels() +} + +func (t *TabbedPanels) updateTabLabels() { + var b bytes.Buffer + for _, panel := range t.panels.panels { + b.WriteString(fmt.Sprintf(`["%s"][darkcyan] %s [white][""]|`, panel.Name, t.tabLabels[panel.Name])) + } + t.tabs.SetText(b.String()) + + reqLines := len(WordWrap(t.tabs.GetText(true), t.width)) + if reqLines < 1 { + reqLines = 1 + } + t.Flex.ResizeItem(t.tabs, 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 primitive onto the screen. +func (t *TabbedPanels) Draw(screen tcell.Screen) { + 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 primitive. +func (t *TabbedPanels) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { + return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + if t.setFocus == nil { + t.setFocus = setFocus + } + t.Flex.InputHandler()(event, setFocus) + }) +} + +// MouseHandler returns the mouse handler for this primitive. +func (t *TabbedPanels) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + if t.setFocus == nil { + t.setFocus = setFocus + } + + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + if t.tabs.InRect(x, y) { + if t.setFocus != nil { + defer t.setFocus(t.panels) + } + defer t.tabs.MouseHandler()(action, event, setFocus) + return true, nil + } + + return t.Flex.MouseHandler()(action, event, setFocus) + }) +}