Wiki

Mac OS X: Handling Apple Events

by Trenton Schulz
Apple events are a high-level interprocess communication mechanism on Mac OS X. They're also the backbone of AppleScript, the scripting language of Mac OS X. Here we take a look at adding support for this feature to a Qt/Mac application.

[Download Source Code]

When a user double clicks on a file in the Finder, Finder sends an Apple event to the application associated with the file and asks it to open the file. If the application is not running, it is launched and then the request is made. The advantage of this approach is that there is only one instance of the application running.

On Windows, this usually is done by launching the application and passing the name of the file as a command line argument. Looking at many files results in launching many instances of the same application. (The QtSingleApplication component, available as a Qt Solution, addresses this issue.)

Qt 3 does not provide an abstraction for handling Apple events, but it is straightforward to add support for them in your application using Mac's Carbon API. Let's assume we have an application called XpmViewer that displays XPM files (an X11 image format). The main window class declaration follows:

class XpmViewer : public QMainWindow
{
    Q_OBJECT
 
public:
    XpmViewer(QWidget *parent = 0);
 
    void loadXpmFile(const QString &fileName);
    ...
};

The loadXpmFile() function takes a the name of an XPM file and displays the image to the user.

In order to process Apple events, we need to install an Apple event handler and pass the contents of the event to the XpmViewer instance. One way to achieve this is to make our own custom event and have the QApplication deal with it. Here's the definition of the event type:

const int OpenEventID = QEvent::User + 100;
 
class OpenEvent : public QCustomEvent
{
public:
    OpenEvent(const QString &fileName)
        : QCustomEvent(OpenEventID), file(fileName) {}
    QString fileName() const { return file; }
 
private:
    QString file;
};

Let's also add the logic of adding and removing the Apple event handler and the handler itself to a QApplication subclass:

class XpmApplication : public QApplication
{
public:
    XpmApplication(int argc, char *argv[]);
     XpmApplication();
 
    void setXpmViewer(XpmViewer *viewer);
 
protected:
    void customEvent(QCustomEvent *event);
 
private:
    static OSStatus appleEventHandler(
            const AppleEvent *event, AppleEvent *, long);
    ...
};

In addition to the Apple event handler and our custom event handler, we have a setXpmViewer() convenience function that we'll need later on to call XpmViewer::loadXpmFile().

XpmApplication::XpmApplication(int argc, char *argv[])
    : QApplication(argc, argv), viewer(0)
{
    AEInstallEventHandler(kCoreEventClass,
           kAEOpenDocuments, appleEventHandler, 0, false);
}
 
XpmApplication:: XpmApplication()
{
    AERemoveEventHandler(kCoreEventClass,
           kAEOpenDocuments, appleEventHandler, 0, false);
}

In the constructor and the destructor, we set up an Apple event handler. We first pass the class of events we are interested in, and then the event ID of the Apple event. In this case, we are only interested in the kAEOpenDocuments event which is part of the AECoreEvent class.

Then we pass our static Apple event function. Like many callback schemes, there is an opportunity to pass some extra data. Here, we just pass zero. Finally we pass false, telling Carbon that we only want this to be an application-specific handler (as opposed to system-wide). If we wanted to look at more events, we could make more calls to AEInstallEventHandler(), each with a different function to handle the event. Alternatively, we could accept all the events and demultiplex them in the handler.

Before we review the function that will give us the OpenEvents, let's take a look at our custom event handler:

void XpmApplication::customEvent(QCustomEvent *event)
{
    if (event->type() == OpenEventID)
        viewer->loadXpmFile(
            static_cast<OpenEvent *>(event)->fileName());
}

The event handler makes sure the the custom event is the right type. Then we call XpmViewer::loadXpmFile() with the file name specified by the event.

OSStatus XpmApplication::appleEventHandler(
        const AppleEvent *event, AppleEvent *, long)
{
    AEDescList docs;
    if (AEGetParamDesc(event, keyDirectObject, typeAEList,
                       &docs) == noErr) {
        long n = 0;
        AECountItems(&docs, &n);
        UInt8 strBuffer[256];
        for (int i = 0; i < n; i++) {
            FSRef ref;
            if (AEGetNthPtr(&docs, i + 1, typeFSRef, 0, 0,
                            &ref, sizeof(ref), 0)
                    != noErr)
                continue;
            if (FSRefMakePath(&ref, strBuffer, 256)
                    == noErr) {
                OpenEvent event(QString::fromUtf8(
                    reinterpret_cast<char *>(strBuffer)));
                QApplication::sendEvent(qApp, &event);
            }
        }
    }
    return noErr;
}

