Skip to content

Commit

Permalink
Add clipboard support to x0vncserver
Browse files Browse the repository at this point in the history
  • Loading branch information
gujjwal00 committed Sep 5, 2024
1 parent 2d5636e commit f714ab0
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 14 deletions.
12 changes: 7 additions & 5 deletions unix/tx/TXWindow.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ std::list<TXWindow*> windows;

Atom wmProtocols, wmDeleteWindow, wmTakeFocus;
Atom xaTIMESTAMP, xaTARGETS, xaSELECTION_TIME, xaSELECTION_STRING;
Atom xaCLIPBOARD;
Atom xaCLIPBOARD, xaUTF8_STRING;
unsigned long TXWindow::black, TXWindow::white;
unsigned long TXWindow::defaultFg, TXWindow::defaultBg;
unsigned long TXWindow::lightBg, TXWindow::darkBg;
Expand Down Expand Up @@ -64,6 +64,7 @@ void TXWindow::init(Display* dpy, const char* defaultWindowClass_)
xaTARGETS = XInternAtom(dpy, "TARGETS", False);
xaSELECTION_TIME = XInternAtom(dpy, "SELECTION_TIME", False);
xaSELECTION_STRING = XInternAtom(dpy, "SELECTION_STRING", False);
xaUTF8_STRING = XInternAtom(dpy, "UTF8_STRING", False);
xaCLIPBOARD = XInternAtom(dpy, "CLIPBOARD", False);
XColor cols[6];
cols[0].red = cols[0].green = cols[0].blue = 0x0000;
Expand Down Expand Up @@ -464,17 +465,18 @@ void TXWindow::handleXEvent(XEvent* ev)
} else {
se.property = ev->xselectionrequest.property;
if (se.target == xaTARGETS) {
Atom targets[2];
Atom targets[3];
targets[0] = xaTIMESTAMP;
targets[1] = XA_STRING;
targets[2] = xaUTF8_STRING;
XChangeProperty(dpy, se.requestor, se.property, XA_ATOM, 32,
PropModeReplace, (unsigned char*)targets, 2);
PropModeReplace, (unsigned char*)targets, 3);
} else if (se.target == xaTIMESTAMP) {
Time t = selectionOwnTime[se.selection];
XChangeProperty(dpy, se.requestor, se.property, XA_INTEGER, 32,
PropModeReplace, (unsigned char*)&t, 1);
} else if (se.target == XA_STRING) {
if (!selectionRequest(se.requestor, se.selection, se.property))
} else if (se.target == XA_STRING || se.target == xaUTF8_STRING) {
if (!selectionRequest(se.requestor, se.selection, se.target, se.property))
se.property = None;
} else {
se.property = None;
Expand Down
3 changes: 2 additions & 1 deletion unix/tx/TXWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class TXWindow {
// returning true if successful, false otherwise.
virtual bool selectionRequest(Window /*requestor*/,
Atom /*selection*/,
Atom /*target*/,
Atom /*property*/) { return false;}

// Static methods
Expand Down Expand Up @@ -226,6 +227,6 @@ class TXWindow {

extern Atom wmProtocols, wmDeleteWindow, wmTakeFocus;
extern Atom xaTIMESTAMP, xaTARGETS, xaSELECTION_TIME, xaSELECTION_STRING;
extern Atom xaCLIPBOARD;
extern Atom xaCLIPBOARD, xaUTF8_STRING;

#endif
1 change: 1 addition & 0 deletions unix/x0vncserver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ add_executable(x0vncserver
XPixelBuffer.cxx
XDesktop.cxx
RandrGlue.c
XSelection.cxx
../vncconfig/QueryConnectDialog.cxx
)

Expand Down
41 changes: 39 additions & 2 deletions unix/x0vncserver/XDesktop.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ static const char * ledNames[XDESKTOP_N_LEDS] = {

XDesktop::XDesktop(Display* dpy_, Geometry *geometry_)
: dpy(dpy_), geometry(geometry_), pb(nullptr), server(nullptr),
queryConnectDialog(nullptr), queryConnectSock(nullptr),
queryConnectDialog(nullptr), queryConnectSock(nullptr), selection(dpy_, this),
oldButtonMask(0), haveXtest(false), haveDamage(false),
maxButtons(0), running(false), ledMasks(), ledState(0),
codeMap(nullptr), codeMapLen(0)
Expand Down Expand Up @@ -181,10 +181,13 @@ XDesktop::XDesktop(Display* dpy_, Geometry *geometry_)
if (XFixesQueryExtension(dpy, &xfixesEventBase, &xfixesErrorBase)) {
XFixesSelectCursorInput(dpy, DefaultRootWindow(dpy),
XFixesDisplayCursorNotifyMask);

XFixesSelectSelectionInput(dpy, DefaultRootWindow(dpy), xaCLIPBOARD,
XFixesSetSelectionOwnerNotifyMask);
} else {
#endif
vlog.info("XFIXES extension not present");
vlog.info("Will not be able to display cursors");
vlog.info("Will not be able to display cursors or monitor clipboard");
#ifdef HAVE_XFIXES
}
#endif
Expand Down Expand Up @@ -891,6 +894,20 @@ bool XDesktop::handleGlobalEvent(XEvent* ev) {
return false;

return setCursor();
}
else if (ev->type == xfixesEventBase + XFixesSelectionNotify) {
XFixesSelectionNotifyEvent* sev = (XFixesSelectionNotifyEvent*)ev;

if (!running)
return true;

if (sev->subtype != XFixesSetSelectionOwnerNotify)
return false;

selection.handleSelectionOwnerChange(sev->owner, sev->selection,
sev->timestamp);

return true;
#endif
#ifdef HAVE_XRANDR
} else if (ev->type == Expose) {
Expand Down Expand Up @@ -1038,3 +1055,23 @@ bool XDesktop::setCursor()
return true;
}
#endif

// X11 clipboard data is available, let the clients know
void XDesktop::handleXClipboardAvailable(bool available) {
server->announceClipboard(available);
}

// A client wants clipboard data
void XDesktop::handleClipboardRequest() {
server->sendClipboardData(selection.getClipboardData().c_str());
}

// When a client says it has clipboard data, request it
void XDesktop::handleClipboardAnnounce(bool available) {
if (available) server->requestClipboard();
}

// Client has sent the data
void XDesktop::handleClipboardData(const char* data) {
if (data) selection.setClipboardData(data);
}
12 changes: 11 additions & 1 deletion unix/x0vncserver/XDesktop.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

#include <vncconfig/QueryConnectDialog.h>

#include "XSelection.h"

class Geometry;
class XPixelBuffer;

Expand All @@ -46,7 +48,8 @@ struct AddedKeySym

class XDesktop : public rfb::SDesktop,
public TXGlobalEventHandler,
public QueryResultCallback
public QueryResultCallback,
public XSelectionHandler
{
public:
XDesktop(Display* dpy_, Geometry *geometry);
Expand All @@ -64,6 +67,12 @@ class XDesktop : public rfb::SDesktop,
void keyEvent(uint32_t keysym, uint32_t xtcode, bool down) override;
unsigned int setScreenLayout(int fb_width, int fb_height,
const rfb::ScreenSet& layout) override;
void handleClipboardRequest() override;
void handleClipboardAnnounce(bool available) override;
void handleClipboardData(const char* data) override;

// -=- XSelectionHandler
void handleXClipboardAvailable(bool availble) override;

// -=- TXGlobalEventHandler interface
bool handleGlobalEvent(XEvent* ev) override;
Expand All @@ -79,6 +88,7 @@ class XDesktop : public rfb::SDesktop,
rfb::VNCServer* server;
QueryConnectDialog* queryConnectDialog;
network::Socket* queryConnectSock;
XSelection selection;
uint8_t oldButtonMask;
bool haveXtest;
bool haveDamage;
Expand Down
144 changes: 144 additions & 0 deletions unix/x0vncserver/XSelection.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/* Copyright (C) 2024 Gaurav Ujjwal. All Rights Reserved.
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/

#include "XSelection.h"
#include <X11/Xatom.h>
#include <rfb/LogWriter.h>
#include <rfb/util.h>

static rfb::LogWriter vlog("XSelection");

XSelection::XSelection(Display* dpy_, XSelectionHandler* handler_)
: TXWindow(dpy_, 1, 1, nullptr), handler(handler_)
{
xaTransferProperty = XInternAtom(dpy, "SelectionTransferProperty", False);
xaTimestampProperty = XInternAtom(dpy, "TimestampAccessProperty", False);
addEventMask(PropertyChangeMask); // Required for PropertyNotify events
}

static Bool PropertyEventMatcher(Display* /* dpy */, XEvent* ev, XPointer prop)
{
if (ev->type == PropertyNotify && ev->xproperty.atom == *((Atom*)prop))
return True;
else
return False;
}

Time XSelection::getXServerTime()
{
XEvent ev;
uint8_t data = 0;

// Trigger a PropertyNotify event to extract server time
XChangeProperty(dpy, win(), xaTimestampProperty, XA_STRING, 8,
PropModeReplace, &data, sizeof(data));
XIfEvent(dpy, &ev, &PropertyEventMatcher, (XPointer)&xaTimestampProperty);
return ev.xproperty.time;
}

// Takes ownership on CLIPBOARD selection, backed by given data.
// data should point to a UTF-8 string.
void XSelection::setClipboardData(const char* data)
{
Time time = getXServerTime();
ownSelection(xaCLIPBOARD, time);

if (selectionOwner(xaCLIPBOARD))
clipData = data;
else
vlog.error("Unable to own CLIPBOARD selection");
}

// We own the selection and another X client has asked for data
bool XSelection::selectionRequest(Window requestor, Atom selection, Atom target,
Atom property)
{
if (clipData.empty() || selection != xaCLIPBOARD)
return false;

if (target == XA_STRING) {
std::string latin1 = rfb::utf8ToLatin1(clipData.data(), clipData.length());
XChangeProperty(dpy, requestor, property, XA_STRING, 8, PropModeReplace,
(unsigned char*)latin1.data(), latin1.length());
return true;
}

if (target == xaUTF8_STRING) {
XChangeProperty(dpy, requestor, property, xaUTF8_STRING, 8, PropModeReplace,
(unsigned char*)clipData.data(), clipData.length());
return true;
}

return false;
}

// Selection-owner change event implies a change in selection data
void XSelection::handleSelectionOwnerChange(Window owner, Atom selection,
Time time)
{
if (owner == win() || selection != xaCLIPBOARD)
return;

XConvertSelection(dpy, selection, xaTARGETS, xaTransferProperty, win(), time);
}

// Some information about selection is received from owner
void XSelection::selectionNotify(XSelectionEvent* ev, Atom type, int format,
int nitems, void* data)
{
if (!ev || !data || type == None) {
handler->handleXClipboardAvailable(false);
return;
}

if (ev->target == xaTARGETS) {
if (format != 32 || type != XA_ATOM)
return;

Atom* targets = (Atom*)data;
bool utf8Supported = false;
bool stringSupported = false;

for (int i = 0; i < nitems; i++) {
if (targets[i] == xaUTF8_STRING)
utf8Supported = true;
else if (targets[i] == XA_STRING)
stringSupported = true;
}

// Prefer UTF-8 if available
if (utf8Supported)
XConvertSelection(dpy, ev->selection, xaUTF8_STRING, xaTransferProperty,
win(), ev->time);
else if (stringSupported)
XConvertSelection(dpy, ev->selection, XA_STRING, xaTransferProperty,
win(), ev->time);
} else if (ev->target == xaUTF8_STRING || ev->target == XA_STRING) {
if (format != 8)
return;

if (type == xaUTF8_STRING) {
clipData = rfb::convertLF((char*)data, nitems);
handler->handleXClipboardAvailable(true);
} else if (type == XA_STRING) {
std::string filtered = rfb::convertLF((char*)data, nitems);
clipData = rfb::latin1ToUTF8(filtered.data(), filtered.length());
handler->handleXClipboardAvailable(true);
}
}
}
54 changes: 54 additions & 0 deletions unix/x0vncserver/XSelection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* Copyright (C) 2024 Gaurav Ujjwal. All Rights Reserved.
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/

#ifndef __XSELECTION_H__
#define __XSELECTION_H__

#include <string>
#include <tx/TXWindow.h>

class XSelectionHandler
{
public:
virtual void handleXClipboardAvailable(bool available) = 0;
};


class XSelection : TXWindow
{
public:
XSelection(Display* dpy_, XSelectionHandler* handler_);

std::string& getClipboardData() { return clipData; }
void setClipboardData(const char* data);
void handleSelectionOwnerChange(Window owner, Atom selection, Time time);

private:
std::string clipData; // Always in UTF-8
Atom xaTransferProperty;
Atom xaTimestampProperty;
XSelectionHandler* handler;

Time getXServerTime();
void selectionNotify(XSelectionEvent* ev, Atom type, int format, int nitems,
void* data) override;
bool selectionRequest(Window requestor, Atom selection, Atom target,
Atom property) override;
};

#endif
5 changes: 0 additions & 5 deletions unix/x0vncserver/x0vncserver.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,6 @@ int main(int argc, char** argv)

Configuration::enableServerParams();

// FIXME: We don't support clipboard yet
Configuration::removeParam("AcceptCutText");
Configuration::removeParam("SendCutText");
Configuration::removeParam("MaxCutText");

// Assume different defaults when socket activated
if (hasSystemdListeners())
rfbport.setParam(-1);
Expand Down

0 comments on commit f714ab0

Please sign in to comment.