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

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

0.00/5 (No votes)
18 Feb 2019CPOL7 min read 4K   57  
How to realize text past 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 second tip regarding clipboard text exchange for C# OpenGL applications running on X11. The first tip was around Clipboard Text Provider Capability, now it's around Clipboard Text Consumer 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. The IpcWindow class source code is 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 seperate 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)
                {
                
                    ...
                    
                }
                else if (e.SelectionEvent.type == X11.XEventName.SelectionNotify)
                {
                    X11.XSelectionEvent selectionEventIn = e.SelectionEvent;

                    // Define the window property alias (atom), 
                    // used to handle (requestor <--> owner)
                    // data transfer regarding supported IPC (clipboard) formats.
                    // QT uses: "_QT_SELECTION"
                    // GDK uses: "GDK_SELECTION"
                    // Since the window property alias (atom) is returned unchanged from
                    // X11lib.XSendEvent() after X11lib.XChangeProperty() has overridden the
                    // window property value (with the supported IPC (clipboard) formats),
                    // the exact value is not relevant - it just has to match the window
                    // property alias (atom) used for request of the window property value
                    // (supported IPC (clipboard) formats) by X11lib.XGetWindowProperty().
                    IntPtr dataProperty = ipcWindow.OTK_DATA_PROP;

                    // DnD or Cut/Copy and Paste - STAGE 1:
                    // Evaluate available clipboard formats and - if _XA_STRING 
                    // format is supported -
                    // request the clipboard string.
                    // 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
                    if (selectionEventIn.target    == ipcWindow.XA_TARGETS)
                    {
                        // Define the window property alias (atom), 
                        // used to handle (requestor <--> owner)
                        // data transfer regarding supported IPC (clipboard) formats.
                        // QT uses: "_QT_SELECTION"
                        // GDK uses: "GDK_SELECTION"
                        // Since the window property alias (atom) is returned unchanged from
                        // X11lib.XSendEvent() after X11lib.XChangeProperty() 
                        // has overridden the window property value 
                        // (with the supported IPC (clipboard) formats),
                        // the exact value is not relevant - it just has to match the window
                        // property alias (atom) used for request of the window property value
                        // (supported IPC (clipboard) formats) by X11lib.XGetWindowProperty().
                        IntPtr property = ipcWindow.OTK_TARGETS_PROP;

                        IntPtr      atomActType = IntPtr.Zero;
                        X11.TInt    actFormat   = 0;
                        X11.TUlong  nItems      = 0;
                        X11.TUlong  nRemaining  = 0;
                        X11.TLong   nCapacity   = (X11.TLong)4096;
                        IntPtr      data        = IntPtr.Zero;
                        X11.TInt    result      = X11.X11lib.XGetWindowProperty (
                            selectionEventIn.display, selectionEventIn.requestor,
                            property, (X11.TLong)0, nCapacity, 
                            true, IpcWindow.AnyPropertyType,
                            ref atomActType, ref actFormat, 
                            ref nItems, ref nRemaining, ref data);
                        if (result != 0)
                        {
                            ConsoleMessageSink.WriteError(null, this.GetType ().Name +
                                "::ProcessMessage () Failed to get 
                                the property for SelectionNotify " +
                                "event containing the supported clipboard data formats!");
                            X11.X11lib.XFree(data);
                            return true;
                        }

                        bool supportsUTF8   = false;
                        bool supportsString = false;
                        if (atomActType == X11.XAtoms.XA_ATOM && nItems > 0)
                        {
                            IntPtr[] types = new IntPtr[(int)nItems];
                            for (int index = 0; index < (int)nItems; index++)
                            {
                                types[index] = Marshal.ReadIntPtr(data, index * IntPtr.Size);
                                if (types[index] == ipcWindow.XA_UTF8_STRING)
                                    supportsUTF8   = true;
                                if (types[index] == ipcWindow.XA_STRING)
                                    supportsString = true;
                            }
                        }
                        X11.X11lib.XFree(data);

                        // Signal the selection owner that we have successfully read the data.
                        X11.X11lib.XDeleteProperty(selectionEventIn.display,
                                                   selectionEventIn.requestor,
                                                   property);

                        if (supportsUTF8 == true)
                            Clipboard.RequestData  (ipcWindow.XA_UTF8_STRING,
                                                   dataProperty,
                                                    selectionEventIn.time);
                        else if (supportsString == true)
                            Clipboard.RequestData  (ipcWindow.XA_STRING,
                                                    dataProperty,
                                                    selectionEventIn.time);
                    }
                    // DnD or Cut/Copy and Paste - STAGE 2:
                    // Process the provided - _XA_STRING formatted - clipboard string.
                    // 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
                    if (selectionEventIn.property == dataProperty)
                    {
                        // Get the length of the string.
                        IntPtr      atomActType = IntPtr.Zero;
                        X11.TInt    actFormat   = 0;
                        X11.TUlong  nItems      = 0;
                        X11.TUlong  nRemaining  = 0;
                        X11.TLong   nCapacity   = (X11.TLong)4096;
                        IntPtr      data        = IntPtr.Zero;
                        X11.TInt result = X11.X11lib.XGetWindowProperty(
                            selectionEventIn.display, selectionEventIn.requestor,
                            dataProperty, (X11.TLong)0, nCapacity, true, 
                            IpcWindow.AnyPropertyType,
                            ref atomActType, ref actFormat, 
                            ref nItems, ref nRemaining, ref data);

                        if (result != 0)
                        {
                            ConsoleMessageSink.WriteError(null, this.GetType ().Name +
                                "::XrwProcessSelectionNotify () 
                                Failed to get the property for " +
                                "SelectionNotify event containing the clipboard value!");
                            X11.X11lib.XFree(data);
                            return true;
                        }

                        if (atomActType == ipcWindow.XA_UTF8_STRING ||
                            atomActType == ipcWindow.XA_STRING)
                        {
                            // Provide the investigated clipboard data to the requestor.
                            string text;
                            if (atomActType == ipcWindow.XA_UTF8_STRING)
                            {
                                byte[] s = new byte[(int)nItems];
                                Marshal.Copy(data, s, 0, (int)nItems);

                                text =  System.Text.Encoding.UTF8.GetString(s);
                            }
                            else
                                text = Marshal.PtrToStringAuto (data);

                            X11.ClipboardGetResultDelegate treatClipboardResult = 
                                X11.X11Clipboard.TreatClipboardResultDelegate;
                            if (treatClipboardResult != null)
                                treatClipboardResult (text);
                        }
                        X11.X11lib.XFree(data);

                        // Signal the selection owner that we have successfully read the data.
                        X11.X11lib.XDeleteProperty(selectionEventIn.display,
                                                   selectionEventIn.requestor,
                                                   dataProperty);
                    }
                }
            }
        }
    }

    // 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.

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

