TweetFollow Us on Twitter

OpenDoc Framework
Volume Number:11
Issue Number:11
Column Tag:APPLE TECHNOLOGY

The OpenDoc Development Framework

A modern framework for OpenDoc development.

By Jim Lloyd, jim@melongem.com

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

Developing for OpenDoc

The OpenDoc Development Framework (ODF) is a modern object-oriented framework for developing OpenDoc components for Macintosh and 32-bit Windows platforms. It significantly reduces the work required to create an OpenDoc component editor, especially if you’re developing the part editor for both Macintosh and Windows.

In this article, I’ll provide you with an overview of ODF. Because ODF is too large to address in one short article, I’m forced to condense and highlight. My goal is to provide you with an understanding of the problems ODF solves for you and how it solves them, and to give you a taste of what it is like to develop an OpenDoc component using ODF. I assume some level of understanding of OpenDoc, but not much, so if this is your first exposure to OpenDoc, I suggest you dive in anyway.

Why Another Framework?

Macintosh developers today have their choice of several frameworks. Some of the better known frameworks include Apple’s MacApp, Symantec’s TCL, Metrowerks’ PowerPlant, MacTech’s Sprocket, and Paul Dubois’ TransSkel. If you’ve used any of these frameworks, then you probably appreciate the time and aggravation they can save you by providing a solid foundation on which to build your application. These frameworks not only provide you with code common to all Macintosh applications, they provide you with an architectural framework that makes it easier to design your own application. If all goes well, you simply “fill in the colors and connect the dots,” so to speak.

The degree that “all goes well” depends on the framework you choose and your application’s problem domain. The truth is, any framework is designed to work well with a specific range of application domains. If your application doesn’t fit within that range, then the framework will at best get in your way from time to time, and at worst be totally useless. You are not likely to have much success trying to write a 2K INIT using TCL or MacApp. I don’t consider this to be a shortcoming of those frameworks; common sense dictates that one shouldn’t assume a given tool is the right tool for all jobs, big and small.

Of course, a framework shouldn’t be a single tool; it should be a collection of tools that work well together. If the framework is done well, some of these tools will be applicable to any problem domain. Modern frameworks are designed with this in mind. PowerPlant, for example, is well suited for both simple “dashboard” applications and large-scale applications that use multiple document types, and pieces of the framework can be used in programs that don’t fit these domains.

Why ODF?

Why ODF? Because even a flexible framework like PowerPlant isn’t appropriate for all applications. First, developers need to deliver their applications on multiple platforms, including Microsoft Windows. Second, OpenDoc introduces a new set of requirements that may be very difficult to fulfill unless the requirements are considered in the initial design of the framework. ODF addresses both of these issues. ODF is a modern framework designed to be flexible and robust, to be cross-platform, and to provide complete support for OpenDoc.

ODF is a cross-platform framework because its APIs abstract away the particulars of each operating systems’ specific data structures and APIs. ODF defines platform-independent APIs, and implements these APIs on multiple operating systems. Developers who program to the framework’s APIs exclusively should be able to move their applications to every operating system supported by the framework by simply recompiling their code. ODF currently supports the Macintosh OS for both 68K and PowerPC, and supports Microsoft Windows for its 32-bit operating systems, Windows 95 and Windows NT.

ODF includes a rich foundation of subsystems organized into two layers: the Foundation layer and the OS layer. Subsystems in the Foundation layer have no dependencies on the subsystems in the OS layer, and are generally useful for a very wide range of application domains. Subsystems in the OS layer provide operating system services such as files, resources, and graphics using platform-independent interfaces.

On top of these two layers we add a third layer, known as the Framework layer. The ODF Framework layer is specific to building OpenDoc component editors, and is designed to solve the problems of that particular domain as well as possible. ODF provides full coverage of the OpenDoc APIs, and correctly implements the human interface standards. If you use ODF to develop your OpenDoc part, you’ll need to do less work to make your part functional. You’ll also save yourself much of the additional effort required to make your part interoperate correctly with other parts, as well as the effort required to provide the correct human interface behavior.

An extra benefit to using ODF is that it provides some of the best sample code for OpenDoc parts that you can find. The ODF examples range from the minimalist ODFHello part, which is our interpretation of “Hello World”, implemented as a part, to ODFDraw, a cross-platform drawing part with floating palettes for drawing tools, pattern selection, and color selection. Of greater significance, ODFDraw is a robust container part for embedding other parts. ODFDraw has been the container of choice for OpenDoc demonstrations for much of the last year.

Also included among the ODF examples are a bitmap part, a clock part, a movie part, a table part, and finally the “beeper” part, which will play a big role in this article.

Extending The Beeper’s Functionality

ODFBeeper is a simple button part that can play a sound resource. ODFBeeper, as delivered on previous releases of ODF, supports drag-and-drop so that you can change the sound that is played by dropping a sound file onto the button. In this article, we’ll extend the Beeper part to also execute any compiled AppleScript dropped onto it. Thanks to Eric Jackson for coming up with this idea and drafting me to work on it with him at a recent ODF Coding Retreat. Eric and I did a quick hack so that the Beeper would execute scripts instead of playing a sound. It occurred to me later that with only a little more effort, the Beeper could do either sounds or scripts. The code presented in this article does both, and uses an architecture that would make it easy to add other kinds of actions besides sounds and scripts.

In order to see where we are going, let’s first look at a screen snapshot of the final product:

ODFBeeper User Interface (such as there is)

This screen shot shows an ODFDraw document being used as a generic container. It contains two ODFBeeper parts embedded inside it. Conveniently located nearby are some sound files and some compiled AppleScript files. The user can drag any of these files onto either of the buttons. Clicking on the button will perform the action corresponding to the last file dropped onto the button. Changes to the whole document are persistent. The user can use the capabilities of the container application to rearrange the buttons, and can change the action of a button by dropping a new script or sound file onto the button. Saving the document saves all of the state, so the buttons will be in the same locations and perform the same actions the next time the document is opened.

Developing A Part Editor

Developing a part with ODF is much like developing an application with an application framework like PowerPlant, TCL, or MacApp. You can use your favorite C++ programming tools, including Apple’s, Metrowerks’ and Symantec’s development environments, and other tools such as Object Master from ACIUS and The Debugger from Steve Jasik. The ODF view system is still evolving, so currently there are no visual tools for laying out your part’s views, but visual tools for ODF are under development.

