//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 19 Jun 08 Brian Frank Creation
//
using gfx
**
** Table displays grid of rows and columns.
**
@Js
@Serializable
class Table : Widget
{
**
** Default constructor.
**
new make(|This|? f := null)
{
if (f != null) f(this)
}
**
** Callback when row is double clicked or Return/Enter
** key is pressed.
**
** Event id fired:
** - `EventId.action`
**
** Event fields:
** - `Event.index`: the row index.
**
once EventListeners onAction() { EventListeners() }
**
** Callback when selected rows change.
**
** Event id fired:
** - `EventId.select`
**
** Event fields:
** - `Event.index`: the primary selection row index.
**
once EventListeners onSelect() { EventListeners() }
**
** Callback when user invokes a right click popup action.
** If the callback wishes to display a popup, then set
** the `Event.popup` field with menu to open. If multiple
** callbacks are installed, the first one to return a nonnull
** popup consumes the event.
**
** Event id fired:
** - `EventId.popup`
**
** Event fields:
** - `Event.index`: the row index, or 'null' if this is a
** background popup.
** - `Event.pos`: the mouse position of the popup.
**
once EventListeners onPopup() { EventListeners() }
**
** Horizontal scroll bar.
**
@Transient ScrollBar hbar := ScrollBar.makeNative(Orientation.horizontal) { private set }
**
** Vertical scroll bar.
**
@Transient ScrollBar vbar := ScrollBar.makeNative(Orientation.vertical) { private set }
**
** Draw a border around the widget. Default is true. This
** field cannot be changed once the widget is constructed.
**
const Bool border := true
**
** True to enable multi-row selection, false for single
** row selection. Default is false. This field cannot
** be changed once the widget is constructed.
**
const Bool multi := false
**
** Backing data model of table.
**
TableModel model := TableModel()
**
** Is the header visible. Defaults to true.
**
native Bool headerVisible
**
** Update the rows at the selected indices
**
native Void refreshRows(Int[] indices)
**
** Update the entire table's contents from the model.
**
native Void refreshAll()
**
** Get and set the selected row indices. The indices match
** the logical model row indices which may not be consistent
** with the current view (if the user has resorted rows). No
** guarantee is made that indices are in sorted order.
**
native Int[] selected
**
** Get the zero based row index at the specified coordinate
** relative to this widget or null if not over a valid cell.
**
native Int? rowAt(Point pos)
**
** Get the zero based column index at the specified coordinate
** relative to this widget or null if not over a valid cell.
**
native Int? colAt(Point pos)
**
** Return if the given column is visible. All columns are
** visible by default and can be toggled via `setColVisible`.
**
Bool isColVisible(Int col) { view.isColVisible(col) }
**
** Show or hide the given column. Changing visibility of columns
** does not modify the indexing of TableModel, it only changes how
** the model is viewed. See `isColVisible`. This method does not
** automatically refresh table, call `refreshAll` when complete.
**
Void setColVisible(Int col, Bool visible) { view.setColVisible(col, visible) }
**
** The column index by which the table is currently sorted, or null
** if the table is not currently sorted by a column. See `sort`.
**
Int? sortCol() { view.sortCol }
**
** Return if the table is currently sorting up or down. See `sort`.
**
SortMode sortMode() { view.sortMode }
**
** Sort a table by the given column index. If col is null, then
** the table is ordered by its natural order of the table model.
** Sort order is determined by `TableModel.sortCompare`. Sorting
** does not modify the indexing of TableModel, it only changes how
** the model is viewed. Also see `sortCol` and `sortMode`. This
** method automatically refreshes the table.
**
native Void sort(Int? col, SortMode mode := SortMode.up)
**
** The view wraps the table model to implement the row/col mapping
** from the view coordinate space to the model coordinate space based
** on column visibility and row sort order.
**
@NoDoc TableView view := TableView(this) { get { &view.sync } private set }
}
**************************************************************************
** TableModel
**************************************************************************
**
** TableModel models the data of a table widget.
**
@Js
class TableModel
{
**
** Get number of rows in table.
**
virtual Int numRows() { 0 }
**
** Get number of columns in table. Default returns 1.
**
virtual Int numCols() { 1 }
**
** Get the header text for specified column.
**
virtual Str header(Int col) { "Header $col" }
**
** Get the horizontal alignment for specified column.
** Default is left.
**
virtual Halign halign(Int col) { Halign.left }
**
** Return the preferred width in pixels for this column.
** Return null (the default) to use the Tables default
** width.
**
virtual Int? prefWidth(Int col) { null }
**
** Get the text to display for specified cell.
**
virtual Str text(Int col, Int row) { "$col:$row" }
**
** Get the image to display for specified cell or null.
**
virtual Image? image(Int col, Int row) { null }
**
** Get the font used to render the text for this cell.
** If null, use the default system font.
**
virtual Font? font(Int col, Int row) { null }
**
** Get the foreground color for this cell. If null, use
** the default foreground color.
**
virtual Color? fg(Int col, Int row) { null }
**
** Get the background color for this cell. If null, use
** the default background color.
**
virtual Color? bg(Int col, Int row) { null }
**
** Compare two cells when sorting the given col. Return -1,
** 0, or 1 according to the same semanatics as `sys::Obj.compare`.
** Default behavior sorts `text` using `sys::Str.localeCompare`.
** See `fwt::Table.sort`.
**
virtual Int sortCompare(Int col, Int row1, Int row2)
{
text(col, row1).localeCompare(text(col, row2))
}
}
**************************************************************************
** TableView
**************************************************************************
**
** TableView wraps the table model to implement the row/col mapping
** from the view coordinate space to the model coordinate space based
** on column visibility and row sort order.
**
@NoDoc
@Js
class TableView : TableModel
{
new make(Table table) { this.table = table }
//////////////////////////////////////////////////////////////////////////
// TableModel overrides
//////////////////////////////////////////////////////////////////////////
override Int numRows() { rows.size }
override Int numCols() { cols.size }
override Str header(Int col) { table.model.header(cols[col]) }
override Halign halign(Int col) { table.model.halign(cols[col]) }
override Int? prefWidth(Int col) { table.model.prefWidth(cols[col]) }
override Str text(Int col, Int row) { table.model.text(cols[col], rows[row]) }
override Image? image(Int col, Int row) { table.model.image(cols[col], rows[row]) }
override Font? font(Int col, Int row) { table.model.font(cols[col], rows[row]) }
override Color? fg(Int col, Int row) { table.model.fg(cols[col], rows[row]) }
override Color? bg(Int col, Int row) { table.model.bg(cols[col], rows[row]) }
//////////////////////////////////////////////////////////////////////////
// View Methods
//////////////////////////////////////////////////////////////////////////
Bool isColVisible(Int col) { vis[col] }
This setColVisible(Int col, Bool visible)
{
// if not changing anything then short circuit
if (vis[col] == visible) return this
// update column mappings
vis[col] = visible
cols.clear
vis.each |v, i| { if (v) cols.add(i) }
return this
}
Void sort(Int? col, SortMode mode := SortMode.up)
{
model := table.model
sortCol = col
sortMode = mode
if (col == null)
{
rows.each |val, i| { rows[i] = i }
}
else
{
if (mode === SortMode.up)
rows.sort |a, b| { model.sortCompare(col, a, b) }
else
rows.sortr |a, b| { model.sortCompare(col, a, b) }
}
}
This sync()
{
model := table.model
if (rows.size != model.numRows) syncRows
if (vis.size != model.numCols) syncCols
return this
}
private Void syncRows()
{
// rebuild from scratch using base model order
model := table.model
rows.clear
rows.capacity = model.numRows
model.numRows.times |i| { rows.add(i) }
// only keep selection of rows that still exist
table.selected = table.selected.findAll |Int selIdx->Bool| { selIdx < model.numRows }
// if sort was in-place, then resort
if (sortCol != null && sortCol < model.numCols) sort(sortCol, sortMode)
}
private Void syncCols()
{
// rebuild from scratch
model := table.model
cols.clear; cols.capacity = model.numCols
vis.clear; vis.capacity = model.numCols
model.numCols.times |i| { cols.add(i); vis.add(true) }
}
// View -> Model
Int rowViewToModel(Int i) { rows[i] }
Int colViewToModel(Int i) { cols[i] }
Int[] rowsViewToModel(Int[] i) { i.map |x->Int| { rows[x] } }
Int[] colsViewToModel(Int[] i) { i.map |x->Int| { cols[x] } }
// Model -> View (need to optimize linear scan)
Int rowModelToView(Int i) { rows.findIndex |x| { x == i } }
Int colModelToView(Int i) { cols.findIndex |x| { x == i } }
Int[] rowsModelToView(Int[] i) { i.map |x->Int| { rowModelToView(x) } }
Int[] colsModelToView(Int[] i) { i.map |x->Int| { colModelToView(x) } }
private Table table
private Int[] rows := [,] // view to base row index mapping
private Int[] cols := [,] // view to base col index mapping
private Bool[] vis := [,] // visible
internal Int? sortCol { private set } // model based index
internal SortMode sortMode := SortMode.up { private set }
}