Clipboard support in linux

6 posts / 0 new
Last post
jpo
Offline
Last seen: 16 hours 31 min ago
Joined: 20 Mar 2008 - 13:45
Clipboard support in linux

Hi Jules,

The current version of juce is lacking a good clipboard support in linux. What is currently implemented uses the CUT_BUFFER property of the root window, which is a feature deprecated since X10 according to http://www.jwz.org/doc/x-cut-and-paste.html

For example, pasting to and from firefox, openoffice, konsole, or any other "high level" application does not work. I think nowadays, xterm is the only x11 program to be still compatible with cut buffers.

As you know, the problem with X selections is that they involve message exchanges between the selection owner and the selection requester. They are a pain to understand, and a pain to write properly. The full specification is also a bit on the overbloated side.

Here is the version that I wrote, it could fit for example in a "juce_linux_Clipboard.cpp" file:

  extern Display* display;
  extern Window juce_messageWindowHandle;

  static String localClipboardContent;
  static Atom   atom_UTF8_STRING;
  static Atom   atom_CLIPBOARD;
  static Atom   atom_TARGETS;

  static void initSelectionAtoms() {
    static bool isInit = false;
    if (!isInit) {
      atom_UTF8_STRING = XInternAtom (display, "UTF8_STRING", False);
      atom_CLIPBOARD   = XInternAtom (display, "CLIPBOARD", False);
      atom_TARGETS     = XInternAtom (display, "TARGETS", False);
    }
  }

  /* read the content of a window property as either a locale-dependent string or an utf8 string 
     works only for strings shorter than 1000000 bytes 
  */
  static String juce_x11_readWindowProperty(Window window, Atom prop, 
                                            Atom fmt /* XA_STRING or UTF8_STRING */, 
                                            bool deleteAfterReading) {
    String returnData;
    uint8 *clipData;
    Atom actualType;
    int  actualFormat;
    unsigned long nitems, bytesLeft;
    if (XGetWindowProperty (display, window, prop, 
                            0L /* offset */, 1000000 /* length (max) */, False, 
                            AnyPropertyType /* format */, 
                            &actualType, &actualFormat, &nitems, &bytesLeft,
                            &clipData) == Success) {
      if (actualType == atom_UTF8_STRING && actualFormat == 8) {
        returnData = String::fromUTF8 (clipData, nitems);
      } else if (actualType == XA_STRING && actualFormat == 8) {
        returnData = String((const char*)clipData, nitems);
      }
      
      if (clipData != 0)
        XFree (clipData);
      jassert(bytesLeft == 0 || nitems == 1000000);
    }
    
    if (deleteAfterReading) {
      XDeleteProperty (display, window, prop);
    }

    return returnData;
  }

  /* send a SelectionRequest to the window owning the selection and waits for its answer (with a timeout) */
  static bool juce_x11_requestSelectionContent(String &selection_content, Atom selection, Atom requested_format) {
    Atom property_name = XInternAtom(display, "JUCE_SEL", false); 
    /* the selection owner will be asked to set the JUCE_SEL property on the juce_messageWindowHandle with the selection content */
    XConvertSelection(display, selection, requested_format, property_name, juce_messageWindowHandle, CurrentTime);
    bool gotReply = false;
    int timeoutMs = 200; // will wait at most for 200 ms
    do {
      XEvent event;
      gotReply = XCheckTypedWindowEvent(display, juce_messageWindowHandle, SelectionNotify, &event);
      if (gotReply) {
        if (event.xselection.property == property_name) {
          jassert(event.xselection.requestor == juce_messageWindowHandle); // or I didn't understand anything
          selection_content = juce_x11_readWindowProperty(event.xselection.requestor,
                                                          event.xselection.property, requested_format, true);

          return true;
        } else {
          return false; // the format we asked for was denied.. (event.xselection.property == None)
        }
      }
      /* not very elegant.. we could do a select() or something like that... however clipboard content requesting
         is inherently slow on x11, it often takes 50ms or more so... */
      Thread::sleep(4); timeoutMs -= 4;
    } while (timeoutMs > 0);
    DBG("timeout for juce_x11_requestSelectionContent");
    return false;
  }

  /* called from the event loop in juce_linux_Messaging in response to SelectionRequest events */
  void juce_x11_handleSelectionRequest(XSelectionRequestEvent &evt) {
    initSelectionAtoms();

    /* the selection content is sent to the target window as a window property */    
    XSelectionEvent reply;
    reply.type = SelectionNotify;
    reply.display = evt.display;
    reply.requestor = evt.requestor;
    reply.selection = evt.selection;
    reply.target = evt.target;
    reply.property = None; // == "fail"
    reply.time = evt.time;

    char *data = 0;
    int property_format = 0, data_nitems = 0;
    if (evt.selection == XA_PRIMARY || evt.selection == atom_CLIPBOARD) {
      if (evt.target == XA_STRING) {
        // format data according to system locale
        data = strdup((const char*)localClipboardContent);
        data_nitems = strlen(data);
        property_format = 8; // bits/item
      } else if (evt.target == atom_UTF8_STRING) {
        // translate to utf8
        data = strdup((const char*)localClipboardContent.toUTF8());
        data_nitems = strlen(data);
        property_format = 8; // bits/item
      } else if (evt.target == atom_TARGETS) {
        // another application wants to know what we are able to send
        data_nitems = 2;
        property_format = 32; // atoms are 32-bit        
        data = (char*)malloc(data_nitems * 4);
        ((Atom*)data)[0] = atom_UTF8_STRING;
        ((Atom*)data)[1] = XA_STRING;
      }
    } else {
      DBG("requested unsupported clipboard");
    }
    if (data) {
      const size_t MAX_REASONABLE_SELECTION_SIZE = 1000000;
      // for very big chunks of data, we should use the "INCR" protocol , which is a pain in the *ss
      if (evt.property != None && strlen(data) < MAX_REASONABLE_SELECTION_SIZE) {
        XChangeProperty(evt.display, evt.requestor,
                        evt.property, evt.target,
                        property_format /* 8 or 32 */, PropModeReplace,
                        (const unsigned char*)data, data_nitems);
        reply.property = evt.property; // " == success"
      }
      free(data);
    }

    XSendEvent(evt.display, evt.requestor, 0, NoEventMask,
               (XEvent *) &reply);
  }


  void SystemClipboard::copyTextToClipboard (const String& clipText) throw() {
    initSelectionAtoms();
    localClipboardContent = clipText;
    XSetSelectionOwner(display, XA_PRIMARY, juce_messageWindowHandle, CurrentTime);
    XSetSelectionOwner(display, atom_CLIPBOARD, juce_messageWindowHandle, CurrentTime);
  }

   
  const String SystemClipboard::getTextFromClipboard() throw() {
    String content; // the selection content
    initSelectionAtoms();

    /* 1) try to read from the "CLIPBOARD" selection first (the "high
       level" clipboard that is supposed to be filled by ctrl-C
       etc). When a clipboard manager is running, the content of this
       selection is preserved even when the original selection owner
       exits.

       2) and then try to read from "PRIMARY" selection (the "legacy" selection
       filled by good old x11 apps such as xterm)

       3) a third fallback could be CUT_BUFFER0 but they are obsolete since X10 !
       ( http://www.jwz.org/doc/x-cut-and-paste.html )

       There is not content negotiation here -- we just try to retrieve the selection first
       as utf8 and then as a locale-dependent string
    */
    Atom selection = XA_PRIMARY;
    Window selection_owner = None;
    if ((selection_owner = XGetSelectionOwner(display, selection)) == None) {
      selection = atom_CLIPBOARD;
      selection_owner = XGetSelectionOwner(display, selection);
    }
    
    if (selection_owner != None) {
      if (selection_owner == juce_messageWindowHandle) {
        content = localClipboardContent;
      } else {
        /* first try: we want an utf8 string */
        bool ok = juce_x11_requestSelectionContent(content, selection, atom_UTF8_STRING);
        if (!ok) {
          /* second chance, ask for a good old locale-dependent string ..*/
          ok = juce_x11_requestSelectionContent(content, selection, XA_STRING);
        }
      }
    }
    return content;
  }

