package action import ( luar "layeh.com/gopher-luar" "github.com/micro-editor/tcell/v2" "github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/display" ulua "github.com/zyedidia/micro/v2/internal/lua" "github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/views" ) // The TabList is a list of tabs and a window to display the tab bar // at the top of the screen type TabList struct { *display.TabWindow List []*Tab } // NewTabList creates a TabList from a list of buffers by creating a Tab // for each buffer func NewTabList(bufs []*buffer.Buffer) *TabList { w, h := screen.Screen.Size() iOffset := config.GetInfoBarOffset() tl := new(TabList) tl.List = make([]*Tab, len(bufs)) if len(bufs) > 1 { for i, b := range bufs { tl.List[i] = NewTabFromBuffer(0, 1, w, h-1-iOffset, b) } } else { tl.List[0] = NewTabFromBuffer(0, 0, w, h-iOffset, bufs[0]) } tl.TabWindow = display.NewTabWindow(w, 0) tl.Names = make([]string, len(bufs)) return tl } // UpdateNames makes sure that the list of names the tab window has access to is // correct func (t *TabList) UpdateNames() { t.Names = t.Names[:0] for _, p := range t.List { t.Names = append(t.Names, p.Panes[p.active].Name()) } } // AddTab adds a new tab to this TabList func (t *TabList) AddTab(p *Tab) { t.List = append(t.List, p) t.Resize() t.UpdateNames() } // RemoveTab removes a tab with the given id from the TabList func (t *TabList) RemoveTab(id uint64) { for i, p := range t.List { if len(p.Panes) == 0 { continue } if p.Panes[0].ID() == id { copy(t.List[i:], t.List[i+1:]) t.List[len(t.List)-1] = nil t.List = t.List[:len(t.List)-1] if t.Active() >= len(t.List) { t.SetActive(len(t.List) - 1) } t.Resize() t.UpdateNames() return } } } // Resize resizes all elements within the tab list // One thing to note is that when there is only 1 tab // the tab bar should not be drawn so resizing must take // that into account func (t *TabList) Resize() { w, h := screen.Screen.Size() iOffset := config.GetInfoBarOffset() InfoBar.Resize(w, h-1) if len(t.List) > 1 { for _, p := range t.List { p.Y = 1 p.Node.Resize(w, h-1-iOffset) p.Resize() } } else if len(t.List) == 1 { t.List[0].Y = 0 t.List[0].Node.Resize(w, h-iOffset) t.List[0].Resize() } t.TabWindow.Resize(w, h) } // HandleEvent checks for a resize event or a mouse event on the tab bar // otherwise it will forward the event to the currently active tab func (t *TabList) HandleEvent(event tcell.Event) { switch e := event.(type) { case *tcell.EventResize: t.Resize() case *tcell.EventMouse: mx, my := e.Position() switch e.Buttons() { case tcell.Button1: if my == t.Y && len(t.List) > 1 { if mx == 0 { t.Scroll(-4) } else if mx == t.Width-1 { t.Scroll(4) } else { ind := t.LocFromVisual(buffer.Loc{mx, my}) if ind != -1 { t.SetActive(ind) } } return } case tcell.ButtonNone: if t.List[t.Active()].release { // Mouse release received, while already released t.ResetMouse() return } case tcell.WheelUp: if my == t.Y && len(t.List) > 1 { t.Scroll(4) return } case tcell.WheelDown: if my == t.Y && len(t.List) > 1 { t.Scroll(-4) return } } } t.List[t.Active()].HandleEvent(event) } // Display updates the names and then displays the tab bar func (t *TabList) Display() { t.UpdateNames() if len(t.List) > 1 { t.TabWindow.Display() } } func (t *TabList) SetActive(a int) { t.TabWindow.SetActive(a) for i, p := range t.List { if i == a { if !p.isActive { p.isActive = true err := config.RunPluginFn("onSetActive", luar.New(ulua.L, p.CurPane())) if err != nil { screen.TermMessage(err) } } } else { p.isActive = false } } } // ResetMouse resets the mouse release state after the screen was stopped // or the pane changed. // This prevents situations in which mouse releases are received at the wrong place // and the mouse state is still pressed. func (t *TabList) ResetMouse() { for _, tab := range t.List { if !tab.release && tab.resizing != nil { tab.resizing = nil } tab.release = true for _, p := range tab.Panes { if bp, ok := p.(*BufPane); ok { bp.resetMouse() } } } } // CloseTerms notifies term panes that a terminal job has finished. func (t *TabList) CloseTerms() { for _, tab := range t.List { for _, p := range tab.Panes { if tp, ok := p.(*TermPane); ok { tp.HandleTermClose() } } } } // Tabs is the global tab list var Tabs *TabList func InitTabs(bufs []*buffer.Buffer) { multiopen := config.GetGlobalOption("multiopen").(string) if multiopen == "tab" { Tabs = NewTabList(bufs) } else { Tabs = NewTabList(bufs[:1]) for _, b := range bufs[1:] { if multiopen == "vsplit" { MainTab().CurPane().VSplitBuf(b) } else { // default hsplit MainTab().CurPane().HSplitBuf(b) } } } screen.RestartCallback = Tabs.ResetMouse } func MainTab() *Tab { return Tabs.List[Tabs.Active()] } // A Tab represents a single tab // It consists of a list of edit panes (the open buffers), // a split tree (stored as just the root node), and a uiwindow // to display the UI elements like the borders between splits type Tab struct { *views.Node *display.UIWindow isActive bool Panes []Pane active int resizing *views.Node // node currently being resized // captures whether the mouse is released release bool } // NewTabFromBuffer creates a new tab from the given buffer func NewTabFromBuffer(x, y, width, height int, b *buffer.Buffer) *Tab { t := new(Tab) t.Node = views.NewRoot(x, y, width, height) t.UIWindow = display.NewUIWindow(t.Node) t.release = true e := NewBufPaneFromBuf(b, t) e.SetID(t.ID()) t.Panes = append(t.Panes, e) return t } func NewTabFromPane(x, y, width, height int, pane Pane) *Tab { t := new(Tab) t.Node = views.NewRoot(x, y, width, height) t.UIWindow = display.NewUIWindow(t.Node) t.release = true pane.SetTab(t) pane.SetID(t.ID()) t.Panes = append(t.Panes, pane) return t } // HandleEvent takes a tcell event and usually dispatches it to the current // active pane. However if the event is a resize or a mouse event where the user // is interacting with the UI (resizing splits) then the event is consumed here // If the event is a mouse press event in a pane, that pane will become active // and get the event func (t *Tab) HandleEvent(event tcell.Event) { switch e := event.(type) { case *tcell.EventMouse: mx, my := e.Position() btn := e.Buttons() switch { case btn & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone: // button press or drag wasReleased := t.release t.release = false if btn == tcell.Button1 { if t.resizing != nil { var size int if t.resizing.Kind == views.STVert { size = mx - t.resizing.X } else { size = my - t.resizing.Y + 1 } t.resizing.ResizeSplit(size) t.Resize() return } if wasReleased { t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my}) if t.resizing != nil { return } } } if wasReleased { for i, p := range t.Panes { v := p.GetView() inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height if inpane { t.SetActive(i) break } } } case btn == tcell.ButtonNone: // button release t.release = true if t.resizing != nil { t.resizing = nil return } default: // wheel move for _, p := range t.Panes { v := p.GetView() inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height if inpane { p.HandleEvent(event) return } } } } t.Panes[t.active].HandleEvent(event) } // SetActive changes the currently active pane to the specified index func (t *Tab) SetActive(i int) { t.active = i for j, p := range t.Panes { if j == i { p.SetActive(true) } else { p.SetActive(false) } } } // AddPane adds a pane at a given index func (t *Tab) AddPane(pane Pane, i int) { if len(t.Panes) == i { t.Panes = append(t.Panes, pane) return } t.Panes = append(t.Panes[:i+1], t.Panes[i:]...) t.Panes[i] = pane } // GetPane returns the pane with the given split index func (t *Tab) GetPane(splitid uint64) int { for i, p := range t.Panes { if p.ID() == splitid { return i } } return 0 } // Remove pane removes the pane with the given index func (t *Tab) RemovePane(i int) { copy(t.Panes[i:], t.Panes[i+1:]) t.Panes[len(t.Panes)-1] = nil t.Panes = t.Panes[:len(t.Panes)-1] } // Resize resizes all panes according to their corresponding split nodes func (t *Tab) Resize() { for _, p := range t.Panes { n := t.GetNode(p.ID()) pv := p.GetView() offset := 0 if n.X != 0 { offset = 1 } pv.X, pv.Y = n.X+offset, n.Y p.SetView(pv) p.Resize(n.W-offset, n.H) } } // CurPane returns the currently active pane func (t *Tab) CurPane() *BufPane { p, ok := t.Panes[t.active].(*BufPane) if !ok { return nil } return p }