For the purposes of this article, I started with the source for the ODFBeeper example part included with the 1.0d9 release of ODF, which was provided on the OpenDoc DR3 CD. If you have a source code disk subscription, the project and source for this extended example will be on your source code disk, but you’ll still need the OpenDoc DR3 CD if you want to further enhance the example. If you don’t have a source code subscription, keep an eye out for the OpenDoc DR4 release; ODF 1.0d11 will be on that CD and will include the extended ODFBeeper example.

By the way, there’s no way that MacTech could publish all the source code necessary for this sample (and you wouldn’t want to try to type it all in!), so I won’t be showing complete source code listings in this article. Instead, I’ll show the relevant snippets of code as they are discussed. In these snippets, I’ll sometimes omit code not directly relevant to the discussion by replacing several lines of code with a single line containing an ellipsis and optionally a comment, like this:

...      // irrelevant stuff omitted

ODF parts can be developed for either PowerPC or 68K using several different environments. For this article I used the PowerPC compiler in the CW6 release of Metrowerks CodeWarrior Gold and haven’t yet tested the part with other environments, but you can expect the part to be buildable with all supported development environments on the DR4 CD. The following is a snapshot of the ODFBeeper CodeWarrior project:

ODFBeeper Project

The ODFBeeper example uses four source files. BeeperPart.cpp implements the classes CBeeperPart and CBeeperFrame, which are subclasses of the ODF classes FW_CPart and FW_CFrame respectively. BeeperSel.cpp implements the CBeeperSelection class, which is a subclass of the ODF class FW_CSelection. Actions.cpp implements the classes CAction, CSoundAction, and CScriptAction, all of which are completely specific to ODFBeeper; ODF itself knows nothing about actions. SOMBeeper.cpp contains the C++ bindings for the required SOM subclass of ODPart, which won’t be discussed in this article. In addition, ODFBeeper uses the resource file BeeperPart.rsrc that contains a typical assortment of icons, strings, etc. It won’t be discussed further in this article either.

I’ll now proceed to describe the source code in the three source files Actions.cpp, BeeperPart.cpp, and BeeperSel.cpp, in that order. However, before discussing the Action classes, I think it would be worthwhile to partially discuss the CBeeperPart class. If you are familiar with the Model-View-Controller architecture from Smalltalk that has been adapted in various incarnations in most frameworks, then you can consider the Action classes to be the Model, CBeeperFrame to be the View, and CBeeperPart to be the Controller. Since the CBeeperPart is in control, it is worthwhile starting with its relationship to the Action classes.

CBeeperPart

Here is a partial definition of the class CBeeperPart:

class CAction; // forward declaration

class FW_CLASS_ATTR CBeeperPart : public FW_CPart
{
public:
    ...  // constructors, methods inherited from FW_CPart

//----------------------------------------------------------
// New API
//
public:
 CAction* GetAction() { return fAction; }
 void   SetAction(CAction* action);

public:
 void   DoAction();

//----------------------------------------------------------
// Data Members
//
private:
 CAction* fAction;
};

In ODF 1.0d9, CBeeperPart had a single data member that was a handle to the sound resource to play when the button was clicked. Since we want our new part to perform different kinds of actions, we’ve changed the data member to be a pointer to an abstract class CAction. We’ll use the Action abstraction to minimize the dependencies of CBeeperPart on the details of implementing sound and script actions. CBeeperPart knows about the existence of abstract actions, and has a very simple protocol for actions which include the ability to get and set the current action, and to forward a request to apply the action to the current CAction object.

CAction

Let’s now look at the definition of the abstract class CAction:

class FW_CLASS_ATTR CAction
{
//----------------------------------------------------------
// Initialization/Destruction
//
public: 
 CAction();
 virtual ~CAction();
 
//----------------------------------------------------------
// Action protocol
//
public: 
 virtual void Internalize(Environment* ev, 
 ODStorageUnit* storage) = 0;
 virtual void Externalize(Environment* ev, 
 ODStorageUnit* storage) = 0;
 virtual void DoIt() = 0;
};

Action objects assume a two-step initialization; they are created in a default state and then initialized from a storage unit with the Internalize method. They also know how to save themselves to a storage unit via the Externalize method, and to do their action via the DoIt method. In this abstract base class, the three methods other than the constructor and destructor are pure virtual methods. Subclasses of CAction must provide a default constructor, a destructor, and implementations of the three virtual methods Internalize, Externalize, and DoIt.

CSoundAction

Let’s now look at the implementation of the two Action subclasses. First, the definition for the class CSoundAction:

class FW_CLASS_ATTR CSoundAction : public CAction
{
//----------------------------------------------------------
// Initialization/Destruction
//
public:
 CSoundAction();
 virtual ~CSoundAction();
 
 static FW_Boolean IsInStorage(
 Environment* ev, ODStorageUnit* storage);
 
//----------------------------------------------------------
// Action protocol
//
public:
 virtual void Internalize(Environment* ev, 
 ODStorageUnit* storage);
 virtual void Externalize(Environment* ev,
 ODStorageUnit* storage);
 virtual void DoIt();

//----------------------------------------------------------
// Private implementation
//
private:
 void InternalizeSound(Environment* ev, 
 ODStorageUnit* storage);
 void InternalizeSoundFile(Environment* ev, 
 ODStorageUnit* storage);
 
 Handle fSoundHandle;
};

This class is a simple implementation of the class CAction, so the constructor, destructor, and three methods Internalize, Externalize, and DoIt are dictated by the base class. CSoundAction adds a public static method IsInStorage, two implementation methods for internalizing from specific sound formats, and a data member for storing a sound handle. Let’s look at the definitions of the methods of this class, beginning wit the implementation of the Internalize method inherited from CAction:

CSoundAction::Internalize

void CSoundAction::Internalize(Environment* ev, 
 ODStorageUnit* storage)
{
 FW_Boolean internalized = FALSE;
 
 if (storage->Exists(ev, 
 kODPropContents, kSoundScrapKind, 0))
 {
    // Mac 'snd ' in Scrap
 storage->Focus(ev, 
 kODPropContents, 
 kODPosUndefined, 
 kSoundScrapKind, 
 0,
 kODPosUndefined);
 InternalizeSound(ev, storage);
 }
 else if (storage->Exists(ev, 
 kODPropContents, kSoundFileKind, 0))
 {
    // Mac sound file
 storage->Focus(ev, 
 kODPropContents, 
 kODPosUndefined, 
 kHFSFlavorType, 
 0, 
 kODPosUndefined);
 InternalizeSoundFile(ev, storage);
 }
}

