DIRECTORY Convert USING [ValueToRope], 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], Time USING [Current, defaultTime, Packed, Unpack, Unpacked], ViewerClasses USING [PaintProc, Viewer, ViewerClass, ViewerClassRec], ViewerOps USING [CreateViewer, PaintViewer, RegisterViewerClass, SetMenu]; MyClock: CEDAR MONITOR IMPORTS Convert, Graphics, Menus, Process, RealFns, Rope, ViewerOps, Time = BEGIN OPEN Rope; defaultUntime: Time.Unpacked; 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: Time.Unpacked _ defaultUntime, time: Time.Unpacked _ defaultUntime, packed: Time.Packed _ Time.defaultTime, 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] = { {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: Time.Unpacked _ data.time; curPacked: Time.Packed _ [Time.Current[] + data.offSet]; curTime: Time.Unpacked _ Time.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: Time.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]; IF data.secWidth > 0 THEN {width _ data.secWidth/radius; Graphics.DrawBox[ctx, [-width, 0, width, length]]; Graphics.Restore[ctx, localMark]; RETURN}; 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 -- + hoff -- ) * 5 + oldTime.minute / 12.0, 0.60, 0.03]; needHour _ TRUE}}; 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 {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]; 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 = { 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.ValueToRope[[signed[data.dateTime.day]]]; month: ROPE _ NIL; year: ROPE _ Convert.ValueToRope[[signed[data.dateTime.year - 1900]]]; entry: Menus.MenuEntry _ NIL; SELECT data.dateTime.month FROM 0 => month _ " Jan "; 1 => month _ " Feb "; 2 => month _ " Mar "; 3 => month _ " Apr "; 4 => month _ " May "; 5 => month _ " Jun "; 6 => month _ " Jul "; 7 => month _ " Aug "; 8 => month _ " Sep "; 9 => month _ " Oct "; 10 => month _ " Nov "; 11 => 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: Time.Packed _ Time.Current[]; 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: Time.Packed _ Time.Current[]; IF newPacked # packed THEN { IF viewer.iconic OR NOT data.drawSeconds THEN IF newPacked - packed < 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] = { 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 defaultUntime _ Time.Unpack[Time.defaultTime]; ViewerOps.RegisterViewerClass[$Clock, viewerClass]; }; Process.Detach[FORK Mother[iconicFlag]]; WaitForListChange[old] }; Start: PROC [n: INT _ 1, iconic: BOOL _ TRUE] = { 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.  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. 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). hoff: INTEGER _ data.hourOffset; 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 as a test hack, use a box for the second hand instead of the path stuff 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 offset: red = hours, yellow = minutes, blue = seconds, shift = forwards, noshift = backwards, control = reset. [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] RefreshDate changes the hour offset (red back, blue forward, yellow zeros offset). 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. newPacked: Time.Packed _ IF curPacked # Time.defaultTime THEN curPacked ELSE Time.Current[]; 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. 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 ʘJ˜JšÐblœ™ Jšœ'™'Jšœ™Jšœ ™ J˜JšœÞ™ÞJ˜Jšœ»™»J˜J˜šÏk ˜ šœž˜ Jšœ˜—šœ žœ˜J˜È—šœžœ˜ J˜d—šœž˜ Jšœ#˜#—šœž˜ Jšœ˜—šœž˜ Jšœ žœ˜—šœž˜ Jšœ2˜2—šœž˜Jšœ2˜2—šœ ž˜Jšœ:˜:—J˜—J˜šœžœž˜JšžœB˜IJšœžœžœ˜—J˜JšÏn œ˜J˜šŸœžœžœ ˜JšœC™C—šŸ œžœ˜šžœ˜Jš)œ žœžœžœžœžœ žœžœžœžœžœžœ žœžœ¥Ïcœžœ œžœ žœžœž œ˜ð——J˜š Ÿ œžœžœžœžœ˜>JšœG™Gšœžœžœžœ˜Jš žœžœžœžœžœ˜!šž˜Jš žœžœ žœžœžœ˜%Jšžœžœžœžœ˜ Jšžœ˜Jšžœ˜—Jšœžœ˜Jšžœžœ˜—J˜—J˜šŸ œžœžœ˜(JšœM™Mšœžœžœžœ˜Jšžœžœžœžœ˜Jšœžœ˜Jšž œ˜—J˜—J˜JšŸœž œ '˜CJ˜šŸ œžœžœ˜(Jšœ8™8šœžœžœžœ˜Jšœ žœ˜!Jšž œ˜—J˜—J˜š Ÿœžœžœžœžœ ˜7Jšœ™šœžœžœžœ˜šžœžœ˜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˜9Jšœ0˜0Jšœžœ™!J˜š Ÿœžœ žœžœžœ˜>JšœŸ™ŸJšœ* *˜TJšžœ žœ  ˜=Jšœ% !˜FJšœ( ˜DJšœ) ˜DJšœ) ˜AJšœ $˜AJ˜J˜—šŸœžœžœ˜Jšœ-™-J˜)J˜$J˜!šžœžœžœ žœ˜Jšœ™Jšœžœ˜J˜?Jšžœ˜ —J˜J˜J˜J˜—šŸœžœ%žœ˜Jšœ žœ#žœ˜>Jšœ žœžœžœ˜9Jšœ žœ ˜Jšœ žœ ˜J˜šžœžœžœ˜Jšœ™J˜$šžœ žœ˜Jšœ™Jšœ!žœ˜(—šžœ!žœžœ˜CJšœ™J˜6Jšœ žœ˜—šžœžœ!žœ˜IJšœ™J˜Mšœ žœ˜J˜———Jšœ™J˜$šžœ žœ žœ˜J˜O—Jšžœ žœ7˜Išžœ žœ ˜*Jšœ!žœ˜(—J˜J˜J˜—J˜ J˜šžœž˜ Jšžœ(˜,Jšžœ˜—J˜'J˜%J˜$Jšœžœ˜"J˜J˜J˜š žœžœžœžœž˜AJšœ žœžœ˜)J˜—šžœžœ˜Jšœ™šœžœ ˜Jšžœ˜ šžœ  ˜&J˜$˜J˜C—˜%J˜——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šœ‡™‡Jšœžœ ˜.Jšœžœ˜#Jš œ žœžœžœžœ˜)J˜Jšžœ žœ˜šžœ*žœ ž˜FJ˜ J˜ J˜ Jšžœžœ˜—Jšœžœ˜J˜Jšœ&žœ˜,J˜—J˜šŸ œ˜JšœN™NJšœR™RJšœžœ ˜.Jšœžœ˜#Jšžœžœžœ˜ Jšžœžœžœžœ˜7J˜šœ &˜'Jšœžœ4˜=Jšœžœžœ˜Jšœžœ<˜FJšœžœ˜šžœž˜J˜J˜J˜J˜J˜J˜J˜J˜J˜J˜J˜J˜Jšžœ˜—Jšžœžœ˜;J˜%J˜,˜˜AJšœžœ˜——J˜J˜—Jšœžœ˜J˜Jšœ$žœ˜*J˜—J˜šŸ œžœžœ žœ˜!Jšœa™a—J˜JšŸ œžœ˜-J˜š Ÿœžœžœžœžœ˜2JšœH™HJšœžœ˜$Jšœžœ˜#J˜J˜EJšœ!žœžœ˜7Jšœ ž˜J˜—J˜šŸœžœ:žœžœ˜VJšœx™xJšœ%˜%J˜'˜J˜2—˜J˜9—˜J˜F—˜ ˜J˜X——J˜!J˜J˜Jšœžœ˜šžœ žœžœžœ˜,J˜(Jšœžœžœ žœ™\šžœžœ˜Jšžœžœžœžœ˜.Jšžœžœžœ˜4šžœžœžœ˜Jšœ&žœ ˜4—šžœ#žœ'ž˜RJšœžœ˜—Jšœ g˜|J˜—J˜ Jšž˜—J˜—J˜JšŸœžœ ˜)J˜šŸœžœžœ˜#Jšœ™™™Jšžœžœžœ˜Jšžœ˜J˜—J˜š Ÿœžœžœžœžœ˜2Jšœ™Jšœžœžœ˜ šžœžœžœ˜˜šžœ˜!šœ ˜(Jšœ !˜0Jšœ !˜8———J˜.J˜4J˜—Jšœžœ˜)J˜J˜—J˜š Ÿœžœžœžœžœ˜1Jšœ%™%Jš žœ žœžœ žœžœžœ˜0šžœžœžœžœ˜J˜Jšž˜—J˜—J˜Jšœ  )˜2J˜šžœ˜J˜J˜™-J™ðJšœ ÏrDœ¡n™Ñ—J™J™J™——…—+”H°