This is an automated email from the git hooks/post-receive script. x2go pushed a commit to branch master in repository x2gokdriveclient. commit fa6415964959d733e3d32d6096b4a52f65d50b60 Author: Oleksandr Shneyder <o.shneyder@phoca-gmbh.de> Date: Wed Sep 30 11:36:36 2020 -0500 support sending and recieving selections on demand. Support reading and writing INCR properties. --- client.cpp | 299 +++++++++++++++++++++++++++--------- client.h | 37 +++-- debian/changelog | 1 + xcbclip.cpp | 457 ++++++++++++++++++++++++++++++++++++++++++++++--------- xcbclip.h | 59 ++++++- 5 files changed, 691 insertions(+), 162 deletions(-) diff --git a/client.cpp b/client.cpp index e5fd37e..370b104 100644 --- a/client.cpp +++ b/client.cpp @@ -113,7 +113,7 @@ OutputChunk::OutputChunk(SelectionType selection, SelectionMime mime) { this->selection=selection; mimeData=mime; - compressed=false; + totalSize=0; firstChunk=lastChunk=false; } @@ -801,9 +801,26 @@ void Client::setCursor() void Client::getServerversion() { serverVersion=*((uint16_t*)messageBuffer+2); + serverExtSelection = (serverVersion>1); + qDebug()<<"server version:"<<serverVersion; } +void Client::getClientSelection() +{ + uint16_t sel=*((uint16_t*)messageBuffer+2); + SelectionType selection=PRIMARY; + if(sel) + selection=CLIPBOARD; + qDebug()<<"server demands data for "<<selection; +#ifdef Q_OS_LINUX + clipboard->requestSelectionData(selection); +#else + sendSelectionToServer(selection); +#endif + +} + void Client::getCursor() @@ -843,40 +860,72 @@ 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) + setInputSelectionData(selectionClipboard, selectionFormat, firstChunk, lastChunk, compressed_size, selectionSize, messageBuffer); +#else + clipboard->setInputSelectionData(selectionClipboard, selectionFormat, firstChunk, lastChunk, compressed_size, selectionSize, messageBuffer); +#endif + freeMessageBuffer(); +} + + +#ifndef Q_OS_LINUX +void Client::setInputSelectionData(SelectionType, SelectionMime mime, bool firstChunk, bool lastChunk, uint32_t compressed, uint size, char* data, bool notify) +{ + //if notify is true, we don't have actual data, just notification + //copy data to selection buffer + // qDebug()<<"Get chunk of input selection: selection, myme, firstChunk, lastChunk, compressed, size:"<<selection<<mime<<firstChunk<<lastChunk<<compressed<<size<<notify; + + + if(firstChunk) { - mode=QClipboard::Selection; - qDebug()<<"Got new Primary selection, format:"<<selectionFormat; + selData.clear(); + selMime=mime; + total_compressed=0; } + + if(!compressed) + selData.append(data,size); else - qDebug()<<"Got new Clipboard, format:"<<selectionFormat; + { + QByteArray ba; + ba.append((char*) &size, 4); + ba.append(data, compressed); - QClipboard* clipboard=QGuiApplication::clipboard(); + total_compressed+=compressed; + selData.append(qUncompress(ba)); + // qDebug()<<"uncompress from "<<compressed<<" to "<<size; + } -// qDebug()<<messageBuffer; - switch(selectionFormat) + if(lastChunk ) { - case STRING: clipboard->setText(QString::fromLocal8Bit(messageBuffer, selectionSize), mode); break; - case UTF_STRING: clipboard->setText(QString::fromUtf8(messageBuffer, selectionSize), mode);break; - case PIXMAP: + if(notify) { - QPixmap pix; - pix.loadFromData((const uchar*)messageBuffer, selectionSize); -// qDebug()<<pix; - clipboard->setImage(pix.toImage(), mode); - break; + qDebug()<<"Got selection notify from server"; + } + else + { +// qDebug()<<"total size: "<<selData.size()<<"compressed size"<<total_compressed; + QClipboard* clipboard=QGuiApplication::clipboard(); + QClipboard::Mode mode=QClipboard::Clipboard; + switch(selectionFormat) + { + case STRING: clipboard->setText(QString::fromLocal8Bit(selData), mode); break; + case UTF_STRING: clipboard->setText(QString::fromUtf8(selData), mode);break; + case PIXMAP: + { + QPixmap pix; + pix.loadFromData(selData); + clipboard->setImage(pix.toImage(), mode); + break; + } + } } } -#else - clipboard->setInputSelectionData(selectionClipboard, selectionFormat, firstChunk, lastChunk, compressed, selectionSize, messageBuffer); -#endif - freeMessageBuffer(); } +#endif void Client::getDeletedCursorsList() { @@ -951,20 +1000,44 @@ void Client::getSelection() { firstChunk=*((uint32_t*)messageBuffer+4); lastChunk=*((uint32_t*)messageBuffer+5); - compressed=*((uint32_t*)messageBuffer+6); + compressed_size=*((uint32_t*)messageBuffer+6); + selectionTotalSize=*((uint32_t*)messageBuffer+7); + //if we are supporting selection on demand, check if it's not selection notify event + if(serverSupportsExtSelection() && firstChunk && lastChunk && (selectionSize == 0 ) && (selectionTotalSize == 0)) + { + //set input selection with size 0 and notify true. Clipboard will know that we have a notification +#ifdef Q_OS_LINUX + clipboard->setInputSelectionData(selectionClipboard, selectionFormat, true, true, 0, 0, 0, true); +#else + setInputSelectionData(selectionClipboard, selectionFormat, true, true, 0, 0, 0, true); +#endif + } + else if(serverSupportsExtSelection() && lastChunk && (selectionSize==0)) + { + //it's last chunk of incr selection with size 0 +#ifdef Q_OS_LINUX + clipboard->setInputSelectionData(selectionClipboard, selectionFormat, firstChunk, lastChunk, 0, 0, 0); +#else + setInputSelectionData(selectionClipboard, selectionFormat, firstChunk, lastChunk, 0, 0, 0); +#endif + } } else { //if server doesn't support extended selection it'll send data in one chunk, uncompressed firstChunk=true; lastChunk=true; - compressed=false; + compressed_size=0; } currentDataType=SELECTIONBUFFER; - bytesLeftToRead=selectionSize; - qDebug()<<"Get Selection, is Clipboard"<<selectionClipboard<<selectionFormat<<selectionSize; + //if data is compressed, read the "compressed_size" bytes + if(compressed_size) + bytesLeftToRead=compressed_size; + else + bytesLeftToRead=selectionSize; + // qDebug()<<"Get Selection, is Clipboard"<<selectionClipboard<<selectionFormat<<"chunk size"<<selectionSize<<"left"<<bytesLeftToRead; freeMessageBuffer(); } @@ -1085,7 +1158,7 @@ void Client::readDataHeader() } case SELECTION: { - qDebug()<<"Get Selection"; +// qDebug()<<"Get Selection"; getSelection(); break; } @@ -1094,6 +1167,11 @@ void Client::readDataHeader() getServerversion(); break; } + case DEMANDCLIENTSELECTION: + { + getClientSelection(); + break; + } default: { qDebug()<<"Unsupported header type: "<<data_type; @@ -1364,7 +1442,7 @@ void Client::sendClientVersion() version=FEATURE_VERSION; os=OS_LINUX; #ifdef Q_OS_WIN - os=OS_WIN; + os=OS_WINDOWS; #endif #ifdef Q_OS_DARWIN os=OS_DARWIN @@ -1377,6 +1455,19 @@ void Client::sendClientVersion() sendEvent(evmsg); } +//requesting on demand selection +void Client::requestSelectionFromServer(SelectionType sel) +{ + //sending the feature vesrion and OS version to the server + char evmsg[EVLENGTH]{}; + uint16_t selection=sel; + uint32_t etype; + etype=DEMANDSELECTION; + memcpy(evmsg,(char*)&etype,4); + memcpy(evmsg+4,(char*)&selection,2); + sendEvent(evmsg); +} + void Client::geometryChanged() { @@ -1467,6 +1558,19 @@ void Client::setUseRandr(bool use) } } +void Client::send_selnotify_to_server(SelectionType selection, SelectionMime mime) +{ + OutputChunk* chunk=new OutputChunk(selection, mime); + chunk->firstChunk=chunk->lastChunk=true; + + //attach empty chunk to the end of output chunk queue + addToSelectionOutput(chunk); + //send this chunk + sendOutputSelChunk(); + +} + + #ifndef Q_OS_LINUX void Client::slotSelectionChanged(QClipboard::Mode mode) { @@ -1481,42 +1585,78 @@ void Client::slotSelectionChanged(QClipboard::Mode 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()) + SelectionType destination=PRIMARY; + if(mode== QClipboard::Clipboard) + destination=CLIPBOARD; + + SelectionMime mime; + + + if(mimeData->hasImage()) + { + qDebug()<<"Have new Image"; + mime=PIXMAP; + } + else if(mimeData->hasText()) + { + qDebug()<<"Have new Text"; + mime=UTF_STRING; + } + else { + qDebug()<<"Unsupported MIME type in clipboard"; + return; + } + if(serverSupportsExtSelection()) + { + //send notify to server + send_selnotify_to_server(destination,mime); } else { - //server doesn't support ext selection we are sending clipboard data + //send data to server + sendSelectionToServer(destination); } - QByteArray data; - //add size/mime/type of buffer and start copy data - char evmsg[EVLENGTH]{}; +} - uint32_t etype=SELECTIONEVENT; - uint32_t size; - uint8_t destination=PRIMARY; - if(mode== QClipboard::Clipboard) - destination=CLIPBOARD; +void Client::sendSelectionToServer(SelectionType selection) +{ + //sending selection data to server + if(!connected) + return; + const QClipboard *clipboard = QGuiApplication::clipboard(); + + QClipboard::Mode mode; + if(selection==PRIMARY) + mode= QClipboard::Selection; + else + mode =QClipboard::Clipboard; + + if(mode == QClipboard::Clipboard && clipboard->ownsClipboard()) + return; + if(mode == QClipboard::Selection && clipboard->ownsSelection()) + return; - uint8_t mime; + const QMimeData *mimeData = clipboard->mimeData(mode); + SelectionMime mime; + QByteArray data; if(mimeData->hasImage()) { QBuffer buffer(&data); buffer.open(QIODevice::WriteOnly); QPixmap pix=clipboard->pixmap(mode); - pix.save(&buffer, "JPG"); - qDebug()<<"Have new Image:"<<pix<<data.size(); + pix.save(&buffer, "PNG"); + qDebug()<<"Selection image size"<<pix<<data.size(); mime=PIXMAP; } else if(mimeData->hasText()) { data=clipboard->text(mode).toUtf8(); - qDebug()<<"Have new Text:"<<data.size(); + qDebug()<<"Selection Text"<<data.size(); mime=UTF_STRING; } else @@ -1524,30 +1664,23 @@ void Client::slotSelectionChanged(QClipboard::Mode mode) qDebug()<<"Unsupported MIME type in clipboard"; return; } - size=data.size(); - if(!size) + + if(!data.size()) { - qDebug()<<"Clipboard has no data"; + qDebug()<<"no data"; return; } - memcpy(evmsg,(char*)&etype,4); - memcpy(evmsg+4,(char*)&size,4); - memcpy(evmsg+8,(char*)&destination,1); - memcpy(evmsg+9,(char*)&mime,1); - - qDebug()<<"SEND SELECTION"<<size<<destination<<mime; - uint32_t sentData=(size < EVLENGTH-10)?size:EVLENGTH-10; - memcpy(evmsg+10,data.data(),sentData); - sendEvent(evmsg); - while(sentData<size) - { - int chunk=(size-sentData < EVLENGTH)?size-sentData:EVLENGTH; - memcpy(evmsg, data.data()+sentData, chunk); - sentData+=chunk; - sendEvent(evmsg); - } - qDebug()<<"sent: "<<sentData<<"from"<<size; + OutputChunk* chunk; + chunk=new OutputChunk(selection, mime); + chunk->totalSize=data.size(); + chunk->data=data; + chunk->mimeData=mime; + chunk->selection=selection; + chunk->firstChunk=true; + chunk->lastChunk=true; + addToSelectionOutput(chunk); + sendOutputSelChunk(); } #endif @@ -1558,7 +1691,7 @@ void Client::sendOutputSelChunk() if(outputSelectionQueue.isEmpty()) return; - OutputChunk* chunk=outputSelectionQueue.takeLast(); + OutputChunk* chunk=outputSelectionQueue.takeFirst(); if(!serverSupportsExtSelection() && (!chunk->firstChunk || !chunk->lastChunk)) { //selection has multiply chunks, but this server doesn't support ext selection, not sending anything @@ -1574,7 +1707,7 @@ void Client::sendOutputSelChunk() uint8_t mime=chunk->mimeData; uint8_t firstChunk=chunk->firstChunk; uint8_t lastChunk=chunk->lastChunk; - uint8_t compressed=chunk->compressed; + uint32_t compressed_size=0; uint32_t totalSize=chunk->totalSize; size=chunk->data.size(); @@ -1587,24 +1720,41 @@ void Client::sendOutputSelChunk() //if server supports extended selection, sending extended header if(serverSupportsExtSelection()) { + //if server supports it compress the big string data + if(chunk->mimeData==UTF_STRING && serverSupportsExtSelection() && size >1024) + { + chunk->data=qCompress(chunk->data); + //Qt puting uncompressed size of data in the first 4 bytes of buffer, we won't send them + compressed_size=chunk->data.size()-4; + } memcpy(evmsg+10,(char*)&firstChunk,1); memcpy(evmsg+11,(char*)&lastChunk,1); - memcpy(evmsg+12,(char*)&compressed,1); - memcpy(evmsg+13,(char*)&totalSize,4); + memcpy(evmsg+12,(char*)&compressed_size,4); + memcpy(evmsg+16,(char*)&totalSize,4); +// qDebug()<<"size of chunk: "<<size<<" compressed: "<<compressed_size<<"total: "<<totalSize; } uint headerSize=10; if(serverSupportsExtSelection()) - headerSize=17; + headerSize=20; // qDebug()<<"SEND SELECTION"<<size<<destination<<mime; + char* data_ptr=chunk->data.data(); + + if(compressed_size) + { + //sending data compressed + data_ptr+=4;//don't send first 4 bytes + size=compressed_size; + } + uint32_t sentData=(size < EVLENGTH-headerSize)?size:EVLENGTH-headerSize; - memcpy(evmsg+headerSize,chunk->data.data(),sentData); + memcpy(evmsg+headerSize,data_ptr,sentData); sendEvent(evmsg); while(sentData<size) { int msg_length=(size-sentData < EVLENGTH)?size-sentData:EVLENGTH; - memcpy(evmsg, chunk->data.data()+sentData, msg_length); + memcpy(evmsg, data_ptr+sentData, msg_length); sentData+=msg_length; sendEvent(evmsg); } @@ -1616,3 +1766,12 @@ void Client::addToSelectionOutput(OutputChunk* chunk) { outputSelectionQueue.append(chunk); } + +int Client::max_chunk() +{ + if(serverSupportsExtSelection()) + { + return 1024*256/4; //256KB + } + return 10*1024*1024/4; //10MB +} diff --git a/client.h b/client.h index 52d1cbf..a947653 100644 --- a/client.h +++ b/client.h @@ -24,8 +24,8 @@ //FEATURE_VERSION is not cooresponding to actual version of client //it used to tell server which features are supported by client -//Changes 0 - 1: sending and recieving client and OS version -#define FEATURE_VERSION 1 +//Changes 1 - 2: supporting extended selection and sending selection on demand +#define FEATURE_VERSION 2 //Version of client OS for same reason enum OS_VERSION{OS_LINUX, OS_WINDOWS, OS_DARWIN}; @@ -58,6 +58,7 @@ enum OS_VERSION{OS_LINUX, OS_WINDOWS, OS_DARWIN}; #define UPDATE 8 #define SELECTIONEVENT 9 #define CLIENTVERSION 10 +#define DEMANDSELECTION 11 #define ShiftMask (1<<0) #define LockMask (1<<1) @@ -134,10 +135,9 @@ public: OutputChunk(SelectionType selection, SelectionMime mimeType); QByteArray data; enum SelectionMime mimeData; - bool compressed; - bool firstChunk; - bool lastChunk; - uint totalSize; + bool firstChunk=false; + bool lastChunk=false; + uint32_t totalSize=0; enum SelectionType selection; }; @@ -167,6 +167,11 @@ public: void addToSelectionOutput(OutputChunk* chunk); bool serverSupportsExtSelection(){return serverExtSelection;} ClipboardMode clipboardMode(){return clipMode;} + void requestSelectionFromServer(SelectionType sel); + void send_selnotify_to_server(SelectionType selection, SelectionMime mime); + int max_chunk(); + +public slots: void sendOutputSelChunk(); @@ -203,9 +208,10 @@ public slots: private: enum{ HEADER, FRAMEREGION, REGIONDATA ,CURSORDATA, CURSORLIST, FRAMELIST, SELECTIONBUFFER } currentDataType; - enum HeaderType{ FRAME, DELETEDFRAMES, CURSOR, DELETEDCURSORS, SELECTION, SERVER_VERSION}; + enum HeaderType{ FRAME, DELETEDFRAMES, CURSOR, DELETEDCURSORS, SELECTION, SERVER_VERSION, DEMANDCLIENTSELECTION}; void getServerversion(); + void getClientSelection(); void setUseRandr(bool use); void exitOnError(const QString& message); void getImageFrame(); @@ -226,6 +232,10 @@ private: void sendGeometryEvent(); void setFS(int screenNumber); bool wantRepaint=false; +#ifndef Q_OS_LINUX + void sendSelectionToServer(SelectionType selection); + void setInputSelectionData(SelectionType selection, SelectionMime mime, bool firstChunk, bool lastChunk, uint32_t compressed, uint size, char* data, bool notify=false); +#endif //initial values @@ -233,7 +243,7 @@ private: int height=600; //feature version of server - u_int16_t serverVersion=0; + quint16 serverVersion=0; bool fullscreen=false; bool multidisp=false; @@ -273,6 +283,8 @@ private: //input selection chunk variables //size of current chunk uint32_t selectionSize; + //size of complete selection + uint32_t selectionTotalSize; //format of chunk, string or pix SelectionMime selectionFormat; //if true clipboard else primary @@ -281,8 +293,8 @@ private: bool firstChunk; //if it's the last chunk in multiply sel bool lastChunk; - //if the chunk compressed - bool compressed; + //if the chunk compressed the size is > 0 + uint32_t compressed_size; ////////// ClipboardMode clipMode=CLIP_BOTH; //clipboard mode: both, server, client or none @@ -306,8 +318,13 @@ private: ScreenIdentifier *screenIdentifier=0l; QLabel* fr; + //selection #ifdef Q_OS_LINUX XCBClip* clipboard; +#else + SelectionMime selMime; //mime of selection (string or image) + QByteArray selData; //data + uint32_t total_compressed=0; #endif protected: diff --git a/debian/changelog b/debian/changelog index 84f52c2..9f8af45 100644 --- a/debian/changelog +++ b/debian/changelog @@ -25,5 +25,6 @@ x2gokdriveclient (0.0.0.1-0x2go1) UNRELEASED; urgency=medium - add command line argument --selection (both|none|server|client) to specify selection mode. - send a recive feature versions. - give server some time for initialization before sending version. + - support sending and recieving selections on demand. Support reading and writing INCR properties. -- Mike Gabriel <mike.gabriel@das-netzwerkteam.de> Tue, 04 Jun 2019 11:10:43 +0200 diff --git a/xcbclip.cpp b/xcbclip.cpp index e62d311..34cf6c2 100644 --- a/xcbclip.cpp +++ b/xcbclip.cpp @@ -23,8 +23,12 @@ #include <QX11Info> #include <QTimer> #include <QBuffer> +#include <QDateTime> +#define SELECTION_DELAY 30000 //timeout for selection operation +#define INCR_SIZE 256*1024 //size of part for incr selection incr selection + XCBClip::XCBClip(Client* parent) { @@ -46,6 +50,8 @@ XCBClip::XCBClip(Client* parent) values[0] = screen->white_pixel; values[1] = XCB_EVENT_MASK_PROPERTY_CHANGE; + ATOM_CLIPBOARD=atom("CLIPBOARD"); + //create window which will recieve selection events and provide remote selection to X-clients xcb_create_window (con, XCB_COPY_FROM_PARENT, @@ -76,7 +82,7 @@ XCBClip::XCBClip(Client* parent) //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); + xcb_xfixes_select_selection_input_checked(con, clipWinId, ATOM_CLIPBOARD, mask); } free(xfixes_query); } @@ -84,6 +90,18 @@ XCBClip::XCBClip(Client* parent) QTimer::singleShot(250, this, SLOT(checkEvents())); } +XCBClip::~XCBClip() +{ + for(uint i = delayedSelectionRequests.length()-1; i<=0;--i) + { + discardDelayedRequest(i); + } + //remove all pending INCR requests + remove_obsolete_incr_transactions(false); + xcb_destroy_window(con, clipWinId); + xcb_disconnect(con); +} + xcb_atom_t XCBClip::atom(const QString& name) { @@ -137,6 +155,7 @@ QString XCBClip::atom_name(xcb_atom_t xatom) 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) @@ -228,8 +247,88 @@ xcb_atom_t XCBClip::target_has_atom(const QStringList& atoms, const QString& nam return 0; } +void XCBClip::discardDelayedRequest(uint index) +{ + DelayedRequest *d=delayedSelectionRequests.takeAt(index); + xcb_send_event(con, false, d->request->requestor, XCB_EVENT_MASK_NO_EVENT, (char*)d->event); + xcb_flush(con); + free(d->event); + free((xcb_generic_event_t *)(d->request)); + delete d; +} + + +void XCBClip::processDelayedRequests() +{ + //process delayed requests + for(uint i = delayedSelectionRequests.length()-1; i<=0;--i) + { + DelayedRequest* d=delayedSelectionRequests[i]; + SelectionType selection = selection_from_atom( d->request->selection); + if(currentXTime() > (d->request->time + SELECTION_DELAY)) + { +// qDebug()<<"timeout selection: "<<selection; + discardDelayedRequest(i); + continue; + } + if(!inputSelection[selection].owner) + { +// qDebug()<<"we are not owner of requested selection: "<<selection; + //we are not anymore owners of this selection + discardDelayedRequest(i); + continue; + } + if(inputSelection[selection].timestamp > d->request->time ) + { +// qDebug()<<"selection request for "<<selection<<" is too old"; + //requested selection is older than the current one + discardDelayedRequest(i); + continue; + } + if(!check_req_sanity(d->request)) + { +// qDebug()<<"can't convert selection "<<selection<<" to requested myme type "<<d->request->property; + //our selection don't support requested mime type + discardDelayedRequest(i); + continue; + } + if(inputSelection[selection].state != InputSelection::COMPLETED) + { + //we don't have the data yet + continue; + } + d->event->property=send_data(d->request); + discardDelayedRequest(i); + } +} + + +void XCBClip::updateCurrentTime(xcb_timestamp_t t) +{ + //updating the current X time. It's not very precicely, but enough for us + if(t > lastXTime) + { + //update current time + lastXTime=t; + timeDifference=QDateTime::currentMSecsSinceEpoch() - t; +// qDebug()<<"X time dif:"<<QDateTime::currentMSecsSinceEpoch() - t<<"x11 time"<<t<<"calculated: "<<currentXTime(); + } +} + +xcb_timestamp_t XCBClip::currentXTime() +{ + //get current X time + return QDateTime::currentMSecsSinceEpoch() - timeDifference; + +} + + void XCBClip::checkEvents() { + //check delayed events + processDelayedRequests(); + //delete obsolete INCR transactions if we have something + remove_obsolete_incr_transactions(); xcb_generic_event_t *e=xcb_poll_for_event(con); if(!e) { @@ -244,6 +343,9 @@ void XCBClip::checkEvents() 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; + updateCurrentTime(notify_event->timestamp); + + // qDebug()<<"SEL OWNER notify, selection:"<<notify_event->selection<< " window "<< notify_event->window<< "owner"<< notify_event->owner; if(notify_event->owner == clipWinId) { @@ -265,11 +367,11 @@ void XCBClip::checkEvents() incrAtom=0; if(notify_event->selection==XCB_ATOM_PRIMARY) { - owner[0]=false; + inputSelection[PRIMARY].owner=false; } else { - owner[1]=false; + inputSelection[CLIPBOARD].owner=false; } //get supported mime types request_selection_data( notify_event->selection, atom( "TARGETS"), atom( "TARGETS"), 0); @@ -289,7 +391,13 @@ void XCBClip::checkEvents() } else if (response_type == XCB_SELECTION_REQUEST) { - process_selection_request(e); + if(!process_selection_request(e)) + { + //we delayed processing of this request till data received from server + //we will free the event when we have the data + QTimer::singleShot(10, this, SLOT(checkEvents())); + return; + } } else { @@ -312,6 +420,9 @@ void XCBClip::process_selection_notify(xcb_generic_event_t *e) // qDebug()<<"selection notify"; sel_event=(xcb_selection_notify_event_t *)e; + updateCurrentTime(sel_event->time); + + //processing the event which is reply for convert selection call @@ -326,7 +437,7 @@ void XCBClip::process_selection_notify(xcb_generic_event_t *e) // qDebug()<<"selection notify sel , target , property "<< sel_event->selection<< sel_event->target<< sel_event->property; if(sel_event->property==XCB_NONE) { - qDebug()<<( "NO SELECTION"); +// qDebug()<<( "NO SELECTION"); } else { @@ -338,6 +449,70 @@ void XCBClip::process_selection_notify(xcb_generic_event_t *e) } } +void XCBClip::destroy_incr_transaction(int index) +{ + //destroy incr transaction with index + IncrTransaction* tr=incrTransactions.takeAt(index); + const quint32 mask[] = { XCB_EVENT_MASK_NO_EVENT }; + //don't resive property notify events for this window anymore + xcb_change_window_attributes(con, tr->requestor, + XCB_CW_EVENT_MASK, mask); + xcb_flush(con); + + delete tr; +} + + +void XCBClip::remove_obsolete_incr_transactions( bool checkTs) +{ + //remove_obsolete_incr_transactions + //if checkTS true, check timestamp and destroy only if ts exceed delay + for (int i= incrTransactions.size()-1;i>=0;--i) + { + if( (!checkTs) || ( incrTransactions[i]->timestamp+SELECTION_DELAY < QDateTime::currentMSecsSinceEpoch())) + { + qDebug()<<"timeout INCR selection for "<<incrTransactions[i]->requestor<<incrTransactions[i]->timestamp<< QDateTime::currentMSecsSinceEpoch(); + destroy_incr_transaction(i); + } + } +} + + +void XCBClip::process_incr_transaction_property(xcb_property_notify_event_t * pn) +{ + //process incr transactions + for (int i=0;i < incrTransactions.size();++i) + { + IncrTransaction* tr=incrTransactions[i]; + if((tr->requestor == pn->window) && (tr->property == pn->atom ) && ( pn->state == XCB_PROPERTY_DELETE) ) + { + //requestor ready for the new portion of data + uint left=tr->data.size()-tr->sentBytes; + if(!left) + { +// qDebug()<<"all INCR data sent to"<<tr->requestor; + //all data sent, sending NULL data and destroying transaction + xcb_change_property(con, XCB_PROP_MODE_REPLACE, tr->requestor, tr->property, + tr->target, 8, 0, NULL); + xcb_flush(con); + destroy_incr_transaction(i); + return; + } + uint sendingBytes=(INCR_SIZE< left)?INCR_SIZE:left; + +// qDebug()<<"sending incr bytes"<<sendingBytes ; + + xcb_change_property(con, XCB_PROP_MODE_REPLACE, tr->requestor, tr->property, + tr->target, 8, sendingBytes, tr->data.constData() + tr->sentBytes); + xcb_flush(con); + tr->sentBytes+=sendingBytes; + tr->timestamp=QDateTime::currentMSecsSinceEpoch(); + return; + } + } + //notify event doesn't belong to any of started incr transactions or it's notification for new property + return; +} void XCBClip::process_property_notify(xcb_generic_event_t *e) { @@ -346,9 +521,13 @@ void XCBClip::process_property_notify(xcb_generic_event_t *e) // qDebug()<<("property notify"); pn = (xcb_property_notify_event_t *)e; + updateCurrentTime(pn->time); + if (pn->window != clipWinId) { -// qDebug()<<("not our window"); + //this property doesn't belong to our window; + //let's check if it's not the property corresponding to one of incr transactions + process_incr_transaction_property(pn); return; } // qDebug()<<"property, state "<< pn->atom<< pn->state; @@ -375,7 +554,7 @@ void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) //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()); + cookie= xcb_get_property(con,false, clipWinId, property, XCB_GET_PROPERTY_TYPE_ANY, 0, parent->max_chunk()); reply=xcb_get_property_reply(con, cookie, NULL); if(!reply) { @@ -385,7 +564,7 @@ void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) { if(reply->type==XCB_NONE) { - qDebug()<<( "NONE reply"); +// qDebug()<<( "NONE reply"); } else { @@ -420,17 +599,27 @@ void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) { QStringList atoms=atomsInReply(reply); // qDebug() << "target supports mime types:"<<atoms; - + data_atom=0; + //get the best of supported mime types and request the selection in this format + data_atom=best_atom_from_list(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 + SelectionType sel=selection_from_atom(selection); + best_atom[sel]=data_atom; + SelectionMime mime=UTF_STRING; + + if( ! is_string_atom(best_atom[sel]) ) + { + mime = PIXMAP; + } + +// qDebug()<<"MIME"<<mime; + parent->send_selnotify_to_server(sel,mime); } 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); @@ -440,7 +629,7 @@ void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) request_selection_data( selection, data_atom, data_atom, 0); else { - qDebug()<<( "there are no supported mime types in the target"); +// qDebug()<<( "there are no supported mime types in the target"); } } } @@ -462,9 +651,7 @@ void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) - SelectionType sel= CLIPBOARD; - if(selection==XCB_ATOM_PRIMARY) - sel=PRIMARY; + SelectionType sel= selection_from_atom(selection); SelectionMime mime= UTF_STRING; if(is_image_atom(reply->type)) @@ -478,18 +665,14 @@ void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) 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; + chunk->selection=selection_from_atom(selection); if(incrementalSize && (incrAtom==property)) @@ -540,7 +723,7 @@ void XCBClip::read_selection_property(xcb_atom_t selection, xcb_atom_t property) if(bytes_left) { free(reply); - cookie= xcb_get_property(con, 0, clipWinId, property, XCB_GET_PROPERTY_TYPE_ANY, bytes_read/4,max_chunk()); + cookie= xcb_get_property(con, 0, clipWinId, property, XCB_GET_PROPERTY_TYPE_ANY, bytes_read/4,parent->max_chunk()); reply=xcb_get_property_reply(con, cookie, NULL); if(!reply) { @@ -575,6 +758,21 @@ SelectionType XCBClip::selection_from_atom(xcb_atom_t selection) } +xcb_atom_t XCBClip::atom_from_selection(SelectionType selection) +{ + if(selection == PRIMARY) + return XCB_ATOM_PRIMARY; + return ATOM_CLIPBOARD; + +} + +void XCBClip::requestSelectionData(SelectionType selection) +{ + //Client requesting data for selection using this function + request_selection_data(atom_from_selection(selection), best_atom[selection], best_atom[selection], 0); +} + + 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) @@ -589,23 +787,53 @@ void XCBClip::request_selection_data( xcb_atom_t selection, xcb_atom_t target, } -void XCBClip::setInputSelectionData(SelectionType selection, SelectionMime mime, bool firstChunk, bool lastChunk, bool compressed, uint size, char* data) +void XCBClip::setInputSelectionData(SelectionType selection, SelectionMime mime, bool firstChunk, bool lastChunk, uint32_t compressed, uint size, char* data, bool notify) { + //if notify is true, we don't have actual data, just notification //copy data to selection buffer -// qDebug()<<"Get chunk of input selection: selection, myme, firstChunk, lastChunk, compressed, size:"<<selection<<mime<<firstChunk<<lastChunk<<compressed<<size; +// qDebug()<<"Get chunk of input selection: selection, myme, firstChunk, lastChunk, compressed, size:"<<selection<<mime<<firstChunk<<lastChunk<<compressed<<size<<notify; if(firstChunk) { - selData[selection].clear(); - selMime[selection]=mime; + inputSelection[selection].selData.clear(); + inputSelection[selection].selMime=mime; + total_compressed=0; } - selData[selection].append(data,size); + if(!compressed) + inputSelection[selection].selData.append(data,size); + else + { + QByteArray ba; + ba.append((char*) &size, 4); + ba.append(data, compressed); + + total_compressed+=compressed; + + inputSelection[selection].selData.append(qUncompress(ba)); +// qDebug()<<"uncompress from "<<compressed<<" to "<<size; + } - if(lastChunk) + if(lastChunk ) { - own_selection(selection); + if(notify) + { + inputSelection[selection].state=InputSelection::NOTIFIED; +// qDebug()<<"Got selection notify from server"; + own_selection(selection); + } + else + { + //if state is requested, means we already own a selection + if(inputSelection[selection].state!=InputSelection::REQUESTED) + { + own_selection(selection); + } + inputSelection[selection].state=InputSelection::COMPLETED; +// qDebug()<<"Got selection data for "<<selection<<"total size: "<<inputSelection[selection].selData.size()<<"compressed size"<<total_compressed; + } + } } @@ -620,21 +848,25 @@ void XCBClip::own_selection(SelectionType selection) default: break; } - xcb_atom_t sel=XCB_ATOM_PRIMARY; - if(selection!=PRIMARY) - { - sel=atom("CLIPBOARD"); - } + xcb_atom_t sel=atom_from_selection(selection); xcb_set_selection_owner(con, clipWinId, sel, XCB_CURRENT_TIME); xcb_flush(con); - owner[selection]=true; - timestamp[selection]=XCB_CURRENT_TIME; + inputSelection[selection].owner=true; + inputSelection[selection].timestamp=currentXTime(); } -void XCBClip::process_selection_request(xcb_generic_event_t *e) +bool XCBClip::process_selection_request(xcb_generic_event_t *e) { + //processing selection request. + //return true if the processing is finishing after return + //false if data is not ready and we are delaying processing of this request + //in this case calling function SHOULD NOT destroy the request neither event should not be destroyed + //we'll free this objects after processing of the request when the data is available + xcb_selection_request_event_t *req=(xcb_selection_request_event_t*)e; + updateCurrentTime(req->time); + xcb_selection_notify_event_t* event= (xcb_selection_notify_event_t*)calloc(32, 1); event->response_type = XCB_SELECTION_NOTIFY; event->requestor = req->requestor; @@ -651,25 +883,25 @@ void XCBClip::process_selection_request(xcb_generic_event_t *e) 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]) +// qDebug()<<"selection request for"<<atom_name(req->selection)<<atom_name(req->target)<<atom_name(req->property)<< "from "<<req->requestor<<"we are "<<clipWinId; + if(!inputSelection[sel].owner) { //we don't own this selection - qDebug()<<"not our 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; + return true; } - if(timestamp[sel] > req->time) + if(inputSelection[sel].timestamp > req->time) { //selection changed after request - qDebug()<<"requested selection doesn't exist anymore"; +// 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; + return true; } @@ -678,7 +910,7 @@ void XCBClip::process_selection_request(xcb_generic_event_t *e) event->property=property; // qDebug()<<"requested TIMESTAMP"; xcb_change_property(con, XCB_PROP_MODE_REPLACE, req->requestor, - property, XCB_ATOM_INTEGER, 32, 1, ×tamp[sel]); + property, XCB_ATOM_INTEGER, 32, 1, &inputSelection[sel].timestamp); } else if(req->target==atom("TARGETS")) @@ -689,11 +921,39 @@ void XCBClip::process_selection_request(xcb_generic_event_t *e) } else { - event->property=send_data(req); + if(check_req_sanity(req)) + { + //if data is ready, send it to requestor + if(inputSelection[sel].state == InputSelection::COMPLETED) + event->property=send_data(req); + else + { + //if data is not ready, request it from server and delay the processing of request + delay_selection_request(req, event); + return false; + } + } } xcb_send_event(con, false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (char*)event); xcb_flush(con); free(event); + return true; +} + +void XCBClip::delay_selection_request( xcb_selection_request_event_t *request, xcb_selection_notify_event_t* event) +{ + SelectionType sel=selection_from_atom(request->selection); + DelayedRequest* dr=new DelayedRequest; + dr->event=event; + dr->request=request; + //add new request to the queue + delayedSelectionRequests<<dr; + if(inputSelection[sel].state==InputSelection::NOTIFIED) + { + //if we didn't request the data yet, let's do it now + parent->requestSelectionFromServer(sel); + inputSelection[sel].state=InputSelection::REQUESTED; + } } QString XCBClip::mime_to_QT_img(const QString& mimeType) @@ -710,54 +970,91 @@ QString XCBClip::mime_to_QT_img(const QString& mimeType) 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; + bool support_incr=true; //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"; +// 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) + + if(!support_incr) { -// qDebug()<<"sending "<<data->size()<<atom_name(req->property)<<atom_name(req->target); + if( data->size() < (int)xcb_get_maximum_request_length(con) * 4 - 24) + { + //requester doesn't support INCR, sending data in one chunk + 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; + } + if(data->size() < INCR_SIZE) + { + //if size is < 256K send in one property 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; + //sending INCR atom to let requester know that we are starting data incrementally + uint bytes=data->size(); + qDebug()<<"starting INCR send of size"<<data->size()<<" for win ID "<<req->requestor; + xcb_change_property(con, XCB_PROP_MODE_REPLACE, req->requestor, req->property, + atom("INCR"), 32, 1, (const void *)&bytes); + startIncrTransaction(req->requestor, req->property, req->target, *data ); + xcb_flush(con); + + return req->property; } -xcb_atom_t XCBClip::send_data(xcb_selection_request_event_t* req) + +//creating INCR transaction +void XCBClip::startIncrTransaction(xcb_window_t requestor, xcb_atom_t property, xcb_atom_t target, QByteArray data) { + IncrTransaction* tr=new IncrTransaction; + tr->requestor=requestor; + tr->property=property; + tr->target=target; + tr->data=data; + tr->sentBytes=0; + tr->timestamp=QDateTime::currentMSecsSinceEpoch(); + incrTransactions<<tr; + qDebug()<<"INCR start"<<tr->timestamp; + const quint32 mask[] = { XCB_EVENT_MASK_PROPERTY_CHANGE }; + //we'll recive property change events for requestor window from now + xcb_change_window_attributes(con, requestor, + XCB_CW_EVENT_MASK, mask); +} - //send data - SelectionType sel=selection_from_atom(req->selection); - if(selMime[sel]==UTF_STRING) +//check if the requested mime can be delivered +bool XCBClip::check_req_sanity(xcb_selection_request_event_t* req) +{ + SelectionType sel=selection_from_atom(req->selection); + if(inputSelection[sel].selMime==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]); + // qDebug()<<"sending UTF text"; + return true; } else { qDebug()<<"unsupported property requested:"<<atom_name(req->target); - return XCB_NONE; + return false; } } else @@ -765,11 +1062,28 @@ xcb_atom_t XCBClip::send_data(xcb_selection_request_event_t* req) if(!is_image_atom(req->target)) { qDebug()<<"unsupported property requested:"<<atom_name(req->target); - return XCB_NONE; + return false; } + // qDebug()<<"sending "<<atom_name(req->target); + return true; + } +} + + +xcb_atom_t XCBClip::send_data(xcb_selection_request_event_t* req) +{ + //send data + SelectionType sel=selection_from_atom(req->selection); + if(inputSelection[sel].selMime==UTF_STRING) + { + //if it's one of supported text formats send without convertion + return set_data_property(req, &inputSelection[sel].selData); + } + else + { // qDebug()<<"sending "<<atom_name(req->target); //convert to desireable format - QImage img=QImage::fromData(selData[sel]); + QImage img=QImage::fromData(inputSelection[sel].selData); QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); @@ -777,7 +1091,6 @@ xcb_atom_t XCBClip::send_data(xcb_selection_request_event_t* req) // 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) @@ -792,7 +1105,7 @@ void XCBClip::send_mime_types(xcb_selection_request_event_t* req) if((a=atom("TIMESTAMP"))) targets.append(a); - if(selMime[sel]==PIXMAP) + if(inputSelection[sel].selMime==PIXMAP) { if((a=atom("image/png"))) targets.append(a); @@ -825,10 +1138,4 @@ void XCBClip::send_mime_types(xcb_selection_request_event_t* req) } -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 index c45fc91..7787e50 100644 --- a/xcbclip.h +++ b/xcbclip.h @@ -34,17 +34,18 @@ 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); + ~XCBClip(); + void setInputSelectionData(SelectionType selection, SelectionMime mime, bool firstChunk, bool lastChunk, uint32_t compressed, uint size, char* data, bool notify=false); + void requestSelectionData(SelectionType selection); 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); + bool 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); @@ -56,7 +57,18 @@ private: 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); + xcb_atom_t atom_from_selection(SelectionType selection); QString mime_to_QT_img(const QString& mimeType); + void delay_selection_request( xcb_selection_request_event_t *reqest, xcb_selection_notify_event_t* event); + bool check_req_sanity(xcb_selection_request_event_t* req); + void processDelayedRequests(); + void discardDelayedRequest(uint index); + void updateCurrentTime(xcb_timestamp_t t); + xcb_timestamp_t currentXTime(); + void startIncrTransaction(xcb_window_t requestor, xcb_atom_t property, xcb_atom_t target, QByteArray data); + void process_incr_transaction_property(xcb_property_notify_event_t * pn); + void destroy_incr_transaction(int index); + void remove_obsolete_incr_transactions(bool checkTS=true); private slots: void checkEvents(); @@ -70,10 +82,43 @@ private: xcb_atom_t incrAtom=0; uint incrementalSize=0; uint incrementalSizeRead=0; + xcb_atom_t best_atom[2]; //the best mime type for selection to request on demand sel request from server + xcb_atom_t ATOM_CLIPBOARD=0; //atom for CLIPBOARD selection + //input selection - SelectionMime selMime[2]; - QByteArray selData[2]; - bool owner[2]={0}; - xcb_timestamp_t timestamp[2]; + + struct InputSelection{ + SelectionMime selMime; //mime of selection (string or image) + QByteArray selData; //data + bool owner=false; //if we are owners of this selction + enum { NOTIFIED, REQUESTED, COMPLETED} state; //state of the selection + xcb_timestamp_t timestamp; //timestamp when we own selection + }inputSelection[2]; + + //requests which processing should be delayed till we get data from server + struct DelayedRequest + { + xcb_selection_request_event_t *request; // request from client + xcb_selection_notify_event_t* event; // event which we are going to send to client + }; + QList <DelayedRequest*> delayedSelectionRequests; + + //save running INCR transactions in this struct + struct IncrTransaction + { + xcb_window_t requestor; + xcb_atom_t property; + xcb_atom_t target; + QByteArray data; + uint sentBytes; + qint64 timestamp; + }; + QList <IncrTransaction*> incrTransactions; + + + xcb_timestamp_t lastXTime=XCB_TIME_CURRENT_TIME; //current time of X Server + int32_t timeDifference=0; //differense between X server time and system time + + uint32_t total_compressed=0; }; #endif -- Alioth's /home/x2go-admin/maintenancescripts/git/hooks/post-receive-email on /srv/git/code.x2go.org/x2gokdriveclient.git