This code examines the storage unit for the existence of either a sound resource (OSType 'snd') or a sound file (OSType 'sfil'). If either is found, the code focuses the storage unit and then calls one of the two private member functions for reading the specific data type.

By the way, the constants kSoundScrapKind, kSoundFileKind, and kHFSFlavorType are strings defined in the header file BeeperDef.h using a naming convention agreed upon by the OpenDoc implementors (Apple, IBM, and Novell):

#define kHFSFlavorType "Apple:OSType:Scrap:hfs "
#define kSoundScrapKind "Apple:OSType:Scrap:snd "
#define kSoundFileKind  "Apple:OSType:FileType:sfil"

If Internalize detects a sound resource in the scrap, it calls the method InternalizeSound:

CSoundAction::InternalizeSound

void CSoundAction::InternalizeSound(Environment* ev, 
 ODStorageUnit* storage)
{
    // Assume the storage unit is already focused
 unsigned long size = storage->GetSize(ev);
 if (size > 0)
 {
 FW_CAcquireTemporarySystemHandle handle(size);
 FW_CByteArray byteArray;
 storage->GetValue(ev, size, byteArray);
 byteArray.CopyBuffer(handle.GetPointer(), size);
 fSoundHandle = handle.GetPlatformHandle();
 handle.Orphan();
 }
}

InternalizeSound assumes the entire content of the property value is a sound resource. It allocates a handle of the appropriate size and then reads the data into the handle. Finally, it stores the handle in the fSoundHandle field. Looking at the code, we see that a little more is going on here, which is worth explaining in some detail.

To allocate the handle, the code uses an instance of the class FW_CAcquireTemporarySystemHandle, which is a resource acquisition helper object that allocates a “temporary” handle. The handle is considered to be temporary because unless instructed otherwise, the handle will be disposed when the helper object goes out of scope. On the Macintosh, the constructor of FW_CAcquireTemporarySystemHandle will allocate a handle in Multifinder temporary memory (throwing an exception if the allocation fails), and then lock the handle down in memory. On other platforms, it does something equivalent.

The FW_CAcquireTemporarySystemHandle destructor will unlock the object, and optionally dispose the handle. The default behavior is to dispose the handle, but calling the Orphan method instructs the helper object to not dispose the handle. This may seem a little involved, but it is a very convenient idiom for allocation of resources (like memory) that must be disposed if an exception is thrown before the resource is completely initialized.

In CSoundAction::InternalizeSound, we can assume that any of the three lines of code starting with the call to the method GetValue and ending with the call to the method GetPlatformHandle could throw an exception. In actual fact, with the current implementations of these functions, only the GetValue method could throw an exception, but in this case it hurts nothing to be conservative.

ODF provides a range of utility functions and helper objects that make it possible to write portable code that works with exceptions correctly. In the case of CSoundAction::InternalizeSound, the advantages of the platform-independence of the helper objects FW_CAcquireTemporarySystemHandle and FW_CByteArray aren’t fully realized because the function as a whole is making an assumption specific to the Macintosh: 'snd ' resources exist only on the Macintosh. With a little extra effort, it would be possible to create an abstraction for sound data that would work on both Macintosh and Windows. Such a class will likely make it into ODF someday; in the meantime, we leave it as an exercise for the reader.

Let’s now look at the method to internalize a sound file:

CSoundAction::InternalizeSoundFile

void CSoundAction::InternalizeSoundFile(Environment* ev,
 ODStorageUnit* storage)
{
    // Storage Unit should already be Focused on HFSFlavor
 unsigned long hfsSize = storage->GetSize(ev);
 if (hfsSize > 0)
 {
    // Get the HFS flavor
 HFSFlavor hfsInfo;
 FW_CByteArray byteArray;
 storage->GetValue(ev, sizeof(HFSFlavor), byteArray);
 byteArray.CopyBuffer(&hfsInfo, sizeof(HFSFlavor));
 
    // Create platform independent file spec
 FW_CFileSpecification fileSpec(hfsInfo.fileSpec);
 
    // Open the resource file
 FW_CResourceFile resourceFile(fileSpec);
 
    // Get any (i.e. the first) sound resource
 const FW_ResourceType resourceType = 'snd ';
 const FW_ResourceId resourceId = 1;
 Handle sound = ::Get1IndResource(resourceType, 
 resourceId);
 
 if (sound != NULL)
 {
    // if succeeded, detach the handle and keep it
 ::DetachResource(sound);
 fSoundHandle = sound;
 }
 else
 {
    // No sound resource loaded, throw an exception
 FW_THROW(FW_XResourceNotFound(resourceFile,
 resourceId, 
 resourceType));
 }
 }
}

InternalizeSoundFile is similar to InternalizeSound. The difference is that instead of getting the sound resource directly from the storage unit, it instead gets an HFS file specification from the storage unit. It then uses this file specification to open the file and grab the sound resource from the file. The code here is a bit complicated because sound files don’t use fixed resource IDs, so the code must look for any sound resource. Unfortunately, this is a function that we don’t (yet!) provide in the Resources subsystem of ODF, so we have to resort to direct calls to the Macintosh toolbox resource manager. The good news is that ODF doesn’t get in the way of doing this. In fact, it is still convenient to use an instance of the class FW_CResourceFile to open the file, since it will assume responsibility for closing the file when it goes out of scope. So, we use ODF to create and open the resource file, and then use the two toolbox calls Get1IndResource and DetachResource to get the sound resource from the file. Since we’ve taken direct responsibility for reading the resource, it is our responsibility to handle any errors, which we do by throwing the appropriate exception.

By the way, a new feature already added since ODF 1.0d9 will make it simpler to throw standard exceptions. Instead of writing the above FW_THROW statement, you could instead write something like this:

FW_FailOnError(::ResError());

We’ve seen how to internalize sound resources, now let’s see the code to externalize them:

CSoundAction::Externalize

void CSoundAction::Externalize(Environment* ev, 
 ODStorageUnit* storage)
{
    // Assume we’re focused on the correct property
 storage->AddValue(ev, kSoundScrapKind);
 if (fSoundHandle != NULL)
 {
 FW_CAcquireLockedSystemHandle lock(fSoundHandle);
 FW_CByteArray byteArray(*fSoundHandle, 
 FW_CMemoryManager::
 GetSystemHandleSize(fSoundHandle));
 storage->SetValue(ev, byteArray);
 }
}