How It Works

Assumed, we have any text editor running, select some text and provide the text via "Edit" | "Copy" menu or [Ctrl]+[c] keys, the text editor 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 our OpenTK based C# OpenGL window application requests clipboard text via "Edit" | "Paste" menu or [Ctrl]+[v] keys, it will behave as a selection requestor and call XConvertSelection() to determine the best data interchange format.

The selection requestor's XConvertSelection() call triggers 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 its 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 receives an XEventName.SelectionRequest event asking for supported XA_TARGETS, announces its supported XA_TARGETS to the indicated window property via XChangeProperty() and sends an XEventName.SelectionNotify event to inform the selection requestor about its answer.

Now the selection requestor will receive the XEventName.SelectionNotify event from the selection owner, delivering the supported XA_TARGETS announced to the indicated window property. The selection requestor should evaluate the supported XA_TARGETS, pick the preferred 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 call XConvertSelection() to demand the copy-and-paste content formatted as an XA_UTF8_STRING.

The selection requestor's XConvertSelection() call triggers an XEventName.SelectionRequest event to inform the selection owner about the format in which the selection requestor wants the data is to be delivered.

The selection owner will receive the XEventName.SelectionRequest event asking for the copy-and-paste content formatted as an XA_UTF8_STRING, 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 owner picks up the copy-and-paste content from the indicated window property and pastes 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 requestor side, we have a series of asynchronous processing steps initiated by XConvertSelection() calls, triggering XEventName.SelectionRequest events, and a conclusive asynchronous processing step to accept the requested copy-and-paste text. Or in other words: Implementing clipboard functionality is not straightforward but is reaction to events.

At third, we need to request the copy-and-paste text and accept the delivered copy-and-paste text by our OpenTK based C# OpenGL window application. My application uses the [Ctrl]+[v] key combination to initiate the copy-and-paste text request and to define the callback to accept the delivered copy-and-paste text.

C#
else if (e.Key == (uint)OpenTK.Input.Key.V)
{
    Clipboard.GetText(delegate(object result)
        {

            ...

        });
}

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 requestor, 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>Ask the clipboard selection owner for supported formats.</summary>
    /// <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="property">The <see cref="IntPtr"/> property atom, 
    /// that identifies the data
    /// buffer for data transfer.</param>
    /// <param name="time">The <see cref="TTime"/> time the event 
    /// (typical mouse or button) occurs,
    /// that triggers this clipboard interaction.</param>
    public static void RequestTypes (IntPtr selection, IntPtr property, X11.TTime time)
    {
        IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
        if (ipcWindow == null)
            return;

        X11Clipboard.RequestClipboardTypes (ipcWindow.Display,
                                            ipcWindow.Handle,
                                            selection,
                                            ipcWindow.XA_TARGETS,
                                            property,
                                            time);
    }

    /// <summary>Ask the clipboard selection owner for <c>target</c> data.</summary>
    /// <param name="target">The <see cref="IntPtr"/> target atom, that defines the requested
    /// format.</param>
    /// <param name="property">The <see cref="IntPtr"/> property atom, 
    /// that identifies the data
    /// buffer for data transfer.</param>
    /// <param name="time">The <see cref="TTime"/> time the event 
    /// (typical mouse or button) occurs,
    /// that triggers this clipboard interaction.</param>
    public static void RequestData  (IntPtr target, IntPtr property, X11.TTime time)
    {
        IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
        if (ipcWindow == null)
            return;

        IntPtr[] possibleSelections = new IntPtr[]
        {   ipcWindow.XA_CLIPBOARD,
            ipcWindow.XA_PRIMARY,
            ipcWindow.XA_SECONDARY
        };
        X11Clipboard.RequestClipboardData (ipcWindow.Display,
                                           ipcWindow.Handle,
                                           possibleSelections,
                                           target,
                                           property,
                                           time);
    }

}

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 requestor side only.

See my tip Clipboard Text Provider Capability for the solution on the selection owner 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

  • 18th February, 2019: Initial version

License

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