WRITING VIEWFRAME ADDITIONS Mark Zeren Flashpoint Inc. zeren@shore. net ************************************************************************************ This article is reprinted from issue 3.5 (Sept/Oct 1995) of PDA Developers magazine. Copyright(C) 1996 by Creative Digital Publishing Inc. All rights reserved. ************************************************************************************ In my recent review of ViewFrame 1.1 in PDA Developers 3.5, I briefly mentioned the additions API, saying that it was my favorite new feature. The fact some of ViewFrame's most important functions are implemented using this API indicates its power and flexibility. In this article I introduce the API - including changes and extensions in ViewFrame 1.2 - and walk you through the creation of several sample additions. I assume you have a working knowledge of both ViewFrame and NewtonScript. WHAT CAN ADDITIONS DO? ViewFrame additions usually fall into one of three categories: * Commands appear in the additions pop-up menu (), and generally, provide a navigation shortcut, perform an action on the current object, or implement some other useful function. I use the Get app command (Newton OS 1.x only) from the VF+General addition quite frequently (see Figure 1). * Viewers display specific object types in ViewFrame's object display (see Figure 2). A good example of a viewer is the byte-code decompiler in the VF+Function addition. The current viewer interface is somewhat limited. * Annotations modify the object display, the expression pop-up list, or the info pop-up list (see Figure 2). The annotation API has improved in ViewFrame 1.2. While I structure my examples around these categories, they are by no means rigid divisions. In fact, my viewer and annotation examples both use auxiliary commands. THREE SAMPLE ADDITIONS In this article I present a set of three simple but useful additions. Here are brief descriptions of them. Path Memory The Bookmark >Inspector command (in the VF+General addition) prints bookmarks to the Inspector. For example, a bookmark for ViewFrame's base view looks like this: /* */ GetRoot().|ViewFrame:JRH|:NewValue( "bookmark",GetRoot().|ViewFrame:JRH|) The NewValue() message sent to ViewFrame makes ViewFrame's base view the current object and sets the current path description to "bookmark". While I really like these Inspector-based bookmarks, they never seem to last long. I'm always filling up the Inspector window with printouts of the root view or stack crawls, so I usually end up just clearing out the whole window before starting a new session. Path Memory is an addition that saves simple bookmarks right on the Newton. Native DV DV() is one of those debugging functions that I used before I bought ViewFrame. With ViewFrame I find it easier to navigate and inspect view hierarchies on the Newton. Nonetheless, DV()'s complete list of sub-views is appealing. Native DV is a viewer which displays an interactive, hierarchical list of views. Object Bytes Object Bytes is an annotation that estimates the size of an object based on information published in the Newton Toolkit Q&A's. I add estimated size to the info pop-up and the object display in order to demonstrate these two types of annotations. ARCHITECTURE OVERVIEW Before diving into the implementation of my three additions, I need to briefly outline ViewFrame's addition architecture. Each addition part installs its additions by adding an array to ViewFrame's global frame GetGlobals().|ViewFrame:JRH|. This array, or addition list, is an array of addition frames with the following format: GetGlobals().|ViewFrame:JRH| = { VF+Function:JRH: [{...add frame...}], VF+General:JRH: [{...add frame...}, {...add frame...}, *** {...add frame...}], *** VF+Examples:MJZ: [{...add frame...}, *** {...add frame...}] }; Addition frames define certain methods and slots. For example, a command addition frame must define an AdditionScript slot. Addition methods may control or alter the behavior of ViewFrame through their return values, or by sending messages to ViewFrame. Throughout this article the prefix "VF:" indicates a message which must be sent to ViewFrame's base view . AN ADDITION BOILERPLATE A minimal addition project consists of the following four elements: 1) a project file 2) global constant and variable declarations 3) install and remove scripts 4) an about box The VFExamps Project All of my example additions are in the VFExamps project file that can be found on the source code disk for this issue of PDA Developers. In the Project Settings dialog box I set the project up to build an auto-part and assign the package name and application symbol to '|VF+Examples:MJZ|. Globals.cod The "Globals.cod" file starts the project with the following definitions: constant kAddSym := '|ViewFrame:JRH|; // global frame of additions constant kVFSymbol := '|ViewFrame:JRH|; // The VF application's unique symbol gAddList := []; // Our Additions list As I write each addition, I add its addition frame to gAddList. Install and Remove scripts The InstallScript() takes the completed addition list in gAddList and adds it to ViewFrame's global frame, creating the global frame if necessary. Keep in mind that install scripts for auto parts are not total cloned. Be sure to EnsureInternal() objects that need to be referenced after the user ejects the card or removes the addition's package. My InstallScript(), which is based closely on the sample code provided with ViewFrame, is: DefConst('kAddList,gAddList); InstallScript := func(partFrame, removeFrame) begin // Get VF's global frame local additions := GetGlobals().(kAddSym); // Create it if it doesn't exist if not additions then GetGlobals().(EnsureInternal(kAddSym)) := additions := EnsureInternal({}); // Add our Additions to it. additions.(EnsureInternal(kAppSymbol)) := kAddList; end; The RemoveScript() simply removes the addition list from ViewFrame's global frame, removing the global frame itself if necessary: RemoveScript := func(removeFrame) begin // Get VF's global frame local additions := GetGlobals().(kAddSym); // Exit gracefully if it isn't there if not additions then return; // Remove our Additions list RemoveSlot(additions, kAppSymbol); // and the global frame itself, if empty if Length(additions) = 0 then RemoveSlot(GetGlobals(), kAddSym); end; As I write each addition, I insert its source files and layouts in between "Globals.cod" and "InstRm.cod", the source file with the install and remove scripts. The About Box The last step in constructing my template is adding an about box. About boxes for ViewFrame additions are commands whose titles begin with "About". In ViewFrame 1.1 these commands appear in the additions pop-up with other commands. Under ViewFrame 1.2 they are available through the About... command which appears at the bottom of the additions pop-up. The simplest form of about box uses the view system message view:Notify(). As I described above, an addition frame implements each addition. To create an about box, we first add an addition frame to gAddList: AddArraySlot(gAddList, { title: "About VF+Examples", AdditionScript: func(obj, VF) VF:Notify(kNotifyQAlert, EnsureInternal("VF+Examples"), EnsureInternal( "(c) 1995 PDA Developers.\n" "by Mark Zeren\n" "Command: Path Memory\n" "Viewer: Native DV.\n" "Annotation: Object Bytes.\n")) }); // AddArraySlot In the frame I define two slots, title and AdditionScript. title refers to the string "About VF+Examples...". When the user selects this command, ViewFrame calls AdditionScript(), passing it two parameters: obj, a reference to ViewFrame's current object, and VF, a reference to ViewFrame's base view. The AdditionScript() brings up the about box by sending a Notify() message to ViewFrame. Note that you can send the Notify() message to the root view, or any other view for that matter. The VF parameter just happens to be handy. Figure 3 shows the Notify view. The about box implementation can be found in my source file "About.cod". I include "About.cod" right after "Globals.cod" to ensure that the about box is the first command in ViewFrame 1.1. A SIMPLE PATH My first addition, Path Memory, saves and recalls the path to the current ViewFrame object. I don't attempt to interpret paths that contain ViewFrame's special characters "/" and "%" or other non-NewtonScript expressions. I simply concatenate ViewFrame's current path and store it as a string. I also provide a preferences dialog that allows the user to set the size of the path memory. In all, the addition has three commands: Save Path, Recall Path, and Path Settings.... A Place For My Stuff Before I begin, I need some storage space for Path Memory's state information. While soup storage would be nice, for this article I settle for the simple, if-not-terribly-persistent storage offered by a frame in the root view. I use the addition symbol '|VF+Examples:MJZ| as the slot symbol for the frame. From now on, I refer to this frame as storage. storage.maxPaths holds the maximum number of paths, and storage.paths holds the paths themselves in an array of strings. I create the storage frame by appending the following code to the InstallScript(): GetRoot().(EnsureInternal(kAppSymbol)) := { paths: [], maxPaths: 16, dv: true, bytes: true, }; The dv and bytes slots hold state information for Native DV and Object Bytes, my other two additions. I also add a corresponding line to the RemoveScript() in order to clean up the root view: RemoveSlot(GetRoot(),kAppSymbol); Save Path Like the about box command, Save Path does most of its work in an AdditionScript(). However, unlike the about box command, Save Path should only be available when ViewFrame has a valid path. To implement a context-sensitive command like Save Path, I replace the title slot of the addition frame with GetTitle(). GetTitle() takes one parameter, obj, which is a reference to ViewFrame's current object. If the command appears in the additions pop-up, GetTitle() returns a valid pop-up item, usually a string. Otherwise, GetTitle() returns nil. The GetTitle() script for Save Path checks the length of VF.TextPath which contains the text version of ViewFrame's path as an array of strings. In the AdditionScript() I create a path using the Stringer() function and add it to storage.paths. The kChopPathsFunc utility function removes excess paths in FIFO order: DefConst('kChopPathsFunc, func (s) while Length(s.paths) > s.maxPaths do RemoveSlot(s.paths, 0); ); AddArraySlot(gAddList, { GetTitle: func(obj) if Length(GetRoot().(kVFSymbol).TextPath) > 0 then "Save Path", AdditionScript: func(obj, VF) begin local storage := GetRoot().(kAppSymbol); local path := Stringer(VF.TextPath); AddArraySlot(storage.paths,path); call kChopPathsFunc with (storage); end }); Recall Path Like Save Path, Recall Path is a context-sensitive command. It is only available after one or more paths have been saved. The Addi-tionScript() for Recall Path brings up a pop-up of all saved paths. When the user selects an item in the pop-up, I send the VF:-NewEval() message. VF:NewEval() takes one parameter, expr, which must be a string. ViewFrame compiles and executes expr, setting the current object to the result: AddArraySlot(gAddList, { GetTitle: func (obj) if Length(GetRoot().(kAppSymbol).paths) > 0 then "Recall Path", AdditionScript: func(obj, VF) begin local storage := GetRoot().(kAppSymbol); DoPopup(storage.paths, 0, 0, { pickActionScript: func (index) VF:NewEval(storage.paths[index]) }); end }); When the user selects Recall Path, a pop-up menu of all saved paths appears in the upper right corner of the display (see Figure 4). Path Settings... To create the settings dialog box, I use BuildContext() to open a layout designed using the NTK. The settings layout is a simple protoFloatNGo with one child view, a protoLabelInputLine called "Number of Paths" (see Figure 5). The "Number of Paths" protoLabelInputLine provides a list of default values, does simple error correction for the input, and chops storage.paths in its viewQuitScript(). The implementation of the Path Settings... command, requires only a title slot and an AdditionScript(). The AdditionScript() calls BuildContext() with the Settings layout and opens the resulting view: AddArraySlot(gAddList, { title: "Path Settings...", AdditionScript: func(obj, VF) BuildContext(GetLayout("Path Settings")):Open(); }); That completes the Path Memory example. Adding soup-based storage, interpreting ViewFrame's special characters, and other possible improvements are left as exercises for the adventurous reader. However, even this minimalist implementation can save you many taps if you are repeatedly inspecting the same object. NATIVE DV The Native DV addition creates a hierarchical list of views in ViewFrame's object display. The first line points to the current view's parent view. The second line points to the view itself. Starting with the third line, sub-views are listed indented to indicate their level of nesting: _Parent: . ... . . . ... . . . Refer back to Figure 2 for an example. When the user taps on one of the lines in the object display, Native DV appends the corresponding view to ViewFrame's object path, updating the text path with a valid NewtonScript expression. Modifying the Object Display Viewer additions replace ViewFrame's normal object display, presumably with a more detailed or useful representation of a particular class of object. Each viewer implements the DisplayObject() method in its addition frame. DisplayObject(), like AdditionScript(), takes the two parameters: obj and VF. ViewFrame sends DisplayObject() messages to addition frames until one of them returns a non-nil value, indicating that it has successfully displayed the object. Native DV, like most viewers, adds items to ViewFrame's object display. ViewFrame provides several methods for adding text, bitmaps, pictures, sounds, and integer items. For the Native DV display I use VF:AddSelect(), which adds a tappable, one-line text item. Add-Select() takes two parameters: text and slot. The text parameter specifies the text of the item as it appears in the object listing. If the user selects the item, ViewFrame passes the slot parameter to VF:AppendEval(). VF:AppendEval() is somewhat complex. It takes one parameter, slot, which may be either a symbol, integer, path expression, or frame. If the current object is a frame and slot is a symbol, VF:-AppendEval() evaluates slot in the context of the frame. The result becomes the new current object, and ".slot" is appended to ViewFrame's path. Analogous behavior occurs when the current object is an array and slot is an integer index into that array. Similarly, if slot is a path expression, VF:AppendEval() appends the result of evaluating the path expression in the context of the current frame or array. Finally, if the slot parameter is a frame, it must define two slots: text, a string which is appended to the object path, and value, a new current value. Native DV creates view descriptions with the VF:one- liner() method which produces the one line text descriptions that normally appear in ViewFrame's object display. VF:oneliner() takes three parameters: parent, slot, and value. The parent and slot parameters are used to handle references to self and known integer bit fields such as viewFlags and viewFormat. The last parameter, value, refers to the object in question. To generate a description of a view, I leave the first two parameters nil and pass the view's base frame as the value parameter. VF:AddSelect() and VF:oneliner() encapsulate much of ViewFrame's built-in object display functionality. In fact, using these two functions, you can build a standard display for a frame in just a few lines of NewtonScript: foreach sym, val in theFrame do VF:AddSelect(" " & sym & ": " & VF:oneliner(theFrame ,sym, val),sym); Implementation The first job of any DisplayObject() method is to determine if it knows how to display the current object. Native DV knows how to display open views, so the first thing I do is to see if the current object is a frame. Then using the ViewIsOpen platform function, I determine if the frame is an open view: AddArraySlot(gAddList, { DisplayObject: func(obj , VF) if IsFrame(obj) and call kViewIsOpenFunc with (obj) then begin local DVFunc := func (view, indent, path) begin VF:AddSelect( indent & VF:oneliner(nil,nil,view), {text: path, value: view} ); local kids := view:ChildViewFrames(); if Length(kids) > 0 then begin local newIndent := indent & ". "; local newPath := [path & ":GetChildViews()[", nil, "]"]; foreach index, child in kids do begin newPath[1] := index; call DVFunc with (child, newIndent, Stringer(newPath)); end; end; end; VF:AddSelect(" _Parent: " & VF:oneliner(nil,nil,obj._Parent), '_Parent); call DVFunc with (obj," ",""); return true; // stop other viewers end, }); // AddArraySlot Next, I define DVFunc, a local function which recursively displays the view hierarchy. DVFunc takes three parameters: view, indent, and path. view points to a view which should be added to the object display. indent is a string used to indent the view description. path, also a string, is a NewtonScript expression that should be added to ViewFrame's path in order to get to view from Display-Object()'s original obj parameter. DVFunc first adds view to the object display with VF:Add-Select(), and then checks to see if view has any children. If it does, it calls itself recursively for each child with updated indent and path parameters. The remainder of Native DV's DisplayObject() method is much more straightforward. I start the object display with a line for the parent view, and then call DVFunc. Finally, I return true to inform ViewFrame that I have successfully displayed the object. Troubles with Viewers At this point Native DV works as advertised. Unfortunately, it always works as advertised, overriding the normal display of open views. Because all Viewer additions are considered "normal" formats, choosing alternate from View Frame's Fmt menu can help somewhat. However, the alternate format for frames includes all of the slots found in the proto chain, and is noticeably slower than a normal frame listing. Once Native DV is installed, it's impossible to get a normal frame listing for open views. In addition, because only the first DisplayObject() script gets to run, any viewer that operates on open views does not get a chance to run if it is installed after Native DV. The situation gets even worse once you install more than twenty additions. At that point, the system sorts the GetGlobals().|ViewFrame:JRH| frame and the order of execution of additions depends on the hash value of each addition list's symbol. While it's unlikely that you will ever need to install twenty ViewFrame additions, it remains true that the only way to reorder viewers is to reinstall them in the desired order. Utilities like NewtCase or the new hidden-freeze feature in NOS 2.0 can ease the problem by quickly removing and reinstalling packages. These are, however, awkward solutions to problems that I think ViewFrame should solve internally. Disabling Native DV I work around these problems by creating a command that optionally disables the Native DV viewer. The new command, also called Native DV, toggles the value of the flag storage.dv which the final version of Native DV's DisplayObject() script checks: DisplayObject: func(obj , VF) if GetRoot().(kAppSymbol).dv and IsFrame(obj) and call kViewIsOpenFunc with (obj) then begin ... I use the fact that GetTitle() can return any legal proto-Picker item to indicate the state of the storage.dv flag with a check mark next to the Native DV command (see Figure 6). In the AdditionScript() for the Native DV command I toggle the value of storage.dv and then send the ShowResult() method to ViewFrame to force it to re-display the current object: GetTitle: func (obj) if GetRoot().(kAppSymbol).dv then { mark: kCheckMarkChar, pickable: true, item: "Native DV" } else "Native DV", AdditionScript: func(obj,VF) begin local storage := GetRoot().(kAppSymbol); storage.dv := not storage.dv; VF:ShowResult(); end, Writing a viewer addition demonstrates using DisplayObject() as well as the very useful combination of VF:Add- Select() and VF:onliner(). Don't forget that ViewFrame provides a number of VF:Add...() methods for more specific data types such as bit maps and sounds. BYTE-SIZE ANNOTATIONS Unlike commands and viewers, the API for annotations differs between ViewFrame versions 1.1 and 1.2. In version 1.1 annotations use DisplayObject() scripts that return nil. However, this scheme presents a problem similar to that described above in the Native DV example: once a viewer's DisplayObject() script returns true, subsequent annotations are never called. Version 1.2 solves this problem using the new method AnnotateObject(). ViewFrame calls all AnnotateObject() scripts, ignoring their return values, before it starts calling DisplayObject() scripts. You can use annotations to modify several areas of ViewFrame's human interface elements (refer back to Figure 2): * Object-display annotations modify the object display, usually adding one or more extra items before viewers get a chance to run. * Info pop-up annotations add items to the info pop-up menu that appears right above the object display (ViewFrame 1.2 only) * Expression-line annotations add expressions to the pop-up menu on the left of the entry line. I conclude my article with examples of object-display and information pop-up annotations. Estimating Object Sizes The definition of kBytesFunc, not listed here, is at the beginning of my source file "Bytes.cod". Given an object obj, kBytesFunc returns an integer estimate of the number of bytes that obj occupies in the frames heap. It bases its estimate on the object-size information available in the Newton Toolkit Q&As documentation. Starting with the current object, kBytesFunc performs a depth-first search, recursively calculating object sizes, skipping read-only objects and _Parent slots in frames. It tends to overestimate the actual size of the graph because it always counts frame maps as if they are in the heap, and it does not compensate for shared maps. Annotating the Object Display Because kBytesFunc can take several seconds for large-object graphs, I use an auxiliary command to toggle storage.bytes, which AnnotateObject() checks before it calls kBytesFunc. Once AnnotateObject() gets the result from kBytesFunc, it adds it to the object display with the method VF:AddStatic(). In order to maintain compatibility with ViewFrame 1.1, I also implement the object-bytes annotation with a DisplayObject() script that checks VF.version before executing. Here are two implementations of the same annotation addition. AnnotateObject() works with ViewFrame 1.2 and higher (VF.version is only defined in ViewFrame 1.2 and above); DisplayObject() works with ViewFrame 1.1: AddArraySlot(gAddList, { AnnotateObject: func (obj,VF) if GetRoot().(kAppSymbol).bytes then VF:AddStatic("Bytes: " & NumberStr(call kBytesFunc with (obj))), DisplayObject: func (obj,VF) if not VF.version and GetRoot().(kAppSymbol).bytes then begin VF:AddStatic("Bytes: " & NumberStr(call kBytesFunc with (obj))); nil; // let other Viewer run end, }); // AddArraySlot AnnotateObject() scripts can also modify the expression pop-up using the methods VF.exprLine:FirstExpr() and VF.exprLine:NextExpr(). I don't provide an example of this type of annotation because I can't think of any useful expressions that ViewFrame doesn't already provide. Annotating the Info Pop-up The info pop-up which appears just above the object listing is a new feature in ViewFrame 1.2. You can add items to the info pop-up by implementing an AddInfo() script in your addition frame. AddInfo() takes three parameters: obj, addtap, and addnontap. obj is the current object. addtap and addnontap are actually closures (functions) which you can call to add items to the pop-up. addnontap() takes one parameter which should be either a string or bitmap. Object Bytes uses addnontap() to add one non-tappable item to the info pop-up. AddArraySlot(gAddList, { AddInfo: func(obj, addtap, addnontap) call addnontap with ("Bytes: " & NumberStr(call kBytesFunc with (obj))), }); // AddArraySlot CONCLUSION I've tried to guide you through the most important steps in building a ViewFrame extension, as well as provide immediately useful sample additions. When you purchase ViewFrame, you receive sample code and documentation detailing many more API methods available to potential addition authors. Despite some rough edges, I enjoy working with ViewFrame's addition's APIs. Utility functions like VF:onliner() and ViewFrame's navigational framework make writing additions simple and rewarding. Though ViewFrame 1.1 shipped some time ago, I am surprised that I haven't seen any announcements for third-party additions. I have a few more sitting on the back burner which may see the light some day. Hopefully, this article will inspire other developers to create and distribute ViewFrame additions of their own. The code for Mark's additions can be found on the source code disk for this issue of PDA Developers.