Externalize simply adds a sound scrap property value, and then writes out the sound handle. Since the handle needs to be locked in order to be written out, we take advantage of the FW_CAcquireLockedSystemHandle helper object to lock the object and assume responsibility for unlocking it. The rest of the code should be self-explanatory.

Finally, we round out our implementation of CSoundAction with the DoIt method:

CSoundAction::DoIt

void CSoundAction::DoIt()
{
 if (fSoundHandle != NULL)
 {
 FW_CAcquireLockedSystemHandle lock(fSoundHandle);
 ::SndPlay(NULL, (SndListHandle)fSoundHandle, TRUE);
 }
}

Here we simply lock the handle with a helper object and then use the toolbox function SndPlay to play it. What could be easier?

CScriptAction

The implementation of the class CScriptAction has a lot in common with the class CSoundAction. Compiled scripts, like sounds, are stored in memory as relocatable blocks accessed via handles, and are stored on disk as resources in the resource fork. The code to internalize and externalize a compiled script is therefore similar to the code to internalize and externalize a sound resource. The only significant difference is in the implementation of InternalizeScriptFile, so for the sake of brevity, we’ll skip the code for CScriptAction::InternalizeScript and CScriptAction::Externalize.

CScriptAction::InternalizeScriptFile

void CScriptAction::InternalizeScriptFile(Environment* ev, 
 ODStorageUnit* storage)
{
    // Storage Unit should already be Focused on HFSFlavor
 unsigned long hfsSize = storage->GetSize(ev);
 if (hfsSize > 0)
 {
    // Get the HFS flavor
 HFSFlavor hfsInfo;
 FW_CByteArray byteArray;
 storage->GetValue(ev, sizeof(HFSFlavor), byteArray);
 byteArray.CopyBuffer(&hfsInfo, sizeof(HFSFlavor));
 
    // Make the file specification
 FW_CFileSpecification fileSpec(hfsInfo.fileSpec);
 
    // Acquire an opened resource file, 
    // will be closed automatically
 FW_CResourceFile resourceFile(fileSpec);
 
    // Acquire an opened resource, 
    // will be released automatically
 FW_CResource resource(resourceFile, 128, 'scpt');
 
    // Copy the resource handle
 fScriptHandle = FW_CMemoryManager::
 CopySystemHandle(resource.GetHandle());
 }
}

Compiled scripts are always stored with the fixed ID 128. This allows us to use the ODF resource subsystem to load the resource instead of reverting to the MacOS toolbox. This is done by simply creating a instance of the FW_CResource class. This class loads the resource in its constructor, and releases the resource in its destructor. It is exception-safe, and works correctly on both Macintosh and Windows. All we need to do to finish internalizing the script is to copy the resource handle and store it inside the fScriptHandle data member.

The CSoundAction::DoIt method executes the script using the static method CScriptAction::LoadAndExecuteScript:


CScriptAction::DoIt

void CScriptAction::DoIt()
{
 if (fScriptHandle != NULL)
 LoadAndExecuteScript(fScriptHandle);
}

CScriptAction::LoadAndExecuteScript

void CScriptAction::LoadAndExecuteScript(Handle scriptData)
{
 AEDesc scriptDesc;
 OSAID scriptID, resultID;
 AEDesc scriptText;
 OSAError error;
 static ComponentInstance gComponent = 0;

 if (!gComponent)
 gComponent = ::OpenDefaultComponent(
 kOSAComponentType, 
 kOSAGenericScriptingComponentSubtype);

    // load the script data
 scriptDesc.descriptorType = typeOSAGenericStorage;
 scriptDesc.dataHandle = scriptData;
 error = ::OSALoad(gComponent, &scriptDesc, 
 kOSAModeNull, &scriptID);
 
 if (error == noErr)
 { 
    // execute the compiled script in the default context
 error = ::OSAExecute(gComponent, 
 scriptID, kOSANullScript, 
 kOSAModeNull, &resultID);
 error = ::OSADispose(gComponent, scriptID);
 error = ::OSADispose(gComponent, resultID);
 }
}

There’s not much to say about this code. LoadAndExcecuteScript is lifted almost verbatim from IM:IAC, pp. 10-16. I haven’t bothered to address the fact that it just lets errors fall on the floor unnoticed, though with the post-1.0d9 additions to exception handling it will be a relatively easy task. All that is necessary is to sprinkle a few calls to FW_FailOnError(error).

That basically completes the description of the action classes. Observant readers will note that I’ve not yet described the two static methods IsInStorage, one each for the classes CSoundAction and CScriptAction. We’ll get to that shortly. Let’s move on to the implementation of the class CBeeperPart.

CBeeperPart

Earlier, we saw a partial class definition for CBeeperPart. At that time, we were interested in the protocol inherited from FW_CPart; we were only concerned with the protocol related to Actions. Now that we’re ready to talk about the part protocol, it might be worthwhile to see a complete definition for the class CBeeperPart:

class FW_CLASS_ATTR CBeeperPart : public FW_CPart
{
public:
 static const ODValueType kPartKind;

//----------------------------------------------------------
// Initialization/Destruction
//
public:
 CBeeperPart(ODPart* odPart);
 virtual ~CBeeperPart();
 
 virtual void  Initialize(Environment* ev);
 
//----------------------------------------------------------
// Inherited API
//
public:
 virtual FW_CFrame* NewFrame(Environment* ev,
 ODFrame* odFrame,
 FW_CPresentation* presentation,
 FW_Boolean fromStorage);
 
 virtual void InternalizeContent(Environment* ev, 
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo);
 
 virtual void ExternalizeContent(Environment* ev, 
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo);

//----------------------------------------------------------
// New API
//
public:
 CAction* GetAction() { return fAction; }
 void   SetAction(CAction* action);

public:
 void   DoAction();

//----------------------------------------------------------
// Data Members
//
private:
 CAction* fAction;
};

Let’s begin with the constructor and destructor of CBeeperPart:

CBeeperPart::CBeeperPart

CBeeperPart::CBeeperPart(ODPart* odPart) :
 FW_CPart(odPart, CBeeperPart::kPartKind),
 fAction(NULL)
{
}

CBeeperPart::~CBeeperPart
CBeeperPart::~CBeeperPart()
{
 delete fAction;
}

