mirror of
https://github.com/debauchee/barrier.git
synced 2026-02-11 14:15:46 +08:00
be accessed now between open()/close(). ownership of the clipboard is asserted via the empty() method. this parallels the win32 model (but the win32 code hasn't been updated yet). refactored X11 clipboard code. moved the bulk of it into CXWindowsClipboard and moved some comment event handling into CXWindowsScreen. changed how requests are processed into a hopefully easier to understand model. added support for getting clipboard from and sending clipboard to motif (or at least lesstif) clients. sending to lesstif required a hack to work around an apparent bug in lesstif.
1298 lines
34 KiB
C++
1298 lines
34 KiB
C++
#include "CXWindowsClipboard.h"
|
|
#include "CXWindowsUtil.h"
|
|
#include "CLog.h"
|
|
#include "CStopwatch.h"
|
|
#include "CThread.h"
|
|
#include <stdio.h>
|
|
#include <X11/Xatom.h>
|
|
|
|
//
|
|
// 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_atomAtom = XInternAtom(m_display, "ATOM", False);
|
|
m_atomAtomPair = XInternAtom(m_display, "ATOM_PAIR", False);
|
|
m_atomInteger = XInternAtom(m_display, "INTEGER", False);
|
|
m_atomData = XInternAtom(m_display, "CLIP_TEMPORARY", False);
|
|
m_atomINCR = XInternAtom(m_display, "INCR", False);
|
|
m_atomString = XInternAtom(m_display, "STRING", False);
|
|
m_atomText = XInternAtom(m_display, "TEXT", False);
|
|
m_atomCompoundText = XInternAtom(m_display, "COMPOUND_TEXT", 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;
|
|
}
|
|
|
|
// we have no data
|
|
clearCache();
|
|
}
|
|
|
|
CXWindowsClipboard::~CXWindowsClipboard()
|
|
{
|
|
clearReplies();
|
|
}
|
|
|
|
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_DEBUG "request for clipboard %d, target %d by 0x%08x (property=%d)", m_selection, target, requestor, property));
|
|
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_DEBUG "failed, not owned at time %d", time));
|
|
}
|
|
}
|
|
|
|
if (!success) {
|
|
// send failure
|
|
log((CLOG_DEBUG "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 if (target == m_atomString ||
|
|
target == m_atomText) {
|
|
type = getStringData(data, &format);
|
|
}
|
|
|
|
if (type != None) {
|
|
// success
|
|
log((CLOG_DEBUG "success"));
|
|
insertReply(new CReply(requestor, target, time,
|
|
property, data, type, format));
|
|
return true;
|
|
}
|
|
else {
|
|
// failure
|
|
log((CLOG_DEBUG "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 %d delete from 0x08%x", property, 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);
|
|
|
|
// 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;
|
|
|
|
// 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();
|
|
}
|
|
|
|
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
|
|
{
|
|
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];
|
|
}
|
|
|
|
IClipboard::EFormat CXWindowsClipboard::getFormat(Atom src) const
|
|
{
|
|
// FIXME -- handle more formats (especially mime-type-like formats
|
|
// and various character encodings like unicode).
|
|
if (src == m_atomString ||
|
|
src == m_atomText /*||
|
|
src == m_atomCompoundText*/)
|
|
return IClipboard::kText;
|
|
return IClipboard::kNumFormats;
|
|
}
|
|
|
|
void CXWindowsClipboard::clearCache() const
|
|
{
|
|
const_cast<CXWindowsClipboard*>(this)->doClearCache();
|
|
}
|
|
|
|
void CXWindowsClipboard::doClearCache()
|
|
{
|
|
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
|
|
if (!m_cached) {
|
|
const_cast<CXWindowsClipboard*>(this)->doFillCache();
|
|
}
|
|
}
|
|
|
|
void CXWindowsClipboard::doFillCache()
|
|
{
|
|
if (m_motif) {
|
|
motifFillCache();
|
|
}
|
|
else {
|
|
icccmFillCache();
|
|
}
|
|
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.
|
|
const Atom atomTargets = m_atomTargets;
|
|
Atom target;
|
|
CString data;
|
|
if (!icccmGetSelection(atomTargets, &target, &data)) {
|
|
log((CLOG_DEBUG1 "selection doesn't support TARGETS"));
|
|
data = "";
|
|
|
|
target = XA_STRING;
|
|
data.append(reinterpret_cast<char*>(&target), sizeof(target));
|
|
}
|
|
|
|
// try getting each format
|
|
const Atom* targets = reinterpret_cast<const Atom*>(data.data());
|
|
const UInt32 numTargets = data.size() / sizeof(Atom);
|
|
for (UInt32 i = 0; i < numTargets; ++i) {
|
|
// determine the expected clipboard format
|
|
Atom target = targets[i];
|
|
IClipboard::EFormat expectedFormat = getFormat(target);
|
|
if (expectedFormat == IClipboard::kNumFormats) {
|
|
log((CLOG_DEBUG1 " no format for target %d", target));
|
|
continue;
|
|
}
|
|
log((CLOG_DEBUG1 " source target %d -> %d", target, expectedFormat));
|
|
|
|
// skip already handled targets
|
|
if (m_added[expectedFormat]) {
|
|
log((CLOG_DEBUG1 " skipping handled format %d", expectedFormat));
|
|
continue;
|
|
}
|
|
|
|
Atom actualTarget;
|
|
CString targetData;
|
|
if (!icccmGetSelection(target, &actualTarget, &targetData)) {
|
|
log((CLOG_DEBUG1 " no data for target", target));
|
|
continue;
|
|
}
|
|
logc(actualTarget != target, (CLOG_DEBUG1 " actual target is %d", actualTarget));
|
|
|
|
// use the actual format, not the expected
|
|
IClipboard::EFormat actualFormat = getFormat(actualTarget);
|
|
if (actualFormat == IClipboard::kNumFormats) {
|
|
log((CLOG_DEBUG1 " no format for target %d", actualTarget));
|
|
continue;
|
|
}
|
|
if (m_added[actualFormat]) {
|
|
log((CLOG_DEBUG1 " skipping handled format %d", actualFormat));
|
|
continue;
|
|
}
|
|
|
|
// add to clipboard and note we've done it
|
|
m_data[actualFormat] = targetData;
|
|
m_added[actualFormat] = true;
|
|
log((CLOG_DEBUG " added format %d for target %d", actualFormat, target));
|
|
}
|
|
}
|
|
|
|
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 %d", target));
|
|
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 %d", target));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
IClipboard::Time CXWindowsClipboard::icccmGetTime() const
|
|
{
|
|
Atom actualTarget;
|
|
CString data;
|
|
if (icccmGetSelection(m_atomTimestamp, &actualTarget, &data) &&
|
|
actualTarget == m_atomTimestamp) {
|
|
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 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) {
|
|
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) {
|
|
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;
|
|
}
|
|
if (target != m_atomMotifClipHeader) {
|
|
return false;
|
|
}
|
|
|
|
// check the owner window against the current clipboard owner
|
|
const CMotifClipHeader* header =
|
|
reinterpret_cast<const CMotifClipHeader*>(data.data());
|
|
if (data.size() >= sizeof(CMotifClipHeader) &&
|
|
header->m_id == kMotifClipHeader) {
|
|
if (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;
|
|
}
|
|
if (target != m_atomMotifClipHeader) {
|
|
return;
|
|
}
|
|
|
|
// check that the header is okay
|
|
const CMotifClipHeader* header =
|
|
reinterpret_cast<const CMotifClipHeader*>(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;
|
|
}
|
|
if (target != atomItem) {
|
|
return;
|
|
}
|
|
|
|
// check that the item is okay
|
|
const CMotifClipItem* item =
|
|
reinterpret_cast<const CMotifClipItem*>(data.data());
|
|
if (data.size() < sizeof(CMotifClipItem) ||
|
|
item->m_id != kMotifClipItem ||
|
|
item->m_numFormats < 1) {
|
|
return;
|
|
}
|
|
|
|
// convert each available format
|
|
for (SInt32 i = 0; i < item->m_numFormats; ++i) {
|
|
// get Motif format property from the root window
|
|
sprintf(name, "_MOTIF_CLIP_ITEM_%d", item->m_formats[i]);
|
|
Atom atomFormat = XInternAtom(m_display, name, False);
|
|
CString data;
|
|
if (!CXWindowsUtil::getWindowProperty(m_display, root,
|
|
atomFormat, &data,
|
|
&target, &format, False)) {
|
|
continue;
|
|
}
|
|
if (target != atomFormat) {
|
|
continue;
|
|
}
|
|
|
|
// check that the format is okay
|
|
const CMotifClipFormat* motifFormat =
|
|
reinterpret_cast<const CMotifClipFormat*>(data.data());
|
|
if (data.size() < sizeof(CMotifClipFormat) ||
|
|
motifFormat->m_id != kMotifClipFormat ||
|
|
motifFormat->m_length < 0 ||
|
|
motifFormat->m_type == None) {
|
|
continue;
|
|
}
|
|
|
|
// determine the expected clipboard format
|
|
Atom target = motifFormat->m_type;
|
|
IClipboard::EFormat expectedFormat = getFormat(target);
|
|
if (expectedFormat == IClipboard::kNumFormats) {
|
|
log((CLOG_DEBUG1 " no format for target %d", target));
|
|
continue;
|
|
}
|
|
log((CLOG_DEBUG1 " source target %d -> %d", target, expectedFormat));
|
|
|
|
// skip already handled targets
|
|
if (m_added[expectedFormat]) {
|
|
log((CLOG_DEBUG1 " skipping handled format %d", expectedFormat));
|
|
continue;
|
|
}
|
|
|
|
// get the data (finally)
|
|
SInt32 length = motifFormat->m_length;
|
|
sprintf(name, "_MOTIF_CLIP_ITEM_%d", motifFormat->m_data);
|
|
Atom atomData = XInternAtom(m_display, name, False);
|
|
data = "";
|
|
if (!CXWindowsUtil::getWindowProperty(m_display, root,
|
|
atomData, &data,
|
|
&target, &format, False)) {
|
|
continue;
|
|
}
|
|
if (target != atomData) {
|
|
continue;
|
|
}
|
|
|
|
// truncate data to length specified in the format
|
|
data.erase(length);
|
|
|
|
// add to clipboard and note we've done it
|
|
m_data[expectedFormat] = data;
|
|
m_added[expectedFormat] = true;
|
|
log((CLOG_DEBUG " added format %d for target %d", expectedFormat, motifFormat->m_type));
|
|
}
|
|
}
|
|
|
|
IClipboard::Time CXWindowsClipboard::motifGetTime() const
|
|
{
|
|
// FIXME -- does Motif report this?
|
|
return 0;
|
|
}
|
|
|
|
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
|
|
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
|
|
static const Atom none = None;
|
|
data.replace(i * sizeof(Atom), sizeof(Atom),
|
|
reinterpret_cast<const char*>(&none),
|
|
sizeof(Atom));
|
|
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) {
|
|
XWindowAttributes attr;
|
|
XGetWindowAttributes(m_display, reply->m_requestor, &attr);
|
|
XSelectInput(m_display, reply->m_requestor, attr.your_event_mask |
|
|
StructureNotifyMask | PropertyChangeMask);
|
|
m_eventMasks[reply->m_requestor] = attr.your_event_mask;
|
|
}
|
|
}
|
|
|
|
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()) {
|
|
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 = 4 * 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) {
|
|
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;
|
|
|
|
// HACK -- work around apparent bug in lesstif, which doesn't
|
|
// wait around for the SelectionNotify then gets confused when
|
|
// it sees it the next time it requests the selection. if it
|
|
// looks like a lesstif requestor window then don't send the
|
|
// SelectionNotify. it looks like a lesstif requestor if:
|
|
// it has a _MOTIF_CLIP_LOCK_ACCESS_VALID property
|
|
// it does not have a GDK_SELECTION property
|
|
CString dummy;
|
|
if (!CXWindowsUtil::getWindowProperty(m_display,
|
|
reply->m_requestor,
|
|
m_atomMotifClipAccess,
|
|
&dummy, NULL, NULL, False) ||
|
|
CXWindowsUtil::getWindowProperty(m_display,
|
|
reply->m_requestor,
|
|
m_atomGDKSelection,
|
|
&dummy, NULL, NULL, False)) {
|
|
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;
|
|
XSendEvent(m_display, requestor, False, 0, &event);
|
|
}
|
|
|
|
bool CXWindowsClipboard::wasOwnedAtTime(
|
|
::Time time) const
|
|
{
|
|
// not owned if we've never owned the selection
|
|
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);
|
|
|
|
// construct response
|
|
Atom atom;
|
|
atom = m_atomTargets;
|
|
data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
|
|
atom = m_atomMultiple;
|
|
data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
|
|
atom = m_atomTimestamp;
|
|
data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
|
|
if (m_added[kText]) {
|
|
atom = m_atomString;
|
|
data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
|
|
atom = m_atomText;
|
|
data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
|
|
}
|
|
|
|
*format = 32;
|
|
return m_atomTargets;
|
|
}
|
|
|
|
Atom CXWindowsClipboard::getTimestampData(
|
|
CString& data, int* format) const
|
|
{
|
|
assert(format != NULL);
|
|
|
|
assert(sizeof(m_timeOwned) == 4);
|
|
data.append(reinterpret_cast<const char*>(&m_timeOwned), 4);
|
|
*format = 32;
|
|
return m_atomTimestamp;
|
|
}
|
|
|
|
Atom CXWindowsClipboard::getStringData(
|
|
CString& data, int* format) const
|
|
{
|
|
assert(format != NULL);
|
|
|
|
if (m_added[kText]) {
|
|
data = m_data[kText];
|
|
*format = 8;
|
|
return m_atomString;
|
|
}
|
|
else {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// 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=%d, target=%d, window=%x", selection, target, m_requestor));
|
|
|
|
// 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);
|
|
|
|
// process selection events. use a timeout so we don't get
|
|
// screwed by a bad selection owner.
|
|
CStopwatch timer(true);
|
|
XEvent xevent;
|
|
while (!m_done && !m_failed) {
|
|
// return false if we've timed-out
|
|
if (timer.getTime() >= 0.2) {
|
|
log((CLOG_DEBUG1 "request timed out"));
|
|
XSelectInput(display, m_requestor, attr.your_event_mask);
|
|
return false;
|
|
}
|
|
|
|
// process events
|
|
if (!XCheckIfEvent(display, &xevent,
|
|
&CXWindowsClipboard::CICCCMGetClipboard::eventPredicate,
|
|
reinterpret_cast<XPointer>(this))) {
|
|
// wait a bit to avoid spinning
|
|
CThread::sleep(0.05);
|
|
}
|
|
}
|
|
|
|
// 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::doEventPredicate(
|
|
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) {
|
|
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) {
|
|
return false;
|
|
}
|
|
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 == XInternAtom(display, "INCR", False)) {
|
|
log((CLOG_INFO " INCR")); // FIXME
|
|
if (m_incr) {
|
|
log((CLOG_INFO " INCR repeat")); // FIXME
|
|
m_failed = true;
|
|
m_error = true;
|
|
}
|
|
else if (m_data->size() == oldSize) {
|
|
log((CLOG_INFO " INCR zero size")); // FIXME
|
|
m_failed = true;
|
|
m_error = true;
|
|
}
|
|
else {
|
|
log((CLOG_INFO " INCR start")); // FIXME
|
|
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 %d", target));
|
|
*m_actualTarget = target;
|
|
}
|
|
|
|
// secondary chunks must have the same target
|
|
else {
|
|
log((CLOG_INFO " INCR secondary chunk")); // FIXME
|
|
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 %d", target));
|
|
*m_actualTarget = target;
|
|
m_done = true;
|
|
}
|
|
|
|
// say we're not interested in this event if the conversion is
|
|
// incremental. that'll cause this method to be called again
|
|
// when there's more data. we finally finish the incremental
|
|
// copy when we read a 0 byte property.
|
|
logc(!m_incr, (CLOG_DEBUG1 " got data, %d bytes", m_data->size()));
|
|
return !m_incr;
|
|
}
|
|
|
|
Bool CXWindowsClipboard::CICCCMGetClipboard::eventPredicate(
|
|
Display* display,
|
|
XEvent* xevent,
|
|
XPointer arg)
|
|
{
|
|
CICCCMGetClipboard* self = reinterpret_cast<CICCCMGetClipboard*>(arg);
|
|
return self->doEventPredicate(display, xevent) ? True : False;
|
|
}
|
|
|
|
|
|
//
|
|
// 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
|
|
}
|