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 25, 2024
1 parent 2d5636e commit c23be95
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 14 deletions.
13 changes: 8 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, xaINCR;
unsigned long TXWindow::black, TXWindow::white;
unsigned long TXWindow::defaultFg, TXWindow::defaultBg;
unsigned long TXWindow::lightBg, TXWindow::darkBg;
Expand Down Expand Up @@ -65,6 +65,8 @@ void TXWindow::init(Display* dpy, const char* defaultWindowClass_)
xaSELECTION_TIME = XInternAtom(dpy, "SELECTION_TIME", False);
xaSELECTION_STRING = XInternAtom(dpy, "SELECTION_STRING", False);
xaCLIPBOARD = XInternAtom(dpy, "CLIPBOARD", False);
xaUTF8_STRING = XInternAtom(dpy, "UTF8_STRING", False);
xaINCR = XInternAtom(dpy, "INCR", False);
XColor cols[6];
cols[0].red = cols[0].green = cols[0].blue = 0x0000;
cols[1].red = cols[1].green = cols[1].blue = 0xbbbb;
Expand Down Expand Up @@ -464,17 +466,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, xaINCR;

#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
49 changes: 47 additions & 2 deletions unix/x0vncserver/XDesktop.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#endif
#ifdef HAVE_XFIXES
#include <X11/extensions/Xfixes.h>
#include <X11/Xatom.h>
#endif
#ifdef HAVE_XRANDR
#include <X11/extensions/Xrandr.h>
Expand Down Expand Up @@ -83,7 +84,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 +182,15 @@ XDesktop::XDesktop(Display* dpy_, Geometry *geometry_)
if (XFixesQueryExtension(dpy, &xfixesEventBase, &xfixesErrorBase)) {
XFixesSelectCursorInput(dpy, DefaultRootWindow(dpy),
XFixesDisplayCursorNotifyMask);

XFixesSelectSelectionInput(dpy, DefaultRootWindow(dpy), XA_PRIMARY,
XFixesSetSelectionOwnerNotifyMask);
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 +897,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 +1058,28 @@ bool XDesktop::setCursor()
return true;
}
#endif

// X selection availability changed, let VNC clients know
void XDesktop::handleXSelectionAnnounce(bool available) {
server->announceClipboard(available);
}

// A VNC client wants data, send request to selection owner
void XDesktop::handleClipboardRequest() {
selection.requestSelectionData();
}

// Data is available, send it to clients
void XDesktop::handleXSelectionData(const char* data) {
server->sendClipboardData(data);
}

// 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.handleClientClipboardData(data);
}
13 changes: 12 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,13 @@ 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 interface
void handleXSelectionAnnounce(bool available) override;
void handleXSelectionData(const char* data) override;

// -=- TXGlobalEventHandler interface
bool handleGlobalEvent(XEvent* ev) override;
Expand All @@ -79,6 +89,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
195 changes: 195 additions & 0 deletions unix/x0vncserver/XSelection.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/* 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 <X11/Xatom.h>
#include <rfb/Configuration.h>
#include <rfb/LogWriter.h>
#include <rfb/util.h>
#include <x0vncserver/XSelection.h>

rfb::BoolParameter setPrimary("SetPrimary",
"Set the PRIMARY as well as the CLIPBOARD selection",
true);
rfb::BoolParameter sendPrimary("SendPrimary",
"Send the PRIMARY as well as the CLIPBOARD selection",
true);

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

XSelection::XSelection(Display* dpy_, XSelectionHandler* handler_)
: TXWindow(dpy_, 1, 1, nullptr), handler(handler_), announcedSelection(None)
{
probeProperty = XInternAtom(dpy, "TigerVNC_ProbeProperty", False);
transferProperty = XInternAtom(dpy, "TigerVNC_TransferProperty", False);
timestampProperty = XInternAtom(dpy, "TigerVNC_TimestampProperty", False);
setName("TigerVNC Clipboard (x0vncserver)");
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(), timestampProperty, XA_STRING, 8, PropModeReplace,
&data, sizeof(data));
XIfEvent(dpy, &ev, &PropertyEventMatcher, (XPointer)&timestampProperty);
return ev.xproperty.time;
}

// Takes ownership of selections, backed by given data.
void XSelection::handleClientClipboardData(const char* data)
{
vlog.debug("Received client clipboard data, taking selection ownership");

Time time = getXServerTime();
ownSelection(xaCLIPBOARD, time);
if (!selectionOwner(xaCLIPBOARD))
vlog.error("Unable to own CLIPBOARD selection");

if (setPrimary) {
ownSelection(XA_PRIMARY, time);
if (!selectionOwner(XA_PRIMARY))
vlog.error("Unable to own PRIMARY selection");
}

if (selectionOwner(xaCLIPBOARD) || selectionOwner(XA_PRIMARY))
clientData = data;
}

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

if (target == XA_STRING) {
std::string latin1 = rfb::utf8ToLatin1(clientData.data(), clientData.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*)clientData.data(), clientData.length());
return true;
}

return false;
}

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

if (selection == announcedSelection)
announceSelection(None);

if (owner == None || owner == win())
return;

if (!selectionOwner(XA_PRIMARY) && !selectionOwner(xaCLIPBOARD))
clientData = "";

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

void XSelection::announceSelection(Atom selection)
{
announcedSelection = selection;
handler->handleXSelectionAnnounce(selection != None);
}

void XSelection::requestSelectionData()
{
if (announcedSelection != None)
XConvertSelection(dpy, announcedSelection, xaTARGETS, transferProperty, win(),
CurrentTime);
}

// Some information about selection is received from current owner
void XSelection::selectionNotify(XSelectionEvent* ev, Atom type, int format,
int nitems, void* data)
{
if (!ev || !data || type == None)
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;
}

if (ev->property == probeProperty) {
// Only probing for now, will issue real request when client asks for data
if (stringSupported || utf8Supported)
announceSelection(ev->selection);
return;
}

// Prefer UTF-8 if available
if (utf8Supported)
XConvertSelection(dpy, ev->selection, xaUTF8_STRING, transferProperty, win(),
ev->time);
else if (stringSupported)
XConvertSelection(dpy, ev->selection, XA_STRING, transferProperty, win(),
ev->time);
} else if (ev->target == xaUTF8_STRING || ev->target == XA_STRING) {
if (type == xaINCR) {
// Incremental transfer is not supported
vlog.debug("Selected data is too big!");
return;
}

if (format != 8)
return;

if (type == xaUTF8_STRING) {
std::string result = rfb::convertLF((char*)data, nitems);
handler->handleXSelectionData(result.c_str());
} else if (type == XA_STRING) {
std::string result = rfb::convertLF((char*)data, nitems);
result = rfb::latin1ToUTF8(result.data(), result.length());
handler->handleXSelectionData(result.c_str());
}
}
}
Loading

0 comments on commit c23be95

Please sign in to comment.