VID Extension Kit - Lists
Lists are a big topic, and deserve their own document. The VID Extension Kit offers several building blocks for lists.
All tests for lists are available in the tests/list/ directory in this distribution.
The list styles are built from several basic components as compound styles. Some are not obvious list styles, but are based on them, like the CHOICE style.
Common of all of these lists are that they are based on iterated faces. This means that one face is used to generate the whole list, usually rendering from top to bottom in one operation. It also means that the rows must be of equal height.
|LIST||This is the basic list style, which essentially is a list of iterated faces with predefined access possibilities for changing rendering, column/row layout, row manipulation, keyboard navigation and scrolling accessor.|
|CARET-LIST||Derivative of LIST, which uses an alternative keyboard selection scheme.|
|SCROLLER||Depending on the style, it may require a scroller to scroll the list horizontally or vertically.|
|LIST-CELL||A text cell optimized for lists. If you want clickable rows and custom render functions in the list, this or one of its derivatives mentioned below should be used.|
|LIST-TEXT-CELL||A derivative of LIST-CELL that is optimized for plain text.|
|LIST-IMAGE-CELL||A derivative of LIST-CELL for images.|
|SORT-BUTTON||This is the standard sort button, which is used by DATA-LIST and is automatically placed in the header face. It is derived from BUTTON. Clicking this will begin sorting for the column below it. Any other sorted column will be unsorted.|
|SORT-RESET-BUTTON||This is the standard sort reset button, which presents a diamond shaped icon in the top right hand corner of the list. Clicking this button resets to use no sorting.|
In the future, there will be more styles to provide direct manipulation of rows, such as buttons and inline edit fields.
|NAV-LIST||This is a PANEL that holds the basic words for the elements that a data-list consists of.|
|DATA-LIST||This uses a LIST, a vertical SCROLLER and a PANEL with SORT-BUTTON and SORT-RESET-BUTTON in it to form a regular list. It is derived from NAV-LIST.|
|PARAMETER-LIST||This is derives from DATA-LIST and shows a predefined setup for displaying objects in parameter or key/value form easily.|
|TEXT-LIST||This is a simple 1-dimensional DATA-LIST set up for MUTEX selection mode and has a scroller, but no header. It can be used for very quick display of strings or numbers in a block.|
|CHOICE||This uses a CARET-LIST to show the options list.|
There are multiple methods for specifying the structure and content of a list. Generally there are two ways:
- By specifying an object to SETUP, which gives you minimum flexibility, but minimum code.
- A Specification dialect for SETUP, if you want to specify with greater flexibility.
Specifying a list works the same across DATA-LIST and PARAMETER-LIST. If you want to work with the LIST style, no SETUP is available and you must use the face words directly.
You can also specify a data source. It can be the list itself, in which case the data is stored at FACE/DATA, or you can assign one like so:
And then in layout:
data-list data data-block
DATA-BLOCK is not copied.
When using SETUP, you can either use an object or a dialect block (the specification dialect) to specify the structure of the list. Every time SETUP-FACE is called, the list face is entirely re-done.
The object can typically be the same as the object used to specify each row in the list.
my-list-row: make object! [ name: age: weight: none ]
You can then pass the object to the data list like this:
data-list setup my-list-row
The column display order will match that of the object.
The dialect is formed as a word and a value as argument, usually a block or a word. Each word will typically describe what occurs in all columns for a particular facet of the list, such as the column names or the widths.
The words are:
|input||This is a block of words, describing the columns as structured for the raw data. If this value is not stated, but output is, then INPUT will be the same as OUTPUT.|
|output||This is a block of words, describing the columns as structured in the output display. If this value is not stated, but input is, then INPUT will be used as OUTPUT. If used with SUB-FACE, the output is mapped to each face in the order they appear in the SUB-FACE. Other keywords, like | (pipe char) can also be used to describe a 1 pixel gray spacer between columns.|
|names||This is a block of strings, which describe the shown names in the header of the list. It must have the same order and length as OUTPUT. If this value is not stated, the words will be FORMed from OUTPUT, uppercasing the first character in the word. It is overridden, when using HEADER-FACE or SUB-FACE directly.|
|resize-column||This is a single word which describes which column will be resizable. It is overridden, when using HEADER-FACE or SUB-FACE directly.|
|widths||This is a block of integers describing the width of each column in pixels. The resizable column ignores this value. Note also that if you specify a table width that is much narrower than the combined width of the header, some columns will be perfectly hidden. It is overridden, when using HEADER-FACE or SUB-FACE directly.|
|modes||This is a block of words describing the mode of the button that sits in the header face above the column. SORT will use plain ascending/descending sorting. NO-SORT will create an inert sort button, that will do nothing when clicked. Default for all columns is SORT.|
|types||This is a block of words or datatypes describing the datatype for the column. This is only used to determine which style will be used to render the column. For now string! is supported. Later image! will be used to directly render images that exist in the raw data.|
|adjust||Whether the header and content are LEFT, CENTER or RIGHT adjusted.|
|select-mode||This is either MUTEX, which allows only selecting one row, PERSISTENT, which allows selecting multiple rows without using a qualifier key or MULTI (default), which allows selecting multiple rows using CTRL or SHIFT as qualifier keys. This option is not reset when SETUP-FACE is performed several times.|
|header-face||This is a layout block. When used, it will replace the standard header generated by DATA-LIST. You can use any standard layout|
|sub-face||This is a layout block for each row used in the layout. When using this, you can create an entirely custom layout, even with multiple lines per data row. Words in OUTPUT are distributed in the same order that the faces are described in the layout. Note that the layout cannot change height, so you cannot use variable height layouts in these types of lists.|
|render||This is a function body that uses FACE and CELL as argument. The FACE is the list face inside the data list. The CELL is the cell that is currently being rendered.|
|follow-size||Whether to follow scrolling of a selected row line-by-line or page-by-page. The options are LINE (default) or PAGE. This option is not reset when SETUP-FACE is performed several times.|
You can specify input:
data-list setup [input [name age weight]]
You can specify output, which is useful, if you want to hide or rearrange columns:
data-list setup [input [name age weight] output [weight age]]
In OUTPUT, you can add spacers as a | (pipe char), which will produce a one pixel space between columns:
data-list setup [input [name age weight] output [weight | age]]
You can add specific titles. In cases like these, if you don't need column rearrangement or need to hide columns, you don't have to add the OUTPUT keyword, as INPUT is automatically used as OUTPUT, if nothing else is stated:
data-list setup [input [name age weight] names ["Name" Age [years]" "Weight [kg]"]]
You can specify the width of each column in pixels with the condition that the first column is resizable and therefore is ignored:
data-list setup [input [name age weight] widths [100 150 50]]
You can specify which column is resizable, when the list is resized when resizing the window in which it sits. The condition is that only one column is resizable, as the list resizing model follows that of the VID Extension Kit.
data-list setup [input [name age weight] widths [100 150 50] resize-column 'age]
If you use a plain word for age as input for resize-column, the style will try to see if the word 'age is set with a value. If not, an error is created. This counts for any keyword that requires a single value as argument.
Set a selection mode, where only one row can be selected at a time:
data-list setup [input [name age weight] select-mode 'mutex]
Set follow size to a full page, to jump one whole page in the list, when navigating with cursor keys:
data-list setup [follow-size 'page]
Perform adjustment of the text in a column:
data-list setup [input [name age weight] adjust [left center right]]
Set a column mode for a column, so that sorting is disabled for this column:
data-list setup [input [name age weight] modes [sort no-sort]]
See the file tests/lists/spec-dialect.r for an example.
You can also use words, as long as they are set and for objects, their words are used. This allows the use of object prototypes:
my-obj: make object! [a: b: c: none]
And in the layout:
data-list setup [input my-obj]
See the file tests/lists/spec-dialect-2.r for an example of this.
The contents of a list is usually one or several columns and rows, where each column has a title. DATA-LIST is designed to accept an input directly to display contents, so you don't have to spend time generating a specification of the content.
There are generally 3 types of data content allowed:
A simple 1-dimensional block of elements that are not objects are allowed, such as:
[1 2 3 4]
The list will automatically detect that there is only one column, if the first element is not a block.
You cannot use EDIT-FACE to edit this kind of block. For this, you will need to provide your own custom functions.
2-dimensional blocks are allowed and may contain various types, including objects:
[[1 2] [3 4] [5 6] [7 8]]
The list will automatically figure out the number of columns from the length of the first block.
You cannot use EDIT-FACE to edit this kind of block. For this, you will need to provide your own custom functions.
This is the recommended method for maximum flexibility as the same object can be used both for specifying the list structure and for specifying rows in the list. You can also use EDIT-FACE to edit the list contents this way. Each word in the object is designated as a column and will be used as the column name. So if you have a prototype object:
my-list-row: make object! [ name: age: weight: none ]
The columns shown in the list will be:
Name Age Weight
A list with a header can be sorted by the user, if the header face is specified. The header face usually contains SORT-BUTTONS that handle sorting for you.
See the file tests/lists/sorting.r for an example.
Internally, the sorting mechanism uses SORT/COMPARE and is thus susceptible to the limitations it poses, such as trying to compare two different datatypes. Therefore it's usually not wise to use more than one datatype per column, if you want to sort rows.
You cannot sort multiple columns at the same time. If you select a new column to sort, the original data is resorted by that column.
You cannot yet programmatically sort.
The QUERY-FACE accessor can filter rows, based on an input function. The input function takes a row as an input argument and when returning TRUE, the row will be included in the list. If FALSE, it will be left out.
This is useful for filtering out data that you don't want to see, such as with a real-time string filtering function.
See the file tests/lists/filtering.r for an example.
Example filtering function, which leaves out a line, if the string in F-FILTER cannot be found in any cell:
filter-row: func [row /local value] [ if empty? value: get-face f-filter [return true] foreach cell row [if find cell value [return true]] false ]
And in the list specification the QUERY-FACE accessor uses the filter function to filter in the list per key-press:
f-filter: field on-key [query-face l-data :filter-row] l-data: data-list data list-data
When the list uses a RENDER-FUNC function, each cell can be evaluated for content and adjusted in appearance accordingly. Any facet of the face can be adjusted. This is used for alternate line coloring in the standard DATA-LIST.
But by setting any facet, including DRAW blocks, you can include images or complex indicators in a cell. The RENDER-FUNC function is run, every time a single cell is rendered, so speed is of importance.
See the file tests/lists/rendering.r for an example.
Each cell contains several bits of information to help you determine rendering:
|POS||This is the physical position of the data in the output. If you are rendering the fourth cell in the third row, the value will be 3x4. Using POS/X, you can determine which column you are in. Using POS/Y, you can determine which row you are in, in the data, if you use it to pick the raw value from FACE/DATA-SORTED.|
|DATA||This is the raw data input to the cell. Even if there are no more rows with data to render, such as in a table with fewer rows of data than visible rows in the list, the function will still be called, and in that case, the value is NONE.|
A basic example here uses this method to use bold text for a column with numbers between 0 and 100 that go over the value 90:
data-list with [ render-func: func [face cell] [ cell/font/style: if all [cell/data cell/data > 90] ['bold] ] ]
Notice here that a check of CELL/DATA is done to ensure that the check can only be done on rows that contain output.
Another handy use of the render function is to assign a random color to a cell. Since lists are iterated faces, the entire list is always generated with all faces in one single draw operation, when calling SHOW.
If you are experiencing slowdowns, due to excessive redraws of the list, you can apply a random background color per cell to detect such excessive redraws.
data-list with [ render-func: func [face cell] [ cell/color: random 255.255.255 ] ]
Internally, the LIST does not destroy or modify the original data, when sorting or filtering. It only does so, when using the editing functions with EDIT-FACE.
What occurs on every update, is:
- The original data is fed to the list
- The data is passed through the filter function. If none is specified, all rows pass through and an index is generated for the rows that are allowed to pass through.
- Then the indexes of those filtered data passes through the sorting function and the indexes are sorted appropriately to the currently set column for sorting and the direction of sorting.
- Then, the sorted indexes are used to generate the order of output of the data in the original input.
- When showing the list, this output is then used to place the correct values in the correct cells. Also the output block is moved to the current scroll position.
- And finally during this operation, the render function is called to make any modifications necessary to the cell, before it's displayed.
- Finally the face is shown and all cells are displayed.
The selected rows can be accessed through FACE/SELECTED and will be the indexes of the items in the original unsorted and unfiltered data block.
To get the content itself, use the GET-FACE function.
To select rows, use the SELECT-FACE function, which can take a number of different inputs.
Selection can take place, both on filtered and sorted data as well as the unfiltered and unsorted data. The division works like this:
Unfiltered and unsorted data happen with:
- Numeric selection, blocks of numbers.
Filtered and sorted selection happens with:
- Word based selection (FIRST, PREVIOUS, NEXT, LAST) * TRUE/FALSE/NONE to select all or no rows.
Selecting the sixth row; numeric selection of rows always occur on the unsorted and unfiltered data. When sorting, it will then not be the sixth row displayed, and if the row is filtered, the row is not displayed:
select-face my-list 6
Selecting a number of rows:
select-face my-list [3 4 5]
Selecting rows by function; the function must return TRUE for the row to be selected. The function input value is a row, which then usually is a block or an object:
select-face my-list func [row] [row/height > 90]
Select first filtered and sorted row:
select-face my-list 'first
Select last filtered and sorted row:
select-face my-list 'last
Select next filtered and sorted row:
select-face my-list 'next
Select previous filtered and sorted row:
select-face my-list 'previous
select-face my-list none
select-face my-list false
select-face my-list true
See the file tests/lists/selection.r for examples of selection using SELECT-FACE.
When setting the selection mode in the list, you can determine the behavior of selection:
|MUTEX||This selects only one row at a time, regardless of whether holding down qualifier keys or not. When using MUTEX, using GET-FACE will return the selected item directly, not wrapped in a block.|
|MULTI||When clicking a row, it will be highlighted. When clicking other rows using SHIFT or CONTROL keys, those rows will be highlighted, either in sequence (SHIFT) or appended to the current selection (CTRL).|
|PERSISTENT||When clicking a row it will be highlighted. When clicking another row without holding down any qualifier keys, that row will also be highlighted. Clicking a row again will remove the highlight.|
When using MULTI or PERSISTENT, selecting multiple rows will be returned in a block in the order they are selected, when using GET-FACE on the list.
Using the specification dialect, you can change the selection mode:
my-list: data-list setup [select-mode 'mutex]
When using a DATA-LIST, it is registered as a compound style and internal components cannot be navigated to directly. To use keyboard navigation, the list must be focused, i.e. be covered with the focus ring.
Any usage of the keyboard results in FACE/SELECTED being manipulated and the ON-KEY actor being run.
The LIST style offers a KEY-FACE accessor function with the following functionality:
|up||Selects the previous row. If no items are selected, the first row is selected. If the previous row is out of view, the list scrolls it into view. The use of the FOLLOW-SIZE keyword during setup sets whether to jump by one line or a whole page, when the list scrolls the selected row into view.|
|down||Selects the next row. If no items are selected, the first row is selected. If the next row is out of view, the list scrolls it into view.|
|ctrl-up||Selects a row one page up and deselects the previously selected row. One page is the number of visible rows in the list.|
|ctrl-down||Selects a row one page down and deselects the previously selected row.|
|shift-up||Selects the previous row and does not deselect any previous rows.|
|shift-down||Selects the next row and does not deselect any previous rows.|
|ctrl-shift-up||Selects the entire previous page and does not deselect any previous rows.|
|ctrl-shift-down||Selects the entire next page and does not deselect any previous rows.|
|ctrl-A||Selects all rows.|
|ctrl-shift-A||Deselects all rows.|
See the file tests/lists/selection.r for an example use of ON-KEY on a DATA-LIST.
It is currently not possible to sort by keyboard.
When scrolling, you can use the scroll-wheel on any area inside the list, including the header. When the data-list is resized, the scroller is redragged.
The internal function FOLLOW, which exists inside the list face, allows the list to scroll to a specific physical row. This is used, when needing to scroll a particular row into view, and thus is used during keyboard navigation, to automatically scroll the list, whenever the selected row moves beyond the visible part of the list.
FOLLOW can use two modes; LINE, which is default and PAGE. This can be set during setup, using the FOLLOW-SIZE keyword.
|line||will simply scroll the chosen row into view near the top or bottom edge of the list, whichever is nearest.|
|page||will scroll an entire page size, every time the face is moved out of view. This results in page-by-page style scrolling, when moving by keyboard.|
See the tests/lists/follow-size.r example.
l-data setup [follow-size 'page]
There is currently no horizontal scroller.
Using the EDIT-FACE accessor, you can do some editing of the data block in the list. If the data block is not copied, it can be used to modify an external block.
For simplicity, editing is only available, when the data is formed as a block of objects.
The EDIT-FACE accessor takes three arguments:
|FACE||The face to edit|
|OP||The operation to carry out, in this case ADD, EDIT or DELETE as a word|
|VALUE||The value to pass to the face accessor for the operation.|
Edits are carried out on the selected rows in the list.
Given a data-list L-DATA, we can add a row like this:
edit-face l-data 'add
Adding always happen at the end of the list.
Edit a row:
edit-face l-data 'edit
Delete a row:
edit-face l-data 'delete
- Interactive inline editing is not yet possible. This will be possible, once a text-cell face for this type of editing is made available.
- Single-cell editing using EDIT-FACE is not yet possible.