TweetFollow Us on Twitter

Coding Your Object Model for Advanced Scriptability

Coding Your Object Model for Advanced Scriptability

Ron Reuter

Implementing an Apple event object model makes your application scriptable -- that is, it enables users to control your application with AppleScript or some other OSA-compliant language. You can provide anything from a basic implementation of the object model that handles only simple scripts to a full implementation that can handle the most complex scripts. This article will help you do the latter. It will show you how to write object accessors and handlers that process lists of objects, alert you to some common pitfalls, and suggest other features you can add for completeness.

You've decided to give your users an alternate interface for controlling your application by implementing an Apple event object model. You've read Richard Clark's article, "Apple Event Objects and You," in develop Issue 10 to get an overview of the Apple event object model and how to support it. As you've begun to think about your scripting vocabulary, you've absorbed Cal Simone's article, "Designing a Scripting Implementation," in develop Issue 21 and his According to Script column in Issue 24. You've checked out the portions of Inside Macintosh: Interapplication Communication that apply. You've read and understood the Apple Event Registry, which defines the primary events and objects that you should support in a scriptable application, and have paid particular attention to the Core, Text, and QuickDraw Graphics suites.

With this basic knowledge, you're ready to read this article. Here you'll learn how to structure your code to handle more complex user scripts. After a brief review of the components of an object model implementation, I'll focus on object accessors and show you how to handle script statements that require your code to act on a list of objects. Then I'll describe in detail how to deal with three big "gotchas" that are bound to trip you up unless you know about them. Finally, I'll tell you about some other goodies you can implement for the sake of completeness. All of this is illustrated in the sample application Sketch, which demonstrates object model support for a subset of the QuickDraw Graphics suite. The code for Sketch, which accompanies the article on this issue's CD and develop's Web site, contains many functions that you can use when you get ready to code the object model for your own application.

Components Of An Object Model Implementation

The components of an object model implementation are outlined in the "Apple Event Objects and You" article and discussed in great detail in Inside Macintosh: Interapplication Communication. Here I'll briefly review the basic terms and concepts to refresh your memory and to show how they apply in our sample program.

When a script statement asks your application to perform an action on some object, such as closing document 1, the object specifier (document 1) must be resolved -- that is, the representation of the specified object must be located in memory. Your application resolves the object specifier by way of an object accessor function that converts the object specifier into a token. The token is then passed to an event dispatcher for that object. I'll describe each of these components before reviewing the process of resolving object specifiers and dispatching events.

Object accessors are functions you write and install in an accessor table. These functions are called by the Object Support Library (OSL) function AEResolve when the Apple Event Manager needs to find some object in your application's data structures. Object accessors receive a container, an object specifier for an object to locate inside that container, and a result parameter into which a token is placed. When you install accessors, you tell the Apple Event Manager that your application knows how to find a certain kind of object in a certain kind of container. For instance, you know how to find a rectangle object in a grouped graphic object or a word object in a paragraph object. (More on containers in a minute.)

A token is an application-defined data structure that is populated in your object accessors and is passed later to your object's event dispatcher code, where it's used to find the object that an Apple event will be applied to. The structure and content of a token are private to the application; neither the Apple Event Manager nor the OSL attempts to interpret or use the contents of a token. The Sketch sample application uses a single token structure, shown below, for all of its objects. Note that some fields aren't used for all object types and that the token doesn't contain the object's data or a data value; it contains information about how to locate the object later. You can use a single token structure in your implementation, or you may want to design a unique token structure for each object you support.

typedef struct CoreTokenRecord {
   DescType   dispatchClass;   // class that will handle an event
   DescType   objectClass;     // actual class of this object
   DescType   propertyCode;    // requested property code, 
                               // or typeNull if not a property token
   long       documentNumber;  // unique ID for the document, or 0
   long       elementNumber;   // unique ID for the element, or 0
   WindowPtr  window;          // used for window objects only
} CoreTokenRecord, *CoreTokenPtr, **CoreTokenHandle;

Event dispatchers are application-defined functions that you call after you've called AEResolve and your object accessors have returned a token for the target of the Apple event. You call your event dispatchers and pass the token you created, the original Apple event, and the reply Apple event you received in your Apple event handler. The event dispatcher examines the Apple event, extracts the event ID, and passes its parameters on to a specific event handler for the token object. The token is used to identify the object or objects that the Apple event should act on.

Apple event handlers are functions you write and install that receive a specific Apple event and a reply Apple event. Your event handlers extract parameters from the Apple event, process the event using those parameters, and place the result in the reply Apple event. A single handler can be installed to handle many events. For example, one handler can receive all events in the Core suite, except the Create Element event, if you specify kAECoreSuite for the event class and typeWildCard for the event ID. Because Create Element passes an insertion location instead of an object specifier in the direct object parameter, Sketch installs a separate handler, AECreateElementEventHandler, to handle this event for all Core suite objects.

The Sketch sample code resolves object specifiers and dispatches Core suite Apple events by using the object-first approach. The object-first flow of control proceeds as follows:

  1. In the Core suite event handler, extract the parameter for the direct object of the Apple event. Except in the case of the Create Element event, this is a reference to some object the user is trying to access or modify.

  2. Call AEResolve, which calls one or more of your object accessor functions, each of which finds the requested object in a specified container and then returns a token. AEResolve successively calls object accessors until one of the following three conditions is met: you return a token for the specified object; you return an error code; or AEResolve finds a container-element combination for which there's no installed accessor.

  3. Examine the token to determine what kind of object it references and then send the original event, the reply event, and the token to the event dispatcher for that type of object.

  4. In the object's event dispatcher, extract the event ID and dispatch the event and the token to the object's event handler.

  5. In the object's event handler, apply the event to the object or objects referenced by the token and return the results in the direct object of the reply Apple event. You usually just unpack the parameters in your event handler and then call lower-level functions you've written to do the application-specific work.

Figure 1 shows how this approach is applied as Sketch processes the script statement

set fill color of rectangle 1 of document 1 to blue

Figure 1. How Sketch processes set fill color of rectangle 1 of document 1 to blue

Note that Sketch has one file for each type of scriptable object. Figure 1 shows fragments of three files:

  • AECoreSuite.c, which receives all Apple events from the Core suite, resolves the direct object parameter, and dispatches the token and the Apple event to the dispatcher for a specific object type

  • OSLClassDocument.c, which contains accessors, a dispatcher, and event handlers for document objects

  • OSLClassGraphicObject.c, which contains accessors, a dispatcher, and event handlers for all graphic objects

