/* Journal of Artificial Intelligence Research 5 (1996) 329-349 Submitted 5/96; published 12/96 (c) 1996 AI Access Foundation and Morgan Kaufmann Publishers. All rights reserved. On-Line Appendix A for: Quantitative Results Comparing Three Intelligent Interfaces for Information Capture: A Case Study Adding Name Information into an Electronic Personal Organizer Jeffrey C. Schlimmer (schlimmer@eecs.wsu.edu) School of Electrical Engineering & Computer Science Washington State University, Pullman, WA 99164-2752, U.S.A. Patricia Crane Wells (patricia@allpen.com) AllPen Software, Inc. 16795 Lark Avenue, Suite 200, Los Gatos, CA 95030, U.S.A. Abstract Efficiently entering information into a computer is key to enjoying the benefits of computing. This paper describes three intelligent user interfaces: handwriting recognition, adaptive menus, and predictive fillin. In the context of adding a personUs name and address to an electronic organizer, tests show handwriting recognition is slower than typing on an on-screen, soft keyboard, while adaptive menus and predictive fillin can be twice as fast. This paper also presents strategies for applying these three interfaces to other information collection domains. THIS SOURCE CODE IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND, AND ITS AUTHOR AND THE JOURNAL OF ARTIFICIAL INTELLIGENCE RESEARCH (JAIR) AND JAIR'S PUBLISHERS AND DISTRIBUTORS, DISCLAIM ANY AND ALL WARRANTIES, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, AND ANY WARRANTIES OR NON INFRINGEMENT. THE USER ASSUMES ALL LIABILITY AND RESPONSIBILITY FOR USE OF THIS SOURCE CODE, AND NEITHER THE AUTHOR NOR JAIR, NOR JAIR'S PUBLISHERS AND DISTRIBUTORS, WILL BE LIABLE FOR DAMAGES OF ANY KIND RESULTING FROM ITS USE. Without limiting the generality of the foregoing, neither the author, nor JAIR, nor JAIR's publishers and distributors, warrant that the Source Code will be error-free, will operate without interruption, or will meet the needs of the user. */ // Text of project CardFile++.¹ written on 12/5/96 at 6:59 PM // Beginning of text file Project Data // // Source: NewtonScript // Created: 25 June 1994 // Author: Jeffrey C. Schlimmer // © 1994, Jeffrey C. Schlimmer, Washington State U. // schlimmer@eecs.wsu.edu, (509) 335-2399 // constant kAppSymbol := '|CardF++:WSU-JCS|; constant kAppObject := '["Name", "Names"]; constant kSoupName := ROM_cardfilesoupname; constant kSoupName2 := "mruName:WSU-JCS"; constant kSoupIndexes2 := '[{structure: slot, path: cacheID, type: symbol}]; constant kAppName := "Card File++"; InstallScript := func(packageFrame) begin //if debugOn then write("Entering installScript\n"); // To be notified of changes to the soup (including a changed folder) AddArraySlot(soupNotify, kSoupName); AddArraySlot(soupNotify, kAppSymbol); AddArraySlot(soupNotify, kSoupName2); AddArraySlot(soupNotify, kAppSymbol); /* //if debugOn then write("...Adding soup indexes\n"); foreach slot in '[company, city, region] do begin GetUnionSoup(kSoupName):AddIndex({ structure: 'slot, path: slot, type: 'string }); end; */ //if debugOn then write("Exiting installScript\n"); end; RemoveScript := func(packageFrame) begin //if debugOn then write("Entering removeScript\n"); // remove soup name and app symbol from soupNotify array local SoNoPo := ArrayPos(soupNotify, kAppSymbol, 0, nil); ArrayRemoveCount(soupNotify, SoNoPo - 1, 2); SoNoPo := ArrayPos(soupNotify, kAppSymbol, 0, nil); ArrayRemoveCount(soupNotify, SoNoPo - 1, 2); /* //if debugOn then write("...Removing soup indexes\n"); foreach slot in '[company, city, region] do GetUnionSoup(kSoupName):RemoveIndex(slot); */ //if debugOn then write("...Removing mru soup\n"); local soup; foreach store in GetStores() do begin if store:HasSoup(kSoupName2) then begin soup := store:GetSoup(kSoupName2); soup:RemoveFromStore(); end; end; //if debugOn then write("Exiting removeScript\n"); end; //EOF // End of text file Project Data // Beginning of file showAll.t showAll := {viewFlags: 1, viewBounds: {top: 13, left: 4, right: 1, bottom: -21}, DisplayName: func(nameEntry) begin //:?msg(" Entering showAll.displayName for "); :?msg(nameEntry._uniqueID); :?msg("\n"); target := nameEntry; shell:DisplayName(nameEntry); //:?msg(" Exiting showAll.displayName\n"); end, viewJustify: 240, saveChanges: func() begin //:?msg(" Entering showAll.saveChanges for "); :?msg(target._uniqueID); :?msg("\n"); shell:saveChanges(); //:?msg(" Exiting showAll.saveChanges\n"); end, target: nil // set at run-time by viewSetupFormScript and DisplayName // synchronized with cardFile.target and shell.target , viewSetupFormScript: func() begin //:?msg("Entering showAll.viewSetupFormScript\n"); target := namesCursor:Entry(); //:?msg("Exiting showAll.viewSetupFormScript\n"); end, viewQuitScript: func() begin //:?msg("Entering showAll.viewQuitScript\n"); target := nil; //:?msg("Exiting showAll.viewQuitScript\n"); end, viewFormat: 1, hideSound: nil, showSound: nil, debug: "showAll", viewClass: 74 }; shell := {editWidth: 222, phoneButExtension: func(phoneStr) begin local SpPo := :StrPosLast(phoneStr, " "); local DaPo := :StrPosLast(phoneStr, "-"); local PePo := :StrPosLast(phoneStr, "."); if not SpPo then SpPo := 0; if not DaPo then DaPo := 0; if not PePo then PePo := 0; if SpPo+DaPo+PePo > 0 then SubStr(phoneStr, 0, 1+Max(SpPo, Max(DaPo, PePo))); else Clone(phoneStr); end, indent: 75, viewLineSpacing: 32, viewQuitScript: func() begin //:?msg("Entering shell.viewQuitScript\n"); target := nil; phones := nil; //predictCursors := nil; lines := nil; mruCachedText := nil; //:?msg("Exiting shell.viewQuitScript\n"); end, target: nil // set at run-time by viewSetupFormScript and DisplayName // synchronized with cardFile.target and showAll.target , firstLine: nil, lines: nil // set at run-time by viewSetupFormScript // and linesInit , posLinesEntry: func(pathName) begin //:?msg(" shell.posLinesEntry on "); :?msg(pathName); :?msg("\n"); ArrayPos( lines, pathName, 0, func(key, target) key = target.path ); end, editHeight: 50, predictFromCursors: func(sourcePath) begin //:?msg("Entering shell.predictFromCursors for " & sourcePath); SmCFQuEns := nil; // smart CF query results if target.newP AND // completing new name predictMappings.(sourcePath) AND // path predicts target.(sourcePath) AND // target has field StrLen(target.(sourcePath)) AND // target not empty str (SmCFQuEns := SmartCFQuery( :firstTwoWords( target.(sourcePath) ) )) then begin for EnIn := Length(SmCFQuEns)-1 to 0 by -1 do begin local SmCFQuEn := SmCFQuEns[EnIn]; if SmCFQuEn.(sourcePath) AND BeginsWith(SmCFQuEn.(sourcePath), target.(sourcePath)) then begin local TaPas := predictMappings.(sourcePath); // targets //:?msg("...predicting from " & SmCFQuEn.(sourcePath)); waitDialog:open(); foreach pathOrFunc in TaPas do begin if ClassOf(pathOrFunc) = 'symbol then waitDialog:setText( "Copying " & pathOrFunc & " for " & SmCFQuEn.(sourcePath) ); if ClassOf(pathOrFunc) = 'symbol OR ClassOf(pathOrFunc) = 'pathExpr then begin target.(pathOrFunc) := Clone(SmCFQuEn.(pathOrFunc)); :predictLabelCommands( target, sourcePath, pathOrFunc, SmCFQuEn.(pathOrFunc), SmCFQuEns ); end; else if ClassOf(pathOrFunc) = 'CodeBlock then Apply( pathOrFunc, [{target: target, entry: SmCFQuEn, shell: shell}] ); end; waitDialog:setText(""); waitDialog:Close(); break; end; end; //:?msg("...redoing children"); :RedoChildren(); //:?msg("...redone"); end; //:?msg("Exiting shell.predictFromCursors"); end, predictInitCursors: /* func() // see also installScript begin //:?msg(" Entering shell.predictInitCursors\n"); predictCursors := {}; foreach slot, value in predictMappings do predictCursors.(slot) := Query( namesSoup, {type: 'index, indexPath: slot} ); //:?msg(" Exiting shell.predictInitCursors\n"); end */ nil, viewBounds: {left: 0, top: 0, right: 0, bottom: 0}, saveChanges: func() begin //:?msg(" Entering shell.saveChanges for "); :?msg(target._uniqueID); :?msg("\n"); //:?msg(" ...target.dirtyP: "); :?msg(target.dirtyP); :?msg("\n"); if target.dirtyP then begin :setSortOn(); // see closeEdit() foreach path in '[name, company, address, city, region, postal_code, country, email] do if path = 'name then begin :mruAddLabelCommand( target.name.honorific, 'name.honorific, ["Ms.", "Mrs.", "Mr.", "Dr."], 4 // yes, these are hidden constants... ); :mruAddLabelCommand( target.name.title, 'name.title, [], 4 ); end; else :mruAddLabelCommand( target.(path), path, :originalLabelCommands(path), :mruMaxNumberNewLabelCommands(path) ); end; //:?msg(" Exiting shell.saveChanges\n"); end, firstTwoWords: func(string) begin local FiSpPo := StrPos(string, " ", 0); local SeSpPo := nil; if FiSpPo then begin local SeSpPo := StrPos(string, " ", FiSpPo+1); if SeSpPo then SubStr(string, 0, SeSpPo); else string; end; else string; end, predictMappings: { // see also installScript and removeScript company: [ 'address, 'address2, 'city, 'region, 'postal_code, 'country, func(argFrame) // predict all but extension of phones begin for PhIn := 0 to 3 do // phone index begin if length(argFrame.entry.phones) > PhIn AND argFrame.target.phones then begin local EnPh := argFrame.shell:PhoneButExtension( argFrame.entry.phones[PhIn] ); if Length(argFrame.target.phones) <= PhIn then AddArraySlot(argFrame.target.phones, EnPh); else argFrame.target.phones[PhIn] := EnPh; end; end; end, func(argFrame) // predict @ and domain of e-mail begin //:?msg("Entering company -> e-mail\n"); //:?msg("...argFrame.entry.email: "); :?msg(argFrame.entry.email); :?msg("\n"); if argFrame.entry.email then argFrame.target.email := argFrame.shell:emailDomain( argFrame.entry.email ); end, ], city: [ 'region, 'postal_code, 'country, func(argFrame) // predict region code of phones begin for PhIn := 0 to 3 do // phone index begin if length(argFrame.entry.phones) > PhIn AND argFrame.target.phones then begin local EnPh := argFrame.shell:PhoneRegion( argFrame.entry.phones[PhIn] ); if Length(argFrame.target.phones) <= PhIn then AddArraySlot(argFrame.target.phones, EnPh); else argFrame.target.phones[PhIn] := EnPh; end; end; end, ], region: [ 'country ], }, mruAddLabelCommand: func(newLabelText, path, originalLabelCommands, maxAddedLabels) begin // may be called after the parent is already closed //:?msg(" Entering shell.mruAddLabelCommand with "); :?msg(newLabelText); :?msg(" for "); :?msg(path); :?msg("\n"); if newLabelText AND not StrEqual(newLabelText,"") then begin // there is a label to add if path = 'email then newLabelText := :emailDomain(newLabelText); // The cached array of label commands is split into the // prefix containing only the new commands because the // AppendList function will remove duplicates, and we'd // like to retain the original label commands at the // end past the separator // Set up the array of new label commands local NeLaCos; // new label commands local NuOrLaCos := Length(originalLabelCommands); if mruCachedLabelCommands.(path) then begin // copy over all cached new label commands if NuOrLaCos > 0 then begin // there are original label commands local PoPiSe := ArrayPos( // find the pick separator mruCachedLabelCommands.(path), 'pickSeparator, 0, nil ); NeLaCos := SetLength( // truncate it mruCachedLabelCommands.(path), PoPiSe ); end; else // no originals, so no separator NeLaCos := mruCachedLabelCommands.(path); end; else // nothing previously cached (or parent closed) NeLaCos := []; // Remove any previous occurrence of the new label // Necessary because AppendList is case-sensitive foreach NeLaCo in NeLaCos do if StrEqual(newLabelText, NeLaCo) then begin // found a case-insensitive equality SetRemove(NeLaCos, NeLaCo); break; end; // Add newest label command NeLaCos := AppendList([Clone(newLabelText)],NeLaCos); // Check to see if we're over length local LeNeLaCos := Length(NeLaCos); if LeNeLaCos > maxAddedLabels then begin SetLength(NeLaCos,maxAddedLabels); LeNeLaCos := maxAddedLabels; end; // Reset cached label commands if NuOrLaCos > 0 then begin SetLength(NeLaCos,LeNeLaCos+1+NuOrLaCos); // Add the pick separator NeLaCos[LeNeLaCos] := 'pickSeparator; // Copy over original label commands local NeLaCosIn := LeNeLaCos+1; // index foreach OrLaCo in originalLabelCommands do begin NeLaCos[NeLaCosIn] := OrLaCo; NeLaCosIn := NeLaCosIn + 1; end; end; if mruCachedLabelCommands then // parent open mruCachedLabelCommands.(path) := NeLaCos; end; //:?msg(" Exiting shell.mruAddLabelCommand\n"); end, _proto: @175, EmailDomain: func(emailStr) begin local AtPo := StrPos(emailStr, "@", 0); // at position if AtPo then SubStr(emailStr, AtPo, nil); else Clone(emailStr); end, AddItem: func(theItemProto) begin //:?msg(" Entering shell.AddItem\n"); AddArraySlot(lines, Clone(theItemProto)); numLines := numLines + 1; // if the view is displayed, then update the children if GetView(self) then :RedoChildren(); //:?msg(" Exiting shell.AddItem\n"); end, lineHeight: 14, viewJustify: 240, FlushEdits: func() begin nil; end, numLines: 0, linesINit: func() begin //:?msg(" Entering shell.linesInit\n"); self.lines := []; :AddItem({ _proto: pt_mruTextExpandoProto, label: "Ms./Mr.", labelCommands: ["Ms.", "Mrs.", "Mr.", "Dr."], path: [pathExpr: 'name, 'honorific], entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField, }); :AddItem({ _proto: protoTextExpando, label: "First", path: [pathExpr: 'name, 'first], entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField, }); :AddItem({ _proto: protoTextExpando, label: "Last", path: [pathExpr: 'name, 'last], specialClass: 'name, entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField, }); :AddItem({ _proto: pt_mruTextExpandoProto, label: "Title", path: [pathExpr: 'name, 'title], entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField+vCharsAllowed, }); :AddItem({ _proto: pt_mruTextExpandoProto, label: "Company", path: 'company, specialClass: 'company, entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField+vCharsAllowed, }); :AddItem({ _proto:pt_mruTextExpandoProto, label: "Address", path: 'address, entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField+vAddressField+vCharsAllowed, }); :AddItem({ _proto: protoTextExpando, // no label path: 'address2, entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField+vAddressField+vCharsAllowed, }); :AddItem({ _proto: pt_mruTextExpandoProto, label: "City", path: 'city, entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField, }); :AddItem({ _proto: pt_mruTextExpandoProto, label: "State", path: 'region, entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField, //+vCustomDictionaries, //dictionaries: [kCommonDictionary, kLocalStatesDictionary, kLocalStatesAbbrevsDictionary], }); :AddItem({ _proto: pt_mruTextExpandoProto, label: "Zip Code", path: 'postal_code, entryFlags: vVisible+vClickable+vGesturesAllowed+vCharsAllowed+vNumbersAllowed+vPunctuationAllowed, }); :AddItem({ _proto: pt_mruTextExpandoProto, label: "Country", labelCommands: [ "Australia", //"Belgium", "Canada", "France", "Germany", //"Italy", "Japan", "Netherlands", //"Norway", //"Spain", //"Sweden", "UK", "USA" ], path: 'country, entryFlags: vVisible+vClickable+vGesturesAllowed+vNameField, }); :AddItem({ _proto: pt_mruTextExpandoProto, label: "E-Mail", path: 'email, entryFlags: vVisible+vClickable+vGesturesAllowed+vCharsAllowed+vNumbersAllowed+vPunctuationAllowed, textSetup: func() begin GetRoot().alphaKeyboard:open(); inherited:textSetup(); end, }); phones := []; AddArraySlot(phones, SetClass(Clone(""), 'phone)); AddArraySlot(phones, SetClass(Clone(""), 'phone)); AddArraySlot(phones, SetClass(Clone(""), 'phone)); AddArraySlot(phones, SetClass(Clone(""), 'phone)); :AddItem({ _proto: protoPhoneExpando, path: 'phones, phoneIndex: 0, }); :AddItem({ _proto: protoPhoneExpando, path: 'phones, phoneIndex: 1, }); :AddItem({_proto: protoPhoneExpando, path: 'phones, phoneIndex: 2, }); :AddItem({_proto: protoPhoneExpando, path: 'phones, phoneIndex: 3 }); :AddItem({_proto: protoDateExpando, label: "Birthday", path: 'bday, }); //:?msg(" Exiting shell.linesInit\n"); end, setSortOn: func() begin //:?msg(" Entering shell.setSortOn\n"); if target.name.last then begin if StrEqual(target.name.last, target.SortOn) then nil; // no need to update anything or move cursor else begin target.SortOn := SetClass(Clone(target.name.last), 'name); true; // signal that the cursor should be changed end; end; //else // target.name.last = nil //begin // unlikely to be needed // target.SortOn := SetClass(Clone(""), 'name); // namesCursor:GoTo(target); // true; //end; //:?msg(" Exiting shell.setSortOn\n"); end, originalLabelCommands: func(pathName) begin //:?msg(" shell.originalLabelCommands on "); :?msg(pathName); :?msg("\n"); local PoLiEn := :posLinesEntry(pathName); if PoLiEn then lines[PoLiEn].labelCommands; else nil; end, DisplayName: func(nameEntry) begin //:?msg(" Entering shell.DisplayName for "); :?msg(nameEntry._uniqueID); :?msg("\n"); target := nameEntry; :RedoChildren(); //:?msg(" Exiting shell.DisplayName\n"); end, mruMaxNumberNewLabelCommands: func(pathName) begin //:?msg(" shell.mruMaxNumberNewLabelCommands on "); :?msg(pathName); :?msg("\n"); local PoLiEn := :posLinesEntry(pathName); if PoLiEn then lines[PoLiEn].mruMaxNumberNewLabelCommands; else nil; end, viewSetupFormScript: func() begin //:?msg("Entering shell.viewSetupFormScript\n"); target := namesCursor:Entry(); :linesInit(); mruCachedText := {}; //:predictInitCursors(); //call this after setting up the lines array inherited:?viewSetupFormScript(); //:?msg("Exiting shell.viewSetupFormScript\n"); end, StrPosLast: func(string, substring) begin local Po := StrPos(string, substring, 0); // position if Po then begin local NePo := StrPos(string, substring, Po+1); // new pos while NePo do begin Po := NePo; NePo := StrPos(string, substring, Po+1); end; Po; end; else nil; end, mruCachedText: nil // set at run-time each time a value is changed // a frame with a slot for each proto expando line and a // value of the most recently changed text for that line , closeEdit: func(textExpando) begin //:?msg("Entering shell.closeEdit with "); :?msg(textExpando.path); :?msg("\n"); // see saveChanges() if (ClassOf(textExpando.path) = 'pathExpr AND textExpando.path[0] = 'name AND textExpando.path[1] = 'first) OR (ClassOf(textExpando.path) = 'pathExpr AND textExpando.path[0] = 'name AND textExpando.path[1] = 'last) OR textExpando.path = 'address2 OR textExpando.path = 'phones OR textExpando.path = 'bday then nil; // we're not doing mru pickers on these else :mruAddLabelCommand( target.(textExpando.path), textExpando.path, textExpando.labelCommands, textExpando.mruMaxNumberNewLabelCommands ); // dispatch by matching the label of the expando against // the keys in predictMappings if textExpando.path = 'phones OR textExpando.path = 'bday then nil; // they aren't simple string-valued fields else :predictFromCursors(textExpando.path); inherited:closeEdit(textExpando); //:?msg("Exiting shell.closeEdit\n"); end, debug: "shell", predictLabelCommands: func(targetEntry, sourcePath, targetPath, predictedText, SmCFQuEns) begin //:?msg("Entering shell.predictLabelCommands with " & predictedText & " and " & targetPath); if predictedText then // there was a prediction begin local MaNuNeLaCo := 4; //:mruMaxNumberNewLabelCommands(targetPath); local OrLaCo := :originalLabelCommands(targetPath); if MaNuNeLaCo AND // expando is MRU type and OrLaCo then // has at least empty array begin for EnIn := Length(SmCFQuEns)-1 to 0 by -1 do //foreach SmCFQuEn in SmCFQuEns do begin // look for one that's different local SmCFQuEn := SmCFQuEns[EnIn]; if SmCFQuEn.(targetPath) AND NOT StrEqual( SmCFQuEn.(targetPath), predictedText ) AND SmCFQuEn.(sourcePath) AND BeginsWith( SmCFQuEn.(sourcePath), target.(sourcePath) ) then begin :mruAddLabelCommand( SmCFQuEn.(targetPath), targetPath, OrLaCo, MaNuNeLaCo ); break; // stop looking end; end; end; end; //:?msg("Exiting shell.predictLabelCommands"); end, phones: nil // set at run-time to [] by viewSetupFormScript , predictCursors: nil // set at run-time by predictInitCursor // a frame with a slot for each slot in predictMappings // and a value that is a cursor for prediction // see also predictFromCursors and closeEdit , phoneRegion: func(phoneStr) begin local PhStLe := StrLen(phoneStr); // phone string length local SpPo := StrPos(phoneStr, " ", 0); // space position local DaPo := StrPos(phoneStr, "-", 0); // dash position local PePo := StrPos(phoneStr, ".", 0); // period position if not SpPo then SpPo := PhStLe; if not DaPo then DaPo := PhStLe; if not PePo then PePo := PhStLe; SubStr( phoneStr, 0, Min(1+Min(SpPo, Min(DaPo, PePo)),PhStLe) ); end }; AddStepForm(showAll, shell); StepDeclare(showAll, shell, 'shell); waitDialog := {viewBounds: {left: 0, top: 20, right: 190, bottom: 40}, setText: func(text) begin SetValue(waitText, 'text, text); RefreshViews(); end, viewFlags: 64, viewJustify: 16, debug: "waitDialog", _proto: @179 }; AddStepForm(shell, waitDialog); StepDeclare(shell, waitDialog, 'waitDialog); waitText := {text: "", viewBounds: {top: 2, left: 2, right: -2, bottom: -2}, viewJustify: 8388852, debug: "waitText", _proto: @218 }; AddStepForm(waitDialog, waitText); StepDeclare(waitDialog, waitText, 'waitText); demoButton := {text: "Demo", buttonClickScript: func() begin if Visible(demoWindow) then begin SetValue(self, 'text, "Demo"); demoWindow:Close(); end; else begin SetValue(self, 'text, "Stop"); demoWindow:Open(); demoWindow:nextAct(); end; end, viewBounds: {top: -15, left: -39, right: -6, bottom: -2}, viewJustify: 8388774, debug: "demoButton", _proto: @226 }; AddStepForm(showAll, demoButton); StepDeclare(showAll, demoButton, 'demoButton); demoWindow := {viewBounds: {top: 145, left: 10, right: -10, bottom: 245}, act: nil // set at run-time by viewSetupFormScript and demo // number designating the current step in the script , nextAct: func() begin // Hide the director button directorButton:Toggle(); // Play the sound PlaySoundSync(ROM_flip); // Change view's text if script[act].sceneText then SetValue(stage, 'text, script[act].sceneText); // Update the cue SetValue(cue, 'text, act+1 && "of" && Length(script)-1); // Execute script if script[act].scene then Apply( script[act].scene, [{ win: self, shell: shell, demoWindow: demoWindow, cardFile: cardFile, }] ); // Change director button's text if script[act].directorText then SetValue(directorButton, 'text, script[act].directorText); // Advance script pointer act := act + 1; // Show the director button directorButton:?Toggle(); // if it's still open end, pause: func() begin RefreshViews(); Sleep(1*60); end, script: [ { sceneText: "Recreation of the built-in Names application to study two types of learning. Supports only the detailed \"Show All\" view. Compatible with the built-in Names application. ©1994 Jeffrey C. Schlimmer & Patricia Crane Wells, Washington State U.", directorText: "Start", }, { sceneText: "1. Picker Learning: The first type of learning adds a few, recently entered values to pickers for many of the expando lines. For instance, the \"Title\" expando doesn't have a built-in picker, but now it will with up to four titles recently entered by the user.", directorText: "Next", }, { sceneText: "2. Filler Learning: The second type of learning fills in expando lines by looking at previous Names soup entries. For instance, when the user enters a \"Company\", the address, email suffix, and phone prefix are filled in by looking in the Names soup for another person from the same company.", }, { sceneText: "These two methods are combined by adding an extra lookup value to the picker. For instance, if there are people from the same company but with different addresses, then one address will be inserted into the \"Address\" expando and one will be put into the \"Address\" picker.", }, { sceneText: "In the remainder of the demonstration, three new entries are added to the Names soup. They are compatible with (and may be deleted by) the built-in Names application. ¥ Tap the \"Next\" button to continue, or ¥ Tap the close box to stop.", }, { sceneText: "Now let's add a new Names entry for the first author of this application.", scene: func(argFrame) begin local NaLa := Clone("Schlimmer"); local Co := Clone("Washington State U"); local WoPh := SetClass( Clone("(509) 335-2399"), 'workPhone // doesn't seem to help ); local FaPh := SetClass( Clone("(509) 335-3818"), 'faxPhone ); local Te := ["Dr.", "Jeffrey C.", NaLa, "Assistant Professor", Co, "School of Electrical Engineering", "and Computer Science", "Pullman", "WA", "99164-2752", "USA", "schlimmer@eecs.wsu.edu", WoPh, FaPh]; argFrame.cardFile:createNewName(); for Li := 0 to 13 do begin argFrame.shell:ExpandLine(Li); argFrame.shell:RedoChildren(); debug("protoLabelInputLine"):updateText(Te[Li]); RefreshViews(); end; GetRoot().alphaKeyboard:close(); argFrame.shell:ExpandLine(16); argFrame.shell:RedoChildren(); debug("protoLabelInputLine"):updateText("6/26/60"); RefreshViews(); argFrame.shell:ExpandNone(); end, }, { sceneText: "Notice that most of the expandos now have pickers with their most recent value. For expandos like \"Ms./Mr.\", new values are separated from originals by a line. These pickers are cached in a separate soup. ¥ Tap on various picker labels and see.", }, { sceneText: "Now let's slowly add another Names entry for an office at the university. When we close the \"Company\" expando, values will be filled in by copying from an earlier Names soup entry for \"Washington State U\".", scene: func(argFrame) begin local NaLa := Clone("Registrar"); local Co := Clone("Washington State U"); local Te := ["", "Office of the", NaLa, "", Co]; argFrame.cardFile:createNewName(); for Li := 0 to 4 do begin argFrame.shell:ExpandLine(Li); argFrame.shell:RedoChildren(); debug("protoLabelInputLine"):updateText(Te[Li]); RefreshViews(); end; end, }, { sceneText: "Note how values for the two \"Address\" lines, and \"City\", \"State\", \"Zip Code\", \"Country\" lines have been copied over from an earlier Names soup entry.", scene: func(argFrame) begin if debug("protoLabelInputLine") then argFrame.shell:ExpandNone(); end, }, { sceneText: "Note how the value for \"E-Mail\" is only the suffix of the previous entry, and the phone numbers include only area code and prefix. Let's finish this entry.", }, { sceneText: "Let's finish this entry. Note how the \"Address\" picker has values for both addresses at \"Washington State U\". ¥ Tap on the \"Address\" picker and see.", scene: func(argFrame) begin local WoPh := SetClass( Clone("(509) 335-5346"), 'workPhone // doesn't seem to help ); local FaPh := SetClass( Clone(""), 'faxPhone ); local Te := ["", "Office of the", "Registrar", "", "Washington State U", "346 French Administration Building", "", "Pullman", "WA", "99164-1035", "USA", "", WoPh, FaPh]; for Li := 5 to 13 do begin argFrame.shell:ExpandLine(Li); argFrame.shell:RedoChildren(); debug("protoLabelInputLine"):updateText(Te[Li]); RefreshViews(); end; GetRoot().alphaKeyboard:close(); argFrame.shell:ExpandNone(); end, }, { sceneText: "Now let's slowly add a third Names entry for the second author of this application. As before, when we close the \"Company\" expando, values will be filled in by copying from an earlier Names soup entry for \"Washington State U\".", scene: func(argFrame) begin local NaLa := Clone("Wells"); local Co := Clone("Washington State U"); local Te := ["Ms.", "Patricia Crane", NaLa, "Research Assistant", Co]; argFrame.cardFile:createNewName(); for Li := 0 to 4 do begin argFrame.shell:ExpandLine(Li); argFrame.shell:RedoChildren(); debug("protoLabelInputLine"):updateText(Te[Li]); RefreshViews(); end; end, }, { sceneText: "Because there are two addresses for \"Washington State U\", one is filled in automatically, and one is put at the top of the \"Address\" picker. ¥ Tap on the \"Address\" picker and see.", scene: func(argFrame) begin if debug("protoLabelInputLine") then argFrame.shell:ExpandNone(); end, }, { sceneText: "Changing the \"City\" or \"State\" expandos for a new name also fills in some fields by looking up a matching entry in the names soup. Experiment by adding a new name for someone from your own company, and see what the pickers and expando lines are filled with.", scene: func(argFrame) begin local WoPh := SetClass( Clone("(509) 335-1190"), 'workPhone // doesn't seem to help ); local FaPh := SetClass( Clone("(509) 335-3818"), 'faxPhone ); local Te := ["Ms.", "Patricia Crane", "Wells", "Research Assistant", "Washington State U", "School of Electrical Engineering", "and Computer Science", "Pullman", "WA", "99164-2752", "USA", "pwells@eecs.wsu.edu", WoPh, FaPh]; for Li := 5 to 13 do begin argFrame.shell:ExpandLine(Li); argFrame.shell:RedoChildren(); debug("protoLabelInputLine"):updateText(Te[Li]); RefreshViews(); end; GetRoot().alphaKeyboard:close(); argFrame.shell:ExpandNone(); end, }, { sceneText: "Thanks to PIE for an incredible product and to DTS for their outstanding support. ©1994 Jeffrey C. Schlimmer & Patricia Crane Wells, Washington State U. schlimmer@eecs.wsu.edu, (509) 335-2399", directorText: "Done", }, { scene: func (argFrame) begin argFrame.demoWindow:Close(); end, }, ], viewClickScript: func(unit) // make the demo window movable begin :Drag(unit, nil); end, viewFlags: 576, viewJustify: 48, viewQuitScript: func() begin SetValue(demoButton, 'text, "Demo"); end, viewSetupFormScript: func() begin act := 0; // start at the beginning of demo end, debug: "demoWindow", _proto: @180 }; AddStepForm(showAll, demoWindow); StepDeclare(showAll, demoWindow, 'demoWindow); stage := {text: "", viewBounds: {top: 1, left: 1, right: -1, bottom: -1}, viewFont: simpleFont9 , viewJustify: 240, viewSetupFormScript: func() begin SetValue(self, 'text, ""); end, debug: "stage", _proto: @218 }; AddStepForm(demoWindow, stage); StepDeclare(demoWindow, stage, 'stage); cue := {text: "", viewBounds: {top: -12, left: -122, right: -52, bottom: -3}, viewFormat: 1, viewJustify: 8388773, debug: "cue", _proto: @218 }; AddStepForm(demoWindow, cue); StepDeclare(demoWindow, cue, 'cue); directorButton := {text: "", buttonClickScript: func() begin :nextAct(); end, viewBounds: {top: -12, left: -48, right: -18, bottom: -3}, viewJustify: 8388774, debug: "directorButton", _proto: @226 }; AddStepForm(demoWindow, directorButton); StepDeclare(demoWindow, directorButton, 'directorButton); constant |layout_showAll.t| := showAll; // End of file showAll.t // Beginning of file main.t cardFile := {namesSoup: nil, mruCacheSoup: nil // set at run-time by viewSetupFormScript , viewSetupDoneScript: func() begin //:?msg("Entering cardFile.viewSetupDoneScript\n"); self.cardFile := self; // setup variable referring to base slot self.currentView := self.showAll; // it's already open self:DisplayName(self.namesCursor:Entry()); //:?msg("Exiting cardFile.viewSetupDoneScript\n"); end, viewFormat: 590161, defaultName: { cardType: 1, name: { class: 'person, //first: nil, //last: nil, //honorific: nil, //title: nil }, company: nil, address: nil, address2: nil, city: nil, region: nil, postal_code: nil, phones: [], country: nil, email: nil, bday: nil, sortOn: SetClass(Clone(""), 'name), newP: true, // flag that's removed when this frame is saved dirtyP: true, // flag set when frame is edited }, viewQuitScript: func() begin //:?msg("Entering cardFile.viewQuitScript\n"); // save any changes self:SaveChanges(); self:UnRegisterCardSoup(kSoupName2); // nil out slots with run-time values self.currentView := nil; self.namesCursor := nil; self.namesSoup := nil; self.target := nil; self.mruCacheSoup := nil; self.mruCachedLabelCommands := nil; //:?msg("Exiting cardFile.viewQuitScript\n"); end, hideSound: ROM_drawerclose, target: nil // set at run-time by viewSetupFormScript and DisplayName // synchronized with showAll.target and shell.target , RegisterCardSoup: func(soupName, soupIndexes, appSymbol, appObject) begin //:?msg("Entering cardFile.RegisterCardSoup\n"); // returns a union soup for your app to use // first check for system provided function if functions.RegisterCardSoup then return RegisterCardSoup(soupName, soupIndexes, appSymbol, appObject); CreateAppSoup(soupName, soupIndexes, EnsureInternal([appSymbol]), EnsureInternal(appObject)); // ensure your soup will exists on stores which later become available. AddArraySlot(cardSoups, soupName); AddArraySlot(cardSoups, soupIndexes); // ensure your soup exists on all currently available stores local store; foreach store in GetStores() do if not store:IsReadOnly() and not store:HasSoup(soupName) then store:CreateSoup(soupName, soupIndexes); //:?msg("Exiting cardFile.RegisterCardSoup\n"): return GetUnionSoup(soupName); end, viewFlags: 5, showSound: ROM_draweropen, cardFile: nil, namesCursor: nil, viewBounds: {top: 2, left: 0, right: 236, bottom: 336}, saveChanges: func() begin //:?msg("Entering cardFile.saveChanges for "); :?msg(target._uniqueID); :?msg("\n"); //:?msg("...target.dirtyP: "); :?msg(target.dirtyP); :?msg("\n"); local MoCuP := self.currentView:SaveChanges(); RemoveSlot(self.target, 'newP); if self.target.dirtyP then // set in textChanged begin RemoveSlot(self.target, 'dirtyP); EntryChange(self.target); self.namesSoup:Flush(); EntryChange(self.mruCachedLabelCommands); self.mruCacheSoup:Flush(); end; if MoCuP then self.namesCursor:GoTo(self.target); //:?msg("Exiting cardFile.saveChanges\n"); end, viewScrollDownScript: func() begin //:?msg("Entering cardFile.viewScrollDownScript\n"); self:saveChanges(); // do first because if last name changes // then so will the cursor's position self.namesCursor:Next(); if self.namesCursor:Entry() = NIL then begin GetRoot():SysBeep(); self.namesCursor:Reset(); // wrap around to first end; self:DisplayName(self.namesCursor:Entry()); playSoundSync(ROM_flip); //:?msg("Exiting cardFile.viewScrollDownScript\n"); end, viewOverviewScript: func() begin GetRoot():SysBeep(); end, viewJustify: 16, mruCachedLabelCommands: nil // set at run-time by viewSetupFormScript and // mruAddLabelCommand // soup entry with a slot for each slot in the proto expando // shell and an array value listing labelCommands for that // expando , DisplayName: func(nameEntry) begin //:?msg(" Entering cardFile.displayName for "); :?msg(nameEntry._uniqueID); :?msg("\n"); self.target := nameEntry; self.currentView:DisplayName(nameEntry); //:?msg(" Exiting cardFile.displayName\n"); end, viewScrollUpScript: func() begin //:?msg("Entering cardFile.viewScrollUpScript\n"); self:saveChanges(); // do first because if last name changes // then so will the cursor's position self.namesCursor:Prev(); if self.namesCursor:Entry() = NIL then begin GetRoot():SysBeep(); repeat self.namesCursor:Move(100) until self.namesCursor:Entry() = nil; self.namesCursor:Prev(); end; self:DisplayName(self.namesCursor:Entry()); playSoundSync(ROM_flip); //:?msg("Exiting cardFile.viewScrollUpScript\n"); end, CreateNewName: func() begin //:?msg("Entering cardFile.createNewName when "); :?msg(target._uniqueID); :?msg("\n"); //:?msg("...old target.dirtyP: "); :?msg(target.dirtyP); :?msg("\n"); self:saveChanges(); //:?msg("...saved target.dirtyP: "); :?msg(target.dirtyP); :?msg("\n"); local NeNa := DeepClone(self.defaultName); self.namesSoup:AddToDefaultStore(NeNa); self.namesCursor:GoTo(NeNa); self:DisplayName(self.namesCursor:Entry()); //:?msg("...new "); :?msg(target._uniqueID); :?msg(" target.dirtyP: "); :?msg(target.dirtyP); :?msg("\n"); //:?msg("Exiting cardFile.createNewName\n"); end, declareSelf: 'base, viewSetupFormScript: func() begin //:?msg("Entering cardFile.viewSetupFormScript\n"); self.namesSoup := GetUnionSoup(kSoupName); self.namesCursor := query( self.namesSoup, {type: 'index, indexPath: 'sortOn}); self.target := self.namesCursor:Entry(); self.mruCacheSoup := :RegisterCardSoup( kSoupName2, kSoupIndexes2, kAppSymbol, kAppObject ); local MrCaCu := Query(self.mruCacheSoup, {type: 'index}); self.mruCachedLabelCommands := MrCaCu:Entry(); if not self.mruCachedLabelCommands then begin self.mruCacheSoup:AddToDefaultStore( Clone({cacheID: 'labelCommands}) ); self.mruCachedLabelCommands := MrCaCu:Reset(); end; //:?msg("Exiting cardFile.viewSetupFormScript\n"); end, currentView: nil, msg: func(outputObject) begin if debugOn then begin local Ti := Ticks(); write(outputObject); if msgTicks then write(" [" & Ti-msgTicks & " ticks]"); write("\n"); msgTicks := Ti; end; end, viewEffect: 133120, viewClass: 74, msgTicks: nil, debug: "cardFile", soupChanged: func(theSoupName) // not sure this is ever called begin //:?msg("Entering cardFile.soupChanged\n"); if Visible(self) then begin if self.namesCursor:Entry() = 'deleted then self.namesCursor:Reset(); self:DisplayName(self.namesCursor:Entry()); end; //:?msg("Exiting cardFile.soupChanged\n"); end, UnRegisterCardSoup: func(soupName) begin //:?msg(" Entering UnregisterCardSoup\n"); // first check for system provided function if functions.UnRegisterCardSoup then return UnRegisterCardSoup(soupName); local pos := ArrayPos(cardSoups, soupName, 0, func(x, y) ClassOf(y) = 'String and StrEqual(x, y)); if pos then ArrayRemoveCount(cardSoups, pos, 2); //:?msg(" Exiting UnregisterCardSoup\n"); end }; _view000 := {title: "Card File++", viewBounds: {left: 0, top: 0, right: 150, bottom: 20}, viewJustify: 22, _proto: @229 }; AddStepForm(cardFile, _view000); _view001 := {_proto: @219}; AddStepForm(cardFile, _view001); new := {text: "New", buttonClickScript: func() begin cardFile:CreateNewName(); end, viewBounds: {left: 25, top: 0, right: 51, bottom: 14}, viewJustify: 8388678, debug: "new", _proto: @226 }; AddStepForm(_view001, new); showAll := LinkedSubview(showAll, {viewBounds: {left: 32, top: 40, right: 200, bottom: 104}, debug: "showAll"} ); AddStepForm(cardFile, showAll); StepDeclare(cardFile, showAll, 'showAll); constant |layout_main.t| := cardFile; // End of file main.t // Beginning of file MRUTextExpandoProto _userproto000 := {path: nil, labelCommands: [], textChanged: func() begin //:?msg("Entering" && target._uniqueID && path && "textChanged\n"); //:?msg("...target.dirtyP: "); :?msg(target.dirtyP); :?msg("\n"); mruCachedText.(path) := entryLine.text; target.dirtyP := true; inherited:?textChanged(); //:?msg("Exiting" && path && "textChanged\n"); end, viewSetupDoneScript: func() begin //:?msg("Entering "); :?msg(path); :?msg(" viewSetupDoneScript\n"); //:?msg("...mruCachedLabelCommands: "); :?msg(mruCachedLabelCommands); :?msg("\n"); //:?msg("...mruCachedLabelCommands.(path): "); :?msg(mruCachedLabelCommands.(path)); :?msg("\n"); if mruCachedLabelCommands.(path) then :setLabelCommands(mruCachedLabelCommands.(path)); inherited:?viewSetupDoneScript(); //:?msg("Exiting "); :?msg(path); :?msg(" viewSetupDoneScript\n"); end, mruMaxNumberNewLabelCommands: 4, _proto: @227 }; constant |layout_MRUTextExpandoProto| := _userproto000; // End of file MRUTextExpandoProto