The Apple event handler has the event passed in as its first argument. The second argument is the reply and the last argument is the extra data we could have passed in. Here, we can ignore the last two arguments. Also, since we stated we were only interested in Open Documents events, we can safely assume the only type of event we have here is Open Documents.

The event contains a list of file names. We first get a copy of this list with AEGetParamDesc(). Then we iterate over the list and retrieve a file system reference (FSRef) out of the list. We then call FSRefMakePath() to get the file name as a UTF-8 string. Once we have the file name, we create an OpenEvent and then send it to our XpmApplication, which handles it in customEvent().

When building the application, we must explicitly link against Carbon by adding this line to the .pro file:

LIBS    += -framework Carbon

If we use our XpmApplication in main() and build our program, we should be able to drag an XPM file from the Finder onto the icon in the Dock representing our application.

We now have an application that can open XPM files. Usually you also want to associate your program with the file type. This requires us to add some information to the application's property list, which is part of the application's bundle.

A property list is an XML file of key--value pairs that Mac OS X uses for some runtime information about the application. qmake provides a default property list when it puts the application into the bundle as part of the make process; in our case we need a custom one. Here's our complete property list:

<!DOCTYPE plist PUBLIC
    "-//Apple Computer//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDocumentTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeExtensions</key>
            <array>
                <string>xpm</string>
            </array>
            <key>CFBundleTypeIconFile</key>
            <string>application.icns</string>
            <key>CFBundleTypeMIMETypes</key>
            <array>
                <string>image/x-xpm</string>
            </array>
            <key>CFBundleTypeName</key>
            <string>X PixMap File</string>
            <key>CFBundleTypeRole</key>
            <string>Viewer</string>
            <key>LSIsAppleDefaultForType</key>
            <true/>
        </dict>
    </array>
    <key>CFBundleIconFile</key>
    <string>application.icns</string>
    <key>CFBundleExecutable</key>
    <string>appleevents</string>
    <key>CFBundleIdentifier</key>
    <string>com.trolltech.XpmViewer</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>CSResourcesFileMapped</key>
    <true/>
</dict>
</plist>

The CFBundleDocumentTypes property (shown in bold) is an array of dictionaries that describe supported file types. In our case, there is only one file type (XPM).

The following table gives a quick explanation of what each key means in the CFBundleDocumentTypes array.

KeyDescription
CFBundleTypeExtensionsThe file name extension for the file
CFBundleTypeIconFileThe icon in your bundle that Finder should associate with the file type
CFBundleTypeMIMETypesThe MIME type for the file, this can be an array of multiple strings
CFBundleTypeNameThe text that will be shown in Finder
CFBundleTypeRoleSpecifies whether the program can open (Viewer), open and save (Editor), or is simply a shell to another program
LSIsAppleDefaultForTypeTells Finder (or more specifically Launch Services) that we want to be the default association for the file type

More information on these and other keys is provided on Apple's web site.

We've also added an icon to the bundle using the CFBundleIconFile property. The icon will be used for the application.

We now need to make sure that property list and the icon are copied into the bundle. Thankfully, we can let qmake do the dirty work for us by adding these lines to the .pro file:

RC_FILE             = xpmviewer.icns
QMAKE_INFO_PLIST    = Info.plist

The RC_FILE entry specifies the name of the icon file in the source tree. In the bundle, this file is always renamed application.icns.

After building our application with this new information and moving the resulting application to the Application directory, we should have a fully functioning Qt application that responds to the Apple open event. The full source code for the application can be downloaded here.

The good news is that Qt 4.0 is expected to include a QFileOpenEvent class for Mac OS X that will remove the need to get involved with Carbon directly.

As a final note, if you are interested in more information about Apple events, you might want to consult Apple's Event Manager page.

Naturally, no mention of Qt and scripting would be complete without a plug for Trolltech's own scripting toolkit, Qt Script for Applications (QSA). See the QSA Overview for details.


Copyright © 2005 Trolltech Trademarks