Object Accessors And Your Containment Hierarchy

How you implement your object model will depend largely on the nature of your data and on your containment hierarchy. Your containment hierarchy specifies the objects you support, how script statements should address those objects, and which objects are contained by which other objects. Contained objects are called elements of the container object. Each object also usually contains one or more properties, which represent that object's characteristics, such as font size or color. While an object can contain many elements of a particular type, it contains only one of each of its properties. A script identifies the object to inspect or change by way of an object reference, which specifies the object's location in the containment hierarchy.

Sketch has the containment hierarchy shown in Figure 2. The application object can contain both windows and documents. Documents, in turn, contain objects defined in the QuickDraw Graphics suite, such as rectangles, ovals, graphic lines, and graphic groups. Graphic groups can contain any object from the QuickDraw Graphics suite, including other graphic groups.

Figure 2. The Sketch containment hierarchy

The following complete script navigates through Sketch's containment hierarchy from top to bottom to get a property of an object:

tell application "Sketch"
   tell document "Sales Chart"
      tell rectangle 1
         get fill color
      end tell
   end tell
end tell

Throughout the rest of this article, I'll usually show script fragments consisting of a single statement instead of complete scripts.

The desired object can be specified in one of several ways in a script statement, as you'll see later in the discussion of key forms. Theoretically, for each container-element combination in your containment hierarchy, you need an object accessor function that can find the element type in its container type. In reality, you frequently can get by with a single object accessor function that can handle many container-element pairs, rather than having to write and install a separate function for each one.

The Apple Event Registry lists the elements that can be contained within each object it defines. Recursive definitions occur frequently in the Registry. For example, the word object in the Text suite can contain characters, words, lines, paragraphs, and text. While it seems reasonable that a word can contain a character, when would a word contain a line or a paragraph? Suppose the script asked to do something to words 1 through 200. This is an example of a range specifier, which we'll look at in more detail later. Your application might resolve this range specifier into a list of 200 word objects. Because there could be many paragraphs within that range, asking for paragraph 2 of words 1 through 200 would make sense. It's to support range specifiers that every text object is required to be an element of every other text object.

The upshot of this is that to support the word object in the Text suite, you would need to write object accessors to resolve all these possible containment scenarios: word-from-character, word-from-word, word-from-line, word-from-paragraph, word-from-text, and either word-from-document (for a text editor that supports one large text object per document) or word-from-graphic-text (for a drawing application that supports many text boxes per document). As mentioned earlier, though, you frequently can get by with a single object accessor function that can handle many container-element pairs. Sketch, for example, uses just two object accessors to support all objects in the QuickDraw Graphics suite: GraphicObjectFromDocumentAccessor and GraphicObjectFromGroupAccessor, both of which call GraphicObjectAccessor to do the real work of finding a graphic object.

Object Accessors And Key Forms

Script statements can ask for an object or a collection of objects in a variety of ways. They can ask for a single object by its unique ID, by name, or by its absolute or relative position in a container. A script can also ask not for an object, but for some property of an object, such as the fill color of a rectangle or the font of a paragraph. A script statement can ask for more than one object by using the word every, by specifying a range between some object and some other object in a container, or by specifying a test that the objects must satisfy. The method that's used to reference an object or objects in a script determines the keyForm parameter that an object accessor function will receive when it comes time to resolve the object specifier.

When an object accessor receives one of the simple key forms and associated key data types listed in Table 1, it returns a descriptor containing a token that references a single object in your application. When it receives one of the complex key forms and associated key data types listed in Table 2, it returns a descriptor containing a list of tokens, each of which references a single object.

Table 1. Simple key forms

Key form Key data type Key data value
formUniqueID typeLongInteger A unique number
formPropertyID typeEnumerated An identifier declared in your 'aete' resource
formName typeIntlText The name of an object, such as a document
formAbsolutePosition typeLongInteger
typeAbsoluteOrdinal
A positive or negative number
kAEFirst, kAEMiddle, kAELast, kAEAny
formRelativePosition typeEnumerated kAENext, kAEPrevious

Table 2. Complex key forms

Key form Key data type Key data value
formAbsolutePosition typeAbsoluteOrdinal kAEAll
formRange typeRangeDescriptor See section "Handling formRange."
formTest See section "Handling formTest and formWhose."
formWhose See section "Handling formTest and formWhose."

Note that not all key forms are appropriate for all classes -- a rectangle might not have a name, for example, and some objects, such as a word or a paragraph, might not have a unique ID. According to Inside Macintosh: Interapplication Communication, if a key form isn't supported for an object in one of your containers, you should return errAEEventNotHandled. But you might want to return a more specific error code, such as errAEBadKeyForm or errAENoSuchObject.

Handling Simple Key Forms

Handling the simple key forms is mostly straightforward. Table 3 shows some examples of script fragments using simple keys and their results. For these examples, assume the script is looking at a text block that contains the words "Hi there" in 12-point Helvetica type. For the first two examples, formAbsolutePosition is the key form; for the third example, the key form is formPropertyID, and for the fourth example it's formRelativePosition.

Table 3. Script fragments using simple keys and their results

Script fragment Result type Example result
word 2 word "there"
character 1 of word 2 character "t"
size of word 1 number (font size) 12
word before word 2 word "Hi"

Although formRelativePosition is a simple key form, there's one aspect of handling it that might not be obvious. The container parameter that your object accessor receives in this case is a reference not to a container but to an object inside a container in relation to another object inside that container. In other words, if a script asks for an object before or after another object in a container, as in

get name of the window after window "Sales Chart"

your object accessor will receive a keyForm parameter of formRelativePosition and a keyData parameter that contains a constant, either kAENext or kAEPrevious. Your accessor must then find the object either before or after the "contained" object. This means that to handle formRelativePosition, you'll have to install an accessor that gets an object of one type from another object of the same type.

Although the containment hierarchy for Sketch shows that windows don't contain other windows, you will need a window-from-window accessor installed to handle formRelativePosition. If your accessors can find an object in a container, finding an object either before or after that object should be relatively easy, as long as you remember to install the accessor. Here's how Sketch installs the accessor for its window object:

error = AEInstallObjectAccessor(cWindow, cWindow,
   NewOSLAccessorProc(WindowFromApplicationAccessor), 0L, false);