The constructor initializes the FW_CPart base class, and initializes the fAction field to NULL. The destructor simply deletes the object pointed to by fAction. Of course, the FW_CPart constructor does quite a bit of work; CBeeperPart inherits a lot of default behavior from its FW_CPart base class. It is beyond the scope of this article to go into the details of the class FW_CPart, but suffice it to say that ODF makes it pretty easy to create a new part by overriding just a few methods, as we’re about to see. The next method is the Initialize method:

CBeeperPart::Initialize

void CBeeperPart::Initialize(Environment* ev)
{
 FW_CPart::Initialize(ev);
 
    // ----- Register our Presentation
 CBeeperSelection *selection = 
 new CBeeperSelection(ev, this);
 RegisterPresentation(ev, kODFBeeperPresentation, 
 TRUE, selection);
}

To initialize the CBeeperPart, we must first call the inherited method to initialize the base class. Next, we create an instance of CBeeperSelection, which is a subclass of FW_CSelection. Selection objects are used to designate the user’s selection for data interchange via any of the interchange mechanisms: copy/paste, drag/drop, or linking. Finally, we register a presentation. Parts may have multiple presentations for displaying their data content. A classic example is a spreadsheet application that can display its data in a tabular array, bar chart, pie chart, etc. Each presentation may have its own selection, so the selection object is owned by the presentation.

CBeeperPart::NewFrame

FW_CFrame* CBeeperPart::NewFrame(Environment* ev,
 ODFrame* frame,
 FW_CPresentation* presentation,
 FW_Boolean storage)
{
 return new CBeeperFrame(ev, frame, presentation, this);
}

NewFrame is a factory method which will be called whenever the part needs a new display frame. If a part supports multiple presentations it would create different kinds of frames, but our Beeper part supports only one simple presentation and creates only one kind of frame.

CBeeperPart::InternalizeContent

void CBeeperPart::InternalizeContent(Environment *ev, 
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo)
{
 FW_ASSERT(fAction == NULL);
 
 if (CScriptAction::IsInStorage(ev, storage))
 fAction = new CScriptAction();
 else if (CSoundAction::IsInStorage(ev, storage))
 fAction = new CSoundAction();
 
 if (fAction != NULL)
 fAction->Internalize(ev, storage);
}

InternalizeContent is called when the part is being initialized from storage. CBeeperPart determines whether the storage contains a script or a sound, creates the appropriate action object, and then delegates to the action object. Here we finally see the purpose of the two static methods CScriptAction::IsInStorage and CSoundAction::IsInStorage. These methods examine the storage unit to see if they contain the appropriate data type.

This mechanism for creating an instance of the right action type based upon the contents of the storage unit is a bit simplistic. If we had many action kinds, it would become awkward, and a more extensible mechanism would become desirable. One reasonable choice would be based upon a dictionary mapping known data interchange types to factory functions. However, for the purpose of this example, such an approach is clearly overkill.

Once an action instance is created, we initialize it from the storage unit using the Internalize virtual function.

CBeeperPart::ExternalizeContent

void CBeeperPart::ExternalizeContent(Environment *ev, 
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo)
{
    // If any kind of property content exists...
 if (storage->Exists(ev, kODPropContents, kODNULL, 0))
 {
 storage->Focus(ev, kODPropContents, 
 kODPosUndefined, kODNULL, 0, kODPosAll);
 storage->Remove(ev);
 }
 storage->AddProperty(ev, kODPropContents);
 if (fAction != NULL)
 fAction->Externalize(ev, storage);
}

ExternalizeContent checks to see if any previous content exists in the storage unit, and if so, removes the old data. It then adds a content property, and delegates the task of externalizing to the Action object. This will result in a call to one of the two methods CSoundAction::Externalize or CSoundAction::Internalize, which we have already seen.

CBeeperPart::DoAction

void CBeeperPart::DoAction()
{
 if (fAction != NULL)
 fAction->DoIt();
}

DoAction is executed whenever the button is pressed via a mechanism that we’ll discuss shortly. DoAction merely forwards the request to the current action object.

CBeeperPart::SetAction

void CBeeperPart::SetAction(CAction *action)
{
 if (action && action!=fAction)
 {
 delete fAction;
 fAction = action;
 }
}

SetAction is used to change the action object. It is called when a client of CBeeperPart wants to change the part’s current action. We’ll see it used by the CBeeperSelection class.

CBeeperFrame

ODF contains a class FW_CFrame which provides a rich definition of the behavior of OpenDoc frames. FW_CFrame inherits behavior from four mixin classes (FW_MEventHandler, FW_MGadgetContainer, FW_MDragDroppable, and FW_MIdle), and has about 100 methods! Despite the size and apparent complexity of FW_CFrame, implementing a subclass is relatively easy; CBeeperFrame overrides only five methods:

class FW_CLASS_ATTR CBeeperFrame : public FW_CFrame, 
 public FW_MReceiver
{
//----------------------------------------------------------
// Initialization/Destruction
//
public:
 CBeeperFrame(Environment* ev, ODFrame* frame, 
 FW_CPresentation* presentation, CBeeperPart* part);     
 virtual ~CBeeperFrame();

//----------------------------------------------------------
// FW_CFrame overrides
//
public:
 virtual void CreateGadgetLayout(Environment* ev, 
 FW_CGadgetInitializer& initializer);

 virtual ODDragResult CanAcceptDrop(Environment* ev, 
 ODDragItemIterator* dragInfo);

 virtual void Draw(Environment* ev, 
 ODFacet* facet, ODShape* invalidShape);

 virtual void FrameShapeChanged(Environment* ev);

//----------------------------------------------------------
// FW_MReceiver overrides
//
public:
 virtual void HandleNotification(
 const FW_CNotification& notification);

//----------------------------------------------------------
// Data Members
//
private:
 CBeeperPart*  fBeeperPart;
 FW_CPushButton* fBeeper;
 ODID   fButtonId;
 FW_CHandleFunctionConnection fConnection;
 ODTypeTokenfButtonNotificationToken;
};

CBeeperFrame includes a button gadget, so it overrides the method CreateGadgetLayout to create the button gadget. CBeeperFrame can accept a drop of either scripts or sounds, so it overrides CanAcceptDrop. It overrides Draw to render itself, and it overrides the FrameShapeChanged notification to update its layout when the frame is resized. Finally, it overrides the HandleNotification method from the mixin class FW_MReceiver so that it can respond to button presses. Let’s now look at the implementation of these methods.

