mirror of
https://github.com/VladimirMarkelov/clui.git
synced 2025-04-24 13:48:53 +08:00
TableView's OnBeforeDraw feature closes #113
This commit is contained in:
parent
80da317c70
commit
380e4a5118
@ -7,7 +7,7 @@ Command Line User Interface (Console UI inspired by TurboVision) with built-in t
|
||||
|
||||
|
||||
## Current version
|
||||
The current version is 0.9.0 RC5. Please see details in [changelog](./changelog).
|
||||
The current version is 1.0.0 RC6. Please see details in [changelog](./changelog).
|
||||
|
||||
## Applications that uses the library
|
||||
* Terminal FB2 reader(termfb2): https://github.com/VladimirMarkelov/termfb2
|
||||
|
15
changelog
15
changelog
@ -1,3 +1,16 @@
|
||||
2018-11-09 - version 1.0.0 RC6
|
||||
[*] If no error is found in the next few weeks, this release will become 1.0.
|
||||
It is high time to release 1.0. Everything looks working.
|
||||
[+] TableView new method VisibleArea - returns first visible column and row,
|
||||
and the number of visible columns and rows. It can be useful to prepare
|
||||
data beforehand to draw the data faster
|
||||
[+] Table new event OnBeforeDraw(firstCol, firstRow, colCount, rowCount int).
|
||||
The event is fired right before TableView is going to draw itself. So the
|
||||
application can prepare all data in one step and then fetch them quickly
|
||||
from the cache. The arguments of a callback are the same as returns values
|
||||
of VisibleArea method
|
||||
[+] Added new demo tableview-preload to show how to use new event
|
||||
|
||||
2018-10-08 - version 0.9.0 RC5
|
||||
[*] Fix Frame border drawing
|
||||
|
||||
@ -6,7 +19,7 @@
|
||||
[+] ScrollTo API for scrollable frame
|
||||
[*] Clipper fix
|
||||
[*] ChildAt should skip hidden controls and skip a control if its parent
|
||||
is invisble
|
||||
is invisible
|
||||
[*] Enter key did not work in TableView control
|
||||
|
||||
2018-09-06 - version 0.9.0 RC3 (Thanks to Leandro Dorileo)
|
||||
|
130
demos/tableview-preload/tableview.go
Normal file
130
demos/tableview-preload/tableview.go
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Demo includes:
|
||||
* - How to use OnBeforeDraw event
|
||||
* - a simple example of "DBCache" for faster drawing
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
ui "github.com/VladimirMarkelov/clui"
|
||||
)
|
||||
|
||||
// number of columns in a table
|
||||
const columnInTable = 6
|
||||
|
||||
// dbCache for data from DB. It always caches the whole table row, so it does not
|
||||
// use firstCol and colCount values from OnBeforeDraw event. But you can do more
|
||||
// granular storage to minimize memory usage by cache
|
||||
// dbCache is quite dumb: if it detects that topRow or the number of visible rows
|
||||
// is changed it invalidates the cache and reloads all the data from new row span.
|
||||
// In real application, it would be good to make it smarter, e.g:
|
||||
// - if rowCount descreased and firstRow does not change - the cache is valid,
|
||||
// and redundant rereading data can be skipped
|
||||
// - usually visible area changes by 1 row, so performance-wise the cache can
|
||||
// shift row slice and read only new rows
|
||||
// - etc
|
||||
type dbCache struct {
|
||||
firstRow int // previous first visible row
|
||||
rowCount int // previous visible row count
|
||||
data [][]string // cache - contains at least 'rowCount' rows from DB
|
||||
}
|
||||
|
||||
// cache data from a new row span
|
||||
// It imitates a random data by selecting values from predefined arrays. Sizes
|
||||
// of all arrays should be different to make TableView data look more random
|
||||
func (d *dbCache) preload(firstRow, rowCount int) {
|
||||
if firstRow == d.firstRow && rowCount == d.rowCount {
|
||||
// fast path: view area is the same, return immediately
|
||||
return
|
||||
}
|
||||
|
||||
// slow path: refill cache
|
||||
fNames := []string{"Jack", "Alisa", "Richard", "Paul", "Nicole", "Steven", "Jane"}
|
||||
lNames := []string{"Smith", "Catcher", "Stone", "White", "Black"}
|
||||
posts := []string{"Engineer", "Manager", "Janitor", "Driver"}
|
||||
deps := []string{"IT", "Financial", "Support"}
|
||||
salary := []int{40000, 38000, 41000, 32000}
|
||||
|
||||
d.data = make([][]string, rowCount, rowCount)
|
||||
for i := 0; i < rowCount; i++ {
|
||||
absIndex := firstRow + i
|
||||
d.data[i] = make([]string, columnInTable, columnInTable)
|
||||
d.data[i][0] = fNames[absIndex%len(fNames)]
|
||||
d.data[i][1] = lNames[absIndex%len(lNames)]
|
||||
d.data[i][2] = fmt.Sprintf("%08d", 100+absIndex)
|
||||
d.data[i][3] = posts[absIndex%len(posts)]
|
||||
d.data[i][4] = deps[absIndex%len(deps)]
|
||||
d.data[i][5] = fmt.Sprintf("%d k/year", salary[absIndex%len(salary)]/1000)
|
||||
}
|
||||
|
||||
// do not forget to save the last values
|
||||
d.firstRow = firstRow
|
||||
d.rowCount = rowCount
|
||||
}
|
||||
|
||||
// returns the cell value for a given col and row. Col and row are absolute
|
||||
// value. But cache keeps limited number of rows to minimize memory usage.
|
||||
// So, the position of the value of the cell should be calculated
|
||||
// To simplify, the function just returns empty string if the cell is not
|
||||
// cached. It is unlikely but can happen
|
||||
func (d *dbCache) value(row, col int) string {
|
||||
rowId := row - d.firstRow
|
||||
if rowId >= len(d.data) {
|
||||
return ""
|
||||
}
|
||||
rowValues := d.data[rowId]
|
||||
if col >= len(rowValues) {
|
||||
return ""
|
||||
}
|
||||
return rowValues[col]
|
||||
}
|
||||
|
||||
var (
|
||||
view *ui.Window
|
||||
)
|
||||
|
||||
func createView() *ui.TableView {
|
||||
view = ui.AddWindow(0, 0, 10, 7, "TableView Preload Demo")
|
||||
bch := ui.CreateTableView(view, 35, 12, 1)
|
||||
ui.ActivateControl(view, bch)
|
||||
|
||||
return bch
|
||||
}
|
||||
|
||||
func mainLoop() {
|
||||
// Every application must create a single Composer and
|
||||
// call its intialize method
|
||||
ui.InitLibrary()
|
||||
defer ui.DeinitLibrary()
|
||||
|
||||
cache := &dbCache{firstRow: -1}
|
||||
b := createView()
|
||||
b.SetShowLines(true)
|
||||
b.SetShowRowNumber(true)
|
||||
b.SetRowCount(25)
|
||||
cols := []ui.Column{
|
||||
ui.Column{Title: "First Name", Width: 10, Alignment: ui.AlignLeft},
|
||||
ui.Column{Title: "Last Name", Width: 12, Alignment: ui.AlignLeft},
|
||||
ui.Column{Title: "ID", Width: 12, Alignment: ui.AlignRight},
|
||||
ui.Column{Title: "Post", Width: 12, Alignment: ui.AlignLeft},
|
||||
ui.Column{Title: "Department", Width: 15, Alignment: ui.AlignLeft},
|
||||
ui.Column{Title: "Salary", Width: 12, Alignment: ui.AlignRight},
|
||||
}
|
||||
b.SetColumns(cols)
|
||||
b.OnBeforeDraw(func(col, row, colCnt, rowCnt int) {
|
||||
cache.preload(row, rowCnt)
|
||||
l, t, w, h := b.VisibleArea()
|
||||
view.SetTitle(fmt.Sprintf("Caching: %d:%d - %dx%d", l, t, w, h))
|
||||
})
|
||||
b.OnDrawCell(func(info *ui.ColumnDrawInfo) {
|
||||
info.Text = cache.value(info.Row, info.Col)
|
||||
})
|
||||
|
||||
// start event processing loop - the main core of the library
|
||||
ui.MainLoop()
|
||||
}
|
||||
|
||||
func main() {
|
||||
mainLoop()
|
||||
}
|
76
tableview.go
76
tableview.go
@ -35,14 +35,19 @@ Events:
|
||||
OnKeyPress - called every time a user presses a key. Callback should
|
||||
return true if TableView must skip internal key processing.
|
||||
E.g, a user can disable emitting TableActionDelete event by
|
||||
adding callback OnKeyPress and retun true in case of Delete
|
||||
adding callback OnKeyPress and return true in case of Delete
|
||||
key is pressed
|
||||
OnSelectCell - called in case of the currently selected row or
|
||||
column is changed
|
||||
OnBeforeDraw - called right before the TableView is going to repaint
|
||||
itself. It can be used to prepare all the data beforehand and
|
||||
then quickly use cached data inside OnDrawCell. Callback
|
||||
receives 4 arguments: first visible column, first visible row,
|
||||
number of visible columns, number of visible rows.
|
||||
*/
|
||||
type TableView struct {
|
||||
BaseControl
|
||||
// own listbox members
|
||||
// own TableView members
|
||||
topRow int
|
||||
topCol int
|
||||
selectedRow int
|
||||
@ -57,6 +62,7 @@ type TableView struct {
|
||||
onAction func(TableEvent)
|
||||
onKeyPress func(term.Key) bool
|
||||
onSelectCell func(int, int)
|
||||
onBeforeDraw func(int, int, int, int)
|
||||
|
||||
// internal variable to avoid sending onSelectCell twice or more
|
||||
// in case of current cell is unchanged
|
||||
@ -81,7 +87,7 @@ type Column struct {
|
||||
// will be empty. In addition to it, the callback can
|
||||
// change Bg, Fg, and Alignment to display customizes
|
||||
// info. All other non-mentioned fields are for a user
|
||||
// convinience and used to describe the cell more detailed,
|
||||
// convenience and used to describe the cell more detailed,
|
||||
// changing that fields affects nothing
|
||||
type ColumnDrawInfo struct {
|
||||
// row number
|
||||
@ -121,7 +127,7 @@ type TableEvent struct {
|
||||
NewTableView creates a new frame.
|
||||
view - is a View that manages the control
|
||||
parent - is container that keeps the control. The same View can be a view and a parent at the same time.
|
||||
width and heigth - are minimal size of the control.
|
||||
width and height - are minimal size of the control.
|
||||
scale - the way of scaling the control when the parent is resized. Use DoNotScale constant if the
|
||||
control should keep its original size.
|
||||
*/
|
||||
@ -361,6 +367,11 @@ func (l *TableView) Draw() {
|
||||
x, y := l.Pos()
|
||||
w, h := l.Size()
|
||||
|
||||
if l.onBeforeDraw != nil {
|
||||
firstCol, firstRow, colCount, rowCount := l.VisibleArea()
|
||||
l.onBeforeDraw(firstCol, firstRow, colCount, rowCount)
|
||||
}
|
||||
|
||||
bg := RealColor(l.bg, l.Style(), ColorTableBack)
|
||||
SetBackColor(bg)
|
||||
FillRect(x, y+2, w, h-2, ' ')
|
||||
@ -524,7 +535,7 @@ func (l *TableView) isColVisible(idx int) bool {
|
||||
}
|
||||
|
||||
// EnsureColVisible scrolls the table horizontally
|
||||
// to make the curently selected column fully visible
|
||||
// to make the currently selected column fully visible
|
||||
func (l *TableView) EnsureColVisible() {
|
||||
if l.isColVisible(l.selectedCol) {
|
||||
return
|
||||
@ -567,7 +578,7 @@ func (l *TableView) EnsureColVisible() {
|
||||
}
|
||||
|
||||
// EnsureRowVisible scrolls the table vertically
|
||||
// to make the curently selected row visible
|
||||
// to make the currently selected row visible
|
||||
func (l *TableView) EnsureRowVisible() {
|
||||
length := l.rowCount
|
||||
|
||||
@ -935,9 +946,8 @@ func (l *TableView) OnKeyPress(fn func(term.Key) bool) {
|
||||
// a cell
|
||||
func (l *TableView) OnDrawCell(fn func(*ColumnDrawInfo)) {
|
||||
l.mtx.Lock()
|
||||
defer l.mtx.Unlock()
|
||||
|
||||
l.onDrawCell = fn
|
||||
l.mtx.Unlock()
|
||||
}
|
||||
|
||||
// OnAction is called when the table wants a user application to
|
||||
@ -993,3 +1003,53 @@ func (l *TableView) SetSelectedCol(col int) {
|
||||
l.emitSelectionChange()
|
||||
}
|
||||
}
|
||||
|
||||
// OnBeforeDraw is called when TableView is going to draw its cells.
|
||||
// Can be used to precache the data, and make OnDrawCell faster.
|
||||
// Callback receives 4 arguments: first visible column, first visible row,
|
||||
// the number of visible columns, the number of visible rows
|
||||
func (l *TableView) OnBeforeDraw(fn func(int, int, int, int)) {
|
||||
l.mtx.Lock()
|
||||
l.onBeforeDraw = fn
|
||||
l.mtx.Unlock()
|
||||
}
|
||||
|
||||
// VisibleArea returns which rows and columns are currently visible. It can be
|
||||
// used instead of OnBeforeDraw event to prepare the data for drawing without
|
||||
// waiting until TableView starts drawing itself.
|
||||
// It can be useful in case of you update your database, so at the same moment
|
||||
// you can request the visible area and update database cache - it can improve
|
||||
// performance.
|
||||
// Returns:
|
||||
// * firstCol - first visible column
|
||||
// * firstRow - first visible row
|
||||
// * colCount - the number of visible columns
|
||||
// * rowCount - the number of visible rows
|
||||
func (l *TableView) VisibleArea() (firstCol, firstRow, colCount, rowCount int) {
|
||||
firstRow = l.topRow
|
||||
maxDy := l.height - 3
|
||||
if firstRow+maxDy < l.rowCount {
|
||||
rowCount = maxDy
|
||||
} else {
|
||||
rowCount = l.rowCount - l.topRow
|
||||
}
|
||||
|
||||
total := l.width - 1
|
||||
if l.showRowNo {
|
||||
total -= l.counterWidth()
|
||||
if l.showVLines {
|
||||
total--
|
||||
}
|
||||
}
|
||||
|
||||
colNo := l.topCol
|
||||
colCount = 0
|
||||
for colNo < len(l.columns) && total > 0 {
|
||||
w := l.columns[colNo].Width
|
||||
total -= w
|
||||
colNo++
|
||||
colCount++
|
||||
}
|
||||
|
||||
return l.topCol, l.topRow, colCount, rowCount
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user