Handling Every

If a script asks for every one of a certain kind of object, your accessor will receive a keyForm parameter of formAbsolutePosition and a keyData parameter with a descriptor type of typeAbsoluteOrdinal and a value of kAEAll, and you'll return a descriptor that represents a collection of objects. The Sketch application returns an AEList of tokens that reference each object. Some examples of script fragments using every and their results are shown in Table 4. Again, assume the script is looking at a text block that contains the words "Hi there" in 12-point Helvetica type.

Table 4. Script fragments using every and their results

Script fragment Result type Example result
every word List of words {"Hi", "there"}
character 1 of every word List of characters {"H", "t"}
every character of every word List of list of characters {{"H", "i"}, {"t", "h", "e", "r", "e"}}
font of character 1 of every word List of strings {"Helvetica", "Helvetica"}
size of every character of every word List of list of numbers {{12, 12}, {12, 12, 12, 12, 12}}

Each every specifies another list level: one every will return a list, two will return a list of lists, and so on. Consider, for instance, this statement that navigates through the Text suite hierarchy:

get every character of every word of every paragraph of every document

An application could handle this statement by returning a descriptor containing a four-level list of character tokens. Alternatively, an application could return a flat list (a single-level list of objects all concatenated together), but I don't recommend this practice because it assumes that the information about the deep structure that's thrown away won't be needed for any subsequent processing in the script, and there's really no way to know that reliably.

AEResolve and your individual object accessors have no way to know how deep a list will end up being, but your code that handles the Apple event after the object resolution has been completed must do the right thing with a descriptor referencing a single object and with a descriptor that contains arbitrarily deep lists of such objects.

Handling formRange

If the script asks for objects between some object and some other object in a container, your object accessor for that container will receive a keyForm parameter of formRange. There are many ways to specify a range of objects in a script:

get the fill color of rectangles 1 through 3
get the location of windows from window "Hello" to window 4
get the bounds of graphic objects from oval 1 to rectangle 3

Note that the beginning and ending objects can be specified with different key forms and that they might even be two different object types, as in the third example. Regardless of how they're specified, you need to resolve the two object specifiers and return a descriptor containing a list of tokens for the objects from the first through the last object in the range.

Your object accessors are called three times to completely resolve a formRange statement. On the first call to an object accessor, you receive a key form of formRange and key data that contains a typeRangeDescriptor record. In Sketch, this information is passed on to the ProcessFormRange function, shown in Listing 1. ProcessFormRange begins by coercing the range record into a regular record, which will then contain two object specifiers. Next, it extracts the first descriptor from the record and calls AEResolve, which calls your object accessors again to get a token for the first object in the range. Finally, ProcessFormRange extracts the second descriptor and calls AEResolve again to get a token for the last object in the range. ProcessFormRange is called from your object accessor, and when it returns you'll have tokens for the two boundary objects in the range. Your object accessor then builds a list of all objects in the range and returns that list in the result token.

Listing 1. Resolving the boundary objects for a range request

OSErr ProcessFormRange(AEDesc *keyData, AEDesc *start, AEDesc *stop)
{
   OSErr    error;
   AEDesc   ospec = {typeNull, NULL};
   AEDesc   range = {typeNull, NULL};

   // Coerce the range record data into an AERecord. 
   error = AECoerceDesc(keyData, typeAERecord, &range);
   if (error != noErr) goto CleanUp;
    
   // Resolve the object specifier for the first object in the range.
   error = AEGetKeyDesc(&range, keyAERangeStart,
               typeWildCard, &ospec);
   if (error == noErr && 
         ospec.descriptorType == typeObjectSpecifier)
      error = AEResolve(&ospec, kAEIDoMinimum, start);
   if (error != noErr) goto CleanUp;
   AEDisposeDesc(&ospec);

   // Resolve the object specifier for the last object in the range.
   error = AEGetKeyDesc(&range, keyAERangeStop, typeWildCard,
               &ospec);
   if (error == noErr &&
          ospec.descriptorType == typeObjectSpecifier)
      error = AEResolve(&ospec, kAEIDoMinimum, stop);

CleanUp:
   AEDisposeDesc(&ospec);
   AEDisposeDesc(&range); 
   return error;
}

Handling formTest And formWhose

If the script asks for objects that satisfy some test, such as

get the fill color of every rectangle whose rotation is 45

you'll return a descriptor containing a list of tokens referencing those objects. Fortunately, once you've added support for list processing, you only need to install two functions to gain the incredible power of whose statements: an object-counting function and an object-comparison function. The object-counting function counts the number of objects of a specified class in a specified container. Let's say that your document has three rectangles that are rotated to 45 degrees, and another three that aren't rotated. When the OSL calls your counting function, you return 6, the total number of rectangles in the document container. Now the OSL knows that it has to call your object-comparison function six times, once for each rectangle.

The object-comparison function is given two descriptors and a comparison operator and returns true if the two descriptors satisfy the comparison operator, or false if they don't. For the example above, one descriptor will be an object specifier, such as rotation of rectangle 1, and the second descriptor contains the raw data, 45. You need to resolve the first descriptor, a formPropertyID reference, to get the rotation value for that object. Then you use the comparison operator to compare the resolved property value with the raw comparison data. If the comparison is valid, you return true; otherwise, you return false. When you return true, the OSL adds the token representing the rectangle under consideration to a list of objects that satisfy the test. To make sure the OSL handles formTest and formWhose for you in this way, be sure to specify kAEIDoMinimum as the second parameter to AEResolve.

Because you can have only one counting function and one comparison function installed, they need to be able to work with all of your container types and all the object types you support. The good news is that if you've added support for basic object model scriptability, you've already got most of the functions spread around that do most of the work you'll need to do in your counting and comparison callbacks. Sketch includes both an object-counting function and an object-comparison function, plus a variety of comparison functions for different data types.

Depending on the OSL to handle whose clauses in this way has one drawback -- it can be inefficient when there are a large number of objects. The OSL will call your accessors to find each object and then it will apply the comparison to each one. If you find that this is too slow, you can go the extra mile and handle resolution of whose clauses yourself. For details, see "Speeding Up whose Clause Resolution in Your Scriptable Application" by Greg Anderson in develop Issue 24.

Introducing The Three Big Gotchas

