This is an automated email from the git hooks/post-receive script. x2go pushed a change to branch master in repository x2gokdriveclient. from 04caf87 identify screen, when user hovering the action in the multiple monitors menu. new 5d196b1 not use QClipboard class on Linux. It has problems mit INCR properties. Using XCB API instead. The 1 revisions listed above as "new" are entirely new to this repository and will be described in separate emails. The revisions listed as "adds" were already present in the repository and have only been added to this reference. Summary of changes: client.cpp | 143 ++++++++- client.h | 54 +++- debian/changelog | 1 + displayarea.cpp | 3 +- x2gokdriveclient.pro | 7 +- xcbclip.cpp | 816 +++++++++++++++++++++++++++++++++++++++++++++++++++ xcbclip.h | 79 +++++ 7 files changed, 1088 insertions(+), 15 deletions(-) create mode 100644 xcbclip.cpp create mode 100644 xcbclip.h -- Alioth's /home/x2go-admin/maintenancescripts/git/hooks/post-receive-email on /srv/git/code.x2go.org/x2gokdriveclient.git
This is an automated email from the git hooks/post-receive script. x2go pushed a commit to branch master in repository x2gokdriveclient. commit 5d196b1b1c77db9e65850419e126a313f6326892 Author: Oleksandr Shneyder <o.shneyder@phoca-gmbh.de> Date: Thu Aug 20 15:37:08 2020 -0500 not use QClipboard class on Linux. It has problems mit INCR properties. Using XCB API instead. --- client.cpp | 143 ++++++++- client.h | 54 +++- debian/changelog | 1 + displayarea.cpp | 3 +- x2gokdriveclient.pro | 7 +- xcbclip.cpp | 816 +++++++++++++++++++++++++++++++++++++++++++++++++++ xcbclip.h | 79 +++++ 7 files changed, 1088 insertions(+), 15 deletions(-) diff --git a/client.cpp b/client.cpp index e016ef6..a2178e6 100644 --- a/client.cpp +++ b/client.cpp @@ -53,7 +53,9 @@ #include <QClipboard> #include "screenidentifier.h" - +#ifdef Q_OS_LINUX +#include <xcbclip.h> +#endif X2GoCursor::X2GoCursor(uint16_t width, uint16_t height, uint16_t xhot, uint16_t yhot, uint32_t serialNumber, uint32_t dataSize) @@ -107,10 +109,22 @@ Frame::~Frame() } +OutputChunk::OutputChunk(SelectionType selection, SelectionMime mime) +{ + this->selection=selection; + mimeData=mime; + compressed=false; + firstChunk=lastChunk=false; +} + Client::Client() { + #ifdef Q_OS_LINUX + clipboard=new XCBClip(this); + #endif + setGeometry(0,0,800,600); displayArea=new DisplayArea(this); @@ -130,7 +144,9 @@ Client::Client() connect(clientSocket, SIGNAL(disconnected()), this, SLOT(socketDisconnected())); connect(clientSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError))); connect(clientSocket, SIGNAL(readyRead()), this, SLOT(dataArrived()) ); +#ifndef Q_OS_LINUX connect(QGuiApplication::clipboard(), SIGNAL(changed(QClipboard::Mode)), this, SLOT(slotSelectionChanged(QClipboard::Mode))); +#endif @@ -795,6 +811,9 @@ void Client::getCursor() void Client::getSelectionBuffer() { +#ifndef Q_OS_LINUX +#warning check it +//if using qt class, not supporting on demand load of data QClipboard::Mode mode=QClipboard::Clipboard; if(!selectionClipboard) { @@ -804,22 +823,26 @@ void Client::getSelectionBuffer() else qDebug()<<"Got new Clipboard, format:"<<selectionFormat; + QClipboard* clipboard=QGuiApplication::clipboard(); // qDebug()<<messageBuffer; switch(selectionFormat) { - case STRING: QGuiApplication::clipboard()->setText(QString::fromLocal8Bit(messageBuffer, selectionSize), mode); break; - case UTF_STRING: QGuiApplication::clipboard()->setText(QString::fromUtf8(messageBuffer, selectionSize), mode);break; + case STRING: clipboard->setText(QString::fromLocal8Bit(messageBuffer, selectionSize), mode); break; + case UTF_STRING: clipboard->setText(QString::fromUtf8(messageBuffer, selectionSize), mode);break; case PIXMAP: { QPixmap pix; pix.loadFromData((const uchar*)messageBuffer, selectionSize); - qDebug()<<pix; - QGuiApplication::clipboard()->setImage(pix.toImage()); +// qDebug()<<pix; + clipboard->setImage(pix.toImage(), mode); break; } } +#else + clipboard->setInputSelectionData(selectionClipboard, selectionFormat, firstChunk, lastChunk, compressed, selectionSize, messageBuffer); +#endif freeMessageBuffer(); } @@ -880,9 +903,34 @@ void Client::getDeletedCursors() void Client::getSelection() { bytesReady=0; - selectionClipboard=*((uint32_t*)messageBuffer+1); - selectionFormat=*((uint32_t*)messageBuffer+2); + selectionClipboard=CLIPBOARD; + if( *((uint32_t*)messageBuffer+1 ) != CLIPBOARD) + { + selectionClipboard=PRIMARY; + } + selectionFormat=PIXMAP; + if(*((uint32_t*)messageBuffer+2) != PIXMAP) + { + selectionFormat=UTF_STRING; + } + selectionSize=*((uint32_t*)messageBuffer+3); + + if(serverSupportsExtSelection()) + { + firstChunk=*((uint32_t*)messageBuffer+4); + lastChunk=*((uint32_t*)messageBuffer+5); + compressed=*((uint32_t*)messageBuffer+6); + } + else + { + //if server doesn't support extended selection it'll send data in one chunk, uncompressed + firstChunk=true; + lastChunk=true; + compressed=false; + } + + currentDataType=SELECTIONBUFFER; bytesLeftToRead=selectionSize; qDebug()<<"Get Selection, is Clipboard"<<selectionClipboard<<selectionFormat<<selectionSize; @@ -1360,8 +1408,10 @@ void Client::setUseRandr(bool use) } } +#ifndef Q_OS_LINUX void Client::slotSelectionChanged(QClipboard::Mode mode) { +//we are not using QT Clipboard Class on Linux, seems that it has problems with INCR properties if(!connected) return; const QClipboard *clipboard = QGuiApplication::clipboard(); @@ -1369,8 +1419,19 @@ void Client::slotSelectionChanged(QClipboard::Mode mode) return; if(mode == QClipboard::Selection && clipboard->ownsSelection()) return; - qDebug()<<"selection changed for"<<mode; const QMimeData *mimeData = clipboard->mimeData(mode); + qDebug()<<"selection changed for"<<mode<<mimeData->formats(); + + //if server supports ext selection only sending notification that selection is changed and the list of supported mime types + if(serverSupportsExtSelection()) + { + + } + else + { + //server doesn't support ext selection we are sending clipboard data + } + QByteArray data; //add size/mime/type of buffer and start copy data char evmsg[EVLENGTH]{}; @@ -1428,5 +1489,71 @@ void Client::slotSelectionChanged(QClipboard::Mode mode) sendEvent(evmsg); } qDebug()<<"sent: "<<sentData<<"from"<<size; +} +#endif + +void Client::sendOutputSelChunk() +{ + //sending the first chunk from output selection queue + + if(outputSelectionQueue.isEmpty()) + return; + + OutputChunk* chunk=outputSelectionQueue.takeLast(); + if(!serverSupportsExtSelection() && (!chunk->firstChunk || !chunk->lastChunk)) + { + //selection has multiply chunks, but this server doesn't support ext selection, not sending anything + qDebug()<<"Server doesn't support extended selections"; + delete chunk; + return; + } + + char evmsg[EVLENGTH]{}; + uint32_t etype=SELECTIONEVENT; + uint32_t size; + uint8_t destination=chunk->selection; + uint8_t mime=chunk->mimeData; + uint8_t firstChunk=chunk->firstChunk; + uint8_t lastChunk=chunk->lastChunk; + uint8_t compressed=chunk->compressed; + uint32_t totalSize=chunk->totalSize; + + size=chunk->data.size(); + + memcpy(evmsg,(char*)&etype,4); + memcpy(evmsg+4,(char*)&size,4); + memcpy(evmsg+8,(char*)&destination,1); + memcpy(evmsg+9,(char*)&mime,1); + + //if server supports extended selection, sending extended header + if(serverSupportsExtSelection()) + { + memcpy(evmsg+10,(char*)&firstChunk,1); + memcpy(evmsg+11,(char*)&lastChunk,1); + memcpy(evmsg+12,(char*)&compressed,1); + memcpy(evmsg+13,(char*)&totalSize,4); + } + + uint headerSize=10; + if(serverSupportsExtSelection()) + headerSize=17; +// qDebug()<<"SEND SELECTION"<<size<<destination<<mime; + uint32_t sentData=(size < EVLENGTH-headerSize)?size:EVLENGTH-headerSize; + memcpy(evmsg+headerSize,chunk->data.data(),sentData); + sendEvent(evmsg); + while(sentData<size) + { + int msg_length=(size-sentData < EVLENGTH)?size-sentData:EVLENGTH; + memcpy(evmsg, chunk->data.data()+sentData, msg_length); + sentData+=msg_length; + sendEvent(evmsg); + } +// qDebug()<<"sent: "<<sentData<<"from"<<size; + delete chunk; +} + +void Client::addToSelectionOutput(OutputChunk* chunk) +{ + outputSelectionQueue.append(chunk); } diff --git a/client.h b/client.h index 9211384..fdb3f72 100644 --- a/client.h +++ b/client.h @@ -23,6 +23,7 @@ #define EVLENGTH 41 + #define Button1Mask (1<<8) #define Button2Mask (1<<9) #define Button3Mask (1<<10) @@ -64,12 +65,21 @@ #define HEADER_SIZE 56 #define REGION_HEADER 64 + +enum SelectionMime{STRING,UTF_STRING,PIXMAP}; +enum SelectionType{PRIMARY,CLIPBOARD}; + + #include <QMainWindow> #include <QAbstractSocket> #include <stdlib.h> #include <QClipboard> +#ifdef Q_OS_LINUX +class XCBClip; +#endif + class FrameRegion { public: @@ -104,6 +114,19 @@ public: uint16_t height; }; +class OutputChunk +{ +public: + OutputChunk(SelectionType selection, SelectionMime mimeType); + QByteArray data; + enum SelectionMime mimeData; + bool compressed; + bool firstChunk; + bool lastChunk; + uint totalSize; + enum SelectionType selection; +}; + class QTcpSocket; class DisplayArea; @@ -127,6 +150,10 @@ public: QPixmap getPixmapFromCache(uint32_t crc); QPixmap addPixmapToCache(); bool isDisplayPointer(QMouseEvent* event); + void addToSelectionOutput(OutputChunk* chunk); + bool serverSupportsExtSelection(){return serverExtSelection;} + void sendOutputSelChunk(); + private slots: void slotIdentifyScreen(); @@ -151,7 +178,9 @@ private slots: void slotDisplayFS(); void slotEnableRandr(); void slotResizeFSFinal(); +#ifndef Q_OS_LINUX void slotSelectionChanged(QClipboard::Mode mode); +#endif public slots: void editWindowTitle(); @@ -159,8 +188,6 @@ public slots: private: enum{ HEADER, FRAMEREGION, REGIONDATA ,CURSORDATA, CURSORLIST, FRAMELIST, SELECTIONBUFFER } currentDataType; enum HeaderType{ FRAME, DELETEDFRAMES, CURSOR, DELETEDCURSORS, SELECTION}; - enum SelectionMime{STRING,UTF_STRING,PIXMAP}; - enum SelectionType{PRIMARY,CLIPBOARD}; void setUseRandr(bool use); void exitOnError(const QString& message); @@ -192,6 +219,7 @@ private: bool fullscreen=false; bool multidisp=false; int dispNumber=1; + bool serverExtSelection=false; QString cookie; MenuFrame *FSMenuBar; @@ -222,15 +250,29 @@ private: char* messageBuffer=0l; bool connected=false; + + //input selection chunk variables + //size of current chunk uint32_t selectionSize; - uint32_t selectionFormat; - bool selectionClipboard; + //format of chunk, string or pix + SelectionMime selectionFormat; + //if true clipboard else primary + SelectionType selectionClipboard; + //if it's the first chunk in multiply sel + bool firstChunk; + //if it's the last chunk in multiply sel + bool lastChunk; + //if the chunk compressed + bool compressed; + ////////// uint32_t deletedFramesSize=0; uint32_t deletedCursorsSize=0; QHash <uint32_t, QPixmap> frameCache; QHash <uint32_t, QCursor*> cursorCache; + QList <OutputChunk*> outputSelectionQueue; + int frameCount=0; QTimer* geometryDelay=0l; @@ -243,6 +285,10 @@ private: ScreenIdentifier *screenIdentifier=0l; QLabel* fr; +#ifdef Q_OS_LINUX + XCBClip* clipboard; +#endif + protected: void resizeEvent(QResizeEvent*); void moveEvent(QMoveEvent*); diff --git a/debian/changelog b/debian/changelog index d5ec6d2..7e75b7b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -20,5 +20,6 @@ x2gokdriveclient (0.0.0.1-0x2go1) UNRELEASED; urgency=medium - force client to send geometry event if the size of the display area is not changed after intiation for server to send initial image. - identify screen, when user hovering the action in the multiple monitors menu. + - not use QClipboard class on Linux. It has problems mit INCR properties. Using XCB API instead. -- Mike Gabriel <mike.gabriel@das-netzwerkteam.de> Tue, 04 Jun 2019 11:10:43 +0200 diff --git a/displayarea.cpp b/displayarea.cpp index 855922a..bdb9e1a 100644 --- a/displayarea.cpp +++ b/displayarea.cpp @@ -33,7 +33,6 @@ #ifdef Q_OS_LINUX #include <QX11Info> -#include <X11/Xlib.h> #endif @@ -101,7 +100,7 @@ void DisplayArea::grabKeyboard() { #ifdef Q_OS_LINUX qDebug()<<"Grab X11 Keyboard"; - XGrabKey(QX11Info::display(), AnyKey, AnyModifier, winId(), false, GrabModeAsync, GrabModeAsync); + xcb_grab_key(QX11Info::connection(), 0, winId(), XCB_MOD_MASK_ANY, XCB_GRAB_ANY, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC); #endif #ifdef Q_OS_WIN diff --git a/x2gokdriveclient.pro b/x2gokdriveclient.pro index 339627c..32f89a5 100644 --- a/x2gokdriveclient.pro +++ b/x2gokdriveclient.pro @@ -13,7 +13,7 @@ QT += network linux { message("Building for Linux") QT += x11extras -LIBS += -lX11 +LIBS += -lxcb -lxcb-xfixes } RESOURCES += resources.qrc @@ -22,3 +22,8 @@ RESOURCES += resources.qrc # Input SOURCES += main.cpp client.cpp displayarea.cpp menuframe.cpp screenidentifier.cpp HEADERS += client.h displayarea.h menuframe.h screenidentifier.h + +linux { +SOURCES += xcbclip.cpp +HEADERS += xcbclip.h +} diff --git a/xcbclip.cpp b/xcbclip.cpp new file mode 100644 index 0000000..6722ae4 --- /dev/null +++ b/xcbclip.cpp @@ -0,0 +1,816 @@ +/* + * QT Client for X2GoKDrive + * Copyright (C) 2018 Oleksandr Shneyder <o.shneyder@phoca-gmbh.de> + * Copyright (C) 2018 phoca-GmbH + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 program. If not, see <http://www.gnu.org/licenses/>. + * + */ +#include "xcbclip.h" + +#include <QDebug> +#include <QX11Info> +#include <QTimer> +#include <QBuffer> + + +XCBClip::XCBClip(Client* parent) +{ + + this->parent=parent; + + uint32_t mask = 0; + xcb_generic_error_t *error = 0; + + xcb_xfixes_query_version_cookie_t xfixes_query_cookie; + xcb_xfixes_query_version_reply_t *xfixes_query; + uint32_t values[2]; + + + con = xcb_connect (NULL, NULL); + /* Create the window */ + xcb_screen_t *screen = xcb_setup_roots_iterator (xcb_get_setup (con)).data; + clipWinId = xcb_generate_id (con); + mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; + values[0] = screen->white_pixel; + values[1] = XCB_EVENT_MASK_PROPERTY_CHANGE; + + //create window which will recieve selection events and provide remote selection to X-clients + xcb_create_window (con, + XCB_COPY_FROM_PARENT, + clipWinId, + screen->root, + 0, 0, + 1, 1, + 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + screen->root_visual, + mask, values); + + xcb_flush(con); + //check if we have xfixes, we need it to recieve selection owner events + + reply = xcb_get_extension_data(con, &xcb_xfixes_id); + if (reply && reply->present) + { + xfixes_query_cookie = xcb_xfixes_query_version(con, XCB_XFIXES_MAJOR_VERSION, XCB_XFIXES_MINOR_VERSION); + xfixes_query = xcb_xfixes_query_version_reply (con, xfixes_query_cookie, &error); + if (!xfixes_query || error || xfixes_query->major_version < 2) + { + qDebug()<<"XFixes query failed"; + free(error); + } + else + { + //we'll recieve sel owner events for primary amd clipboard + mask = XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER; + xcb_xfixes_select_selection_input_checked(con,clipWinId, XCB_ATOM_PRIMARY, mask); + xcb_xfixes_select_selection_input_checked(con, clipWinId, atom("CLIPBOARD"), mask); + } + free(xfixes_query); + } + xcb_flush(con); + QTimer::singleShot(250, this, SLOT(checkEvents())); +} + + +xcb_atom_t XCBClip::atom(const QString& name) +{ + //get atom for the name, return 0 if not found + xcb_intern_atom_cookie_t cookie; + xcb_intern_atom_reply_t *reply; + xcb_atom_t a=0; + + cookie = xcb_intern_atom(con, 0, name.length(), name.toLatin1().data()); + if ((reply = xcb_intern_atom_reply(con, cookie, NULL))) + { +// qDebug()<<"found atom for "<<name; + a=reply->atom; + free(reply); + } + return a; +} + +bool XCBClip::is_image_atom(xcb_atom_t at) +{ + //check if selection data is image + if(!at) + return false; + if( at == atom("image/png") || + at == atom("image/xpm") || + at == atom("image/jpg") || + at == atom("image/jpeg") || + at == atom("PIXMAP") || + at == atom("image/bmp")) + return true; + return false; +} + +bool XCBClip::is_string_atom(xcb_atom_t at) +{ + //check if selection data is string/text + if(!at) + return false; + if( at == atom("UTF8_STRING") || + at == atom("STRING") || + at == atom("TEXT") || + at == atom("text/plain;charset=utf-8") || + at == atom("text/plain")) + return true; + return false; +} + +QString XCBClip::atom_name(xcb_atom_t xatom) +{ + QString name; + xcb_get_atom_name_cookie_t cookie = xcb_get_atom_name(con, xatom); + xcb_get_atom_name_reply_t *reply=xcb_get_atom_name_reply(con, cookie, NULL); + + if(!reply) + return name; + if(!reply->name_len) + { + free(reply); + return name; + } + name=QString(QByteArray(xcb_get_atom_name_name(reply), xcb_get_atom_name_name_length(reply))); + free(reply); + return name; +} + +QStringList XCBClip::atomsInReply(xcb_get_property_reply_t *reply) +{ + QStringList atoms; + xcb_atom_t* tg=(xcb_atom_t*) xcb_get_property_value(reply); + for(uint i=0;i<xcb_get_property_value_length(reply)/sizeof(xcb_atom_t);++i) + { + atoms << atom_name(tg[i]); + } + return atoms; +} + +xcb_atom_t XCBClip::best_atom_from_list(const QStringList& list) +{ + //here we chose the best of supported formats for selection + xcb_atom_t a; + if((a=target_has_atom(list, "UTF8_STRING"))) + { + return a; + } + + if((a=target_has_atom(list, "text/plain;charset=utf-8"))) + { + return a; + } + + if((a=target_has_atom(list, "STRING"))) + { + return a; + } + if((a=target_has_atom(list, "TEXT"))) + { + return a; + } + if((a=target_has_atom(list, "text/plain"))) + { + return a; + } + + //Server supports only PNG and JPEG formats + + if((a=target_has_atom(list, "image/png"))) + { + return a; + } +/* if((a=target_has_atom(list, "image/xpm"))) + { + return a; + } + if((a=target_has_atom(list, "PIXMAP"))) + { + return a; + } + if((a=target_has_atom(list, "image/bmp"))) + { + return a; + }*/ + if((a=target_has_atom(list, "image/jpg"))) + { + return a; + } + if((a=target_has_atom(list, "image/jpeg"))) + { + return a; + } + return 0; +} + +xcb_atom_t XCBClip::target_has_atom(const QStringList& atoms, const QString& name) +{ + foreach (const QString& xatom, atoms) + { + if(xatom==name) + { + return atom(name); + } + } + return 0; +} + +void XCBClip::checkEvents() +{ + xcb_generic_event_t *e=xcb_poll_for_event(con); + if(!e) + { + //don't have events let*s check again in 100msec + QTimer::singleShot(100, this, SLOT(checkEvents())); + return; + } + + uint response_type=e->response_type & ~0x80; + + //we notified that selection is changed in primary or clipboard + if (response_type == reply->first_event + XCB_XFIXES_SELECTION_NOTIFY) + { + xcb_xfixes_selection_notify_event_t *notify_event=(xcb_xfixes_selection_notify_event_t *)e; +// qDebug()<<"SEL OWNER notify, selection:"<<notify_event->selection<< " window "<< notify_event->window<< "owner"<< notify_event->owner; + if(notify_event->owner == clipWinId) + { +// qDebug()<<"not processing, we are the owner of this notify"; + } + else + { + //cancel all previous incr reading + incrementalSize=incrementalSizeRead=0; + incrAtom=0; + if(notify_event->selection==XCB_ATOM_PRIMARY) + { + owner[0]=false; + } + else + { + owner[1]=false; + } + //get supported mime types + request_selection_data( notify_event->selection, atom( "TARGETS"), atom( "TARGETS"), 0); + } + } + else + { + //we notified that property is changed + if (response_type == XCB_PROPERTY_NOTIFY) + { + process_property_notify(e); + } + //we got reply to our selection request (mime types or data) + else if (response_type == XCB_SELECTION_NOTIFY) + { + process_selection_notify(e); + } + else if (response_type == XCB_SELECTION_REQUEST) + { + process_selection_request(e); + } + else + { +// qDebug()<<"not processing this event "<<response_type; + } + } + + free(e); + + //return to the main eventsloop before processing new events, selection has less priority than other events + QTimer::singleShot(10, this, SLOT(checkEvents())); +} + + + + +void XCBClip::process_selection_notify(xcb_generic_event_t *e) +{ + xcb_selection_notify_event_t *sel_event; + +// qDebug()<<"selection notify"; + sel_event=(xcb_selection_notify_event_t *)e; + + //processing the event which is reply for convert selection call + + + if (sel_event->requestor != clipWinId) + { +// qDebug()<<("not our window"); + return; + } + else + { +// qDebug()<<"selection notify sel , target , property "<< sel_event->selection<< sel_event->target<< sel_event->property; + if(sel_event->property==XCB_NONE) + { + qDebug()<<( "NO SELECTION"); + } + else + { + + currentSelection=sel_event->selection; + //read property + read_selection_property(currentSelection, sel_event->property); + } + } +} + + +void XCBClip::process_property_notify(xcb_generic_event_t *e) +{ + xcb_property_notify_event_t *pn; + +// qDebug()<<("property notify"); + + pn = (xcb_property_notify_event_t *)e; + if (pn->window != clipWinId) + { +// qDebug()<<("not our window"); + return; + } +// qDebug()<<"property, state "<< pn->atom<< pn->state; + if(pn->state==XCB_PROPERTY_NEW_VALUE) + { + if(incrAtom==pn->atom && incrementalSize) + { + //we recieveing the selection data incrementally, let's read a next chunk +// qDebug()<<"reading incr property "<< pn->atom; + read_selection_property(currentSelection, pn->atom); + } + } +} + +void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) +{ + QString stype, sprop; + xcb_atom_t data_atom; + unsigned int bytes_left, bytes_read=0; + xcb_get_property_cookie_t cookie; + xcb_get_property_reply_t *reply; + OutputChunk* chunk; + + + //request property which represents value of selection (data or mime types) + //get max 100K of data, we don't need to send more than that over network for perfomance reasons + cookie= xcb_get_property(con,false, clipWinId, property, XCB_GET_PROPERTY_TYPE_ANY, 0, max_chunk()); + reply=xcb_get_property_reply(con, cookie, NULL); + if(!reply) + { + qDebug()<<( "NULL reply"); + } + else + { + if(reply->type==XCB_NONE) + { + qDebug()<<( "NONE reply"); + } + else + { + //here we have type of data + stype=atom_name(reply->type); + sprop=atom_name(property); + +// qDebug()<< "Property, type, format, length"<< sprop<< stype<< reply->format<< reply->length; + //need to read property incrementally + if(reply->type == atom("INCR")) + { + unsigned int sz=*((unsigned int*) xcb_get_property_value(reply)); +// qDebug()<< "have incr property size: "<< sz; + incrAtom=property; + incrementalSize=sz; + incrementalSizeRead=0; + + //deleteing property should tell the selection owner that we are ready for incremental reading of data + xcb_delete_property(con, clipWinId, property); + xcb_flush(con); + free(reply); + return; + } + //we have supported mime types in reply + if(reply->type == atom( "ATOM")) + { + if(reply->format!=32) + { + qDebug()<<( "wrong format for TARGETS"); + } + else + { + QStringList atoms=atomsInReply(reply); +// qDebug() << "target supports mime types:"<<atoms; + + if(parent->serverSupportsExtSelection()) + { + //servere support extended selection, we'll send the selection data first when it's requested by the server + //now we'll just notify the server that there is the new selection and send supported mime types + } + else + { + data_atom=0; + //get the best of supported mime types and request the selection in this format + data_atom=best_atom_from_list(atoms); + + xcb_delete_property( con, clipWinId, property); + xcb_flush(con); + + //request the data from selection + if(data_atom) + request_selection_data( selection, data_atom, data_atom, 0); + else + { + qDebug()<<( "there are no supported mime types in the target"); + } + } + } + } + else + { + //here we have selection as string or image + if(is_image_atom( reply->type) || is_string_atom( reply->type)) + { + //read property data in loop in the chunks with size (100KB) + do + { + bytes_left=reply->bytes_after; + //now we can access property data + + /*FILE* cp=fopen("/tmp/clip", "a"); + * fwrite(xcb_get_property_value(reply),1, xcb_get_property_value_length(reply),cp); + * fclose(cp);*/ + + + + SelectionType sel= CLIPBOARD; + if(selection==XCB_ATOM_PRIMARY) + sel=PRIMARY; + + SelectionMime mime= UTF_STRING; + if(is_image_atom(reply->type)) + mime=PIXMAP; + + chunk=new OutputChunk(sel, mime); + + + if(xcb_get_property_value_length(reply)) + { + chunk->data.setRawData((const char*) xcb_get_property_value(reply),xcb_get_property_value_length(reply)); + } + + chunk->compressed=false; + if(is_string_atom(property)) + chunk->mimeData=UTF_STRING; + else + chunk->mimeData=PIXMAP; + + if(selection == XCB_ATOM_PRIMARY) + { + chunk->selection=PRIMARY; + } + else + chunk->selection=CLIPBOARD; + + + if(incrementalSize && (incrAtom==property)) + { + //we are doing incremental reading + if(incrementalSizeRead == 0) + { + //it's the first chunk + chunk->firstChunk=true; + } + incrementalSizeRead+=xcb_get_property_value_length(reply); + if(!bytes_left && ! bytes_read && !xcb_get_property_value_length(reply)) + { + //we got the property with 0 size it means that we recieved all data of incr property +// qDebug()<<"INCR Property done, read " << incrementalSizeRead; + incrAtom=0; + incrementalSize=0; + //it's the last chunk + chunk->lastChunk=true; + } + chunk->totalSize=incrementalSize; + } + else + { + //we are doing simple read + if(bytes_read==0) + { + //it's the first chunk + chunk->firstChunk=true; + } + if(bytes_left==0) + { + //the last chunk + chunk->lastChunk=true; + } + chunk->totalSize=xcb_get_property_value_length(reply)+bytes_left; + } + + bytes_read+=xcb_get_property_value_length(reply); +// qDebug()<<"read chunk of selection - size, total read, left, first:, last:"<< xcb_get_property_value_length(reply)<< bytes_read<< bytes_left<< chunk->firstChunk<< chunk->lastChunk; + + //attach chunk to the end of output chunk queue + parent-> addToSelectionOutput(chunk); + + //send this chunk + parent->sendOutputSelChunk(); + + if(bytes_left) + { + free(reply); + cookie= xcb_get_property(con, 0, clipWinId, property, XCB_GET_PROPERTY_TYPE_ANY, bytes_read/4,max_chunk()); + reply=xcb_get_property_reply(con, cookie, NULL); + if(!reply) + { + //something is wrong + qDebug()<<("NULL reply"); + break; + } + } + //read in loop till no data left + }while(bytes_left); + } + else + { + stype=atom_name(reply->type); + qDebug()<<"Not supported mime type: "<<stype << reply->type; + } + } + if(reply) + free(reply); + //if reading incr property this will say sel owner that we are ready for the next chunk of data + xcb_delete_property(con, clipWinId, property); + xcb_flush(con); + } + } +} + +SelectionType XCBClip::selection_from_atom(xcb_atom_t selection) +{ + if(selection == XCB_ATOM_PRIMARY) + return PRIMARY; + return CLIPBOARD; + +} + +void XCBClip::request_selection_data( xcb_atom_t selection, xcb_atom_t target, xcb_atom_t property, xcb_timestamp_t t) +{ + //execute convert selection for primary or clipboard to get mimetypes or data (depends on target atom) + if(!t) + t=XCB_CURRENT_TIME; + if(property) + { + xcb_delete_property(con,clipWinId,property); + } + xcb_convert_selection(con, clipWinId,selection, target, property, t); + xcb_flush(con); +} + + +void XCBClip::setInputSelectionData(SelectionType selection, SelectionMime mime, bool firstChunk, bool lastChunk, bool compressed, uint size, char* data) +{ + //copy data to selection buffer +// qDebug()<<"Get chunk of input selection: selection, myme, firstChunk, lastChunk, compressed, size:"<<selection<<mime<<firstChunk<<lastChunk<<compressed<<size; + + + if(firstChunk) + { + selData[selection].clear(); + selMime[selection]=mime; + } + + selData[selection].append(data,size); + + if(lastChunk) + { + own_selection(selection); + } +} + +void XCBClip::own_selection(SelectionType selection) +{ + xcb_atom_t sel=XCB_ATOM_PRIMARY; + if(selection!=PRIMARY) + { + sel=atom("CLIPBOARD"); + } + xcb_set_selection_owner(con, clipWinId, sel, XCB_CURRENT_TIME); + xcb_flush(con); + owner[selection]=true; + timestamp[selection]=XCB_CURRENT_TIME; +} + +void XCBClip::process_selection_request(xcb_generic_event_t *e) +{ + xcb_selection_request_event_t *req=(xcb_selection_request_event_t*)e; + + xcb_selection_notify_event_t* event= (xcb_selection_notify_event_t*)calloc(32, 1); + event->response_type = XCB_SELECTION_NOTIFY; + event->requestor = req->requestor; + event->selection = req->selection; + event->target = req->target; + event->property = XCB_NONE; + event->time = req->time; + + xcb_atom_t property=req->property; + xcb_atom_t target=req->target; + + if(property == XCB_NONE) + property=target; + + SelectionType sel=selection_from_atom(req->selection); + +// qDebug()<<"selection request for"<<atom_name(req->selection)<<atom_name(req->target)<<atom_name(req->property); + if(!owner[sel]) + { + //we don't own this selection + qDebug()<<"not our selection"; + xcb_send_event(con, false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (char*)event); + xcb_flush(con); + free(event); + return; + } + + if(timestamp[sel] > req->time) + { + //selection changed after request + qDebug()<<"requested selection doesn't exist anymore"; + xcb_send_event(con, false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (char*)event); + xcb_flush(con); + free(event); + return; + } + + + if(req->target==atom("TIMESTAMP")) + { + event->property=property; +// qDebug()<<"requested TIMESTAMP"; + xcb_change_property(con, XCB_PROP_MODE_REPLACE, req->requestor, + property, XCB_ATOM_INTEGER, 32, 1, ×tamp[sel]); + + } + else if(req->target==atom("TARGETS")) + { + event->property=property; +// qDebug()<<"requested TARGETS"; + send_mime_types(req); + } + else + { + event->property=send_data(req); + } + xcb_send_event(con, false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (char*)event); + xcb_flush(con); + free(event); +} + +QString XCBClip::mime_to_QT_img(const QString& mimeType) +{ + //convert mimeType to internal QT image format: + //https://doc.qt.io/qt-5/qimage.html#reading-and-writing-image-files + QString f=mimeType; + if(mimeType=="PIXMAP") + return "XPM"; + f=f.replace("image/","").toUpper(); + return f; +} + + +xcb_atom_t XCBClip::set_data_property(xcb_selection_request_event_t* req, QByteArray* data) +{ + + //set data to window property + + //change when implemented + bool support_incr=false; + //this types of application not supporting incr selection + if(atom_name(req->property)=="_XT_SELECTION_0" || atom_name(req->property)=="_QT_SELECTION") + { + qDebug()<<atom_name(req->property)<<"doesn't support INCR"; + support_incr=false; + } +// qDebug()<<"check if data size < "<<xcb_get_maximum_request_length(con) * 4 - 24; + + //check if we are sending incr + if(data->size() < (int)xcb_get_maximum_request_length(con) * 4 - 24) + { +// qDebug()<<"sending "<<data->size()<<atom_name(req->property)<<atom_name(req->target); + + xcb_change_property(con, XCB_PROP_MODE_REPLACE, req->requestor, req->property, req->target, + 8, data->size(), (const void *)data->constData()); + + xcb_flush(con); + return req->property; + } + + qDebug()<<"data is too big"; + return XCB_NONE; +} + + +xcb_atom_t XCBClip::send_data(xcb_selection_request_event_t* req) +{ + + //send data + SelectionType sel=selection_from_atom(req->selection); + + if(selMime[sel]==UTF_STRING) + { + //if it's one of supported text formats send without convertion + if(is_string_atom(req->target)) + { +// qDebug()<<"sending UTF text"; + return set_data_property(req, &selData[sel]); + } + else + { + qDebug()<<"unsupported property requested:"<<atom_name(req->target); + return XCB_NONE; + } + } + else + { + if(!is_image_atom(req->target)) + { + qDebug()<<"unsupported property requested:"<<atom_name(req->target); + return XCB_NONE; + } +// qDebug()<<"sending "<<atom_name(req->target); + //convert to desireable format + QImage img=QImage::fromData(selData[sel]); + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + img.save(&buffer, mime_to_QT_img(atom_name(req->target)).toLatin1()); +// qDebug()<<"converted to"<<mime_to_QT_img(atom_name(req->target))<<ba.size(); + return set_data_property(req, &ba); + } + return XCB_NONE; +} + +void XCBClip::send_mime_types(xcb_selection_request_event_t* req) +{ + //send supported targets + SelectionType sel=selection_from_atom(req->selection); + QVector<xcb_atom_t> targets; + xcb_atom_t a; + + if((a=atom("TARGETS"))) + targets.append(a); + if((a=atom("TIMESTAMP"))) + targets.append(a); + + if(selMime[sel]==PIXMAP) + { + if((a=atom("image/png"))) + targets.append(a); + if((a=atom("image/jpg"))) + targets.append(a); + if((a=atom("image/jpeg"))) + targets.append(a); + if((a=atom("image/bmp"))) + targets.append(a); + if((a=atom("image/xpm"))) + targets.append(a); + } + else + { + if((a=atom("UTF8_STRING"))) + targets.append(a); + if((a=atom("text/plain;charset=utf-8"))) + targets.append(a); + if((a=atom("STRING"))) + targets.append(a); + if((a=atom("TEXT"))) + targets.append(a); + if((a=atom("text/plain"))) + targets.append(a); + } + + xcb_change_property(con, XCB_PROP_MODE_REPLACE, req->requestor, req->property, XCB_ATOM_ATOM, + 32, targets.size(), (const void *)targets.constData()); + xcb_flush(con); + +} + +uint XCBClip::max_chunk() +{ + if(parent->serverSupportsExtSelection()) + return 1024*100/4; //100KB + else + return 10*1024*1024/4; //10MB +} diff --git a/xcbclip.h b/xcbclip.h new file mode 100644 index 0000000..c45fc91 --- /dev/null +++ b/xcbclip.h @@ -0,0 +1,79 @@ +/* + * QT Client for X2GoKDrive + * Copyright (C) 2018 Oleksandr Shneyder <o.shneyder@phoca-gmbh.de> + * Copyright (C) 2018 phoca-GmbH + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef XCBCLIP_H +#define XCBCLIP_H +#include <QMainWindow> +#include <QClipboard> + +#include <xcb/xcb.h> +#include <xcb/xfixes.h> +#include <QThread> + +#include "client.h" + +class XCBClip : public QObject +{ + Q_OBJECT +public: + XCBClip(Client* parent); + void setInputSelectionData(SelectionType selection, SelectionMime mime, bool firstChunk, bool lastChunk, bool compressed, uint size, char* data); + +private: + + uint max_chunk(void); + xcb_atom_t atom(const QString& name); + QString atom_name(xcb_atom_t xatom); + QStringList atomsInReply(xcb_get_property_reply_t *reply); + xcb_atom_t best_atom_from_list(const QStringList& atoms); + void process_selection_notify(xcb_generic_event_t *e); + void process_selection_request(xcb_generic_event_t *e); + void request_selection_data( xcb_atom_t selection, xcb_atom_t target, xcb_atom_t property, xcb_timestamp_t t); + void process_property_notify(xcb_generic_event_t *e); + void read_selection_property(xcb_atom_t selection, xcb_atom_t property); + bool is_string_atom( xcb_atom_t at); + bool is_image_atom( xcb_atom_t at); + xcb_atom_t target_has_atom(const QStringList& atoms, const QString& name); + void own_selection(SelectionType selection); + void send_mime_types(xcb_selection_request_event_t* req); + xcb_atom_t send_data(xcb_selection_request_event_t* req); + xcb_atom_t set_data_property(xcb_selection_request_event_t* req, QByteArray* data); + SelectionType selection_from_atom(xcb_atom_t selection); + QString mime_to_QT_img(const QString& mimeType); + +private slots: + void checkEvents(); +private: + Client* parent; + const xcb_query_extension_reply_t *reply; + xcb_window_t clipWinId; + xcb_connection_t* con; +//output selection + xcb_atom_t currentSelection; + xcb_atom_t incrAtom=0; + uint incrementalSize=0; + uint incrementalSizeRead=0; +//input selection + SelectionMime selMime[2]; + QByteArray selData[2]; + bool owner[2]={0}; + xcb_timestamp_t timestamp[2]; +}; +#endif -- Alioth's /home/x2go-admin/maintenancescripts/git/hooks/post-receive-email on /srv/git/code.x2go.org/x2gokdriveclient.git