MyClock.mesa
Russ Atkinson, January 19, 1983 4:42 pm
McGregor, 18-Jan-82 16:03:16
Teitelman, June 25, 1983 8:42 pm
This module implements a graphic clock that is displayed as a dial with hands. The size of the clock is determined dynamically according to the space allowed for the containing viewer. The clock will display and update a second hand when it is large (non-iconic), and at all times will display and update minute and hour hands. Options are provided to invert the clock, and to change the offset of the hour (mostly for fun). The user may have multiple clocks on the screen.
Besides telling the time in an attractive manner, this module provides an example of a user-written Viewers class, and a simple use of Cedar Graphics. It also provides some stylistic guidelines for writing monitors (such as UNWIND => NULL), processes (handling ABORTED), and suggests an indentation style for Mesa.
Last Edited by: Maxwell, January 5, 1984 12:38 pm
DIRECTORY
BasicTime
USING
[GMT, Now, earliestGMT, Period, Unpack, Unpacked, Update],
Convert
USING
[RopeFromInt],
Graphics
USING
[black, Box, Color, Context, DrawArea, DrawBox, DrawStroke, FlushPath, GetBounds, LineTo, Mark, MoveTo, NewPath, Path, Restore, Rotate, Save, SetFat, Scale, SetColor, SetPaintMode, Translate, white],
Menus
USING
[AppendMenuEntry, CreateEntry, CreateMenu, FindEntry, Menu, MenuEntry, MenuProc, ReplaceMenuEntry],
Process
USING
[Detach, MsecToTicks, SetTimeout],
RealFns
USING
[CosDeg, SinDeg],
Rope
USING
[Concat, ROPE],
ViewerClasses
USING
[PaintProc, Viewer, ViewerClass, ViewerClassRec],
ViewerOps
USING
[CreateViewer, PaintViewer, RegisterViewerClass, SetMenu];
MyClock:
CEDAR
MONITOR
IMPORTS Convert, Graphics, Menus, Process, RealFns, Rope, ViewerOps, BasicTime
= BEGIN OPEN Rope;
defaultUntime: BasicTime.Unpacked;
MyData:
TYPE =
REF MyDataRec;
A MyData object is used to retain state used in updating the clock.
MyDataRec:
TYPE =
RECORD
[viewer: ViewerClasses.Viewer ← NIL,
area: BOOL ← FALSE,
live: BOOL ← TRUE,
painting: BOOL ← FALSE,
drawSeconds: BOOL ← TRUE,
forceClear: BOOL ← TRUE,
dateEntry: ROPE ← NIL,
dateTime: BasicTime.Unpacked ← defaultUntime,
time: BasicTime.Unpacked ← defaultUntime,
packed: BasicTime.GMT ← BasicTime.earliestGMT,
foreground: Graphics.Color ← Graphics.black,
-- hourOffset: INTEGER ← 0,
offSet: INT ← 0, -- in seconds
path: Graphics.Path ← NIL,
minWidth: REAL ← 0.0,
secWidth: REAL ← 0.0,
paintingChange: CONDITION];
EnterPaint:
ENTRY
PROC [data: MyData]
RETURNS [died:
BOOL] = {
EnterPaint claims the paint lock, returns FALSE if the viewer has died.
{
ENABLE
UNWIND =>
NULL;
IF data = NIL THEN RETURN [TRUE];
DO
IF NOT data.live THEN RETURN [TRUE];
IF NOT data.painting THEN EXIT;
WAIT data.paintingChange
ENDLOOP;
data.painting ← TRUE;
RETURN [FALSE]};
};
ExitPaint:
ENTRY
PROC [data: MyData] = {
ExitPaint releases the paint lock, ignore NIL data (could be a destroy race).
{
ENABLE
UNWIND =>
NULL;
IF data = NIL THEN RETURN;
data.painting ← FALSE;
BROADCAST data.paintingChange}
};
clockListChange: CONDITION; -- notified when the clock list changes
AddMeToList:
ENTRY
PROC [me: MyData] = {
AddMeToList atomically adds a clock to the current list.
{
ENABLE
UNWIND =>
NULL;
clockList ← CONS[me, clockList];
BROADCAST clockListChange}
};
WaitForListChange:
ENTRY
PROC [old:
LIST
OF MyData] = {
Wait for a clock list change.
{
ENABLE
UNWIND =>
NULL;
WHILE clockList = old
DO
WAIT clockListChange
ENDLOOP}
};
PaintMe: ViewerClasses.PaintProc =
TRUSTED {
[self: Viewer, context: Graphics.Context, whatChanged: REF, clear: BOOL]
PaintMe is called to repaint the clock (we do try to minimize the work involved).
ctx: Graphics.Context ← context;
data: MyData ← NARROW[self.data];
IF
NOT EnterPaint[data]
THEN {
box: Graphics.Box ← Graphics.GetBounds[ctx]; -- scale to viewer size
maxX: REAL ← box.xmax;
maxY: REAL ← box.ymax;
minX: REAL ← box.xmin;
minY: REAL ← box.ymin;
halfX: REAL ← (maxX - minX) / 2.0;
halfY: REAL ← (maxY - minY) / 2.0;
radius: REAL ← IF halfX < halfY THEN halfX ELSE halfY;
foreground: Graphics.Color ← data.foreground;
background: Graphics.Color ←
IF foreground = Graphics.white THEN Graphics.black ELSE Graphics.white;
spokes: NAT ← 32;
oldTime: BasicTime.Unpacked ← data.time;
curPacked: BasicTime.GMT ← BasicTime.Update[BasicTime.Now[], data.offSet];
curTime: BasicTime.Unpacked ← BasicTime.Unpack[curPacked];
hoff: INTEGER ← data.hourOffset;
TickMark:
PROC [seconds:
REAL, d:
REAL ← 1.0 / 32] =
TRUSTED {
TickMark is used to paint a single tick mark. It just puts a rectangle at the position given by seconds. We include extra comments to explain Graphics usage.
mark: Graphics.Mark ← Graphics.Save[ctx]; -- mark original state of graphics context
IF self.iconic THEN d ← d + d; -- iconic => double tick size
Graphics.SetColor[ctx, foreground]; -- use foreground color for ticks
Graphics.Rotate[ctx, -6.0 * seconds]; -- rotate to tick mark angle
Graphics.Translate[ctx, d - 1.0, 0.0]; -- move to near circle edge
Graphics.DrawBox[ctx, [-d, -d, d, d]]; -- actually draw the box
Graphics.Restore[ctx, mark]; -- restore context to original state
};
Face:
PROC =
TRUSTED {
Face is used to draw a clock face (a circle).
mark: Graphics.Mark ← Graphics.Save[ctx];
Graphics.SetColor[ctx, background];
Graphics.MoveTo[path, 1.0, 0.0];
FOR i:
NAT
IN [1..60]
DO
add to the path
deg: NAT ← 6*i;
Graphics.LineTo[path, RealFns.CosDeg[deg], RealFns.SinDeg[deg]]
ENDLOOP;
Graphics.DrawArea[ctx, path];
Graphics.Restore[ctx, mark]
};
DrawTime:
PROC [oldTime, newTime: BasicTime.Unpacked] =
TRUSTED {
DrawTime is called to draw the new time. We assume that the old time is the one currently displayed.
Handy:
PROC [seconds, length, width:
REAL, invert:
BOOL ←
FALSE] =
TRUSTED {
Handy draws a hand at the position given by seconds (not necessarily integral), where the length and width are normalized to a radius of 1. If invert, we are actually erasing an old hand.
degrees: REAL ← -6.0 * seconds;
localMark: Graphics.Mark ← Graphics.Save[ctx];
closed: BOOL ← FALSE;
IF invert
THEN {
force inversion of the hands
Graphics.SetColor[ctx, Graphics.black];
[] ← Graphics.SetPaintMode[ctx, invert]};
rotate to proper angle
Graphics.Rotate[ctx, degrees];
as a test hack, use a box for the second hand instead of the path stuff
IF data.secWidth > 0
THEN
{width ← data.secWidth/radius;
Graphics.DrawBox[ctx, [-width, 0, width, length]];
Graphics.Restore[ctx, localMark];
RETURN};
enter the path
Graphics.MoveTo[path, 0.0, 0.0];
Graphics.LineTo[path, 0.0, length];
IF width > 0
THEN
not a simple line, but a triangle
{closed ←
TRUE;
Graphics.LineTo[path, -width, -width];
Graphics.LineTo[path, width, -width];
Graphics.LineTo[path, 0.0, length]};
IF data.area
AND width > 0.0
AND
NOT self.iconic
THEN Graphics.DrawArea[ctx, path]
ELSE Graphics.DrawStroke[ctx, path, 0.0, closed, round];
Graphics.Restore[ctx, localMark]
};
mark: Graphics.Mark ← Graphics.Save[ctx];
oldSecMod: CARDINAL ← oldTime.second - oldTime.second MOD 10;
newSecMod: CARDINAL ← newTime.second - newTime.second MOD 10;
needSecond: BOOL ← data.drawSeconds AND NOT self.iconic;
needMinute: BOOL ← clear;
needHour: BOOL ← clear;
IF
NOT clear
THEN {
erase the hands
Graphics.SetColor[ctx, background];
IF needSecond
THEN
erase the second hand
Handy[oldTime.second, 0.85, 0.0, TRUE];
IF oldTime.minute # newTime.minute
OR oldSecMod # newSecMod
THEN {
erase the minute hand
Handy[oldTime.minute + oldSecMod / 60.0, 0.80, 0.02];
needMinute ← TRUE};
IF oldTime.hour # newTime.hour
OR oldTime.minute # newTime.minute
THEN {
erase the hour hand
Handy[(oldTime.hour -- + hoff -- ) * 5 + oldTime.minute / 12.0, 0.60, 0.03];
draw the hands
Graphics.SetColor[ctx, foreground];
IF needHour
OR needMinute
THEN
Handy[(newTime.hour -- + hoff -- ) * 5 + newTime.minute / 12.0, 0.60, 0.03];
IF needMinute THEN Handy[newTime.minute + newSecMod / 60.0, 0.80, 0.02];
IF needSecond
THEN
-- draw the second hand
Handy[newTime.second, 0.85, 0.0, TRUE];
Graphics.Restore[ctx, mark]
};
path: Graphics.Path ← data.path;
IF path =
NIL
THEN data.path ← path ← Graphics.NewPath[16]
ELSE Graphics.FlushPath[path];
Graphics.Translate[ctx, halfX, halfY];
Graphics.Scale[ctx, radius, radius];
Graphics.SetColor[ctx, foreground];
[] ← Graphics.SetFat[ctx, FALSE];
data.time ← curTime;
IF whatChanged =
NIL
OR curTime = oldTime
OR data.forceClear
THEN
{clear ← TRUE; data.forceClear ← FALSE};
IF clear
THEN
init the face
{
IF self.iconic
THEN Face[]
ELSE {
-- init the screen to background
Graphics.SetColor[ctx, background];
Graphics.DrawBox
[ctx, [-halfX/radius, -halfY/radius, halfX/radius, halfY/radius]];
Graphics.SetColor[ctx, foreground]};
draw the tick marks
FOR i:
NAT
IN [0..12)
DO
TickMark[i * 5]
ENDLOOP};
draw the hands
DrawTime[oldTime, curTime];
ExitPaint[data]}
};
SwapColor: Menus.MenuProc = {
[parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL]
SwapColor swaps the foreground & background colors.
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: MyData ← NARROW[viewer.data];
[] ← EnterPaint[data];
IF data.foreground = Graphics.white
THEN data.foreground ← Graphics.black
ELSE data.foreground ← Graphics.white;
data.forceClear ← TRUE;
ExitPaint[data];
ViewerOps.PaintViewer[viewer, client, FALSE]
};
ChangeAllOffSets:
PROC [offSet:
INT] = {
FOR l:
LIST
OF MyData ← clockList, l.rest
UNTIL l =
NIL
DO
data: MyData ← l.first;
[] ← EnterPaint[data];
data.offSet ← offSet;
data.forceClear ← TRUE;
ExitPaint[data];
ViewerOps.PaintViewer[data.viewer, client, FALSE]
ENDLOOP;
};
ChangeOffset: Menus.MenuProc = {
[parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL]
ChangeOffset changes the offset: red = hours, yellow = minutes, blue = seconds, shift = forwards, noshift = backwards, control = reset.
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: MyData ← NARROW[viewer.data];
direction: INT = IF shift THEN 1 ELSE -1;
[] ← EnterPaint[data];
IF control THEN data.offSet ← 0
ELSE data.offSet ← data.offSet + (direction *
(SELECT mouseButton
FROM
red => 3600,
yellow => 60,
blue => 1,
ENDCASE => ERROR));
data.forceClear ← TRUE;
ExitPaint[data];
ViewerOps.PaintViewer[viewer, client, FALSE]
};
RefreshDate: Menus.MenuProc = {
[parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL]
RefreshDate changes the hour offset (red back, blue forward, yellow zeros offset).
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: MyData ← NARROW[viewer.data];
IF EnterPaint[data] THEN RETURN;
IF data.dateEntry = NIL THEN {ExitPaint[data]; RETURN};
data.dateTime ← data.time;
{
-- now split out the parts of the date
day: ROPE ← Convert.RopeFromInt[data.dateTime.day];
month: ROPE ← NIL;
year: ROPE ← Convert.RopeFromInt[data.dateTime.year - 1900];
entry: Menus.MenuEntry ← NIL;
SELECT data.dateTime.month
FROM
January => month ← " Jan ";
February => month ← " Feb ";
March => month ← " Mar ";
April => month ← " Apr ";
May => month ← " May ";
June => month ← " Jun ";
July => month ← " Jul ";
August => month ← " Aug ";
September => month ← " Sep ";
October => month ← " Oct ";
November => month ← " Nov ";
December => month ← " Dec ";
ENDCASE => month ← " ??? ";
IF data.dateTime.day < 10 THEN day ← Rope.Concat[" ", day];
day ← day.Concat[month.Concat[year]];
entry ← Menus.CreateEntry[day, RefreshDate];
Menus.ReplaceMenuEntry
[viewer.menu, Menus.FindEntry[viewer.menu, data.dateEntry], entry
! UNWIND => ExitPaint[data]];
data.dateEntry ← day;
};
data.forceClear ← TRUE;
ExitPaint[data];
ViewerOps.PaintViewer[viewer, menu, FALSE]
};
clockList:
LIST
OF MyData ←
NIL;
We keep a list of the created clocks (it is not really needed by anyone, but can help debugging).
viewerClass: ViewerClasses.ViewerClass ← NIL;
Mother:
PROC [iconicFlag:
BOOL ←
TRUE] =
TRUSTED {
Mother gives the extra level of frame needed to handle ABORTED properly.
viewer: ViewerClasses.Viewer ← NIL;
data: MyData ← NEW[MyDataRec ← []];
data.dateTime.year ← 0;
Process.SetTimeout[@data.paintingChange, Process.MsecToTicks[pause]];
Child[viewer, data, iconicFlag ! ABORTED => CONTINUE];
data.live ← FALSE;
};
Child:
PROC [viewer: ViewerClasses.Viewer, data: MyData, iconicFlag:
BOOL] =
TRUSTED {
This procedure just loops, repainting the clock as the time changes. We only return when the clock viewer is destroyed.
packed: BasicTime.GMT ← BasicTime.Now[];
menu: Menus.Menu ← Menus.CreateMenu[];
Menus.AppendMenuEntry
[menu, Menus.CreateEntry["SwapColor", SwapColor]];
Menus.AppendMenuEntry
[menu, Menus.CreateEntry["ChangeOffset", ChangeOffset]];
Menus.AppendMenuEntry
[menu, Menus.CreateEntry[data.dateEntry ← "XX-XXX-XX", RefreshDate]];
viewer ←
ViewerOps.CreateViewer
[flavor: $Clock, info: [name: "Clock", column: right, iconic: iconicFlag, data: data]];
ViewerOps.SetMenu[viewer, menu];
AddMeToList[data];
data.viewer ← viewer;
RefreshDate[viewer, NIL];
WHILE data.live
AND
NOT viewer.destroyed
DO
newPacked: BasicTime.GMT ← BasicTime.Now[];
newPacked: BasicTime.GMT ← IF curPacked # BasicTime.earliestGMT THEN curPacked ELSE BasicTime.Now[];
IF newPacked # packed
THEN {
IF viewer.iconic OR NOT data.drawSeconds THEN
IF BasicTime.Period[packed, newPacked] < 30 THEN {Rest[data]; LOOP};
IF
NOT data.painting
THEN
ViewerOps.PaintViewer[viewer, client, FALSE, $time];
IF data.time.day # data.dateTime.day
OR data.time.month # data.dateTime.month
THEN
RefreshDate[viewer, NIL];
packed ← newPacked; -- note that if curPacked is set to some specific time, then the clock will not move until it is reset.
};
Rest[data]
ENDLOOP
};
pause: CARDINAL ← 200; -- in milliseconds
Rest:
ENTRY
PROC [data: MyData] = {
Just pause for the appropriate time. The pause should be long enough to not burden the processor and short enough to keep the second hand update smooth.
ENABLE UNWIND => NULL;
WAIT data.paintingChange;
};
Start1:
PROC [iconicFlag:
BOOL ←
TRUE] =
TRUSTED {
Start up one clock.
old: LIST OF MyData ← clockList;
IF viewerClass =
NIL
THEN {
viewerClass ←
NEW [ViewerClasses.ViewerClassRec
← [paint: PaintMe,
-- called to repaint
icon: private, -- picture to display when small
cursor: textPointer]]; -- cursor when mouse is in viewer
defaultUntime ← BasicTime.Unpack[BasicTime.earliestGMT];
ViewerOps.RegisterViewerClass[$Clock, viewerClass];
};
Process.Detach[FORK Mother[iconicFlag]];
WaitForListChange[old]
};
Start:
PROC [n:
INT ← 1, iconic:
BOOL ←
TRUE] = {
Start up n clocks, iconic by default.
IF iconic # TRUE AND iconic # FALSE THEN ERROR;
FOR i:
INT
IN [1..n]
DO
Start1[iconic]
ENDLOOP
};
Start1[] -- the default is to start 1 iconic clock
END.
Edited on June 25, 1983 8:42 pm, by Teitelman
changed ChangeOffSet button to changeOffset for hours, minutes, or seconds, direction determined by shift key. reset now done by control
defined ChangeAllOffSets to allow changing offset from program. required adding viewer field to MyData.
changes to: defaultUntime, MyData, MyDataRec, clockListChange, PaintMe, DrawTime (local of PaintMe), ChangeOffset, clockList, viewerClass, Child, pause, ChangeAllOffSets, RefreshDate, Mother, END, ChangeOffset
Edited on January 5, 1984 12:29 pm, by Maxwell
changes to: DIRECTORY, MyClock, defaultUntime, MyDataRec, PaintMe, DrawTime (local of PaintMe), RefreshDate, Child, Start1, END