A CwMatrix Spreadsheet

Gamma/Photon: Using Gamma™ with the Photon microGUIŽ, Version 4.0
Prev | Chapter 6. Sample Code and Cool Stuff | Next

# 6.4. A CwMatrix Spreadsheet

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";
    }

Prev | Home | Next
A CwGraph Rotating Cube | Up | Reference

Copyright 1995-2002 by Cogent Real-Time Systems, Inc.