From 5f5b79b00ecadbc4755705f15a2230793436dda2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 27 Mar 2020 21:13:03 +0100 Subject: [PATCH] Added mouse support for Table and TreeView. --- README.md | 2 +- demos/table/main.go | 2 +- demos/treeview/main.go | 2 +- dropdown.go | 5 ++- table.go | 82 +++++++++++++++++++++++++++++++++++++++--- treeview.go | 32 +++++++++++++++++ 6 files changed, 115 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ddceec7..8ce62bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Rich Interactive Widgets for Terminal UIs -[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/rivo/tview) +[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/rivo/tview) [![Go Report](https://img.shields.io/badge/go%20report-A%2B-brightgreen.svg)](https://goreportcard.com/report/github.com/rivo/tview) This Go package provides commonly needed components for terminal based user interfaces. diff --git a/demos/table/main.go b/demos/table/main.go index 38cb0c1..906ed56 100644 --- a/demos/table/main.go +++ b/demos/table/main.go @@ -39,7 +39,7 @@ func main() { table.GetCell(row, column).SetTextColor(tcell.ColorRed) table.SetSelectable(false, false) }) - if err := app.SetRoot(table, true).Run(); err != nil { + if err := app.SetRoot(table, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/treeview/main.go b/demos/treeview/main.go index bce3cba..5a6805d 100644 --- a/demos/treeview/main.go +++ b/demos/treeview/main.go @@ -56,7 +56,7 @@ func main() { } }) - if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { + if err := tview.NewApplication().SetRoot(tree, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/dropdown.go b/dropdown.go index 12d04ed..89d50c1 100644 --- a/dropdown.go +++ b/dropdown.go @@ -419,8 +419,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr }) } -// evalPrefix is selects an item in the drop-down list based on the current -// prefix. +// evalPrefix selects an item in the drop-down list based on the current prefix. func (d *DropDown) evalPrefix() { if len(d.prefix) > 0 { for index, option := range d.options { @@ -430,7 +429,7 @@ func (d *DropDown) evalPrefix() { } } - // Prefix does not match any item. Remove last rune. TODO: Use uniseg here. + // Prefix does not match any item. Remove last rune. r := []rune(d.prefix) d.prefix = string(r[:len(r)-1]) } diff --git a/table.go b/table.go index 04fe448..05923b7 100644 --- a/table.go +++ b/table.go @@ -251,6 +251,13 @@ type Table struct { // The number of visible rows the last time the table was drawn. visibleRows int + // The indices of the visible columns as of the last time the table was drawn. + visibleColumnIndices []int + + // The net widths of the visible columns as of the last time the table was + // drawn. + visibleColumnWidths []int + // The style of the selected rows. If this value is 0, selected rows are // simply inverted. selectedStyle tcell.Style @@ -360,8 +367,8 @@ func (t *Table) GetSelection() (row, column int) { // Select sets the selected cell. Depending on the selection settings // specified via SetSelectable(), this may be an entire row or column, or even // ignored completely. The "selection changed" event is fired if such a callback -// is available (even if the selection ends up being the same as before, even if -// cells are not selectable). +// is available (even if the selection ends up being the same as before and even +// if cells are not selectable). func (t *Table) Select(row, column int) *Table { t.selectedRow, t.selectedColumn = row, column if t.selectionChanged != nil { @@ -537,6 +544,49 @@ func (t *Table) GetColumnCount() int { return t.lastColumn + 1 } +// cellAt returns the row and column located at the given screen coordinates. +// Each returned value may be negative if there is no row and/or cell. This +// function will also process coordinates outside the table's inner rectangle so +// callers will need to check for bounds themselves. +func (t *Table) cellAt(x, y int) (row, column int) { + rectX, rectY, _, _ := t.GetInnerRect() + + // Determine row as seen on screen. + if t.borders { + row = (y - rectY - 1) / 2 + } else { + row = y - rectY + } + + // Respect fixed rows and row offset. + if row >= 0 { + if row >= t.fixedRows { + row += t.rowOffset + } + if row >= len(t.cells) { + row = -1 + } + } + + // Saerch for the clicked column. + column = -1 + if x >= rectX { + columnX := rectX + if t.borders { + columnX++ + } + for index, width := range t.visibleColumnWidths { + columnX += width + 1 + if x < columnX { + column = t.visibleColumnIndices[index] + break + } + } + } + + return +} + // ScrollToBeginning scrolls the table to the beginning to that the top left // corner of the table is shown. Note that this position may be corrected if // there is a selection. @@ -824,8 +874,8 @@ ColumnLoop: cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes)) if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 { - _, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY) - printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style) + _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) + printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style) } } @@ -964,6 +1014,9 @@ ColumnLoop: } } } + + // Remember column infos. + t.visibleColumnIndices, t.visibleColumnWidths = columns, widths } // InputHandler returns the handler for this primitive. @@ -1173,3 +1226,24 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi } }) } + +// MouseHandler returns the mouse handler for this primitive. +func (t *Table) 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) { + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + switch action { + case MouseLeftClick: + if t.rowsSelectable || t.columnsSelectable { + t.Select(t.cellAt(x, y)) + } + consumed = true + setFocus(t) + } + + return + }) +} diff --git a/treeview.go b/treeview.go index 2a8c16c..95ababa 100644 --- a/treeview.go +++ b/treeview.go @@ -730,3 +730,35 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr t.process() }) } + +// MouseHandler returns the mouse handler for this primitive. +func (t *TreeView) 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) { + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + switch action { + case MouseLeftClick: + _, rectY, _, _ := t.GetInnerRect() + y -= rectY + if y >= 0 && y < len(t.nodes) { + node := t.nodes[y] + if node.selectable { + if t.currentNode != node && t.changed != nil { + t.changed(node) + } + if t.selected != nil { + t.selected(node) + } + t.currentNode = node + } + } + consumed = true + setFocus(t) + } + + return + }) +}