
Carbon pasteboards allow applications to share data at runtime. By supporting
different types of data, pasteboards take the familiar concept of the Clipboard
to a new level.
Using the techniques discussed in this article, you can easily add pasteboard
support to an application, including copy and paste as well as drag and drop.
Setting up a service is slightly more involved, but once you handle the required
Carbon Events then you can fine-tune any translation or other capabilities your
application provides.
If you have used the Carbon Scrap Manager, then the Pasteboard
Manager API will feel comfortable, yet provide your application with better
control over the copy and paste process. Pasteboards also provide enhanced drag
and drop capability when used with the Drag Manager. If you have used Cocoa
Pasteboards under AppKit, then you will notice many similarities to Carbon
Pasteboards.
Evolution of Pasteboards
The Carbon Scrap Manager provides a single-item Clipboard for interapplication communication (IAC). The Clipboard has historically been the mechanism for live transfer of data between Carbon (and previously, Classic) applications. Because the Scrap Manager only supports one item at a time, there is neither a history nor a set of items that can be transferred.
The Carbon Drag Manager provides more interactivity than the Scrap Manager by permitting the user to control the placement of pasted items using the mouse. In Mac OS X v10.3 Panther the Drag Manager API was updated to use pasteboards in drag operations.
Cocoa pasteboards (embodied in the NSPasteboard class) are similar to the Scrap and Drag Managers in that all three support multiple data types. However, NSPasteboard does not support multiple items.
Benefits
Adding pasteboard support to your Carbon application results in the following benefits:
- A pasteboard may contain multiple items. Your application can iterate over the pasteboard contents looking for a particular item by type, index, content, etc.
- A single data transfer model, unifying the APIs for copy and paste, drag and drop, and services.
- Cocoa compatibility allows Carbon and Cocoa applications to share data through use of Uniform Type Identifiers (discussed in the section Pasteboard Terms).
Carbon services may also make use of pasteboards. A service is a faceless process that provides a specific capability to applications or other services. This article provides an example of a service that handles text copying between applications, including Carbon and Cocoa.
System-Provided Pasteboards
System pasteboards exist in two familiar settings:
The Clipboard is available to Carbon applications, and is designated for copy and paste operations. If your application has limited data exchange needs, the Clipboard's behavior might be sufficient. The examples in this article use the Clipboard.
-
The system Find utility accepts search terms from other sources, and the Find pasteboard is also available to applications, as demonstrated by one of the applications in this article.
Pasteboard Terms
A pasteboard reference, type PasteboardRef, is the primary data structure for distinguishing between pasteboards. As with other Carbon data types, the pasteboard reference is opaque and you must use functions to access its fields, such as its name and contained items.
Pasteboards may support a variety of data flavors. This allows pasteboards to support multiple data types, distinguished from each other through the use of Uniform Type Identifiers, which map types in a hierarchical manner and allow for equality checking and, especially, a representation or display format. UTIs use a reverse domain name scheme. For example, com.adobe.pdf identifies the Portable Document Format flavor. LaunchServices/UTType.h has a short primer on the subject.
Pasteboards may support multiple items. You can traverse the set of items in a pasteboard by index and identifier. For each item you can inspect and change its properties.
A promise is an agreement between the provider of data and a pasteboard (the user of that data). The provider agrees to make the data available at a later time in the desired format. This is useful for large data elements that may take time to assemble, and for deferring memory allocation for large elements. The user of the data (the pasteboard) must call-in promises prior to making use of the data. This forces the provider to send the data. A promise is implemented as a callback, and registered using PasteboardSetPromiseKeeper. The operating system automatically invokes the callback when the promise is called-in.
A pasteboard consists of a global instance or resource and one or more local references to that instance. This allows multiple interface elements (such as windows) in an application to work with a local copy of the pasteboard. You need to synchronize the local reference with the global instance at certain times: any changes to the global resource will then be reflected in the local reference. The examples in this article synchronize during reads from and writes to the pasteboard.
In the sample code discussed in this article, Carbon Events provide a message-passing structure for interacting with pasteboards. This is not a requirement, and there are no pasteboard-specific events. This technique is shown as a convenience. Refer to the documents at the end of this article for more information about the Carbon Event model.
A Simple Copy and Paste Example
This first example illustrates how to copy and paste PDF data using pasteboards. The Carbon Event handler fragment in Listing 1 responds to the Copy and Paste menu item events, invoking functions that add data to a pasteboard and retrieve data from a pasteboard, respectively.
Listing 1: Handling Copy and Paste Events
// ...
switch (command.commandID)
{
case kHICommandCopy:
// for now, only demonstrate how to put the current document content
// as 'pdf' on the clip board
{
err = AddWindowContentToPasteboardAsPDF( GetPasteboard(), docStP );
}
break;
case kHICommandPaste:
// for now, only demonstrate how to paste in a 'pdf' from the clip board
{
CFDataRef pdfData;
if ( PasteboardContainsPDF( GetPasteboard(), &pdfData ) )
{
AttachPDFToWindow( pdfData, docStP ); // this will retain the
// pdfData in docStP
CFRelease( pdfData );
}
else
{
fprintf( stderr, "kHICommandPaste: Paste menu item should have been
disabled!\n" );
}
}
break;
// Handle additional events...
Each of the item handlers calls the accessor function shown in Listing 2 to retrieve the static pasteboard reference to the system Clipboard. If you are not familiar with the modifier static, it marks a variable as requiring declaration and initialization only once, even inside a function (where variables are typically local in scope). Once a static variable's value is set, the variable retains its value for the life of the program or until explicitly reassigned. In this function, the declaration and assignment of sPasteboard occurs only the first time this function gets called.
Listing 2: Retrieving the Static Pasteboard Reference
PasteboardRef GetPasteboard( void )
{
static PasteboardRef sPasteboard = NULL;
if ( sPasteboard == NULL )
{
PasteboardCreate( kPasteboardClipboard, &sPasteboard );
}
return sPasteboard;
}
The constant kPasteboardClipboard refers to the system Clipboard, defined in HIServices/Pasteboard.h as a string:
#define kPasteboardClipboard CFSTR( "com.apple.pasteboard.clipboard" )
The Clipboard pasteboard is reserved for copy and paste operations. If your application requires other operations, you should create your own pasteboard(s).
Copying To a Pasteboard
AddWindowContentToPasteboardAsPDF (Listing 3) transfers data to the pasteboard in response to kHICommandCopy. After first clearing the pasteboard, this function invokes a set of custom callbacks to populate the pdfData variable. (This population step is not shown. You may find it more convenient to create the data in a separate function, and then pass a pointer to that data in to a function similar to this one.) AddWindowContentToPasteboardAsPDF then calls PasteboardPutItemFlavor to copy pdfData to the pasteboard item whose ID is 1. Note that this is not necessarily the pasteboard item at index 1. The UTI identifying the flavor of this item is com.adobe.pdf.
The require variants are assertion macros that check specific conditions and jump to one of the labeled locations in this function on failure.
Listing 3: Copying PDF Data to the Pasteboard
static OSStatus AddWindowContentToPasteboardAsPDF( PasteboardRef pasteboard,
const DocStorage* docStP )
{
OSStatus err = noErr;
CGRect docRect = CGRectMake(0, 0, docStP->docSize.h,
docStP->docSize.v);
CFDataRef pdfData = CFDataCreateMutable( kCFAllocatorDefault, 0 );
CGContextRef pdfContext;
CGDataConsumerRef consumer;
CGDataConsumerCallbacks cfDataCallbacks = { MyCFDataPutBytes, MyCFDataRelease };
// We need to clear the pasteboard of it's current contents so that this
// application can own it and add it's own data.
err = PasteboardClear( pasteboard );
require_noerr( err, PasteboardClear_FAILED );
// snip: write the actual bytes to pdfData
err = PasteboardPutItemFlavor( pasteboard, ( PasteboardItemID )1,
CFSTR( "com.adobe.pdf" ), pdfData, 0 );
require_noerr( err, PasteboardPutItemFlavor_FAILED );
CGPDFContextCreate_FAILED:
PasteboardPutItemFlavor_FAILED:
// snip: handle failures
PasteboardClear_FAILED:
return err;
}
Copying From a Pasteboard
Pasting, or more accurately "copying from the pasteboard", is a two-step process in this example (refer back to the event handler). The first call, PasteboardContainsPDF, checks whether there is PDF data on the pasteboard. If so, copy the data to the destination by calling AttachPDFToWindow. Otherwise print an error message.
The function PasteboardContainsPDF in Listing 4 iterates over the pasteboard contents looking for the first PDF item. Note the comparison against the UTI "com.adobe.pdf" as the desired flavor. If found, the associated data is copied to the CFDataRef pointer passed in the second argument. If you look at the API documentation for PasteboardCopyItemFlavorData, you will find that the client using the CFDataRef is responsible for releasing the pointer memory.
You may consider iterating over the pasteboard contents to be a waste of cycles if you only intend to support one item and one flavor on your pasteboard. For small or simple applications that may be true. However, most applications do not remain small or simple over time. Including this loop may make modifications easier in the future. For additional flexibility, move the loop into a standalone function that returns the nth item for a given flavor, passing the flavor as a parameter. Then your pasteboard will support the return of arbitrary items and flavors without requiring you to rewrite much if any of the pasteboard traversal code.
Note that the outer loop, which iterates over the pasteboard items, is 1-based. This aspect of the Pasteboard Manager API retains compatibility with the old Drag Manager API, which was 1-based. However, the inner loop, which iterates over the CFArrayRef contents, is 0-based.
Listing 4: Copying PDF Data from the Pasteboard
static Boolean PasteboardContainsPDF( PasteboardRef inPasteboard, CFDataRef* pdfData )
{
Boolean gotPDF = false;
OSStatus err = noErr;
PasteboardSyncFlags syncFlags = PasteboardSynchronize( inPasteboard );
ItemCount itemCount;
UInt32 itemIndex;
if ( syncFlags & kPasteboardModified )
{
fprintf( stderr, "Pasteboard modified\n" );
}
// Count the number of items on the pasteboard so we can iterate through them.
err = PasteboardGetItemCount( inPasteboard, &itemCount );
require_noerr( err, PasteboardGetItemCount_FAILED );
for ( itemIndex = 1; itemIndex <= itemCount; ++itemIndex )
{
PasteboardItemID itemID;
CFArrayRef flavorTypeArray;
CFIndex flavorCount;
CFIndex flavorIndex;
// Every item is identified by a unique value.
err = PasteboardGetItemIdentifier( inPasteboard, itemIndex, &itemID );
require_noerr( err, PasteboardGetItemIdentifier_FAILED );
// The flavorTypeArray is a CFType and we'll need to call CFRelease on it
// later.
err = PasteboardCopyItemFlavors( inPasteboard, itemID, &flavorTypeArray );
require_noerr( err, PasteboardCopyItemFlavors_FAILED );
flavorCount = CFArrayGetCount( flavorTypeArray );
for ( flavorIndex = 0; flavorIndex < flavorCount; ++flavorIndex )
{
CFStringRef flavorType;
CFComparisonResult comparisonResult;
// grab the flavorType so we can extract it's flags and data
flavorType = ( CFStringRef )CFArrayGetValueAtIndex( flavorTypeArray,
flavorIndex );
// If it's a pdf, get it and return true
comparisonResult = UTTypeConformsTo( flavorType, CFSTR( "com.adobe.pdf" ) );
if ( comparisonResult == kCFCompareEqualTo )
{
if ( pdfData != NULL )
{
err = PasteboardCopyItemFlavorData( inPasteboard, itemID,
flavorType, pdfData );
require_noerr( err, PasteboardCopyItemFlavorData_FAILED );
}
// Exit this loop after finding the PDF flavor in this item.
// The break eliminates unnecessary iterations.
// However, if other items contain PDF data they will
// overwrite the first one found.
gotPDF = true;
break;
}
PasteboardCopyItemFlavorData_FAILED:
;
}
CFRelease( flavorTypeArray );
PasteboardCopyItemFlavors_FAILED:
PasteboardGetItemIdentifier_FAILED:
;
}
PasteboardGetItemCount_FAILED:
return gotPDF;
}
The function AttachPDFToWindow, shown in Listing 5, associates the passed-in data with the second argument, a DocStorage pointer. The DocStorage type is defined elsewhere and is a custom data structure, not a window as suggested by the function name. DocStorage may or may not be able to draw the PDF data directly, which is why this function name uses the verb attach rather than paste.
If the DocStorage pointer already has a reference to existing data, it releases that reference before adding a reference to the new data. CFRetain and CFRelease are part of the reference counting mechanism that allows the operating system to automatically free the memory associated with an object when no more active references to that object exist. Note the complementary use of CFRetain and CFRelease when operating on the CFDataRef argument. Always use the same API for reference counting and for memory allocation and deallocation, otherwise unexpected behavior may occur.
Listing 5: Associating Data with a Window
static void AttachPDFToWindow( CFDataRef pdfData, DocStorage* docStP )
{
if ( docStP->pdfData != NULL )
{
CFRelease( docStP->pdfData );
docStP->pdfData = NULL;
}
docStP->pdfData = CFRetain( pdfData );
}
Advanced Pasteboard Manipulation
The next example is the Pasteboard Peeker sample code, available on the ADC website. Pasteboard Peeker runs as an application that also provides a text manipulation service. It makes available in the Services menu an item named Pasteboard Peeker Service that copies text to a pasteboard, then displays that text in multiple flavors in a HITextView object (or Text View).
Figure 1 shows the contents of the pasteboard after copying text containing an HTML tag.
Figure 1: Pasteboard Content
To experiment, first run the Pasteboard Peeker application. In the text window, highlight a portion of the default text (or enter something new), and select Edit > Copy, then Edit > Paste. Pasteboard Peeker displays the selected text in several forms. The main window contains a HITextView object that displays the pasteboard content (one item) in multiple flavors as well as converted to uppercase and lowercase. You can also paste text from other applications. In addition, Pasteboard Peeker supports the revised drag and drop model (new in Mac OS X v10.3 Panther) that relies on pasteboards.
Pasteboard Related Events
Listing 6 shows the Pasteboard Peeker event handler, which supports pasteboard related events, including Copy, Paste, dragging and dropping, and several custom events. This event handler is broken into sections responsible for specific event classes and event IDs. Note the hierarchical relationship between class and ID.
The first handled event class is kEventClassCommand, which includes events generated by menu commands. This function responds to the events kHICommandCopy, kHICommandPaste, and several custom commands, including converting selected text to uppercase, converting to lowercase, and displaying the pasteboard contents in the Text View. All command IDs are of type OSType, a 32-bit integer commonly displayed as a four character sequence, such as 'UPPR', 'LOWR', and so on. Figure 2 shows how this sequence maps to the Command field in the Menu Item Info view for the uppercase translation item in Interface Builder.
Figure 2: Uppercase Menu Item Info
The second class of supported event is kEventClassControl. The only supported event in that class is kEventControlTrack, which occurs as a result of dragging text in or from the Text View. The function handling this event illustrates the Panther approach to initiating a drag, using the Pasteboard Manager API.
The third supported event class is kEventClassService. This class defines the events that help distinguish the service provided by this application.
All of the system-defined event constant definitions (those beginning with a 'k') discussed here are found in HIToolbox/CarbonEvents.h. The commandIDs specified as four-character literals are custom to this application.
The TXNObject type used here is the text object associated with
the HITextView in the application's window. In this article the
distinction is blurred: the text object is referred to as "the Text View", and
for practical purposes it is the focus of the text display. For a better
description of the distinction between HITextView and
TXNObject refer to the
Multilingual Text Engine Reference.
Listing 6: A Carbon Event Handler that Supports Multiple Pasteboard Related Events
OSStatus EventHandler( EventHandlerCallRef inHandlerCallRef, EventRef inEvent,
void *inUserData )
{
OSStatus err = eventNotHandledErr;
EventClass eventClass = GetEventClass( inEvent );
EventKind eventKind = GetEventKind( inEvent );
TXNObject txnObject = ( TXNObject )inUserData;
switch( eventClass )
{
case kEventClassCommand:
{
HICommand command;
require_noerr( GetEventParameter( inEvent, kEventParamDirectObject,
typeHICommand, NULL, sizeof( command ), NULL, &command ) ,
CantGetHICommand );
switch( command.commandID )
{
case kHICommandCopy: // override the text view's copy support
{
err = AddDataToPasteboard( gClipboard, txnObject, true );
}
break;
case kHICommandPaste: // override the text view's paste support
{
err = GetDataFromPasteboard( gClipboard, txnObject );
}
break;
case 'UPPR': // handle the request for uppercase translation
{
err = RequestTextTranslation(
CFSTR( "com.apple.pasteboardpeeker.uppercasetext" ),
txnObject );
}
break;
case 'LOWR': // handle the request for lowercase translation
{
err = RequestTextTranslation(
CFSTR( "com.apple.pasteboardpeeker.lowercasetext" ),
txnObject );
}
break;
case 'FSEL': // add selected text to the find pasteboard
{
err = AddDataToPasteboard( gFindPasteboard, txnObject, false );
}
break;
case 'FIND': // show the find pasteboard contents
{
err = GetDataFromPasteboard( gFindPasteboard, txnObject );
}
break;
}
}
break;
case kEventClassControl:
{
switch( eventKind )
{
case kEventControlTrack: // initiate a drag
{
err = HandleDragInitiation( inEvent, txnObject );
}
break;
}
}
break;
case kEventClassService:
{
switch( eventKind )
{
case kEventServiceGetTypes: // advertise servicable types
{
err = HandleGetTypes( inEvent, txnObject );
}
break;
case kEventServiceCopy: // copy data from the text object to the
// service pasteboard
{
err = HandleServiceCopy( inEvent, txnObject );
}
break;
case kEventServicePaste: // paste data from the service pasteboard
// to the text object
{
err = HandleServicePaste( inEvent, txnObject );
}
break;
case kEventServicePerform: // handle service and translation
// requests
{
err = HandlePerformService( inEvent, txnObject );
}
break;
}
}
break;
}
CantGetHICommand:
return err;
}
The remainder of this article discusses the individual event functions.
Inspecting Pasteboard Contents
PasteboardPeeker displays its contents in a Text View inside a window. Most pasteboards will not need this capability, but it is useful for understanding how a pasteboard stores data and for inspecting flavors and content. Much of the function GetDataFromPasteboard in Listing 7 involves formatting data for display. This discussion covers the entire function, but your pasteboards may not need all this capability.
GetDataFromPasteboard is called from several places in the program. This function takes as arguments a reference to a pasteboard and a text object. If you follow the calling chain back up several levels to main you will see that this text object is the Text View. During installation of the event handler the Text View was specified as the void * inUserData parameter.
TXNObject txnObject = NULL;
...
err = InstallControlEventHandler( textView, EventHandler,
GetEventTypeCount( eventList ), eventList, txnObject, NULL );
The TXNObject type is defined in HIToolbox/MacTextEditor.h.
The call to PasteboardSynchronize forces the local pasteboard reference to sync with the global pasteboard. The inline comments in the source code state that this is not a typical use of synchronization. Most applications should check the pasteboard upon receiving the kEventAppActivated event, and adjust the Paste menu item accordingly.
Following synchronization, this function sets the initial content for the Text View to the pasteboard reference value and item count. The function then iterates over each item in the pasteboard, retrieving and appending to the Text View content the index and ID values. Note that the loop index is 1-based: this conforms to the old Drag Manager API, and should simplify porting to the new pasteboard model. Next, for each flavor of the item, the function appends the UTI, the flavor as a pasteboard flavor type (if defined) or an OSType, and an appropriate representation of the text.
Listing 7: Displaying Pasteboard Data
OSStatus GetDataFromPasteboard( PasteboardRef inPasteboard, TXNObject inTXNObject )
{
OSStatus err = noErr;
PasteboardSyncFlags syncFlags;
ItemCount itemCount;
char pasteboardText[ 64 ];
// Check to see whether the pasteboard has been updated. This isn't strictly
// necessary because the creation routine syncs automatically. Typically the
// sync of the pasteboard would be checked when the application is brought
// forward to see if there is any tasty information on the pasteboard. If so,
// the paste menu item should be enabled. For PasteboardPeeker, we accept
// all data so the paste menu item is always enabled.
// For instance, the commented out require statement would only allow data to be
// retrieved from the pasteboard if it had been modified since the last time we
// looked.
syncFlags = PasteboardSynchronize( inPasteboard );
//require_action( syncFlags&kPasteboardModified, PasteboardOutOfSync,
err = badPasteboardSyncErr );
// Count the number of items on the pasteboard so we can iterate through them.
err = PasteboardGetItemCount( inPasteboard, &itemCount );
require_noerr( err, CantGetPasteboardItemCount );
// We need to clear the text view and add the pasteboard and item count
// information.
sprintf( pasteboardText, "PasteboardRef: %d ItemCount: %d\n",
( int )inPasteboard, ( int )itemCount );
TXNSetData( inTXNObject, kTXNTextData, pasteboardText, strlen( pasteboardText ),
kTXNStartOffset, kTXNEndOffset );
for( UInt32 itemIndex = 1; itemIndex <= itemCount; itemIndex++ )
{
PasteboardItemID itemID;
CFArrayRef flavorTypeArray;
CFIndex flavorCount;
char itemText[ 64 ];
// Every item is identified by a unique value.
err = PasteboardGetItemIdentifier( inPasteboard, itemIndex, &itemID );
require_noerr( err, CantGetPasteboardItemIdentifier );
// Now lets add the item index, identifier and flavor count information
// to the text view.
sprintf( itemText, " Index: %d item ID: %d\n", (int)itemIndex,
(int)itemID );
TXNSetData( inTXNObject, kTXNTextData, itemText, strlen( itemText ),
kTXNEndOffset, kTXNEndOffset );
// The item's flavor types are retreived as an array which we are responsible
// for releasing later. It's important to take into account all flavors,
// their flags and the context the data will be used when deciding which
// flavor ought to be used. The flavor type array is a CFType and we'll need
// to call CFRelease on it later.
err = PasteboardCopyItemFlavors( inPasteboard, itemID, &flavorTypeArray );
require_noerr( err, CantCopyPasteboardItemFlavors );
// Count the number of flavors in the item so we can iterate through them.
flavorCount = CFArrayGetCount( flavorTypeArray );
for( CFIndex flavorIndex = 0; flavorIndex <= flavorCount; flavorIndex++ )
{
CFStringRef flavorType, nsPboardFlavorType, osTypeFlavorType;
PasteboardFlavorFlags flavorFlags;
CFDataRef flavorData;
CFIndex flavorDataSize;
char flavorTypeStr[ 128 ], nsPboardFlavorTypeStr[ 64 ],
osTypeFlavorTypeStr[ 5 ];
char flavorText[ 256 ];
if ( flavorIndex < flavorCount )
// grab the flavor name so we can extract it's flags and data
flavorType = ( CFStringRef )CFArrayGetValueAtIndex( flavorTypeArray,
flavorIndex );
else
// go through these steps one extra time to look for our
// request only flavor
flavorType = CFSTR( "com.apple.pasteboardpeeker.requestonly" );
// Getting the flavor flags gives us insight to the nature of the flavor.
// If your application doesn't want to access
err = PasteboardGetItemFlavorFlags( inPasteboard, itemID, flavorType,
&flavorFlags );
require_noerr( err, CantGetItemFlavorFlags );
// Having looked at the item's flavors and their flags we've settled on
// the data we want to reteive. Because we're copying the flavor data
// we'll need to dispose of it via CFRelease when we no longer need it.
err = PasteboardCopyItemFlavorData( inPasteboard, itemID, flavorType,
&flavorData );
require_noerr( err, CantCopyFlavorData );
flavorDataSize = CFDataGetLength( flavorData );
nsPboardFlavorType = UTTypeCopyPreferredTagWithClass( flavorType,
kUTTagClassNSPboardType );
osTypeFlavorType = UTTypeCopyPreferredTagWithClass( flavorType,
kUTTagClassOSType );
// Now that we have the flavor, flags, and data we need to format it
// nicely for the text view.
CFStringGetCString( flavorType, flavorTypeStr, 128,
kCFStringEncodingMacRoman );
if ( nsPboardFlavorType != NULL )
CFStringGetCString( nsPboardFlavorType, nsPboardFlavorTypeStr, 64,
kCFStringEncodingMacRoman );
else
nsPboardFlavorTypeStr[0] = 0;
if ( osTypeFlavorType != NULL )
CFStringGetCString( osTypeFlavorType, osTypeFlavorTypeStr, 5,
kCFStringEncodingMacRoman );
else
osTypeFlavorTypeStr[0] = 0;
sprintf( flavorText,
" \"%.80s\"\n \"%s\"\n '%s' %c%c%c%c%c%c %d ",
flavorTypeStr, nsPboardFlavorTypeStr, osTypeFlavorTypeStr,
( flavorFlags&kPasteboardFlavorPromised ) ? 'P' : '_',
( flavorFlags&kPasteboardFlavorSystemTranslated ) ? 'T' : '_',
( flavorFlags&kPasteboardFlavorRequestOnly ) ? 'r' : '_',
( flavorFlags&kPasteboardFlavorNotSaved ) ? 'n' : '_',
( flavorFlags&kPasteboardFlavorSenderTranslated ) ? 't' : '_',
( flavorFlags&kPasteboardFlavorSenderOnly ) ? 's' : '_',
( int )flavorDataSize );
TXNSetData( inTXNObject, kTXNTextData, flavorText, strlen( flavorText ),
kTXNEndOffset, kTXNEndOffset );
if ( nsPboardFlavorType != NULL )
CFRelease( nsPboardFlavorType );
if ( osTypeFlavorType != NULL )
CFRelease( osTypeFlavorType );
flavorDataSize = (flavorDataSize<80) ? flavorDataSize : 80;
for( short dataIndex = 0; dataIndex <= flavorDataSize; dataIndex++ )
{
char byte = *(CFDataGetBytePtr( flavorData ) + dataIndex);
flavorText[dataIndex] = ( byte > 40 ) ? byte : ' ';
}
flavorText[ flavorDataSize ] = '\n';
flavorText[ flavorDataSize + 1 ] = '\n';
TXNSetData( inTXNObject, kTXNTextData, flavorText, flavorDataSize+2,
kTXNEndOffset, kTXNEndOffset );
CantCopyFlavorData:
CantGetItemFlavorFlags:
// Don't report the error if we couldn't find the request only flavor.
if ( ( err == badPasteboardFlavorErr ) && ( flavorIndex == flavorCount ) )
err = noErr;
}
CantCopyPasteboardItemFlavors:
CantGetPasteboardItemIdentifier:
;
}
CantGetPasteboardItemCount:
///PasteboardOutOfSync:
return err;
}
Keeping Promises
A promise is a guarantee that data will be provided to a pasteboard at a later time. AddDataToPasteboard (discussed next) contains a call to PasteboardSetPromiseKeeper, which sets a promise on the pasteboard. The implementation of a promise is a callback of type PasteboardPromiseKeeperProcPtr. Its arguments include:
- the local
PasteboardRef to which the data should be written
- the identifier of the pasteboard item in which to place the data
- the flavor as a UTI in
CFStringRef form. This is not used to format the data, but rather to inform the pasteboard item of the type being added.
- the programmer-defined context passed to
PasteboardSetPromiseKeeper. This example passes the Text View from the application window as the context.
The callback places the data of the appropriate flavor in the requested item on the pasteboard. Note that the CFDataRef gets released after use. The pasteboard copies the data, so the original reference is not required. Since the data was allocated dynamically using CFDataCreate, you should release the memory using CFRelease, which decrements the reference count of the memory object. Not releasing will ultimately result in a memory leak; releasing or directly freeing memory using a different API (such as free) will have unpredictable and possibly bad results.
Listing 8: Providing Data through a Promise Keeper Callback
OSStatus PromiseKeeper( PasteboardRef inPasteboard, PasteboardItemID inItem,
CFStringRef inFlavorType, void *inContext )
{
OSStatus err = noErr;
CFDataRef promisedData = NULL;
// create the promised flavor data
promisedData = CFDataCreate( kCFAllocatorDefault, (UInt8*)"Promised data", 14 );
require_action( promisedData != NULL, CantCreatePromisedData, err = memFullErr );
// fulfill the promised data request for item and flavor type
err = PasteboardPutItemFlavor( inPasteboard, inItem, inFlavorType, promisedData, 0 );
CFRelease( promisedData );
require_noerr( err, CantFulfillPromise );
CantFulfillPromise:
CantCreatePromisedData:
return err;
}
Copying To a Pasteboard, Revisited
This example copies data to a pasteboard in a slightly different manner than the first example. The function AddDataToPasteboard sets a promise keeper callback for the Text View. The implied agreement is that when the Text View requires data the callback will make data available. The operating system automatically invokes the callback.
This function also checks the current selection range in the Text View. The selected text is placed into pasteboard item 1 with a UTI of com.apple.traditional-mac-plain-text. This forms the basis for the new content of the Text View. The boolean argument addExtraTypes determines whether two additional, arbitrary flavors are added to the pasteboard item. The flavor flags kPasteboardPromisedData and kPasteboardFlavorRequestOnly are defined and discussed in HIServices/Pasteboard.h. kPasteboardPromisedData marks a promised piece of data, as discussed in the previous section. kPasteboardFlavorRequestOnly is a privacy marker: the receiver of the data must explicitly request this flavor. The UTIs are unique to this application.
Note the call to DisposeHandle to dispose of the memory (of type Handle *) allocated by TXNGetDataEncoded. This is the recommended approach for memory returned by that particular function. Note that TXNGetDataEncoded has been deprecated in favor of TXNWriteRangeToCFData. See HIToolbox/MacTextEditor.h for details.
Listing 9: Copying to a Pasteboard
OSStatus AddDataToPasteboard( PasteboardRef inPasteboard, TXNObject inTXNObject,
bool addExtraTypes )
{
OSStatus err = noErr;
PasteboardSyncFlags syncFlags;
TXNOffset start, end;
Handle dataHandle;
CFDataRef textData = NULL, requestOnlyData = NULL;
// We need to clear the pasteboard of it's current contents so that this
// application can own it and add it's own data.
err = PasteboardClear( inPasteboard );
require_noerr( err, CantClearPasteboard );
// Check to see whether the pasteboard is owned by this client. This isn't
// really necessary since we just cleared it, but it shows off an added feature
// of the PasteboardSynchronize API. If we were a plug-in in a larger
// application we could find out if someone in the application has cleared
// a pasteboard we care about. If so, we would know that we could go ahead
// and add data.
syncFlags = PasteboardSynchronize( inPasteboard );
require_action( !( syncFlags&kPasteboardModified ), PasteboardNotSynchedAfterClear,
err = badPasteboardSyncErr );
require_action( ( syncFlags&kPasteboardClientIsOwner ), ClientNotPasteboardOwner,
err = notPasteboardOwnerErr );
// add a promise keeper to the pasteboard before making a promise
err = PasteboardSetPromiseKeeper( inPasteboard, PromiseKeeper, inTXNObject );
require_noerr( err, CantSetPromiseKeeper );
// get the selection from the text view and the data within it
TXNGetSelection( inTXNObject, &start, &end );
err = TXNGetDataEncoded( inTXNObject, start, end, &dataHandle, kTXNTextData );
require_noerr( err, CantGetDataFromTextObject );
// allocate data based on the size of the selection
textData = CFDataCreate( kCFAllocatorSystemDefault, (UInt8*)*dataHandle,
end-start );
DisposeHandle( dataHandle );
require_action( textData != NULL, CantCreateTextData, err = memFullErr );
// add text data to the pasteboard
err = PasteboardPutItemFlavor( inPasteboard, (PasteboardItemID)1,
CFSTR("com.apple.traditional-mac-plain-text"), textData, 0 );
require_noerr( err, CantPutTextData );
if ( addExtraTypes )
{
// add some promised data
err = PasteboardPutItemFlavor( inPasteboard, ( PasteboardItemID )1,
CFSTR( "com.apple.pasteboardpeeker.promised" ),
kPasteboardPromisedData, 0 );
require_noerr( err, CantPutPromisedData );
// add some data that is available upon explicit request only
requestOnlyData = CFDataCreate( kCFAllocatorDefault,
( UInt8 * )"Request only data", 18 );
require_action( requestOnlyData != NULL, CantCreateRequestOnlyData,
err = memFullErr );
err = PasteboardPutItemFlavor( inPasteboard, ( PasteboardItemID )1,
CFSTR( "com.apple.pasteboardpeeker.requestonly" ), requestOnlyData,
kPasteboardFlavorRequestOnly );
CFRelease( requestOnlyData );
require_noerr( err, CantPutPasteboardRequestOnly );
}
CantPutPasteboardRequestOnly:
CantCreateRequestOnlyData:
CantPutPromisedData:
CantPutTextData:
CantCreateTextData:
CantGetDataFromTextObject:
CantSetPromiseKeeper:
ClientNotPasteboardOwner:
PasteboardNotSynchedAfterClear:
CantClearPasteboard:
return err;
}
Drag and Drop
Supporting drag and drop requires assignment of callbacks for drag tracking and drop handling. Listing 10 shows this setup in main. Note that tracking occurs within the HITextView, while dropping targets the text object associated with the HITextView.
Listing 10: Setting Up Drag and Drop Handlers
err = InstallTrackingHandler( DragTrackingHandler, window, textView );
require_noerr( err, CantInstallDragTrackingHandler );
err = InstallReceiveHandler( DragReceiveHandler, window, txnObject );
require_noerr( err, CantInstallDragReceiveHandler );
The tracking handler in Listing 11 highlights the HITextView when the drag enters and unhighlights when the drag leaves. A more complete approach would be to check the drag pasteboard contents and determine if the item being dragged corresponds to one of the types supported by this application. Call GetDragPasteboard to get the PasteboardRef; see HIToolbox/Drag.h for more information.
Listing 11: Tracking a Drag
OSErr DragTrackingHandler( DragTrackingMessage inMessage, WindowRef inWindow,
void *inUserData, DragRef inDrag )
{
switch( inMessage )
{
// we accept all drags so hilight whenever any drag tracks over our window
case kDragTrackingEnterWindow:
{
DragAttributes attributes;
GetDragAttributes( inDrag, &attributes );
// only show the drag hilight if the drag has left the sender window per
// HI spec
if ( attributes & kDragHasLeftSenderWindow )
{
HIViewRef textView = ( HIViewRef )inUserData;
HIRect textFrame;
RgnHandle hiliteRgn = NewRgn();
// get the text view's frame ...
HIViewGetFrame( textView, &textFrame );
// ... and convert it into a region for ShowDragHilite
HIShapeRef textShape = HIShapeCreateWithRect( &textFrame );
HIShapeGetAsQDRgn( textShape, hiliteRgn );
CFRelease( textShape );
// add the drag hilight to the inside of the text view
ShowDragHilite( inDrag, hiliteRgn, true );
DisposeRgn( hiliteRgn );
}
}
break;
case kDragTrackingLeaveWindow: // hide the drag hilight when leaving
// the window
{
HideDragHilite( inDrag );
}
break;
}
return noErr;
}
The application event handler responds to drag events (kEventControlTrack) initiated in the Text View. After checking for a valid selection and waiting for the mouse to move, this function sets up a new pasteboard for the drag. Passing kPasteboardUniqueName, which is defined as NULL in Pasteboard.h, forces a new pasteboard to be created. Next, the selected text is added to the pasteboard, which is then associated with a DragRef. The DragRef is used to track the drag. When the user drops the selection, then the receive handler function will be called. Recall that DragReceiveHandler was installed in main as the receive handler, and will be invoked automatically.
Listing 12: Initiating a Drag
OSStatus HandleDragInitiation( EventRef inEvent, TXNObject inTXNObject )
{
OSStatus err = eventNotHandledErr;
EventRecord convertedEvent;
HIPoint eventPoint;
TXNOffset start, end, offset;
require_noerr( GetEventParameter( inEvent, kEventParamMouseLocation, typeHIPoint,
NULL, sizeof( eventPoint ), NULL, &eventPoint ), CantGetHIPoint );
require_noerr( TXNHIPointToOffset( inTXNObject, &eventPoint, &offset ),
CantGetTXNOffsetForPoint );
TXNGetSelection( inTXNObject, &start, &end );
ConvertEventRefToEventRecord( inEvent, &convertedEvent );
// create a drag if the mouse is tracking within a text selection
if ( start <= offset && offset <= end && WaitMouseMoved( convertedEvent.where ) )
{
PasteboardRef pasteboard;
DragRef drag;
Point trackPoint;
RgnHandle theRegion, insetRegion;
// Create a uniquely named pasteboard for use with the drag. Because we're
// creating the memory for the pasteboard we must release it later.
err = PasteboardCreate( kPasteboardUniqueName, &pasteboard );
require_noerr( err, CantCreatePasteboardForDrag );
// add data to the drag pasteboard using the generic mechanism
err = AddDataToPasteboard( pasteboard, inTXNObject, true );
require_noerr( err, CantAddDataToTheDragPasteboard );
// Create a new drag with an already existing pasteboard. Alternately, we
// could have used the pasteboard generated with a call to NewDrag but this
// demonstrates a little more flexibility with the new APIs.
err = NewDragWithPasteboard( pasteboard, &drag );
require_noerr( err, CantCreateDrag );
trackPoint.h = convertedEvent.where.h;
trackPoint.v = convertedEvent.where.v;
LocalToGlobal( &trackPoint );
theRegion = NewRgn();
insetRegion = NewRgn();
SetRectRgn( theRegion,
convertedEvent.where.h - 10, convertedEvent.where.v - 5,
convertedEvent.where.h + 10, convertedEvent.where.v + 5 );
CopyRgn( theRegion, insetRegion );
InsetRgn( insetRegion, 1, 1 );
DiffRgn( theRegion, insetRegion, theRegion );
DisposeRgn( insetRegion );
// perform the drag
err = TrackDrag( drag, &convertedEvent, theRegion );
require_noerr( err, CantTrackDrag );
CantTrackDrag:
DisposeRgn( theRegion );
check_noerr( DisposeDrag( drag ) );
CantCreateDrag:
CantAddDataToTheDragPasteboard:
CFRelease( pasteboard );
}
CantCreatePasteboardForDrag:
CantGetTXNOffsetForPoint:
CantGetHIPoint:
return err;
}
The drag receive handler in Listing 13 obtains the PasteboardRef and then calls the GetDataFromPasteboard function discussed earlier.
Listing 13: Handling a Drop
OSErr DragReceiveHandler( WindowRef inWindow, void *inUserData, DragRef inDrag )
{
OSStatus err = noErr;
PasteboardRef pasteboard;
TXNObject txnObject = ( TXNObject )inUserData;
// Grab the pasteboard from the drag. Releaseing of the pasteboard will be
// handled by the Drag Manager.
err = GetDragPasteboard( inDrag, &pasteboard );
require_noerr( err, CantGetDragPasteboard );
// just take the pasteboard and hand it off to the common pasteboard code
err = GetDataFromPasteboard( pasteboard, txnObject );
CantGetDragPasteboard:
return err;
}
A Pasteboard Text Translation Service
A service needs to respond to specific service events. This application includes functions that handle the three events (kEventServiceGetTypes, kEventServiceCopy, and kEventServicePaste) for a service user, and the kEventServicePerform event for a service provider. See Introduction to Application Services for more information.
The first returns a list of the OSTypes that this service handles for both copy and paste. The values are placed in the arrays passed in the respective event parameters. Here only the 'TEXT' type is supported.
Listing14: Returning the OSTypes Supported by this Service
OSStatus HandleGetTypes( EventRef inEvent, TXNObject inTXNObject )
{
OSStatus err = noErr;
TXNOffset start, end;
CFMutableArrayRef copyTypes, pasteTypes;
CFStringRef type = CreateTypeStringWithOSType( 'TEXT' );
TXNGetSelection( inTXNObject, &start, &end );
// only add copy types if text is selected
if ( start < end )
{
err = GetEventParameter( inEvent, kEventParamServiceCopyTypes,
typeCFMutableArrayRef, NULL, sizeof( copyTypes ), NULL, ©Types );
require_noerr( err, CantGetCopyTypes );
CFArrayAppendValue( copyTypes, type );
}
err = GetEventParameter( inEvent, kEventParamServicePasteTypes,
typeCFMutableArrayRef, NULL, sizeof( pasteTypes ), NULL, &pasteTypes );
require_noerr( err, CantGetPasteTypes );
CFArrayAppendValue( pasteTypes, type );
CantGetPasteTypes:
CantGetCopyTypes:
return err;
}
The service copy and paste events use the earlier functions that write to and read from the pasteboard.
Listing 15: Handling Copy and Paste Service Events
OSStatus HandleServiceCopy( EventRef inEvent, TXNObject inTXNObject )
{
OSStatus err = noErr;
PasteboardRef pasteboard;
err = GetEventParameter( inEvent, kEventParamPasteboardRef, typePasteboardRef,
NULL, sizeof( pasteboard ), NULL, &pasteboard );
require_noerr( err, CantGetServicePasteboard );
err = AddDataToPasteboard( pasteboard, inTXNObject, false );
CantGetServicePasteboard:
return err;
}
OSStatus HandleServicePaste( EventRef inEvent, TXNObject inTXNObject )
{
OSStatus err = noErr;
PasteboardRef pasteboard;
err = GetEventParameter( inEvent, kEventParamPasteboardRef, typePasteboardRef,
NULL, sizeof( pasteboard ), NULL, &pasteboard );
require_noerr( err, CantGetServicePasteboard );
err = GetDataFromPasteboard( pasteboard, inTXNObject );
CantGetServicePasteboard:
return err;
}
HandlePerformService responds to the final service event, kEventServicePerform. This event must be handled by service providers (as opposed to service users). The action to be taken is based on the serviceName extracted from the event.
The first action handled in Listing 16 simply places the existing pasteboard data in the Text View. This occurs in response to the user selecting the NSMenuItem "Pasteboard Peeker Service" in the Services menu, which passes a NSMessage value of "PeekerService". This menu item and message are defined in the project's Info.plist.
If Service > Pasteboard Peeker Service was not selected, then the request must be for a conversion to uppercase or lowercase. The two NSFilter items in the Info.plist determine which of these will occur: one maps to "UpperCaseTranslation", the other to "LowerCaseTranslation". The converted text is placed back on the pasteboard. Note the use of CFDataGetBytePtr and CFDataGetMutableBytePtr to "cast" the source and destination data references, the latter to a mutable array.
The inline comments describe the translation process a bit further.
Listing 16: Responding to Translation Requests
// HandlePerformService
//
// PasteboardPeeker provides two data translations from
// "com.apple.traditional-mac-plain-text" to
// "com.apple.pasteboardpeeker.uppercasetext" and
// "com.apple.pasteboardpeeker.lowercasetext".
//
// To provide these translations, PasteboardPeeker must be a filter service.
// This means adding a set of NSServices entries in the info.plist (NSFilter,
// NSSendTypes, NSReturnTypes, and NSSupportsData/FileTranslations) describing
// the translations provided. When a translation is requested, the filter
// service will be launched and sent a kEventServicePerform message.
// This routine handles that message.
OSStatus HandlePerformService( EventRef inEvent, TXNObject inTXNObject )
{
OSStatus err = noErr;
CFStringRef serviceName, returnType;
PasteboardRef pasteboard;
PasteboardItemID item;
CFDataRef sourceData;
CFIndex sourceSize;
CFMutableDataRef returnData = NULL;
const UInt8* sourceBytes;
UInt8* returnBytes;
// Find out what service is being requested of us. The services we support
// are defined in the applicaiton's plist.
err = GetEventParameter( inEvent, kEventParamServiceMessageName, typeCFStringRef,
NULL, sizeof( CFStringRef ), NULL, &serviceName );
require_noerr( err, CantGetServiceName );
// Get ahold of the pasteboard containing the data being sent to us. Don't
// release it as it was created for us, not by us. Later we'll clear it
// and add our translated data to it.
err = GetEventParameter( inEvent, kEventParamPasteboardRef, typePasteboardRef,
NULL, sizeof( PasteboardRef ), NULL, &pasteboard );
require_noerr( err, CantGetPasteboardRef );
// perform peeker service
if ( CFStringCompare( serviceName, CFSTR( "PeekerService" ), 0 ) ==
kCFCompareEqualTo )
{
GetDataFromPasteboard( pasteboard, inTXNObject );
}
else // handle the translation services
{
// Get ahold of the data to be translated. We only support translation of
// "com.apple.traditional-mac-plain-text" so if it doesn't exist in the
// first item we must fail.
err = PasteboardGetItemIdentifier( pasteboard, 1, &item );
require_noerr( err, CantGetItemIdentifier );
err = PasteboardCopyItemFlavorData( pasteboard, item,
CFSTR( "com.apple.traditional-mac-plain-text" ), &sourceData );
require_noerr( err, CantGetSourceData );
sourceSize = CFDataGetLength( sourceData );
// create the translation's return data storage
returnData = CFDataCreateMutable( kCFAllocatorDefault, sourceSize );
require_action( returnData != NULL, CantCreateReturnData, err = memFullErr );
sourceBytes = CFDataGetBytePtr( sourceData );
returnBytes = CFDataGetMutableBytePtr( returnData );
CFDataSetLength( returnData, sourceSize );
// perform the uppercase translation
if ( CFStringCompare( serviceName, CFSTR( "UpperCaseTranslation" ), 0 ) ==
kCFCompareEqualTo )
{
returnType = CFSTR( "com.apple.pasteboardpeeker.uppercasetext" );
for( CFIndex i = 0; i< sourcesize; i++ )
returnbytes[i] = ( UInt8 )toupper( sourceBytes[ i ] );
}
else // perform the lowercase translation
{
returnType = CFSTR( "com.apple.pasteboardpeeker.lowercasetext" );
for( CFIndex i = 0; i< sourceSize; i++ )
returnBytes[ i ] = ( UInt8 )tolower( sourceBytes[ i ] );
}
// clear the pasteboard so we can add our translated return data
err = PasteboardClear( pasteboard );
require_noerr( err, CantClearPasteboard );
// add the translated data
err = PasteboardPutItemFlavor( pasteboard, item, returnType, returnData, 0 );
require_noerr( err, CantAddTranslatedData );
CantAddTranslatedData:
CantClearPasteboard:
CFRelease( returnData );
CantCreateReturnData:
CFRelease( sourceData );
}
CantGetSourceData:
CantGetItemIdentifier:
CantGetPasteboardRef:
CantGetServiceName:
return err;
}
For More Information
Many header files document their respective APIs very thoroughly. The tricky
part can be determining which framework to look in:
Carbon.framework
HIToolbox/CarbonEvents.h contains information about the kEventClassService suite of events, and receiving and providing services.
HIToolbox/Drag.h discusses the new pasteboard support in the Drag Manager.
ApplicationServices.framework
LaunchServices/UTType.h has information on Uniform Type Identifiers and the programming interface.
HIServices/Pasteboard.h contains function, data type, and constant information.
HIServices/TranslationServices.h explains more about providing a translation service and the API for requesting translations.
Updated: 2005-04-29
|