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