<> <> <> <<>> <> <> DIRECTORY DefaultRemoteNames USING [Get], FS USING [Error, defaultStreamOptions, StreamOptions, StreamOpen], GVAnswer USING [MakeHeader], GVMailParse, InputFocus USING [GetInputFocus], Menus, IO, Process USING [Detach], Rope, RopeList USING [Append], RuntimeError USING [BoundsFault], TiogaOps USING [ Pattern, Ref, Location, ViewerDoc, CallWithLocks, CancelSelection, CaretBefore, CreateSimplePattern, Delete, FindWord, FindText, GetSelection, InsertRope, NextPlaceholder, SelectDocument, SelectPoint, SelectionRoot, SelectionSearch, SetSelection, SetSelectionLooks, ToPrimary], ViewerOps, ViewerClasses, ViewerEvents USING [EventProc, RegisterEventProc, UnRegisterEventProc], ViewerTools, WalnutDocumentRope, WalnutParseMsg, WalnutSendInternal, WalnutSendOps, WalnutSendOpsExtras; WalnutSendOpsImpl: CEDAR MONITOR IMPORTS DefaultRemoteNames, FS, IO, Process, Rope, RopeList, RuntimeError, GVAnswer, GVMailParse, InputFocus, Menus, TiogaOps, ViewerEvents, ViewerOps, ViewerTools, WalnutDocumentRope, WalnutSendInternal, WalnutSendOps, WalnutSendOpsExtras EXPORTS WalnutSendInternal, WalnutSendOps, WalnutSendOpsExtras, WalnutParseMsg SHARES Menus = BEGIN OPEN WalnutSendOps, WalnutSendInternal, WalnutParseMsg; <<************************************************************************>> ROPE: TYPE = Rope.ROPE; Viewer: TYPE = ViewerClasses.Viewer; STREAM: TYPE = IO.STREAM; TiogaContents: TYPE = ViewerTools.TiogaContents; viewerStart: ViewerTools.SelPos = NEW[ViewerTools.SelPosRec _ [0, 0]]; nullIndex: INT = LAST[INT]; defaultRegistry: PUBLIC ROPE _ DefaultRemoteNames.Get[].registry; messageParseArray: PUBLIC ARRAY MessageFieldIndex OF MessageInfo _ [ ["Reply-to", simpleRope], -- this is really wrong, a special case for now ["Sender", simpleRope], ["From", simpleRope], ["To", rNameList], ["cc", rNameList], ["c", rNameList], ["bcc", rNameList], ["Date", simpleRope], ["Subject", simpleRope], ["Categories", rCatList], ["In-reply-to", simpleRope], ["VoiceFileID", simpleRope] ]; Answer: PUBLIC PROC[msgHeaders: ROPE, who: Viewer_ NIL] RETURNS [v: Viewer] = { notOk: BOOL; errorIndex: INT; answer: ROPE; answerForm: WalnutSendOps.Form; AnswerGetChar: PROC[pos: INT] RETURNS[CHAR] = {RETURN[msgHeaders.Fetch[pos]]}; [notOk, answer, errorIndex]_ GVAnswer.MakeHeader[AnswerGetChar, msgHeaders.Length[], simpleUserName, defaultRegistry]; IF notOk THEN { start, end: INT _ errorIndex; BEGIN ENABLE RuntimeError.BoundsFault => {start _ 0; end _ 1; CONTINUE}; IF start = nullIndex THEN start _ 0 ELSE {UNTIL msgHeaders.Fetch[start] = '\n DO start _ start - 1; ENDLOOP; start _ start + 1}; IF end = nullIndex THEN end _ start + 1 ELSE {UNTIL msgHeaders.Fetch[end] = '\n DO end _ end + 1; ENDLOOP; end _ end - 1}; END; IF who # NIL THEN ShowErrorFeedback[who, start, end]; WalnutSendOpsExtras.SenderReport[IO.PutFR[ "\nSyntax error in header line \"%g\"", IO.rope[msgHeaders.Substr[start, end-start+1] ]]]; IF answer.Length[] = 0 THEN RETURN; WalnutSendOpsExtras.SenderReport["\n*****Partial answer has been generated\n"]; }; answerForm _ NEW[ WalnutSendOps.FormRec _ [formText: answerText, fields: ParseAnswerHeader[answer] ] ]; v_ BuildSendViewer[TRUE, FALSE, answerForm, who]; ClearFileAssoc[v]; GrabFocus[v]; }; ParseAnswerHeader: PROC[header: ROPE] RETURNS[fields: LIST OF ROPE] = { HeaderLines: ARRAY[0..3] OF Rope.ROPE; startPos: INT _ Rope.Find[header, ": ", 0]; endOfLine: INT _ Rope.Find[header, "\n", startPos]; FOR i: INT IN [0..3] DO HeaderLines[i] _ Rope.Substr[header, startPos+2, endOfLine-startPos-2]; startPos _ Rope.Find[header, ": ", endOfLine]; IF startPos = -1 THEN EXIT ELSE endOfLine _ Rope.Find[header, "\n", startPos] ENDLOOP; RETURN[ LIST[ HeaderLines[0], HeaderLines[1], HeaderLines[2], HeaderLines[3] ] ] }; Forward: PUBLIC PROC[msg: Viewer, who: Viewer _ NIL] RETURNS[v: Viewer] = TRUSTED { forwardForm: WalnutSendOps.Form = NEW[WalnutSendOps.FormRec _ [formText: forwardText, fields: NIL]]; messagePattern: TiogaOps.Pattern = TiogaOps.CreateSimplePattern["ForwardedMessage"]; headerPattern: TiogaOps.Pattern = TiogaOps.CreateSimplePattern["MessageHeader"]; pstart, pend: TiogaOps.Location; root: TiogaOps.Ref; found: BOOL; v_ BuildSendViewer[TRUE, FALSE, forwardForm, who]; ClearFileAssoc[v]; TiogaOps.SelectDocument[v]; root _ TiogaOps.SelectionRoot[]; pstart _ TiogaOps.Location[ root, 0 ]; TiogaOps.SelectPoint[ v, pstart ]; found _ TiogaOps.SelectionSearch[pattern: headerPattern]; IF found THEN { [ ,pstart, pend, , , ] _ TiogaOps.GetSelection[]; TiogaOps.SetSelection[viewer: v, start: pstart, end: pend, level: word, pendingDelete: TRUE]; TiogaOps.InsertRope[ msg.name ] }; found _ TiogaOps.SelectionSearch[pattern: messagePattern]; IF NOT found THEN RETURN; [ ,pstart, pend, , , ] _ TiogaOps.GetSelection[]; TiogaOps.SetSelection[ viewer: v, start: pstart, end: pend, level: node, pendingDelete: TRUE ]; TiogaOps.SelectDocument[ viewer: msg, which: secondary ]; TiogaOps.ToPrimary[]; UnsetNewVersion[v]; GrabFocus[v] }; ReSend: PUBLIC PROC[msg: Viewer, who: Viewer _ NIL] RETURNS[v: ViewerClasses.Viewer] = { reSendForm: WalnutSendOps.Form = NEW[WalnutSendOps.FormRec _ [formText: ViewerTools.GetTiogaContents[msg], fields: NIL]]; v _ BuildSendViewer[TRUE, FALSE, reSendForm, who]; ClearFileAssoc[v]; TiogaOps.SelectDocument[v]; IF TiogaOps.FindText[viewer: v, rope: "Date:", whichDir: anywhere, case: FALSE] THEN { TiogaOps.CaretBefore[]; TiogaOps.SetSelectionLooks[]; TiogaOps.InsertRope["Original-"]; }; IF TiogaOps.FindText[viewer: v, rope: "Sender:", whichDir: anywhere, case: FALSE] THEN { TiogaOps.CaretBefore[]; TiogaOps.SetSelectionLooks[]; TiogaOps.InsertRope["Original-"]; }; IF TiogaOps.FindWord[viewer: v, rope: "From:", whichDir: anywhere, case: FALSE] THEN { TiogaOps.CaretBefore[]; TiogaOps.SetSelectionLooks[]; TiogaOps.InsertRope["Originally-"]; }; UnsetNewVersion[v]; }; ClearFileAssoc: PUBLIC PROC[v: Viewer] = { IF v.file # NIL THEN v.file_ NIL; v.name_ sendCaption; ViewerOps.PaintViewer[v, caption]; }; SenderNewVersion: PUBLIC PROC[viewer: Viewer] = { OPEN Menus; menu: Menus.Menu = viewer.menu; <> firstForm: Menus.MenuEntry _ NIL; getEntry: Menus.MenuEntry_ Menus.FindEntry[menu, "Get"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, TRUE]; getEntry_ Menus.FindEntry[menu, "Default"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, TRUE]; getEntry_ Menus.FindEntry[menu, "PrevMsg"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, TRUE]; FOR i: Menus.MenuLine IN [1..5) DO thisLine: Menus.MenuEntry = Menus.GetLine[menu, i]; IF thisLine = NIL THEN EXIT; IF firstForm = NIL THEN EXIT; IF Rope.Equal[thisLine.name, firstForm.name] THEN FOR entry: MenuEntry _ thisLine, entry.link UNTIL entry = NIL DO SetGuarded[entry, TRUE] ENDLOOP; ENDLOOP; ViewerOps.PaintViewer[viewer, menu]; -- show as guarded viewer.newVersion_ TRUE; }; UnsetNewVersion: PUBLIC PROC[viewer: Viewer] = { OPEN Menus; menu: Menu = viewer.menu; firstForm: Menus.MenuEntry _ NIL; <> getEntry: Menus.MenuEntry_ Menus.FindEntry[menu, "Get"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, FALSE]; getEntry_ Menus.FindEntry[menu, "Default"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, FALSE]; getEntry_ Menus.FindEntry[menu, "PrevMsg"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, FALSE]; FOR i: Menus.MenuLine IN [1..5) DO thisLine: Menus.MenuEntry = Menus.GetLine[menu, i]; IF thisLine = NIL THEN EXIT; IF firstForm = NIL THEN EXIT; IF Rope.Equal[thisLine.name, firstForm.name] THEN FOR entry: MenuEntry _ thisLine, entry.link UNTIL entry = NIL DO SetGuarded[entry, FALSE] ENDLOOP; ENDLOOP; ViewerOps.PaintViewer[viewer, menu]; -- show as unguarded viewer.newVersion_ FALSE; ViewerOps.PaintViewer[viewer, caption]; }; <<* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *>> TiogaTextFromStrm: PUBLIC PROC [strm: IO.STREAM, startPos: INT_ 0, len: INT_ LAST[INT]] RETURNS [contents: TiogaContents] = { <> ENABLE IO.EndOfStream => GOTO TooShort; fulltext: ROPE _ RopeFromStream[strm, startPos, len]; formatPos: INT _ Rope.Index[fulltext, 0, "\000\000"]; contents_ NEW[ViewerTools.TiogaContentsRec]; contents.contents_ Rope.Substr[fulltext, 0, formatPos]; IF formatPos < Rope.Length[fulltext] THEN contents.formatting_ Rope.Substr[fulltext, formatPos]; EXITS TooShort => RETURN[NIL]; }; RopeFromStream: PUBLIC PROC [strm: STREAM, startPos, len: INT] RETURNS [contents: ROPE] = { <> rem: INT _ IO.GetLength[strm] - startPos; IF rem < len THEN len _ rem; IF len < 2048 THEN TRUSTED { <> nat: NAT _ len; text: Rope.Text _ Rope.NewText[nat]; IO.SetIndex[strm, startPos]; [] _ IO.GetBlock[strm, LOOPHOLE[text], 0, nat]; RETURN [text]; }; <> rem _ len/2; len _ len - rem; contents _ RopeFromStream[strm, startPos, len]; <> contents _ Rope.Concat[contents, RopeFromStream[strm, startPos+len, rem] ]; }; GetFieldBody: PUBLIC PROC[text, fieldName: ROPE] RETURNS[fieldBody: ROPE] = { <> OPEN GVMailParse; mPos: INT_ 0; lastCharPos: INT_ text.Length[]; pH: GVMailParse.ParseHandle_ GVMailParse.InitializeParse[]; NextChar: PROC[] RETURNS [ch: CHAR] = { IF mPos > lastCharPos THEN ch_ endOfInput ELSE ch_ text.Fetch[mPos]; mPos_ mPos + 1; }; BEGIN field: ROPE_ NIL; DO found, fieldNotRecognized: BOOL_ TRUE; [field, found]_ GVMailParse.GetFieldName[pH, NextChar ! ParseError => GOTO parseErrorExit]; IF ~found THEN EXIT; IF Rope.Equal[fieldName, field, FALSE] THEN -- ignore case { fieldBody_ GVMailParse.GetFieldBody[pH, NextChar]; EXIT} ELSE []_ GVMailParse.GetFieldBody[pH, NextChar, TRUE]; ENDLOOP; GVMailParse.FinalizeParse[pH]; EXITS parseErrorExit => { GVMailParse.FinalizeParse[pH]; RETURN[NIL]}; END; }; GetRecipients: PUBLIC PROC[sender: Viewer] RETURNS[rList: LIST OF ROPE, parseError: BOOL] = { <> text: ROPE; status: SendParseStatus; sPos, mPos: INT; TRUSTED {text _ WalnutDocumentRope.Create[LOOPHOLE [TiogaOps.ViewerDoc[sender]]]}; [status, sPos, mPos, rList] _ Parse[text]; IF (status # ok) AND (status # includesPublicDL) THEN BEGIN SELECT status FROM fieldNotAllowed => IF sPos # mPos THEN { ShowErrorFeedback[sender, sPos, mPos]; WalnutSendOpsExtras.SenderReport[Rope.Substr[text, MAX[0, sPos-1], mPos-sPos]]; WalnutSendOpsExtras.SenderReport[" field is not allowed\n"]} ELSE WalnutSendOpsExtras.SenderReport[IO.PutFR[" field at pos %g is not allowed\n", IO.int[sPos]]]; syntaxError => IF sPos # mPos THEN { ShowErrorFeedback[sender, sPos, mPos]; WalnutSendOpsExtras.SenderReport["\nSyntax error on line beginning with "]; WalnutSendOpsExtras.SenderReport[Rope.Substr[text, MAX[0, sPos-1], mPos-sPos]]} ELSE WalnutSendOpsExtras.SenderReport[IO.PutFR["..... Syntax error at position %g ", IO.int[sPos]]]; includesPrivateDL => WalnutSendOpsExtras.SenderReport[" Private dl's are not yet implemented\n"]; ENDCASE => ERROR; ViewerOps.BlinkIcon[sender, IF sender.iconic THEN 0 ELSE 1]; RETURN[NIL, TRUE] END; RETURN[rList, FALSE] }; <> ParseHeadersFromRope: PUBLIC PROC[headers: ROPE, proc: ParseProc] RETURNS[msgHeaders: MsgHeaders] = <> BEGIN OPEN GVMailParse; mPos: INT _ 0; len: INT _ headers.Length[]; pH: GVMailParse.ParseHandle _ GVMailParse.InitializeParse[]; NextChar: PROC[] RETURNS [ch: CHAR] = { IF mPos >= len THEN ch _ endOfInput ELSE ch _ headers.Fetch[mPos]; mPos _ mPos + 1; }; msgHeaders _ NIL; IF headers.Fetch[0] = '\n THEN mPos _ 1; -- ignore initial CR (tioga formatting nonsense) BEGIN ENABLE ParseError => GOTO parseErrorExit; fieldName: ROPE _ NIL; found: BOOL; wantThisField, continue: BOOL _ TRUE; DO [fieldName, found] _ GVMailParse.GetFieldName[pH, NextChar]; IF ~found THEN EXIT; IF proc # NIL THEN [wantThisField, continue] _ proc[fieldName]; IF wantThisField THEN msgHeaders _ CONS[[fieldName, GVMailParse.GetFieldBody[pH, NextChar]], msgHeaders] ELSE [] _ GVMailParse.GetFieldBody[pH, NextChar, TRUE]; IF ~continue THEN EXIT; ENDLOOP; GVMailParse.FinalizeParse[pH]; EXITS parseErrorExit => { GVMailParse.FinalizeParse[pH]; RETURN[msgHeaders]}; END; END; ParseMsgFromStream: PUBLIC PROC[strm: IO.STREAM, len: INT, proc: ParseProc] RETURNS[msgHeaders: MsgHeaders] = <> BEGIN OPEN GVMailParse; mPos: INT _ 0; pH: GVMailParse.ParseHandle _ GVMailParse.InitializeParse[]; NextChar: PROC[] RETURNS [ch: CHAR] = { IF mPos > len THEN ch _ endOfInput ELSE ch _ strm.GetChar[ ! IO.EndOfStream => {mPos _ len; ch _ endOfInput; CONTINUE} ]; mPos _ mPos + 1; }; msgHeaders _ NIL; IF strm.PeekChar[] = '\n THEN { -- ignore initial CR (tioga formatting nonsense) [] _ strm.GetChar[]; mPos _ 1; }; BEGIN ENABLE ParseError => GOTO parseErrorExit; fieldName: ROPE _ NIL; found: BOOL; wantThisField, continue: BOOL _ TRUE; DO [fieldName, found] _ GVMailParse.GetFieldName[pH, NextChar]; IF ~found THEN EXIT; IF proc # NIL THEN [wantThisField, continue] _ proc[fieldName]; IF wantThisField THEN msgHeaders _ CONS[[fieldName, GVMailParse.GetFieldBody[pH, NextChar]], msgHeaders] ELSE [] _ GVMailParse.GetFieldBody[pH, NextChar, TRUE]; IF ~continue THEN EXIT; ENDLOOP; GVMailParse.FinalizeParse[pH]; EXITS parseErrorExit => { GVMailParse.FinalizeParse[pH]; RETURN[msgHeaders]}; END; END; Parse: PUBLIC PROC[text: ROPE] RETURNS[status: SendParseStatus, sPos, mPos: INT, rList: LIST OF ROPE] = BEGIN OPEN GVMailParse; mLF: MessageInfo; lastCharPos: INT _ text.Length[] - 1; lastCharIsCR: BOOL _ (text.Fetch[lastCharPos] = '\n); GetNextChar: PROC[] RETURNS [ch: CHAR] = { IF mPos <= lastCharPos THEN ch _ text.Fetch[mPos] ELSE IF (mPos=lastCharPos+1) AND ~lastCharIsCR THEN ch _ '\n ELSE ch _ endOfInput; mPos _ mPos + 1; }; RNameListField: PROC[index: MessageFieldIndex] = BEGIN fieldBody, fbEnd: LIST OF ROPE _ NIL; AnotherRName: PROC[r1, r2: ROPE, isFile, isNested: BOOL] RETURNS [ROPE, BOOLEAN] = BEGIN name: ROPE _ CanonicalName[r1, r2]; countOfRecipients: INT _ 0; -- too lazy to figure this out now IF fbEnd=NIL THEN fbEnd _ fieldBody _ CONS[name, NIL] ELSE fbEnd _ fbEnd.rest _ CONS[name, NIL]; IF isFile THEN status _ includesPrivateDL ELSE IF name.Find["^"] < 0 THEN countOfRecipients _ countOfRecipients + 1 ELSE IF status # includesPrivateDL THEN status _ includesPublicDL; RETURN[NIL, FALSE]; END; ParseNameList[pH, GetNextChar, AnotherRName, NIL]; SELECT index FROM toF, ccF, cF, bccF => IF rList = NIL THEN rList _ fieldBody ELSE rList _ RopeList.Append[rList, fieldBody]; ENDCASE => NULL; END; pH: ParseHandle; field: ROPE _ NIL; fieldNotRecognized: BOOL; mPos _ 0; -- where we are in the fulltext status _ ok; -- start with good status pH _ InitializeParse[]; DO sPos _ mPos; [field, fieldNotRecognized] _ GetFieldName[pH, GetNextChar ! ParseError => { FinalizeParse[pH]; GOTO errorExit}]; IF ~fieldNotRecognized THEN EXIT; FOR i: MessageFieldIndex IN MessageFieldIndex DO { mLF _ messageParseArray[i]; IF Rope.Equal[messageParseArray[i].name, field, FALSE] THEN { fieldNotRecognized _ FALSE; IF mLF.fType = rNameList THEN RNameListField[i ! ParseError => GOTO errorExit] ELSE [] _ GetFieldBody[pH, GetNextChar, TRUE]; }; }; ENDLOOP; IF fieldNotRecognized THEN [] _ GetFieldBody[pH, GetNextChar, TRUE]; -- skip anything not recognized ENDLOOP; FinalizeParse[pH]; EXITS errorExit => RETURN[syntaxError, sPos, mPos, NIL]; END; CanonicalName: PUBLIC PROC [simpleName, registry: ROPE] RETURNS[name: ROPE] = BEGIN name _ simpleName; IF registry.Length[] = 0 THEN name _ name.Cat[".", WalnutSendOps.defaultRegistry] ELSE name _ name.Cat[".", registry]; END; <> GetSendForm: PUBLIC PROC[fileName: ROPE] RETURNS[text: ViewerTools.TiogaContents] = { s: IO.STREAM; so: FS.StreamOptions _ FS.defaultStreamOptions; so[tiogaRead] _ FALSE; s _ FS.StreamOpen[fileName: fileName, streamOptions: so ! FS.Error => { s _ NIL; CONTINUE }]; IF s # NIL THEN { text _ TiogaTextFromStrm[s ! FS.Error => { text _ NIL; CONTINUE }]; s.Close[ ! FS.Error => { s _ NIL; CONTINUE }] } }; InternalDisplayTioga: PUBLIC PROC[senderInfo: SenderInfo, tc: TiogaContents, grab: BOOL] = { <> senderV: Viewer _ senderInfo.senderV; iHadFocus: BOOL _ InputFocus.GetInputFocus[].owner = senderV; IF senderV.link # NIL THEN InternalDestroySplits[senderV]; IF TiogaOps.GetSelection[feedback].viewer = senderV THEN TiogaOps.CancelSelection[feedback]; ViewerTools.SetTiogaContents[senderV, tc]; -- test if I had the focus & no-one else has it now IF grab AND iHadFocus AND InputFocus.GetInputFocus[].owner = NIL THEN { ViewerTools.SetSelection[senderV, viewerStart]; [] _ TiogaOps.NextPlaceholder[gotoend: FALSE] }; UnsetNewVersion[senderV]; senderInfo.successfullySent _ FALSE; }; InsertForm: PUBLIC PROC[ sender: SenderInfo, form: WalnutSendOps.Form, force: BOOL ] = { senderV: Viewer = sender.senderV; whoHasIt: Viewer; IF senderV.iconic THEN { ViewerOps.AddProp[senderV, $WalnutSendOpsForm, form]; sender.openEvent _ ViewerEvents.RegisterEventProc[ proc: OpenSendViewer, event: open, filter: senderV, before: FALSE]; UnsetNewVersion[senderV]; -- not strictly true but want to reuse RETURN}; -- first stuff the text of the form into the Viewer InternalDisplayTioga[sender, form.formText, FALSE]; IF force OR ((whoHasIt _ InputFocus.GetInputFocus[].owner) = NIL) OR (whoHasIt = senderV) THEN DoPlaceHolders[senderV, form.fields] ELSE { ViewerOps.AddProp[senderV, $WalnutSendOpsFields, form.fields]; sender.focusEvent _ ViewerEvents.RegisterEventProc[ proc: SetFocusInSendViewer, event: setInputFocus, filter: senderV, before: FALSE]; UnsetNewVersion[senderV]; -- not strictly true but want to reuse }; }; OpenSendViewer: ViewerEvents.EventProc = BEGIN OPEN Menus; senderInfo: SenderInfo _ NARROW[ViewerOps.FetchProp[viewer, $SenderInfo]]; form: WalnutSendOps.Form; ra: REF ANY; IF senderInfo = NIL THEN RETURN[FALSE]; IF (ra _ ViewerOps.FetchProp[viewer, $WalnutSendOpsForm]) = NIL THEN RETURN[FALSE]; form _ NARROW[ra]; ViewerEvents.UnRegisterEventProc[senderInfo.openEvent, open]; senderInfo.openEvent _ NIL; TRUSTED { Process.Detach[FORK EntryInsertForm[senderInfo, form]]}; RETURN[FALSE]; END; SetFocusInSendViewer: ViewerEvents.EventProc = BEGIN OPEN Menus; senderInfo: SenderInfo _ NARROW[ViewerOps.FetchProp[viewer, $SenderInfo]]; fieldsList: LIST OF ROPE; ra: REF ANY; IF senderInfo = NIL THEN RETURN[FALSE]; IF (ra _ ViewerOps.FetchProp[viewer, $WalnutSendOpsFields]) = NIL THEN RETURN[FALSE]; fieldsList _ NARROW[ra]; ViewerEvents.UnRegisterEventProc[senderInfo.focusEvent, setInputFocus]; senderInfo.focusEvent _ NIL; TRUSTED { Process.Detach[FORK EntryPlaceHolders[senderInfo, fieldsList]]}; RETURN[FALSE]; END; DoPlaceHolders: PUBLIC PROC[senderV: Viewer, fieldsList: LIST OF ROPE] = { AddFields: PROC[ root: TiogaOps.Ref ] = { -- now try to find all of the placeholders in the text and match them with the entries in the fields list FOR rl: LIST OF ROPE _ fieldsList, rl.rest UNTIL rl = NIL DO field: ROPE = rl.first; IF NOT TiogaOps.NextPlaceholder[gotoend: FALSE].found THEN EXIT; IF field = NIL THEN LOOP; TiogaOps.Delete[]; TiogaOps.InsertRope[field] ENDLOOP }; IF senderV.destroyed OR senderV.iconic THEN RETURN; --woops ViewerTools.SetSelection[ senderV, viewerStart]; TiogaOps.CallWithLocks[ AddFields ]; UnsetNewVersion[senderV]; ViewerTools.SetSelection[ senderV, viewerStart]; [] _ TiogaOps.NextPlaceholder[gotoend: FALSE]; }; END.