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.
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,
(X11.TInt)0,
X11.XWindowClass.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:
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.
public bool ProcessMessage ()
{
if (!_glWindow.Exists)
return false;
if (!_context.IsCurrent)
{
_context.MakeCurrent(_glWindow.WindowInfo);
}
IpcWindow ipcWindow = XrwGL.Application.Current.IpcWindow;
if (ipcWindow != null)
{
X11.XEvent e = new X11.XEvent();
{
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;
IntPtr dataProperty = ipcWindow.OTK_DATA_PROP;
if (selectionEventIn.target == ipcWindow.XA_TARGETS)
{
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);
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);
}
if (selectionEventIn.property == dataProperty)
{
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)
{
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);
X11.X11lib.XDeleteProperty(selectionEventIn.display,
selectionEventIn.requestor,
dataProperty);
}
}
}
}
}
_glWindow.ProcessEvents ();
...
}
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.
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:
public static class Clipboard
{
#region Static Properties
public static string Text { get; private set; }
#endregion Static Properties
#region Static Methods
...
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);
}
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.
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