This code creates a simple spreadsheet, using the CwMatrix widget, a contributed widget created at Cogent Real-Time Systems, Inc. The program uses a dynamic library, which requires that Gamma be called from it's own directory. For example, if Gamma is in your /usr/cogent/bin/ directory, you would start this program with the following command:
$ /user/cogent/bin/phgamma matrix.g
There are only three spreadsheet functions implemented in this example, though more could be easily added. They are:
sum (range) produces the sum of a given range. The range is of the form A4:B7, and can be entered by typing =sum( followed by highlighting the range, followed by typing ) into a spreadsheet cell, or by just typing in the range.
Example: =sum(A4:B7)
db (name) reflects the value of a Cascade DataHub point. This allows the example to display data from any live process that has a driver to the Cascade DataHub.
Example: =db(valve_1)
dbout (name,range) writes a value to the Cascade DataHub. The name is the Cascade DataHub point name, and the range indicates the cell whose value will be transmitted, which must be entered as a range from the cell to itself. This command will transmit its value whenever the sheet is recomputed.
Example: =dbout(valve_1_alarm, B5:B5)
The code starts here:
#!/usr/cogent/bin/phgamma
/*
* This is a simple spreadsheet application written for QNX Photon(TM) with
* the Gamma (TM) programming language. It is not intended to be either
* complete or correct, but merely to act as a demonstration of how to use
* the CwMatrix widget.
*
* This code is provided free of charge or license by Cogent Real-Time
* Systems Inc. You are hereby given permission to use and modify this
* code without restriction of any kind.
*/
/*
* If we are running a Gamma executable that does not have the CwMatrix
* widget defined, load the dynamic object libraries necessary to support
* it.
if (undefined_p (CwMatrix))
{
if (!dyna_add_lib ("/usr/cogent/lib/photon_s.dlb"))
{
princ ("Could not load dynamic library 'photon_s.dlb'\n");
exit_program (1);
}
if (!dyna_add_lib ("/usr/cogent/lib/phwidgets.dlb"))
{
princ ("Could not load dynamic library 'phwidgets.dlb'\n");
exit_program (1);
}
}
*/
/*
* Include the necessary utility libraries to enhance the Photon widget
* set. These files are found in /usr/cogent/lib
*/
require_lisp ("PhotonWidgets");
require ("PopupMenu");
/*
* Declare some global variables. The := syntax tells Gamma that only
* the first attempt to set the variable will be honored. This is useful
* if you decide to re-read the source file while the application is
* running. It also allows you to set alternate values for these
* variables through another program or from the command line prior to
* loading this file.
*/
MENUHEIGHT := 34;
YOFFSET := 20;
XOFFSET := 20;
FormulaCells := make_array(0);
/*
* Declare a sub-class of PtButton that has an extra resource called
* sequence. This sequence indicates the column or row number for which
* this button is the header. We will add DivButton widgets to dividers
* for the row/col headers.
*/
class DivButton PtButton
{
sequence;
}
/*
* Declare a callback function for PtDivider to be called when the user
* resizes a row. We only need to resize the rows on either side of the
* divider boundary, and then resize the last row in case the entire
* divider has changed size.
*/
function cbDividerSetHeights (mat)
{
local left, right, sizes = widget.divider_sizes, n = length(sizes);
left = cbinfo.divider.left;
right = cbinfo.divider.right;
mat.SetRowHeight (left.sequence, left.dim.h +
left.border_width * 2, 0);
mat.SetRowHeight (right.sequence, right.dim.h +
right.border_width * 2, 0);
mat.SetRowHeight (n - 1, sizes[n - 1].y - sizes[n - 1].x, 0);
mat.FlushDamage ();
}
/*
* Declare a callback function for PtDivider to be called when the user
* resizes a column.
*/
function cbDividerSetWidths (mat)
{
local left, right, sizes = widget.divider_sizes, n = length(sizes);
left = cbinfo.divider.left;
right = cbinfo.divider.right;
mat.SetColumnWidth (left.sequence, left.dim.w +
left.border_width * 2, 0);
mat.SetColumnWidth (right.sequence, right.dim.w +
right.border_width * 2, 0);
mat.SetColumnWidth (n - 1, sizes[n - 1].y - sizes[n - 1].x, 0);
mat.FlushDamage ();
}
/*
* Create the row label divider and buttons. This is only called at
* startup.
*/
function RowLabels (mat)
{
local i, divider, button;
divider = new (PtDivider);
divider.SetPos (0,YOFFSET + MENUHEIGHT);
divider.SetDim (XOFFSET,mat.dim.h);
divider.group_orientation = Pt_GROUP_VERTICAL;
divider.divider_flags = cons (Pt_DIVIDER_RESIZE_BOTH, nil);
PtAttachCallback (divider, Pt_CB_DIVIDER_DRAG,
`cbDividerSetHeights (@mat));
for (i=0; i<mat.matrix_rows; i++)
{
button = new (DivButton);
button.sequence = i;
button.text_string = string (i);
button.border_width = 0;
button.margin_height = 0;
button.margin_width = 0;
button.text_font = "helv10";
button.border_width = 2;
button.SetDim (XOFFSET - 2 * button.border_width,
mat.RowHeight (i) - 2 * button.border_width);
}
PtRealizeWidget (divider);
/* Since we cannot predict the size of the final button, we must
adjust the final matrix row to match the button size. */
mat.SetRowHeight (mat.matrix_rows - 1, button.dim.h +
2 * button.border_width, 1);
}
/*
* Create the column label divider and buttons. This is only called at
* startup.
*/
function ColumnLabels (mat)
{
local i, divider, button;
divider = new (PtDivider);
divider.SetPos (XOFFSET,MENUHEIGHT);
divider.SetDim (mat.dim.w,YOFFSET);
divider.group_orientation = Pt_GROUP_HORIZONTAL;
divider.divider_flags = cons (Pt_DIVIDER_RESIZE_BOTH, nil);
PtAttachCallback (divider, Pt_CB_DIVIDER_DRAG,
`cbDividerSetWidths (@mat));
for (i=0; i<mat.matrix_cols; i++)
{
button = new (DivButton);
button.sequence = i;
button.text_string = i < 26 ? char('A' + i) :
string ("A", char('A' + i % 26));
button.border_width = 0;
button.margin_height = 0;
button.margin_width = 0;
button.text_font = "helv10";
button.border_width = 2;
button.SetDim (mat.ColumnWidth (i) - 2 * button.border_width,
YOFFSET - 2 * button.border_width);
}
PtRealizeWidget (divider);
/* Since we cannot predict the size of the final button, we must
adjust the final matrix column to match the button size. */
mat.SetColumnWidth (mat.matrix_cols - 1, button.dim.w +
2 * button.border_width, 1);
}
/*
* Compute the position of a menu button. This is handy since we need
* this position in order to determine where to put a pop_up menu.
*/
function MenuButtonPos (widget)
{
local pos;
pos = PtGetAbsPosition (widget);
pos.y += widget.dim.h + widget.border_width * 2;
pos;
}
/*
* Construct the template for the "Effects" menu, accessible through the
* Effects button in the upper left corner. This template does create an
* actual PtMenu until you call its Instantiate(...) member function.
*/
function CreateEffectMenu (mat)
{
local emenu, textcolormenu, fillcolormenu;
emenu = new (PopupMenu);
emenu.AddItem ("Border On", nil, `cbBorder(@mat,t), nil, nil);
emenu.AddItem ("Border Off", nil, `cbBorder(@mat,nil), nil, nil);
emenu.AddItem ("-", nil, nil, nil, nil);
emenu.AddItem ("Justify Left", nil, `cbJustify(@mat,Cw_CELL_LEFT),
nil, nil);
emenu.AddItem ("Justify Right", nil, `cbJustify(@mat,Cw_CELL_RIGHT),
nil, nil);
emenu.AddItem ("Justify Center", nil, `cbJustify(@mat,Cw_CELL_CENTER),
nil, nil);
emenu.AddItem ("-", nil, nil, nil, nil);
emenu.AddItem ("Invert", nil, `cbInvert(@mat, 1), nil, nil);
emenu.AddItem ("Un-Invert", nil, `cbInvert(@mat, 0), nil, nil);
emenu.AddItem ("-", nil, nil, nil, nil);
fillcolormenu = new (PopupMenu);
fillcolormenu.AddItem ("red", nil,
`cbFillColor(@mat,0xff0000), nil, nil);
fillcolormenu.AddItem ("green", nil,
`cbFillColor(@mat,0x00ff00), nil, nil);
fillcolormenu.AddItem ("blue", nil,
`cbFillColor(@mat,0x0000ff), nil, nil);
fillcolormenu.AddItem ("white", nil,
`cbFillColor(@mat,0xffffff), nil, nil);
fillcolormenu.AddItem ("black", nil,
`cbFillColor(@mat,0x000000), nil, nil);
textcolormenu = new (PopupMenu);
textcolormenu.AddItem ("red", nil,
`cbTextColor(@mat,0xff0000), nil, nil);
textcolormenu.AddItem ("green", nil,
`cbTextColor(@mat,0x00ff00), nil, nil);
textcolormenu.AddItem ("blue", nil,
`cbTextColor(@mat,0x0000ff), nil, nil);
textcolormenu.AddItem ("white", nil,
`cbTextColor(@mat,0xffffff), nil, nil);
textcolormenu.AddItem ("black", nil,
`cbTextColor(@mat,0x000000), nil, nil);
emenu.AddItem ("Text Color", nil, textcolormenu, nil, nil);
emenu.AddItem ("Fill Color", nil, fillcolormenu, nil, nil);
emenu;
}
/*
* Various functions to be called when the menu buttons are selected.
*/
/* Turn on/off borders around the currently selected region */
function cbBorder (mat, on)
{
local flags = on ? Cw_CELL_ALL_BORDERS_THICK : 0;
mat.OutlineRange (mat.matrix_range, flags, Cw_CELL_ALL_BORDERS_THICK);
mat.FlushDamage();
}
/* Justify left/right/center in the currently selected region */
function cbJustify (mat, flag)
{
mat.JustifyRange (mat.matrix_range, flag);
mat.FlushDamage();
}
/* Invert text/background colors in the currently selected region */
function cbInvert (mat, yesno)
{
mat.InvertRange (mat.matrix_range, yesno);
mat.FlushDamage();
}
/* Set the background color in the currently selected region */
function cbFillColor (mat, color)
{
local row, col, range, cell;
range=mat.matrix_range;
for (row=range.ul.y; row<=range.lr.y; row++)
{
for (col=range.ul.x; col<=range.lr.x; col++)
{
if (cell = mat.Cell (row, col))
cell.fill_color = color;
}
}
}
/* Set the text color in the currently selected region */
function cbTextColor (mat, color)
{
local row, col, range, cell;
range=mat.matrix_range;
for (row=range.ul.y; row<=range.lr.y; row++)
{
for (col=range.ul.x; col<=range.lr.x; col++)
{
if (cell = mat.Cell (row, col))
cell.text_color = color;
}
}
}
/*
* This is a callback function that is called when the user begins to
* edit the contents of a cell. This gives us an opportunity to insert
* a different text string from the one shown for editing purposes. In
* this case, if the cell has a formula, edit the formula instead of the
* visible text.
*/
function cbBeginEdit ()
{
if (cbinfo.matrix.cell.formula)
cbinfo.matrix.text_string = string ("=",cbinfo.matrix.cell.formula);
}
/*
* This is a callback function that is called when the text in a cell
* has changed. We check to see whether this is a formula, and if so
* attempt to parse it and evaluate it.
* In any case, we must re-compute the contents of the sheet in case
* this edit has affected something elsewhere.
*/
function cbTextChange ()
{
local answer, str, _parser_throws_error_ = t;
str = cbinfo.matrix.text_string;
if (str[0] == '=')
{
str = substr(str,1,-1);
SetFormula (cbinfo.matrix.cell, str);
EvaluateCell (widget, cbinfo.matrix.cell.row, cbinfo.matrix.cell.col);
}
else
SetFormula (cbinfo.matrix.cell, nil);
Recompute (widget);
}
/*
* This is a bsearch comparison function that orders the cells in row/column
* order. If we wanted to evaluate in column/row order, we would modify
* this function to suit.
*/
function CmpCellPos (!c1, !c2)
{
if (c1.row < c2.row)
-1;
else if (c1.row == c2.row)
c1.col - c2.col;
else
1;
}
/*
* Add a cell containing a formula to the global formula array. We do this
* so that when we recompute the sheet, we only have to visit those cells
* that are known to have a formula. Since a sheet is usually sparse, this
* can save a lot of time.
*/
function AddFormulaCell (cell)
{
local found;
found = bsearch (FormulaCells, cell, CmpCellPos);
if (undefined_p (car(found)))
insert (FormulaCells, cdr(found), cell);
}
/*
* Remove a cell from the global formulas array.
*/
function RemoveFormulaCell (cell)
{
local found;
found = bsearch (FormulaCells, cell, CmpCellPos);
if (!undefined_p(car(found)))
delete (FormulaCells, cdr(found));
}
/*
* Set the formula for a cell. By offering this single entry point for
* modifying the formula, we can ensure that we know which cells to
* look at when we recompute the sheet.
*/
function SetFormula (cell, str)
{
if (!str || str == "")
RemoveFormulaCell (cell);
else
AddFormulaCell (cell);
cell.formula = str;
}
/*
* Evaluate the formula of a cell, and set the cell's visible text string
* to the result. We attempt this once using the exact formula text. If
* it fails, we append a semicolon to the formula and try again. If there
* is an error, we do something cheesy to get rid of some of the error
* message, since it could be much too long for the cell on-screen.
*/
function EvaluateCell (mat, row, col)
{
local answer, str, _parser_throws_error_ = t;
if (cell = mat.ExistingCell (row, col))
{
str = cell.formula;
if (str && str != "")
{
try
{
answer = eval_string (str,t);
}
catch
{
try
{
answer = eval_string (string (str, ";"),t);
}
catch
{
answer = string_split(_last_error_, ":", 3);
if (length(answer) == 4)
answer = car(reverse(answer));
else
answer = _last_error_;
}
}
cell.text_string = string (answer);
}
}
}
/*
* This is an exhaustive recomputation function that looks to see if a
* cell has any data, and then attempts to evaluate it. It is not very
* efficient, so we don't use it. It is just here for demonstration.
*/
function Recompute (mat)
{
local row, col, cell;
for (row = 0; row < mat.matrix_rows; row ++)
{
for (col = 0; col < mat.matrix_cols; col ++)
{
if (cell = mat.ExistingCell (row, col))
{
EvaluateCell (mat, row, col);
}
}
}
}
/*
* This is the real recomputation function. We keep track of which cells
* contain a formula so we only have to deal with those. It cannot help
* but be faster. The FormulaCells are sorted in row/col order, so we
* do not need to do anything fancy to get the evaluation order correct.
*/
function Recompute (mat)
{
with cell in FormulaCells do
{
EvaluateCell (mat, cell.row, cell.col);
}
}
/*
* A little Gamma cheat. : (colon) is a function taking two arguments. We
* override it here, and just have it generate a rectangle from its args.
* Now we can express ranges as A2:B5 without breaking the Gamma syntax.
*/
function \: (!x,!y)
{
local rect = new (PhRect);
SpreadsheetToPoint (string(x), rect.ul);
SpreadsheetToPoint (string(y), rect.lr);
rect;
}
/*
* Convert a number to an alpha number for the column designation.
*/
function ConvertToAlpha (num)
{
num < 26 ? char('A' + num) :
string ("A", char('A' + num % 26));
}
/*
* Convert a range rectangle into a A2:B4 style designation.
*/
function cbRangeConvert ()
{
local str, rect = cbinfo.matrix.cur_range;
str = string (ConvertToAlpha(rect.ul.x), rect.ul.y, ":",
ConvertToAlpha(rect.lr.x), rect.lr.y);
cbinfo.matrix.text_string = str;
}
/*
* The mainline. Here we set up interprocess communication and build the
* main screen.
*/
function main ()
{
local w, mat, menubar, mbutton, emenu;
init_ipc ("matrix","matrix");
PtInit (nil);
w = new (PtWindow);
w.SetDim (400, 300);
w.resize_flags = cons (Pt_RESIZE_X_INITIAL | Pt_RESIZE_Y_INITIAL, -1);
menubar = new (PtMenuBar);
menubar.SetDim (menubar.dim.w, MENUHEIGHT - 2 * menubar.border_width);
mbutton = new (PtMenuButton);
mbutton.text_string = "Effects";
PtRealizeWidget (w);
PtSetParentWidget (w);
mat = new (CwMatrix);
mat.matrix_rows = 10;
mat.matrix_cols = 10;
mat.SetArea (XOFFSET, YOFFSET + MENUHEIGHT, 400 - XOFFSET,
300 - (YOFFSET + MENUHEIGHT));
mat.anchor_flags = cons (Pt_LEFT_ANCHORED_LEFT | Pt_RIGHT_ANCHORED_RIGHT |
Pt_TOP_ANCHORED_TOP | Pt_BOTTOM_ANCHORED_BOTTOM,
-1);
PtAttachCallback (mat, Cw_CB_MATRIX_BEGIN_EDIT, `cbBeginEdit());
PtAttachCallback (mat, Cw_CB_MATRIX_TEXT_CHANGE, `cbTextChange());
PtAttachCallback (mat, Cw_CB_MATRIX_RANGE_CONVERT, `cbRangeConvert());
PtRealizeWidget (mat);
PtSetParentWidget (w);
RowLabels(mat);
PtSetParentWidget (w);
ColumnLabels(mat);
emenu = CreateEffectMenu(mat);
PtAttachCallback (mbutton, Pt_CB_ARM,
`(@emenu).Instantiate (@mbutton,
MenuButtonPos(@mbutton)));
PtMainLoop ();
}
/* ---------------------- Spreadsheet functions ---------------- */
/*
* This section contains functions that are callable from a spreadsheet
* cell. We need to specify these specially, as they must expect to
* take a range as input. Unfortunately, this means that we cannot
* simply use the functions built into Gamma.
*/
function SpreadsheetToPoint (str, point)
{
local i, row, col, colstr;
for (i=0; str[i] >= 'A' && str[i] <= 'Z'; i++);
colstr = substr(str,0,i);
row = number (substr(str,i,-1));
if (i==1)
col = str[0] - 'A';
else
col = (str[0] - 'A') * 26 + str[1] - 'A';
point.x = col;
point.y = row;
point;
}
/*
* Functions usable in the spreadsheet. There are global variables
* cell and mat defined during these function.
*
* A range is a PhRect representing the upper left and lower right
* corners of the range.
*
* In a spreadsheet cell, you would see: =sum(A5:B6)
*/
function sum (range)
{
local rect = range, row, col, cell, result=0;
for (row = rect.ul.y; row <= rect.lr.y; row++)
{
for (col = rect.ul.x; col <= rect.lr.x; col++)
{
if (cell = mat.ExistingCell(row,col))
{
result += number(cell.text_string);
}
}
}
result;
}
/*
* Exception function that updates a cell when a &datahub; point
* change occurs, and then recomputes the spreadsheet.
*/
function exCellException(cell)
{
cell.text_string = string (value);
Recompute(mat);
}
/*
* A spreadsheet function that causes a cell to be attached to a
* &datahub; point.
* In the cell you would see: =db(tank_level)
* Notice that the point name is unevaluated, so you do not need to
* use the # or ` syntax to protect the point name.
*/
function db (!name)
{
local sym = symbol(name);
if (!getprop (sym,#registered))
{
setprop (sym, #registered, t);
register_exception (sym, `exCellException(@cell));
}
eval(sym);
}
/*
* A spreadsheet function that writes a value to the &datahub;.
* This function transmits a value from the top-left corner of the given
* range to the named point every time it is evaluated. This will not
* transmit the value of the current cell, since this would be a bit of
* a chicken-and-egg problem. The result in the cell will be the point
* name on success, or an error message on failure.
*/
function dbout (!name,range)
{
local sym = symbol(name);
local rect = range, outcell;
if (outcell = mat.ExistingCell (rect.ul.y, rect.ul.x))
{
write_point (sym, number(outcell.text_string));
name;
}
else
"No value";
}
Copyright © 1995-2010 by Cogent Real-Time Systems, Inc. All rights reserved.