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; MyDataRec: TYPE = RECORD [ viewer: ViewerClasses.Viewer _ NIL, area: BOOL _ FALSE, live: BOOL _ TRUE, painting: BOOL _ FALSE, drawSeconds: BOOL _ TRUE, forceClear: BOOL _ TRUE, forceRepaint: BOOL _ FALSE, showDateIconic: BOOL _ FALSE, dateEntry: ROPE _ NIL, textDay: ROPE _ NIL, 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] = { 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] = { 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] = { 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] = { ENABLE UNWIND => NULL; WHILE clockList = old DO WAIT clockListChange; ENDLOOP; }; PaintMe: ViewerClasses.PaintProc = { 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: REAL _ MIN[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] = { 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 = { circle: PROC ~ { path: Imager.PathProc ~ { moveTo[[1.0, 0.0]]; FOR i: NAT IN [1..60] DO 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] = { Handy: PROC [seconds, length, width: REAL, invert: BOOL _ FALSE] = { 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 { Imager.SetColor[ctx, background]; IF needSecond THEN Handy[oldTime.second, 0.85, 0.0, TRUE]; IF oldTime.minute # newTime.minute OR oldSecMod # newSecMod THEN { Handy[oldTime.minute + oldSecMod / 60.0, 0.80, 0.02]; needMinute _ TRUE; }; IF oldTime.hour # newTime.hour OR oldTime.minute # newTime.minute THEN { Handy[oldTime.hour * 5 + oldTime.minute / 12.0, 0.60, 0.03]; needHour _ TRUE; } }; 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 { 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]; }; FOR i: NAT IN [0..12) DO TickMark[i * 5] ENDLOOP; }; 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 = { 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 = { 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 = { 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: 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 _ " ??? "; 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; viewerClass: ViewerClasses.ViewerClass _ NIL; Mother: PROC [iconicFlag: BOOL _ TRUE] = TRUSTED { viewer: ViewerClasses.Viewer _ NIL; data: MyData _ NEW[MyDataRec _ []]; data.dateTime.year _ 0; SafeStorage.PinObject[data]; 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 { 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 { ENABLE UNWIND => NULL; IF data.live THEN { Process.DisableTimeout[@data.paintingChange]; data.live _ FALSE; BROADCAST data.paintingChange; SafeStorage.UnpinObject[data]; }; }; Rest: ENTRY PROC [data: MyData] = { ENABLE UNWIND => NULL; WAIT data.paintingChange; }; Start1: PROC [iconicFlag: BOOL _ TRUE] = TRUSTED { 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. >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. A MyData object is used to retain state used in updating the clock. EnterPaint claims the paint lock, returns TRUE if the viewer has died. ExitPaint releases the paint lock, ignore NIL data (could be a destroy race). AddMeToList atomically adds a clock to the current list. Wait for a clock list change. [self: Viewer, context: Imager.Context, whatChanged: REF, clear: BOOL] PaintMe is called to repaint the clock (we do try to minimize the work involved). 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. Face is used to draw a clock face (a circle). add to the path DrawTime is called to draw the new time. We assume that the old time is the one currently displayed. 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. erase the hands erase the second hand erase the minute hand erase the hour hand draw the hands init the face draw the tick marks draw the hands [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] SwapColor swaps the foreground & background colors. [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] ChangeOffset changes the hour offset (red back, blue forward, yellow zeros offset) [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] We keep a list of the created clocks (it is not really needed by anyone, but can help debugging). Mother gives the extra level of frame needed to handle ABORTED properly. This is an attempt to pin the object to avoid faults from a timed-out variable This procedure just loops, repainting the clock as the time changes. We only return when the clock viewer is destroyed. Turn off the clock for good. There should be no further possible timeouts on the condition variable 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. Start up one clock. Κ‘˜codešœ ™ Kšœ Οmœ1™KšœF™FKšžœžœžœ˜Kš žœžœžœžœžœ˜!šž˜Kš žœžœ žœžœžœ˜$Kšžœžœžœžœ˜Kšžœ˜Kšžœ˜—Kšœžœ˜Kšžœžœ˜K˜—K˜š‘ œžœžœ˜(KšœM™MKšžœžœžœ˜Kšžœžœžœžœ˜Kšœžœ˜Kšž œ˜K˜—K˜Kšœž œ '˜CK˜š‘ œžœžœ˜(Kšœ8™8Kšžœžœžœ˜Kšœ žœ˜ Kšž œ˜K˜—K˜šΟb œžœ žœ˜4Kšžœžœžœ˜Kšœžœ=žœ˜Xš žœžœžœžœžœž˜:Kšœ(˜(Kšœžœ˜Kšœžœ˜Kšžœ˜—K˜K˜—š ‘œžœžœžœžœ ˜7Kšœ™Kšžœžœžœ˜šžœž˜Kšžœ˜Kšž˜—K˜—K˜š‘œ˜$KšœF™FKšœQ™QK˜K˜Kšœžœ ˜!šžœžœžœ˜K˜7Kšœžœ ˜Kšœžœ˜Kšœžœ ˜Kšœžœ˜Kšœžœ˜Kšœžœ˜Kš œžœ žœžœ žœ ˜FKšœ žœ˜ Kšœžœ˜#Kšœžœ˜#Kšœžœžœ˜"K˜,Kšœžœžœžœ˜[Kšœžœ˜Kšœ)˜)Kšœžœ˜Kšœžœ-˜EKšœ;˜;K˜š’œžœ˜Kšœ#˜#Kšœ2˜2K˜—K˜š‘œžœ žœžœ˜6KšœŸ™Ÿšœžœ˜Kšžœ žœ  ˜Kšœ žœ#žœ˜>Kšœ žœžœžœ˜9Kšœ žœ ˜Kšœ žœ ˜šœ žœ˜šžœžœžœ˜Kšœ™K˜"šžœ žœ˜Kšœ™Kšœ!žœ˜(—šžœ!žœžœ˜CKšœ™K˜6Kšœ žœ˜Kšœ˜—šžœžœ!žœ˜IKšœ™K˜=Kšœ žœ˜Kšœ˜—šœ˜K˜——Kšœ™K˜"šžœ žœ žœ˜K˜?—Kšžœ žœ7˜Išžœ žœ ˜*Kšœ!žœ˜(—K˜—K˜K˜K˜—š’œžœ˜šžœ žœžœžœ˜EKšœ!˜!Kšœ6˜6Kšœ!˜!Kšœ˜Kšœ!˜!Kšœ#˜#Kšœ˜—Kšœ$˜$Kšžœžœ˜0K˜—K˜š‘ œžœ˜K˜(K˜K˜"K˜šžœžœ˜Kšœ™Kšžœ žœ˜šžœ  ˜&K˜"˜K˜C—K˜!˜K˜——Kšœ™Kš žœžœžœ žœžœ˜1šœ˜K˜——Kšœ™K˜K˜K˜—K˜K˜š žœžœžœžœžœ˜CKšœžœ˜ Kšœžœ˜Kšœ˜—Kšžœžœ žœ˜;Kšžœ žœ˜/Kšœ˜K˜K˜—K˜—K˜š‘ œ˜KšœN™NKšœ3™3Kšœžœ ˜.Kšœžœ˜#K˜šžœ ˜"Kšžœ ˜$Kšžœ!˜%—Kšœžœ˜K˜Kšœ&žœ˜,K˜—K˜š‘œžœ žœ˜(š žœžœžœžœžœž˜:Kšœ˜K˜Kšœ˜Kšœžœ˜K˜Kšœ+žœ˜1Kšžœ˜—K˜K˜—š‘ œ˜ KšœN™NKšœR™RKšœžœ ˜.Kšœžœ˜#K˜šžœ˜ Kšžœ˜šžœ˜Kšœžœ˜šžœ ž˜Kšœ˜Kšœ˜Kšœ˜Kšžœ˜—Kšœžœžœžœ˜IK˜——Kšœžœ˜K˜Kšœ&žœ˜,K˜—K˜š‘ œ˜KšœN™NKšœžœ ˜.Kšœžœ˜#Kšžœžœžœ˜ Kšžœžœžœžœ˜7K˜šœ &˜'Kšœžœ*˜3Kšœ žœ˜Kšœžœžœ˜Kšœžœ2˜