The getTextFromClipboard can take some time to complete if the client owning the selection is not cooperative. I have put a timeout of 200ms on it.

The code is using the juce_messageWindowHandle as the target for SelectionRequest events sent by other X clients when the juce application owns the selection. So I also added the following code in the message loop of juce_linux_Messaging.cpp :

    } else if (evt.type == SelectionRequest && evt.xany.window == juce_messageWindowHandle) 
    {
        juce_x11_handleSelectionRequest(evt.xselectionrequest);
    } else if (evt.type == SelectionClear && evt.xany.window == juce_messageWindowHandle) 
    {   
        /* another window just grabbed the selection -- we just don't care */ 
    }
    else if (evt.xany.window != juce_messageWindowHandle)
    {
        juce_windowMessageReceive (&evt);
    }

That way it is possible to paste text to and from firefox, openoffice etc. I believe the implementation above is quite clean, and sufficiently complete -- it won't handle cut & paste of text of more than 1000000 bytes however, but who cares.. My best reference when I wrote that was the xsel source http://www.vergenet.net/~conrad/software/xsel/

I put the source above in public domain, I don't claim any copyright on it, so Jules if you want to put the standard juce header on it, reformat it according to your rules and include it in juce, feel free to do it.

jules
Offline
Last seen: 9 hours 47 min ago
Joined: 29 Apr 2013 - 18:37

Wow, excellent stuff, thanks very much! As soon as I have a moment I'll go through that and see what I can do with it!

hkt
Offline
Last seen: 1 year 8 months ago
Joined: 17 Nov 2007 - 17:53
Re: Clipboard support in linux

im using juce 1.50 amalgamated sources , while testing my new code editor (subclass of CodeEditorComponent) on linux i see that i cant paste text that I copy inside other apps. I found this note and am wondering if these changes are going to be encorporated into JUCE anytime soon??

thanks for any info,

rick

jules
Offline
Last seen: 9 hours 47 min ago
Joined: 29 Apr 2013 - 18:37
Re: Clipboard support in linux

Yes, I think that went in there a while ago. You should grab the tip and try it.

roeland
Offline
Last seen: 6 hours 16 min ago
Joined: 26 Jan 2012 - 22:31
Re: Clipboard support in linux

Hi Jules,

I was looking for support for additional data types for the clipboard, and I found a bug in the linux clipboard code. If a request for atom_TARGETS comes in, the reply has the type atom_TARGETS (the same as the requested property). This should be set to XA_ATOM, otherwise a lot of applications will treat the reply as invalid.

in juce_amalgamated from line 264184:

		else if (evt.target == ClipboardHelpers::atom_TARGETS)
		{
			// another application wants to know what we are able to send
			numDataItems = 2;
			propertyFormat = 32; // atoms are 32-bit
			data.calloc (numDataItems * 4);
			Atom* atoms = reinterpret_cast<Atom*> (data.getData());
			atoms[0] = ClipboardHelpers::atom_UTF8_STRING;
			atoms[1] = XA_STRING;

			evt.target = XA_ATOM;   // <-- call XChangeProperty with correct type
		}

--
Roeland

jules
Offline
Last seen: 9 hours 47 min ago
Joined: 29 Apr 2013 - 18:37
Re: Clipboard support in linux

Nice one Roeland, thanks for that! I'll get that sorted out right away!