Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / OpenGL

Add Clipboard Text Provider Capability to Your C# OpenGL App on X11

5.00/5 (3 votes)
18 Feb 2019CPOL7 min read 6.1K   60  
How to realize text copy functionality via inter process communication for your OpenTK based C# OpenGL application running on X11

Introduction

Clipboard functionality (to provide copy-and-paste or drag-and-drop) is much harder to realize for X11 than for Windows. This is because X11 design is much older than Windows design and had only slight extensions through its lifetime.

Clipboard functionality must be realized according to ICCM (Inter-Client Communication Conventions Manual) Chapter 2. Peer-to-Peer Communication by Means of Selections.

I have already realized copy-and-paste for my Roma Widget Set, but this is completely based on Xlib and the approach could not be transferred 1:1 to an OpenTK based C# OpenGL window.

The solution was a trick, that is often used for client applications running on X11: An unmapped window takes care of the inter-process communication. The "X11: How does “the” clipboard work?" blog post describes the basics of inter-client communication via clipboards - which are called selections in X11.

This is the first tip regarding clipboard text exchange for C# OpenGL applications running on X11. The second tip will be around Clipboard Text Consumer Capability, this one is around Clipboard Text Provider Capability.

Background

For clipboard functionality, we only need to care for events of type XEventName.SelectionNotify and XEventName.SelectionRequest. (Implementation details can be determined by reading the OpenTK source code of the X11GLNative class.)

  • The XEventName.SelectionNotify event type is evaluated by OpenTK.NativeWindow.ProcessEvents() - but only regarding the freedesktop.org extension of the ICCM for drag-and-drop. This implementation is focused to receive a list of file paths only. This is not helpful for the Clipboard Text Provider Capability to implement.
  • The XEventName.SelectionRequest event type is not evaluated by OpenTK.NativeWindow.ProcessEvents().

An OpenTK based C# OpenGL window processes all its events via OpenTK.NativeWindow.ProcessEvents(). There is no chance to register an event handler for SelectionRequest and SelectionNotify or to inject code into the ProcessEvents() method. There is also no possibility to derive from OpenTK.NativeWindow because it is an internal sealed class.

The X11GLNative class uses the Xlib methods XCheckWindowEvent() and XCheckTypedWindowEvent() to pick certain events from the event queue.

But extrapolating the X11GLNative approach - to pick the XEventName.SelectionNotify and XEventName.SelectionRequest events via XCheckWindowEvent() and/or XCheckTypedWindowEvent() methods from the event queue - before the ProcessEvents() method is called, leads to a corrupted application.

The introduction of an unmapped window, that takes care of the inter-client communication, solved the problem.

Using the Code

At first, we need an unmapped window for Inter-process/inter-client communication - I call it IpcWindow. This window is located at the Application class and created/destroyed during registration/deregistration of the Application's main window - the OpenTK native window.

From this point on, the IpcWindow instance is ready to manage the inter-client communication.

C#
/// <summary>Get or set the main window of the application.</summary>
public Window MainWindow
{   get    {    return _mainWindow; }
    internal set
    {
        if (_mainWindow != null && value != null)
            throw new InvalidOperationException (
                "Unable to change main window after first initialization.");

        _mainWindow = value;
        if (_mainWindow != null)
        {
            var attributeMask     = X11.XWindowAttributeMask.CWEventMask;
            var attributes        = new X11.XSetWindowAttributes();
            attributes.event_mask = (X11.TLong)X11.EventMask.PropertyChangeMask;
            IntPtr handle         = X11.X11lib.XCreateWindow(_mainWindow.Display,
                                                             _mainWindow.RootHandle,
                                                             (X11.TInt)10, (X11.TInt)10,
                                                             (X11.TUint)10, (X11.TUint)10,
                                                             (X11.TUint)0,
                                                             /* CopyFromParent */ (X11.TInt)0,
                                                             X11.XWindowClass.CopyFromParent,
                                                             /* CopyFromParent */ IntPtr.Zero,
                                                             attributeMask, ref attributes);
            if (_ipcWindow == null)
                _ipcWindow = new IpcWindow(_mainWindow.Display, _mainWindow.Screen,
                                           _mainWindow.RootHandle, handle);
        }
        else
        {
            if (_ipcWindow != null)
            {
                if (_ipcWindow.Display != IntPtr.Zero && _ipcWindow.Handle != IntPtr.Zero)
                    X11.X11lib.XDestroyWindow (_ipcWindow.Display, _ipcWindow.Handle);

                _ipcWindow = null;
            }
        }
    }
}

