Grid is a new widget for the classic VW GUI that combines elements of the Table and Dataset widgets for a simpler and more flexible interface. It is based on the Grid from the Widgetry project.
It consists of three packages:
Grid Features | Current and projected features. |
Grid Elements | Grid support classes. |
Grid Column Models | How to setup a column model. |
Grid UIPainter Interface | Creating and editing Grid attributes in a canvas. |
Grid Navigation | Scrolling and keyboard navigation to a new destination. |
Grid Selection | Selecting rows or cells in a Grid. |
Grid Sorting | Row sort with and without a UI. |
Grid Drag and Drop | Reorder columns or move and insert rows |
Grid Interface | Application layer interface object that simplifies Grid setup and use. |
Grid Tutorial | Simple example Grid creation and manipulation. |
New for a Grid are classes defined to represent and take the roles of its rows, columns, and cells. This improves the accessibility and control of a Grid by one or more of its elements. The DataSetView and TableView were sorely lacking in this encapsulated behavior.
The Grid itself is a CompositeView that contains only the elements visible in its pane. Its components are dynamically added and removed as the bounds of the Grid pane changes or is scrolled. As the counterparts of a Grid's persistent elements these visual elements are built and have a lifetime only while elements are visible in the pane. These classes are private for use within the Grid.
GridVisualSegment
GridVisualRow
GridVisualColumn
A GridVisualSegment is the abstract superclass of a GridVisualColumn and a GridVisualRow. It knows of and can access its adjacent visible neighbor in a linked list. If a GridVisualSegment has no predecessor or successor then it resides at the visible or scrollable border of a Grid pane. If the segment is a GridVisualRow it knows its top and bottom position in the Grid, its GridRow, and its height. If the segment is a GridVisualColumn it knows its left and right Grid position, its GridColumn, and its width.
GridVisualCell -- Visual counterpart of GridCell that constructs and maintains a Grid component using the Grid's builder.
The model for a GridColumn specification defines the aspect path required to obtain the value for a cell in the column for any row. Although the model may be provided directly as an AspectAdaptor whose subject is the row item, typically it is specified as a Symbol for the aspect path that the Grid cell builder will use to create and assign as model. When specified as a Symbol, it is declared in this format
#'_row *<unary accessor>'
where '_row' is a reserved word denoting the model will be accessed from the current row item and '*<unary accessor>' is one or more unary accessor messages that form the aspect path to obtain the value for the cell from the row item.
In use, the GridColumn spec declaration might appear as
(GridColumnSpec new
label: 'First Name';
width: 80;
spec: (InputFieldSpec new model:#'_row firstName');
yourself)
Examples:
#'_row firstName' |
The value is obtained by sending #firstName to the row item. The row item must also respond to #firstName: when the cell value is set by an editor. |
#'_row address state' |
The value is obtained by sending #state and set by sending #state: to the object obtained by sending #address to the row item. |
#'_row 2 y' |
The value is obtained by sending #y to the second element of the row item. The row item is most likely a SequenceableCollection of Points and this aspect path will access or modify the 'y' value of its second element. |
If the cell model is to be buffered for changes the model may be declared in this format:
#'_row *<unary accessor> | <buffered aspect>'
where '<buffered aspect>' is the aspect for a ValueModel which when set true accepts the edited value from a buffer. If the '<buffered aspect>' model is set to false the last edited value in a buffer is canceled and the value is restored from the row item.
The Grid-UIPainter component enables the following editing tasks:
Select the Grid widget and the PainterTool should show a Basics page. Press the New Column button to add a new column. Each new column will add a child node to the Grid widget appearing in the PainterTool hierarchical tree of components in the canvas.
Either select the Grid child node appearing for the column in the PainterTool tree or <Alt>-<Select> the desired column in the Grid appearing in the UIPainter canvas. The selected column will appear highlighted and the PainterTool pages will change to display and enable editing of a column's attributes. If the content for a column has not been defined only the Basics page will appear.
A column may contain any widget but only widgets that have models are useful. Defining a column with a label, view holder, region, resizing splitter, and group box will only show a column of identical items. Likewise, a column of dataset, click widget, chart, notebook, tab widget, subcanvas, and even grid widgets are to be avoided since they unnecessarily complicate an interface. The most useful and recommended widgets for column content are input fields, text editors, spin buttons, combo boxes, check boxes, and sliders.
Select the Grid widget in the canvas then select the widget you would like to define in the UIPainter palette. Drop your selected widget onto a column by clicking on it. This defines the content for the column. If the column had no content defined for it previously then additional pages besides Basics will appear in the PainterTool to edit the attributes of the content (e.g. Component, Color, Details, etc). If the Grid widget is not first selected then dropping a widget from the palette on the Grid will simply place the widget on top the Grid.
Select a Grid column either from the PropertiesTool tree or by clicking on the column using <Alt>-<Select>. Press the Remove Column button on the Basics page the the column will be removed.
Select a Grid column in the canvas then drag and drop a column by its header to a new position among other columns.
Drag a column border with the mouse to resize it. Currently there isn't an entry field on the column Basics page to set the size to a particular width in pixels.
Row height is only defined and relevant for rows added to a Grid at runtime. One may set the default row height in the Grid Basics page--the height a row will assume if it isn't individually assigned a height.
Select the Grid and the PropertiesTool Basics page. In the Rows group box, select Add Header. This should add a leftmost row selector column to the Grid. This should also enable the Buttons and Numbered check box options to change the style of the row selector column. If you select Numbered for a numbered row selector column then a non-zero entry to Offset will offset the initial row number by the number given.
Select the Grid and the PropertiesTool Basics page. In the Columns group box, select Add Header. To edit each column name select a Grid column either from the PropertiesTool tree or by clicking on the column using <Alt>-<Select> then enter its name in the column Basics page.
Modify the following check boxes on the PainterTool Basics page to change the following column and row attributes:
Action | Keypress | Grid Message | Notes |
Scroll page up | <PageUp> | aGrid pageUp |
|
Scroll page down | <PageDown> | aGrid pageDown |
|
Scroll to top line | <Control>-<Home> | aGrid home |
|
Scroll to bottom line | <Control>-<End> | aGrid end |
|
Edit next field | <Tab> | aGrid moveEditHorizontallyBy: 1 |
aGrid horizontalPolicy ~= #none |
Edit prior field | <Shift>-<Tab> | aGrid moveEditHorizontallyBy: -1 |
aGrid horizontalPolicy ~= #none |
Edit upper field | <Up Arrow> | aGrid moveEditVerticallyBy: 1 |
aGrid verticalPolicy ~= #none |
Edit lower field | <Down Arrow> | aGrid moveEditVerticallyBy: -1 |
aGrid verticalPolicy ~= #none |
Scroll to make the row visible at the closest border | aGridRow scrollToView |
Scroll to make the row visible at the closest border | aGridRow scrollToView: #nearest. |
Scroll to make the row visible at the Grid top | aGridRow scrollToView: #top. |
Scroll to make the row visible at the Grid bottom | aGridRow scrollToView: #bottom. |
Scroll to make the row visible at the Grid center | aGridRow scrollToView: #center. |
Scroll to make the column visible at the closest border | aGridColumn scrollToView |
Scroll to make the column visible at the closest border | aGridColumn scrollToView: #nearest. |
Scroll to make the column visible at the Grid left | aGridColumn scrollToView: #left. |
Scroll to make the column visible at the Grid right | aGridColumn scrollToView: #right. |
Scroll to make the column visible at the Grid center | aGridColumn scrollToView: #center. |
Scroll to make the cell visible at any closest border | aGridCell scrollToView |
Scroll to make the cell visible at any closest border | aGridCell scrollToRowView: #nearest andColumnView: #nearest. |
Scroll to make the cell visible at the top left | aGridCell scrollToRowView: #top andColumnView: #left. |
Scroll to make the cell visible at the bottom left | aGridCell scrollToRowView: #bottom andColumnView: #right. |
Scroll to make the cell visible at the top right | aGridCell scrollToRowView: #top andColumnView: #right. |
Scroll to make the column visible at the pane center | aGridCell scrollToRowView: #center andColumnView: #center. |
Scroll to make the cell visible at the horizontal center | aGridCell scrollToRowView: #nearest andColumnView: #center. |
"Scroll the editor into view"
aGrid editCell scrollToView.
Selection | Range |
Line 1 (6 columns) | Set with: (1@1 corner: 6@1). |
Cell 2@2 | Set with: (2@2 extent: 0@0). |
Line 1 and 2 (6 columns) | Set with: (1@1 corner: 6@2). |
Cell 1@1 and 2@2 | Set with: (1@1 extent: 0@0) with: (2@2 extent: 0@0). |
Cell 1@1, 1@2, and 2@2 | Set with: (1@1 extent: 0@1) with: (2@2 extent: 0@0). |
Adjacent Cells 1@1 thru 2@2 | Set with: (1@1 extent: 1@1). |
"Single select, row selection. 7 columns"
grid multiSelect: false.
grid selectByCell: false.
Action | Code | Resulting Selection Set |
Add row 13 | grid selectRange: (1@13 extent: 0@0). |
Set with: (1@13 corner: 7@13) |
Change to row 14 | grid selectRange: (1@14 extent: 0@0). |
Set with: (1@14 corner: 7@14) |
Change to row 16 | grid selectRange: (1@16 extent: 0@0). |
Set with: (1@16 corner: 7@16) |
Change to row 15 | grid selectRange: (1@15 extent: 0@0). |
Set with: (1@15 corner: 7@15) |
Clear all selections | grid resetSelections. |
Set new |
"Multi select, row selection. 7 columns"
grid multiSelect: true.
grid selectByCell: false.
Action | Code | Resulting Selection Set |
Add row 13 | grid selectRange: (1@13 extent: 0@0). |
Set with: (1@13 corner: 7@13) |
Add row 14 | grid selectRange: (1@14 extent: 0@0). |
Set with: (1@13 corner: 7@14) |
Add row 16 | grid selectRange: (1@16 extent: 0@0). |
Set with: (1@16 corner: 7@16) with: (1@13 corner: 7@14) |
Add row 15 | grid selectRange: (1@15 extent: 0@0). |
Set with: (1@13 corner: 7@16) |
Clear all selections | grid resetSelections. |
Set new |
"Single select, cell selection"
grid multiSelect: false.
grid selectByCell: true.
Action | Code | Resulting Selection Set |
Add cell 1@13 | grid selectRange: (1@13 extent: 0@0). |
Set with: (1@13 extent: 0@0) |
Change to cell 1@14 | grid selectRange: (1@14 extent: 0@0). |
Set with: (1@14 extent: 0@0) |
Change to cell 1@16 | grid selectRange: (1@16 extent: 0@0). |
Set with: (1@16 extent: 0@0) |
Change to cell 1@15 | grid selectRange: (1@15 extent: 0@0). |
Set with: (1@15 extent: 0@0) |
Clear all selections | grid resetSelections. |
Set new |
"Multi select, cell selection"
grid multiSelect: true.
grid selectByCell: true.
Action | Code | Resulting Selection Set |
Add cell 1@13 | grid selectRange: (1@13 extent: 0@0). |
Set with: (1@13 extent: 0@0) |
Add cell 1@14 | grid selectRange: (1@14 extent: 0@0). |
Set with: (1@13 extent: 0@1) |
Add cell 1@16 | grid selectRange: (1@16 extent: 0@0). |
Set with: (1@16 extent: 0@0) with: (1@13 extent: 0@1) |
Add cell 1@15 | grid selectRange: (1@15 extent: 0@0). |
Set with: (1@13 extent: 0@3) |
Clear all selections | grid resetSelections. |
Set new |
"Setting selections then merging them. It normally isn't necessary
to explicitly request a selection range merge when ranges are added/removed
one at a time"
grid multiSelect: true.
grid selectByCell: true.
grid selections: (Set with: (1@43 extent: 0@0) with: (1@44 extent: 0@0) with:
(1@46 extent: 0@0) with:(1@45 extent: 0@0)).
grid mergeSelections.
grid selections. "Set with: (1@43 extent: 0@3)"
"Multi select, cell selection"
grid multiSelect: true.
grid selectByCell: true.
Action | Code | Resulting Selection Set |
Add cells 1@13 to 1@16 | grid selectRange: (1@13 extent: 0@3). |
Set with: (1@13 extent: 0@3) |
Remove cell 1@14 | grid selectRange: (1@14 extent: 0@0). |
Set with: (1@13 extent: 0@0) with: (1@15 extent: 0@1) |
Remove cell 1@16 | grid selectRange: (1@16 extent: 0@0). |
Set with: (1@13 extent: 0@0) with: (1@15 extent: 0@0) |
Remove cell 1@15 | grid selectRange: (1@15 extent: 0@0). |
Set with: (1@13 extent: 0@0) |
Clear all selections | grid resetSelections. |
Set new |
Do | Don't |
Use the Grid selection API methods to add or remove selection ranges.
This ensures the Grid UI will update and the selection set appears in
minimal form.grid selectRange: aRectangle |
Add or remove selection ranges directly to the Grid selection set. The
Grid will not update to show new selections, line and multiple selection
rules for Grid #selectByCell or #multiSelect: attributes will not be obeyed,
and the selection range may not be in minimal form.grid selections add: aRectangle |
"Single argument blocks are passed each whole selected row
item"
itemsSelected := OrderedCollection new.
grid selectionDo:[[:item| itemsSelected add: item].
"Dual argument blocks are passed each the selected GridRow and its item"
rowsSelected := OrderedCollection new.
itemsSelected := OrderedCollection new.
grid selectionDo:[[:row :item|
rowsSelected add: row.
itemsSelected add: item].
Grid implements #selectionCellDo: for enumerating all cell selections, including
those within a full row selection. Like #selectionDo: it accepts one or two
argument blocks. This will also be the method implemented by a future SelectionInGrid
model. If a single argument block is used with #selectionCellDo: its argument
is expected to be the value of a selected cell. When a two argument block is
used the first argument is the selected GridCell instance and the second is
its cell value.
"Single argument blocks are passed the value of selected cells"
valuesSelected := OrderedCollection new.
grid selectionCellDo:[[:value| valuesSelected add: value].
"Dual argument blocks are passed each the selected GridCell and its value"
cellsSelected := OrderedCollection new.
valuesSelected := OrderedCollection new.
grid selectionCellDo:[[:cell :value|
cellsSelected add: cell.
valuesSelected add: value].
A multiple sort attempt is unlikely to be meaningful when there are no two items that match under a primary sort. For example, attempting to sort a database of US Congressmen by house address and last name would not yield any different sort than a single sort would by house address. This presumes no two congressmen live in the same house. Presuming there are more than one Congressman with the same last name then sorting by last name and then by house address might be helpful. Several Congressmen may represent a single state and each state representative may represent a different party--Democrat, Republican, or Independent. Sorting by state and then by party is likely to be a meaningful multiple sort.
For this example you will want to use the file congress.str in Smalltalk #readFrom: format. This file provides over 400 records of congressmen information of the 2007-2008 US Congress as "screen scraped" from the US House of Representatives Office of the Clerk (http://clerk.house.gov). It provides the sample data the for Grid sort example below. All three Grid packages Grid, Grid SUnit Tests, and Grid Samples should be loaded.
"Sorting"
"Create columns and add to a Grid that will not be opened. Note the model
for each of the columns contains an AspectAdaptor"
grid := Grid new.
congress := CongressTable new readMembers.
firstName := (GridColumn spec: (ReadFieldSpec new model:#'_row firstName'))
addToGrid: grid.
lastName := (GridColumn spec: (ReadFieldSpec new model:#'_row lastName'))
addToGrid: grid.
mi := (GridColumn spec: (ReadFieldSpec new model:#'_row middle')) addToGrid:
grid.
state := (GridColumn spec: (ReadFieldSpec new model:#'_row state')) addToGrid:
grid.
district := (GridColumn spec: (ReadFieldSpec new model:#'_row district'))
addToGrid: grid.
rows := congress members.
"Sort rows by state"
state sortConstraint sortDirection: #ascending. "Set the state column to sort
in ascending order"
SequenceableCollectionSorter sort: rows using: state sortConstraint. "Sort
the congressmen instances by state, AK to WA"
"Sorting by last name"
lastName sortDirection: #descending. "Sort in descending order by last name"
SequenceableCollectionSorter sort: rows using: lastName sortConstraint. "Sort
the congressmen instances by last name"
"Multiple sort"
"Sort congressmen by state. For congressmen that appear in the same state
sort by last name"
state sortConstraint sortDirection: #ascending. "Set the state column to sort
in ascending order"
lastName sortConstraint sortDirection: #descending. "Sort in descending order
by last name"
SequenceableCollectionSorter sort: rows using: state sortConstraint & lastName
sortConstraint.
The GridHeaderRow defines a sort size (i.e. sortSize
) property
for the maximum number of columns that may be sorted at once. If this property
is set above 0 the first distinct sortSize number columns selected in a Grid
will be sorted with the first selected column as the primary sort constraint,
the second selected column as the secondary constraint, etc. When exactly
sortSize
number columns have been selected, selecting a new column
in the GridHeaderRow resets all sort constraints and the new column selected
will be the primary sort column. Select the column again to change its sort
direction. Select a new column to add a secondary sort column. Continue to
select new columns or toggle the sort direction of exising sort columns until
sortSize
number of columns have been selected again.
"Open window on sorted rows and add a column header with labels"
EmptyGridApp openWith: grid.
firstName label: 'First Name'.
lastName label: 'Last Name'.
mi label:'Middle'.
state label:'State'.
district label:'District'.
party label:'Party'.
grid columns do:[:each| each width: 60].
grid rows: (rows collect:[:each| GridRow on: each height: 20]).
headerRow := (GridHeaderRow new)
height:
25;
yourself.
grid addRow: headerRow beforeIndex: 1.
grid allowSorting: true. "Allow interactive column sorting"
headerRow sortSize: 2. "Allow sorting on two columns"
"Select the 'First Name' column header to sort congressmen rows by
first name."
"Select the 'First Name' column header to toggle sort direction."
"Select the 'State' column header for a secondary sort by state"
"Select either the 'State' or 'First Name' column header again to toggle their
sort direction"
"Select a new column. Since the header row has been set to sort only as many
as 2 columns selecting a new column clears all sort constraints and sets the
new column as the primary (and only) sort constraint"
"What columns are primary and secondary constraints and what are their
sort directions?"
headerRow constraintColumns collect:[:each| each label->each sortDirection].
"Reset sort constraints"
headerRow clearConstraints.
"Prevent columns from being reordered by drag and drop"
aGrid allowColumnReordering: false
Whether or where individual columns are selectively dragged is enforced by an
optional #allowColumnMoveFrom:toIndex: that may be implemented in the application
hosting the Grid. This message is sent to the application whenever the Grid
allows column reordering and a column header is dragged over another column
header. The arguments are the index of the column being dragged and the index
of the target column.
allowColumnMoveFrom: columnIndex toIndex: destIndex
"Allow only columns 1 and 7 to be interchanged"
^(columnIndex = 7 and: [[destIndex = 1]) or: [[columnIndex = 1 and: [[destIndex
= 7]]
Much of the GridInterface functionality requires an operating Grid instance. This usually is supplied upon building a Grid from spec.
You will also want to load the "Grid" and "Grid SUnit Tests" packages from Bear.
Create a Grid on the name, state, district, and party from a database of all 2007 US congressmen. Two classes are required: Congressman, an instance of a congressman's identity and contact information, and CongressTable, a manager object that reads and writes Congressman instances by file. A Smalltalk #readFrom: syntax file named congress.str must appear in the default file directory to read the database.
"Create a Grid and all its GridColumns. All columns are editable input
fields for the name, state, district, and party for a list ofCongressmen appearing
as rows. EmptyGridApp is a simple window that displays an empty grid that
will be customized."
congress := CongressTable new readMembers.
grid := Grid new.
app := EmptyGridApp openWith: grid.
firstName := (GridColumn new)
spec: (InputFieldSpec new model: #'_row
firstName');
width: 80;
label: 'First Name';
yourself.
grid addColumn: firstName.
lastName := (GridColumn new)
spec: (InputFieldSpec new model: #'_row
lastName');
width: 80;
label: 'Last Name';
yourself.
grid addColumn: lastName.
mi := (GridColumn new)
spec: (InputFieldSpec new model: #'_row
middle');
width: 80;
label: 'Middle';
yourself.
grid addColumn: mi.
state := (GridColumn new)
spec: (InputFieldSpec new model: #'_row
state');
width: 80;
label: 'State';
yourself.
grid addColumn: state.
district := (GridColumn new)
spec: (InputFieldSpec new model: #'_row
district');
width: 80;
label: 'District';
yourself.
grid addColumn: district.
party := (GridColumn new)
spec: (InputFieldSpec new model: #'_row
party');
width: 80;
label: 'Party';
yourself.
grid addColumn: party.
"Create GridRows on each Congressman instance and assign the list to the Grid"
rows := congress members asList collect:[:each| GridRow on: each height: 25].
grid rows: rows.
"Add a Grid column header"
grid addRow: ((GridHeaderRow new)
height: 25;
yourself)
beforeIndex: 1.
"Add a Grid row header with buttons and line numbering"
grid addColumn: ((GridHeaderColumn new)
lineNumbers: true;
buttons: true;
width: 30;
yourself)
beforeIndex: 1.
"Adjust line numbering for the column header"
grid columns first offset: -1.
grid setVisibleColumns.
"Removing or moving rows and columns"
"Swap party and district columns"
grid moveColumnIndex: 7 toIndex: 6.
"Remove the next 10 rows"
grid removeRowsFrom: 2 to: 11.
"Remove row at index 5"
grid removeRowIndex: 5.
"Examples of changing column attributes without reopening the Grid"
"Disable the party column"
party spec initiallyDisabled: true.
grid setVisibleColumns.
"Change the party column to be read-only"
party spec initiallyDisabled: false.
party spec isReadOnly: true.
grid setVisibleColumns.
"Change the party column label"
party label: 'Affiliation'.
grid setVisibleColumns.
"Change state column width"
state width: 30.
" Column attributes may be saved and restored"
"Save column attributes and widths to a column spec array to be restored later"
columnSpecs := grid columns literalArrayEncoding.
"Restore columns from specs"
grid columns: (columnSpecs asList collect:[:each| each decodeAsLiteralArray]).
Load Grid-UIPainter and open a new canvas. The UIPainter palette should include a new selection for a Grid widget. Drop and layout a Grid in the canvas window.
Next define 6 columns by pressing the New Column button on the Grid Basics page 6 times. Add an input field as content for each column. To do this first select the Grid, then select the input field widget from the palette and drop one on each column.
Select each column in the Grid then enter the label and component model according to the table below
Column number | Basics-Grid Column Label String | Component-Aspect | |
1 | First Name | _row firstName | |
2 | Last Name | _row lastName | |
3 | Middle | _row middle | |
4 | State | _row state | |
5 | District | _row district | |
6 | Party | _row party |
Leave the ID of the Grid widget as the default: #Grid1.
Install the canvas to a new ApplicationModel class named MyCongressGrid or similar. You will need to load the package/parcel "Grid Samples" to obtain data to fill this example. Define the #postBuildWith: for this application class as
postBuildWith: aUIBuilder
| grid rows |
super postBuildWith: aUIBuilder.
grid := self widgetAt: #Grid1.
rows := CongressTable longExample members
asList
collect: [:each | GridRow
on: each height: 25].
grid rows: rows.
"Add column header"
grid addRow: ((GridHeaderRow new)
height: 25;
yourself)
beforeIndex: 1
Be sure to place the file congress.str in the image directory of your VW installation. This file will be read to fill the table. Open the sample Grid application and enjoy.
VisualWorks 7.7 | Revised September 30, 2009 |