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.
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.
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:
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)
{
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);
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
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)
{
...
}
}
}
}
_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.
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.
...
else if (e.Key == (uint)OpenTK.Input.Key.C)
{
int anchorOffset = _selection.AnchorPosition.GetSymbolOffset();
int movingOffset = _selection.MovingPosition.GetSymbolOffset();
int selLength = -(movingOffset - anchorOffset);
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:
public static class Clipboard
{
#region Static Properties
public static string Text { get; private set; }
#endregion Static Properties
#region Static Methods
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.
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