The IpcWindow class has a very small implementation and adds only some atoms. All atoms are initialized by the constructor.

UPDATE:

  • The IpcWindow class source code is now provided for download at the top of the tip.
  • I've introduced two non-standard atoms: OTK_TARGETS_PROP and OTK_DATA_PROP to identify the windows properties, that are used for inter-client communication. (The communication between X11 windows - specifically betwen clipboard owner and clipboard requestor - uses window properties.)
  • The OTK_TARGETS_PROP atom cares for communication concerning supported clipboard formats.
  • The OTK_DATA_PROP atom cares for clipboard data transfer.
  • I've made the experience that negotiation of supported clipboard formats and clipboard data transfer need two separate windows properties.
  • Beside the windows properties the inter-client communication needs to signal the type of data to interchange. This is where the atoms XA_TARGETS, XA_TIMESTAMP, XA_UTF8_STRING and XA_STRING come into play.

This is the initialization of IpcWindow's atoms:

C#
/// <summary>Initialize a new instance of the <see cref="OpenFW.Platform.X11.IpcWindow"/>
/// class with display, screen and rootHandle.</summary>
/// <param name="display">Display.</param>
/// <param name="screen">Screen.</param>
/// <param name="rootHandle">Root handle.</param>
public IpcWindow (IntPtr display, int screen, IntPtr rootHandle, IntPtr handle)
{
    if (display == IntPtr.Zero)
        throw new ArgumentNullException("display");
    if (screen < 0)
        throw new ArgumentNullException("screen");
    if (rootHandle == IntPtr.Zero)
        throw new ArgumentNullException("rootHandle");
    if (handle == IntPtr.Zero)
        throw new ArgumentNullException("handle");

    Display        = display;
    Screen         = screen;
    RootHandle     = rootHandle;
    Handle         = handle;

    WM_GENERIC       = X11lib.XInternAtom(display, "WM_GENERIC_CLIENT_MESSAGE", false);
    XA_CLIPBOARD     = X11lib.XInternAtom(display, "CLIPBOARD", false);
    XA_PRIMARY       = X11lib.XInternAtom(display, "PRIMARY", false);
    XA_SECONDARY     = X11lib.XInternAtom(display, "SECONDARY", false);
    XA_TARGETS       = X11lib.XInternAtom(display, "TARGETS", false);
    XA_TIMESTAMP     = X11lib.XInternAtom(display, "TIMESTAMP", false);
    XA_UTF8_STRING   = X11lib.XInternAtom(display, "UTF8_STRING", false);
    XA_STRING        = X11lib.XInternAtom(display, "STRING", false);

    OTK_TARGETS_PROP = X11lib.XInternAtom(display, "OTK_TARGETS_PROP", false);
    OTK_DATA_PROP    = X11lib.XInternAtom(display, "OTK_DATAP_ROP", false);
}

Second, we need to evaluate the XEventName.SelectionRequest event for the IpcWindow. I realized it directly before the ProcessEvents() method is called for the Application's main window - the OpenTK native window.

