diff --git a/.luacheckrc b/.luacheckrc index 12721317..1fa2ff9a 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -28,6 +28,7 @@ read_globals = { "mousegrabber", "root", "selection", + "selection_watcher", "tag", "window", "table.unpack", diff --git a/.travis.yml b/.travis.yml index 29110628..69e3a8c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ install: # Install build dependencies. # See also `apt-cache showsrc awesome | grep -E '^(Version|Build-Depends)'`. - - sudo apt-get install -y libcairo2-dev gir1.2-gtk-3.0 libpango1.0-dev libxcb-xtest0-dev libxcb-icccm4-dev libxcb-randr0-dev libxcb-keysyms1-dev libxcb-xinerama0-dev libdbus-1-dev libxdg-basedir-dev libstartup-notification0-dev imagemagick libxcb1-dev libxcb-shape0-dev libxcb-util0-dev libx11-xcb-dev libxcb-cursor-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev + - sudo apt-get install -y libcairo2-dev gir1.2-gtk-3.0 libpango1.0-dev libxcb-xtest0-dev libxcb-icccm4-dev libxcb-randr0-dev libxcb-keysyms1-dev libxcb-xinerama0-dev libdbus-1-dev libxdg-basedir-dev libstartup-notification0-dev imagemagick libxcb1-dev libxcb-shape0-dev libxcb-util0-dev libx11-xcb-dev libxcb-cursor-dev libxcb-xkb-dev libxcb-xfixes0-dev libxkbcommon-dev libxkbcommon-x11-dev - sudo gem install asciidoctor # Deps for tests. diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d3d3b4d..a2555f2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ set(AWE_SRCS ${BUILD_DIR}/objects/drawin.c ${BUILD_DIR}/objects/key.c ${BUILD_DIR}/objects/screen.c + ${BUILD_DIR}/objects/selection_watcher.c ${BUILD_DIR}/objects/tag.c ${BUILD_DIR}/objects/window.c) diff --git a/awesome.c b/awesome.c index e711cf70..4a058acc 100644 --- a/awesome.c +++ b/awesome.c @@ -53,6 +53,7 @@ #include #include #include +#include #include @@ -733,6 +734,7 @@ main(int argc, char **argv) xcb_prefetch_extension_data(globalconf.connection, &xcb_randr_id); xcb_prefetch_extension_data(globalconf.connection, &xcb_xinerama_id); xcb_prefetch_extension_data(globalconf.connection, &xcb_shape_id); + xcb_prefetch_extension_data(globalconf.connection, &xcb_xfixes_id); if (xcb_cursor_context_new(globalconf.connection, globalconf.screen, &globalconf.cursor_ctx) < 0) fatal("Failed to initialize xcb-cursor"); @@ -794,6 +796,13 @@ main(int argc, char **argv) p_delete(&reply); } + /* check for xfixes extension */ + query = xcb_get_extension_data(globalconf.connection, &xcb_xfixes_id); + globalconf.have_xfixes = query && query->present; + if (globalconf.have_xfixes) + xcb_discard_reply(globalconf.connection, + xcb_xfixes_query_version(globalconf.connection, 1, 0).sequence); + event_init(); /* Allocate the key symbols */ diff --git a/awesomeConfig.cmake b/awesomeConfig.cmake index 79eae7e7..9c6d5f42 100644 --- a/awesomeConfig.cmake +++ b/awesomeConfig.cmake @@ -141,6 +141,7 @@ set(AWESOME_DEPENDENCIES xcb-keysyms>=0.3.4 xcb-icccm xcb-icccm>=0.3.8 + xcb-xfixes # NOTE: it's not clear what version is required, but 1.10 works at least. # See https://github.com/awesomeWM/awesome/pull/149#issuecomment-94208356. xcb-xkb diff --git a/docs/01-readme.md b/docs/01-readme.md index b983ab6e..7a997d5a 100644 --- a/docs/01-readme.md +++ b/docs/01-readme.md @@ -68,6 +68,7 @@ environment): - [libxcb-util >= 0.3.8](https://xcb.freedesktop.org/) - [libxcb-keysyms >= 0.3.4](https://xcb.freedesktop.org/) - [libxcb-icccm >= 0.3.8](https://xcb.freedesktop.org/) +- [libxcb-xfixes](https://xcb.freedesktop.org/) - [xcb-util-xrm >= 1.0](https://github.com/Airblader/xcb-util-xrm) - [libxkbcommon](http://xkbcommon.org/) with X11 support enabled - [libstartup-notification >= diff --git a/event.c b/event.c index ddda3a6d..6f23bd08 100644 --- a/event.c +++ b/event.c @@ -24,6 +24,7 @@ #include "property.h" #include "objects/tag.h" #include "objects/drawin.h" +#include "objects/selection_watcher.h" #include "xwindow.h" #include "ewmh.h" #include "objects/client.h" @@ -43,6 +44,7 @@ #include #include #include +#include #define DO_EVENT_HOOK_CALLBACK(type, xcbtype, xcbeventprefix, arraytype, match) \ static void \ @@ -1116,6 +1118,7 @@ void event_handle(xcb_generic_event_t *event) EXTENSION_EVENT(randr, XCB_RANDR_NOTIFY, event_handle_randr_output_change_notify); EXTENSION_EVENT(shape, XCB_SHAPE_NOTIFY, event_handle_shape_notify); EXTENSION_EVENT(xkb, 0, event_handle_xkb_notify); + EXTENSION_EVENT(xfixes, XCB_XFIXES_SELECTION_NOTIFY, event_handle_xfixes_selection_notify); #undef EXTENSION_EVENT } @@ -1134,6 +1137,10 @@ void event_init(void) reply = xcb_get_extension_data(globalconf.connection, &xcb_xkb_id); if (reply && reply->present) globalconf.event_base_xkb = reply->first_event; + + reply = xcb_get_extension_data(globalconf.connection, &xcb_xfixes_id); + if (reply && reply->present) + globalconf.event_base_xfixes = reply->first_event; } // vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/globalconf.h b/globalconf.h index 46d42fd4..1001f913 100644 --- a/globalconf.h +++ b/globalconf.h @@ -110,9 +110,12 @@ typedef struct bool have_input_shape; /** Check for XKB extension */ bool have_xkb; + /** Check for XFixes extension */ + bool have_xfixes; uint8_t event_base_shape; uint8_t event_base_xkb; uint8_t event_base_randr; + uint8_t event_base_xfixes; /** Clients list */ client_array_t clients; /** Embedded windows */ diff --git a/luaa.c b/luaa.c index bfe8eee3..c26d3a31 100644 --- a/luaa.c +++ b/luaa.c @@ -49,6 +49,7 @@ #include "objects/drawable.h" #include "objects/drawin.h" #include "objects/screen.h" +#include "objects/selection_watcher.h" #include "objects/tag.h" #include "property.h" #include "selection.h" @@ -1035,6 +1036,9 @@ luaA_init(xdgHandle* xdg, string_array_t *searchpath) /* Export keys */ key_class_setup(L); + /* Export selection watcher */ + selection_watcher_class_setup(L); + /* add Lua search paths */ lua_getglobal(L, "package"); if (LUA_TTABLE != lua_type(L, 1)) diff --git a/objects/selection_watcher.c b/objects/selection_watcher.c new file mode 100644 index 00000000..69dd6914 --- /dev/null +++ b/objects/selection_watcher.c @@ -0,0 +1,199 @@ +/* + * selection_watcher.h - selection change watcher + * + * 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_watcher.h" +#include "common/luaobject.h" +#include "globalconf.h" + +#include + +#define REGISTRY_WATCHER_TABLE_INDEX "awesome_selection_watchers" + +typedef struct selection_watcher_t +{ + LUA_OBJECT_HEADER + /** Is this watcher currently active and watching? Used as reference with luaL_ref */ + int active_ref; + /** Atom identifying the selection to watch */ + xcb_atom_t selection; + /** Window used for watching */ + xcb_window_t window; +} selection_watcher_t; + +static lua_class_t selection_watcher_class; +LUA_OBJECT_FUNCS(selection_watcher_class, selection_watcher_t, selection_watcher) + +void +event_handle_xfixes_selection_notify(xcb_generic_event_t *ev) +{ + xcb_xfixes_selection_notify_event_t *e = (void *) ev; + lua_State *L = globalconf_get_lua_State(); + + /* Iterate over all active selection watchers */ + lua_pushliteral(L, REGISTRY_WATCHER_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_type(L, -1) == LUA_TUSERDATA) { + selection_watcher_t *selection = lua_touserdata(L, -1); + + if (selection->selection == e->selection && selection->window == e->window) { + lua_pushboolean(L, e->owner != XCB_NONE); + luaA_object_emit_signal(L, -2, "selection_changed", 1); + } + } + /* Remove the watcher */ + lua_pop(L, 1); + } + /* Remove watcher table */ + lua_pop(L, 1); +} + +/** Create a new selection watcher object. + * \param L The Lua VM state. + * \return The number of elements pushed on the stack. + */ +static int +luaA_selection_watcher_new(lua_State *L) +{ + size_t name_length; + const char *name; + xcb_intern_atom_reply_t *reply; + selection_watcher_t *selection; + + name = luaL_checklstring(L, 2, &name_length); + selection = (void *) selection_watcher_class.allocator(L); + selection->active_ref = LUA_NOREF; + selection->window = XCB_NONE; + + /* Get the atom identifying the selection to watch */ + reply = xcb_intern_atom_reply(globalconf.connection, + xcb_intern_atom_unchecked(globalconf.connection, false, name_length, name), + NULL); + if (reply) { + selection->selection = reply->atom; + p_delete(&reply); + } + + return 1; +} + +static int +luaA_selection_watcher_set_active(lua_State *L, selection_watcher_t *selection) +{ + bool b = luaA_checkboolean(L, -1); + bool is_active = selection->active_ref != LUA_NOREF; + if(b != is_active) + { + if (b) + { + /* Selection becomes active */ + + /* Create a window for it */ + if (selection->window == XCB_NONE) + 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); + + /* Start watching for selection changes */ + if (globalconf.have_xfixes) + { + xcb_xfixes_select_selection_input(globalconf.connection, selection->window, selection->selection, + XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER | + XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_WINDOW_DESTROY | + XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_CLIENT_CLOSE); + } else { + luaA_warn(L, "X11 server does not support the XFixes extension; cannot watch selections"); + } + + /* Reference the selection watcher. For this, first get the tracking + * table out of the registry. */ + lua_pushliteral(L, REGISTRY_WATCHER_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + + /* Then actually get the reference */ + lua_pushvalue(L, -3 - 1); + selection->active_ref = luaL_ref(L, -2); + + /* And pop the tracking table again */ + lua_pop(L, 1); + } else { + /* Stop watching and destroy the window */ + if (globalconf.have_xfixes) + xcb_xfixes_select_selection_input(globalconf.connection, selection->window, selection->selection, 0); + xcb_destroy_window(globalconf.connection, selection->window); + + /* Unreference the selection object */ + lua_pushliteral(L, REGISTRY_WATCHER_TABLE_INDEX); + lua_rawget(L, LUA_REGISTRYINDEX); + luaL_unref(L, -1, selection->active_ref); + lua_pop(L, 1); + + selection->active_ref = LUA_NOREF; + } + luaA_object_emit_signal(L, -3, "property::active", 0); + } + return 0; +} + +static int +luaA_selection_watcher_get_active(lua_State *L, selection_watcher_t *selection) +{ + lua_pushboolean(L, selection->active_ref != LUA_NOREF); + return 1; +} + +void +selection_watcher_class_setup(lua_State *L) +{ + static const struct luaL_Reg selection_watcher_methods[] = + { + LUA_CLASS_METHODS(selection_watcher) + { "__call", luaA_selection_watcher_new }, + { NULL, NULL } + }; + + static const struct luaL_Reg selection_watcher_meta[] = { + LUA_OBJECT_META(selection_watcher) + LUA_CLASS_META + { NULL, NULL } + }; + + /* Reference a table in the registry that tracks active watchers. This code + * does debug.getregistry()[REGISTRY_WATCHER_TABLE_INDEX] = {} + */ + lua_pushliteral(L, REGISTRY_WATCHER_TABLE_INDEX); + lua_newtable(L); + lua_rawset(L, LUA_REGISTRYINDEX); + + luaA_class_setup(L, &selection_watcher_class, "selection_watcher", NULL, + (lua_class_allocator_t) selection_watcher_new, NULL, NULL, + luaA_class_index_miss_property, luaA_class_newindex_miss_property, + selection_watcher_methods, selection_watcher_meta); + luaA_class_add_property(&selection_watcher_class, "active", + (lua_class_propfunc_t) luaA_selection_watcher_set_active, + (lua_class_propfunc_t) luaA_selection_watcher_get_active, + (lua_class_propfunc_t) luaA_selection_watcher_set_active); +} + +// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/objects/selection_watcher.h b/objects/selection_watcher.h new file mode 100644 index 00000000..cb743076 --- /dev/null +++ b/objects/selection_watcher.h @@ -0,0 +1,33 @@ +/* + * selection_watcher.h - selection change watcher 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_WATCHER_H +#define AWESOME_OBJECTS_SELECTION_WATCHER_H + +#include +#include + +void selection_watcher_class_setup(lua_State*); +void event_handle_xfixes_selection_notify(xcb_generic_event_t*); + +#endif + +// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/test-selection-watcher.lua b/tests/test-selection-watcher.lua new file mode 100644 index 00000000..59fb7465 --- /dev/null +++ b/tests/test-selection-watcher.lua @@ -0,0 +1,120 @@ +-- Test the selection watcher API + +local runner = require("_runner") +local spawn = require("awful.spawn") + +local header = [[ +local lgi = require("lgi") +local Gdk = lgi.Gdk +local Gtk = lgi.Gtk +local GLib = lgi.GLib +local clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) +clipboard:set_text("This is an experiment", -1) +]] + +local acquire_and_clear_clipboard = header .. [[ +GLib.idle_add(GLib.PRIORITY_DEFAULT, Gtk.main_quit) +Gtk.main() +]] + +local acquire_clipboard = header .. [[ +GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 60, Gtk.main_quit) +GLib.idle_add(GLib.PRIORITY_DEFAULT, function() + print("initialisation done") + io.stdout:flush() +end) +Gtk.main() +]] + +local had_error = false +local owned_clipboard_changes, unowned_clipboard_changes = 0, 0 + +local clipboard_watcher = selection_watcher("CLIPBOARD") +local clipboard_watcher_inactive = selection_watcher("CLIPBOARD") +local primary_watcher = selection_watcher("PRIMARY") + +clipboard_watcher:connect_signal("selection_changed", function(_, owned) + if owned then + owned_clipboard_changes = owned_clipboard_changes + 1 + else + unowned_clipboard_changes = unowned_clipboard_changes + 1 + end +end) +clipboard_watcher_inactive:connect_signal("selection_changed", function() + had_error = true + error("Unexpected signal on inactive CLIPBOARD watcher") +end) +primary_watcher:connect_signal("selection_changed", function() + had_error = true + error("Unexpected signal on PRIMARY watcher") +end) + +local function check_state(owned, unowned) + assert(not had_error, "there was an error") + assert(owned_clipboard_changes == owned, + string.format("expected %d owned changes, but got %d", owned, owned_clipboard_changes)) + assert(unowned_clipboard_changes == unowned, + string.format("expected %d unowned changes, but got %d", unowned, unowned_clipboard_changes)) +end + +local continue = false +runner.run_steps{ + -- Clear the clipboard to get to a known state + function() + check_state(0, 0) + spawn.with_line_callback({ "lua", "-e", acquire_and_clear_clipboard }, + { exit = function() continue = true end }) + return true + end, + + function() + -- Wait for the clipboard to be cleared + check_state(0, 0) + if not continue then + return + end + + -- Activate the watchers + clipboard_watcher.active = true + primary_watcher.active = true + awesome.sync() + + -- Set the clipboard + continue = false + spawn.with_line_callback({ "lua", "-e", acquire_clipboard }, + { stdout = function(line) + assert(line == "initialisation done", + "Unexpected line: " .. line) + continue = true + end }) + + return true + end, + + function() + -- Wait for the clipboard to be set + if not continue then + return + end + check_state(1, 0) + + -- Now clear the clipboard again + continue = false + spawn.with_line_callback({ "lua", "-e", acquire_and_clear_clipboard }, + { exit = function() continue = true end }) + + return true + end, + + function() + -- Wait for the clipboard to be set + if not continue then + return + end + + check_state(2, 1) + return true + end +} + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80