<> <> <> <> <> <<(Corrected destruction of viewer properties when playing log; simplified CarefullyApply)>> <<(Changed reset to close transaction before doing redisplay so that cached pages aren't used -- this gets the proper stuff redisplayed!)>> <<(Added WBError and changed error handling behavior in the code -- now most of it is in CarefullyApply, where it should have been all along)>> <> DIRECTORY Atom, BasicTime, Booting, DB, DBIcons, DBTools, Nut, Process, Rope, ViewerClasses USING [Viewer, ViewerRec], ViewerLocks USING [CallUnderWriteLock], ViewerPrivate USING[ReleaseWriteLock], ViewerOps, ViewerTools, UserCredentials, UserProfile, WhiteboardDB, WhiteboardDBPrivate, WhiteboardDump, WhiteboardLoad, WhiteboardViewers; WhiteboardDBImpl: CEDAR MONITOR IMPORTS BasicTime, Booting, DB, DBIcons, DBTools, Nut, Process, Rope, ViewerLocks, ViewerPrivate, ViewerOps, ViewerTools, WhiteboardDBPrivate, UserCredentials, UserProfile, WhiteboardViewers, WhiteboardDump, WhiteboardLoad EXPORTS WhiteboardDB SHARES ViewerLocks = BEGIN OPEN DB, WhiteboardDBPrivate; Viewer: TYPE = ViewerClasses.Viewer; ROPE: TYPE = Rope.ROPE; WhiteboardLog: TYPE = LIST OF WhiteboardLogEntry; WhiteboardLogEntry: TYPE = RECORD[action: ATOM, to: Viewer]; <> <<$NewIcon>> <<$NewBox>> <<$Delete>> <<$EditText>> <<$Grow>> <<$Move>> <<$Erase>> whiteboard: PUBLIC ATOM _ WhiteboardDBPrivate.wbType; destroyedList: LIST OF Viewer; <> ticksToWait: Process.Ticks _ Process.SecondsToTicks[5*60]; stopped: PUBLIC BOOL _ FALSE; -- true if the Rollback proc has been executed and the transaction is to remain closed readOnly: PUBLIC BOOL _ TRUE; -- default to the current database being read only doingRestart: BOOL _ FALSE; -- true during the restart process, to keep iconic viewers from being opened newWhiteboardDB: ROPE; -- the name of a new whiteboard database if pendingChange (it will be used when the first call to CarefullyApply is made) pendingChange: BOOL _ FALSE; -- this is set by the rollback and credentials change procs to remember a potential change of state; it is checked by CarefullyApply, which will call ResetSchema if it is true activity: BOOLEAN _ TRUE; -- true if a database operation has been performed recently WBError: PUBLIC ERROR[reason: ATOM] = CODE; -- the error that may be raised from this interface WatchDBActivity: PROC[] = { <> WHILE TRUE DO Process.Pause[ticksToWait]; CheckConnection[] ENDLOOP }; CheckConnection: ENTRY PROC[] = { aborted: BOOLEAN _ FALSE; IF stopped THEN RETURN; IF NOT activity THEN CloseTransaction[]; activity _ FALSE }; Close: PUBLIC ENTRY PROC[] = { CloseTransaction[]; DBIcons.Close[]; DBTools.Close[]; stopped _ TRUE }; CloseTransaction: INTERNAL PROC [] = { ENABLE DB.Error, DB.Failure => CONTINUE; trans: DB.Transaction = DB.GetSegmentInfo[$Whiteboard].trans; caughtAborted: BOOL _ FALSE; IF trans # NIL THEN DB.CloseTransaction[trans ! DB.Aborted => { caughtAborted _ TRUE; CONTINUE }]; IF caughtAborted THEN DB.AbortTransaction[trans] }; <> Display: PUBLIC ENTRY PROC[eName: ROPE, v: Viewer _ NIL] RETURNS [viewer: Viewer] = BEGIN ENABLE UNWIND => NULL; DoDisplay: PROC[] = { viewer _ InternalDisplay[eName, v] }; CarefullyApply[DoDisplay] END; InternalDisplay: PROC[eName: ROPE, v: Viewer _ NIL] RETURNS [Viewer] = BEGIN locked: BOOLEAN _ FALSE; wb: Whiteboard = DeclareWhiteboard[eName]; children: ChildSet = Children[wb]; newViewer: BOOL _ FALSE; nextChild: WBItem; child: Viewer; x, y: INT; DisplayNoteEntity: PROC[] = BEGIN text: ViewerTools.TiogaContents = GetContents[nextChild]; w, h: INT; [w, h] _ Size[nextChild]; child _ WhiteboardViewers.AddTextBox[v, x, y, w, h, text] END; DisplayIconEntity: PROC[] = BEGIN label, icon: ROPE; [label, icon] _ GetDisplayProps[nextChild]; child _ WhiteboardViewers.AddIcon[v, label, DBIcons.GetIcon[icon], x, y]; END; DoDisplay: PROC = { versionNumber: INTEGER; locked _ TRUE; -- now we're inside the forbidden region <> IF NOT v.newVersion THEN { v.newVersion _ TRUE; WhiteboardViewers.SetMenu[v]; WhiteboardViewers.SetWBName[v, eName]; }; <> BEGIN gridSize: INT = GetGridSize[wb]; WhiteboardViewers.ResetGrid[v, gridSize] END; <> versionNumber _ GetVersion[wb]; ViewerOps.AddProp[v, $Version, NEW[INTEGER _ versionNumber]]; WHILE (nextChild _ NextChild[children]) # NIL DO [x, y] _ Position[nextChild]; IF TypeOf[nextChild] = note THEN DisplayNoteEntity[] ELSE DisplayIconEntity[]; StoreEntity[child, DB.NameOf[nextChild]]; ENDLOOP; v.newVersion _ FALSE; ViewerOps.PaintViewer[v, all] }; BEGIN ENABLE UNWIND => { IF locked THEN ViewerPrivate.ReleaseWriteLock[v]; IF newViewer THEN ViewerOps.DestroyViewer[v] }; IF v = NIL OR v.class.flavor # WhiteboardViewers.wb THEN { v _ ViewerOps.CreateViewer[flavor: WhiteboardViewers.wb, info: [name: eName, iconic: TRUE]]; newViewer _ TRUE } ELSE v.child _ NIL; -- flushes viewer ViewerLocks.CallUnderWriteLock[DoDisplay, v]; locked _ FALSE; IF NOT doingRestart AND v.iconic AND NOT v.destroyed THEN ViewerOps.OpenIcon[v]; ViewerOps.AddProp[v, $Log, NIL]; -- establish log [] _ Nut.SetFrozenProperty[v, FALSE]; Nut.SetNutInfo[v, $Whiteboard, "Whiteboard", eName]; ViewerOps.AddProp[v, $Lines, NIL]; RETURN[v] END END; Destroy: PUBLIC ENTRY PROC[eName: ROPE] = BEGIN ENABLE UNWIND => NULL; DoDestroy: PROC[] = { wb: Whiteboard = IF Exists[eName] THEN DeclareWhiteboard[eName] ELSE NIL; IF readOnly THEN WBError[$ReadOnly]; IF wb = NIL THEN RETURN ELSE DestroyWhiteboard[wb]; DB.MarkTransaction[DB.GetSegmentInfo[$Whiteboard].trans] }; v: Viewer; CarefullyApply[DoDestroy]; <> v _ ViewerOps.FindViewer[Rope.Cat["Whiteboard: ", eName]]; IF v # NIL THEN ViewerOps.DestroyViewer[v]; END; New: PUBLIC ENTRY PROC[eName: ROPE] RETURNS[success: BOOL] = BEGIN ENABLE UNWIND => NULL; wbEntity: DB.Entity; DoNew: PROC[] = { IF readOnly THEN WBError[$ReadOnly]; wbEntity _ DeclareWhiteboard[eName]; DB.MarkTransaction[DB.GetSegmentInfo[$Whiteboard].trans] }; CarefullyApply[DoNew]; success _ wbEntity # NIL END; Reset: PUBLIC ENTRY PROC[wb: Viewer] = BEGIN ENABLE UNWIND => NULL; wb.newVersion _ TRUE; ViewerOps.PaintViewer[wb, caption]; CloseTransaction[]; InternalReset[wb]; END; InternalReset: INTERNAL PROC[wb: Viewer] = BEGIN DoRedisplay: PROC[] = { name: ROPE = GetWBName[wb]; IF WhiteboardDBPrivate.Exists[name] THEN [] _ InternalDisplay[name, wb] ELSE ViewerOps.DestroyViewer[wb] }; ViewerOps.AddProp[wb, $Log, NIL]; -- Reset Log [] _ Nut.SetFrozenProperty[wb, FALSE]; ViewerOps.AddProp[wb, $Lines, NIL]; CarefullyApply[DoRedisplay]; END; Save: PUBLIC ENTRY PROC[wb: Viewer] = BEGIN ENABLE UNWIND => NULL; eventLog: WhiteboardLog = NARROW[ViewerOps.FetchProp[wb, $Log], WhiteboardLog]; versionNumber: INTEGER = NARROW[ViewerOps.FetchProp[wb, $Version], REF INTEGER]^; DoSave: PROC[] = { IF readOnly THEN WBError[$ReadOnly]; BEGIN wbEntity: Whiteboard = DeclareWhiteboard[GetWBName[wb]]; IF GetVersion[wbEntity] # versionNumber THEN WBError[$WrongVersion]; FOR log: WhiteboardLog _ eventLog, log.rest UNTIL log = NIL DO SELECT log.first.action FROM $NewIcon => DoNewIcon[wbEntity, log.first.to]; $NewBox => DoNewBox[wbEntity, log.first.to]; $Delete => DoDelete[log.first.to]; $EditText => DoEditText[log.first.to]; $Grow => DoGrow[log.first.to]; $Move => DoMove[log.first.to]; $Erase => DoErase[wbEntity]; ENDCASE; ENDLOOP; SetGridSize[wbEntity, WhiteboardViewers.GetGrid[wb]]; SetVersion[wbEntity, versionNumber + 1]; SetCreateDate[wbEntity, BasicTime.Now[]]; DB.MarkTransaction[DB.GetSegmentInfo[$Whiteboard].trans] END }; [] _ wb.class.scroll[wb, thumb, 0]; CarefullyApply[DoSave]; <> DestroyViewerList[]; InternalReset[wb] END; DestroyViewerList: PROC[] = { FOR l: LIST OF Viewer _ destroyedList, l.rest UNTIL l = NIL DO l.first.props _ NIL; l.first.data _ NIL ENDLOOP; destroyedList _ NIL }; GetWBName: PUBLIC PROC[wb: Viewer] RETURNS [name: ROPE] = BEGIN domain: ROPE; [domain ~ domain, entity ~ name] _ Nut.GetNutInfo[wb]; IF Rope.Equal[domain, "Whiteboard"] THEN RETURN; RETURN[NIL] END; WBExists: PUBLIC ENTRY PROC[eName: ROPE] RETURNS [alreadyExists: BOOL] = BEGIN ENABLE UNWIND => NULL; DoCheck: PROC[] = {alreadyExists _ Exists[eName]}; CarefullyApply[DoCheck]; END; GetCreateDate: PUBLIC ENTRY PROC[eName: ROPE] RETURNS [date: BasicTime.GMT] = BEGIN ENABLE UNWIND => NULL; DoGet: PROC[] = { wb: Whiteboard = DeclareWhiteboard[eName]; date _ WhiteboardDBPrivate.GetCreateDate[wb]}; CarefullyApply[DoGet]; END; CopyWB: PUBLIC ENTRY PROC[from, to: ROPE] = BEGIN ENABLE UNWIND => NULL; DoCopy: INTERNAL PROC[] = { IF readOnly THEN WBError[$ReadOnly]; BEGIN fromWB: Whiteboard = DeclareWhiteboard[from]; toWB: Whiteboard = DeclareWhiteboard[to]; children: ChildSet = Children[fromWB]; FOR child: WBItem _ NextChild[children], NextChild[children] UNTIL child = NIL DO IF TypeOf[child] = note THEN CopyNote[toWB, child] ELSE CopyIcon[toWB, child] ENDLOOP; <> DB.MarkTransaction[DB.GetSegmentInfo[$Whiteboard].trans] END }; CarefullyApply[DoCopy] END; CopyNote: INTERNAL PROC[to: Whiteboard, note: WBNote] = BEGIN text: ViewerTools.TiogaContents = GetContents[note]; x, y: INT; w, h: INT; [x, y] _ Position[note]; [w, h] _ Size[note]; [] _ NewNote[to, x, y, w, h, text] END; CopyIcon: INTERNAL PROC[to: Whiteboard, icon: WBIcon] = BEGIN x, y: INT; name, label, iconName, argument: ROPE; type: ATOM; [x, y] _ Position[icon]; [label, iconName] _ GetDisplayProps[icon]; [name, type] _ WhiteboardDBPrivate.GetIconProps[icon]; argument _ WhiteboardDBPrivate.GetToolArgument[icon]; [] _ WhiteboardDBPrivate.NewIcon[to, x, y, name, label, iconName, argument, type] END; GetChildren: PUBLIC ENTRY PROC[eName: ROPE _ NIL] RETURNS [wbList: LIST OF ROPE] = BEGIN ENABLE UNWIND => NULL; EnumWBs: PROC[] = { entityList: LIST OF WBItem; children: ChildSet = Children[DeclareWhiteboard[eName], TRUE]; FOR child: WBItem _ NextChild[children], NextChild[children] UNTIL child = NIL DO entityList _ CONS[child, entityList]; ENDLOOP; FOR eL: LIST OF Whiteboard _ entityList, eL.rest UNTIL eL = NIL DO wbList _ CONS[WhiteboardDBPrivate.GetIconProps[eL.first].name, wbList]; ENDLOOP }; CarefullyApply[EnumWBs] END; Enumerate: PUBLIC ENTRY PROC[pattern: ROPE _ NIL] RETURNS [wbList: LIST OF ROPE] = BEGIN ENABLE UNWIND => NULL; EnumWBs: PROC[] = { entityList: LIST OF Whiteboard = WhiteboardDBPrivate.Enumerate[pattern]; FOR eL: LIST OF Whiteboard _ entityList, eL.rest UNTIL eL = NIL DO wbList _ CONS[DB.NameOf[eL.first], wbList]; ENDLOOP }; CarefullyApply[EnumWBs] END; Dump: PUBLIC ENTRY PROC[to: ROPE] = { ENABLE UNWIND => NULL; DoDump: INTERNAL PROC[] = {WhiteboardDump.DumpToFile[to] }; CarefullyApply[DoDump] }; Load: PUBLIC ENTRY PROC[from: ROPE] = { ENABLE UNWIND => NULL; DoLoad: INTERNAL PROC[] = { IF readOnly THEN WBError[$ReadOnly]; WhiteboardLoad.LoadFromFile[from] }; CarefullyApply[DoLoad] }; <> NewIcon: PUBLIC ENTRY PROC[wb: Viewer, x, y: INT, name: ROPE, type: ATOM, icon: ROPE, label: ROPE _ NIL, argument: ROPE _ NIL] RETURNS [new: Viewer] = BEGIN ENABLE UNWIND => NULL; new _ WhiteboardViewers.AddIcon[wb, IF label = NIL THEN name ELSE label, DBIcons.GetIcon[icon], x, y]; ViewerOps.AddProp[new, $EntityName, name]; ViewerOps.AddProp[new, $IconLabel, label]; ViewerOps.AddProp[new, $IconType, type]; ViewerOps.AddProp[new, $IconIcon, icon]; ViewerOps.AddProp[new, $IconArgument, argument]; LogAction[wb, $NewIcon, new]; ViewerOps.PaintViewer[new, all]; ViewerOps.PaintViewer[wb, caption]; END; NewBox: PUBLIC ENTRY PROC[wb: Viewer, x, y, w, h: INT, contents: ViewerTools.TiogaContents _ NIL] RETURNS [new: Viewer] = BEGIN ENABLE UNWIND => NULL; new _ WhiteboardViewers.AddTextBox[wb, x, y, w, h, contents]; LogAction[wb, $NewBox, new]; ViewerOps.PaintViewer[new, all]; ViewerOps.PaintViewer[wb, caption]; END; Delete: PUBLIC ENTRY PROC[child: Viewer] = BEGIN ENABLE UNWIND => NULL; logIt: BOOL = NOT WhiteboardViewers.DontLog[child]; props: Atom.PropList = child.props; data: REF ANY = child.data; <> IF logIt THEN LogAction[child.parent, $Delete, child]; ViewerOps.DestroyViewer[viewer: child, paint: FALSE]; IF logIt THEN {child.props _ props; child.data _ data} END; EditText: PUBLIC ENTRY PROC[box: Viewer] = BEGIN ENABLE UNWIND => NULL; LogAction[box.parent, $EditText, box]; END; Grow: PUBLIC ENTRY PROC[box: Viewer] = BEGIN ENABLE UNWIND => NULL; LogAction[box.parent, $Grow, box]; END; Move: PUBLIC ENTRY PROC[child: Viewer] = BEGIN ENABLE UNWIND => NULL; IF NOT WhiteboardViewers.DontLog[child] THEN LogAction[child.parent, $Move, child]; END; Erase: PUBLIC ENTRY PROC[wb: Viewer] = BEGIN ENABLE UNWIND => NULL; LogAction[wb, $Erase, wb]; wb.child _ NIL; -- flushes viewer ViewerOps.PaintViewer[wb, client]; END; LogAction: PROC[wb: Viewer, action: ATOM, v: Viewer] = BEGIN log: WhiteboardLog _ NARROW[ViewerOps.FetchProp[wb, $Log], WhiteboardLog]; AddToEnd: PROC[log: WhiteboardLog, entry: WhiteboardLogEntry] RETURNS[WhiteboardLog] = { IF log = NIL THEN RETURN[LIST[entry]] ELSE RETURN[CONS[log.first, AddToEnd[log.rest, entry]]] }; log _ AddToEnd[log, [action, v]]; ViewerOps.AddProp[wb, $Log, log]; END; <> DoNewIcon: PROC[wb: Whiteboard, child: Viewer] = <> BEGIN name: ROPE = NARROW[ViewerOps.FetchProp[child, $EntityName], ROPE]; label: ROPE = NARROW[ViewerOps.FetchProp[child, $IconLabel], ROPE]; type: ATOM = NARROW[ViewerOps.FetchProp[child, $IconType], ATOM]; icon: ROPE = NARROW[ViewerOps.FetchProp[child, $IconIcon], ROPE]; argument: ROPE = NARROW[ViewerOps.FetchProp[child, $IconArgument], ROPE]; newIcon: WBItem = WhiteboardDBPrivate.NewIcon[wb, child.wx, child.wy, name, label, icon, argument, type]; StoreEntity[child, DB.NameOf[newIcon]] -- in case subsequent operations in the log reference this viewer (eg., to move it) END; DoNewBox: PROC[wb: Whiteboard, box: Viewer] = <> BEGIN text: ViewerTools.TiogaContents = ViewerTools.GetTiogaContents[box]; newNote: WBItem = WhiteboardDBPrivate.NewNote[wb, box.wx, box.wy, box.ww, box.wh, text]; StoreEntity[box, DB.NameOf[newNote]] -- in case subsequent operations in the log reference this viewer (eg., to move it) END; DoDelete: PROC[child: Viewer] = <> BEGIN entity: WBItem = FetchEntity[child]; WhiteboardDBPrivate.Destroy[entity]; <> destroyedList _ CONS[child, destroyedList]; END; DoEditText: PROC[box: Viewer] = <> BEGIN note: WBNote = FetchEntity[box]; text: ViewerTools.TiogaContents = ViewerTools.GetTiogaContents[box]; SetContents[note, text] END; DoGrow: PROC[box: Viewer] = <> BEGIN note: WBNote = FetchEntity[box]; WhiteboardDBPrivate.Grow[note, box.ww, box.wh] END; DoMove: PROC[child: Viewer] = <> BEGIN entity: WBItem = FetchEntity[child]; WhiteboardDBPrivate.Move[entity, child.wx, child.wy] END; DoErase: PROC[wb: Whiteboard] = <> BEGIN name: ROPE = NameOf[wb]; WhiteboardDBPrivate.DestroyWhiteboard[wb]; [] _ DeclareWhiteboard[name] END; <> FetchEntity: PUBLIC PROC[v: Viewer] RETURNS[e: DB.Entity] = { name: ROPE = NARROW[ViewerOps.FetchProp[v, $Entity], ROPE]; IF name = NIL THEN RETURN[NIL]; SELECT v.class.flavor FROM WhiteboardViewers.text => e _ FetchWBItem[name, note]; WhiteboardViewers.icon => e _ FetchWBItem[name, icon]; ENDCASE => e _ NIL }; StoreEntity: PUBLIC PROC[v: Viewer, name: ROPE] = { ViewerOps.AddProp[v, $Entity, name ] }; <> GetIconProps: PUBLIC ENTRY PROC[wbIcon: Viewer] RETURNS[ name: ROPE, type: ATOM ] = { ENABLE UNWIND => NULL; DoGet: PROC[] = { iconEntity: WBIcon = FetchEntity[wbIcon]; [name, type] _ WhiteboardDBPrivate.GetIconProps[iconEntity] }; name _ NARROW[ViewerOps.FetchProp[wbIcon, $EntityName]]; IF name # NIL THEN type _ NARROW[ViewerOps.FetchProp[wbIcon, $IconType]] ELSE CarefullyApply[DoGet] }; GetToolArgument: PUBLIC ENTRY PROC[wbIcon: Viewer] RETURNS[ argument: ROPE] = { ENABLE UNWIND => NULL; DoGet: PROC[] = { iconEntity: WBIcon = FetchEntity[wbIcon]; <> IF iconEntity = NIL THEN {argument _ NIL; RETURN}; argument _ WhiteboardDBPrivate.GetToolArgument[iconEntity] }; argument _ NARROW[ViewerOps.FetchProp[wbIcon, $IconArgument]]; IF argument = NIL THEN CarefullyApply[DoGet]; ViewerOps.AddProp[wbIcon, $IconArgument, argument] }; <> EstablishWhiteboardDB: PUBLIC ENTRY PROC[DBFile: ROPE _ NIL] = { <> <> ENABLE UNWIND => NULL; IF DBFile = NIL THEN IF WhiteboardDBPrivate.WBSegment # NIL THEN newWhiteboardDB _ WhiteboardDBPrivate.WBSegment ELSE newWhiteboardDB _ UserProfile.Token[key: "Whiteboard.Segment", default: "[Luther.Alpine]Whiteboard.segment"] ELSE newWhiteboardDB _ DBFile; stopped _ FALSE; pendingChange _ TRUE; InternalResetViewers[newWhiteboardDB # WhiteboardDBPrivate.WBSegment] }; InternalResetViewers: INTERNAL PROC[destroy: BOOL] = { IF destroy THEN { DestroyProc: ViewerOps.EnumProc = { IF v.class.flavor = $Whiteboard THEN ViewerOps.DestroyViewer[v]; RETURN[TRUE] }; aborted: BOOL _ FALSE; <> CloseTransaction[]; <> ViewerOps.EnumerateViewers[enum: DestroyProc] } ELSE { ResetProc: INTERNAL ViewerOps.EnumProc = { <> IF v.class.flavor = $Whiteboard AND NOT v.newVersion AND NOT v.destroyed THEN InternalReset[v]; RETURN[TRUE] }; doingRestart _ TRUE; ViewerOps.EnumerateViewers[enum: ResetProc]; <> doingRestart _ FALSE } }; CarefullyApply: INTERNAL PROC [proc: PROC[]] ~ { ENABLE BEGIN DB.Error => ERROR WBError[$InternalError]; DB.Failure => ERROR WBError[$ServerDown]; DB.Aborted => ERROR WBError[$TransactionAbort]; END; aborted: BOOL _ FALSE; IF stopped THEN RETURN; -- don't bother trying to do the operation activity _ TRUE; IF pendingChange THEN { pendingChange _ FALSE; Initialize[newWhiteboardDB]; readOnly _ DB.GetSegmentInfo[whiteboard].readOnly }; BEGIN ENABLE DB.Aborted => { aborted _ TRUE; CONTINUE }; [] _ WhiteboardDBPrivate.ResetSchema[]; proc[] END; IF NOT aborted THEN RETURN; -- no aborted occurred DB.AbortTransaction[DB.GetSegmentInfo[$Whiteboard].trans]; WhiteboardDBPrivate.ResetSchema[]; proc[]; -- don't bother trying to restart here -- }; CloseWB: ENTRY Booting.CheckpointProc = { ENABLE UNWIND => NULL; CloseTransaction[]; stopped _ TRUE }; OpenUp: Booting.RollbackProc = { DB.Initialize[nCachePages: 256] }; NewUserReset: ENTRY UserCredentials.CredentialsChangeProc = { ENABLE UNWIND => NULL; DestroyProc: ViewerOps.EnumProc = { IF v.class.flavor = $Whiteboard THEN ViewerOps.DestroyViewer[v]; RETURN[TRUE] }; <> CloseTransaction[]; <> ViewerOps.EnumerateViewers[enum: DestroyProc] }; ProfileChangeReset: ENTRY UserProfile.ProfileChangedProc = { ENABLE UNWIND => NULL; DestroyProc: ViewerOps.EnumProc = { IF v.class.flavor = $Whiteboard THEN ViewerOps.DestroyViewer[v]; RETURN[TRUE] }; SELECT reason FROM firstTime, rollBack => { newDB: ROPE = UserProfile.Token[key: "Whiteboard.Segment", default: "[Luther.Alpine]Whiteboard.segment"]; IF NOT Rope.Equal[WhiteboardDBPrivate.WBSegment, newDB] THEN { newWhiteboardDB _ newDB; pendingChange _ TRUE } }; -- don't open up the new database yet; simply remember that it must be done rollBack => RETURN; edit => { -- Only do something if the Whiteboard segment has changed newWhiteboardDB _ UserProfile.Token[key: "Whiteboard.Segment", default: "[Luther.Alpine]Whiteboard.segment"]; IF NOT Rope.Equal[newWhiteboardDB, WhiteboardDBPrivate.WBSegment] THEN { pendingChange _ TRUE; InternalResetViewers[TRUE] } } ENDCASE }; <> TRUSTED { [] _ EstablishWhiteboardDB[]; Process.Detach[FORK WatchDBActivity[]]; Booting.RegisterProcs[c: CloseWB, r: OpenUp]; UserCredentials.RegisterForChange[NewUserReset]; UserProfile.CallWhenProfileChanges[ProfileChangeReset] } END...