DIRECTORY BasicTime USING [GMT, Now, 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]; Clock: CEDAR MONITOR IMPORTS BasicTime, Convert, Graphics, Menus, Process, RealFns, Rope, ViewerOps = BEGIN OPEN Rope; defaultTime: BasicTime.GMT = BasicTime.Now[]; defaultUntime: BasicTime.Unpacked = BasicTime.Unpack[defaultTime]; 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, dateEntry: ROPE _ NIL, dateTime: BasicTime.Unpacked _ defaultUntime, time: BasicTime.Unpacked _ defaultUntime, packed: BasicTime.GMT _ defaultTime, foreground: Graphics.Color _ Graphics.black, 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] = { 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; }; WaitForListChange: ENTRY PROC [old: LIST OF MyData] = { ENABLE UNWIND => NULL; WHILE clockList = old DO WAIT clockListChange; ENDLOOP; }; PaintMe: ViewerClasses.PaintProc = TRUSTED { 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; offset: INT _ data.offset; curPacked: BasicTime.GMT _ BasicTime.Update[BasicTime.Now[], offset]; curTime: BasicTime.Unpacked _ BasicTime.Unpack[curPacked]; TickMark: PROC [seconds: REAL, d: REAL _ 1.0 / 32] = TRUSTED { 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 { mark: Graphics.Mark _ Graphics.Save[ctx]; Graphics.SetColor[ctx, background]; Graphics.MoveTo[path, 1.0, 0.0]; FOR i: NAT IN [1..60] DO 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 { Handy: PROC [seconds, length, width: REAL, invert: BOOL _ FALSE] = TRUSTED { degrees: REAL _ -6.0 * seconds; localMark: Graphics.Mark _ Graphics.Save[ctx]; closed: BOOL _ FALSE; IF invert THEN { Graphics.SetColor[ctx, Graphics.black]; [] _ Graphics.SetPaintMode[ctx, invert]}; Graphics.Rotate[ctx, degrees]; Graphics.MoveTo[path, 0.0, 0.0]; Graphics.LineTo[path, 0.0, length]; IF width > 0 THEN { 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 { Graphics.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}}; Graphics.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]; 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 { 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]}; FOR i: NAT IN [0..12) DO TickMark[i * 5] ENDLOOP}; DrawTime[oldTime, curTime]; ExitPaint[data]} }; SwapColor: Menus.MenuProc = { 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 = { 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]; 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; viewerClass: ViewerClasses.ViewerClass _ NIL; Mother: PROC [iconicFlag: BOOL _ TRUE] = TRUSTED { 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 { 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; AddMeToList[data]; RefreshDate[viewer, NIL]; WHILE data.live AND NOT viewer.destroyed DO newPacked: BasicTime.GMT _ 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; }; Rest[data] ENDLOOP }; pause: CARDINAL _ 200; -- in milliseconds 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: PROC [n: INT _ 1, iconic: BOOL _ TRUE] = { FOR i: INT IN [1..n] DO Start1[iconic] ENDLOOP }; Start1[] -- the default is to start 1 iconic clock END. ¤Clock.mesa Russ Atkinson, December 14, 1983 2:56 pm McGregor, 18-Jan-82 16:03:16 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. A MyData object is used to retain state used in updating the clock. EnterPaint claims the paint lock, returns FALSE 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: Graphics.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. force inversion of the hands rotate to proper angle enter the path not a simple line, but a triangle 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 procedure just loops, repainting the clock as the time changes. We only return when the clock viewer is destroyed. 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. Start up n clocks, iconic by default. ʘJ˜šÐblœ™ Jšœ(™(Jšœ™—J˜JšœÞ™ÞJ˜Jšœ»™»J˜J˜šÏk ˜ šœ ž˜Jšœžœ*˜.—šœž˜ Jšœ˜—šœ žœ˜J˜È—šœžœ˜ J˜d—šœž˜ Jšœ#˜#—šœž˜ Jšœ˜—šœž˜ Jšœ žœ˜—šœž˜Jšœ2˜2—šœ ž˜Jšœ:˜:—J˜—J˜šœžœž˜JšžœG˜NJšœžœžœ˜—J˜Jšœžœ˜.JšœC˜CJ˜šœžœžœ ˜JšœC™C—šœ žœžœ˜Jšœžœ˜#Jšœžœžœ˜Jšœžœžœ˜Jšœ žœžœ˜Jšœ žœžœ˜Jšœ žœžœ˜Jšœ žœžœ˜Jšœ-˜-Jšœ)˜)Jšœžœ˜$Jšœ,˜,Jšœžœ˜Jšœžœ˜Jšœ žœ˜Jšœ žœ˜Jšœž œ˜J˜—š Ïn œžœžœžœžœ˜>JšœG™GJšžœžœžœ˜Jš žœžœžœžœžœ˜!šž˜Jš žœžœ žœžœžœ˜$Jšžœžœžœžœ˜Jšžœ˜Jšžœ˜—Jšœžœ˜Jšžœžœ˜J˜—J˜šŸ œžœžœ˜(JšœM™MJšžœžœžœ˜Jšžœžœžœžœ˜Jšœžœ˜Jšž œ˜J˜—J˜Jšœž œÏc'˜CJ˜šŸ œžœžœ˜(Jšœ8™8Jšžœžœžœ˜Jšœ žœ˜ Jšž œ˜J˜—J˜š Ÿœžœžœžœžœ ˜7Jšœ™Jšžœžœžœ˜šžœž˜Jšžœ˜Jšž˜—J˜—J˜šŸœžœ˜,JšœH™HJšœQ™QJ˜J˜!Jšœžœ ˜!šžœžœžœ˜Jšœ- ˜EJšœžœ ˜Jšœžœ ˜Jšœžœ ˜Jšœžœ ˜Jšœžœ˜#Jšœžœ˜#Jš œžœžœžœžœ˜7J˜.˜Jšžœžœžœ˜H—Jšœžœ˜Jšœ)˜)Jšœžœ˜Jšœžœ-˜EJšœ;˜;J˜š Ÿœžœ žœžœžœ˜>JšœŸ™ŸJšœ* *˜TJšžœ žœ  ˜=Jšœ% !˜FJšœ( ˜DJšœ) ˜DJšœ) ˜AJšœ $˜AJ˜J˜—šŸœžœžœ˜Jšœ-™-J˜)J˜$J˜!šžœžœžœ žœ˜Jšœ™Jšœžœ˜J˜?Jšžœ˜ —J˜J˜J˜J˜—šŸœžœ*žœ˜AJšœe™eJ˜š Ÿœžœžœ žœžœžœ˜LJšœ¼™¼J˜Jšœ žœ˜ J˜.Jšœžœžœ˜J˜šžœžœ˜Jšœ™J˜'˜*J˜——Jšœ™J˜J˜Jšœ™J˜!J˜$šžœ žœ˜Jšœ!™!Jšœ žœ˜J˜'J˜&˜%J˜——šžœ žœ žœžœ ˜1Jšžœ˜"Jšžœ5˜9—J˜ J˜J˜—J˜*Jšœ žœ#žœ˜>Jšœ žœ#žœ˜>Jšœ žœžœžœ˜9Jšœ žœ ˜Jšœ žœ ˜J˜šžœžœžœ˜Jšœ™J˜$šžœ žœ˜Jšœ™Jšœ!žœ˜(—šžœ!žœžœ˜CJšœ™J˜6Jšœ žœ˜—šžœžœ!žœ˜IJšœ™J˜=šœ žœ˜J˜———Jšœ™J˜$šžœ žœ žœ˜J˜?—Jšžœ žœ7˜Išžœ žœ ˜*Jšœ!žœ˜(—J˜J˜J˜—J˜ J˜šžœž˜ Jšžœ(˜,Jšžœ˜—J˜'J˜%J˜$Jšœžœ˜"J˜J˜J˜š žœžœžœžœžœ˜CJšœžœ˜ Jšœžœ˜J˜—šžœžœ˜Jšœ™šžœ ˜Jšžœ˜ šžœ  ˜&J˜$˜J˜C—˜%J˜———Jšœ™šžœžœžœ žœ˜J˜šžœ˜ J˜———Jšœ™J˜J˜—J˜—J˜šŸ œ˜JšœN™NJšœ3™3Jšœžœ ˜.Jšœžœ˜#J˜šžœ"˜$Jšžœ"˜&Jšžœ#˜'—Jšœžœ˜J˜Jšœ&žœ˜,J˜—J˜šŸœžœ žœ˜(š žœžœžœžœžœž˜:Jšœ˜J˜Jšœ˜Jšœžœ˜J˜Jšœ+žœ˜1Jšžœ˜—J˜J˜—šŸ œ˜ JšœN™NJšœR™RJšœžœ ˜.Jšœžœ˜#J˜šžœ˜ Jšžœ˜šžœ˜Jšœžœ˜šžœ ž˜Jšœ˜Jšœ˜Jšœ˜Jšžœ˜—Jšœžœžœžœ˜IJ˜——Jšœžœ˜J˜Jšœ&žœ˜,J˜—J˜šŸ œ˜JšœN™NJšœžœ ˜.Jšœžœ˜#Jšžœžœžœ˜ Jšžœžœžœžœ˜7J˜šœ &˜'Jšœžœ*˜3Jšœžœžœ˜Jšœžœ2˜