diff --git a/.luacheckrc b/.luacheckrc index 0261cdb0a..d5a3ccad8 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -27,6 +27,7 @@ read_globals = { "keygrabber", "mousegrabber", "root", + "selection_acquire", "selection_getter", "selection", "selection_watcher", diff --git a/CMakeLists.txt b/CMakeLists.txt index fc933c01a..26d5ebad1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,8 @@ set(AWE_SRCS ${BUILD_DIR}/objects/drawin.c ${BUILD_DIR}/objects/key.c ${BUILD_DIR}/objects/screen.c + ${BUILD_DIR}/objects/selection_acquire.c + ${BUILD_DIR}/objects/selection_transfer.c ${BUILD_DIR}/objects/selection_watcher.c ${BUILD_DIR}/objects/tag.c ${BUILD_DIR}/objects/selection_getter.c diff --git a/event.c b/event.c index 444ca08c0..94b6b8fd7 100644 --- a/event.c +++ b/event.c @@ -25,6 +25,7 @@ #include "objects/tag.h" #include "objects/selection_getter.h" #include "objects/drawin.h" +#include "objects/selection_acquire.h" #include "objects/selection_watcher.h" #include "xwindow.h" #include "ewmh.h" @@ -1013,7 +1014,8 @@ event_handle_selectionclear(xcb_selection_clear_event_t *ev) { warn("Lost WM_Sn selection, exiting..."); g_main_loop_quit(globalconf.loop); - } + } else + selection_handle_selectionclear(ev); } /** \brief awesome xerror function. @@ -1109,6 +1111,7 @@ void event_handle(xcb_generic_event_t *event) EVENT(XCB_UNMAP_NOTIFY, event_handle_unmapnotify); EVENT(XCB_SELECTION_CLEAR, event_handle_selectionclear); EVENT(XCB_SELECTION_NOTIFY, event_handle_selectionnotify); + EVENT(XCB_SELECTION_REQUEST, selection_handle_selectionrequest); #undef EVENT } diff --git a/luaa.c b/luaa.c index 317f2cf0d..803d44d01 100644 --- a/luaa.c +++ b/luaa.c @@ -50,6 +50,8 @@ #include "objects/drawin.h" #include "objects/selection_getter.h" #include "objects/screen.h" +#include "objects/selection_acquire.h" +#include "objects/selection_transfer.h" #include "objects/selection_watcher.h" #include "objects/tag.h" #include "property.h" @@ -1037,6 +1039,12 @@ luaA_init(xdgHandle* xdg, string_array_t *searchpath) /* Export keys */ key_class_setup(L); + /* Export selection acquire */ + selection_acquire_class_setup(L); + + /* Export selection transfer */ + selection_transfer_class_setup(L); + /* Export selection watcher */ selection_watcher_class_setup(L); diff --git a/objects/selection_acquire.c b/objects/selection_acquire.c new file mode 100644 index 000000000..46868b113 --- /dev/null +++ b/objects/selection_acquire.c @@ -0,0 +1,243 @@ +/* + * selection_acquire.c - objects for selection ownership + * + * Copyright © 2019 Uli Schlachter + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "objects/selection_acquire.h" +#include "objects/selection_transfer.h" +#include "common/luaobject.h" +#include "globalconf.h" + +#define REGISTRY_ACQUIRE_TABLE_INDEX "awesome_selection_acquires" + +typedef struct selection_acquire_t +{ + LUA_OBJECT_HEADER + /** The selection that is being owned. */ + xcb_atom_t selection; + /** Window used for owning the selection. */ + xcb_window_t window; + /** Timestamp used for acquiring the selection. */ + xcb_timestamp_t timestamp; +} selection_acquire_t; + +static lua_class_t selection_acquire_class; +LUA_OBJECT_FUNCS(selection_acquire_class, selection_acquire_t, selection_acquire) + +static void +luaA_pushatom(lua_State *L, xcb_atom_t atom) +{ + lua_pushnumber(L, atom); +} + +static int +selection_acquire_find_by_window(lua_State *L, xcb_window_t window) +{ + /* Iterate over all active selection acquire objects */ + lua_pushliteral(L, REGISTRY_ACQUIRE_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_type(L, -1) == LUA_TUSERDATA) { + selection_acquire_t *selection = lua_touserdata(L, -1); + if (selection->window == window) + { + /* Remove table and key */ + lua_remove(L, -2); + lua_remove(L, -2); + return 1; + } + } + /* Remove the value, leaving only the key */ + lua_pop(L, 1); + } + /* Remove the table */ + lua_pop(L, 1); + + return 0; +} + +static void +selection_release(lua_State *L, int ud) +{ + selection_acquire_t *selection = luaA_checkudata(L, ud, &selection_acquire_class); + + luaA_object_emit_signal(L, ud, "release", 0); + + /* Destroy the window, this also releases the selection in X11 */ + xcb_destroy_window(globalconf.connection, selection->window); + selection->window = XCB_NONE; + + /* Unreference the object, it's now dead */ + lua_pushliteral(L, REGISTRY_ACQUIRE_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + luaA_pushatom(L, selection->selection); + lua_pushnil(L); + lua_rawset(L, -3); + + selection->selection = XCB_NONE; + + lua_pop(L, 1); +} + +void +selection_handle_selectionclear(xcb_selection_clear_event_t *ev) +{ + lua_State *L = globalconf_get_lua_State(); + + if (selection_acquire_find_by_window(L, ev->owner) == 0) + return; + + selection_release(L, -1); + lua_pop(L, 1); +} + +void +selection_handle_selectionrequest(xcb_selection_request_event_t *ev) +{ + lua_State *L = globalconf_get_lua_State(); + + if (ev->property == XCB_NONE) + /* Obsolete client */ + ev->property = ev->target; + + if (selection_acquire_find_by_window(L, ev->owner) == 0) + { + selection_transfer_reject(ev->requestor, ev->selection, ev->target, ev->time); + return; + } + + selection_transfer_begin(L, -1, ev->requestor, ev->selection, ev->target, + ev->property, ev->time); + + lua_pop(L, 1); +} + +static int +luaA_selection_acquire_new(lua_State *L) +{ + size_t name_length; + const char *name; + xcb_intern_atom_reply_t *reply; + xcb_get_selection_owner_reply_t *selection_reply; + xcb_atom_t name_atom; + selection_acquire_t *selection; + + luaA_checktable(L, 2); + lua_pushliteral(L, "selection"); + lua_gettable(L, 2); + name = luaL_checklstring(L, -1, &name_length); + + /* Get the atom identifying the selection */ + reply = xcb_intern_atom_reply(globalconf.connection, + xcb_intern_atom_unchecked(globalconf.connection, false, name_length, name), + NULL); + name_atom = reply ? reply->atom : XCB_NONE; + p_delete(&reply); + + /* Create a selection object */ + selection = (void *) selection_acquire_class.allocator(L); + selection->selection = name_atom; + selection->timestamp = globalconf.timestamp; + selection->window = xcb_generate_id(globalconf.connection); + xcb_create_window(globalconf.connection, globalconf.screen->root_depth, + selection->window, globalconf.screen->root, -1, -1, 1, 1, 0, + XCB_COPY_FROM_PARENT, globalconf.screen->root_visual, 0, NULL); + + /* Try to acquire the selection */ + xcb_set_selection_owner(globalconf.connection, selection->window, name_atom, selection->timestamp); + selection_reply = xcb_get_selection_owner_reply(globalconf.connection, + xcb_get_selection_owner(globalconf.connection, name_atom), + NULL); + if (selection_reply == NULL || selection_reply->owner != selection->window) { + /* Acquiring the selection failed, return nothing */ + p_delete(&selection_reply); + + xcb_destroy_window(globalconf.connection, selection->window); + selection->window = XCB_NONE; + return 0; + } + + /* Everything worked, register the object in the table */ + lua_pushliteral(L, REGISTRY_ACQUIRE_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + + luaA_pushatom(L, name_atom); + lua_rawget(L, -2); + if (!lua_isnil(L, -1)) { + /* There is already another selection_acquire object for this selection, + * release it now. X11 does not send us SelectionClear events for our + * own changes to the selection. + */ + selection_release(L, -1); + } + + luaA_pushatom(L, name_atom); + lua_pushvalue(L, -4); + lua_rawset(L, -4); + lua_pop(L, 2); + + return 1; +} + +static int +luaA_selection_acquire_release(lua_State *L) +{ + luaA_checkudata(L, 1, &selection_acquire_class); + selection_release(L, 1); + + return 0; +} + +static bool +selection_acquire_checker(selection_acquire_t *selection) +{ + return selection->selection != XCB_NONE; +} + +void +selection_acquire_class_setup(lua_State *L) +{ + static const struct luaL_Reg selection_acquire_methods[] = + { + { "__call", luaA_selection_acquire_new }, + { NULL, NULL } + }; + + static const struct luaL_Reg selection_acquire_meta[] = + { + LUA_OBJECT_META(selection_acquire) + LUA_CLASS_META + { "release", luaA_selection_acquire_release }, + { NULL, NULL } + }; + + /* Store a table in the registry that tracks active selection_acquire_t. */ + lua_pushliteral(L, REGISTRY_ACQUIRE_TABLE_INDEX); + lua_newtable(L); + lua_rawset(L, LUA_REGISTRYINDEX); + + luaA_class_setup(L, &selection_acquire_class, "selection_acquire", NULL, + (lua_class_allocator_t) selection_acquire_new, NULL, + (lua_class_checker_t) selection_acquire_checker, + luaA_class_index_miss_property, luaA_class_newindex_miss_property, + selection_acquire_methods, selection_acquire_meta); +} + +// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/objects/selection_acquire.h b/objects/selection_acquire.h new file mode 100644 index 000000000..8036a9f45 --- /dev/null +++ b/objects/selection_acquire.h @@ -0,0 +1,34 @@ +/* + * selection_acquire.c - objects for selection ownership header + * + * Copyright © 2019 Uli Schlachter + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#ifndef AWESOME_OBJECTS_SELECTION_ACQUIRE_H +#define AWESOME_OBJECTS_SELECTION_ACQUIRE_H + +#include +#include + +void selection_acquire_class_setup(lua_State*); +void selection_handle_selectionclear(xcb_selection_clear_event_t*); +void selection_handle_selectionrequest(xcb_selection_request_event_t*); + +#endif + +// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/objects/selection_transfer.c b/objects/selection_transfer.c new file mode 100644 index 000000000..e15530148 --- /dev/null +++ b/objects/selection_transfer.c @@ -0,0 +1,386 @@ +/* + * selection_transfer.c - objects for selection transfer header + * + * Copyright © 2019 Uli Schlachter + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "objects/selection_transfer.h" +#include "common/luaobject.h" +#include "common/atoms.h" +#include "globalconf.h" + +#define REGISTRY_TRANSFER_TABLE_INDEX "awesome_selection_transfers" +#define TRANSFER_DATA_INDEX "data_for_next_chunk" + +enum transfer_state { + TRANSFER_WAIT_FOR_DATA, + TRANSFER_INCREMENTAL_SENDING, + TRANSFER_INCREMENTAL_DONE, + TRANSFER_DONE +}; + +typedef struct selection_transfer_t +{ + LUA_OBJECT_HEADER + /** Reference in the special table to this object */ + int ref; + /* Information from the xcb_selection_request_event_t */ + xcb_window_t requestor; + xcb_atom_t selection; + xcb_atom_t target; + xcb_atom_t property; + xcb_timestamp_t time; + /* Current state of the transfer */ + enum transfer_state state; + /* Offset into TRANSFER_DATA_INDEX for the next chunk of data */ + size_t offset; + /* Can there be more data coming from Lua? */ + bool more_data; +} selection_transfer_t; + +static lua_class_t selection_transfer_class; +LUA_OBJECT_FUNCS(selection_transfer_class, selection_transfer_t, selection_transfer) + +static size_t max_property_length(void) +{ + uint32_t max_request_length = xcb_get_maximum_request_length(globalconf.connection); + max_request_length = MIN(max_request_length, (1<<16) - 1); + return max_request_length * 4 - sizeof(xcb_change_property_request_t); +} + +static void +selection_transfer_notify(xcb_window_t requestor, xcb_atom_t selection, + xcb_atom_t target, xcb_atom_t property, xcb_timestamp_t time) +{ + xcb_selection_notify_event_t ev; + + p_clear(&ev, 1); + ev.response_type = XCB_SELECTION_NOTIFY; + ev.requestor = requestor; + ev.selection = selection; + ev.target = target; + ev.property = property; + ev.time = time; + + xcb_send_event(globalconf.connection, false, requestor, + XCB_EVENT_MASK_NO_EVENT, (char *) &ev); +} + +void +selection_transfer_reject(xcb_window_t requestor, xcb_atom_t selection, + xcb_atom_t target, xcb_timestamp_t time) +{ + selection_transfer_notify(requestor, selection, target, XCB_NONE, time); +} + +static void +transfer_done(lua_State *L, selection_transfer_t *transfer) +{ + transfer->state = TRANSFER_DONE; + + lua_pushliteral(L, REGISTRY_TRANSFER_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + luaL_unref(L, -1, transfer->ref); + transfer->ref = LUA_NOREF; + lua_pop(L, 1); +} + +static void +transfer_continue_incremental(lua_State *L, int ud) +{ + const char *data; + size_t data_length; + selection_transfer_t *transfer = luaA_checkudata(L, ud, &selection_transfer_class); + + ud = luaA_absindex(L, ud); + + /* Get the data that is to be sent next */ + luaA_getuservalue(L, ud); + lua_pushliteral(L, TRANSFER_DATA_INDEX); + lua_rawget(L, -2); + lua_remove(L, -2); + + data = luaL_checklstring(L, -1, &data_length); + if (transfer->offset == data_length) { + if (transfer->more_data) { + /* Request the next piece of data from Lua */ + transfer->state = TRANSFER_INCREMENTAL_DONE; + luaA_object_emit_signal(L, ud, "continue", 0); + if (transfer->state != TRANSFER_INCREMENTAL_DONE) { + /* Lua gave us more data to send. */ + lua_pop(L, 1); + return; + } + } + /* End of transfer */ + xcb_change_property(globalconf.connection, XCB_PROP_MODE_REPLACE, + transfer->requestor, transfer->property, UTF8_STRING, 8, + 0, NULL); + xcb_change_window_attributes(globalconf.connection, + transfer->requestor, XCB_CW_EVENT_MASK, + (uint32_t[]) { 0 }); + transfer_done(L, transfer); + } else { + /* Send next piece of data */ + assert(transfer->offset < data_length); + size_t next_length = MIN(data_length - transfer->offset, max_property_length()); + xcb_change_property(globalconf.connection, XCB_PROP_MODE_REPLACE, + transfer->requestor, transfer->property, UTF8_STRING, 8, + next_length, &data[transfer->offset]); + transfer->offset += next_length; + } + lua_pop(L, 1); +} + +void +selection_transfer_begin(lua_State *L, int ud, xcb_window_t requestor, + xcb_atom_t selection, xcb_atom_t target, xcb_atom_t property, + xcb_timestamp_t time) +{ + ud = luaA_absindex(L, ud); + + /* Allocate a transfer object */ + selection_transfer_t *transfer = (void *) selection_transfer_class.allocator(L); + transfer->requestor = requestor; + transfer->selection = selection; + transfer->target = target; + transfer->property = property; + transfer->time = time; + transfer->state = TRANSFER_WAIT_FOR_DATA; + + /* Save the object in the registry */ + lua_pushliteral(L, REGISTRY_TRANSFER_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, -2); + transfer->ref = luaL_ref(L, -2); + lua_pop(L, 1); + + /* Get the atom name */ + xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply(globalconf.connection, + xcb_get_atom_name_unchecked(globalconf.connection, target), NULL); + if (reply) { + lua_pushlstring(L, xcb_get_atom_name_name(reply), + xcb_get_atom_name_name_length(reply)); + p_delete(&reply); + } else + lua_pushnil(L); + + /* Emit the request signal with target and transfer object */ + lua_pushvalue(L, -2); + luaA_object_emit_signal(L, ud, "request", 2); + + /* Reject the transfer if Lua did not do anything */ + if (transfer->state == TRANSFER_WAIT_FOR_DATA) { + selection_transfer_reject(requestor, selection, target, time); + transfer_done(L, transfer); + } + + /* Remove the transfer object from the stack */ + lua_pop(L, 1); +} + +static int +luaA_selection_transfer_send(lua_State *L) +{ + size_t data_length; + bool incr = false; + size_t incr_size = 0; + + selection_transfer_t *transfer = luaA_checkudata(L, 1, &selection_transfer_class); + if (transfer->state != TRANSFER_WAIT_FOR_DATA && transfer->state != TRANSFER_INCREMENTAL_DONE) + luaL_error(L, "Transfer object is not ready for more data to be sent"); + + luaA_checktable(L, 2); + + lua_pushliteral(L, "continue"); + lua_rawget(L, 2); + transfer->more_data = incr = lua_toboolean(L, -1); + if (incr && lua_isnumber(L, -1)) + incr_size = lua_tonumber(L, -1); + lua_pop(L, 1); + + if (transfer->state == TRANSFER_INCREMENTAL_DONE) { + /* Save the data on the transfer object */ + lua_pushliteral(L, "data"); + lua_rawget(L, 2); + + luaA_getuservalue(L, 1); + lua_pushliteral(L, TRANSFER_DATA_INDEX); + lua_pushvalue(L, -3); + lua_rawset(L, -3); + lua_pop(L, 1); + + /* Continue the incremental transfer */ + transfer->state = TRANSFER_INCREMENTAL_SENDING; + transfer->offset = 0; + + transfer_continue_incremental(L, 1); + + return 0; + } + + /* Get format and data from the table */ + lua_pushliteral(L, "format"); + lua_rawget(L, 2); + lua_pushliteral(L, "data"); + lua_rawget(L, 2); + + if (lua_isstring(L, -2)) { + const char *format_string = luaL_checkstring(L, -2); + if (A_STRNEQ(format_string, "atom")) + luaL_error(L, "Unknown format '%s'", format_string); + if (incr) + luaL_error(L, "Cannot transfer atoms in pieces"); + + /* 'data' is a table with strings */ + size_t len = luaA_rawlen(L, -1); + + /* Get an array with atoms */ + size_t atom_lengths[len]; + const char *atom_strings[len]; + for (size_t i = 0; i < len; i++) { + lua_rawgeti(L, -1, i+1); + atom_strings[i] = luaL_checklstring(L, -1, &atom_lengths[i]); + lua_pop(L, 1); + } + + xcb_intern_atom_cookie_t cookies[len]; + xcb_atom_t atoms[len]; + for (size_t i = 0; i < len; i++) { + cookies[i] = xcb_intern_atom_unchecked(globalconf.connection, false, + atom_lengths[i], atom_strings[i]); + } + for (size_t i = 0; i < len; i++) { + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(globalconf.connection, + cookies[i], NULL); + atoms[i] = reply ? reply->atom : XCB_NONE; + p_delete(&reply); + } + + xcb_change_property(globalconf.connection, XCB_PROP_MODE_REPLACE, + transfer->requestor, transfer->property, XCB_ATOM_ATOM, 32, + len, &atoms[0]); + } else { + /* 'data' is a string with the data to transfer */ + const char *data = luaL_checklstring(L, -1, &data_length); + + if (!incr) + incr_size = data_length; + + if (data_length >= max_property_length()) + incr = true; + + if (incr) { + xcb_change_window_attributes(globalconf.connection, + transfer->requestor, XCB_CW_EVENT_MASK, + (uint32_t[]) { XCB_EVENT_MASK_PROPERTY_CHANGE }); + + xcb_change_property(globalconf.connection, XCB_PROP_MODE_REPLACE, + transfer->requestor, transfer->property, INCR, 32, 1, + (const uint32_t[]) { incr_size }); + + /* Save the data on the transfer object */ + luaA_getuservalue(L, 1); + lua_pushliteral(L, TRANSFER_DATA_INDEX); + lua_pushvalue(L, -3); + lua_rawset(L, -3); + lua_pop(L, 1); + + transfer->state = TRANSFER_INCREMENTAL_SENDING; + transfer->offset = 0; + } else { + xcb_change_property(globalconf.connection, XCB_PROP_MODE_REPLACE, + transfer->requestor, transfer->property, UTF8_STRING, 8, + data_length, data); + } + } + + selection_transfer_notify(transfer->requestor, transfer->selection, + transfer->target, transfer->property, transfer->time); + if (!incr) + transfer_done(L, transfer); + + return 0; +} + +void +selection_transfer_handle_propertynotify(xcb_property_notify_event_t *ev) +{ + lua_State *L = globalconf_get_lua_State(); + + if (ev->state != XCB_PROPERTY_DELETE) + return; + + /* Iterate over all active selection acquire objects */ + lua_pushliteral(L, REGISTRY_TRANSFER_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_type(L, -1) == LUA_TUSERDATA) { + selection_transfer_t *transfer = lua_touserdata(L, -1); + if (transfer->state == TRANSFER_INCREMENTAL_SENDING + && transfer->requestor == ev->window + && transfer->property == ev->atom) { + transfer_continue_incremental(L, -1); + /* Remove table, key and transfer object */ + lua_pop(L, 3); + return; + } + } + /* Remove the value, leaving only the key */ + lua_pop(L, 1); + } + /* Remove the table */ + lua_pop(L, 1); +} + +static bool +selection_transfer_checker(selection_transfer_t *transfer) +{ + return transfer->state != TRANSFER_DONE; +} + +void +selection_transfer_class_setup(lua_State *L) +{ + static const struct luaL_Reg selection_transfer_methods[] = + { + { NULL, NULL } + }; + + static const struct luaL_Reg selection_transfer_meta[] = + { + LUA_OBJECT_META(selection_transfer) + LUA_CLASS_META + { "send", luaA_selection_transfer_send }, + { NULL, NULL } + }; + + /* Store a table in the registry that tracks active selection_transfer_t. */ + lua_pushliteral(L, REGISTRY_TRANSFER_TABLE_INDEX); + lua_newtable(L); + lua_rawset(L, LUA_REGISTRYINDEX); + + luaA_class_setup(L, &selection_transfer_class, "selection_transfer", NULL, + (lua_class_allocator_t) selection_transfer_new, NULL, + (lua_class_checker_t) selection_transfer_checker, + luaA_class_index_miss_property, luaA_class_newindex_miss_property, + selection_transfer_methods, selection_transfer_meta); +} + +// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/objects/selection_transfer.h b/objects/selection_transfer.h new file mode 100644 index 000000000..25d197b42 --- /dev/null +++ b/objects/selection_transfer.h @@ -0,0 +1,36 @@ +/* + * selection_transfer.c - objects for selection transfer header + * + * Copyright © 2019 Uli Schlachter + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#ifndef AWESOME_OBJECTS_SELECTION_TRANSFER_H +#define AWESOME_OBJECTS_SELECTION_TRANSFER_H + +#include +#include + +void selection_transfer_class_setup(lua_State*); +void selection_transfer_reject(xcb_window_t, xcb_atom_t, xcb_atom_t, xcb_timestamp_t); +void selection_transfer_begin(lua_State*, int, xcb_window_t, xcb_atom_t, + xcb_atom_t, xcb_atom_t, xcb_timestamp_t); +void selection_transfer_handle_propertynotify(xcb_property_notify_event_t*); + +#endif + +// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/property.c b/property.c index 30fd44fd7..a585ce915 100644 --- a/property.c +++ b/property.c @@ -26,6 +26,7 @@ #include "objects/client.h" #include "objects/drawin.h" #include "objects/selection_getter.h" +#include "objects/selection_transfer.h" #include "xwindow.h" #include @@ -478,6 +479,7 @@ property_handle_propertynotify(xcb_property_notify_event_t *ev) globalconf.timestamp = ev->time; property_handle_propertynotify_xproperty(ev); + selection_transfer_handle_propertynotify(ev); /* Find the correct event handler */ #define HANDLE(atom_, cb) \ diff --git a/tests/test-selection-transfer.lua b/tests/test-selection-transfer.lua new file mode 100644 index 000000000..1f851bb8d --- /dev/null +++ b/tests/test-selection-transfer.lua @@ -0,0 +1,237 @@ +-- Test the selection ownership and transfer API + +local runner = require("_runner") +local spawn = require("awful.spawn") + +-- Assemble data for the large transfer that will be done later +local large_transfer_piece = "a" +for _ = 1, 25 do + large_transfer_piece = large_transfer_piece .. large_transfer_piece +end +large_transfer_piece = large_transfer_piece .. large_transfer_piece .. large_transfer_piece + +local large_transfer_piece_count = 3 +local large_transfer_size = #large_transfer_piece * large_transfer_piece_count + +local header = [[ +local lgi = require("lgi") +local Gdk = lgi.Gdk +local Gtk = lgi.Gtk +local GLib = lgi.GLib +local function assert_equal(a, b) + assert(a == b, + string.format("%s == %s", a or "nil/false", b or "nil/false")) +end +local clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) +]] + +local acquire_and_clear_clipboard = header .. [[ +clipboard:set_text("This is an experiment", -1) +GLib.idle_add(GLib.PRIORITY_DEFAULT, Gtk.main_quit) +Gtk.main() +]] + +local done_footer = [[ +io.stdout:write("done") +io.stdout:flush() +]] + +local check_targets_and_text = header .. [[ +local targets = clipboard:wait_for_targets() +assert_equal(targets[1]:name(), "TARGETS") +assert_equal(targets[2]:name(), "UTF8_STRING") +assert_equal(#targets, 2) +assert_equal(clipboard:wait_for_text(), "Hello World!") +]] .. done_footer + +local check_large_transfer = header + .. string.format("\nassert_equal(#clipboard:wait_for_text(), %d)\n", large_transfer_size) + .. done_footer + +local check_empty_selection = header .. [[ +assert_equal(clipboard:wait_for_targets(), nil) +assert_equal(clipboard:wait_for_text(), nil) +]] .. done_footer + +local selection +local selection_released +local continue + +local function wait_a_bit(count) + if continue or count == 5 then + return true + end +end + +runner.run_steps{ + function() + -- Get the selection + local s = assert(selection_acquire{ selection = "CLIPBOARD" }, + "Failed to acquire the clipboard selection") + + -- Steal selection ownership from ourselves and test that it works + local s_released + s:connect_signal("release", function() s_released = true end) + + selection = assert(selection_acquire{ selection = "CLIPBOARD" }, + "Failed to acquire the clipboard selection") + + assert(s_released) + + -- Now test selection transfers + selection = assert(selection_acquire{ selection = "CLIPBOARD" }, + "Failed to acquire the clipboard selection") + selection:connect_signal("request", function(_, target, transfer) + if target == "TARGETS" then + transfer:send{ + format = "atom", + data = { "TARGETS", "UTF8_STRING" }, + } + elseif target == "UTF8_STRING" then + transfer:send{ data = "Hello World!" } + end + end) + awesome.sync() + spawn.with_line_callback({ "lua", "-e", check_targets_and_text }, + { stdout = function(line) + assert(line == "done", "Unexpected line: " .. line) + continue = true + end }) + return true + end, + + function() + -- Wait for the test to succeed + if not continue then + return + end + continue = false + + -- Now test piece-wise selection transfers + selection = assert(selection_acquire{ selection = "CLIPBOARD" }, + "Failed to acquire the clipboard selection") + selection:connect_signal("request", function(_, target, transfer) + if target == "TARGETS" then + transfer:send{ + format = "atom", + data = { "TARGETS", "UTF8_STRING" }, + } + elseif target == "UTF8_STRING" then + local offset = 1 + local data = "Hello World!" + local function send_piece() + local piece = data:sub(offset, offset) + transfer:send{ + data = piece, + continue = piece ~= "" + } + offset = offset + 1 + end + transfer:connect_signal("continue", send_piece) + send_piece() + end + end) + awesome.sync() + spawn.with_line_callback({ "lua", "-e", check_targets_and_text }, + { stdout = function(line) + assert(line == "done", "Unexpected line: " .. line) + continue = true + end }) + return true + end, + + function() + -- Wait for the test to succeed + if not continue then + return + end + continue = false + + -- Now test a huge transfer + selection = assert(selection_acquire{ selection = "CLIPBOARD" }, + "Failed to acquire the clipboard selection") + selection:connect_signal("request", function(_, target, transfer) + if target == "TARGETS" then + transfer:send{ + format = "atom", + data = { "TARGETS", "UTF8_STRING" }, + } + elseif target == "UTF8_STRING" then + local count = 0 + local function send_piece() + count = count + 1 + local done = count == large_transfer_piece_count + transfer:send{ + data = large_transfer_piece, + continue = not done, + } + end + transfer:connect_signal("continue", send_piece) + send_piece() + end + end) + awesome.sync() + spawn.with_line_callback({ "lua", "-e", check_large_transfer }, + { stdout = function(line) + assert(line == "done", "Unexpected line: " .. line) + continue = true + end }) + return true + end, + + -- The large data transfer above transfers 3 * 2^25 bytes of data. That's + -- 96 MiB and takes a while. + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + + function() + -- Wait for the test to succeed + if not continue then + return + end + continue = false + + -- Now test that :release() works + selection:release() + awesome.sync() + spawn.with_line_callback({ "lua", "-e", check_empty_selection }, + { stdout = function(line) + assert(line == "done", "Unexpected line: " .. line) + continue = true + end }) + + return true + end, + + function() + -- Wait for the test to succeed + if not continue then + return + end + continue = false + + -- Test for "release" signal when we lose selection + selection = assert(selection_acquire{ selection = "CLIPBOARD" }, + "Failed to acquire the clipboard selection") + selection:connect_signal("release", function() selection_released = true end) + awesome.sync() + spawn.with_line_callback({ "lua", "-e", acquire_and_clear_clipboard }, + { exit = function() continue = true end }) + return true + end, + + function() + -- Wait for the previous test to succeed + if not continue then + return + end + continue = false + assert(selection_released) + return true + end, +} + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80