Handling the key forms and the lists your object accessors can return goes a long way toward making an object model implementation capable of handling complex user scripts. But there's more you need to do -- namely, you have to know about the three big gotchas so that you can avoid getting into trouble with them.

I first encountered the gotchas while I was taking the "Programming Apple Events" course at Apple Developer University. The instructor, James Sulzen, was showing us some slides when he boldly exclaimed, "And here is the most important slide in the course!" It was the slide listing the gotchas I'm about to describe. But I didn't discover just how correct his pronouncement was until sometime later, when I'd read the Registry several times over and had started implementing an object model for a high-end graphics package. Simply stated, the gotchas are these:

  • Any Apple event parameter can be an object specifier.

  • Any resolution can return either a descriptor containing a token for a single object or a descriptor containing a list of tokens.

  • The meaning of a token's contents must be preserved during the execution of an Apple event that uses that token.

I'll explain each of these and describe what you need to do in your code to deal with them.

Gotcha #1: The "Any Parameter" Gotcha

Any Apple event parameter can be an object specifier.

To help you grasp the implications of this gotcha, let's look first at a script that results in sending your application a keyData parameter that's not an object specifier:

set stroke size of rectangle 1 of graphic group 2 to 3

In your 'aete' resource, you've included the QuickDraw Graphics suite that defines a rectangle object and its stroke size property. When a script sends the above statement to your application, your accessors will be called to find rectangle 1. In this example, accessors for document-from-application, group-from-document, and rectangle-from-group will be called. The last accessor, the one that actually finds the rectangle, returns a token that will allow your event handler to find this specific rectangle later. Next, since AEResolve has done its work, your Core suite dispatcher examines the type of object the token refers to and dispatches it to the appropriate object's event dispatcher.

Your event dispatcher looks at the Apple event ID and determines that it's a Set Data event, so it calls the object's Set Data event handler, passing in the token returned from your object accessor, the original event, and the reply event. In the object's event handler, you examine the token to determine that it references the stroke size property of a particular rectangle, and you examine the Apple event to extract the keyData parameter, which contains the value 3. Finally, you update the data structure that represents that rectangle, setting the stroke size to 3, and probably do something to generate an update event so that the screen is redrawn to show the rectangle's new visual appearance.

Now, suppose the user typed a slightly different statement:

set stroke size of rectangle 1 to the stroke size of oval 2

This time the keyData parameter isn't a simple number like 3 but is instead an object specifier, stroke size of oval 2. There's only one way to convert this to a value to use to set the stroke size of rectangle 1 -- you have to resolve the keyData parameter. You first have to resolve the object specifier to acquire a token that references the stroke size of oval 2, and then, since you need the actual value of that property for the Set Data event, you must use that token and emulate a Get Data event to extract that value from oval 2.

How to deal with gotcha #1. Again, gotcha #1 says any parameter to an Apple event can be an object specifier. Since this is the case, we might as well write a generic function that extracts parameters from an Apple event and that can handle parameters that contain raw data as well as parameters that contain object specifiers. Sketch uses this approach, calling its ExtractKeyDataParameter function from its Set Data event handlers.

The ExtractKeyDataParameter function, shown in Listing 2, extracts the key data from the Apple event without changing its form. It then passes that data to the ExtractData function (Listing 3), which looks at the descriptor type and calls AEResolve if it determines that the source parameter contains an object specifier. ExtractData can receive an object specifier, an object token, a property token, or raw data (text, number, and so on); it converts whatever it receives into raw data and returns that. Besides being called from ExtractKeyDataParameter, it's also called by the OSLCompareObjectsCallback function, which is used to resolve whose clauses.


Listing 2. Extracting the keyData parameter from an Apple event

OSErr ExtractKeyDataParameter(const AppleEvent *appleEvent, 
   AEDesc *data)
{
   OSErr    error = noErr;
   AEDesc   keyData = {typeNull, NULL};

   error = AEGetKeyDesc(appleEvent, keyAEData, typeWildCard,
               &keyData);
   if (error == noErr)
      error = ExtractData(&keyData, data);
   
   AEDisposeDesc(&keyData);
   return error;
}

Listing 3. Extracting raw data from a descriptor

OSErr ExtractData(const AEDesc *source, AEDesc *data)
{
   OSErr       error = noErr;
   AEDesc      temp = {typeNull, NULL};
   DescType    dispatchClass;
      
   if ((source->descriptorType == typeNull) ||
       (source->dataHandle == NULL)) {
      error = errAENoSuchObject;
      goto CleanUp;
   }
   
   // If it's an object specifier, resolve it into a token; 
   // otherwise just copy it.
   if (source->descriptorType == typeObjectSpecifier)
      error = AEResolve(source, kAEIDoMinimum, &temp);
   else error = AEDuplicateDesc(source, &temp);
   if (error != noErr) goto CleanUp;
   
   // Next, determine which object should handle it, if any.
   // If it's a property token, get the dispatch class.
   // Otherwise, it's either an object token or raw data.
   if (temp.descriptorType == typeProperty)
      dispatchClass = ExtractDispatchClassFromToken(&temp);
   else dispatchClass = temp.descriptorType;
   
   // If it's a property token, get the data it refers to;
   // otherwise just duplicate it.
   switch (dispatchClass) {
      case cApplication:
         error = errAEEventNotHandled;
         break;
      case cDocument:
         error = GetDataFromDocumentObject(&temp, NULL, data);
         break;
      case cWindow:
         error = GetDataFromWindowObject(&temp, NULL, data);
         break;
      case cGraphicObject:
         error = GetDataFromGraphicObject(&temp, NULL, data);
         break;
      default:
         // This is raw data or a nonproperty token.
         error = AEDuplicateDesc(&temp, data);
         break;
   }

CleanUp:
   AEDisposeDesc(&temp);
   return error;
}

There are some circumstances where extracting raw data isn't the correct thing to do, as in

set selection of application "Sketch" to oval 2

In this case, we just want to return the token for oval 2, not some property data as in the previous example. To handle this case, ExtractData checks to make sure that the token's propertyCode field doesn't contain typeNull before we dispatch the token to one of the GetDataFrom functions. If it isn't a property token, we just return the token itself and not its data.

Gotcha #2: The "Any Resolution" Gotcha

Any resolution can return either a descriptor containing a token for a single object or a descriptor containing a list of tokens.

As noted earlier, the presence of every in a script statement, or a range request, or a whose statement all require that you generate and return a descriptor containing a list of tokens. Let's look at a script statement and follow the resolution process as it calls each of our accessors in turn. Here's the statement:

get every character of word 2 of every line of paragraph 2 of document 1

Let's assume document 1 looks like this:

Hello there!¶
This text block contains three lines
and two of them are long but one
is not.¶

In the Core suite, AEResolve works from the top of the containment hierarchy down to the requested object, so in our example it first calls the document-from-application accessor, which returns a token identifying the frontmost document. I'll introduce a notation here, where a letter refers to the object type, and a number refers to an index, so "D1" means "document 1."

resolve "document 1" => D1

Next, AEResolve asks us to find a paragraph by calling our paragraph-from-document accessor, which returns a token for paragraph 2:

resolve "paragraph 2 of document 1" => D1P2

Next, AEResolve calls our line-from-paragraph accessor. Because of the every keyword, we must return a list of tokens:

resolve "every line of paragraph 2 of document 1" =>
   {D1P2L1, D1P2L2, D1P2L3}

Next, AEResolve asks for word 2 and calls our word-from-line accessor. In this case, however, our accessor must be able to find a word in each token in a list of line tokens. The accessor's result is a list of word tokens. The list depth doesn't change, because the statement doesn't ask for every word.

resolve "word 2 of every line of paragraph 2 of document 1" =>
   {D1P2L1W2, D1P2L2W2, D1P2L3W2}

The final resolution asks for every character of each of those three words. Because this is our second every in the statement, we know we're going to return a list of lists:

resolve "every character of word 2 of every line of paragraph 2 of
document 1" => 
   {{D1P2L1W2C1, D1P2L1W2C2, D1P2L1W2C3, D1P2L1W2C4},
    {D1P2L2W2C1, D1P2L2W2C2, D1P2L2W2C3},
    {D1P2L3W2C1, D1P2L3W2C2, D1P2L3W2C3}}

or, as it would be displayed as an AppleScript result:

{{"t", "e", "x", "t"}, {"t", "w", "o"}, {"n", "o", "t"}}

A list of tokens can also be accumulated by the OSL in the course of handling a whose clause. For example, consider the following statement:

resolve "every word of paragraph 2 of document 1 that contains
"e"" =>
   {"text", "three", "lines", "them", "are", "one"}

When this statement is resolved, the OSL will call your object accessors for word 1 through word 16 of the token for paragraph 2 of document 1 and pass each word token to your object-comparison function. Those tokens that match (words that contain the letter e in this example) are copied into an AEList with AEPutDesc, and the original is disposed of with AEDisposeDesc. Tokens that don't match are disposed of with your token disposal callback if you've installed one, or with AEDisposeDesc otherwise.

There's a corollary to gotcha #2: Any token list can be or can contain an empty list or lists. Given the statement

get every character of word 3 of every line of paragraph 2 of
document 1

we must deal with the fact that line 3 (the last line) of paragraph 2 contains only two words. What then should we do with "word 3 of line 3"? If this were a standalone statement, we'd feel justified in returning an errAEIllegalIndex error to let the user know that the requested word doesn't exist. However, since we're returning lists in the more complex statement, we might want to return an empty list as part of our result instead. For example:

{{"b", "l", "o", "c", "k"}, {"o", "f"}, {}}

Another example, again from the Text suite, involves words from paragraphs. Suppose paragraph 2 is empty, as in the following block of text:

Hello there!¶

How are you?¶

What will you do with "get every word of every paragraph" in this case? If you decide to support empty lists or empty sublists, all of your handlers will need to be able to deal not only with a single token and arbitrarily deep lists of tokens, but also with an empty list.

How to deal with gotcha #2. Designing your object accessors and your event handlers to be list savvy enables your code to fully respond to script statements that require you to return lists of objects or to apply Apple events to lists of objects.

To handle lists, an object accessor must be able to return a descriptor containing a token that references a single object or a descriptor that contains a list of tokens. For example, a property-from-object accessor must be able to receive a list of object tokens and return a list of property tokens for those objects. For each object you support, you need one of these property-from-object accessors. In Sketch, these basically duplicate the token for the object and then stuff the requested property ID into the token's propertyCode data field.

An object's event handler must also be able to receive a descriptor that contains a single token or a descriptor that contains a list of tokens. It must then apply the event to the object referenced by each token. In addition, the event handler must apply the event to each object in a manner that addresses gotcha #3, discussed later.

If you've installed a token disposal callback function, it too must be able to handle an AEList of tokens.

The Sketch sample handles this gotcha by implementing recursion in both its object accessors and its event handlers. The basic structure of an accessor then consists of three functions. For example, for the QuickDraw Graphics suite, the property-from-object accessor uses these three functions, as shown in Listing 4:

  • PropertyFromGraphicObjectAccessor -- installed function that calls one of the following two static functions, depending on whether it receives a token or a token list

  • PropertyFromListAccessor -- always receives a list, and calls itself recursively until it finds a token that doesn't contain a list, when it calls PropertyFromObjectAccessor

  • PropertyFromObjectAccessor -- always receives a token for a single object, and returns a token representing a property of that object.


Listing 4. Functions used by our property-from-object accessor

pascal OSErr PropertyFromGraphicObjectAccessor(
   DescType desiredClass,
   const AEDesc* containerToken, DescType containerClass,
   DescType keyForm, const AEDesc* keyData, AEDesc* resultToken,
   long refcon)
{
   OSErr  error;

   if (containerToken->descriptorType != typeAEList) 
      error = PropertyFromObjectAccessor(desiredClass, 
         containerToken, containerClass, keyForm, keyData, 
         resultToken, refcon);
   else {
      error = AECreateList(NULL, 0L, false, resultToken);
      if (error == noErr)
         error = PropertyFromListAccessor(desiredClass, 
            containerToken, containerClass, keyForm, keyData, 
            resultToken, refcon);
   }
   return error;
}
static OSErr PropertyFromListAccessor(DescType desiredClass,
   const AEDesc* containerToken, DescType containerClass,
   DescType keyForm, const AEDesc* keyData, AEDesc* resultToken,
   long refcon)
{
   OSErr       error = noErr;
   long        index, numItems;
   DescType    keyword;
   AEDesc      srcItem = {typeNull, NULL);
   AEDesc      dstItem = {typeNull, NULL};

   error = AECountItems((AEDescList*)containerToken, &numItems);
   if (error != noErr) goto CleanUp;

   for (index = 1; index <= numItems; index++) {
      error = AEGetNthDesc(containerToken, index, typeWildCard, 
         &keyword, &srcItem);
      if (error != noErr) goto CleanUp;
      
      if (srcItem.descriptorType != typeAEList) {
         error = PropertyFromObjectAccessor(desiredClass, &srcItem,
            containerClass, keyForm, keyData, &dstItem, refcon);
      }
      else {
         error = AECreateList(NULL, 0L, false, &dstItem);
         if (error == noErr)
            error = PropertyFromListAccessor(desiredClass, &srcItem,
               containerClass, keyForm, keyData, &dstItem, refcon);
      }
      if (error != noErr) goto CleanUp;

      error = AEPutDesc(resultToken, index, &dstItem);
      if (error != noErr) goto CleanUp;
            
      AEDisposeDesc(&srcItem);
      AEDisposeDesc(&dstItem);
   }
   
CleanUp:
   AEDisposeDesc(&srcItem);
   AEDisposeDesc(&dstItem);
   return error;
}