CBeeperFrame::CBeeperFrame

CBeeperFrame::CBeeperFrame(Environment* ev, 
 ODFrame* frame, 
 FW_CPresentation* presentation, 
 CBeeperPart* part) :
 FW_CFrame(ev, frame, presentation, part),
 fBeeperPart(part),
 fBeeper(NULL),
 fButtonId(0),
 fConnection(this),
 fButtonNotificationToken(0)
{
 fConnection.Connect();
 this->AddToFocusSet(ev,part->GetClipboardFocusToken(ev));
 SetDroppable(ev, TRUE);
}

The constructor initializes the base class and data members, and then sets up the notification connection, prepares to receive data from the clipboard, and registers itself as being interested in receiving data through drag-and-drop.

CBeeperFrame::~CBeeperFrame
CBeeperFrame::~CBeeperFrame()
{
 FW_CInterest interest(fBeeper, fButtonNotificationToken);
 fConnection.RemoveInterest(interest);
}

The destructor removes the notification connection.

CBeeperFrame::CreateGadgetLayout

void CBeeperFrame::CreateGadgetLayout(Environment* ev, 
 FW_CGadgetInitializer& initializer)
{
 FW_CDynamicString label("Beep");
 
 FW_CRect frameRect;
 this->GetFrameShapeBounds(ev, frameRect);

 fBeeper = FW_NEW(FW_CPushButton, 
 (ev, initializer, this, fButtonId, 
 frameRect[FW_kTopLeft], frameRect[FW_kBotRight],
 label));
 fBeeper->SetDefault(ev, false);

 fButtonNotificationToken = 
 fBeeper->GetButtonPressedNotificationToken(ev);
 FW_CInterest interest(fBeeper, fButtonNotificationToken);
 fConnection.AddInterest(interest);
}

CreateGadgetLayout creates a button of type FW_CPushButton, and then establishes its interest in receiving notifications for button clicks. The notification subsystem is a general-purpose mechanism for establishing event dependencies between objects. Objects may serve as Notifiers and Receivers. In our case, the push-button is a notifier, and the frame is the receiver. The FW_CInterest object establishes an interest in a particular kind of notification, in this case button pressed notifications. Adding the interest to the connection object establishes the connection. Once we establish the connection, button presses will result in the receiver’s HandleNotification method (see below) being called.

CBeeperFrame::CanAcceptDrop

ODDragResult CBeeperFrame::CanAcceptDrop(Environment* ev, 
 ODDragItemIterator* dragInfo)
{
 ODDragResult acceptDrop = 
 FW_CFrame::CanAcceptDrop(ev, dragInfo);

 if (!acceptDrop)
 {
 for (ODStorageUnit *dragSU = dragInfo->First(ev); 
 dragSU != NULL; 
 dragSU = dragInfo->Next(ev))
 {
 if (CSoundAction::IsInStorage(ev, dragSU))
 return TRUE;
 if (CScriptAction::IsInStorage(ev, dragSU))
 return TRUE;
 }
 }

 return acceptDrop;
}

CanAcceptDrop iterates over the storage units of the OpenDoc drag item, looking for one that contains recognizable data. If it finds either a sound or a script, CanAcceptDrop returns true.

CBeeperFrame::Draw
void CBeeperFrame::Draw(Environment* ev, 
 ODFacet* facet, 
 ODShape* invalidShape)
{
 FW_CFacetContext fc(ev, facet, invalidShape);
 
    // Just erase and assume gadgets will draw
 FW_CRect invalidRect;
 fc.GetClipRect(invalidRect);
 FW_CRectShape::RenderRect(fc, invalidRect, 
 FW_kFill, FW_kWhiteEraseInk);
}

The only rendering that a CBeeperFrame needs to explicitly do is to erase; the PushButton gadget will draw itself.

CBeeperFrame::HandleNotification

void CBeeperFrame::HandleNotification(
 const FW_CNotification& notification)
{
 const FW_CButtonPressedNotification& buttonPressed =
 (FW_CButtonPressedNotification&) notification;
 
 if (buttonPressed.GetButtonId() == fButtonId)
 {
 fBeeperPart->DoAction();
 }
}

This method is inherited from FW_MReceiver, and is called by the notification subsystem. This method first checks to ensure that it is receiving notification from the correct button, and then forwards the action request to the part (which will forward the request to the current CAction object).

CBeeperFrame::FrameShapeChanged

void CBeeperFrame::FrameShapeChanged(Environment* ev)
{
 FW_CFrame::FrameShapeChanged(ev);

    // Resize the Beeper to fit the new frame size
 FW_CRect frameRect;
 this->GetFrameShapeBounds(ev, frameRect);
 fBeeper->SetSize(ev, frameRect.BotRight());

    // Redraw the entire frame
 this->Invalidate(ev);
}

When the frame shape changes, the frame simply resizes the push-button to take up the entire frame.

CBeeperSelection

The ODF class FW_CSelection is an abstract base class used by ODF to represent the user’s active selection of data. A Copy command will copy this data to the clipboard, drag operations copy or move the selected data, etc. FW_CSelection defines four pure virtual methods that must be overridden; FW_CBeeperSelection defines these and overrides two more. As it turns out, the “selection” of a Beeper part is always the entire data content, so the implementation of these methods is especially simple; most don’t do anything.

class FW_CLASS_ATTR CBeeperPart;

class FW_CLASS_ATTR CBeeperSelection : public FW_CSelection
{
//----------------------------------------------------------
// Initialization/Destruction
//
public:
 CBeeperSelection(Environment* ev,
 CBeeperPart* beeperPart);
 virtual ~CBeeperSelection();
 
//----------------------------------------------------------
// Inherited API
//
public:
 virtual void CloseSelection(Environment* ev);
 virtual FW_Boolean ClearSelection(Environment* ev, 
 FW_ClearSelection clearAfter);
 virtual FW_Boolean IsEmpty(Environment* ev) const;
 virtual void SelectAll(Environment* ev);

 virtual void DoExternalizeSelection(Environment* ev, 
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo);

 virtual FW_Boolean DoInternalizeSelection(Environment* ev, 
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo);
 
//----------------------------------------------------------
// Data Members
//
private:
 CBeeperPart* fBeeperPart;
};

CBeeperSelection::CBeeperSelection

CBeeperSelection::CBeeperSelection(Environment* ev, 
 CBeeperPart* beeperPart) :
 FW_CSelection(ev, FALSE, FALSE),
 fBeeperPart(beeperPart)
{
}