C#
/// <summary>Processes one GL window message.</summary>
/// <returns>Returns <c>true</c>, if display has been validated, or <c>false</c> 
/// otherwise.</returns>
/// <remarks>While a typical X11 message loop (utilizing XNextEvent()) blocks until
/// the next event arrives (and saves CPU power), a typical GL message loop
/// (utilizing ProcessEvents()) doesn't block (and wastes CPU power).</remarks>
public bool ProcessMessage ()
{
    if (!_glWindow.Exists)
        return false;

    // Can be called from child windows (with own GL content) as well.
    // Thus we have to ensure the right GL context.
    if (!_context.IsCurrent)
    {
        // ConsoleMessageSink.WriteInfo (null, typeof(WindowBase).Name +
        //                               "::ProcessMessage() Reactivating GL context={0} ...",
        //                               _context.GetHashCode());
        _context.MakeCurrent(_glWindow.WindowInfo);
    }

    IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
    if (ipcWindow != null)
    {
        X11.XEvent e = new X11.XEvent();
        //using (new XLock(ipcWindow.Display))
        {
            if (X11.X11lib.XCheckTypedWindowEvent(ipcWindow.Display, ipcWindow.Handle,
                           X11.XEventName.SelectionNotify,  ref e) != (X11.TBoolean)0 ||
                X11.X11lib.XCheckTypedWindowEvent(ipcWindow.Display, ipcWindow.Handle,
                           X11.XEventName.SelectionRequest, ref e) != (X11.TBoolean)0    )
            {
                if (e.SelectionEvent.type      == X11.XEventName.SelectionRequest)
                {
                    X11.XSelectionRequestEvent selectionEventIn = e.SelectionRequestEvent;

                    X11.XEvent selectionEventOut = new X11.XEvent();
                    selectionEventOut.SelectionEvent.type        = 
                                                      X11.XEventName.SelectionNotify;
                    selectionEventOut.SelectionEvent.display     = selectionEventIn.display;
                    selectionEventOut.SelectionEvent.requestor   = selectionEventIn.requestor;
                    selectionEventOut.SelectionEvent.selection   = selectionEventIn.selection;
                    selectionEventOut.SelectionEvent.target      = selectionEventIn.target;
                    selectionEventOut.SelectionEvent.property    = selectionEventIn.property;
                    selectionEventOut.SelectionEvent.time        = selectionEventIn.time;

                    if (selectionEventIn.target    == ipcWindow.XA_TARGETS)
                    {
                        ConsoleMessageSink.WriteInfo 
                              (null, "SelectionRequest: XA_TARGETS", null);
                        // DnD or Cut/Copy and Paste - SUPPORT:
                        // Provide available clipboard formats.
                        // For DnD see also: 
                        // https://github.com/dfelinto/blender/tree/master/extern/xdnd
                        // and https://www.uninformativ.de/blog/postings/
                        // 2017-04-02/0/POSTING-de.html

                        IntPtr data = Marshal.AllocHGlobal (3 * IntPtr.Size);
                        Marshal.WriteIntPtr (data, ipcWindow.XA_TARGETS);
                        Marshal.WriteIntPtr (data, IntPtr.Size, ipcWindow.XA_STRING);
                        Marshal.WriteIntPtr (data, IntPtr.Size * 2, ipcWindow.XA_UTF8_STRING);
                        X11.X11lib.XChangeProperty(selectionEventIn.display,
                                                   selectionEventIn.requestor,
                                                   selectionEventIn.property, 
                                                    X11.XAtoms.XA_ATOM,
                                                   (X11.TInt)32,
                                                   (X11.TInt)X11.XChangePropertyMode.
                                                                 PropModeReplace,
                                                   data, (X11.TInt)2);
                        Marshal.FreeHGlobal(data);
                    }
                    else if (selectionEventIn.target    == ipcWindow.XA_TIMESTAMP)
                    {
                        ConsoleMessageSink.WriteInfo 
                            (null, "SelectionRequest: XA_TIMESTAMP", null);
                        ConsoleMessageSink.WriteInfo(null, this.GetType ().Name +
                            "::ProcessMessage () Provide requested format '{0}', " +
                            "that is acquired by a 'SelectionRequest' event.", "TIMESTAMP");
            
                        int datasize = sizeof (X11.TUlong);
                        IntPtr data = Marshal.AllocHGlobal (datasize);
                        if (datasize == 4)
                            Marshal.WriteInt32 (data, (int)X11.X11lib.CurrentTime);
                        else // (datasize == 8)
                            Marshal.WriteInt64 (data, (long)X11.X11lib.CurrentTime);
                        X11.X11lib.XChangeProperty (selectionEventIn.display,
                                                    selectionEventIn.requestor,
                                                    selectionEventIn.property, 
                                                    selectionEventIn.target,
                                                    (X11.TInt)32,
                                                    (X11.TInt)X11.
                                                    XChangePropertyMode.PropModeReplace,
                                                    data, (X11.TInt)(datasize == 4 ? 1 : 2));
                        Marshal.FreeHGlobal(data);
                    }
                    else if (selectionEventIn.target    == ipcWindow.XA_UTF8_STRING)
                    {
                        ConsoleMessageSink.WriteInfo (null, this.GetType ().Name +
                            "::ProcessMessage () Provide requested format '{0}', " +
                            "that is acquired by a 'SelectionRequest' event.", "UTF8_STRING");

                        byte[] s = System.Text.Encoding.UTF8.GetBytes(Clipboard.Text);
                            
                        IntPtr data = Marshal.AllocHGlobal(Marshal.SizeOf
                                      (typeof(byte)) * s.Length);
                        Marshal.Copy(s, 0, data, s.Length);
                        X11.X11lib.XChangeProperty(selectionEventIn.display,
                                                   selectionEventIn.requestor,
                                                   selectionEventIn.property, 
                                                   ipcWindow.XA_UTF8_STRING,
                                                   (X11.TInt)8,
                                                   (X11.TInt)X11.XChangePropertyMode.
                                                   PropModeReplace,
                                                   data, (X11.TInt)s.Length);
                        Marshal.FreeHGlobal(data);
                    }
                    else if (selectionEventIn.target    == ipcWindow.XA_STRING)
                    {
                        ConsoleMessageSink.WriteInfo (null, this.GetType ().Name +
                            "::ProcessMessage () Provide requested format '{0}', " +
                            "that is acquired by a 'SelectionRequest' event.", "STRING");
            
                        string s = Clipboard.Text;
                            
                        IntPtr data = Marshal.StringToHGlobalAuto (s);
                        X11.X11lib.XChangeProperty(selectionEventIn.display,
                                                   selectionEventIn.requestor,
                                                   selectionEventIn.property, 
                                                   ipcWindow.XA_STRING,
                                                   (X11.TInt)8,
                                                   (X11.TInt)X11.XChangePropertyMode.
                                                   PropModeReplace,
                                                   data, (X11.TInt)s.Length);
                        Marshal.FreeHGlobal(data);
                    }
                    else
                    {
                        IntPtr pFormatName = X11.X11lib.XGetAtomName (selectionEventIn.display,
                                                                      selectionEventIn.target);
                        string sFormatName = Marshal.PtrToStringAuto (pFormatName);
                        ConsoleMessageSink.WriteWarning (null, this.GetType ().Name +
                            "::ProcessMessage () Unhandled requested format '{0}', " +
                            "that is acquired by a 'SelectionRequest' event.", sFormatName);
                    }
                    X11.X11lib.XSendEvent (selectionEventIn.display, selectionEventIn.requestor,
                                          (X11.TBoolean)1, (X11.TLong)X11.EventMask.NoEventMask,
                                          ref selectionEventOut);
                }
                else if (e.SelectionEvent.type == X11.XEventName.SelectionNotify)
                {

                    ...

                }
            }
        }
    }

    // ProcessEvents() source code:
    // https://github.com/opentk/opentk/blob/master/src/OpenTK/Platform/X11/X11GLNative.cs
    _glWindow.ProcessEvents ();
    // Calls: implementation.ProcessEvents (): Just delegates complete 
    // processing to implementation.
        // Calls LinuxNativeWindow.ProcessEvents (): Just calls ProcessKeyboard() 
        // and ProcessMouse().
            // Calls NativeWindowBase.ProcessEvents (): Just clears keyboard,
            // to prevent confusion on missing KeyUp.

    ...

}

