12to11/select.c
hujianwei b022f0dbd9 Fix handling of MULTIPLE selections and add test
* select.c (SignalConversionPerformed): Improve diagnostic
messages.
(ConvertSelectionMultiple): Use correct atoms.
(HandleSelectionRequest): Allow selection requests with target
MULTIPLE.

* tests/Imakefile (SRCS12, OBJS12): Add select_helper_multiple.c
and .o.
(PROGRAMS): Add select_helper_multiple.
(select_helper_multiple): New program.
* tests/select_helper.c (main): Fix typo.

* tests/select_test.c (verify_sample_text_multiple): New function.
(test_single_step): Call it.
* tests/svnignore.txt: Add select_helper_multiple.
2022-11-12 08:07:43 +00:00

1920 lines
50 KiB
C

/* Wayland compositor running on top of an X serer.
Copyright (C) 2022 to various contributors.
This file is part of 12to11.
12to11 is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
12to11 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 12to11. If not, see <https://www.gnu.org/licenses/>. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "compositor.h"
typedef struct _PropertyAtom PropertyAtom;
typedef struct _SelectInputData SelectInputData;
typedef struct _SelectionOwnerInfo SelectionOwnerInfo;
typedef struct _QueuedTransfer QueuedTransfer;
typedef struct _MultipleRecord MultipleRecord;
/* Structure representing a single property allocated for selection
transfers.
Since Wayland data sources are not read from a wl_surface, there is
no way to associate each transfer with a specific window. In
addition, the compositor might want to obtain selection data in the
background as well. Because of this, we must rely on all ongoing
data transfers from other owners having a unique property. */
struct _PropertyAtom
{
/* The atom in question. */
Atom atom;
/* The id used to generate the atom. */
uint32_t counter;
/* The next and last atoms in this chain. */
PropertyAtom *next, *last;
};
struct _SelectionOwnerInfo
{
/* When this selection was last owned. */
Timestamp time;
/* The targets of this atom. */
Atom *targets;
/* The number of targets. */
int ntargets;
/* The callback for this selection. NULL means the selection is no
longer owned. */
GetDataFunc (*get_transfer_function) (WriteTransfer *,
Atom, Atom *);
};
enum
{
IsFinished = 1,
IsIncr = (1 << 1),
IsWaitingForChunk = (1 << 2),
IsStarted = (1 << 3),
IsFailed = (1 << 4),
IsWaitingForDelete = (1 << 5),
IsWaitingForIncr = (1 << 6),
IsReadable = (1 << 7),
IsFlushed = (1 << 8),
};
struct _ReadTransfer
{
/* The selection owner. */
Window owner;
/* Some state associated with this transfer. */
int state;
/* The atom being used to transfer data from the selection
owner. */
PropertyAtom *property;
/* The timestamp at which the selection request was issued. */
Time time;
/* The selection and target that are being requested. */
Atom selection, target;
/* The current offset, in 4-byte multiples, into the property data.
Reset each time a new chunk of data is available. */
unsigned long read_offset;
/* The format of the property data. */
unsigned long read_format;
/* A function called once selection data begins to be read from the
selection. The second arg is the type of the selection data, and
the third arg is the format of the selection data. */
void (*data_start_func) (ReadTransfer *, Atom, int);
/* A function called once data some data can be read from the
selection. It may be called multiple times, depending on how big
the response from the owner is.
The args are as follows: a pointer to the transfer, the atom
describing the type of the data, the format of the data (see the
doc for XGetWindowProperty), and the size of the data that can be
read in bytes. */
void (*data_read_func) (ReadTransfer *, Atom, int, ptrdiff_t);
/* A function called after the selection transfer completes. The
second arg means whether or not the transfer was successful. */
Bool (*data_finish_func) (ReadTransfer *, Bool);
/* Caller-specified data for those functions. */
void *data;
/* The next and last transfers on this chain. */
ReadTransfer *next, *last;
/* A timer that times out after 5 seconds of inactivity. */
Timer *timeout;
};
struct _WriteTransfer
{
/* The next and last transfers on this chain. */
WriteTransfer *next, *last;
/* The requestor of the transfer. Spelt with an "O" because that's
what the ICCCM does. */
Window requestor;
/* Some state associated with this transfer. */
int state;
/* The selection being requested from us, the target, the property,
and the type of the data. */
Atom selection, target, property, type;
/* The time of the request. */
Time time;
/* Data buffer. */
unsigned char *buffer;
/* The size of the data buffer, and how much has been read from
it. */
ptrdiff_t size, offset;
/* The SelectionNotify event that should be sent. */
XEvent event;
/* Or a MultipleRecord that should be worked on. */
MultipleRecord *record;
/* The offset of this transfer into record->atoms. */
unsigned long multiple_offset;
/* Function called to return a piece of selection data in a buffer.
If size arg is -1, free associated data. Return False if EOF was
reached. */
GetDataFunc transfer_function;
/* User data for the write transfer. */
void *data;
/* A timer that times out after 5 seconds of inactivity. */
Timer *timeout;
#ifdef DEBUG
/* Total bytes of data written to the X server as part of this
transfer. */
size_t total_written;
#endif
};
struct _QueuedTransfer
{
/* Queued event. */
XEvent event;
/* Next and last items in the queue. */
QueuedTransfer *next, *last;
};
struct _SelectInputData
{
/* Number of requests from this requestor that are in progress. */
int refcount;
};
struct _MultipleRecord
{
/* Number of conversions that still have not been made. */
int pending;
/* The notification event that should be sent. */
XEvent event;
/* The atom pair array. Free this with XFree! */
Atom *atoms;
/* The number of atoms in the atom pair array. */
unsigned long nitems;
};
/* Window used for selection data transfer. */
Window selection_transfer_window;
/* Assoc table used to keep track of PropertyNotify selections. */
static XLAssocTable *foreign_notify_table;
/* Counter used for determining the selection property atom. */
static uint32_t prop_counter;
/* Chain of selection property atoms currently in use. */
static PropertyAtom prop_atoms;
/* Chain of selection property atoms that can be reused. */
static PropertyAtom free_list;
/* Circular queue of all outstanding selection data transfers to this
client. */
static ReadTransfer read_transfers;
/* List of all outstanding selection data transfers to other
clients. */
static WriteTransfer write_transfers;
/* Table of atoms to selection owner info. */
static XLAssocTable *selection_owner_info;
/* Circular queue of SelectionRequests, to be drained once all
outstanding write transfers finish. */
static QueuedTransfer queued_transfers;
#ifdef DEBUG
static void __attribute__ ((__format__ (gnu_printf, 1, 2)))
DebugPrint (const char *format, ...)
{
va_list ap;
va_start (ap, format);
vfprintf (stderr, format, ap);
va_end (ap);
}
#else
#define DebugPrint(fmt, ...) ((void) 0)
#endif
/* Allocate a new atom to use for selection property transfers. This
atom is guaranteed to not be currently in use for a selection
transfer to selection_transfer_window.
Once the transfer completes, ReleasePropAtom must be called with
the returned PropertyAtom object to allow reusing the atom in the
future. */
static PropertyAtom *
AllocPropAtom (void)
{
PropertyAtom *atom;
char name[sizeof "_XL_UXXXXXXXX" + 1];
if (free_list.next != &free_list)
{
/* There is a free atom that we can use. Remove it from the
free list. */
atom = free_list.next;
atom->next->last = atom->last;
atom->last->next = atom->next;
}
else
{
if (IntAddWrapv (prop_counter, 1, &prop_counter))
{
fprintf (stderr, "Failed to allocate selection property"
" after running out of valid values!");
abort ();
}
sprintf (name, "_XL_U%x", prop_counter);
atom = XLMalloc (sizeof *atom);
/* Use XInternAtom instead of InternAtom. These atoms should
only be interned once, so there is no point allocating memory
in the global atoms table. */
atom->atom = XInternAtom (compositor.display, name, False);
atom->counter = prop_counter;
}
/* Now, link atom onto the used list and return it. */
atom->last = &prop_atoms;
atom->next = prop_atoms.next;
prop_atoms.next->last = atom;
prop_atoms.next = atom;
return atom;
}
static void
ReleasePropAtom (PropertyAtom *atom)
{
/* Move atom from the used list onto the free list. */
atom->next->last = atom->last;
atom->last->next = atom->next;
atom->last = &free_list;
atom->next = free_list.next;
free_list.next->last = atom;
free_list.next = atom;
/* Now the atom has been released and can also be used for future
selection data transfers. */
}
static void
FinishReadTransfer (ReadTransfer *transfer, Bool success)
{
Bool delay;
if (transfer->data_finish_func
/* This means to delay deallocating the transfer for a
while. */
&& !transfer->data_finish_func (transfer, success))
delay = True;
else
delay = False;
transfer->next->last = transfer->last;
transfer->last->next = transfer->next;
ReleasePropAtom (transfer->property);
RemoveTimer (transfer->timeout);
if (!delay)
XLFree (transfer);
}
static void
HandleTimeout (Timer *timer, void *data, struct timespec time)
{
ReadTransfer *transfer;
transfer = data;
/* Delete the data transfer property from the window. */
XDeleteProperty (compositor.display,
selection_transfer_window,
transfer->property->atom);
/* Finish the read transfer. */
FinishReadTransfer (transfer, False);
}
static void
CancelTransferEarly (ReadTransfer *transfer)
{
/* Delete the data transfer property from the window. */
XDeleteProperty (compositor.display,
selection_transfer_window,
transfer->property->atom);
/* Mark the transfer as finished. */
transfer->state |= IsFailed | IsFinished;
}
/* After calling this function, set the appropriate functions! */
static ReadTransfer *
ConvertSelection (Atom selection, Atom target, Time time)
{
ReadTransfer *transfer;
/* If time is CurrentTime, obtain the time from the X server. */
if (time == CurrentTime)
time = XLGetServerTimeRoundtrip ();
transfer = XLCalloc (1, sizeof *transfer);
transfer->property = AllocPropAtom ();
transfer->time = time;
transfer->selection = selection;
transfer->target = target;
/* Link the transfer onto the list of outstanding selection
transfers. */
transfer->next = read_transfers.next;
transfer->last = &read_transfers;
/* Add a timeout for 5 seconds. */
transfer->timeout = AddTimer (HandleTimeout, transfer,
MakeTimespec (5, 0));
read_transfers.next->last = transfer;
read_transfers.next = transfer;
/* Delete the property from the window beforehand. The property
might be left over from a failed transfer. */
XDeleteProperty (compositor.display, selection_transfer_window,
transfer->property->atom);
/* Now issue the ConvertSelection request. */
XConvertSelection (compositor.display, selection, target,
transfer->property->atom,
selection_transfer_window, time);
return transfer;
}
ReadTransfer *
ConvertSelectionFuncs (Atom selection, Atom target, Time time, void *data,
void (*data_start) (ReadTransfer *, Atom, int),
void (*data_read) (ReadTransfer *, Atom, int, ptrdiff_t),
Bool (*data_finish) (ReadTransfer *, Bool))
{
ReadTransfer *transfer;
transfer = ConvertSelection (selection, target, time);
transfer->data = data;
transfer->data_start_func = data_start;
transfer->data_read_func = data_read;
transfer->data_finish_func = data_finish;
return transfer;
}
/* Find the first outstanding read transfer for SELECTION, TARGET and
TIME in the queue of outstanding selection transfers. */
static ReadTransfer *
FindReadTransfer (Atom selection, Atom target, Time time)
{
ReadTransfer *transfer;
transfer = read_transfers.last;
while (transfer != &read_transfers)
{
if (transfer->selection == selection
&& transfer->target == target
&& transfer->time == time)
return transfer;
transfer = transfer->last;
}
return NULL;
}
long
SelectionQuantum (void)
{
long request;
request = XExtendedMaxRequestSize (compositor.display);
if (!request)
request = XMaxRequestSize (compositor.display);
return request;
}
static ptrdiff_t
FormatTypeSize (int format)
{
switch (format)
{
case 8:
return sizeof (char);
case 16:
return sizeof (short);
case 32:
return sizeof (long);
}
abort ();
}
void
FinishTransfers (void)
{
ReadTransfer *transfer, *last;
transfer = read_transfers.last;
while (transfer != &read_transfers)
{
last = transfer;
transfer = transfer->last;
if (last->state & IsFinished)
FinishReadTransfer (last, !(last->state & IsFailed));
}
}
/* Signal that the current chunk of data from TRANSFER has been
completely read. */
static void
FinishChunk (ReadTransfer *transfer)
{
if (transfer->state & IsIncr)
{
transfer->state |= IsWaitingForChunk;
return;
}
/* If INCR transfer is not in progress, post a timer to finish
TRANSFER entirely, the next time the event loop is entered. */
transfer->state |= IsFinished;
}
void
SkipChunk (ReadTransfer *transfer)
{
/* Just delete the property. */
XDeleteProperty (compositor.display,
selection_transfer_window,
transfer->property->atom);
/* And mark the chunk as finished. */
FinishChunk (transfer);
}
/* Read a chunk of data from TRANSFER. LONG_LENGTH gives the length
of the data to read. Return a pointer to the data, or NULL if
reading the data failed, and return the actual length of the data
in *NBYTES. Free the data returned using Xlib functions! */
unsigned char *
ReadChunk (ReadTransfer *transfer, int long_length, ptrdiff_t *nbytes,
ptrdiff_t *bytes_after_return)
{
unsigned char *prop_data;
Atom actual_type;
int actual_format, rc;
unsigned long nitems, bytes_after;
prop_data = NULL;
/* Now read the actual property data. */
rc = XGetWindowProperty (compositor.display, selection_transfer_window,
transfer->property->atom, transfer->read_offset,
long_length, True, AnyPropertyType, &actual_type,
&actual_format, &nitems, &bytes_after, &prop_data);
/* Reading the property data failed. Signal failure by returning
NULL. Also, cancel the whole transfer here too. */
if (!prop_data)
{
CancelTransferEarly (transfer);
return NULL;
}
if (actual_type == None
|| actual_format != transfer->read_format
|| rc != Success)
{
/* Same goes here. */
if (prop_data)
XFree (prop_data);
CancelTransferEarly (transfer);
return NULL;
}
if (!bytes_after)
/* The property has now been deleted, so finish the chunk. */
FinishChunk (transfer);
/* Now return the number of bytes read. */
*nbytes = nitems * FormatTypeSize (actual_format);
/* Bump the read offset. */
transfer->read_offset += long_length;
/* Return bytes_after to the caller. */
if (bytes_after_return)
{
if (actual_format == 32)
*bytes_after_return = bytes_after * (sizeof (long) / 4);
else
*bytes_after_return = bytes_after;
}
return prop_data;
}
static void
StartSelectionRead (ReadTransfer *transfer)
{
unsigned char *prop_data;
Atom actual_type;
int actual_format, rc;
unsigned long nitems, bytes_after;
prop_data = NULL;
/* First, figure out how big the property data is. */
rc = XGetWindowProperty (compositor.display, selection_transfer_window,
transfer->property->atom, 0, 0, True, AnyPropertyType,
&actual_type, &actual_format, &nitems, &bytes_after,
&prop_data);
if (prop_data)
XFree (prop_data);
if (rc != Success)
{
FinishReadTransfer (transfer, False);
return;
}
/* If the property is of type INCR, this is the beginning of an INCR
transfer. In this case, delete prop_data, the property, and wait
for a the property to be set with a new value by the owner. */
if (actual_type == INCR)
{
transfer->state |= IsIncr;
transfer->state |= IsWaitingForChunk;
XDeleteProperty (compositor.display, selection_transfer_window,
transfer->property->atom);
return;
}
/* If actual_type is None, the property does not exist. Signal
failure. */
if (actual_type == None || !actual_format)
{
FinishReadTransfer (transfer, False);
return;
}
/* If bytes_after is 0, the property is empty. Immediately call
data_finish_func. If the transfer is incremental, then an empty
property means the data has been fully transferred. */
if (!bytes_after)
goto finish_early;
/* Otherwise, we now know how long the selection is, and the type of
the data. Announce that information, along with the beginning of
this chunk. */
transfer->read_offset = 0;
/* Also announce the format. */
transfer->read_format = actual_format;
if (transfer->data_start_func
/* Make sure this isn't run multiple times in the case of INCR
selections. */
&& !(transfer->state & IsStarted))
transfer->data_start_func (transfer, actual_type,
actual_format);
transfer->state |= IsStarted;
if (actual_format == 32)
/* Give actual data size, depending on the size of long. */
bytes_after = bytes_after * (sizeof (long) / 4);
if (transfer->data_read_func)
transfer->data_read_func (transfer, actual_type, actual_format,
bytes_after);
return;
finish_early:
FinishReadTransfer (transfer, True);
return;
}
static ReadTransfer *
FindReadTransferByProp (Atom atom)
{
ReadTransfer *transfer;
transfer = read_transfers.last;
while (transfer != &read_transfers)
{
if (transfer->property->atom == atom)
return transfer;
transfer = transfer->last;
}
return NULL;
}
static Bool
SendEvent (XEvent *event)
{
CatchXErrors ();
XSendEvent (compositor.display, event->xany.window,
False, NoEventMask, event);
return UncatchXErrors (NULL);
}
static void
SendEventUnsafe (XEvent *event)
{
XSendEvent (compositor.display, event->xany.window,
False, NoEventMask, event);
}
static Bool
CanConvertTarget (SelectionOwnerInfo *info, Atom target)
{
int i;
if (target == TARGETS
|| target == TIMESTAMP)
return True;
for (i = 0; i < info->ntargets; ++i)
{
if (target == info->targets[i])
return True;
}
return False;
}
static GetDataFunc
GetTransferFunction (SelectionOwnerInfo *info,
WriteTransfer *transfer,
Atom target, Atom *type)
{
return info->get_transfer_function (transfer, target, type);
}
static WriteTransfer *
FindWriteTransfer (Window requestor, Atom property,
int ignore_state)
{
WriteTransfer *transfer;
transfer = write_transfers.last;
while (transfer != &write_transfers)
{
if (transfer->requestor == requestor
&& transfer->property == property
&& (!ignore_state
|| ((transfer->state & ignore_state)
!= ignore_state)))
return transfer;
transfer = transfer->last;
}
return NULL;
}
static Bool
FindQueuedTransfer (Window requestor, Atom property)
{
QueuedTransfer *transfer;
transfer = queued_transfers.next;
while (transfer != &queued_transfers)
{
if (transfer->event.xselectionrequest.requestor == requestor
|| transfer->event.xselectionrequest.property == property)
return True;
transfer = transfer->next;
}
return False;
}
static void
SignalConversionPerformed (WriteTransfer *transfer)
{
if (transfer->record)
{
/* This conversion is part of a larger conversion to MULTIPLE.
Subtract this conversion from the number of conversions that
are still pending. If there are no more pending conversions,
send the SelectionNotify event and return. */
DebugPrint ("Conversion complete; %d conversion(s) are still pending\n",
transfer->record->pending - 1);
if (!(--transfer->record->pending))
{
SendEventUnsafe (&transfer->record->event);
XFree (transfer->record->atoms);
XLFree (transfer->record);
}
/* In any case, make transfer->record NULL. */
transfer->record = NULL;
}
/* If the SelectionNotify has not yet been sent, send it now. */
if (transfer->event.type)
SendEventUnsafe (&transfer->event);
transfer->event.type = 0;
}
static void
FlushTransfer (WriteTransfer *transfer, Bool force)
{
long size;
/* Flushing the buffer while waiting for property deletion is an
invalid operation. */
XLAssert (!(transfer->state & IsWaitingForDelete));
if ((transfer->state & IsStarted) || force)
{
if (force)
DebugPrint ("Forcing transfer\n");
else
DebugPrint ("Starting transfer\n");
/* If the transfer has already started, or FORCE is set, write
the property to the window. */
CatchXErrors ();
DebugPrint ("Writing property of size %zd\n",
transfer->offset);
XChangeProperty (compositor.display, transfer->requestor,
transfer->property, transfer->type, 8,
PropModeReplace, transfer->buffer,
transfer->offset);
#ifdef DEBUG
transfer->total_written += transfer->offset;
#endif
SignalConversionPerformed (transfer);
UncatchXErrors (NULL);
transfer->offset = 0;
}
else
{
DebugPrint ("Writing INCR property...\n");
/* Otherwise, write an INCR property and wait for it to be
deleted. */
size = transfer->size;
CatchXErrors ();
XChangeProperty (compositor.display, transfer->requestor,
transfer->property, INCR, 32,
PropModeReplace, (unsigned char *) &size, 1);
SignalConversionPerformed (transfer);
UncatchXErrors (NULL);
transfer->state |= IsWaitingForIncr;
}
/* Now, begin to wait for the property to be deleted. */
transfer->state |= IsWaitingForDelete;
/* And mark the transfer as having been flushed. */
transfer->state |= IsFlushed;
}
static void
SelectPropertyNotify (Window window)
{
SelectInputData *data;
data = XLLookUpAssoc (foreign_notify_table, window);
if (data)
data->refcount++;
else
{
data = XLMalloc (sizeof *data);
data->refcount = 1;
XLMakeAssoc (foreign_notify_table, window, data);
/* Actually select for input from the window. */
CatchXErrors ();
XSelectInput (compositor.display, window,
PropertyChangeMask);
UncatchXErrors (NULL);
DebugPrint ("Selecting for PropertyChangeMask on %lu\n",
window);
}
}
static void
DeselectPropertyNotify (Window window)
{
SelectInputData *data;
data = XLLookUpAssoc (foreign_notify_table, window);
XLAssert (data != NULL);
if (--data->refcount)
return;
DebugPrint ("De-selecting for PropertyChangeMask on %lu\n",
window);
CatchXErrors ();
XSelectInput (compositor.display, window, NoEventMask);
UncatchXErrors (NULL);
XLDeleteAssoc (foreign_notify_table, window);
XLFree (data);
}
/* Forward declaration. */
static void DrainQueuedTransfers (void);
static void
FreeTransfer (WriteTransfer *transfer)
{
DebugPrint ("Deallocating transfer data\n");
/* The transfer completed. Unlink it, and also deselect for
PropertyNotify events from the requestor. */
transfer->next->last = transfer->last;
transfer->last->next = transfer->next;
if (transfer->state & IsStarted)
{
#ifdef DEBUG
DebugPrint ("Writing zero-length property data; total: %zu\n",
transfer->total_written);
#endif
CatchXErrors ();
/* Write zero-length property data to complete the INCR
transfer. */
XChangeProperty (compositor.display, transfer->requestor,
transfer->property, transfer->type, 8,
PropModeReplace, NULL, 0);
UncatchXErrors (NULL);
}
if (transfer->record)
{
/* Since FreeTransfer was called without transfer->record having
being released earlier, the conversion associated with this
transfer has failed. Write the conversion failure. */
transfer->record->atoms[transfer->multiple_offset] = None;
DebugPrint ("Conversion at offset %lu failed\n",
transfer->multiple_offset);
CatchXErrors ();
XChangeProperty (compositor.display, transfer->requestor,
transfer->record->event.xselection.property,
ATOM_PAIR, 32, PropModeReplace,
(unsigned char *) transfer->record->atoms,
transfer->record->nitems);
UncatchXErrors (NULL);
/* Now, dereference the record, and send the SelectionNotify if
there are no more pending conversions. */
if (!(--transfer->record->pending))
{
DebugPrint ("Completing MULTIPLE transfer\n");
SendEventUnsafe (&transfer->record->event);
XFree (transfer->record->atoms);
XLFree (transfer->record);
}
}
/* If there are no more transfers, drain queued ones. */
if (write_transfers.next == &write_transfers)
DrainQueuedTransfers ();
DeselectPropertyNotify (transfer->requestor);
RemoveTimer (transfer->timeout);
XLFree (transfer->buffer);
XLFree (transfer);
}
static void
FinishTransferEarly (WriteTransfer *transfer)
{
/* Call the transfer function with negative values, to force it to
free data. */
transfer->transfer_function (transfer, NULL, -1, NULL);
/* Finish the transfer now. */
FreeTransfer (transfer);
}
static void
TransferFinished (WriteTransfer *transfer)
{
if (transfer->state & IsWaitingForDelete)
{
/* If we are still waiting for property deletion, simply set the
IsFinished flag. Zero-length property data will be written the
next time the property is deleted to mark the completion of
this transfer. */
transfer->state |= IsFinished;
DebugPrint ("Transfer finished; waiting for property deletion\n");
}
else if (transfer->offset
/* Even if there is no more data, we must still write
zero-length property data to complete the selection
transfer if nothing has previously been written. */
|| !(transfer->state & IsFlushed))
{
DebugPrint ("Transfer finished, but there is still property data"
" unwritten (offset %td)\n", transfer->offset);
/* There is still property data left to be written. */
FlushTransfer (transfer, True);
transfer->state |= IsFinished;
}
else
/* The transfer is really finished. */
FreeTransfer (transfer);
}
static void
TransferBecameReadable (WriteTransfer *transfer)
{
ptrdiff_t bytes_read;
ReadStatus status;
/* If we are still waiting for the INCR property to be deleted,
don't read anything, but flag the read as pending. */
if (transfer->state & IsWaitingForDelete)
{
DebugPrint ("Transfer became readable, but we are still waiting"
" for a property deletion\n");
transfer->state |= IsReadable;
return;
}
DebugPrint ("Reading from transfer function\n");
/* The data source became readable. Every time the source becomes
readable, try to fill up the transfer buffer.
If we get EOF without filling up the buffer, simply set the
property to the data and the correct type and finish the
transfer; otherwise, initiate INCR transfer. */
transfer->state &= ~IsReadable;
status = transfer->transfer_function (transfer,
(transfer->buffer
+ transfer->offset),
(transfer->size
- transfer->offset),
&bytes_read);
if (status != EndOfFile)
{
transfer->offset += bytes_read;
DebugPrint ("Read %td bytes, offset is now %td into %td\n",
bytes_read, transfer->offset, transfer->size);
/* Make sure nothing overflowed. */
XLAssert (transfer->offset <= transfer->size);
if (transfer->offset == transfer->size
/* If a bigger buffer is required, flush the transfer now,
so the entire transfer buffer can be reused. */
|| status == NeedBiggerBuffer)
{
if (status == NeedBiggerBuffer)
/* Since a bigger buffer is needed, that means there is
still data buffered up to be read. */
transfer->state |= IsReadable;
FlushTransfer (transfer, False);
}
}
else
{
DebugPrint ("Transfer complete, bytes read as part of EOF: %td, "
"off: %td size: %td\n", bytes_read, transfer->offset,
transfer->size);
transfer->offset += bytes_read;
TransferFinished (transfer);
}
}
static void
ConvertSelectionTargets1 (SelectionOwnerInfo *info,
Window requestor, Atom property)
{
Atom *targets;
targets = alloca ((3 + info->ntargets)
* sizeof *targets);
/* Add the required targets, TARGETS and MULTIPLE. */
targets[0] = TARGETS;
targets[1] = MULTIPLE;
targets[2] = TIMESTAMP;
/* And the rest of the targets. */
memcpy (&targets[3], info->targets,
info->ntargets * sizeof *targets);
XChangeProperty (compositor.display, requestor,
property, XA_ATOM, 32, PropModeReplace,
(unsigned char *) targets,
2 + info->ntargets);
}
static void
ConvertSelectionTargets (SelectionOwnerInfo *info, XEvent *notify)
{
/* Set the property and send the SelectionNotify event. */
CatchXErrors ();
ConvertSelectionTargets1 (info, notify->xselection.requestor,
notify->xselection.property);
SendEventUnsafe (notify);
UncatchXErrors (NULL);
}
static void
ConvertSelectionTimestamp1 (SelectionOwnerInfo *info, Window requestor,
Window property)
{
XChangeProperty (compositor.display, requestor, property,
XA_ATOM, 32, PropModeReplace,
(unsigned char *) &info->time, 1);
}
static void
ConvertSelectionTimestamp (SelectionOwnerInfo *info, XEvent *notify)
{
/* Set the property and send the SelectionNotify event. */
CatchXErrors ();
ConvertSelectionTimestamp1 (info, notify->xselection.requestor,
notify->xselection.property);
SendEventUnsafe (notify);
UncatchXErrors (NULL);
}
static void
QueueTransfer (XEvent *event)
{
QueuedTransfer *transfer;
transfer = XLMalloc (sizeof *transfer);
transfer->event = *event;
transfer->next = queued_transfers.next;
transfer->last = &queued_transfers;
queued_transfers.next->last = transfer;
queued_transfers.next = transfer;
}
static void
HandleWriteTimeout (Timer *timer, void *data, struct timespec time)
{
DebugPrint ("Transfer timeout\n");
FinishTransferEarly (data);
}
static void
ConvertSelectionMultiple (SelectionOwnerInfo *info, XEvent *event,
XEvent *notify)
{
Status rc;
unsigned char *prop_data;
Atom *atoms;
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after, i, quantum;
WriteTransfer *transfer;
Bool prop_data_changed;
MultipleRecord *record;
prop_data = NULL;
prop_data_changed = False;
/* Try to get the ATOM_PAIRs describing the targets and properties
from the source. */
CatchXErrors ();
rc = XGetWindowProperty (compositor.display,
event->xselectionrequest.requestor,
event->xselectionrequest.property,
0, 65535, False, ATOM_PAIR,
&actual_type, &actual_format,
&nitems, &bytes_after, &prop_data);
UncatchXErrors (NULL);
if (rc != Success || actual_format != 32 || nitems % 2
|| actual_type != ATOM_PAIR || !prop_data)
{
if (prop_data)
XFree (prop_data);
DebugPrint ("Failed to retrieve ATOM_PAIR parameter\n");
return;
}
atoms = (Atom *) prop_data;
DebugPrint ("Number of items in atom pair: %lu\n", nitems / 2);
for (i = 0; i < nitems; i += 2)
{
DebugPrint ("Verifying MULTIPLE transfer; target = %lu, property = %lu\n",
atoms[i + 0], atoms[i + 1]);
if (FindWriteTransfer (event->xselectionrequest.requestor, atoms[1], 0)
|| FindQueuedTransfer (event->xselectionrequest.requestor, atoms[1]))
{
DebugPrint ("Found ongoing selection transfer with same requestor "
"and property; this MULTIPLE request will have to be "
"queued.\n");
QueueTransfer (event);
if (prop_data)
XLFree (prop_data);
return;
}
}
record = XLMalloc (sizeof *record);
record->pending = 0;
record->event = *notify;
record->nitems = nitems;
record->atoms = atoms;
CatchXErrors ();
for (i = 0; i < nitems; i += 2)
{
/* Fortunately, we don't have to verify every transfer here to
make sure the client didn't specify duplicate property names,
as the ICCCM doesn't explicitly specify how programs should
behave in that case. Things will simply go wrong later on,
and the transfer will time out. */
DebugPrint ("Starting MULTIPLE transfer; target = %lu, property = %lu\n",
atoms[i + 0], atoms[i + 1]);
if (atoms[0] == MULTIPLE)
{
DebugPrint ("Saw nested MULTIPLE transfer; "
"such conversions are not allowed\n");
atoms[0] = None;
prop_data_changed = True;
continue;
}
if (!CanConvertTarget (info, atoms[0]))
{
DebugPrint ("Couldn't convert to target for a simple reason;"
" replacing atom with NULL\n");
atoms[0] = None;
prop_data_changed = True;
continue;
}
if (atoms[0] == TARGETS)
{
DebugPrint ("Converting to special target TARGETS...\n");
ConvertSelectionTargets1 (info, event->xselectionrequest.requestor,
atoms[1]);
continue;
}
if (atoms[0] == TIMESTAMP)
{
DebugPrint ("Converting to special target TIMESTAMP...\n");
ConvertSelectionTimestamp1 (info, event->xselectionrequest.requestor,
atoms[1]);
continue;
}
/* Create the write transfer and link it onto the list. */
transfer = XLCalloc (1, sizeof *transfer);
/* This seems to be a sufficiently reasonable value. */
quantum = MIN (SelectionQuantum (), 65535 * 2);
transfer->next = write_transfers.next;
transfer->last = &write_transfers;
transfer->requestor = event->xselectionrequest.requestor;
transfer->selection = event->xselectionrequest.selection;
transfer->target = atoms[i + 0];
transfer->property = atoms[i + 1];
transfer->time = event->xselectionrequest.time;
transfer->buffer = XLMalloc (quantum);
transfer->size = quantum;
/* Add a timeout for 5 seconds. */
transfer->timeout = AddTimer (HandleWriteTimeout, transfer,
MakeTimespec (5, 0));
write_transfers.next->last = transfer;
write_transfers.next = transfer;
SelectPropertyNotify (transfer->requestor);
transfer->transfer_function
= GetTransferFunction (info, transfer,
transfer->target,
&transfer->type);
if (!transfer->transfer_function)
{
/* Something failed (most probably, we ran out of fds to
make the pipe). Cancel the transfer and signal
failure. */
atoms[0] = None;
prop_data_changed = True;
FreeTransfer (transfer);
}
else
{
/* Otherwise, add the transfer record. */
record->pending++;
transfer->record = record;
transfer->multiple_offset = i;
}
}
/* If prop_data changed, which happens when a conversion fails,
write its new contents back onto the requestor to let it know
which conversions could not be made. */
if (prop_data_changed)
XChangeProperty (compositor.display,
event->xselectionrequest.requestor,
event->xselectionrequest.property,
ATOM_PAIR, 32, PropModeReplace,
prop_data, nitems);
if (prop_data && !record->pending)
/* If record->pending, then the individual write transfers still
reference the atom pair data. */
XFree (prop_data);
/* If no pending conversions remain, immediately send success and
free the conversion. */
if (!record->pending)
{
SendEvent (&record->event);
XLFree (record);
}
UncatchXErrors (NULL);
}
static Bool
HandleSelectionRequest (XEvent *event)
{
XEvent notify;
WriteTransfer *transfer, *existing_transfer;
long quantum;
SelectionOwnerInfo *info;
DebugPrint ("Received SelectionRequest. Time: %lu, requestor: %lu"
", target: %lu, selection: %lu, property: %lu, serial: %lu\n",
event->xselectionrequest.time,
event->xselectionrequest.requestor,
event->xselectionrequest.target,
event->xselectionrequest.selection,
event->xselectionrequest.property,
event->xselectionrequest.serial);
notify.xselection.type = SelectionNotify;
notify.xselection.requestor = event->xselectionrequest.requestor;
notify.xselection.time = event->xselectionrequest.time;
notify.xselection.target = event->xselectionrequest.target;
notify.xselection.selection = event->xselectionrequest.selection;
notify.xselection.property = None;
info = XLLookUpAssoc (selection_owner_info,
event->xselectionrequest.selection);
if (!info
|| !info->get_transfer_function
/* The ICCCM prohibits clients from using CurrentTime, but some
do anyway. */
|| (event->xselectionrequest.time != CurrentTime
&& TimeIs (event->xselectionrequest.time, Earlier, info->time))
|| (!CanConvertTarget (info, event->xselectionrequest.target)
/* CanConvertTarget itself does not understand MULTIPLE,
because it itself is called while handling MULTIPLE. */
&& event->xselectionrequest.target != MULTIPLE))
{
DebugPrint ("Couldn't convert selection due to simple reason\n");
/* The event is out of date, or the selection cannot be
converted to the given target. Send failure. */
SendEvent (&notify);
return True;
}
/* If a selection request with the same property and window already
exists, delay this request for later. */
existing_transfer
= FindWriteTransfer (event->xselectionrequest.requestor,
event->xselectionrequest.property,
/* Ignore write transfers that are finished
but pending property deletion. If the
existing transfer is finished, but we are
still waiting for property deletion, allow
the new transfer to take place. This is
because some very popular programs ask for
TARGETS, and then ask for STRING with the
same property, but only delete the
property for the first request after the
data for the second request arrives. This
is against the ICCCM, as it says:
The requestor should set the property
argument to the name of a property that
the owner can use to report the value of
the selection. Requestors should ensure
that the named property does not exist
on the window before issuing the
ConvertSelection request.
and:
Once all the data in the selection has
been retrieved (which may require
getting the values of several properties
-- see the section called "Use of
Selection Properties". ), the requestor
should delete the property in the
SelectionNotify request by using a
GetProperty request with the delete
argument set to True. As previously
discussed, the owner has no way of
knowing when the data has been
transferred to the requestor unless the
property is removed.
Both paragraphs mean that the property
should have been deleted by the time the
second request is made! */
IsFinished | IsWaitingForDelete);
if (existing_transfer
/* We need to look at the queue too; otherwise, events from the
future might be handled out of order, if the original write
transfer is gone, but some events are still queued. */
|| FindQueuedTransfer (event->xselectionrequest.requestor,
event->xselectionrequest.property))
{
DebugPrint ("Queueing this selection request for later, because"
" an identical transfer is already taking place\n");
QueueTransfer (event);
return True;
}
/* Otherwise, begin a selection transfer to the requestor. */
notify.xselection.property = event->xselectionrequest.property;
if (notify.xselection.property == None)
/* This is an obsolete client; use the target as the property. */
notify.xselection.property = event->xselectionrequest.target;
/* Handle special targets, such as TARGETS and TIMESTAMP. */
if (notify.xselection.target == TARGETS)
{
DebugPrint ("Converting selection to special target TARGETS\n");
ConvertSelectionTargets (info, &notify);
return True;
}
else if (notify.xselection.target == TIMESTAMP)
{
DebugPrint ("Converting selection to special target TIMESTAMP\n");
ConvertSelectionTimestamp (info, &notify);
return True;
}
else if (notify.xselection.target == MULTIPLE)
{
if (event->xselectionrequest.property == None)
{
DebugPrint ("Got malformed MULTIPLE request with no property\n");
notify.xselection.property = None;
SendEvent (&notify);
return True;
}
DebugPrint ("Converting selection to special target MULTIPLE\n");
ConvertSelectionMultiple (info, event, &notify);
return True;
}
DebugPrint ("Starting selection transfer\n");
/* Create the write transfer and link it onto the list. */
transfer = XLCalloc (1, sizeof *transfer);
/* This seems to be a sufficiently reasonable value. */
quantum = MIN (SelectionQuantum (), 65535 * 2);
transfer->next = write_transfers.next;
transfer->last = &write_transfers;
transfer->requestor = event->xselectionrequest.requestor;
transfer->selection = event->xselectionrequest.selection;
transfer->target = event->xselectionrequest.target;
transfer->property = notify.xselection.property;
transfer->time = event->xselectionrequest.time;
transfer->event = notify;
transfer->buffer = XLMalloc (quantum);
transfer->size = quantum;
/* Add a timeout for 5 seconds. */
transfer->timeout = AddTimer (HandleWriteTimeout, transfer,
MakeTimespec (5, 0));
write_transfers.next->last = transfer;
write_transfers.next = transfer;
SelectPropertyNotify (transfer->requestor);
transfer->transfer_function
= GetTransferFunction (info, transfer,
transfer->target,
&transfer->type);
if (!transfer->transfer_function)
{
/* Something failed (most likely, we ran out of fds to make the
pipe). Cancel the transfer and signal failure. */
notify.xselection.property = None;
SendEvent (&notify);
FreeTransfer (transfer);
}
return True;
}
static void
DrainQueuedTransfers (void)
{
QueuedTransfer temp, *item, *last;
/* If nothing is queued, return. */
if (queued_transfers.next == &queued_transfers)
return;
/* First, relink everything onto this temp sentinel. That way, if
stuff gets queued again in HandleSelectionRequest, the list
structure won't change underneath our noses. */
queued_transfers.last->next = &temp;
queued_transfers.next->last = &temp;
temp.next = queued_transfers.next;
temp.last = queued_transfers.last;
queued_transfers.next = &queued_transfers;
queued_transfers.last = &queued_transfers;
/* Then, read from the other side of the queue, and handle
everything. */
item = temp.last;
while (item != &temp)
{
last = item;
item = item->last;
DebugPrint ("Draining one request with serial: %lu\n",
last->event.xselectionrequest.serial);
HandleSelectionRequest (&last->event);
XLFree (last);
}
}
static Bool
HandleSelectionNotify (XEvent *event)
{
ReadTransfer *transfer;
/* If event->property is None, then the selection transfer could not
be made. Find which transfer couldn't be made by looking in the
queue for the first transfer matching event->selection,
event->target and event->time, and unlink it. */
if (event->xselection.property == None)
{
transfer = FindReadTransfer (event->xselection.selection,
event->xselection.target,
event->xselection.time);
if (!transfer)
return True;
FinishReadTransfer (transfer, False);
return True;
}
/* Otherwise, find the outstanding selection transfer using the
property. */
transfer = FindReadTransferByProp (event->xselection.property);
if (!transfer)
return True;
/* And start transferring data from it. */
StartSelectionRead (transfer);
return True;
}
static Bool
HandlePropertyDelete (XEvent *event)
{
WriteTransfer *transfer, *last;
transfer = write_transfers.last;
DebugPrint ("Handling property deletion for %lu; window %lu\n",
event->xproperty.atom, event->xproperty.window);
while (transfer != &write_transfers)
{
last = transfer->last;
/* There can be multiple finished transfers with the same
property pending deletion if a client not compliant with the
ICCCM is in use. See the large comment in
HandleSelectionRequest. */
DebugPrint ("Handling transfer %p\n", event);
if (transfer->state & IsFinished)
{
DebugPrint ("Completing transfer\n");
/* The transfer is now complete; finish it by freeing its
data, and potentially writing zero-length data. */
FreeTransfer (transfer);
}
else if (transfer->state & IsWaitingForIncr)
{
/* If transfer is waiting for the INCR property to be
deleted, mark it as started, and flush it again to write
the first piece of property data. */
DebugPrint ("Starting transfer in response to INCR property deletion\n");
transfer->state |= IsStarted;
transfer->state &= ~IsWaitingForIncr;
transfer->state &= ~IsWaitingForDelete;
/* Flush the transfer again to write the property data. */
FlushTransfer (transfer, False);
}
else
{
DebugPrint ("Continuing transfer\n");
/* Clear IsWaitingForRelease. */
transfer->state &= ~IsWaitingForDelete;
if (transfer->state & IsReadable)
{
DebugPrint ("Picking read back up from where it was left\n");
/* And signal that the transfer is readable again. */
TransferBecameReadable (transfer);
}
}
transfer = last;
}
return True;
}
static Bool
HandlePropertyNotify (XEvent *event)
{
ReadTransfer *transfer;
if (event->xproperty.window != DefaultRootWindow (compositor.display))
/* Xlib selects for PropertyNotifyMask on the root window, which
results in a lot of noise here. */
DebugPrint ("PropertyNotify event:\n"
"serial:\t%lu\n"
"window:\t%lu\n"
"atom:\t%lu\n"
"time:\t%lu\n"
"state:\t%s\n",
event->xproperty.serial,
event->xproperty.window,
event->xproperty.atom,
event->xproperty.time,
(event->xproperty.state == PropertyNewValue
? "PropertyNewValue" : "PropertyDelete"));
if (event->xproperty.state != PropertyNewValue)
return HandlePropertyDelete (event);
/* Find the transfer corresponding to this property. */
transfer = FindReadTransferByProp (event->xproperty.atom);
if (!transfer)
return False;
/* By the time transfer->state & IsIncr, no more out-of-date
PropertyNotify events from previous selection requests can be
received. */
if (transfer->state & IsIncr
&& transfer->state & IsWaitingForChunk)
{
/* Clear the waiting-for-chunk flag, and begin reading from this
chunk. */
transfer->state &= ~IsWaitingForChunk;
StartSelectionRead (transfer);
}
return True;
}
Bool
HookSelectionEvent (XEvent *event)
{
if (event->xany.type == SelectionNotify)
return HandleSelectionNotify (event);
if (event->xany.type == SelectionRequest)
return HandleSelectionRequest (event);
if (event->xany.type == PropertyNotify)
return HandlePropertyNotify (event);
return False;
}
void *
GetTransferData (ReadTransfer *transfer)
{
return transfer->data;
}
Time
GetTransferTime (ReadTransfer *transfer)
{
return transfer->time;
}
void *
GetWriteTransferData (WriteTransfer *transfer)
{
return transfer->data;
}
void
SetWriteTransferData (WriteTransfer *transfer, void *data)
{
transfer->data = data;
}
void
CompleteDelayedTransfer (ReadTransfer *transfer)
{
/* Right now, we don't have to do any more than this. */
XFree (transfer);
}
Bool
OwnSelection (Timestamp time, Atom selection,
GetDataFunc (*hook) (WriteTransfer *transfer,
Atom, Atom *),
Atom *targets, int ntargets)
{
Window owner;
SelectionOwnerInfo *info;
info = XLLookUpAssoc (selection_owner_info, selection);
if (info && TimestampIs (time, Earlier, info->time))
/* The timestamp is out of date. */
return False;
XSetSelectionOwner (compositor.display, selection,
selection_transfer_window,
time.milliseconds);
/* Check if selection ownership was actually set. */
owner = XGetSelectionOwner (compositor.display, selection);
/* If ownership wasn't successfully set, return. */
if (owner != selection_transfer_window)
return False;
/* Otherwise, update or allocate the info structure. */
if (!info)
{
info = XLMalloc (sizeof *info);
XLMakeAssoc (selection_owner_info, selection,
info);
}
else
XLFree (info->targets);
info->time = time;
info->targets = XLMalloc (sizeof *info->targets * ntargets);
info->ntargets = ntargets;
info->get_transfer_function = hook;
memcpy (info->targets, targets, sizeof *targets * ntargets);
return True;
}
void
DisownSelection (Atom selection)
{
SelectionOwnerInfo *info;
info = XLLookUpAssoc (selection_owner_info, selection);
if (info && info->get_transfer_function)
{
XSetSelectionOwner (compositor.display, selection,
None, info->time.milliseconds);
/* Also free info->targets. */
XLFree (info->targets);
info->targets = NULL;
/* And set the selection to disowned. */
info->get_transfer_function = NULL;
}
}
void
StartReading (WriteTransfer *transfer)
{
TransferBecameReadable (transfer);
}
void
InitSelections (void)
{
XSetWindowAttributes attrs;
int flags;
/* Set up sentinel nodes for various lists. */
prop_atoms.next = &prop_atoms;
prop_atoms.last = &prop_atoms;
free_list.next = &free_list;
free_list.last = &free_list;
read_transfers.next = &read_transfers;
read_transfers.last = &read_transfers;
write_transfers.next = &write_transfers;
write_transfers.last = &write_transfers;
queued_transfers.next = &queued_transfers;
queued_transfers.last = &queued_transfers;
/* Make the window used to transfer selection data. */
attrs.override_redirect = True;
attrs.event_mask = PropertyChangeMask;
flags = CWEventMask | CWOverrideRedirect;
selection_transfer_window
= XCreateWindow (compositor.display,
DefaultRootWindow (compositor.display),
-1, -1, 1, 1, 0, CopyFromParent, InputOnly,
CopyFromParent, flags, &attrs);
/* Make the assoc tables used to keep track of input masks and
selection data. */
foreign_notify_table = XLCreateAssocTable (32);
selection_owner_info = XLCreateAssocTable (32);
DebugPrint ("Selection transfer window is %lu\n",
selection_transfer_window);
}