Monday, August 1, 2011

Coding Copy and Paste Functionality

Copy and Paste Features
A well designed application should probably support Copy and Paste. While it sounds simple, it actually entails a few features.

Copying from Your Application to Other Applications
This means that your application should offer up data formats that other applications can read. So a user should be able to copy in your application, and paste into another one. This may entail converting your internal data format into a new one that other programs will expect.

Copying from Other Applications into Your Application
This means reading other applications data formats and converting it your applications internal representation.

Copying and Pasting within Your Application or between Instances of Your Application
This entails marshaling all the data that your application needs. If a user can run multiple instances of your Application, it won't be sufficient to simply copy an instance of an object, since one instance won't have access to what is in the memory of another instances.

How Does Copy and Paste Work?
You start out by accessing the desktop's clipboard. Then, you tell that clipboard what kind of data your app can put on the clipboard, and then what kind of data your app can take off of the clipboard. Then, you right some functions to handle the following situations:

If the user selects a Copy command in your application, you tell the clipboard that data is available (but you don't actually put the data on the clipboard until the user selects paste).
If the user selects Paste in your application, you ask the clipboard for the data, and the application with the data then supplies it to the clipboard. Sometimes the application supplying the data is your application, and sometimes it is a different application.
If the user selects Paste in a different application, then the clipboard might ask your application to supply the data.

Set Up Variables
So, let's get to the code. First thing, we need to create a reference to desktop's clipboard, and also create a variable to store a reference to whatever item may be copied.
     self.clipboard = gtk.Clipboard()
self.clipboard_item = None
I put this in finish_initializing.

Handling the Copy Command
Then I wrote a function called "copy_menu" which is called when the user selects "Copy" fromt he menu. the first thing this function does is tells the clipboard what kinds of data is supported by passing in a list of tuples. Each tuple has a string that specifies the type, info regarding drag and drop, and a number that you can make up to specify what the type again. For Photobomb, only the strings are important. You need to use types that are commonly understood on the Linux desktop so that apps know that you can give them data they care about. I chose "UTF8_STRING" for putting strings on the clipboard, and the mime type "image/png" for images. For example, Gedit recongizes UTF8_STRING, and Gimp recognizes image/png, so Photobomb can copy data into both of those apps. Only Photobomb recognizes "PhotobombItem", of course. These strings are called "targets" in Gtk.

After specifying what data types you will support, you then tell the clipboard about those data types, along with what function to call if the user tries to paste, and what function to call when the clipboard gets cleared. Finally, the function stores a reference to the currently selected item. It's important to store this reference seperately, as the selection could change before the user pastes.
   def copy_menu(self, widget, data=None):
paste_data = [("UTF8_STRING", 0, 0),
("image/png", 0, 1),
("PhotobombItem", 0, 2)]
self.clipboard.set_with_data(paste_data, self.format_for_clipboard, clipboard_cleared)
self.clipboard_item = self.selected_item
Handling Programs Pasting from Photobomb
After telling the clipboard what kind of data you can put on it, the user may try to actually Paste! If this happens, you have to figure out what kind of data they are asking for, and then put the correct kind of data onto the clipboard. To figure out what is being asked for, you check the target. This info is stored in the selectiondata argument, that gets passed in.

Putting Text on the Clipboard
When an application is asking for a string, life is really simple. Every PhotobombItem has a text property, so it's simple to simply set the text of the clipboard with this data.
   def format_for_clipboard(self, clipboard, selectiondata, info, data=None):
if selectiondata.get_target() == "UTF8_STRING":
self.clipboard.set_text(self.clipboard_item.text)
Putting an Image on the Clipboard
But what about when the app has asked to paste an image? The clipboard only supports text, so you can't paste an image, right? Actually, the contents of any binary stream or file can be converted to text, and that text can be put on the clipboard. However, you don't want to simply set the text for the clipboard, as binary data can have characters in it that will confuse a program that thinks it's a normal string. For example, when you read in a file of binary data, you can set that to a string in Python, but other programs not written in Python will probably get have errors if they try to treat it as a string type.

In fact, the clipboard will reject text that is binary data. So, if you have binary data, you need to:
  1. convert it into text
  2. set the selectiondata with the proper target
Photobomb has a function called "image_for_item" that returns image data as binary text, so Phothobomb can just call that function and put the resulting text into the selection data. The "8" in the second parameter tells the selectiondata object essentially boils down to text.
     if selectiondata.get_target() == "image/png":
selectiondata.set("image/png", 8, self.image_for_item(self.clipboard_item))

Putting a PhotobombItem on the Clipboard
If an instance of Photobomb wants to paste, we need to provide enough information to create a proper PhotobombItem. A PhotobombItem knows a lot about itself, for example, it's clipping path, it's rotation, it's scale, etc... When copyng and pasting within Photobbomb or between instances of Photobomb, we don't want to lose all of that data.

So how do we paste a PhotobombItem if we can only put text on the clipboard? Python has built in support for turning objects into strings. This is called "pickling". So, in the simplest case, we could just convert the Python object to a string, put that string onto the clipboard, and then when convert it from the string back to a Python object.

So, first, import the pickle module:
import pickle

Then you can use "dumps" to dump the object to string:
   if selectiondata.get_target() == "PhotobombItem":
s = pickle.dumps(self.clipboard_item)
self.clipboard.set_text(s)
Sadly, this doesn't work for Photobomb because the GooCanvas classes that PhotobombItems derive from don't support pickling. So we can't do this in the simple way. However, what we *can* do, is create a dictionary with enough information to create identical PhotobombItems, and then pickle that dictionary. For photobomb, this gets a bit annoying complex and is not relevant to the blog posting, so I snipped out most of the code. You can always look at the full code on launchpad, of course.

This is, unfortunately, a lot more involved. However, it works just fine:
   if selectiondata.get_target() == "PhotobombItem":
d = {"type":type(self.clipboard_item)}
d["clip"] = self.clipboard_item._clip_path
#snipped out all the other properties that need to be addeed to the dicitonary
s = pickle.dumps(d)
self.clipboard.set_text(s)
You don't have to use pickling. For example, you could represent this dictionary in JSON instead. However, pickling is the native serialization formatting for Python, so it's easiest to use it when you can.

Now Photobomb can support copying and pasting into text editors, like Gedit, and image editors, such as the Gimp, as well as into Photobomb itself.

Handling Pasting in Photobomb
The essential paste function is run when the Photobomb user uses Photobomb's Paste command. This function tries to figure out if there are any interesting data types on the clipboard, and then uses them. Typically, such a function should ask for the richest and most intersting data it can handle first, and then less rich data types in order. For Photobomb, the richest data is a PhotobombItem, or course. Then an image, and then finally text. You may have noticed above that when a user pastes from a clipboard, it runs a function in some application. There is no gauruntee that these functions will be fast. For this reason, it's best to use the asyncronous "request" methods on the clipboard.

So, the paste function simply tries to figure out which is the best data type to paste, if any, and then asks the clipboard to call the correct function when the data is ready.

 def paste_menu(self, widget, data=None):
targets = self.clipboard.wait_for_targets()
if "PhotobombItem" in targets:
self.clipboard.request_contents("PhotobombItem", self.paste_photobomb_item)
return
for t in targets:
if t.lower().find("image") > -1:
self.clipboard.request_image(self.paste_image)
return
for t in targets:
if t.lower().find("string") > -1 or t.lower().find("text") > -1:
self.clipboard.request_text(self.paste_text)
return

In the cases where another applications has put an image or text onto the clipboard, it's easy to use functions already built into Photobomb to paste those data types. Notice that the return functions already have all the data ready in the form of text or the pixbuf.

   def paste_text(self, clipboard, text, data=None):
self.add_new_text(text)
def paste_image(self, clipboard, pixbuf, data=None):
self.add_image_from_pixbuf(self, pixbuf)
When an instance of Photobomb has put the data on the clipboard, it's a bit more complex, because Photobomb has to de-serialize the dictionary, and then create the proper item. Again, I snipped out most of the code for using the de-serialized dictionary, because it just unnecessarily complicated the sample:

   def paste_photobomb_item(self, clipboard, selectiondata, data=None):
item_dict = pickle.loads(clipboard.wait_for_text())
if item_dict["type"] is PhotobombPath:
new_item = PhotobombPath(self.__goo_canvas,
item_dict["path"],
item_dict["width"], None)
#snip out all the code to handle all of the other properties
new_item.set_clip_path(item_dict["clip"])
new_item.translate(10,10)
new_item.set_property("parent", self.__root)
self.z_order.append(new_item)
self.add_undo(new_item, None)
Persisting the Clipboard Item
You may have noticed that an item isn't actually put on the clipboard until the user tries to paste. This is a great optimization, because pasting complex or big things can take a lot of time, and there is no point on the user waiting for a copy command to finish if they are never going to actually paste with it. However, what if the user selects "Copy" in your app, and then quits your app? How is the item going to get pasted?

Gtk handles this by letting you "store" data on the clipboard. This can actually be used at any time in your application, but to handle this particular situation, Photobomb just includes a few lines in it's on_destroy function. This causes the desktop's clipboard to store all of the data types, so all the functions for servicing a paste command get called, and the desktop's clipboard holds onto the data as long as it needs.

   def on_destroy(self, widget, data=None):
"""on_destroy - called when the PhotobombWindow is close. """
#clean up code for saving application state should be added here
paste_data = [("UTF8_STRING", 0, 0),
("image/png", 0, 1),
("PhotobombItem", 0, 2)]
self.clipboard.set_can_store(paste_data)
self.clipboard.store()
gtk.main_quit()

1 comment:

  1. Could you fix up the code snippets, please? It's really, really hard to read Python code with inconsistent indenting :-(

    ReplyDelete