The standard text interchange format is XA_UTF8_STRING, as defined in the freedesktop.org extension of the ICCM for drag-and-drop.

UPDATE

  • The WindowBase class source code, that contains the ProcessMessage() method, is now provided for download at the top of the tip.

How It Works

Assumed, we have selected some text in our OpenTK based C# OpenGL window application and provide the text via "Edit" | "Copy" menu or [Ctrl]+[c] keys, our application registers itself as a selection owner for copy-and-paste content by calling XSetSelectionOwner() - which enables other applications to ask for the copy-and-paste content.

Additionally assumed, we have any text editor running and request clipboard text via "Edit" | "Paste" menu or [Ctrl]+[v] keys, it will behave as a selection requestor and send an XEventName.SelectionRequest event to inform the selection owner about its need and to ask for the supported XA_TARGETS.

  • Every XEventName.SelectionRequest event, sent by the selection requestor, indicates a window property, that defines an anchor point shared by selection requestor and selection owner to realize data interchange. Consequentially, the selection owner is instructed to use the indicated window property for it's answer.
  • Typically the XA_CLIPBOARD atom is used to identify the window property for data interchange. Rarely used alternatives are the XA_PRIMARY and XA_SECONDARY atoms.
  • The XA_TARGETS atom is used by the selection requestor to request which data formats are supported by the selection owner.

The selection owner will receive the XEventName.SelectionRequest event asking for supported XA_TARGETS to announce them to the indicated window property. The selection owner should announce its supported XA_TARGETS to the indicated window property via XChangeProperty() and send an XEventName.SelectionNotify event to inform the selection requestor about its answer.

Now the selection requestor can evaluate the supported XA_TARGETS, pick a suitable format - the standard text interchange format is XA_UTF8_STRING as defined in the freedesktop.org extension of the ICCM for drag-and-drop - and ask the selection owner for the copy-and-paste content formatted as an XA_UTF8_STRING, sending an XEventName.SelectionRequest event.

