« Two Cores, Less Waiting | Main | Coffeenomics »

Print Dialog Extensions vs. Application Print Settings

For BBEdit 8.0, I did a bit of rework so that the application could present the printing sheets (as opposed the application-modal dialogs that were used by older versions). One of the technical obstacles that arose regarded the addition of items to the print dialog for application-specific printing options; for example: Show Line Numbers, Print Page Headers, and so forth.

When using the application-modal print dialog APIs, adding items to the print dialogs is relatively straightforward; you can install callback functions that get called when the style (Page Setup) and job (Print) dialogs come up, add items to the dialog with AppendDITL(), and handle item hits. When using the sheets, however, it's a little more complicated: you have to write a Print Dialog Extension (PDE). Apple does provide documentation and sample code for a PDE, however, so it's a manageable job.

So, if none of this is all that hard, what's the problem?

Well, the problem is settings, that's what. The application passes its specific settings to the PDE in the print session record, the PDE modifies them and passes the changed settings back to the application. That's fine, except when custom presets are in use (the ones that you create and manipulate on the Presets menu in the print job sheet). In that case, the printing subsystem completely ignores the settings that the application provides, and instead loads the application-specific settings from the preset. Except when you're using the "Standard" preset!

This is a real problem, because it essentially means that if you use custom presets for any reason (the most common uses being for custom paper handling, N-up printing, or the occasional printer-specific configuration tweak), your BBEdit printing settings are ignored. Even worse, if the preset was created in a different application, then no settings are loaded and the values passed in by the application are ignored, typically resulting in non-sensical zero values for all of the application-specific print options.

You can imagine the support mail this generates. And it's doubly frustrating that there's nothing an application can do to modify the behavior of the system.

(Aside: I think the reason it's done this way is because the PDE mechanism was originally conceived to support printer-driver-specific settings. If you look at the problem through a printer driver's eyes, the behavior makes a certain amount of sense, since printer driver settings live below the application level; thus, when you save a custom preset, the driver settings get snapshotted and saved.)

So, what to do? Well, in the abstract, the problem could be solved by figuring out an out-of-band way to exchange settings between the application and the PDE. AppKit manages to solve this somehow; there are plug-ins in the Carbon printing framework which presumably wrangles the UI and data interchange between Cocoa applications and the printing framework. Unfortunately, Apple is not able to describe the mechanism by which the Cocoa bridge works, so examining the system's own solution to this problem is not an option.

Then my colleague Jim Correia had a very clever idea, which turned out to be just the ticket: since the PDE runs as a plug-in to the application, it's a bundle (in CF nomenclature) that's loaded into the app's process space. So, the PDE can, in principle, call an entry point that has been exported from the application.

So, here's how it works: BBEdit exports two entry points, as follows:

extern "C"
    CFDictionaryRef CopyPrintSettingsForPrintSession(PMPrintSession session, OSStatus &err) __attribute__((visibility("default")));
    OSStatus        SetPrintSettingsForPrintSession(PMPrintSession session, CFDictionaryRef settings) __attribute__((visibility("default")));

Note the use of __attribute__((visibility("default"))). This causes these entry points to be exported from the application binary when it's linked. (We're using Xcode to build; if you're still using CodeWarrior, check its documentation for the syntax to export functions.)

In the PDE, then, we write a little glue to fetch the functions by name from the main bundle (which will always be the hosting application, and since our PDE is application-specific, that's just what we want):

CFDictionaryRef CPDEBase::CopyAppPrintSettingsForSession(PMPrintSession session, OSStatus &err)
    typedef CFDictionaryRef (*vCopyPrintSettingsForSession)(PMPrintSession, OSStatus &);

    static  vCopyPrintSettingsForSession    copyPrintSettingsForSession;

    if (NULL == copyPrintSettingsForSession)
        copyPrintSettingsForSession = reinterpret_cast(CFBundleGetFunctionPointerForName(CFBundleGetMainBundle(),
        check(NULL != copyPrintSettingsForSession);

    if (NULL != copyPrintSettingsForSession)
        return copyPrintSettingsForSession(session, err);

    err = kCantGetAppFunctionPointerFromPDE;
    return NULL;

OSStatus    CPDEBase::SetAppPrintSettingsForSession(PMPrintSession session, CFDictionaryRef dict)
    typedef OSStatus    (*vSetPrintSettingsForSession)(PMPrintSession, CFDictionaryRef);

    static  vSetPrintSettingsForSession setPrintSettingsForSession;

    if (NULL == setPrintSettingsForSession)
        setPrintSettingsForSession = reinterpret_cast(CFBundleGetFunctionPointerForName(CFBundleGetMainBundle(),
        check(NULL != setPrintSettingsForSession);

    if (NULL != setPrintSettingsForSession)
        return setPrintSettingsForSession(session, dict);

    return kCantGetAppFunctionPointerFromPDE;

Et voilà! So now, whenever the PDE needs the settings from the application (for example, to populate the application-specific items in the sheet), it calls its CopyAppPrintSettingsForSession, which calls back into the app, which will cons up a dictionary with the settings. Whenever the PDE needs to push the settings back to the app, it creates a dictionary with the settings, calls its SetAppPrintSettingsForSession and the application gets to capture the settings for its own nefarious purposes. The PMPrintSession value that's passed back and forth functions as a refcon in case there are multiple printing sheets up (which doesn't often happen in nature, but is something that the application developer should be prepared to handle): the application can match the print session value to whatever internal data structure it uses to track the printing process, as necessary.

Now that a separate pipeline exists between the application and its PDE, the problem of print settings being stored in the system presets is completely bypassed, and the system's management of print settings is left alone, to function as intended for printer drivers.

(Aside #2: Those of you in the home audience may observe that the print settings problem persists in current public releases of BBEdit and TextWrangler. It has been fixed internally, and it's our plan to release the fix in the next maintenance update for both products.)


"Note the use of __attribute__((visibility("default")))"

No, I do not note these. Safari does not show it. That probably is because of the

    .layout-one-column #container { width: 520px; }

in the stylesheet.

Yes, it is too narrow! Too narrow!!