/* * synergy -- mouse and keyboard sharing utility * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * found in the file COPYING that should have accompanied this file. * * This package 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. */ #include "CXWindowsClipboard.h" #include "CXWindowsClipboardTextConverter.h" #include "CXWindowsClipboardUCS2Converter.h" #include "CXWindowsClipboardUTF8Converter.h" #include "CXWindowsClipboardHTMLConverter.h" #include "CXWindowsClipboardBMPConverter.h" #include "CXWindowsUtil.h" #include "CThread.h" #include "CLog.h" #include "CStopwatch.h" #include "CArch.h" #include "stdvector.h" #include #include // // CXWindowsClipboard // CXWindowsClipboard::CXWindowsClipboard(Display* display, Window window, ClipboardID id) : m_display(display), m_window(window), m_id(id), m_open(false), m_time(0), m_owner(false), m_timeOwned(0), m_timeLost(0) { // get some atoms m_atomTargets = XInternAtom(m_display, "TARGETS", False); m_atomMultiple = XInternAtom(m_display, "MULTIPLE", False); m_atomTimestamp = XInternAtom(m_display, "TIMESTAMP", False); m_atomInteger = XInternAtom(m_display, "INTEGER", False); m_atomAtom = XInternAtom(m_display, "ATOM", False); m_atomAtomPair = XInternAtom(m_display, "ATOM_PAIR", False); m_atomData = XInternAtom(m_display, "CLIP_TEMPORARY", False); m_atomINCR = XInternAtom(m_display, "INCR", False); m_atomMotifClipLock = XInternAtom(m_display, "_MOTIF_CLIP_LOCK", False); m_atomMotifClipHeader = XInternAtom(m_display, "_MOTIF_CLIP_HEADER", False); m_atomMotifClipAccess = XInternAtom(m_display, "_MOTIF_CLIP_LOCK_ACCESS_VALID", False); m_atomGDKSelection = XInternAtom(m_display, "GDK_SELECTION", False); // set selection atom based on clipboard id switch (id) { case kClipboardClipboard: m_selection = XInternAtom(m_display, "CLIPBOARD", False); break; case kClipboardSelection: default: m_selection = XA_PRIMARY; break; } // add converters, most desired first m_converters.push_back(new CXWindowsClipboardHTMLConverter(m_display, "text/html")); m_converters.push_back(new CXWindowsClipboardBMPConverter(m_display)); m_converters.push_back(new CXWindowsClipboardUTF8Converter(m_display, "text/plain;charset=UTF-8")); m_converters.push_back(new CXWindowsClipboardUTF8Converter(m_display, "UTF8_STRING")); m_converters.push_back(new CXWindowsClipboardUCS2Converter(m_display, "text/plain;charset=ISO-10646-UCS-2")); m_converters.push_back(new CXWindowsClipboardUCS2Converter(m_display, "text/unicode")); m_converters.push_back(new CXWindowsClipboardTextConverter(m_display, "text/plain")); m_converters.push_back(new CXWindowsClipboardTextConverter(m_display, "STRING")); // we have no data clearCache(); } CXWindowsClipboard::~CXWindowsClipboard() { clearReplies(); clearConverters(); } void CXWindowsClipboard::lost(Time time) { LOG((CLOG_DEBUG "lost clipboard %d ownership at %d", m_id, time)); if (m_owner) { m_owner = false; m_timeLost = time; clearCache(); } } void CXWindowsClipboard::addRequest(Window owner, Window requestor, Atom target, ::Time time, Atom property) { // must be for our window and we must have owned the selection // at the given time. bool success = false; if (owner == m_window) { LOG((CLOG_DEBUG1 "request for clipboard %d, target %s by 0x%08x (property=%s)", m_selection, CXWindowsUtil::atomToString(m_display, target).c_str(), requestor, CXWindowsUtil::atomToString(m_display, property).c_str())); if (wasOwnedAtTime(time)) { if (target == m_atomMultiple) { // add a multiple request. property may not be None // according to ICCCM. if (property != None) { success = insertMultipleReply(requestor, time, property); } } else { addSimpleRequest(requestor, target, time, property); // addSimpleRequest() will have already handled failure success = true; } } else { LOG((CLOG_DEBUG1 "failed, not owned at time %d", time)); } } if (!success) { // send failure LOG((CLOG_DEBUG1 "failed")); insertReply(new CReply(requestor, target, time)); } // send notifications that are pending pushReplies(); } bool CXWindowsClipboard::addSimpleRequest(Window requestor, Atom target, ::Time time, Atom property) { // obsolete requestors may supply a None property. in // that case we use the target as the property to store // the conversion. if (property == None) { property = target; } // handle targets CString data; Atom type = None; int format = 0; if (target == m_atomTargets) { type = getTargetsData(data, &format); } else if (target == m_atomTimestamp) { type = getTimestampData(data, &format); } else { IXWindowsClipboardConverter* converter = getConverter(target); if (converter != NULL) { IClipboard::EFormat clipboardFormat = converter->getFormat(); if (m_added[clipboardFormat]) { try { data = converter->fromIClipboard(m_data[clipboardFormat]); format = converter->getDataSize(); type = converter->getAtom(); } catch (...) { // ignore -- cannot convert } } } } if (type != None) { // success LOG((CLOG_DEBUG1 "success")); insertReply(new CReply(requestor, target, time, property, data, type, format)); return true; } else { // failure LOG((CLOG_DEBUG1 "failed")); insertReply(new CReply(requestor, target, time)); return false; } } bool CXWindowsClipboard::processRequest(Window requestor, ::Time /*time*/, Atom property) { CReplyMap::iterator index = m_replies.find(requestor); if (index == m_replies.end()) { // unknown requestor window return false; } LOG((CLOG_DEBUG1 "received property %s delete from 0x08%x", CXWindowsUtil::atomToString(m_display, property).c_str(), requestor)); // find the property in the known requests. it should be the // first property but we'll check 'em all if we have to. CReplyList& replies = index->second; for (CReplyList::iterator index2 = replies.begin(); index2 != replies.end(); ++index2) { CReply* reply = *index2; if (reply->m_replied && reply->m_property == property) { // if reply is complete then remove it and start the // next one. pushReplies(index, replies, index2); return true; } } return false; } bool CXWindowsClipboard::destroyRequest(Window requestor) { CReplyMap::iterator index = m_replies.find(requestor); if (index == m_replies.end()) { // unknown requestor window return false; } // destroy all replies for this window clearReplies(index->second); m_replies.erase(index); // note -- we don't stop watching the window for events because // we're called in response to the window being destroyed. return true; } Window CXWindowsClipboard::getWindow() const { return m_window; } Atom CXWindowsClipboard::getSelection() const { return m_selection; } bool CXWindowsClipboard::empty() { assert(m_open); LOG((CLOG_DEBUG "empty clipboard %d", m_id)); // assert ownership of clipboard XSetSelectionOwner(m_display, m_selection, m_window, m_time); if (XGetSelectionOwner(m_display, m_selection) != m_window) { LOG((CLOG_DEBUG "failed to grab clipboard %d", m_id)); return false; } // clear all data. since we own the data now, the cache is up // to date. clearCache(); m_cached = true; // FIXME -- actually delete motif clipboard items? // FIXME -- do anything to motif clipboard properties? // save time m_timeOwned = m_time; m_timeLost = 0; // we're the owner now m_owner = true; LOG((CLOG_DEBUG "grabbed clipboard %d", m_id)); return true; } void CXWindowsClipboard::add(EFormat format, const CString& data) { assert(m_open); assert(m_owner); LOG((CLOG_DEBUG "add %d bytes to clipboard %d format: %d", data.size(), m_id, format)); m_data[format] = data; m_added[format] = true; // FIXME -- set motif clipboard item? } bool CXWindowsClipboard::open(Time time) const { assert(!m_open); LOG((CLOG_DEBUG "open clipboard %d", m_id)); // assume not motif m_motif = false; // lock clipboard if (m_id == kClipboardClipboard) { if (!motifLockClipboard()) { return false; } // check if motif owns the selection. unlock motif clipboard // if it does not. m_motif = motifOwnsClipboard(); LOG((CLOG_DEBUG1 "motif does %sown clipboard", m_motif ? "" : "not ")); if (!m_motif) { motifUnlockClipboard(); } } // now open m_open = true; m_time = time; // be sure to flush the cache later if it's dirty m_checkCache = true; return true; } void CXWindowsClipboard::close() const { assert(m_open); LOG((CLOG_DEBUG "close clipboard %d", m_id)); // unlock clipboard if (m_motif) { motifUnlockClipboard(); } m_motif = false; m_open = false; } IClipboard::Time CXWindowsClipboard::getTime() const { checkCache(); return m_timeOwned; } bool CXWindowsClipboard::has(EFormat format) const { assert(m_open); fillCache(); return m_added[format]; } CString CXWindowsClipboard::get(EFormat format) const { assert(m_open); fillCache(); return m_data[format]; } void CXWindowsClipboard::clearConverters() { for (ConverterList::iterator index = m_converters.begin(); index != m_converters.end(); ++index) { delete *index; } m_converters.clear(); } IXWindowsClipboardConverter* CXWindowsClipboard::getConverter(Atom target, bool onlyIfNotAdded) const { IXWindowsClipboardConverter* converter = NULL; for (ConverterList::const_iterator index = m_converters.begin(); index != m_converters.end(); ++index) { converter = *index; if (converter->getAtom() == target) { break; } } if (converter == NULL) { LOG((CLOG_DEBUG1 " no converter for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); return NULL; } // optionally skip already handled targets if (onlyIfNotAdded) { if (m_added[converter->getFormat()]) { LOG((CLOG_DEBUG1 " skipping handled format %d", converter->getFormat())); return NULL; } } return converter; } void CXWindowsClipboard::checkCache() const { if (!m_checkCache) { return; } m_checkCache = false; // get the time the clipboard ownership was taken by the current // owner. if (m_motif) { m_timeOwned = motifGetTime(); } else { m_timeOwned = icccmGetTime(); } // if we can't get the time then use the time passed to us if (m_timeOwned == 0) { m_timeOwned = m_time; } // if the cache is dirty then flush it if (m_timeOwned != m_cacheTime) { clearCache(); } } void CXWindowsClipboard::clearCache() const { const_cast(this)->doClearCache(); } void CXWindowsClipboard::doClearCache() { m_checkCache = false; m_cached = false; for (SInt32 index = 0; index < kNumFormats; ++index) { m_data[index] = ""; m_added[index] = false; } } void CXWindowsClipboard::fillCache() const { // get the selection data if not already cached checkCache(); if (!m_cached) { const_cast(this)->doFillCache(); } } void CXWindowsClipboard::doFillCache() { if (m_motif) { motifFillCache(); } else { icccmFillCache(); } m_checkCache = false; m_cached = true; m_cacheTime = m_timeOwned; } void CXWindowsClipboard::icccmFillCache() { LOG((CLOG_DEBUG "ICCCM fill clipboard %d", m_id)); // see if we can get the list of available formats from the selection. // if not then use a default list of formats. note that some clipboard // owners are broken and report TARGETS as the type of the TARGETS data // instead of the correct type ATOM; allow either. const Atom atomTargets = m_atomTargets; Atom target; CString data; if (!icccmGetSelection(atomTargets, &target, &data) || (target != m_atomAtom && target != m_atomTargets)) { LOG((CLOG_DEBUG1 "selection doesn't support TARGETS")); data = ""; CXWindowsUtil::appendAtomData(data, XA_STRING); } CXWindowsUtil::convertAtomProperty(data); const Atom* targets = reinterpret_cast(data.data()); const UInt32 numTargets = data.size() / sizeof(Atom); LOG((CLOG_DEBUG " available targets: %s", CXWindowsUtil::atomsToString(m_display, targets, numTargets).c_str())); // try each converter in order (because they're in order of // preference). for (ConverterList::const_iterator index = m_converters.begin(); index != m_converters.end(); ++index) { IXWindowsClipboardConverter* converter = *index; // skip already handled targets if (m_added[converter->getFormat()]) { continue; } // see if atom is in target list Atom target = None; // XXX -- just ask for the converter's target to see if it's // available rather than checking TARGETS. i've seen clipboard // owners that don't report all the targets they support. target = converter->getAtom(); /* for (UInt32 i = 0; i < numTargets; ++i) { if (converter->getAtom() == targets[i]) { target = targets[i]; break; } } */ if (target == None) { continue; } // get the data Atom actualTarget; CString targetData; if (!icccmGetSelection(target, &actualTarget, &targetData)) { LOG((CLOG_DEBUG1 " no data for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); continue; } // add to clipboard and note we've done it IClipboard::EFormat format = converter->getFormat(); m_data[format] = converter->toIClipboard(targetData); m_added[format] = true; LOG((CLOG_DEBUG " added format %d for target %s (%u %s)", format, CXWindowsUtil::atomToString(m_display, target).c_str(), targetData.size(), targetData.size() == 1 ? "byte" : "bytes")); } } bool CXWindowsClipboard::icccmGetSelection(Atom target, Atom* actualTarget, CString* data) const { assert(actualTarget != NULL); assert(data != NULL); // request data conversion CICCCMGetClipboard getter(m_window, m_time, m_atomData); if (!getter.readClipboard(m_display, m_selection, target, actualTarget, data)) { LOG((CLOG_DEBUG1 "can't get data for selection target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); LOGC(getter.m_error, (CLOG_WARN "ICCCM violation by clipboard owner")); return false; } else if (*actualTarget == None) { LOG((CLOG_DEBUG1 "selection conversion failed for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); return false; } return true; } IClipboard::Time CXWindowsClipboard::icccmGetTime() const { Atom actualTarget; CString data; if (icccmGetSelection(m_atomTimestamp, &actualTarget, &data) && actualTarget == m_atomInteger) { Time time = *reinterpret_cast(data.data()); LOG((CLOG_DEBUG1 "got ICCCM time %d", time)); return time; } else { // no timestamp LOG((CLOG_DEBUG1 "can't get ICCCM time")); return 0; } } bool CXWindowsClipboard::motifLockClipboard() const { // fail if anybody owns the lock (even us, so this is non-recursive) Window lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock); if (lockOwner != None) { LOG((CLOG_DEBUG1 "motif lock owner 0x%08x", lockOwner)); return false; } // try to grab the lock // FIXME -- is this right? there's a race condition here -- // A grabs successfully, B grabs successfully, A thinks it // still has the grab until it gets a SelectionClear. Time time = CXWindowsUtil::getCurrentTime(m_display, m_window); XSetSelectionOwner(m_display, m_atomMotifClipLock, m_window, time); lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock); if (lockOwner != m_window) { LOG((CLOG_DEBUG1 "motif lock owner 0x%08x", lockOwner)); return false; } LOG((CLOG_DEBUG1 "locked motif clipboard")); return true; } void CXWindowsClipboard::motifUnlockClipboard() const { LOG((CLOG_DEBUG1 "unlocked motif clipboard")); // fail if we don't own the lock Window lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock); if (lockOwner != m_window) { return; } // release lock Time time = CXWindowsUtil::getCurrentTime(m_display, m_window); XSetSelectionOwner(m_display, m_atomMotifClipLock, None, time); } bool CXWindowsClipboard::motifOwnsClipboard() const { // get the current selection owner // FIXME -- this can't be right. even if the window is destroyed // Motif will still have a valid clipboard. how can we tell if // some other client owns CLIPBOARD? Window owner = XGetSelectionOwner(m_display, m_selection); if (owner == None) { return false; } // get the Motif clipboard header property from the root window Atom target; SInt32 format; CString data; Window root = RootWindow(m_display, DefaultScreen(m_display)); if (!CXWindowsUtil::getWindowProperty(m_display, root, m_atomMotifClipHeader, &data, &target, &format, False)) { return false; } // check the owner window against the current clipboard owner const CMotifClipHeader* header = reinterpret_cast(data.data()); if (data.size() >= sizeof(CMotifClipHeader) && header->m_id == kMotifClipHeader) { if (static_cast(header->m_selectionOwner) == owner) { return true; } } return false; } void CXWindowsClipboard::motifFillCache() { LOG((CLOG_DEBUG "Motif fill clipboard %d", m_id)); // get the Motif clipboard header property from the root window Atom target; SInt32 format; CString data; Window root = RootWindow(m_display, DefaultScreen(m_display)); if (!CXWindowsUtil::getWindowProperty(m_display, root, m_atomMotifClipHeader, &data, &target, &format, False)) { return; } // check that the header is okay const CMotifClipHeader* header = reinterpret_cast(data.data()); if (data.size() < sizeof(CMotifClipHeader) || header->m_id != kMotifClipHeader || header->m_numItems < 1) { return; } // get the Motif item property from the root window char name[18 + 20]; sprintf(name, "_MOTIF_CLIP_ITEM_%d", header->m_item); Atom atomItem = XInternAtom(m_display, name, False); data = ""; if (!CXWindowsUtil::getWindowProperty(m_display, root, atomItem, &data, &target, &format, False)) { return; } // check that the item is okay const CMotifClipItem* item = reinterpret_cast(data.data()); if (data.size() < sizeof(CMotifClipItem) || item->m_id != kMotifClipItem || item->m_numFormats - item->m_numDeletedFormats < 1) { return; } // format list is after static item structure elements const SInt32 numFormats = item->m_numFormats - item->m_numDeletedFormats; const SInt32* formats = reinterpret_cast(item->m_size + reinterpret_cast(data.data())); // get the available formats typedef std::map CMotifFormatMap; CMotifFormatMap motifFormats; for (SInt32 i = 0; i < numFormats; ++i) { // get Motif format property from the root window sprintf(name, "_MOTIF_CLIP_ITEM_%d", formats[i]); Atom atomFormat = XInternAtom(m_display, name, False); CString data; if (!CXWindowsUtil::getWindowProperty(m_display, root, atomFormat, &data, &target, &format, False)) { continue; } // check that the format is okay const CMotifClipFormat* motifFormat = reinterpret_cast(data.data()); if (data.size() < sizeof(CMotifClipFormat) || motifFormat->m_id != kMotifClipFormat || motifFormat->m_length < 0 || motifFormat->m_type == None || motifFormat->m_deleted != 0) { continue; } // save it motifFormats.insert(std::make_pair(motifFormat->m_type, data)); } //const UInt32 numMotifFormats = motifFormats.size(); // try each converter in order (because they're in order of // preference). for (ConverterList::const_iterator index = m_converters.begin(); index != m_converters.end(); ++index) { IXWindowsClipboardConverter* converter = *index; // skip already handled targets if (m_added[converter->getFormat()]) { continue; } // see if atom is in target list CMotifFormatMap::const_iterator index2 = motifFormats.find(converter->getAtom()); if (index2 == motifFormats.end()) { continue; } // get format const CMotifClipFormat* motifFormat = reinterpret_cast( index2->second.data()); const Atom target = motifFormat->m_type; // get the data (finally) Atom actualTarget; CString targetData; if (!motifGetSelection(motifFormat, &actualTarget, &targetData)) { LOG((CLOG_DEBUG1 " no data for target %s", CXWindowsUtil::atomToString(m_display, target).c_str())); continue; } // add to clipboard and note we've done it IClipboard::EFormat format = converter->getFormat(); m_data[format] = converter->toIClipboard(targetData); m_added[format] = true; LOG((CLOG_DEBUG " added format %d for target %s", format, CXWindowsUtil::atomToString(m_display, target).c_str())); } } bool CXWindowsClipboard::motifGetSelection(const CMotifClipFormat* format, Atom* actualTarget, CString* data) const { // if the current clipboard owner and the owner indicated by the // motif clip header are the same then transfer via a property on // the root window, otherwise transfer as a normal ICCCM client. if (!motifOwnsClipboard()) { return icccmGetSelection(format->m_type, actualTarget, data); } // use motif way // FIXME -- this isn't right. it'll only work if the data is // already stored on the root window and only if it fits in a // property. motif has some scheme for transferring part by // part that i don't know. char name[18 + 20]; sprintf(name, "_MOTIF_CLIP_ITEM_%d", format->m_data); Atom target = XInternAtom(m_display, name, False); Window root = RootWindow(m_display, DefaultScreen(m_display)); return CXWindowsUtil::getWindowProperty(m_display, root, target, data, actualTarget, NULL, False); } IClipboard::Time CXWindowsClipboard::motifGetTime() const { return icccmGetTime(); } bool CXWindowsClipboard::insertMultipleReply(Window requestor, ::Time time, Atom property) { // get the requested targets Atom target; SInt32 format; CString data; if (!CXWindowsUtil::getWindowProperty(m_display, requestor, property, &data, &target, &format, False)) { // can't get the requested targets return false; } // fail if the requested targets isn't of the correct form if (format != 32 || target != m_atomAtomPair) { return false; } // data is a list of atom pairs: target, property CXWindowsUtil::convertAtomProperty(data); const Atom* targets = reinterpret_cast(data.data()); const UInt32 numTargets = data.size() / sizeof(Atom); // add replies for each target bool changed = false; for (UInt32 i = 0; i < numTargets; i += 2) { const Atom target = targets[i + 0]; const Atom property = targets[i + 1]; if (!addSimpleRequest(requestor, target, time, property)) { // note that we can't perform the requested conversion CXWindowsUtil::replaceAtomData(data, i, None); changed = true; } } // update the targets property if we changed it if (changed) { CXWindowsUtil::setWindowProperty(m_display, requestor, property, data.data(), data.size(), target, format); } // add reply for MULTIPLE request insertReply(new CReply(requestor, m_atomMultiple, time, property, CString(), None, 32)); return true; } void CXWindowsClipboard::insertReply(CReply* reply) { assert(reply != NULL); // note -- we must respond to requests in order if requestor,target,time // are the same, otherwise we can use whatever order we like with one // exception: each reply in a MULTIPLE reply must be handled in order // as well. those replies will almost certainly not share targets so // we can't simply use requestor,target,time as map index. // // instead we'll use just the requestor. that's more restrictive than // necessary but we're guaranteed to do things in the right order. // note that we could also include the time in the map index and still // ensure the right order. but since that'll just make it harder to // find the right reply when handling property notify events we stick // to just the requestor. const bool newWindow = (m_replies.count(reply->m_requestor) == 0); m_replies[reply->m_requestor].push_back(reply); // adjust requestor's event mask if we haven't done so already. we // want events in case the window is destroyed or any of its // properties change. if (newWindow) { // note errors while we adjust event masks bool error = false; { CXWindowsUtil::CErrorLock lock(m_display, &error); // get and save the current event mask XWindowAttributes attr; XGetWindowAttributes(m_display, reply->m_requestor, &attr); m_eventMasks[reply->m_requestor] = attr.your_event_mask; // add the events we want XSelectInput(m_display, reply->m_requestor, attr.your_event_mask | StructureNotifyMask | PropertyChangeMask); } // if we failed then the window has already been destroyed if (error) { m_replies.erase(reply->m_requestor); delete reply; } } } void CXWindowsClipboard::pushReplies() { // send the first reply for each window if that reply hasn't // been sent yet. for (CReplyMap::iterator index = m_replies.begin(); index != m_replies.end(); ++index) { assert(!index->second.empty()); if (!index->second.front()->m_replied) { pushReplies(index, index->second, index->second.begin()); } } } void CXWindowsClipboard::pushReplies(CReplyMap::iterator mapIndex, CReplyList& replies, CReplyList::iterator index) { CReply* reply = *index; while (sendReply(reply)) { // reply is complete. discard it and send the next reply, // if any. index = replies.erase(index); delete reply; if (index == replies.end()) { break; } reply = *index; } // if there are no more replies in the list then remove the list // and stop watching the requestor for events. if (replies.empty()) { CXWindowsUtil::CErrorLock lock(m_display); Window requestor = mapIndex->first; XSelectInput(m_display, requestor, m_eventMasks[requestor]); m_replies.erase(mapIndex); m_eventMasks.erase(requestor); } } bool CXWindowsClipboard::sendReply(CReply* reply) { assert(reply != NULL); // bail out immediately if reply is done if (reply->m_done) { LOG((CLOG_DEBUG1 "clipboard: finished reply to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); return true; } // start in failed state if property is None bool failed = (reply->m_property == None); if (!failed) { LOG((CLOG_DEBUG1 "clipboard: setting property on 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); // send using INCR if already sending incrementally or if reply // is too large, otherwise just send it. const UInt32 maxRequestSize = 3 * XMaxRequestSize(m_display); const bool useINCR = (reply->m_data.size() > maxRequestSize); // send INCR reply if incremental and we haven't replied yet if (useINCR && !reply->m_replied) { UInt32 size = reply->m_data.size(); if (!CXWindowsUtil::setWindowProperty(m_display, reply->m_requestor, reply->m_property, &size, 4, m_atomINCR, 32)) { failed = true; } } // send more INCR reply or entire non-incremental reply else { // how much more data should we send? UInt32 size = reply->m_data.size() - reply->m_ptr; if (size > maxRequestSize) size = maxRequestSize; // send it if (!CXWindowsUtil::setWindowProperty(m_display, reply->m_requestor, reply->m_property, reply->m_data.data() + reply->m_ptr, size, reply->m_type, reply->m_format)) { failed = true; } else { reply->m_ptr += size; // we've finished the reply if we just sent the zero // size incremental chunk or if we're not incremental. reply->m_done = (size == 0 || !useINCR); } } } // if we've failed then delete the property and say we're done. // if we haven't replied yet then we can send a failure notify, // otherwise we've failed in the middle of an incremental // transfer; i don't know how to cancel that so i'll just send // the final zero-length property. // FIXME -- how do you gracefully cancel an incremental transfer? if (failed) { LOG((CLOG_DEBUG1 "clipboard: sending failure to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); reply->m_done = true; if (reply->m_property != None) { CXWindowsUtil::CErrorLock lock(m_display); XDeleteProperty(m_display, reply->m_requestor, reply->m_property); } if (!reply->m_replied) { sendNotify(reply->m_requestor, m_selection, reply->m_target, None, reply->m_time); // don't wait for any reply (because we're not expecting one) return true; } else { static const char dummy = 0; CXWindowsUtil::setWindowProperty(m_display, reply->m_requestor, reply->m_property, &dummy, 0, reply->m_type, reply->m_format); // wait for delete notify return false; } } // send notification if we haven't yet if (!reply->m_replied) { LOG((CLOG_DEBUG1 "clipboard: sending notify to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)); reply->m_replied = true; // dump every property on the requestor window to the debug2 // log. we've seen what appears to be a bug in lesstif and // knowing the properties may help design a workaround, if // it becomes necessary. if (CLOG->getFilter() >= CLog::kDEBUG2) { CXWindowsUtil::CErrorLock lock(m_display); int n; Atom* props = XListProperties(m_display, reply->m_requestor, &n); LOG((CLOG_DEBUG2 "properties of 0x%08x:", reply->m_requestor)); for (int i = 0; i < n; ++i) { Atom target; CString data; char* name = XGetAtomName(m_display, props[i]); if (!CXWindowsUtil::getWindowProperty(m_display, reply->m_requestor, props[i], &data, &target, NULL, False)) { LOG((CLOG_DEBUG2 " %s: ", name)); } else { // if there are any non-ascii characters in string // then print the binary data. static const char* hex = "0123456789abcdef"; for (CString::size_type j = 0; j < data.size(); ++j) { if (data[j] < 32 || data[j] > 126) { CString tmp; tmp.reserve(data.size() * 3); for (j = 0; j < data.size(); ++j) { unsigned char v = (unsigned char)data[j]; tmp += hex[v >> 16]; tmp += hex[v & 15]; tmp += ' '; } data = tmp; break; } } char* type = XGetAtomName(m_display, target); LOG((CLOG_DEBUG2 " %s (%s): %s", name, type, data.c_str())); if (type != NULL) { XFree(type); } } if (name != NULL) { XFree(name); } } if (props != NULL) { XFree(props); } } sendNotify(reply->m_requestor, m_selection, reply->m_target, reply->m_property, reply->m_time); } // wait for delete notify return false; } void CXWindowsClipboard::clearReplies() { for (CReplyMap::iterator index = m_replies.begin(); index != m_replies.end(); ++index) { clearReplies(index->second); } m_replies.clear(); m_eventMasks.clear(); } void CXWindowsClipboard::clearReplies(CReplyList& replies) { for (CReplyList::iterator index = replies.begin(); index != replies.end(); ++index) { delete *index; } replies.clear(); } void CXWindowsClipboard::sendNotify(Window requestor, Atom selection, Atom target, Atom property, Time time) { XEvent event; event.xselection.type = SelectionNotify; event.xselection.display = m_display; event.xselection.requestor = requestor; event.xselection.selection = selection; event.xselection.target = target; event.xselection.property = property; event.xselection.time = time; CXWindowsUtil::CErrorLock lock(m_display); XSendEvent(m_display, requestor, False, 0, &event); } bool CXWindowsClipboard::wasOwnedAtTime(::Time time) const { // not owned if we've never owned the selection checkCache(); if (m_timeOwned == 0) { return false; } // if time is CurrentTime then return true if we still own the // selection and false if we do not. else if we still own the // selection then get the current time, otherwise use // m_timeLost as the end time. Time lost = m_timeLost; if (m_timeLost == 0) { if (time == CurrentTime) { return true; } else { lost = CXWindowsUtil::getCurrentTime(m_display, m_window); } } else { if (time == CurrentTime) { return false; } } // compare time to range Time duration = lost - m_timeOwned; Time when = time - m_timeOwned; return (/*when >= 0 &&*/ when <= duration); } Atom CXWindowsClipboard::getTargetsData(CString& data, int* format) const { assert(format != NULL); // add standard targets CXWindowsUtil::appendAtomData(data, m_atomTargets); CXWindowsUtil::appendAtomData(data, m_atomMultiple); CXWindowsUtil::appendAtomData(data, m_atomTimestamp); // add targets we can convert to for (ConverterList::const_iterator index = m_converters.begin(); index != m_converters.end(); ++index) { IXWindowsClipboardConverter* converter = *index; // skip formats we don't have if (m_added[converter->getFormat()]) { CXWindowsUtil::appendAtomData(data, converter->getAtom()); } } *format = 32; return m_atomAtom; } Atom CXWindowsClipboard::getTimestampData(CString& data, int* format) const { assert(format != NULL); checkCache(); CXWindowsUtil::appendTimeData(data, m_timeOwned); *format = 32; return m_atomInteger; } // // CXWindowsClipboard::CICCCMGetClipboard // CXWindowsClipboard::CICCCMGetClipboard::CICCCMGetClipboard( Window requestor, Time time, Atom property) : m_requestor(requestor), m_time(time), m_property(property), m_incr(false), m_failed(false), m_done(false), m_reading(false), m_data(NULL), m_actualTarget(NULL), m_error(false) { // do nothing } CXWindowsClipboard::CICCCMGetClipboard::~CICCCMGetClipboard() { // do nothing } bool CXWindowsClipboard::CICCCMGetClipboard::readClipboard(Display* display, Atom selection, Atom target, Atom* actualTarget, CString* data) { assert(actualTarget != NULL); assert(data != NULL); LOG((CLOG_DEBUG1 "request selection=%s, target=%s, window=%x", CXWindowsUtil::atomToString(display, selection).c_str(), CXWindowsUtil::atomToString(display, target).c_str(), m_requestor)); m_atomNone = XInternAtom(display, "NONE", False); m_atomIncr = XInternAtom(display, "INCR", False); // save output pointers m_actualTarget = actualTarget; m_data = data; // assume failure *m_actualTarget = None; *m_data = ""; // delete target property XDeleteProperty(display, m_requestor, m_property); // select window for property changes XWindowAttributes attr; XGetWindowAttributes(display, m_requestor, &attr); XSelectInput(display, m_requestor, attr.your_event_mask | PropertyChangeMask); // request data conversion XConvertSelection(display, selection, target, m_property, m_requestor, m_time); // synchronize with server before we start following timeout countdown XSync(display, False); // Xlib inexplicably omits the ability to wait for an event with // a timeout. (it's inexplicable because there's no portable way // to do it.) we'll poll until we have what we're looking for or // a timeout expires. we use a timeout so we don't get locked up // by badly behaved selection owners. XEvent xevent; std::vector events; CStopwatch timeout(true); static const double s_timeout = 0.25; // FIXME -- is this too short? bool noWait = false; while (!m_done && !m_failed) { // fail if timeout has expired if (timeout.getTime() >= s_timeout) { m_failed = true; break; } // process events if any otherwise sleep if (noWait || XPending(display) > 0) { while (!m_done && !m_failed && (noWait || XPending(display) > 0)) { XNextEvent(display, &xevent); if (!processEvent(display, &xevent)) { // not processed so save it events.push_back(xevent); } else { // reset timer since we've made some progress timeout.reset(); // don't sleep anymore, just block waiting for events. // we're assuming here that the clipboard owner will // complete the protocol correctly. if we continue to // sleep we'll get very bad performance. noWait = true; } } } else { ARCH->sleep(0.01); } } // put unprocessed events back for (UInt32 i = events.size(); i > 0; --i) { XPutBackEvent(display, &events[i - 1]); } // restore mask XSelectInput(display, m_requestor, attr.your_event_mask); // return success or failure LOG((CLOG_DEBUG1 "request %s", m_failed ? "failed" : "succeeded")); return !m_failed; } bool CXWindowsClipboard::CICCCMGetClipboard::processEvent( Display* display, XEvent* xevent) { // process event switch (xevent->type) { case DestroyNotify: if (xevent->xdestroywindow.window == m_requestor) { m_failed = true; return true; } // not interested return false; case SelectionNotify: if (xevent->xselection.requestor == m_requestor) { // done if we can't convert if (xevent->xselection.property == None || xevent->xselection.property == m_atomNone) { m_done = true; return true; } // proceed if conversion successful else if (xevent->xselection.property == m_property) { m_reading = true; break; } } // otherwise not interested return false; case PropertyNotify: // proceed if conversion successful and we're receiving more data if (xevent->xproperty.window == m_requestor && xevent->xproperty.atom == m_property && xevent->xproperty.state == PropertyNewValue) { if (!m_reading) { // we haven't gotten the SelectionNotify yet return true; } break; } // otherwise not interested return false; default: // not interested return false; } // get the data from the property Atom target; const CString::size_type oldSize = m_data->size(); if (!CXWindowsUtil::getWindowProperty(display, m_requestor, m_property, m_data, &target, NULL, True)) { // unable to read property m_failed = true; return true; } // note if incremental. if we're already incremental then the // selection owner is busted. if the INCR property has no size // then the selection owner is busted. if (target == m_atomIncr) { if (m_incr) { m_failed = true; m_error = true; } else if (m_data->size() == oldSize) { m_failed = true; m_error = true; } else { m_incr = true; // discard INCR data *m_data = ""; } } // handle incremental chunks else if (m_incr) { // if first incremental chunk then save target if (oldSize == 0) { LOG((CLOG_DEBUG1 " INCR first chunk, target %s", CXWindowsUtil::atomToString(display, target).c_str())); *m_actualTarget = target; } // secondary chunks must have the same target else { if (target != *m_actualTarget) { LOG((CLOG_WARN " INCR target mismatch")); m_failed = true; m_error = true; } } // note if this is the final chunk if (m_data->size() == oldSize) { LOG((CLOG_DEBUG1 " INCR final chunk: %d bytes total", m_data->size())); m_done = true; } } // not incremental; save the target. else { LOG((CLOG_DEBUG1 " target %s", CXWindowsUtil::atomToString(display, target).c_str())); *m_actualTarget = target; m_done = true; } // this event has been processed LOGC(!m_incr, (CLOG_DEBUG1 " got data, %d bytes", m_data->size())); return true; } // // CXWindowsClipboard::CReply // CXWindowsClipboard::CReply::CReply(Window requestor, Atom target, ::Time time) : m_requestor(requestor), m_target(target), m_time(time), m_property(None), m_replied(false), m_done(false), m_data(), m_type(None), m_format(32), m_ptr(0) { // do nothing } CXWindowsClipboard::CReply::CReply(Window requestor, Atom target, ::Time time, Atom property, const CString& data, Atom type, int format) : m_requestor(requestor), m_target(target), m_time(time), m_property(property), m_replied(false), m_done(false), m_data(data), m_type(type), m_format(format), m_ptr(0) { // do nothing }