The selection owner will receive the XEventName.SelectionRequest event asking for the copy-and-paste content formatted as an XA_UTF8_STRING to announce it to the indicated window property. The selection owner should announce copy-and-paste content to the indicated window property via XChangeProperty() and send a XEventName.SelectionNotify event to inform the selection requestor about its answer.

Finally, the selection requestor can pick up the copy-and-paste content from the indicated window property and paste the text.

This procedure needs the processing of a series of XEventName.SelectionRequest events at selection owner side and XEventName.SelectionNotify events at selection requestor side. This means, at selection owner side, we have a series of asynchronous processing steps - triggered by XEventName.SelectionRequest events. Or in other words: Implementing clipboard functionality is not straightforward but is reaction to events.

Third, we need to provide the copy-and-paste text / need to register our OpenTK based C# OpenGL window application as a clipboard text provider. My application uses the [Ctrl]+[c] key combination to initiate that.

C#
...

else if (e.Key == (uint)OpenTK.Input.Key.C)
{
    int anchorOffset = _selection.AnchorPosition.GetSymbolOffset();
    int movingOffset = _selection.MovingPosition.GetSymbolOffset();
    int selLength    = -(movingOffset - anchorOffset);

    // No selected range.
    if (selLength == 0)
        return;

    if (e.KeyboardDevice == null)
        return;
    if (!e.KeyboardDevice.IsKeyDown(System.Windows.Input.Key.LeftCtrl) &&
        !e.KeyboardDevice.IsKeyDown(System.Windows.Input.Key.RightCtrl))
        return;

    string content;
    if (selLength < 0)
        content = (this.TextContainer as TextContainer).TextInternal
                                      (movingOffset+selLength, -selLength);
    else
        content = (this.TextContainer as TextContainer).TextInternal(movingOffset, selLength);

    Clipboard.ProvideText(content, X11.X11lib.CurrentTime);
}

...

I've created a static Clipboard class to implement some re-usable copy-and-paste inter-client communication helper methods. The methods, that are relevant for the selection owner, are:

C#
public static class Clipboard
{
    #region Static Properties

    /// <summary>Get or set the last registered clipboard text.</summary>
    /// <value>The last registered clipboard text.</value>
    public static string Text    {    get;    private set;    }

    #endregion Static Properties

    #region Static Methods

    /// <summary>Register as data provider for clipboard interaction.</summary>
    /// <param name="text">The <see cref="Object"/> text to provide to the clipboard.</param>
    /// <param name="selection">The <see cref="IntPtr"/> selection atom, that defines the
    /// selection buffer to use. Predefined are CLIPBOARD, PRIMARY and SECONDARY.</param>
    /// <param name="time">The <see cref="TTime"/> time the event 
    /// (typical mouse or button) occurs,
    /// that triggers this clipboard interaction.</param>
    /// <returns>Returns <see cref="Boolean"/> <c>true</c> on success, or <c>false</c>
    /// otherwise.</returns>
    public static bool ProvideText (string text, IntPtr selection, X11.TTime time)
    {
        if (string.IsNullOrEmpty (text))
            return false;

        IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
        if (ipcWindow == null)
            return false;
        
        if (selection == IntPtr.Zero)
            selection = ipcWindow.XA_CLIPBOARD;

        return X11Clipboard.ProvideClipboardText (ipcWindow.Display,
                                                  ipcWindow.Handle,
                                                  selection,
                                                  time, text);
    }


    ...

}

UPDATE

  • The Clipboard class source code is now provided for download at the top of the tip.
  • The X11Clipboard class source code is part of the downloadable X11Wrapper assembly project.

Additional Prerequisites

All Xlib function calls, structures, data types and constants, that are used for native code calls (and a lot more), are prototyped or defined by the X11Wrapper_V1.1_Preview project - a spin-off of my Roma Widget Set. I've added the complete library project (including source code) for download.

To include and compile the library project for your own solution, you must specify the target platform via compiler symbol. Use x86 for 32 bit platform and x64 for 64 bit platform builds.

Image 1

Limitations

This tip provides the solution on the selection owner side only.

See my tip Clipboard Text Consumer Capability for the solution on the selection requestor side.

It also ignores the additional effort for the case in which a big copy-and-paste content has to be transferred in multiple pieces because the indicated window property has a limited capacity.

This tip is compiled and tested with OpenTK version 1.1.0 from my operation system's installation package - only a little adoption of the GL.BlendFunc() parameters is required to enable compilation with OpenTK version 3.0.1 from NuGet.

History

  • 14th February, 2019: Initial version
  • 18th February, 2019: First update

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)