Introduction To The Clipboard and Drag & Drop
- Feature Owner
- Mike Pinkerton
Overview
As far as Mozilla is concerned, transfering data between applications via the clipboard and drag and drop are virtually identical, even though the OS-specific mechanisms under the hood are very different. The goal is to provide a uniform API for inter- and intra-application communication regardless of the transport mechanism used. This document covers that API, and subsequent documents describe the various services in detail.
The meat of this API is nsITransferable, an interface to an object that contains various representations, or "flavors," of a piece of data. Each transferable holds only one item and its various flavors.
- Data Flavors
- Data Objects
- Converters
- Using The Transferable
Data Flavors
Each piece of data, such as a hunk of text or a single bookmark item in a tree, can (and should) have multiple representations of the data of varying (usually decreasing) fidelity. This allows other applications to still be able to process the data even if they don't understand the richest representation used by mozilla. For example, the hunk of text might have html styled text, the stripped-down plain text representation, and possibly a gif illustrating the text. Mozilla knows what to do with the styled html, but SimpleText, for example, does not, yet we still want to be able to communicate with SimpleText.
Each representation of the data is referred to as a "flavor." When referring to data in the transferable, you need to know the flavor. When you can accept a variety of flavors, there are mechanisms for asking for the best one.
Even though there is a flavor "text/plain" to represent plain ASCII text, clients of the Clipboard and the Drag and Drop Services should not use it -- use "text/unicode" instead. Conversion to and from "text/plain" is performed automatically within these services. Assume all your data is double-byte and life will be pure and good. Please fix any Clipboard/D&D code that uses "text/plain" directly ASAP, as it is not guaranteed to work in the future.
Data Objects
In order to work with XPConnect and allow writing clipboard/drag&drop code in JavaScript, the actual must be wrapped in typed objects. The two most common of these objects are nsISupportsString and nsISupportsWString for one byte and two byte strings, respectively.
These "wrapper" objects must be created using the component manager and the actual data to transfer must be placed within them before passing the data to the Transferable object. Here's a code snipped in JavaScript to do this.
// create the wrapper and QI it to the right interface var wrapper = Components.classes["component://netscape/supports-string"].createInstance(Components.interfaces.nsISupportsString); if ( wrapper ) { // ... get the data, place it in |id| wrapper.data = id; }
Converters
Talk about data converters
Using The Transferable
Placing Data Into The Transferable
The basic usage pattern for adding data to the transferable is as follows:
- Create the Transferable object
- Register the approprate data flavors
- Create the data objects for each flavor (as above)
- Add the data objects to the transferable
- Pass the transferable to a service that will use the data, such as the clipboard or drag&drop services
Note that the flavor must be registered with the transferable before data of that type can be added. Why? That seems quite stupid. Shouldn't it just add it if it can't find it? Also note that the length parameter of setTransferData() is in bytes, not characters, so for double-byte strings, you need to make sure you do the math correctly.
// 1. create the transferable var trans = Components.classes["component://netscape/widget/transferable"].createInstance(Components.interfaces.nsITransferable); if ( trans ) { // 2. register the data flavors trans.addDataFlavor("text/html"); trans.addDataFlavor("text/unicode"); // 3. create the data objects var textWrapper = Components.classes["component://netscape/supports-wstring"].createInstance(Components.interfaces.nsISupportsWString); var htmlWrapper = Components.classes["component://netscape/supports-wstring"].createInstance(Components.interfaces.nsISupportsWString); if ( textWrapper && htmlWrapper ) { // get the data textWrapper.data = plainTextRepresentation; htmlWrapper.data = htmlRepresentation; // 4. add data objects to transferable trans.setTransferData ( "text/html", htmlWrapper, id.length*2 ); // double byte data (len*2) trans.setTransferData ( "text/unicode", textWrapper, id.length ); // double byte data (len*2) } }
Retrieving Data From The Transferable
There are two basic cases from retrieving data from the transferable: when you know exactly what you want and when you have a list of several flavors you support and you want the best one available (the more common case, probably). In either case, the basic steps are similiar:
- Create the Transferable object
- Register the approprate data flavors (see below)
- Pass the transferable a service that will fill in the data, such as the clipboard or drag and drop service.
- Ask the transferable for the data (see below)
The only tricky part (which only applies to JavaScript)
is that since the objects returned by
get[Any]TransferData()
are out parameters, you must create new JS objects to hold
the result and the length. After these objects are filled
in, you can get at the actual out parameter values by
accessing the value
member of
these objects.
When you know exactly what you're looking for
This is the simple case or the case where you only care
about one flavor. For this, use
getTransferData()
.
// 1. create the transferable var trans = Components.classes["component://netscape/widget/transferable"].createInstance(Components.interfaces.nsITransferable); if ( trans ) { // 2. register the data flavor you want trans.addDataFlavor("text/unicode"); // 3. ...pass transferable to clipboard, etc... // 4. ask transferable for the data. Need to create new JS // objects for the out params. var dataObj = new Object(); var len = new Object(); trans.getTransferData ( "text/unicode", dataObj, len ); if ( dataObj ) dataObj = dataObj.value.QueryInterface(Components.interfaces.nsISupportsWString); if ( dataObj ) { // do something with the data var id = dataObj.data; } }
When you want the best available
The most common case is where a client supports a variety
of flavors (mozilla flavors plus some from other
applications), but certainly has a preference about which
flavors it wants have if they are present. For this, use
getAnyTransferData()
.
In order for
getAnyTransferData()
to work
correctly, the order in which you register them is very
important. You must register the flavors you are
interested in from most interested to least interested
(usually highest fidelity to lowest, but not necessarily).
Haphazardly registering flavors in random order will cause
you to not get the flavor you are expecting.
// 1. create the transferable var trans = Components.classes["component://netscape/widget/transferable"].createInstance(Components.interfaces.nsITransferable); if ( trans ) { // 2. register the data flavors you want, highest fidelity first! trans.addDataFlavor("text/html"); trans.addDataFlavor("text/unicode"); // 3. ...pass transferable to clipboard, etc... // 4. ask transferable for the best flavor. Need to create new JS // objects for the out params. var dataObj = new Object(); var bestFlavor = new Object(); var len = new Object(); trans.getAnyTransferData ( bestFlavor, dataObj, len ); if ( bestFlavor.value == "text/html" || bestFlavor.value == "text/unicode" ) { if ( dataObj ) dataObj = dataObj.value.QueryInterface(Components.interfaces.nsISupportsWString); if ( dataObj ) { // ...do something with the data. remember len is in bytes, not chars var id = dataObj.data.substring(0, len.value / 2); } } }