UNSAFE AT ANY SPEED: NEWTON HACKING John Schettino GTE Laboratories, Inc. js12@gte.com ************************************************************************************ This article is reprinted from issue 2.6 (Nov/Dec 1994) of PDA Developers magazine. Copyright(C) 1996 by Creative Digital Publishing Inc. All rights reserved. ************************************************************************************ You can admit it, you've been poking around inside your Newton. Maybe you've just nosed around with the Inspector, or maybe you're a full-blown ViewFrame junky. Think about all of the code required to replicate the functionality of a basic feature like the NotePad's overview window. Why do all that work when you can reuse Apple's code with some clever NewtonScripting? I've poked around and asked those questions - there's some interesting things in those 4MB of ROM. This article is all about accessing and using some of those gems Apple put in the Newton ROM, without getting into too much trouble. I doubt Apple would be happy with you using any undocumented methods, functions or data in a shipping application, but then again you just might want to at least look at what you can get away with. You can also fix some annoying things in the built-in apps. In this article I show you how to hack the NotePad and Calendar, how to "borrow" a keyboard definition, how to reuse the overview code in the NotePad and how to structure a floater app that "fronts" for another program (the NotePad in the example, but others work as well.) The "front" floater intercepts system messages to keep track of what's displayed in the application below it. Not only are these interesting hacks in themselves, but the approach used to identify and hook into existing code can be applied elsewhere. The first thing you've got to do is get into the proper frame of mind. When looking at implementing a user-interface component of a Newton program, consider the built-in applications and how they do similar functions. Hook up the Inspector or use a tool like ViewFrame, and have a look at what's going on when you interact with the visual elements. With primitive tools such as the Inspector, you're limited to two approaches: tracing and frame inspection. With ViewFrame you can snoop around until you have complete understanding. Little Hacks I - the NotePad The NotePad has several data elements in its root frame that you can use for fun and profit. The following slots are worth knowing more about. You access them via getroot().paperRoll.: dataCursor the cursor for the notes soup viewOriginY the # of pixels scrolled into the current note redochildren() method to call to refresh the NotePad display Using these three slots, you can make the NotePad into a Hypertext system. All you need do to link to a particular note is to keep a pointer to that entry (let's say you call it saveEntry). You can then make the NotePad scroll back to that note using the following code snippet: // assume we want to link the current note // save a pointer to the interesting entry saveEntry:= dataCursor:entry(); // use this code later, when you want to scroll back! // You need to test if it's a valid entry if IsSoupEntry(saveEntry) then begin getroot().paperroll.dataCursor:goto(saveEntry); // move database getroot().paperroll.viewOriginY := 0; // display to top-of-note getroot().paperroll:redochildren(); // redraw Note end; You can try this out in the Inspector. Move to a note (the datacursor:entry() is the note at the top of the NotePad display). Execute the first line above using the Inspector. Next, scroll up or down in the NotePad. Finally, execute the if...end code. Look at the NotePad screen - you're back at the top of the original note. Not bad for six lines of code. In production code you don't want to keep too many entries around because it uses up heap. As long as you don't access the entry, you won't use much at all. The entry is only brought into the frames heap when accessed. You can remove the in-heap image of an entry using entryUndoChanges(). You might want to check if the entry is dirty first via frameDirty(). Little Hacks II - the Calendar. The Calendar also contains slots that can be used to add and change its behavior. One annoyance for me is the way Alarms are handled. Don't get your hopes up - we're not fixing the part were an alarm goes off, we're fixing the part where you set an alarm. It just seems silly to have to tap the "alarm on" checkbox when setting an alarm for a new appointment. Also, 10 minutes is not my normal advanced warning period - I like a half hour. Let's dig in and see what the Calendar is doing. We start (using the Inspector) by examining the calendar root frame after a reset when the calendar is closed: getroot().calendar #4407429 {_Parent: {#441A139}, _proto: {#4B}, viewCObject: NIL, alarmTime: 47723630, alarmText: "Decker review", notifyData: [#44053C9]} Not much to go on. Let's go ahead and open Dates, and check again: getroot().calendar #4407429 {_Parent: {#441A139}, _proto: {#4B}, viewCObject: 0x1109E57, MonthOverView: {#440B659}, MeetingSoup: {#440B959}, dateFrame: {#44106A1}, mainDisplay: {#4412859}, selectedRepeatSoup: {#440C781}, YearView: {#440B8E1}, nextMonth: {#440B3E1}, target: <1>, StartTime: 480, viewFlags: 5, viewBounds: {#44109C1}, RepeatNotes: {#440E419}, viewclipper: 17866247, DatesOverviewForm: {#440B721}, DateView: {#4415D91}, alarmText: "Decker review", TodoOverView: {#440B7F9}, MonthTitle: {#4415CB1}, RepeatSoup: {#440C781}, SelectedDates: [#44105A9], notifyData: [#44053C9], cribView: {#4412079}, mailSlip: {#4406441}, MainTodo: {#440B739}, ScheduleView: {#4412859}, monthView: {#440A221}, previousMonth: {#440A2A9}, viewSetupFormScript: , StopTime: 960, selectedSingleSoup: {#440B959}, TodoButton: {#440B601}, alarmTime: 47723630, MeetingNotes: {#440B811}, targetView: <1>, notes: {#440D5B1}, base: <1>} Well, that's a start. Finally, create a new appointment. Draw a line from 1 P.M. to 2 P.M. Don't do anything else. Check one more time with the Inspector. Still nothing new. Tap on the new appointment you just made (it should bring up the detail window.) This time, try looking at the frontmost view: getview('viewfrontmost) #440A419 {_Parent: {#440CB19}, _proto: {#2FA081}, viewCObject: 0x110A252, startValue: {#4410E31}, stopValue: {#4410F49}, RepeatButton: {#4410941}, AlarmButton: {#44109A1}, ChangeRepeatingMeeting: {#44109B9}, RepeatingView: {#4410A29}, WarningView: {#4410A41}, mtgStartDate: 47723820, mtgDuration: 60, MinHourSpec: 170636, alarmTime: NIL, oldSpec: NIL, viewBounds: {#4410BD9}, base: <1>, viewFlags: 37} Aha! Where's this view defined in calendar? You can find out with DV: dv(getview('viewfrontmost)) MeetingNotes #440A419 |Header #4410C91 ||TitleLabel #4410CA9 ||MeetingText #4410CC1 ||StartLabel #4410D61 ||StartValue #44108D9 ||StopLabel #4410E79 ||StopValue #4410929 |EditView #4410F91 |Status #4410FF9 |Alarm #44109A1 |Frequency #4410941 |Delete #4411011 |cancel butto #4411029 This MeetingNotes slot must have something to do with the window we just created. Go ahead and examine the slots in it. You'll see several slots for variables, and the telltale _proto: slot. This slot (as you know) means there are read-only view system frames associated with this frame. It also means that you can hook into this frame with ease. Some more snooping and we're ready to write our patch. Enable the alarm for this appointment, and set the time for 30 minutes. Let's find our where those values are. First, use DV with the alarm window open on the meeting: dv(getview('viewfrontmost)) MeetingNotes #440A419 ... |WarningView #4410A41 That new view in the hierarchy must be our alarm window. Take one last look with the Inspector at this new slot: getroot().calendar.meetingnotes.warningview #4410B51 {_Parent: {#4410859}, _proto: {#2FA029}, viewCObject: 0x110AAA0, viewFlags: 33, AlarmOn: TRUE, unitsValue: 30} This is the way we would like this frame to look when the view is opened. If we can do that, then our new values will be used in place of the defaults compiled into the Calendar App. But how? If you look at the _proto slot, you find a viewSetUpformScript(). This method gets called before the view is created. If we insert our own viewSetUpformScript() into the Calendar App, ours is called instead. What if something important happens in the original one, and we forget to do it in ours? No problem - we call the original one as the very first step in ours. Here's an AutoPart Application that installs the patch: // This small patch is applied to the Calendar app // It changes alarm window for NEW meetings to default // to 30 minutes notice, and it enables the alarm InstallScript := func(packageFrame, removeframe) begin getroot().calendar.MeetingNotes.(ensureInternal( 'viewSetUpFormScript)) := func() begin inherited:?viewSetUpformScript(); // call overridden method if entryModTime(selectedMeeting) = 0 then // new meeting begin WarningView.unitsValue := 30; WarningView.AlarmOn := true; end; end; end; RemoveScript := func(packageFrame) begin removeslot(getroot().calendar.MeetingNotes, 'viewSetUpformScript) end; The two slots within the MeetingNotes frame that control the alarm function are unitsValue (an integer representation of the number of minutes of advanced warning for the alarm) and AlarmOn (a boolean.) We only want to affect newly created appointments, so we check the entryModeTime of the soup entry for the current meeting. It is zero if the entry has never been modified. The selectedMeeting slot in the root Calendar frame is a pointer to the currently selected entry. The Big Easy - Swiping a Keyboard. Setting up the keyDefinitions slot is not fun, it's very tedious. If one of the standard keyboards in the ROM has what you're looking for, borrow it. This viewSetupFormScript() for a keyboard view uses the keypad keys from the phoneKeyboard. Using the Inspector, you can examine the phoneKeyboard's _proto frame. There you find the viewChildren array. A little snooping reveals that the first child is the keypad. By examining the keyDefinitions, we see that the first four of these keyDefinitions are the dialing keys. The really cool thing is that the view system resizes these keys to your keyboard's viewBounds. Create a clKeyboardView in the NTK and delete the keyDefinitions slot. Create a viewSetupFormScript to install the ROM key definitions: func() begin local stdPhoneKeys := getroot().phoneKeyboard._proto.viewChildren[0]. keyDefinitions; keyDefinitions := [stdPhoneKeys[0], stdPhoneKeys[1], stdPhoneKeys[2], stdPhoneKeys[3]]; end Bigger Hacks I - the Calendar. This one's for my wife: she hates the fact that the month view displayed in the Calendar when it's in a day or to-do view does not indicate which days have appointments set for them. This feature is in every $50 electronic organizer, yet our high-powered Newton is unable to do it. Let's fix that problem. What I would like to be able to do is draw boxes around the days that have Day notes in the current month and draw a line to the right of each day with meetings. I draw a line near the top of the day if there are any meetings in the morning, and near the bottom if there are any in the afternoon (see Figure 1). The code for this Auto Part is too large to insert into the article [it's included on the source disk for this issue] because I have to dig through four soups just to find all the meetings for a month. I just show the snippets relating to hacking it into the Calendar. To hook in this patch, we need to know whenever the month displayed in the calendar changes. The NTK documentation would have us believe that a monthChangedScript() message is sent via the proto when this happens. If you hunt around in the Inspector in the calendar app, you indeed find a monthChangedScrip() in there. Overriding this method does not seem to produce the desired results, however. This calls for serious measures. We first use the trick of reading the method names in the _proto frame. We find a suspect: redisplayScript() sounds a lot like what we want. Is it called whenever the calendar changes? Let's install a little patch and then exercise the Calendar (I also go ahead and slip in an additional message to override viewSetUpdoneScript. Enter this into the Inspector: {test:func() begin getroot().calendar.(ensureinternal ('redisplayScript)) := func() begin print ("HELLO (redisplayScript)!"); print (selectedDates[0]); print (visible(getroot().calendar.monthview)); inherited:?redisplayScript(); end; getroot().calendar.(ensureinternal ('viewSetUpdoneScript)) := func() begin print ("HELLO (viewSetUpdoneScript)!"); print (selectedDates[0]); print (visible(getroot().calendar.monthview)); inherited:?viewSetUpdoneScript(); end; end; }:test() It turns out that this is exactly what we need: viewSetUpdoneScript is always called when Calendar is opened, and redisplayScript() is called whenever the user selects a new date, adds a meeting or deletes a meeting. If we have our Auto Part install a hook into these methods, we have a chance to draw our extra information at just the right times. Bigger Hacks II - Fronting for NotePad Let's say you're interested in creating an application that floats above the NotePad, and acts on the currently displayed note. There's only one problem (well, OK, there's probably more than one) - how do you know when the currently displayed note changes? The paperRoll application is not going to tell you. You can always use an idle routine to check the dataCursor:entry() and see if it changes. Not too cool, using up the batteries when you can find out via clever coding. The current note changes during Scrolling, selecting from an Overview, Finding, Filing, Routing and when the user creates a new note. Sounds like we have to cover these bases if we want to know too. Scrolling If we enable the vApplication flag for our floater application, we receive viewScrollUpScript(), viewScrollDownScript() and viewOverviewScript() messages from the system when the user taps on those buttons. For ScrollUp and ScrollDown, our program can simply pass the message on to the application under it. Our programs then checks to see what is running underneath it. If its the one we're fronting for (paperRoll) we can check to see if the dataCursor:entry() points at a new note (in our check-NewEntry() method) when we get back. An example for ScrollUp is shown below: viewScrollUpScript: func() begin // get frontmost view (not us, we're a floater!) local frontView := GetView('viewFrontMostApp); frontView:viewScrollUpScript(); // pass the message if frontView.appSymbol = 'paperRoll and :checkNewEntry() then :newEntryAction(); // new entry! end, Overview The Overview action is a bit more problematic. Let's exercise it while attached with the Inspector and see what we can learn. First let's do an overview in notes. Check the protoRoll root frame before and after you pop up an overview. Notice anything different? We've got a new slot called paperover. Looks like a prime suspect for the view we just created. We use a similar approach to that taken with the Calendar hack, in that we override one of the standard methods to do our bidding. What we really want to know is: when is the user done with the overview? Or, when can we check to see if the NotePad's at a new note? It seems to me that when the view closes it's pretty much done, so let's add some code to the viewQuitScript(). Here's the complete code for the viewOverviewScript() method: func() begin // get frontmost view (not us, we're a floater!) local frontView := GetView('viewFrontMostApp); frontView:viewOverviewScript(); // pass along the message. once we passed on the // message, an overview view was created. // we'll piggyback onto the quit message if frontView.appSymbol = 'paperRoll then begin // get new frontmost view (the overview window) local frontView := GetView('viewFrontMostApp); frontView.(ensureinternal('viewQuitScript)) := func() begin // call base viewQuitScript(close the overview) inherited:?viewQuitScript(); local myapp := getRoot().(kAppSymbol); // find myself since I'm executing in the // overview's self if myapp and getView(myapp) and // we're around and open myapp:checkNewEntry() then // on a new entry myapp:newEntryAction(); // new entry! // as a final action, clean up the frame removeslot(frontView,'viewQuitScript); end; end; end, Find As you probably know, Newton's Find calls the showFoundItem() method in your application when it wants you to display a found item. Make sure you check to see the number of parameters the program you're hacking has in its showFoundItem() method - there are two types, one that takes a single parameter and one that takes two. What if we just sneak into the NotePad's frame and insert our own code? We get called whenever a found record is displayed. Here's a code snippet that's just the ticket: getroot().paperRoll.(ensureInternal( 'showFoundItem)) := func(a,b) begin inherited:?showFoundItem(a,b); // call overridden method local myapp := getRoot().(kAppSymbol); // find myself since I'm executing in the // paperRoll's self if myapp and getView(myapp) and // we're around and open myapp:checkNewEntry() then // on a new entry myapp:newEntryAction(); // new entry! end; Add this code in the fronting application's installScript. You also must remember to clean up after yourself. Add this code to the removeScript: removeslot(getroot().paperRoll,'showFoundItem); Deleting via Find If the user does a Find and deletes all found items, they might delete the current entry. This is one case where the soupChanged method is called by the system. If you add a soupChanged handler for the soup you're tracking, you get called too. You can check if your entry is still valid there. Filter Change When the user changes the filter for an application that supports filing, a filterChanged() message is sent. Override this message just like showFoundItem() above to check if the currentEntry has changed. Adding a New Entry You would hope you could use the soupChange() notification for detecting when the NotePad adds a new note. No such luck, since it doesn't follow the rules. Here's a case where tracing can do the trick. All you need to do is turn tracing on, make a new note, and turn tracing off. OK, you also have to sort through a million lines of code, but after that you'll have the answer. Here's a trace := 'functions log from making a new note. I took the liberty of editing out the uninteresting parts. trace := 'functions #23A8D9 functions Calling StrokeBounds(18070423) Sending SetNoteHeight(#4406A29, 114) to #4406C39 Calling Time() Sending NewNote(#44105B9, NIL, NIL) to #4406939 Notice that call to NewNote(#44105B9, NIL, NIL). That's our last hook. We add code that overrides this method, just like all the others: getroot().paperRoll.(ensureInternal( 'NewNote)) := func(a,b,c) begin inherited:?NewNote(a,b,c); // call overridden method local myapp := getRoot().(kAppSymbol); // find myself since I'm executing in the // paperRoll's self if myapp and getView(myapp) and // we're around and open myapp:checkNewEntry() then // on a new entry myapp:newEntryAction(); // new entry! end; Add this code in the fronting application's installScript. You also must remember to clean up after yourself. Add this code to the removeScript: removeslot(getroot().paperRoll,'NewNote); We've now covered all the bases, and can tell just exactly what the NotePad is doing. Of course, this may all go out the window with the next OS release, but you can just get back in there with your Inspector and find the new behavior. Bigger Hacks II - Uh, Can I Borrow That View Frame? Reuse has been a hot topic in software development for years now, so we're going to really get with the program and start reusing large chunks of other people's code. The overview code from the NotePad springs to mind. Consider all the work that went into designing the view, as well as writing and debugging the code. Let's use it, lock, stock, and barrel! All you need is a cursor from a query on the Notes soup and you too can display the overview. func() begin local over := getroot().paperroll._parent.paperover; local cursor := fcsForNotepad:query(); if not cursor then return; getroot().JCSsavecursor := getroot().paperroll.dataCursor:clone(); getroot().paperroll.dataCursor := cursor; over.viewQuitScript := func() begin currententry := overCursor:entry(); getroot().paperroll.dataCursor := getroot().JCSsavecursor; getroot().paperroll.datacursor:goto(currententry); // force top-of-note getroot().paperroll.viewOriginY := 0; getroot().paperroll:redochildren(); inherited:?viewQuitScript(); removeslot(getroot().paperroll._parent.paperover, 'viewQuitScript); removeslot(getroot(),'JCSsavecursor); end; over:open(); end, I found this frame in the ROM in the usual way: I opened an Overview and then hunted around in the Inspector. In this case, getView('viewfrontmostapp) and DV() were my main tools. Once I had access to the frame I wanted, it was just a matter of hacking around in the Inspector until something worked. The routine above does the following: first, get the overview frame out of ROM. Stash away the cursor used by the NotePad in a safe place. Next, replace it with my own cursor. Then override the viewQuit-Script for the overview with my own. It cleans up this little hack by restoring the original cursor, positioning it to the selected entry, and removing all traces of itself. The stage is set: all that remains is to open the hacked view. Hacking Hiatus We're all done breaking the rules. Remember, this code is not product-ready or lawyer approved. Apple does not condone the hacking of their applications in this way. It sure is fun, though, and can be very useful as well. The techniques shown here can be used for examining any existing program for reuse opportunities or customization purposes. John Schettino is a Computer Science researcher at GTE Laboratories, Inc. His background includes Object-Oriented analysis, design, and implementation, as well as Relational and Object-Oriented databases. He has done extensive work in the area of software reuse. He holds a BS and an MS in Computer Science, with a minor in Software Engineering.