The CBeeperSelection constructor simply initializes its base classes and caches away a pointer to the part.

CBeeperSelection::~CBeeperSelection

CBeeperSelection::~CBeeperSelection()
{
}

The destructor doesn’t need to do anything.

CBeeperSelection::CloseSelection

void CBeeperSelection::CloseSelection(Environment* ev)
{
}

In our case, closing a selection is a no-op...

CBeeperSelection::ClearSelection

FW_Boolean CBeeperSelection::ClearSelection(Environment* ev, 
 FW_ClearSelection clearAfter)
{
 return FALSE; // Do nothing
}

...as is clearing the selection.

CBeeperSelection::IsEmpty

FW_Boolean CBeeperSelection::IsEmpty(Environment* ev) const
{
 return (fBeeperPart->GetAction() == NULL);
}

A CBeeperSelection is empty if it does not have an action.

CBeeperSelection::SelectAll

void CBeeperSelection::SelectAll(Environment* ev)
{
}

The Beeper’s data is always selected, so there’s nothing to do.

CBeeperSelection::DoExternalizeSelection

void CBeeperSelection::DoExternalizeSelection(
 Environment* ev, 
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo)
{
 CAction *action = fBeeperPart->GetAction();
 if (action)
 {
 storage->AddProperty(ev, kODPropContents);
 action->Externalize(ev, storage);
 }
}

DoExternalizeSelection isn’t a no-op, but it is still trivial; We merely forward the operation onto the action object.

CBeeperSelection::DoInternalizeSelection

FW_Boolean CBeeperSelection::DoInternalizeSelection(
 Environment* ev,
 ODStorageUnit* storage, 
 FW_CCloneInfo* cloneInfo)
{
 FW_Boolean internalized = FALSE;
 
 CAction *action = NULL;
 if (CScriptAction::IsInStorage(ev, storage))
 action = new CScriptAction();
 else if (CSoundAction::IsInStorage(ev, storage))
 action = new CSoundAction();
 
 if (action != NULL)
 {
 action->Internalize(ev, storage);
 fBeeperPart->SetAction(action);
 fBeeperPart->Changed(ev);
 internalized = true;
 }

 return internalized;
}

DoInternalizeSelection is slightly more complicated. It should look familiar, since there is not much difference between this method and the CBeeperPart::InternalizeContent method. We simply determine type of data to internalize, create the appropriate action object, initialize it with the data, and then ask the part to make it be the current action object.

What Have We Done?

You’ve now seen all the custom code needed to create our enhanced Beeper part. It is not really a complete part yet; it is missing some obvious features that a truly useful Beeper should have, not the least of which is a better name!

One key feature that the Beeper needs is the ability to change the title of the button. If you look back over the code, you’ll see that the button title is hard-coded to be Beep inside the method CBeeperFrame::CreateGadgetLayout. A real part would allow the default title to be specified from a resource, and provide some simple means of changing the title. One possibility is to allow dropping a text clipping as the means of setting the title. Another possibility is to set the button title to the name of the file dropped onto the button. No doubt you can think of other features that a button-as-part should have, such as a visual style more interesting than the simple rounded-rectangle.

If you look past these shortcomings, I believe it’s fair to say that this example is a pretty impressive demonstration of the power of OpenDoc and ODF. With only a small amount of code, we have created a truly reusable software component. I’m not talking about the code-level reuse that we were able to achieve by using ODF, which is no small matter. I’m instead talking of the much more dramatic reuse achieved by high-level componentization. The ODFBeeper part is a component that can be reused without needing a single additional line of C++ code. End users with just a modest understanding of AppleScript could use the ODFBeeper component to create applications customized to their needs.

Clearly, the ODFBeeper component by itself, or even combined with a container part like ODFDraw, would not significantly empower end-users. We need quite a few more components before we’ll see such a dramatic shift. However, considering how easy it was to create this component, it doesn’t take much imagination to believe that we could soon have a wide assortment of powerful components.

Wrapping Up

