Clock.mesa
Copyright c 1985 by Xerox Corporation. All rights reserved.
Russ Atkinson, March 6, 1985 12:54:47 pm PST
Bob Hagmann May 1, 1985 8:21:23 am PDT
Doug Wyatt, May 20, 1985 9:19:16 pm PDT
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 the Cedar Imager. It also provides some stylistic guidelines for writing monitors (such as UNWIND => NULL), processes (handling ABORTED), and suggests an indentation style for Mesa.
DIRECTORY
BasicTime USING [GMT, Now, Period, Unpack, Unpacked, Update],
Commander USING [CommandProc, Register],
Convert USING [RopeFromInt],
Imager,
ImagerBackdoor USING [GetBounds, invert, MakeStipple],
Menus USING [AppendMenuEntry, CreateEntry, CreateMenu, FindEntry, Menu, MenuEntry, MenuProc, ReplaceMenuEntry],
Process USING [Detach, DisableTimeout, MsecToTicks, SetTimeout],
Real USING [Fix],
RealFns USING [CosDeg, SinDeg],
Rope USING [Concat, ROPE],
SafeStorage USING [PinObject, UnpinObject],
UserProfile USING [Boolean, CallWhenProfileChanges, ProfileChangedProc],
VFonts USING [EstablishFont],
ViewerClasses USING [Column, PaintProc, Viewer, ViewerClass, ViewerClassRec],
ViewerOps USING [CreateViewer, PaintViewer, RegisterViewerClass, SetMenu];
Clock: CEDAR MONITOR
IMPORTS BasicTime, Commander, Convert, Imager, ImagerBackdoor, Menus, Process, Real, RealFns, Rope, SafeStorage, UserProfile, VFonts, ViewerOps
SHARES ViewerOps
= BEGIN OPEN Rope;
defaultTime: BasicTime.GMT = BasicTime.Now[];
defaultUntime: BasicTime.Unpacked = BasicTime.Unpack[defaultTime];
desktopGrey: Imager.Color ~ ImagerBackdoor.MakeStipple[104042B]; -- magic bit pattern for desk top grey
labelFont: Imager.Font ← NIL;
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: BOOLFALSE,
live: BOOLTRUE,
painting: BOOLFALSE,
drawSeconds: BOOLTRUE,
forceClear: BOOLTRUE,
forceRepaint: BOOLFALSE,
showDateIconic: BOOLFALSE,
dateEntry: ROPENIL,
textDay: ROPENIL,
dateCount: INTEGER ← -3,
dateTime: BasicTime.Unpacked ← defaultUntime,
time: BasicTime.Unpacked ← defaultUntime,
packed: BasicTime.GMT ← defaultTime,
foreground: Imager.Color ← Imager.black,
offset: INT ← 0, -- in seconds
minWidth: REAL ← 0.0,
secWidth: REAL ← 0.0,
paintingChange: CONDITION
];
EnterPaint: ENTRY PROC [data: MyData] RETURNS [died: BOOL] = {
EnterPaint claims the paint lock, returns TRUE 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;
};
NewProfile: ENTRY UserProfile.ProfileChangedProc = {
ENABLE UNWIND => NULL;
showDateIconic: BOOL = UserProfile.Boolean[key: "Clock.showDateIconic", default: FALSE];
FOR l: LIST OF MyData ← clockList, l.rest UNTIL l = NIL DO
l.first.showDateIconic ← showDateIconic;
l.first.forceRepaint ← TRUE;
l.first.forceClear ← TRUE;
ENDLOOP;
};
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 = {
[self: Viewer, context: Imager.Context, whatChanged: REF, clear: BOOL]
PaintMe is called to repaint the clock (we do try to minimize the work involved).
ctx: Imager.Context ← context;
data: MyData ← NARROW[self.data];
IF NOT EnterPaint[data] THEN {
rect: Imager.Rectangle ~ ImagerBackdoor.GetBounds[ctx];
minX: REAL ← rect.x;
minXI: INT ← Real.Fix[minX];
minY: REAL ← rect.y;
minYI: INT ← Real.Fix[minY];
maxX: REAL ← minX+rect.w;
widthI: INT ← Real.Fix[rect.w];
maxY: REAL ← minY+(IF data.showDateIconic THEN rect.h-12 ELSE rect.h);
heightI: INT ← Real.Fix[rect.h];
halfX: REAL ← (maxX - minX) / 2.0;
halfY: REAL ← (maxY - minY) / 2.0;
radius: REALMIN[halfX, halfY];
foreground: Imager.Color ← data.foreground;
background: Imager.Color ← IF foreground=Imager.white THEN Imager.black ELSE Imager.white;
spokes: NAT ← 32;
oldTime: BasicTime.Unpacked ← data.time;
offset: INT ← data.offset;
curPacked: BasicTime.GMT ← BasicTime.Update[BasicTime.Now[], offset];
curTime: BasicTime.Unpacked ← BasicTime.Unpack[curPacked];
greyIcon: PROC ~ {
Imager.SetColor[ctx, desktopGrey];
Imager.MaskRectangleI[ctx, 0, 0, widthI, heightI];
};
TickMark: PROC [seconds: REAL, d: REAL ← 1.0 / 32] = {
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.
tick: PROC ~ {
IF self.iconic THEN d ← d + d; -- iconic => double tick size
Imager.SetColor[ctx, foreground]; -- use foreground color for ticks
Imager.RotateT[ctx, -6.0 * seconds]; -- rotate to tick mark angle
Imager.TranslateT[ctx, [d - 1.0, 0.0]]; -- move to near circle edge
Imager.MaskBox[ctx, [-d, -d, d, d]]; -- actually draw the box
};
Imager.DoSave[ctx, tick]; -- restore context to original state after calling tick
};
Face: PROC = {
Face is used to draw a clock face (a circle).
circle: PROC ~ {
path: Imager.PathProc ~ {
moveTo[[1.0, 0.0]];
FOR i: NAT IN [1..60] DO
add to the path
deg: NAT ← 6*i;
lineTo[[RealFns.CosDeg[deg], RealFns.SinDeg[deg]]];
ENDLOOP;
};
Imager.SetColor[ctx, background];
Imager.MaskFill[ctx, path];
};
Imager.DoSave[ctx, circle];
};
DrawTime: PROC [oldTime, newTime: BasicTime.Unpacked] = {
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: BOOLFALSE] = {
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.
hand: PROC ~ {
degrees: REAL ← -6.0 * seconds;
Imager.RotateT[ctx, degrees];
IF invert THEN Imager.SetColor[ctx, ImagerBackdoor.invert];
IF data.area AND width > 0.0 AND NOT self.iconic THEN {
triangle: Imager.PathProc ~ {
moveTo[[0.0, length]];
lineTo[[-width, -width]];
lineTo[[width, -width]];
};
Imager.MaskFill[ctx, triangle];
}
ELSE {
path: Imager.PathProc ~ {
moveTo[[0.0, 0.0]];
lineTo[[0.0, length]];
lineTo[[-width, -width]];
lineTo[[width, -width]];
lineTo[[0.0, length]];
};
Imager.SetStrokeWidth[ctx, 0];
Imager.SetStrokeJoint[ctx, round];
IF width=0 THEN Imager.MaskVector[ctx, [0.0, 0.0], [0.0, length]]
ELSE Imager.MaskStroke[context: ctx, path: path, closed: TRUE];
};
};
Imager.DoSave[ctx, hand];
};
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;
drawHands: PROC ~ {
IF NOT clear THEN {
erase the hands
Imager.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 * 5 + oldTime.minute / 12.0, 0.60, 0.03];
needHour ← TRUE;
}
};
draw the hands
Imager.SetColor[ctx, foreground];
IF needHour OR needMinute THEN
Handy[(newTime.hour) * 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];
};
Imager.DoSave[ctx, drawHands];
};
Banner: PROC = {
IF self.iconic AND data.showDateIconic AND data.dateCount <= 0 THEN {
Imager.SetColor[ctx, background];
Imager.MaskRectangleI[ctx, 0, heightI-10, widthI, 13];
Imager.SetColor[ctx, foreground];
Imager.SetFont[ctx, labelFont];
Imager.SetXYI[ctx, 0, heightI-9];
Imager.ShowRope[ctx, data.textDay];
};
data.dateCount ← data.dateCount + 1;
IF data.dateCount > 10 THEN data.dateCount ← 0 ;
};
ClockFace: PROC = {
Imager.TranslateT[ctx, [halfX, halfY]];
Imager.ScaleT[ctx, radius];
Imager.SetColor[ctx, foreground];
IF clear THEN {
init the face
IF self.iconic THEN Face[]
ELSE {-- init the screen to background
Imager.SetColor[ctx, background];
Imager.MaskBox
[ctx, [-halfX/radius, -halfY/radius, halfX/radius, halfY/radius]];
Imager.SetColor[ctx, foreground];
};
draw the tick marks
FOR i: NAT IN [0..12) DO TickMark[i * 5] ENDLOOP;
};
draw the hands
data.dateCount ← -2;
DrawTime[oldTime, curTime];
};
data.time ← curTime;
IF whatChanged = NIL OR curTime = oldTime OR data.forceClear THEN {
clear ← TRUE;
data.forceClear ← FALSE;
};
IF clear AND self.iconic THEN Imager.DoSave[ctx, greyIcon];
IF radius>0 THEN Imager.DoSave[ctx, ClockFace];
Imager.DoSave[ctx, Banner];
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 = Imager.white
THEN data.foreground ← Imager.black
ELSE data.foreground ← Imager.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 hour offset (red back, blue forward, yellow zeros offset)
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: MyData ← NARROW[viewer.data];
[] ← EnterPaint[data];
IF control
THEN data.offset ← 0
ELSE {
delta: INT ← 0;
SELECT mouseButton FROM
red => delta ← 60*60;
yellow => delta ← 60;
blue => delta ← 1;
ENDCASE;
data.offset ← IF shift THEN data.offset + delta ELSE data.offset - delta;
};
data.forceClear ← TRUE;
ExitPaint[data];
ViewerOps.PaintViewer[viewer, client, FALSE]
};
RefreshDate: Menus.MenuProc = {
[parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL]
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];
weekDay: ROPE ← "";
month: ROPENIL;
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 ← " ??? ";
SELECT data.dateTime.weekday FROM
Monday => weekDay ← "mon ";
Tuesday => weekDay ← "tue ";
Wednesday => weekDay ← "wed ";
Thursday => weekDay ← "thu ";
Friday => weekDay ← "fri ";
Saturday => weekDay ← "sat ";
Sunday => weekDay ← "sun ";
ENDCASE => weekDay ← "???";
IF data.dateTime.day < 10 THEN day ← Rope.Concat[" ", day];
data.textDay ← weekDay.Concat[day.Concat[month]];
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: BOOLTRUE] = 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;
SafeStorage.PinObject[data];
This is an attempt to pin the object to avoid faults from a timed-out variable
Process.SetTimeout[@data.paintingChange, Process.MsecToTicks[pause]];
Child[viewer, data, iconicFlag ! ABORTED => CONTINUE];
TurnOff[data];
};
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];
data.viewer ← viewer;
data.showDateIconic ← UserProfile.Boolean[key: "Clock.showDateIconic", default: FALSE];
AddMeToList[data];
RefreshDate[viewer, NIL];
WHILE data.live AND NOT viewer.destroyed DO
newPacked: BasicTime.GMT ← BasicTime.Now[];
IF newPacked # packed THEN {
IF ~data.forceRepaint AND (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;
data.forceRepaint ← FALSE;
};
Rest[data]
ENDLOOP
};
pause: CARDINAL ← 200; -- in milliseconds
TurnOff: ENTRY PROC [data: MyData] = TRUSTED {
Turn off the clock for good.
ENABLE UNWIND => NULL;
IF data.live THEN {
Process.DisableTimeout[@data.paintingChange];
data.live ← FALSE;
BROADCAST data.paintingChange;
SafeStorage.UnpinObject[data];
There should be no further possible timeouts on the condition variable
};
};
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: BOOLTRUE] = 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
ViewerOps.RegisterViewerClass[$Clock, viewerClass];
};
Process.Detach[FORK Mother[iconicFlag]];
WaitForListChange[old]
};
Start: Commander.CommandProc = {
Start1[TRUE]
};
labelFont ← VFonts.EstablishFont["Helvetica", 8];
Commander.Register["Clock", Start, "starts a clock to display the time"];
UserProfile.CallWhenProfileChanges[NewProfile];
END.