Implement the INCR ICCCM selection mechanism
xcb_get_property is now also called with a length that is a multiple of four so that we advance the offset properly. Some cleanup. Selection code should work pretty well by now. The amount of code supporting it has more than doubled since GTK+.
This commit is contained in:
parent
86417d512b
commit
2efaed1a56
163
src/sdtui.c
163
src/sdtui.c
|
@ -1833,8 +1833,13 @@ struct selection_watch
|
||||||
|
|
||||||
guint watch; ///< X11 connection watcher
|
guint watch; ///< X11 connection watcher
|
||||||
xcb_window_t wid; ///< Withdrawn communications window
|
xcb_window_t wid; ///< Withdrawn communications window
|
||||||
|
xcb_atom_t atom_incr; ///< INCR
|
||||||
xcb_atom_t atom_utf8_string; ///< UTF8_STRING
|
xcb_atom_t atom_utf8_string; ///< UTF8_STRING
|
||||||
xcb_timestamp_t in_progress; ///< Timestamp of last processed event
|
xcb_timestamp_t in_progress; ///< Timestamp of last processed event
|
||||||
|
GString * buffer; ///< UTF-8 text buffer
|
||||||
|
|
||||||
|
gboolean incr; ///< INCR running
|
||||||
|
gboolean incr_failure; ///< INCR failure indicator
|
||||||
};
|
};
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
|
@ -1877,57 +1882,48 @@ on_selection_text_received (SelectionWatch *self, const gchar *text)
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
read_utf8_property (SelectionWatch *self, xcb_window_t wid, xcb_atom_t property,
|
read_utf8_property (SelectionWatch *self, xcb_window_t wid, xcb_atom_t property,
|
||||||
GString *buf)
|
gboolean *empty)
|
||||||
{
|
{
|
||||||
guint32 offset = 0;
|
guint32 offset = 0;
|
||||||
gboolean loop = TRUE, ok = TRUE;
|
gboolean more_data = TRUE, ok = TRUE;
|
||||||
while (ok && loop)
|
xcb_get_property_reply_t *gpr;
|
||||||
|
while (ok && more_data)
|
||||||
{
|
{
|
||||||
xcb_get_property_reply_t *gpr = xcb_get_property_reply (self->X,
|
if (!(gpr = xcb_get_property_reply (self->X,
|
||||||
xcb_get_property (self->X, FALSE /* delete */, wid,
|
xcb_get_property (self->X, FALSE /* delete */, wid,
|
||||||
property, XCB_GET_PROPERTY_TYPE_ANY, offset, 0x7fff), NULL);
|
property, XCB_GET_PROPERTY_TYPE_ANY, offset, 0x8000), NULL)))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
if (!gpr || gpr->type != self->atom_utf8_string || gpr->format != 8)
|
|
||||||
ok = FALSE;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int len = xcb_get_property_value_length (gpr);
|
int len = xcb_get_property_value_length (gpr);
|
||||||
g_string_append_len (buf, xcb_get_property_value (gpr), len);
|
if (offset == 0 && len == 0 && empty)
|
||||||
offset += len >> 2;
|
*empty = TRUE;
|
||||||
loop = gpr->bytes_after > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ok = gpr->type == self->atom_utf8_string && gpr->format == 8;
|
||||||
|
more_data = gpr->bytes_after != 0;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
offset += len >> 2;
|
||||||
|
g_string_append_len (self->buffer,
|
||||||
|
xcb_get_property_value (gpr), len);
|
||||||
|
}
|
||||||
free (gpr);
|
free (gpr);
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
process_x11_event (SelectionWatch *self, xcb_generic_event_t *event)
|
on_x11_selection_change (SelectionWatch *self,
|
||||||
|
xcb_xfixes_selection_notify_event_t *e)
|
||||||
{
|
{
|
||||||
xcb_generic_error_t *err = NULL;
|
|
||||||
int event_code = event->response_type & 0x7f;
|
|
||||||
if (event_code == 0)
|
|
||||||
{
|
|
||||||
err = (xcb_generic_error_t *) event;
|
|
||||||
g_warning (_("X11 request error (%d, major %d, minor %d)"),
|
|
||||||
err->error_code, err->major_code, err->minor_code);
|
|
||||||
}
|
|
||||||
else if (event_code ==
|
|
||||||
self->xfixes->first_event + XCB_XFIXES_SELECTION_NOTIFY)
|
|
||||||
{
|
|
||||||
xcb_xfixes_selection_notify_event_t *e =
|
|
||||||
(xcb_xfixes_selection_notify_event_t *) event;
|
|
||||||
|
|
||||||
// Not checking whether we should give up when this interrupts our
|
// Not checking whether we should give up when this interrupts our
|
||||||
// current retrieval attempt--the timeout solves this
|
// current retrieval attempt--the timeout mostly solves this for all cases
|
||||||
if (e->owner == XCB_NONE)
|
if (e->owner == XCB_NONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Don't try to process two things at once. Each request gets a few
|
// Don't try to process two things at once. Each request gets a few seconds
|
||||||
// seconds to finish, then we move on, hoping that a property race
|
// to finish, then we move on, hoping that a property race doesn't commence.
|
||||||
// doesn't commence. Ideally we'd set up a separate queue for these
|
// Ideally we'd set up a separate queue for these skipped requests and
|
||||||
// skipped requests and process them later.
|
// process them later.
|
||||||
if (self->in_progress != 0 && e->timestamp - self->in_progress < 5000)
|
if (self->in_progress != 0 && e->timestamp - self->in_progress < 5000)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -1936,26 +1932,95 @@ process_x11_event (SelectionWatch *self, xcb_generic_event_t *event)
|
||||||
|
|
||||||
(void) xcb_convert_selection (self->X, self->wid, e->selection,
|
(void) xcb_convert_selection (self->X, self->wid, e->selection,
|
||||||
self->atom_utf8_string, XCB_ATOM_PRIMARY, e->timestamp);
|
self->atom_utf8_string, XCB_ATOM_PRIMARY, e->timestamp);
|
||||||
|
|
||||||
self->in_progress = e->timestamp;
|
self->in_progress = e->timestamp;
|
||||||
}
|
self->incr = FALSE;
|
||||||
else if (event_code == XCB_SELECTION_NOTIFY)
|
}
|
||||||
{
|
|
||||||
xcb_selection_notify_event_t *e =
|
static void
|
||||||
(xcb_selection_notify_event_t *) event;
|
on_x11_selection_receive (SelectionWatch *self,
|
||||||
if (e->time != self->in_progress)
|
xcb_selection_notify_event_t *e)
|
||||||
|
{
|
||||||
|
if (e->requestor != self->wid
|
||||||
|
|| e->time != self->in_progress)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
self->in_progress = 0;
|
self->in_progress = 0;
|
||||||
if (e->property == XCB_ATOM_NONE)
|
if (e->property == XCB_ATOM_NONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
GString *buf = g_string_new (NULL);
|
xcb_get_property_reply_t *gpr = xcb_get_property_reply (self->X,
|
||||||
if (read_utf8_property (self, e->requestor, e->property, buf))
|
xcb_get_property (self->X, FALSE /* delete */, e->requestor,
|
||||||
on_selection_text_received (self, buf->str);
|
e->property, XCB_GET_PROPERTY_TYPE_ANY, 0, 0), NULL);
|
||||||
g_string_free (buf, TRUE);
|
if (!gpr)
|
||||||
|
return;
|
||||||
|
|
||||||
(void) xcb_delete_property (self->X, self->wid, e->property);
|
// Garbage collection, GString only ever expands in size
|
||||||
|
g_string_free (self->buffer, TRUE);
|
||||||
|
self->buffer = g_string_new (NULL);
|
||||||
|
|
||||||
|
// When you select a lot of text in VIM, it starts the ICCCM INCR mechanism,
|
||||||
|
// from which there is no opt-out
|
||||||
|
if (gpr->type == self->atom_incr)
|
||||||
|
{
|
||||||
|
self->in_progress = e->time;
|
||||||
|
self->incr = TRUE;
|
||||||
|
self->incr_failure = FALSE;
|
||||||
}
|
}
|
||||||
|
else if (read_utf8_property (self, e->requestor, e->property, NULL))
|
||||||
|
on_selection_text_received (self, self->buffer->str);
|
||||||
|
|
||||||
|
free (gpr);
|
||||||
|
(void) xcb_delete_property (self->X, self->wid, e->property);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_x11_property_notify (SelectionWatch *self, xcb_property_notify_event_t *e)
|
||||||
|
{
|
||||||
|
if (!self->incr
|
||||||
|
|| e->window != self->wid
|
||||||
|
|| e->state != XCB_PROPERTY_NEW_VALUE
|
||||||
|
|| e->atom != XCB_ATOM_PRIMARY)
|
||||||
|
return;
|
||||||
|
|
||||||
|
gboolean empty = FALSE;
|
||||||
|
if (!read_utf8_property (self, e->window, e->atom, &empty))
|
||||||
|
// We need to keep deleting the property
|
||||||
|
self->incr_failure = TRUE;
|
||||||
|
|
||||||
|
// Once it's empty, we've consumed everything and can move on undisturbed
|
||||||
|
if (empty)
|
||||||
|
{
|
||||||
|
if (!self->incr_failure)
|
||||||
|
on_selection_text_received (self, self->buffer->str);
|
||||||
|
|
||||||
|
self->in_progress = 0;
|
||||||
|
self->incr = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void) xcb_delete_property (self->X, e->window, e->atom);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
process_x11_event (SelectionWatch *self, xcb_generic_event_t *event)
|
||||||
|
{
|
||||||
|
int event_code = event->response_type & 0x7f;
|
||||||
|
if (event_code == 0)
|
||||||
|
{
|
||||||
|
xcb_generic_error_t *err = (xcb_generic_error_t *) event;
|
||||||
|
g_warning (_("X11 request error (%d, major %d, minor %d)"),
|
||||||
|
err->error_code, err->major_code, err->minor_code);
|
||||||
|
}
|
||||||
|
else if (event_code ==
|
||||||
|
self->xfixes->first_event + XCB_XFIXES_SELECTION_NOTIFY)
|
||||||
|
on_x11_selection_change (self,
|
||||||
|
(xcb_xfixes_selection_notify_event_t *) event);
|
||||||
|
else if (event_code == XCB_SELECTION_NOTIFY)
|
||||||
|
on_x11_selection_receive (self,
|
||||||
|
(xcb_selection_notify_event_t *) event);
|
||||||
|
else if (event_code == XCB_PROPERTY_NOTIFY)
|
||||||
|
on_x11_property_notify (self,
|
||||||
|
(xcb_property_notify_event_t *) event);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
|
@ -1991,6 +2056,8 @@ selection_watch_init (SelectionWatch *self, Application *app)
|
||||||
// fallback might be good to add (COMPOUND_TEXT is complex)
|
// fallback might be good to add (COMPOUND_TEXT is complex)
|
||||||
g_return_if_fail
|
g_return_if_fail
|
||||||
((self->atom_utf8_string = resolve_atom (self->X, "UTF8_STRING")));
|
((self->atom_utf8_string = resolve_atom (self->X, "UTF8_STRING")));
|
||||||
|
g_return_if_fail
|
||||||
|
((self->atom_incr = resolve_atom (self->X, "INCR")));
|
||||||
|
|
||||||
self->xfixes = xcb_get_extension_data (self->X, &xcb_xfixes_id);
|
self->xfixes = xcb_get_extension_data (self->X, &xcb_xfixes_id);
|
||||||
g_return_if_fail (self->xfixes->present);
|
g_return_if_fail (self->xfixes->present);
|
||||||
|
@ -2005,9 +2072,10 @@ selection_watch_init (SelectionWatch *self, Application *app)
|
||||||
|
|
||||||
xcb_screen_t *screen = setup_iter.data;
|
xcb_screen_t *screen = setup_iter.data;
|
||||||
self->wid = xcb_generate_id (self->X);
|
self->wid = xcb_generate_id (self->X);
|
||||||
|
const uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE};
|
||||||
(void) xcb_create_window (self->X, screen->root_depth, self->wid,
|
(void) xcb_create_window (self->X, screen->root_depth, self->wid,
|
||||||
screen->root, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
screen->root, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
||||||
screen->root_visual, 0, NULL);
|
screen->root_visual, XCB_CW_EVENT_MASK, values);
|
||||||
|
|
||||||
(void) xcb_xfixes_select_selection_input (self->X, self->wid,
|
(void) xcb_xfixes_select_selection_input (self->X, self->wid,
|
||||||
XCB_ATOM_PRIMARY, XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER |
|
XCB_ATOM_PRIMARY, XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER |
|
||||||
|
@ -2017,6 +2085,9 @@ selection_watch_init (SelectionWatch *self, Application *app)
|
||||||
(void) xcb_flush (self->X);
|
(void) xcb_flush (self->X);
|
||||||
self->watch = g_io_add_watch (g_io_channel_unix_new
|
self->watch = g_io_add_watch (g_io_channel_unix_new
|
||||||
(xcb_get_file_descriptor (self->X)), G_IO_IN, process_x11, self);
|
(xcb_get_file_descriptor (self->X)), G_IO_IN, process_x11, self);
|
||||||
|
|
||||||
|
// Never NULL so that we don't need to care about pointer validity
|
||||||
|
self->buffer = g_string_new (NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -2026,6 +2097,8 @@ selection_watch_destroy (SelectionWatch *self)
|
||||||
xcb_disconnect (self->X);
|
xcb_disconnect (self->X);
|
||||||
if (self->watch)
|
if (self->watch)
|
||||||
g_source_remove (self->watch);
|
g_source_remove (self->watch);
|
||||||
|
if (self->buffer)
|
||||||
|
g_string_free (self->buffer, TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // WITH_X11
|
#endif // WITH_X11
|
||||||
|
|
Loading…
Reference in New Issue