With that uplifting vision of the future, it’s time for me to tie a bow around this article and ship it. One of my goals for this article was to give you a taste of what it’s like to develop for OpenDoc with ODF. Hopefully it’s left you thirsting for more. If you’d like more information about ODF, send a note to odfseed@apple.com; or feel free to write to me.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Capture One 11.0.1.40 - RAW workflow sof...
Capture One is a professional RAW converter offering you ultimate image quality with accurate colors and incredible detail from more than 400 high-end cameras -- straight out of the box. It offers... Read more
GraphicConverter 10.5.4 - $39.95
GraphicConverter is an all-purpose image-editing program that can import 200 different graphic-based formats, edit the image, and export it to any of 80 available file formats. The high-end editing... Read more
Dash 4.1.3 - Instant search and offline...
Dash is an API documentation browser and code snippet manager. Dash helps you store snippets of code, as well as instantly search and browse documentation for almost any API you might use (for a full... Read more
Microsoft OneNote 16.9 - Free digital no...
OneNote is your very own digital notebook. With OneNote, you can capture that flash of genius, that moment of inspiration, or that list of errands that's too important to forget. Whether you're at... Read more
DEVONthink Pro 2.9.17 - Knowledge base,...
Save 10% with our exclusive coupon code: MACUPDATE10 DEVONthink Pro is your essential assistant for today's world, where almost everything is digital. From shopping receipts to important research... Read more
OmniGraffle 7.6 - Create diagrams, flow...
OmniGraffle helps you draw beautiful diagrams, family trees, flow charts, org charts, layouts, and (mathematically speaking) any other directed or non-directed graphs. We've had people use Graffle to... Read more
iFinance 4.3.7 - Comprehensively manage...
iFinance allows you to keep track of your income and spending -- from your lunchbreak coffee to your new car -- in the most convenient and fastest way. Clearly arranged transaction lists of all your... Read more
Opera 50.0.2762.58 - High-performance We...
Opera is a fast and secure browser trusted by millions of users. With the intuitive interface, Speed Dial and visual bookmarks for organizing favorite sites, news feature with fresh, relevant content... Read more
Microsoft Office 2016 16.9 - Popular pro...
Microsoft Office 2016 - Unmistakably Office, designed for Mac. The new versions of Word, Excel, PowerPoint, Outlook and OneNote provide the best of both worlds for Mac users - the familiar Office... Read more
SoftRAID 5.6.4 - High-quality RAID manag...
SoftRAID allows you to create and manage disk arrays to increase performance and reliability. SoftRAID allows the user to create and manage RAID 4 and 5 volumes, RAID 1+0, and RAID 1 (Mirror) and... Read more

Latest Forum Discussions

See All

Around the Empire: What have you missed...
Around this time every week we're going to have a look at the comings and goings on the other sites in Steel Media's pocket-gaming empire. We'll round up the very best content you might have missed, so you're always going to be up to date with the... | Read more »
The 7 best games that came out for iPhon...
Well, it's that time of the week. You know what I mean. You know exactly what I mean. It's the time of the week when we take a look at the best games that have landed on the App Store over the past seven days. And there are some real doozies here... | Read more »
Popular MMO Strategy game Lords Mobile i...
Delve into the crowded halls of the Play Store and you’ll find mobile fantasy strategy MMOs-a-plenty. One that’s kicking off the new year in style however is IGG’s Lords Mobile, which has beaten out the fierce competition to receive Google Play’s... | Read more »
Blocky Racing is a funky and fresh new k...
Blocky Racing has zoomed onto the App Store and Google Play this week, bringing with it plenty of classic kart racing shenanigans that will take you straight back to your childhood. If you’ve found yourself hooked on games like Mario Kart or Crash... | Read more »
Cytus II (Games)
Cytus II 1.0.1 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0.1 (iTunes) Description: "Cytus II" is a music rhythm game created by Rayark Games. It's our fourth rhythm game title, following the footsteps of three... | Read more »
JYDGE (Games)
JYDGE 1.0.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.0 (iTunes) Description: Build your JYDGE. Enter Edenbyrg. Get out alive. JYDGE is a lawful but awful roguehate top-down shooter where you get to build your... | Read more »
Tako Bubble guide - Tips and Tricks to S...
Tako Bubble is a pretty simple and fun puzzler, but the game can get downright devious with its puzzle design. If you insist on not paying for the game and want to manage your lives appropriately, check out these tips so you can avoid getting... | Read more »
Everything about Hero Academy 2 - The co...
It's fair to say we've spent a good deal of time on Hero Academy 2. So much so, that we think we're probably in a really good place to give you some advice about how to get the most out of the game. And in this guide, that's exactly what you're... | Read more »
Everything about Hero Academy 2: Part 3...
In the third part of our Hero Academy 2 guide we're going to take a look at the different modes you can play in the game. We'll explain what you need to do in each of them, and tell you why it's important that you do. [Read more] | Read more »
Everything about Hero Academy 2: Part 2...
In this second part of our guide to Hero Academy 2, we're going to have a look at the different card types that you're going to be using in the game. We'll split them up into different sections too, to make sure you're getting the most information... | Read more »

Price Scanner via MacPrices.net

Deals on clearance 15″ Apple MacBook Pros wit...
B&H Photo has clearance 2016 15″ MacBook Pros available for up to $800 off original MSRP. Shipping is free, and B&H charges NY & NJ sales tax only: – 15″ 2.7GHz Touch Bar MacBook Pro... Read more
Apple restocked Certified Refurbished 13″ Mac...
Apple has restocked a full line of Certified Refurbished 2017 13″ MacBook Airs starting at $849. An Apple one-year warranty is included with each MacBook, and shipping is free: – 13″ 1.8GHz/8GB/128GB... Read more
How to find the lowest prices on 2017 Apple M...
Apple has Certified Refurbished 13″ and 15″ 2017 MacBook Pros available for $200 to $420 off the cost of new models. Apple’s refurbished prices are the lowest available for each model from any... Read more
The lowest prices anywhere on Apple 12″ MacBo...
Apple has Certified Refurbished 2017 12″ Retina MacBooks available for $200-$240 off the cost of new models. Apple will include a standard one-year warranty with each MacBook, and shipping is free.... Read more
Apple now offering a full line of Certified R...
Apple is now offering Certified Refurbished 2017 10″ and 12″ iPad Pros for $100-$190 off MSRP, depending on the model. An Apple one-year warranty is included with each model, and shipping is free: –... Read more
27″ iMacs on sale for $100-$130 off MSRP, pay...
B&H Photo has 27″ iMacs on sale for $100-$130 off MSRP. Shipping is free, and B&H charges sales tax for NY & NJ residents only: – 27″ 3.8GHz iMac (MNED2LL/A): $2199 $100 off MSRP – 27″ 3.... Read more
2.8GHz Mac mini on sale for $899, $100 off MS...
B&H Photo has the 2.8GHz Mac mini (model number MGEQ2LL/A) on sale for $899 including free shipping plus NY & NJ sales tax only. Their price is $100 off MSRP. Read more
Apple offers Certified Refurbished iPad minis...
Apple has Certified Refurbished 128GB iPad minis available today for $339 including free shipping. Apple’s standard one-year warranty is included. Their price is $60 off MSRP. Read more
Amazon offers 13″ 256GB MacBook Air for $1049...
Amazon has the 13″ 1.8GHz/256B #Apple #MacBook Air on sale today for $150 off MSRP including free shipping: – 13″ 1.8GHz/256GB MacBook Air (MQD42LL/A): $1049.99, $150 off MSRP Read more
9.7-inch 2017 WiFi iPads on sale starting at...
B&H Photo has 9.7″ 2017 WiFi #Apple #iPads on sale for $30 off MSRP for a limited time. Shipping is free, and pay sales tax in NY & NJ only: – 32GB iPad WiFi: $299, $30 off – 128GB iPad WiFi... Read more

Jobs Board

*Apple* Data Center Site Selection and Strat...
# Apple Data Center Site Selection and Strategy Research Analyst Job Number: 83708609 Santa Clara Valley, California, United States Posted: 18-Jan-2018 Weekly Hours: Read more
Security Engineering Coordinator, *Apple* R...
# Security Engineering Coordinator, Apple Retail Job Number: 113237456 Santa Clara Valley, California, United States Posted: 18-Jan-2018 Weekly Hours: 40.00 **Job Read more
Firmware Engineer - *Apple* Accessories - A...
# Firmware Engineer - Apple Accessories Job Number: 113422485 Santa Clara Valley, California, United States Posted: 18-Jan-2018 Weekly Hours: 40.00 **Job Summary** Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Store Leader - Retail District Manag...
Job Description:Job SummaryAs more and more people discover Apple , they visit our retail stores seeking ways to incorporate our products into their lives. It's your Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.