static OSErr PropertyFromObjectAccessor(DescType desiredType,
   const AEDesc* containerToken, DescType containerClass,
   DescType keyForm, const AEDesc* keyData, AEDesc* resultToken,
   long refcon)
{
   OSErr    error = noErr;
   DescType requestedProperty = **(DescType**)(keyData->dataHandle);
   if (CanGetProperty(containerClass, requestedProperty) 
    || CanSetProperty(containerClass, requestedProperty)) {
      error = AEDuplicateDesc(containerToken, resultToken);
      if (error == noErr) {
         resultToken->descriptorType = desiredType;
         (**(CoreTokenHandle)(resultToken->dataHandle)).propertyCode
            = requestedProperty;
         (**(CoreTokenHandle)(resultToken->dataHandle)).objectClass 
            = containerClass;
      }
   }
   else error = errAEEventNotHandled;
   return error;
}

The event handlers use this same three-tiered mechanism to apply events to descriptors that contain either a single token or a list of tokens. For example, the Get Data event will eventually receive the property token returned by the property-from-object accessor above and deal with it as shown in Listing 5.


Listing 5. How a Get Data event handles a property token

static OSErr HandleGetData(AEDesc *token,
   const AppleEvent *appleEvent, 
   AppleEvent *reply, long refcon)
{
   OSErr    error = noErr;   
   AEDesc   data = {typeNull, NULL};
   AEDesc   desiredTypes = {typeNull, NULL};   

   AEGetParamDesc(appleEvent, keyAERequestedType, typeAEList, 
      &desiredTypes);
      // "as" is an optional parameter; don't check for error.
   error = GetDataFromGraphicObject(token, &desiredTypes, &data);
   if (error == noErr && reply != NULL)
      error = AEPutKeyDesc(reply, keyDirectObject, &data);
      
   AEDisposeDesc(&data);
   AEDisposeDesc(&desiredTypes);
   return error;
}

OSErr GetDataFromGraphicObject(AEDesc *tokenOrTokenList, 
   AEDesc *desiredTypes, AEDesc *data)
{
   OSErr   error = noErr;
   
   if (tokenOrTokenList->descriptorType != typeAEList)
      error = GetDataFromObject(tokenOrTokenList, desiredTypes,
                  data);
   else {
      error = AECreateList(NULL, 0L, false, data);
      if (error == noErr)
         error = GetDataFromList(tokenOrTokenList, desiredTypes,
                     data);
   }
   return error;
}

Again, the event handler passes the first parameter on to GetDataFromGraphicObject, which calls GetDataFromList if the parameter contains a list of tokens, or GetDataFromObject if it contains a token for a single object. Both the object accessor and the event handler use the same three-tiered mechanism to deal with either lists or single tokens. Most of the work is done, in both cases, in the third tier, and if you've already implemented simple object model scriptability, you've already written most of the code for the third tier. To support lists, you just have to add the switching code for the first and second tier, which is almost identical for all object accessors and all event handlers. Using this mechanism, fully supporting lists of any depth is nearly trivial.

Flattening lists. Sometimes, after your object resolution code has built an arbitrarily deep list of lists to satisfy the tail end of a script statement, the final resolution might require you to flatten it back into a single-level list. Sketch includes the FlattenAEList function to perform this duty:

OSErr FlattenAEList(AEDescList *deepList, AEDescList *flatList);

Here's an example of when you might use it, again from the Text suite:

get text of every character of every word of every paragraph ~
   of every document

Since the Text class isn't required to handle either formRange or the every construct, you can return a string that spans from the first character in the list to the last character in the list. A function to flatten a typeAEList token from an arbitrary depth to a single list is useful for this purpose, and for use in your Apple event handlers, such as the handlers for Count and Delete. For example, the statement

count every character of every word of every line of every paragraph

is allowed, and your accessors will return a four-deep list of characters. The Count event handler doesn't care about the structure of the list, only about the number of objects in its sublists, so rather than deal with recursion to step through the list structure you can just flatten the list and then call AECountItems to get the number of elements. This example is somewhat contrived, and although this script fragment would be processed correctly, such processing might be very slow for a large number of objects. This is a side effect of a strict object-first implementation. For some events, such as Count, you may want to write custom counting code that short-circuits your standard object resolution and dispatching mechanism.

Gotcha #3: The "Preserve A Token's Meaning" Gotcha

The meaning of a token's contents must be preserved during the execution of an Apple event that uses that token.

You're most likely to come up against this gremlin when one of your handlers receives a list of tokens and some action needs to be performed on the objects referenced by the tokens in the list. Consider, for example, the following statement:

delete character 2 of every word

Let's say all of your text tokens are implemented by storing a beginning offset and a length, where the offset is measured from the beginning of a text block. Resolving the above statement will return a list of tokens, with offsets for character 2 of each word in the text block. Next, your handler iterates through the list of objects referenced by the tokens and deletes the character referenced in each object. The first deletion works just fine; you use the offset contained in the first token and delete character 2 of word 1. This causes every following character to move one position to the left to fill the spot vacated by the deleted character. Uh oh! Now the offsets for the remainder of your objects are all off by 1! The next deletion will use the now incorrect offsets, and character 3 of word 2 will be deleted. The next call will delete character 4 of word 3, and so on. This implementation has violated gotcha #3 -- you received a single Delete event, but that single event operates on multiple objects, and although your object accessors computed the object tokens correctly at the time they were called, your handler causes the meaning of the tokens to be inaccurate each time it processes another object.

