<> <> <> <> <> <> <> <> <> <<>> DIRECTORY BasicTime, Buttons USING [Button, ButtonProc, Create, ReLabel], Containers, Convert, IO, Menus USING [AppendMenuEntry, CreateEntry, CreateMenu, Menu, MenuProc], PFS, PFSCanonicalNames USING [ParseName, UnparseName], PFSNames USING [Directory], Remind, RemindEdit, Rope, SimpleFeedback, SystemNames USING [MachineName, UserCedarDir, UserName], TimeParse USING [Adjust, Parse, ParseError], UserProfile USING [Boolean, CallWhenProfileChanges, Number, ProfileChangedProc, Token], VFonts, ViewerClasses, ViewerOps, ViewerSpecs, ViewerTools; RemindEditImpl: CEDAR MONITOR IMPORTS BasicTime, Buttons, Containers, Convert, IO, Menus, PFS, PFSCanonicalNames, PFSNames, Remind, Rope, SimpleFeedback, SystemNames, TimeParse, UserProfile, VFonts, ViewerOps, ViewerSpecs, ViewerTools EXPORTS RemindEdit = { ROPE: TYPE = Rope.ROPE; Viewer: TYPE ~ ViewerClasses.Viewer; Meeting: TYPE = Remind.Meeting; Reminder: TYPE ~ Remind.Reminder; ReminderRecord: TYPE ~ Remind.ReminderRecord; Categories: TYPE = {start, duration, explanation, repeat, more, leadtime, remindduration, mailto}; SingleCategories: TYPE ~ Categories[start..more]; MultiCategories: TYPE ~ Categories[leadtime..mailto]; EditData: TYPE = REF EditDataRec; EditDataRec: TYPE = RECORD [ outerViewer: Viewer ¬ NIL, -- handle for the enclosing container inputs: ARRAY SingleCategories OF InputViewers, -- for user input multis: MultupleList ¬ NIL, privacyButton: Buttons.Button ¬ NIL, ref: REF Viewer, -- kludge because can't modify viewers data oldMtg: Meeting, --already in DB newMtg: Meeting, --being edited seminarViewer: Viewer ¬ NIL, -- Used for seminar meetings to pass in the viewer containing the seminar announcement message to send. name: ROPE, edge: INTEGER ¬ 0, -- wx for next component bot: INTEGER ¬ 0 -- wy for next component ]; InputViewers: TYPE ~ RECORD [ prompt: Buttons.Button, -- prompting button input: Viewer -- Text Box for user input ]; Multuple: TYPE ~ RECORD [ rmd: Reminder, delete: Buttons.Button, iv: ARRAY MultiCategories OF InputViewers]; MultupleList: TYPE ~ LIST OF Multuple; RemindData: TYPE ~ REF RemindDataRecord; RemindDataRecord: TYPE ~ RECORD [editData: EditData, rmd: Reminder]; entryVSpace: CARDINAL = 3; -- vertical leading space between lines entryHeight: CARDINAL = 15; -- how tall to make each line of items botSpace: INTEGER = 1; --extra space at the bottom secsPerDay: INT ~ 60*60*24; secsPerWeek: INT ~ secsPerDay*7; CommandMeetingLength: INT = 15; <> doAlert, doMail: BOOL ¬ TRUE; leadTime: INT ¬ 0; -- value of Remind.RemindLeadTime remindDuration: INT ¬ 10; -- value of Remind.RemindDuration mailLeadTime: INT ¬ 0; -- value of Remind.MailLeadTime mailRecipient: ROPE ¬ "???@???"; -- value of Remind.MailTo seminarMsgFormName: ROPE ¬ "/cedar/forms/SeminarMsg.form"; dfFileDirectory: ROPE ¬ NIL; -- Directory in which the CalendarTool DF file resides. SpecifyMeeting: PUBLIC PROC[proto: Remind.Meeting, existing: BOOL, dbName: ROPE, column: ViewerClasses.Column] = { meeting: Remind.Meeting ¬ proto; editData: EditData ¬ NEW[EditDataRec]; myMenu: Menus.Menu ¬ Menus.CreateMenu[]; nLines: INTEGER; -- number of lines in tool meetingName: ROPE; estWH: INTEGER; IF NOT existing THEN { IF meeting.type = meeting OR meeting.type = seminar THEN { IF doAlert THEN meeting.reminders ¬ CONS[ NEW[ReminderRecord ¬ [NIL, alert[ start: TimeParse.Adjust[baseTime: meeting.start, minutes: -leadTime].time, stop: TimeParse.Adjust[baseTime: meeting.start, minutes: remindDuration-leadTime].time ]]], meeting.reminders]; IF doMail THEN meeting.reminders ¬ CONS[ NEW[ReminderRecord ¬ [NIL, mail[ when: TimeParse.Adjust[baseTime: meeting.start, minutes: -mailLeadTime].time, to: mailRecipient ]]], meeting.reminders]; IF meeting.type = meeting THEN { meetingName ¬ "Create New Meeting"; IF meeting.explanation=NIL THEN meeting.explanation ¬ "Explanation"; } ELSE { meetingName ¬ "Create New Seminar"; IF meeting.explanation=NIL THEN meeting.explanation ¬ "Subject"; }; } ELSE { -- command; protectedCmd can't happen IF doAlert THEN meeting.reminders ¬ CONS[ NEW[ReminderRecord ¬ [NIL, alert[ start: meeting.start, stop: meeting.start ]]], meeting.reminders]; meetingName ¬ "Create New Command"; IF meeting.explanation=NIL THEN meeting.explanation ¬ "Command"; }; } ELSE { meetingName ¬ meeting.explanation; }; nLines ¬ 4 --Start, Explanation(2), Repeat--; IF meeting.type=meeting OR meeting.type=seminar THEN { nLines ¬ nLines+1--Duration--; FOR rl: Remind.RemindList ¬ meeting.reminders, rl.rest WHILE rl#NIL DO nLines ¬ nLines+1; ENDLOOP}; IF meeting.more#NIL AND meeting.type=meeting THEN nLines ¬ nLines + 2; Menus.AppendMenuEntry[ menu: myMenu, entry: Menus.CreateEntry[name: "OK", proc: OKFunction, clientData: editData] ]; Menus.AppendMenuEntry[ menu: myMenu, entry: Menus.CreateEntry[name: "Add Alert", proc: AddAlertFunction, clientData: editData] ]; Menus.AppendMenuEntry[ menu: myMenu, entry: Menus.CreateEntry[name: "Add Mail", proc: AddMailFunction, clientData: editData] ]; editData.oldMtg ¬ IF existing THEN proto ELSE NIL; editData.newMtg ¬ IF existing THEN NEW [Remind.MeetingRec ¬ proto­] ELSE proto; editData.name ¬ dbName; estWH ¬ nLines*(entryHeight + entryVSpace) + botSpace + ViewerSpecs.windowBorderSize*2 + ViewerSpecs.captionHeight + ViewerSpecs.menuHeight + ViewerSpecs.menuBarHeight; editData.outerViewer ¬ Containers.Create[info: [ name: meetingName, data: editData, openHeight: estWH, iconic: FALSE, column: column, menu: myMenu, <> scrollable: FALSE]]; editData.inputs[start].prompt ¬ MakeButton[editData, "Start:"]; editData.inputs[start].input ¬ MakeViewer[editData, Convert.RopeFromTime[from: meeting.start, includeDayOfWeek: TRUE]]; IF meeting.type=meeting OR meeting.type=seminar THEN { editData.inputs[duration].prompt ¬ MakeButton[editData, "Duration:"]; editData.inputs[duration].input ¬ MakeViewer[editData, Convert.RopeFromInt[meeting.duration]]; }; editData.inputs[explanation].prompt ¬ MakeButton[editData, SELECT meeting.type FROM meeting => "Explanation:", command => "Command:", seminar => "Seminar subject:", protectedCmd => "Protected cmd:", ENDCASE => ERROR]; editData.inputs[explanation].input ¬ MakeViewer[editData, meeting.explanation, 2]; editData.privacyButton ¬ MakePrivacyToggle[editData]; editData.inputs[repeat].prompt ¬ MakeButton[editData, "repeat:", editData.edge]; editData.inputs[repeat].input ¬ MakeViewer[editData, Remind.RopeFromRepetition[meeting.repeat]]; IF meeting.more # NIL AND meeting.type=meeting THEN { editData.inputs[more].prompt ¬ MakeButton[editData, "more:"]; editData.inputs[more].input ¬ MakeViewer[editData, meeting.more, 2]; }; IF meeting.type=meeting OR meeting.type=seminar THEN { FOR rl: Remind.RemindList ¬ meeting.reminders, rl.rest WHILE rl#NIL DO AddReminder[editData, rl.first]; ENDLOOP; }; ViewerTools.SetSelection[editData.inputs[explanation].input, NIL]; IF meeting.type=seminar THEN { info: ViewerClasses.ViewerRec; msgViewer: Viewer; ut: BasicTime.Unpacked ¬ BasicTime.Unpack[meeting.start]; secs: CARD; dateRope, dateRope1: ROPE; info.name ¬ "Seminar Msg"; info.iconic ¬ FALSE; info.file ¬ seminarMsgFormName; msgViewer ¬ ViewerTools.MakeNewTextViewer[info]; ViewerOps.SetNewFile[msgViewer]; dateRope ¬ Rope.Cat[Convert.RopeFromCard[ut.year], "-", Convert.RopeFromCard[ORD[ut.month]+1], "-", Convert.RopeFromCard[ut.day]]; dateRope ¬ Rope.Cat[dateRope, "-", Convert.RopeFromCard[ut.hour], "-", Convert.RopeFromCard[ut.minute]]; dateRope1 ¬ Rope.Cat[dateRope, "-", Convert.RopeFromCard[ut.second]]; msgViewer.file ¬ Rope.Cat[dfFileDirectory, dateRope1, ".tioga"]; secs ¬ ut.second; WHILE PFS.FileLookup[PFS.PathFromRope[msgViewer.file], NIL] # NIL DO <> secs ¬ secs + 1; dateRope1 ¬ Rope.Cat[dateRope, "-", Convert.RopeFromCard[secs]]; msgViewer.file ¬ Rope.Cat[dfFileDirectory, dateRope1, ".tioga"]; ENDLOOP; editData.seminarViewer ¬ msgViewer; }; }; MakePrivacyToggle: PROC [editData: EditData, wx: INTEGER ¬ 0] RETURNS [b : Buttons.Button] = { b ¬ Buttons.Create[ info: [ name: "private", wh: entryHeight, wx: wx, wy: editData.bot-1, parent: editData.outerViewer, border: TRUE ], proc: TogglePrivacy, clientData: editData]; IF editData.newMtg.public THEN Buttons.ReLabel[b, "public"]; editData.edge ¬ b.wx + b.ww + 13; RETURN}; TogglePrivacy: PROC [parent: Viewer, clientData: REF ANY ¬ NIL, mouseButton: ViewerClasses.MouseButton ¬ red, shift, control: BOOL ¬ FALSE] = { ed: EditData = NARROW[clientData]; ed.newMtg.public ¬ NOT ed.newMtg.public; Buttons.ReLabel[ed.privacyButton, IF ed.newMtg.public THEN "public" ELSE "private"]; RETURN}; MakeButton: PROC [editData: EditData, name: ROPE, wx: INTEGER ¬ 0] RETURNS [b : Buttons.Button] = { viewRef: REF Viewer ¬ NEW[Viewer]; b ¬ Buttons.Create[ info: [ name: name, <> wh: entryHeight, -- specify rather than defaulting so line is uniform wx: wx, wy: editData.bot-1, parent: editData.outerViewer, border: FALSE ], proc: Prompt, clientData: viewRef]; -- this will be passed to our button proc editData.ref ¬ viewRef; editData.edge ¬ b.wx + b.ww + 3; RETURN}; MakeViewer: PROC [editData: EditData, viewerData: ROPE, nLines: INT ¬ 1, w: INTEGER ¬ INTEGER.LAST] RETURNS [v: Viewer] = { --the previous MakeButton was for this text viewer v ¬ ViewerTools.MakeNewTextViewer[ info: [ parent: editData.outerViewer, wx: editData.edge, wy: editData.bot, ww: MIN[editData.outerViewer.cw - editData.edge, w], wh: entryHeight*nLines + entryVSpace*(nLines-1), data: viewerData, scrollable: nLines>1, border: FALSE]]; IF w=INTEGER.LAST THEN {Containers.ChildXBound[editData.outerViewer, v]; editData.bot ¬ editData.bot + entryVSpace + entryHeight*nLines} ELSE editData.edge ¬ v.wx + v.ww + 6; <> editData.ref­ ¬ v; }; DeleteReminder: Buttons.ButtonProc ~ { rd: RemindData ~ NARROW[clientData]; editData: EditData ~ rd.editData; m: Multuple ~ FindMultuple[editData.multis, rd.rmd]; prevRL: Remind.RemindList ¬ NIL; prevML: MultupleList ¬ NIL; y0: INTEGER ~ m.iv[leadtime].input.wy; dy: INTEGER ~ -(entryVSpace+entryHeight); FixViewer: PROC [v: Viewer] ~ { IF v#NIL AND v.wy>y0 THEN ViewerOps.MoveViewer[v, v.wx, v.wy+dy, v.ww, v.wh, FALSE]; RETURN}; FOR curRL: Remind.RemindList ¬ editData.newMtg.reminders, curRL.rest WHILE curRL#NIL DO IF curRL.first = rd.rmd THEN { IF prevRL#NIL THEN prevRL.rest ¬ curRL.rest ELSE editData.newMtg.reminders ¬ curRL.rest; EXIT}; prevRL ¬ curRL; REPEAT FINISHED => ERROR ENDLOOP; ViewerOps.DestroyViewer[m.delete, FALSE]; FOR i: MultiCategories IN MultiCategories DO IF m.iv[i].prompt#NIL THEN ViewerOps.DestroyViewer[m.iv[i].prompt, FALSE]; IF m.iv[i].input#NIL THEN ViewerOps.DestroyViewer[m.iv[i].input, FALSE]; ENDLOOP; FOR curML: MultupleList ¬ editData.multis, curML.rest WHILE curML#NIL DO IF curML.first.rmd = rd.rmd THEN { IF prevML#NIL THEN prevML.rest ¬ curML.rest ELSE editData.multis ¬ curML.rest; } ELSE { FOR i: MultiCategories IN MultiCategories DO FixViewer[curML.first.iv[i].prompt]; FixViewer[curML.first.iv[i].input]; ENDLOOP; FixViewer[curML.first.delete]}; prevML ¬ curML; ENDLOOP; editData.bot ¬ editData.bot + dy; ViewerOps.SetOpenHeight[editData.outerViewer, editData.bot + botSpace]; ViewerOps.ComputeColumn[editData.outerViewer.column]; ViewerOps.PaintViewer[editData.outerViewer, all]; RETURN}; FindMultuple: PROC [ml: MultupleList, rmd: Reminder] RETURNS [Multuple] ~ { FOR ml ¬ ml, ml.rest WHILE ml#NIL DO IF ml.first.rmd = rmd THEN RETURN [ml.first]; REPEAT FINISHED => ERROR; ENDLOOP; }; Prompt: Buttons.ButtonProc = { <> <> <> <> <> str: ROPE; view: REF Viewer ¬ NARROW[clientData]; -- get our data SELECT mouseButton FROM red => NULL; yellow => { str ¬ ViewerTools.GetSelectionContents[]; ViewerTools.SetContents[view­, str]; }; blue => { ViewerTools.SetContents[view­, NIL]; }; ENDCASE => ERROR; ViewerTools.SetSelection[view­, NIL] }; AddAlertFunction: Menus.MenuProc = { editData: EditData ¬ NARROW[clientData]; -- get our data meeting: Meeting ¬ editData.newMtg; AddNewReminder[editData, NEW[ReminderRecord ¬ [NIL, alert[ start: TimeParse.Adjust[baseTime: meeting.start, minutes: -leadTime].time, stop: TimeParse.Adjust[baseTime: meeting.start, minutes: remindDuration-leadTime].time]]] ]; }; AddMailFunction: Menus.MenuProc = { editData: EditData ¬ NARROW[clientData]; -- get our data meeting: Meeting ¬ editData.newMtg; AddNewReminder[editData, NEW[ReminderRecord ¬ [NIL, mail[ when: TimeParse.Adjust[baseTime: meeting.start, minutes: -mailLeadTime].time, to: mailRecipient]]] ]; }; AddNewReminder: PROC [editData: EditData, rmd: Reminder] ~ { ViewerOps.SetOpenHeight[editData.outerViewer, editData.bot + entryVSpace+entryHeight + botSpace]; ViewerOps.ComputeColumn[editData.outerViewer.column]; editData.newMtg.reminders ¬ CONS[rmd, editData.newMtg.reminders]; AddReminder[editData, rmd]; RETURN}; AddReminder: PROC [editData: EditData, rmd: Reminder] ~ { rd: RemindData ~ NEW [RemindDataRecord ¬ [editData, rmd]]; meeting: Meeting ~ editData.newMtg; mtp: Multuple ¬ [rmd, Buttons.Create[ info: [name: "Delete", wy: editData.bot-1, wh: entryHeight, wx: 0, parent: editData.outerViewer, border: TRUE], proc: DeleteReminder, clientData: rd], ALL[[NIL, NIL]]]; editData.edge ¬ mtp.delete.wx + mtp.delete.ww + 6; WITH rmd SELECT FROM x: Remind.AlertReminder => { mtp.iv[leadtime].prompt ¬ MakeButton[editData, "Alert Lead Time:", editData.edge]; mtp.iv[leadtime].input ¬ MakeViewer[editData, Convert.RopeFromInt[BasicTime.Period[x.start, meeting.start]/60], 1, 11*VFonts.CharWidth['0] ]; mtp.iv[remindduration].prompt ¬ MakeButton[editData, "Duration:", editData.edge]; mtp.iv[remindduration].input ¬ MakeViewer[editData, Convert.RopeFromInt[BasicTime.Period[x.start, x.stop]/60] ]; }; x: Remind.MailReminder => { mtp.iv[leadtime].prompt ¬ MakeButton[editData, "Mail Lead Time:", editData.edge]; mtp.iv[leadtime].input ¬ MakeViewer[editData, Convert.RopeFromInt[BasicTime.Period[x.when, meeting.start]/60], 1, 11*VFonts.CharWidth['0] ]; mtp.iv[mailto].prompt ¬ MakeButton[editData, "Recipient:", editData.edge]; mtp.iv[mailto].input ¬ MakeViewer[editData, x.to]; }; ENDCASE => ERROR; editData.multis ¬ CONS[mtp, editData.multis]; RETURN}; OKFunction: Menus.MenuProc = { <> editData: EditData ¬ NARROW[clientData]; -- get our data meeting: Meeting ¬ editData.newMtg; remindLeadTime, remindDuration: INT; MyGet: PROC[i: Categories, rmd: Reminder ¬ NIL] RETURNS [str: ROPE] = { IF i IN SingleCategories THEN str ¬ ViewerTools.GetContents[editData.inputs[i].input] ELSE { m: Multuple ~ FindMultuple[editData.multis, rmd]; str ¬ ViewerTools.GetContents[m.iv[i].input]; }; IF Rope.IsEmpty[str] THEN RETURN[NIL]; IF Rope.SkipOver[str, 0, " \t\n\r"] = Rope.Length[str] THEN RETURN[NIL]; RETURN}; meeting.start ¬ TimeParse.Parse[MyGet[start], BasicTime.Now[] ! TimeParse.ParseError => { SimpleFeedback.Append[$CalendarTool, oneLiner, $Error, "Can't parse \"Start:\" field"]; SimpleFeedback.Blink[$CalendarTool, $Error]; GOTO done; }].time; meeting.explanation ¬ MyGet[explanation]; meeting.repeat ¬ Remind.RepetitionFromRope[MyGet[repeat]]; IF meeting.type = meeting THEN meeting.more ¬ MyGet[more]; IF meeting.type = meeting OR meeting.type = seminar THEN { meeting.duration ¬ Convert.IntFromRope[MyGet[duration]]; FOR rl: Remind.RemindList ¬ meeting.reminders, rl.rest WHILE rl#NIL DO WITH rl.first SELECT FROM x: Remind.AlertReminder => { remindLeadTime ¬ Convert.IntFromRope[MyGet[leadtime, x]]; remindDuration ¬ Convert.IntFromRope[MyGet[remindduration, x]]; x.start ¬ BasicTime.Update[meeting.start, -60*remindLeadTime]; x.stop ¬ BasicTime.Update[meeting.start, 60*(-remindLeadTime + remindDuration)]; }; x: Remind.MailReminder => { remindLeadTime ¬ Convert.IntFromRope[MyGet[leadtime, x]]; x.when ¬ BasicTime.Update[meeting.start, -60*remindLeadTime]; x.to ¬ MyGet[mailto, x]; }; ENDCASE => ERROR; ENDLOOP; } ELSE { -- command; protectedCmd can't happen. meeting.duration ¬ CommandMeetingLength; meeting.reminders ¬ LIST[NEW[ReminderRecord ¬ [NIL, alert[ start: meeting.start, stop: meeting.start]]] ]; }; IF meeting.type = seminar THEN { <> ScheduleSeminarAnnouncements[meeting, editData.seminarViewer, editData.name, editData.oldMtg#NIL, editData.oldMtg.start]; }; IF editData.oldMtg # NIL THEN [] ¬ Remind.DeleteMeeting[start: editData.oldMtg.start, id: editData.oldMtg.uniqID, closeDb: FALSE, dbName: editData.name]; [] ¬ Remind.AddMeeting[meeting: meeting, dbName: editData.name]; ViewerOps.DestroyViewer[editData.outerViewer]; EXITS done => NULL; }; ScheduleSeminarAnnouncements: PROC [meeting: Meeting, seminarViewer: Viewer, dbName: ROPE, alreadyExists: BOOL, oldStart: BasicTime.GMT ¬ BasicTime.nullGMT] = <> BEGIN semMsgContents: ViewerTools.TiogaContents ¬ ViewerTools.GetTiogaContents[seminarViewer]; cmd1: Meeting ¬ NEW [Remind.MeetingRec]; cmd2: Meeting ¬ NEW [Remind.MeetingRec]; cmd1ID, cmd2ID: INT; msgFileName: ROPE ¬ seminarViewer.file; cmd1.start ¬ BasicTime.Update[meeting.start, -secsPerWeek]; <> IF BasicTime.Period[cmd1.start, BasicTime.Now[]] > 0 THEN { <> cmd1.start ¬ BasicTime.Update[BasicTime.Now[], 60]; }; cmd1.type ¬ protectedCmd; cmd1.explanation ¬ IO.PutFR1["Open %g", IO.rope[msgFileName]]; cmd1.repeat ¬ meeting.repeat; cmd1.duration ¬ CommandMeetingLength; cmd1.reminders ¬ LIST[NEW[ReminderRecord ¬ [NIL, alert[ start: cmd1.start, stop: cmd1.start]]] ]; cmd2­ ¬ cmd1­; cmd2.start ¬ BasicTime.Update[meeting.start, -secsPerDay]; <> cmd2.reminders ¬ LIST[NEW[ReminderRecord ¬ [NIL, alert[ start: cmd2.start, stop: cmd2.start]]] ]; [] ¬ ViewerOps.SaveViewer[seminarViewer]; ViewerOps.DestroyViewer[seminarViewer]; IF alreadyExists THEN { from: BasicTime.GMT ~ oldStart.Update[-secsPerWeek*2]; ids: IO.STREAM ¬ IO.RIS[meeting.more]; uniqID1: INT ¬ IO.GetInt[ids]; uniqID2: INT ¬ IO.GetInt[ids]; cmd1ID ¬ Remind.AddMeeting[meeting: cmd1, closeDb: FALSE, dbName: dbName]; [] ¬ Remind.DeleteMeetingInRange[from: from, to: oldStart, id: uniqID1, dbName: dbName]; cmd2ID ¬ Remind.AddMeeting[meeting: cmd2, closeDb: FALSE, dbName: dbName]; [] ¬ Remind.DeleteMeeting[start: oldStart.Update[-secsPerDay], id: uniqID2, dbName: dbName]; } ELSE { cmd1ID ¬ Remind.AddMeeting[meeting: cmd1, dbName: dbName]; cmd2ID ¬ Remind.AddMeeting[meeting: cmd2, dbName: dbName]; }; meeting.more ¬ IO.PutFR["%g %g %g", IO.int[cmd1ID], IO.int[cmd2ID], IO.rope[msgFileName]]; END; CacheProfileData: ENTRY UserProfile.ProfileChangedProc = { <> ENABLE UNWIND => NULL; dbName: ROPE ¬ Remind.GetDefaultDbName[]; flatdbName: ROPE ¬ Rope.Flatten[dbName]; doAlert ¬ UserProfile.Boolean["Remind.Alert", TRUE]; leadTime ¬ UserProfile.Number["Remind.AlertLeadTime", UserProfile.Number["Remind.RemindLeadTime", 20]]; remindDuration ¬ UserProfile.Number["Remind.AlertDuration", UserProfile.Number["Remind.RemindDuration", 20]]; doMail ¬ UserProfile.Boolean["Remind.Mail", TRUE]; mailLeadTime ¬ UserProfile.Number["Remind.MailLeadTime", 30]; mailRecipient ¬ UserProfile.Token["Remind.MailTo", SystemNames.UserName[].Cat["@", SystemNames.MachineName[]] ]; seminarMsgFormName ¬ UserProfile.Token["CalendarTool.SeminarMsgForm", seminarMsgFormName]; IF Rope.IsEmpty[dbName] THEN SimpleFeedback.Append[$Remind, oneLiner, $Warning, "No GetDefaultDbName in RemindEditImpl\n"] ELSE { IF dbName.Find["@"] >= 0 THEN dfFileDirectory ¬ SystemNames.UserCedarDir["appts"] ELSE dfFileDirectory ¬ PFSCanonicalNames.UnparseName[PFSNames.Directory[PFSCanonicalNames.ParseName[dbName]]]; }; }; UserProfile.CallWhenProfileChanges[CacheProfileData]; }.