summaryrefslogtreecommitdiffstats
path: root/src/lib/platform/XWindowsClipboard.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/platform/XWindowsClipboard.cpp')
-rw-r--r--src/lib/platform/XWindowsClipboard.cpp1525
1 files changed, 1525 insertions, 0 deletions
diff --git a/src/lib/platform/XWindowsClipboard.cpp b/src/lib/platform/XWindowsClipboard.cpp
new file mode 100644
index 0000000..8e7d864
--- /dev/null
+++ b/src/lib/platform/XWindowsClipboard.cpp
@@ -0,0 +1,1525 @@
+/*
+ * barrier -- mouse and keyboard sharing utility
+ * Copyright (C) 2012-2016 Symless Ltd.
+ * 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 LICENSE 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.
+ *
+ * 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 "platform/XWindowsClipboard.h"
+
+#include "platform/XWindowsClipboardTextConverter.h"
+#include "platform/XWindowsClipboardUCS2Converter.h"
+#include "platform/XWindowsClipboardUTF8Converter.h"
+#include "platform/XWindowsClipboardHTMLConverter.h"
+#include "platform/XWindowsClipboardBMPConverter.h"
+#include "platform/XWindowsUtil.h"
+#include "mt/Thread.h"
+#include "arch/Arch.h"
+#include "base/Log.h"
+#include "base/Stopwatch.h"
+#include "common/stdvector.h"
+
+#include <cstdio>
+#include <cstring>
+#include <X11/Xatom.h>
+
+//
+// XWindowsClipboard
+//
+
+XWindowsClipboard::XWindowsClipboard(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 XWindowsClipboardHTMLConverter(m_display,
+ "text/html"));
+ m_converters.push_back(new XWindowsClipboardBMPConverter(m_display));
+ m_converters.push_back(new XWindowsClipboardUTF8Converter(m_display,
+ "text/plain;charset=UTF-8"));
+ m_converters.push_back(new XWindowsClipboardUTF8Converter(m_display,
+ "UTF8_STRING"));
+ m_converters.push_back(new XWindowsClipboardUCS2Converter(m_display,
+ "text/plain;charset=ISO-10646-UCS-2"));
+ m_converters.push_back(new XWindowsClipboardUCS2Converter(m_display,
+ "text/unicode"));
+ m_converters.push_back(new XWindowsClipboardTextConverter(m_display,
+ "text/plain"));
+ m_converters.push_back(new XWindowsClipboardTextConverter(m_display,
+ "STRING"));
+
+ // we have no data
+ clearCache();
+}
+
+XWindowsClipboard::~XWindowsClipboard()
+{
+ clearReplies();
+ clearConverters();
+}
+
+void
+XWindowsClipboard::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
+XWindowsClipboard::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, XWindowsUtil::atomToString(m_display, target).c_str(), requestor, XWindowsUtil::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 Reply(requestor, target, time));
+ }
+
+ // send notifications that are pending
+ pushReplies();
+}
+
+bool
+XWindowsClipboard::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
+ String 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 Reply(requestor, target, time,
+ property, data, type, format));
+ return true;
+ }
+ else {
+ // failure
+ LOG((CLOG_DEBUG1 "failed"));
+ insertReply(new Reply(requestor, target, time));
+ return false;
+ }
+}
+
+bool
+XWindowsClipboard::processRequest(Window requestor,
+ ::Time /*time*/, Atom property)
+{
+ ReplyMap::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", XWindowsUtil::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.
+ ReplyList& replies = index->second;
+ for (ReplyList::iterator index2 = replies.begin();
+ index2 != replies.end(); ++index2) {
+ Reply* 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
+XWindowsClipboard::destroyRequest(Window requestor)
+{
+ ReplyMap::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
+XWindowsClipboard::getWindow() const
+{
+ return m_window;
+}
+
+Atom
+XWindowsClipboard::getSelection() const
+{
+ return m_selection;
+}
+
+bool
+XWindowsClipboard::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
+XWindowsClipboard::add(EFormat format, const String& 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
+XWindowsClipboard::open(Time time) const
+{
+ if (m_open) {
+ LOG((CLOG_DEBUG "failed to open clipboard: already opened"));
+ return false;
+ }
+
+ 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
+XWindowsClipboard::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
+XWindowsClipboard::getTime() const
+{
+ checkCache();
+ return m_timeOwned;
+}
+
+bool
+XWindowsClipboard::has(EFormat format) const
+{
+ assert(m_open);
+
+ fillCache();
+ return m_added[format];
+}
+
+String
+XWindowsClipboard::get(EFormat format) const
+{
+ assert(m_open);
+
+ fillCache();
+ return m_data[format];
+}
+
+void
+XWindowsClipboard::clearConverters()
+{
+ for (ConverterList::iterator index = m_converters.begin();
+ index != m_converters.end(); ++index) {
+ delete *index;
+ }
+ m_converters.clear();
+}
+
+IXWindowsClipboardConverter*
+XWindowsClipboard::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", XWindowsUtil::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
+XWindowsClipboard::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
+XWindowsClipboard::clearCache() const
+{
+ const_cast<XWindowsClipboard*>(this)->doClearCache();
+}
+
+void
+XWindowsClipboard::doClearCache()
+{
+ m_checkCache = false;
+ m_cached = false;
+ for (SInt32 index = 0; index < kNumFormats; ++index) {
+ m_data[index] = "";
+ m_added[index] = false;
+ }
+}
+
+void
+XWindowsClipboard::fillCache() const
+{
+ // get the selection data if not already cached
+ checkCache();
+ if (!m_cached) {
+ const_cast<XWindowsClipboard*>(this)->doFillCache();
+ }
+}
+
+void
+XWindowsClipboard::doFillCache()
+{
+ if (m_motif) {
+ motifFillCache();
+ }
+ else {
+ icccmFillCache();
+ }
+ m_checkCache = false;
+ m_cached = true;
+ m_cacheTime = m_timeOwned;
+}
+
+void
+XWindowsClipboard::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;
+ String data;
+ if (!icccmGetSelection(atomTargets, &target, &data) ||
+ (target != m_atomAtom && target != m_atomTargets)) {
+ LOG((CLOG_DEBUG1 "selection doesn't support TARGETS"));
+ data = "";
+ XWindowsUtil::appendAtomData(data, XA_STRING);
+ }
+
+ XWindowsUtil::convertAtomProperty(data);
+ const Atom* targets = reinterpret_cast<const Atom*>(data.data()); // TODO: Safe?
+ const UInt32 numTargets = data.size() / sizeof(Atom);
+ LOG((CLOG_DEBUG " available targets: %s", XWindowsUtil::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;
+ String targetData;
+ if (!icccmGetSelection(target, &actualTarget, &targetData)) {
+ LOG((CLOG_DEBUG1 " no data for target %s", XWindowsUtil::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, XWindowsUtil::atomToString(m_display, target).c_str(), targetData.size(), targetData.size() == 1 ? "byte" : "bytes"));
+ }
+}
+
+bool
+XWindowsClipboard::icccmGetSelection(Atom target,
+ Atom* actualTarget, String* 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", XWindowsUtil::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", XWindowsUtil::atomToString(m_display, target).c_str()));
+ return false;
+ }
+ return true;
+}
+
+IClipboard::Time
+XWindowsClipboard::icccmGetTime() const
+{
+ Atom actualTarget;
+ String data;
+ if (icccmGetSelection(m_atomTimestamp, &actualTarget, &data) &&
+ actualTarget == m_atomInteger) {
+ Time time = *reinterpret_cast<const Time*>(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
+XWindowsClipboard::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 = XWindowsUtil::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
+XWindowsClipboard::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 = XWindowsUtil::getCurrentTime(m_display, m_window);
+ XSetSelectionOwner(m_display, m_atomMotifClipLock, None, time);
+}
+
+bool
+XWindowsClipboard::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;
+ String data;
+ Window root = RootWindow(m_display, DefaultScreen(m_display));
+ if (!XWindowsUtil::getWindowProperty(m_display, root,
+ m_atomMotifClipHeader,
+ &data, &target, &format, False)) {
+ return false;
+ }
+
+ // check the owner window against the current clipboard owner
+ if (data.size() >= sizeof(MotifClipHeader)) {
+ MotifClipHeader header;
+ std::memcpy (&header, data.data(), sizeof(header));
+ if ((header.m_id == kMotifClipHeader) &&
+ (static_cast<Window>(header.m_selectionOwner) == owner)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void
+XWindowsClipboard::motifFillCache()
+{
+ LOG((CLOG_DEBUG "Motif fill clipboard %d", m_id));
+
+ // get the Motif clipboard header property from the root window
+ Atom target;
+ SInt32 format;
+ String data;
+ Window root = RootWindow(m_display, DefaultScreen(m_display));
+ if (!XWindowsUtil::getWindowProperty(m_display, root,
+ m_atomMotifClipHeader,
+ &data, &target, &format, False)) {
+ return;
+ }
+
+ MotifClipHeader header;
+ if (data.size() < sizeof(header)) { // check that the header is okay
+ return;
+ }
+ std::memcpy (&header, data.data(), sizeof(header));
+ if (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 (!XWindowsUtil::getWindowProperty(m_display, root,
+ atomItem, &data,
+ &target, &format, False)) {
+ return;
+ }
+
+ MotifClipItem item;
+ if (data.size() < sizeof(item)) { // check that the item is okay
+ return;
+ }
+ std::memcpy (&item, data.data(), sizeof(item));
+ if (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<const SInt32*>(item.m_size +
+ static_cast<const char*>(data.data()));
+
+ // get the available formats
+ typedef std::map<Atom, String> MotifFormatMap;
+ MotifFormatMap 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);
+ String data;
+ if (!XWindowsUtil::getWindowProperty(m_display, root,
+ atomFormat, &data,
+ &target, &format, False)) {
+ continue;
+ }
+
+ // check that the format is okay
+ MotifClipFormat motifFormat;
+ if (data.size() < sizeof(motifFormat)) {
+ continue;
+ }
+ std::memcpy (&motifFormat, data.data(), sizeof(motifFormat));
+ if (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
+ MotifFormatMap::const_iterator index2 =
+ motifFormats.find(converter->getAtom());
+ if (index2 == motifFormats.end()) {
+ continue;
+ }
+
+ // get format
+ MotifClipFormat motifFormat;
+ std::memcpy (&motifFormat, index2->second.data(), sizeof(motifFormat));
+ const Atom target = motifFormat.m_type;
+
+ // get the data (finally)
+ Atom actualTarget;
+ String targetData;
+ if (!motifGetSelection(&motifFormat, &actualTarget, &targetData)) {
+ LOG((CLOG_DEBUG1 " no data for target %s", XWindowsUtil::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, XWindowsUtil::atomToString(m_display, target).c_str()));
+ }
+}
+
+bool
+XWindowsClipboard::motifGetSelection(const MotifClipFormat* format,
+ Atom* actualTarget, String* 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 XWindowsUtil::getWindowProperty(m_display, root,
+ target, data,
+ actualTarget, NULL, False);
+}
+
+IClipboard::Time
+XWindowsClipboard::motifGetTime() const
+{
+ return icccmGetTime();
+}
+
+bool
+XWindowsClipboard::insertMultipleReply(Window requestor,
+ ::Time time, Atom property)
+{
+ // get the requested targets
+ Atom target;
+ SInt32 format;
+ String data;
+ if (!XWindowsUtil::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
+ XWindowsUtil::convertAtomProperty(data);
+ const Atom* targets = reinterpret_cast<const Atom*>(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
+ XWindowsUtil::replaceAtomData(data, i, None);
+ changed = true;
+ }
+ }
+
+ // update the targets property if we changed it
+ if (changed) {
+ XWindowsUtil::setWindowProperty(m_display, requestor,
+ property, data.data(), data.size(),
+ target, format);
+ }
+
+ // add reply for MULTIPLE request
+ insertReply(new Reply(requestor, m_atomMultiple,
+ time, property, String(), None, 32));
+
+ return true;
+}
+
+void
+XWindowsClipboard::insertReply(Reply* 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;
+ {
+ XWindowsUtil::ErrorLock 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
+XWindowsClipboard::pushReplies()
+{
+ // send the first reply for each window if that reply hasn't
+ // been sent yet.
+ for (ReplyMap::iterator index = m_replies.begin();
+ index != m_replies.end(); ) {
+ assert(!index->second.empty());
+ ReplyList::iterator listit = index->second.begin();
+ while (listit != index->second.end()) {
+ if (!(*listit)->m_replied)
+ break;
+ ++listit;
+ }
+ if (listit != index->second.end() && !(*listit)->m_replied) {
+ pushReplies(index, index->second, listit);
+ }
+ else {
+ ++index;
+ }
+ }
+}
+
+void
+XWindowsClipboard::pushReplies(ReplyMap::iterator& mapIndex,
+ ReplyList& replies, ReplyList::iterator index)
+{
+ Reply* 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()) {
+ XWindowsUtil::ErrorLock lock(m_display);
+ Window requestor = mapIndex->first;
+ XSelectInput(m_display, requestor, m_eventMasks[requestor]);
+ m_replies.erase(mapIndex++);
+ m_eventMasks.erase(requestor);
+ }
+ else {
+ ++mapIndex;
+ }
+}
+
+bool
+XWindowsClipboard::sendReply(Reply* 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 (!XWindowsUtil::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 (!XWindowsUtil::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) {
+ XWindowsUtil::ErrorLock 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;
+ XWindowsUtil::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() >= kDEBUG2) {
+ XWindowsUtil::ErrorLock 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;
+ String data;
+ char* name = XGetAtomName(m_display, props[i]);
+ if (!XWindowsUtil::getWindowProperty(m_display,
+ reply->m_requestor,
+ props[i], &data, &target, NULL, False)) {
+ LOG((CLOG_DEBUG2 " %s: <can't read property>", name));
+ }
+ else {
+ // if there are any non-ascii characters in string
+ // then print the binary data.
+ static const char* hex = "0123456789abcdef";
+ for (String::size_type j = 0; j < data.size(); ++j) {
+ if (data[j] < 32 || data[j] > 126) {
+ String 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
+XWindowsClipboard::clearReplies()
+{
+ for (ReplyMap::iterator index = m_replies.begin();
+ index != m_replies.end(); ++index) {
+ clearReplies(index->second);
+ }
+ m_replies.clear();
+ m_eventMasks.clear();
+}
+
+void
+XWindowsClipboard::clearReplies(ReplyList& replies)
+{
+ for (ReplyList::iterator index = replies.begin();
+ index != replies.end(); ++index) {
+ delete *index;
+ }
+ replies.clear();
+}
+
+void
+XWindowsClipboard::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;
+ XWindowsUtil::ErrorLock lock(m_display);
+ XSendEvent(m_display, requestor, False, 0, &event);
+}
+
+bool
+XWindowsClipboard::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 = XWindowsUtil::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
+XWindowsClipboard::getTargetsData(String& data, int* format) const
+{
+ assert(format != NULL);
+
+ // add standard targets
+ XWindowsUtil::appendAtomData(data, m_atomTargets);
+ XWindowsUtil::appendAtomData(data, m_atomMultiple);
+ XWindowsUtil::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()]) {
+ XWindowsUtil::appendAtomData(data, converter->getAtom());
+ }
+ }
+
+ *format = 32;
+ return m_atomAtom;
+}
+
+Atom
+XWindowsClipboard::getTimestampData(String& data, int* format) const
+{
+ assert(format != NULL);
+
+ checkCache();
+ XWindowsUtil::appendTimeData(data, m_timeOwned);
+ *format = 32;
+ return m_atomInteger;
+}
+
+
+//
+// XWindowsClipboard::CICCCMGetClipboard
+//
+
+XWindowsClipboard::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
+}
+
+XWindowsClipboard::CICCCMGetClipboard::~CICCCMGetClipboard()
+{
+ // do nothing
+}
+
+bool
+XWindowsClipboard::CICCCMGetClipboard::readClipboard(Display* display,
+ Atom selection, Atom target, Atom* actualTarget, String* data)
+{
+ assert(actualTarget != NULL);
+ assert(data != NULL);
+
+ LOG((CLOG_DEBUG1 "request selection=%s, target=%s, window=%x", XWindowsUtil::atomToString(display, selection).c_str(), XWindowsUtil::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<XEvent> events;
+ Stopwatch timeout(false); // timer not stopped, not triggered
+ 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 after %fs", m_failed ? "failed" : "succeeded", timeout.getTime()));
+ return !m_failed;
+}
+
+bool
+XWindowsClipboard::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 String::size_type oldSize = m_data->size();
+ if (!XWindowsUtil::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", XWindowsUtil::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", XWindowsUtil::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;
+}
+
+
+//
+// XWindowsClipboard::Reply
+//
+
+XWindowsClipboard::Reply::Reply(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
+}
+
+XWindowsClipboard::Reply::Reply(Window requestor, Atom target, ::Time time,
+ Atom property, const String& 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
+}