How to deal with gotcha #3. Here are several ways to solve the problem resulting from processing the script statement above:

  • You could construct your tokens as offsets from the end of the text block instead of from its beginning. Then, as characters are deleted from the first word to the last, since the end of the list is shrinking also, the offsets will still be correct.

  • You could have your handler keep track of the number of characters deleted so far and adjust the offsets in your tokens as you go.

  • You could step through the list in reverse order, from the last token down to the first.

These methods all produce the correct results for the script statement above, but they might produce incorrect results for other valid statements. For instance, suppose your user built the word list herself and then reversed the list and sent it to your Delete handler. With the last solution above, you cleverly work from the end of the list to the beginning, but since the user has already reversed the list, you're really back to deleting from the beginning of the text block toward its end, and you experience the very problem you were trying to avoid!

If you're implementing the Text suite, pay particular attention to gotcha #3. Test your implementation with many different scripting constructs, and have people who write scripts very differently from you test it also. If necessary, you may need to first manipulate the order of the tokens in the list you receive to make sure you can preserve the meaning of those tokens until the event has been applied to each one of them.

Other Goodies For Completeness

Now you know how to handle lists and some ways to avoid the big gotchas. But there are still a few more things you can do to make your object model implementation more complete. Specifically, you can implement a "properties" property, implement a property-from-property accessor, provide your own coercions, and return meaningful error codes.

Implementing A "Properties" Property

You should implement a "properties" property and return a record containing all the properties for an object. This provides a real boon for the scripter, who can then set or get several properties with a single statement, and it speeds up execution as well since it avoids the need to send many events to get or set properties one at a time.

For instance, if the script says

get the properties of rectangle 1

the Get Data event should return a record containing the name and value of each property for that object:

{bounds: {0, 0, 100, 200}, fill color: red, stroke size: 10, ...}

The script could also say something like

set properties of rectangle 1 ~
   to {stroke size: 3, fill color: blue, location: {20, 40}}

In Sketch, the Set Data event handler looks at the property token it receives. If the token references a single property, it packages it into a record containing that property and passes the record on to the SetProperties function. If, instead, it receives a record, it just passes that record on to SetProperties. The SetProperties function always receives a record; it examines the record for each property of the object and then applies the value of each property it finds in the record to the object.

Implementing A Property-From-Property Accessor

If you implement the "properties" property, you should also implement a property-from-property accessor. If you don't, you won't be able to get a single property out of the property record you've already built. The first statement below will work, but the second one will generate an error:

get fill color of rectangle 1
get fill color of properties of rectangle 1 -- won't work

To get around this, the script writer will need to first assign the results to a variable and then depend on AppleScript to extract the property out of that variable:

set myProps to properties of rectangle 1
get fill color of myProps

But since one of our goals should be to make scripting intuitive and not force the script author into particular programming constructs when not absolutely necessary, both methods of asking for the property should be handled in your code.

Another reason you may need a property-from-property accessor arises when, in the process of defining your object containment hierarchy, you define two classes, make one class an element of the other class, and then realize that the container can contain one and only one instance of that element. For example, imagine a very limited drawing program that allows many graphic objects but only one text block, an instance of the QuickDraw Graphics graphic text class. If you stick with a straight containment metaphor, the script author will need to use a statement like

graphic text 1 of document "Graphic Chart"

to reference the one and only text block. But why should the scripter have to specify the index of 1 when there can be only one per document? This also invites the scripter to ask for graphic text 2, for which you would need to return an errAENoSuchObject error.

One way to handle this case is to implement the singleton object as a property of an object rather than a contained class. In your 'aete' resource, define a property (say "label" for the above example) of type graphic text, which is a class defined elsewhere in your 'aete' resource. Now, the script statement

get the label of document "Graphic Chart"

doesn't need to specify an index, since "label" is a property. What will that statement return? You decide. You might just return the contents of the graphic text as a string. But since the "label" property also references a class, you could return the properties of the text object as a record, such as:

{contents: "Financial Results", font: "Times", size: 12, ...}

By implementing a property-from-property accessor, you can also properly resolve a statement like this:

get font of label of document "Graphic Chart"

There's an ongoing debate in the developer community about the best way to design for this single-element situation. Some developers believe that the design discussed above leads to intuitive script statements that make it easier for users to script your application. Others contend that elements and properties serve very different purposes, and that intermixing them in this way both corrupts the object design and confuses the beginning script writer. You'll have to decide for yourself how you want to handle this in your application; there may not be one best design. In any case, if you find yourself in this situation, take the advice Cal Simone gives in his According to Script columns: write down script statements as part of your design and make sure that they seem natural and intuitive before you write your code.

Providing Application-Specific Coercions

Provide your own coercions. There are several places where these come in handy. First, the Get Data event can take an optional parameter, keyAERequestedType, a list of types that the user would like for the returned data. For instance, a fill color might be represented as one of the following:

  • typeEnumerated, such as red

  • typeChar or typeIntlText, such as "red"

  • typeRGBColor, such as {32767, 0, 0}

Thus, a scripter might ask for

fill color of rectangle 1 as constant
fill color of rectangle 1 as string
fill color of rectangle 1 as RGB color

The Registry defines the type of the as parameter as typeAEList, indicating that the first item in the list is the user's preferred data type, the second is the user's next most preferred type, and so on. However, I haven't been able to persuade AppleScript to accept a list for this parameter. It seems as though get fill color of rectangle 1 as string (or RGB color or constant) should work, but it won't compile.

Note that there's a bug in AppleScript 1.1 that generates an error when you implement both lists and the kAERequestedType parameter. The following statement will expose the error:

get fill color of every rectangle as string

The every statement causes you to generate a list of property tokens, which is then passed to your graphic object's Get Data event handler. There, you examine each token, get the fill color from its rectangle, and convert it to a string (presumably the name of the color), as specified in the as string part of the statement. Since you received a list of tokens, you return a list of strings, as you should. You've done the right thing, but AppleScript isn't satisfied! It doesn't realize that you've already handled the as string coercion, so it tries to coerce the list of strings you returned into a string and it reports a coercion error. There's really nothing you can do in your application to work around this bug; you'll have to wait for it to be fixed in a future version of AppleScript. There is a way that scripts can handle the error, however:

tell document 1 of application "Sketch"
   try
      set colorNames to fill color of every rectangle as string
   on error number -1700 from offendingVariable
		set colorNames to offendingVariable
   end try
end tell 

Returning Useful Error Codes

One last suggestion: Return a meaningful error code and error message if you don't or can't handle an event, an object, or a data type. Table 5 presents a list of some of the most common return codes, when to use them, and the error message that AppleScript generates when one of these errors occurs.

List 5. Common error codes and examples of when you might return them

Error to return Example of when to return it Error message
errAEEventNotHandled (-1708) This isn't just a catch-all error; it has specific side effects in certain situations. If your code doesn't handle an event, this is a signal for the Apple Event Manager to give any system handlers a shot at the event. <object-reference> doesn't understand the <event> message.
errAECoercionFail (-1700) When you can't coerce some data to the requested type. Can't make some data into the expected type.
errAENoSuchObject (-1728) When the requested object doesn't exist. Can't get <object reference>.
errAENotASingleObject (-10014) When a handler that doesn't handle lists receives a list. Handler only handles single objects.
errAENotAnElement (-10008) When you get a request to delete a property. The specified object is a property, not an element.
errAENotModifiable (-10003) When the object can never be modified, such as a read-only property. See also errAEWriteDenied. Can't set <property> to <value>. Access not allowed.
errAEWriteDenied (-10006) When the object can't currently be modified, such as a locked rectangle that can't be changed until it's unlocked. See also errAENotModifiable. Can't set <property> to <value>.
errAECantHandleClass (-10010) When an event handler can't handle objects of this class, or when an object-counting callback receives an object type it can't count. Handler can't handle objects of this type.
errAEIllegalIndex (-1719) When the scripter asks for an index greater than the number of objects or less than 1. Remember that negative indexes are legal, so convert them to a positive index before performing a range check. Can't get <object-reference>. Invalid index.
errAEImpossibleRange (-1720) When you process formRange and you can't return a list of objects between the boundary objects -- for example, when two rectangles are specified and are in different documents. Invalid range.
errAEWrongDataType (-1703) When a descriptor contains an unexpected data type, or when an object-comparison callback doesn't know how to compare one of the data types. <value> is the wrong data type.
errAETypeError (-10001) When you receive a Set Data event, and the descriptor isn't of the expected type and can't be coerced into that type. <value> is the wrong type.
errAEBadKeyForm (-10002) When an object is requested by a key form that your accessor doesn't support -- for example, rectangle by name. Invalid key form.
errAECantSupplyType (-10009) When you can't return the type of data specified in the as parameter of a Get Data event. Can't supply the requested type for the data.

Some error codes have a very generic error message as a default, but you can supply additional parameters in the reply event so that the error message will be more specific. For example, an errAECoercionFail message usually says, "Can't make some data into the expected type," but if you add kOSAErrorOffendingObject and kOSAErrorExpectedType parameters to the reply event, you'll get a much more informative message, such as "Can't make fill color of rectangle 1 into a string." These parameters can also be added to errAEWrongDataType and errAETypeError replies. For more detail on giving better error messages using this technique, see Developer Notes in the AppleScript Software Development Toolkit. You may want to define additional error codes for your application, and if so you should be sure to also set the error text in the reply event. Take a look at the PutReplyErrorNumber and PutReplyErrorMessage functions in the Sketch source code to see how to do this.

Exercising Your Implementation

This article has described some things you can do to implement an Apple event object model in your application so that it can handle complex scripts. Take a close look at the code for the Sketch application to see how it uses the object-first method to handle events and scriptable objects. Carefully examine the dictionaries of several applications that are fully scriptable, such as QuarkXPress, the Scriptable Text Editor, or PhotoFlash. Pay attention to how their 'aete' resources are constructed, and read the develop columns by Cal Simone ("According to Script") to gain further insight into how to organize both your 'aete' resource and your object model.

Then give your implementation a thorough workout to see if you can spot any problems. Write AppleScript test cases to exercise the most complex AppleScript scripts that you want to support. Use the key forms that return lists, and mix them unmercifully in your test scripts. Exercise every gotcha. If your application stands up to the test, shout "Ship it!"

Recommended Reading

  • "Apple Event Objects and You" by Richard Clark, develop Issue 10.

  • "Speeding Up whose Clause Resolution in Your Scriptable Application" by Greg Anderson, develop Issue 24.

  • "Designing a Scripting Implementation" by Cal Simone, develop Issue 21. Also, look for Cal's According to Script columns starting with develop Issue 22.

  • Inside Macintosh: Interapplication Communication by Apple Computer, Inc. (Addison-Wesley, 1993).

  • Apple Event Registry: Standard Suites (Apple Computer, Inc., 1992).

  • AppleScript Language Guide by Apple Computer, Inc. (Addison-Wesley, 1993). This book is included in the AppleScript Software Development Toolkit.

RON REUTER (rlreute@uswest.com) is a software developer for USWEST Media Group, "a leading provider of Yellow Pages and interactive multimedia information services" or, as it used to be known, the phone company. He's spent the last two years as technical lead on a team that's developing a graphics package to be used across a fourteen-state region to build ads for USWEST Yellow Page directories. Programming is Ron's sixth career; he's also been a offset press operator, a furniture maker, a luthier, a traveling jewelry salesman, and a Zen student -- but he'd rather be dancing beneath the diamond sky with one hand waving free.*

Thanks to our technical reviewers Greg Anderson, Andy Bachorski, Greg Friedman, C. K. Haun, and Jon Pugh for reviewing this article.*

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »

Price Scanner via MacPrices.net

Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more
Sunday Sale: 13-inch M3 MacBook Air for $999,...
Several Apple retailers have the new 13″ MacBook Air with an M3 CPU in stock and on sale today for only $999 in Midnight. These are the lowest prices currently available for new 13″ M3 MacBook Airs... Read more
Multiple Apple retailers are offering 13-inch...
Several Apple retailers have 13″ MacBook Airs with M2 CPUs in stock and on sale this weekend starting at only $849 in Space Gray, Silver, Starlight, and Midnight colors. These are the lowest prices... Read more

Jobs Board

Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
Liquor Stock Clerk - S. *Apple* St. - Idaho...
Liquor Stock Clerk - S. Apple St. Boise Posting Begin Date: 2023/10/10 Posting End Date: 2024/10/14 Category: Retail Sub Category: Customer Service Work Type: Part Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.