diff --git a/.editorconfig b/.editorconfig index 64f18915..b6b6a367 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,4 @@ trim_trailing_whitespace = true [*.xml] indent_style = space indent_size = 2 +tab_width = 8 diff --git a/include/rootston/desktop.h b/include/rootston/desktop.h index e5c5f806..289875c5 100644 --- a/include/rootston/desktop.h +++ b/include/rootston/desktop.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include "rootston/view.h" @@ -41,6 +42,7 @@ struct roots_desktop { struct wlr_gamma_control_manager *gamma_control_manager; struct wlr_screenshooter *screenshooter; struct wlr_server_decoration_manager *server_decoration_manager; + struct wlr_primary_selection_device_manager *primary_selection_device_manager; struct wl_listener output_add; struct wl_listener output_remove; diff --git a/include/wlr/types/wlr_primary_selection.h b/include/wlr/types/wlr_primary_selection.h new file mode 100644 index 00000000..a639b913 --- /dev/null +++ b/include/wlr/types/wlr_primary_selection.h @@ -0,0 +1,53 @@ +#ifndef WLR_TYPES_WLR_PRIMARY_SELECTION_H +#define WLR_TYPES_WLR_PRIMARY_SELECTION_H + +#include +#include + +struct wlr_primary_selection_device_manager { + struct wl_global *global; + + struct wl_listener display_destroy; + + void *data; +}; + +struct wlr_primary_selection_offer; + +struct wlr_primary_selection_source { + struct wl_resource *resource; + struct wlr_primary_selection_offer *offer; + struct wlr_seat_client *seat_client; + + struct wl_array mime_types; + + void (*send)(struct wlr_primary_selection_source *source, + const char *mime_type, int32_t fd); + void (*cancel)(struct wlr_primary_selection_source *source); + + struct { + struct wl_signal destroy; + } events; + + void *data; +}; + +struct wlr_primary_selection_offer { + struct wl_resource *resource; + struct wlr_primary_selection_source *source; + + struct wl_listener source_destroy; + + void *data; +}; + +struct wlr_primary_selection_device_manager * + wlr_primary_selection_device_manager_create(struct wl_display *display); +void wlr_primary_selection_device_manager_destroy( + struct wlr_primary_selection_device_manager *manager); + +void wlr_seat_client_send_primary_selection(struct wlr_seat_client *seat_client); +void wlr_seat_set_primary_selection(struct wlr_seat *seat, + struct wlr_primary_selection_source *source, uint32_t serial); + +#endif diff --git a/include/wlr/types/wlr_seat.h b/include/wlr/types/wlr_seat.h index dea9a9d0..28e9a615 100644 --- a/include/wlr/types/wlr_seat.h +++ b/include/wlr/types/wlr_seat.h @@ -21,6 +21,7 @@ struct wlr_seat_client { struct wl_list keyboards; struct wl_list touches; struct wl_list data_devices; + struct wl_list primary_selection_devices; struct { struct wl_signal destroy; @@ -181,12 +182,16 @@ struct wlr_seat { struct wlr_data_source *selection_source; uint32_t selection_serial; + struct wlr_primary_selection_source *primary_selection_source; + uint32_t primary_selection_serial; + struct wlr_seat_pointer_state pointer_state; struct wlr_seat_keyboard_state keyboard_state; struct wlr_seat_touch_state touch_state; struct wl_listener display_destroy; struct wl_listener selection_data_source_destroy; + struct wl_listener primary_selection_source_destroy; struct { struct wl_signal pointer_grab_begin; @@ -201,6 +206,7 @@ struct wlr_seat { struct wl_signal request_set_cursor; struct wl_signal selection; + struct wl_signal primary_selection; } events; void *data; diff --git a/protocol/gtk-primary-selection.xml b/protocol/gtk-primary-selection.xml new file mode 100644 index 00000000..02cab94f --- /dev/null +++ b/protocol/gtk-primary-selection.xml @@ -0,0 +1,225 @@ + + + + Copyright © 2015, 2016 Red Hat + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol provides the ability to have a primary selection device to + match that of the X server. This primary selection is a shortcut to the + common clipboard selection, where text just needs to be selected in order + to allow copying it elsewhere. The de facto way to perform this action + is the middle mouse button, although it is not limited to this one. + + Clients wishing to honor primary selection should create a primary + selection source and set it as the selection through + wp_primary_selection_device.set_selection whenever the text selection + changes. In order to minimize calls in pointer-driven text selection, + it should happen only once after the operation finished. Similarly, + a NULL source should be set when text is unselected. + + wp_primary_selection_offer objects are first announced through the + wp_primary_selection_device.data_offer event. Immediately after this event, + the primary data offer will emit wp_primary_selection_offer.offer events + to let know of the mime types being offered. + + When the primary selection changes, the client with the keyboard focus + will receive wp_primary_selection_device.selection events. Only the client + with the keyboard focus will receive such events with a non-NULL + wp_primary_selection_offer. Across keyboard focus changes, previously + focused clients will receive wp_primary_selection_device.events with a + NULL wp_primary_selection_offer. + + In order to request the primary selection data, the client must pass + a recent serial pertaining to the press event that is triggering the + operation, if the compositor deems the serial valid and recent, the + wp_primary_selection_source.send event will happen in the other end + to let the transfer begin. The client owning the primary selection + should write the requested data, and close the file descriptor + immediately. + + If the primary selection owner client disappeared during the transfer, + the client reading the data will receive a + wp_primary_selection_device.selection event with a NULL + wp_primary_selection_offer, the client should take this as a hint + to finish the reads related to the no longer existing offer. + + The primary selection owner should be checking for errors during + writes, merely cancelling the ongoing transfer if any happened. + + + + + The primary selection device manager is a singleton global object that + provides access to the primary selection. It allows to create + wp_primary_selection_source objects, as well as retrieving the per-seat + wp_primary_selection_device objects. + + + + + Create a new primary selection source. + + + + + + + Create a new data device for a given seat. + + + + + + + + Destroy the primary selection device manager. + + + + + + + + Replaces the current selection. The previous owner of the primary selection + will receive a wp_primary_selection_source.cancelled event. + + To unset the selection, set the source to NULL. + + + + + + + + Introduces a new wp_primary_selection_offer object that may be used + to receive the current primary selection. Immediately following this + event, the new wp_primary_selection_offer object will send + wp_primary_selection_offer.offer events to describe the offered mime + types. + + + + + + + The wp_primary_selection_device.selection event is sent to notify the + client of a new primary selection. This event is sent after the + wp_primary_selection.data_offer event introducing this object, and after + the offer has announced its mimetypes through + wp_primary_selection_offer.offer. + + The data_offer is valid until a new offer or NULL is received + or until the client loses keyboard focus. The client must destroy the + previous selection data_offer, if any, upon receiving this event. + + + + + + + Destroy the primary selection device. + + + + + + + A wp_primary_selection_offer represents an offer to transfer the contents + of the primary selection clipboard to the client. Similar to + wl_data_offer, the offer also describes the mime types that the source + will transferthat the + data can be converted to and provides the mechanisms for transferring the + data directly to the client. + + + + + To transfer the contents of the primary selection clipboard, the client + issues this request and indicates the mime type that it wants to + receive. The transfer happens through the passed file descriptor + (typically created with the pipe system call). The source client writes + the data in the mime type representation requested and then closes the + file descriptor. + + The receiving client reads from the read end of the pipe until EOF and + closes its end, at which point the transfer is complete. + + + + + + + + Destroy the primary selection offer. + + + + + + Sent immediately after creating announcing the wp_primary_selection_offer + through wp_primary_selection_device.data_offer. One event is sent per + offered mime type. + + + + + + + + The source side of a wp_primary_selection_offer, it provides a way to + describe the offered data and respond to requests to transfer the + requested contents of the primary selection clipboard. + + + + + This request adds a mime type to the set of mime types advertised to + targets. Can be called several times to offer multiple types. + + + + + + + Destroy the primary selection source. + + + + + + Request for the current primary selection contents from the client. + Send the specified mime type over the passed file descriptor, then + close it. + + + + + + + + This primary selection source is no longer valid. The client should + clean up and destroy this primary selection source. + + + + diff --git a/protocol/meson.build b/protocol/meson.build index ff54815a..f6aca5f5 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -23,6 +23,7 @@ wayland_scanner_client = generator( protocols = [ [wl_protocol_dir, 'unstable/xdg-shell/xdg-shell-unstable-v6.xml'], 'gamma-control.xml', + 'gtk-primary-selection.xml', 'screenshooter.xml', 'server-decoration.xml', ] @@ -30,6 +31,7 @@ protocols = [ client_protocols = [ [wl_protocol_dir, 'unstable/xdg-shell/xdg-shell-unstable-v6.xml'], 'gamma-control.xml', + 'gtk-primary-selection.xml', 'screenshooter.xml', 'server-decoration.xml', ] diff --git a/rootston/desktop.c b/rootston/desktop.c index c9fc0dc6..1431dc5d 100644 --- a/rootston/desktop.c +++ b/rootston/desktop.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -470,6 +471,8 @@ struct roots_desktop *desktop_create(struct roots_server *server, wlr_server_decoration_manager_set_default_mode( desktop->server_decoration_manager, WLR_SERVER_DECORATION_MANAGER_MODE_CLIENT); + desktop->primary_selection_device_manager = + wlr_primary_selection_device_manager_create(server->wl_display); return desktop; } diff --git a/types/meson.build b/types/meson.build index 4669165a..82031e89 100644 --- a/types/meson.build +++ b/types/meson.build @@ -12,6 +12,7 @@ lib_wlr_types = static_library( 'wlr_output.c', 'wlr_output_layout.c', 'wlr_pointer.c', + 'wlr_primary_selection.c', 'wlr_region.c', 'wlr_screenshooter.c', 'wlr_seat.c', diff --git a/types/wlr_primary_selection.c b/types/wlr_primary_selection.c new file mode 100644 index 00000000..63bdeceb --- /dev/null +++ b/types/wlr_primary_selection.c @@ -0,0 +1,375 @@ +#define _XOPEN_SOURCE 700 +#include +#include +#include +#include +#include +#include +#include + +static void client_source_send(struct wlr_primary_selection_source *source, + const char *mime_type, int32_t fd) { + gtk_primary_selection_source_send_send(source->resource, mime_type, fd); + close(fd); +} + +static void client_source_cancel( + struct wlr_primary_selection_source *source) { + gtk_primary_selection_source_send_cancelled(source->resource); +} + + +static void offer_handle_receive(struct wl_client *client, + struct wl_resource *resource, const char *mime_type, int32_t fd) { + struct wlr_primary_selection_offer *offer = + wl_resource_get_user_data(resource); + + if (offer->source && offer == offer->source->offer) { + offer->source->send(offer->source, mime_type, fd); + } else { + close(fd); + } +} + +static void offer_handle_destroy(struct wl_client *client, + struct wl_resource *resource) { + wl_resource_destroy(resource); +} + +static const struct gtk_primary_selection_offer_interface offer_impl = { + .receive = offer_handle_receive, + .destroy = offer_handle_destroy, +}; + +static void offer_resource_handle_destroy(struct wl_resource *resource) { + struct wlr_primary_selection_offer *offer = + wl_resource_get_user_data(resource); + + if (!offer->source) { + goto out; + } + + wl_list_remove(&offer->source_destroy.link); + + if (offer->source->offer != offer) { + goto out; + } + + if (offer->source->resource) { + gtk_primary_selection_source_send_cancelled(offer->source->resource); + } + + offer->source->offer = NULL; +out: + free(offer); +} + +static void offer_handle_source_destroy(struct wl_listener *listener, + void *data) { + struct wlr_primary_selection_offer *offer = + wl_container_of(listener, offer, source_destroy); + + offer->source = NULL; +} + + +static struct wlr_primary_selection_offer *source_send_offer( + struct wlr_primary_selection_source *source, + struct wlr_seat_client *target) { + if (wl_list_empty(&target->primary_selection_devices)) { + return NULL; + } + + struct wlr_primary_selection_offer *offer = + calloc(1, sizeof(struct wlr_primary_selection_offer)); + if (offer == NULL) { + return NULL; + } + + uint32_t version = wl_resource_get_version( + wl_resource_from_link(target->primary_selection_devices.next)); + offer->resource = wl_resource_create(target->client, + >k_primary_selection_offer_interface, version, 0); + if (offer->resource == NULL) { + free(offer); + return NULL; + } + wl_resource_set_implementation(offer->resource, &offer_impl, offer, + offer_resource_handle_destroy); + + offer->source_destroy.notify = offer_handle_source_destroy; + wl_signal_add(&source->events.destroy, &offer->source_destroy); + + struct wl_resource *target_resource; + wl_resource_for_each(target_resource, &target->primary_selection_devices) { + gtk_primary_selection_device_send_data_offer(target_resource, + offer->resource); + } + + char **p; + wl_array_for_each(p, &source->mime_types) { + gtk_primary_selection_offer_send_offer(offer->resource, *p); + } + + offer->source = source; + source->offer = offer; + + return offer; +} + +static void source_handle_offer(struct wl_client *client, + struct wl_resource *resource, const char *mime_type) { + struct wlr_primary_selection_source *source = + wl_resource_get_user_data(resource); + + char **p = wl_array_add(&source->mime_types, sizeof(*p)); + if (p) { + *p = strdup(mime_type); + } + if (p == NULL || *p == NULL) { + wl_resource_post_no_memory(resource); + } +} + +static void source_handle_destroy(struct wl_client *client, + struct wl_resource *resource) { + wl_resource_destroy(resource); +} + +static const struct gtk_primary_selection_source_interface source_impl = { + .offer = source_handle_offer, + .destroy = source_handle_destroy, +}; + +static void source_resource_handle_destroy(struct wl_resource *resource) { + struct wlr_primary_selection_source *source = + wl_resource_get_user_data(resource); + + wl_signal_emit(&source->events.destroy, source); + + char **p; + wl_array_for_each(p, &source->mime_types) { + free(*p); + } + wl_array_release(&source->mime_types); + + free(source); +} + + +void wlr_seat_client_send_primary_selection( + struct wlr_seat_client *seat_client) { + if (wl_list_empty(&seat_client->primary_selection_devices)) { + return; + } + + if (seat_client->seat->primary_selection_source) { + struct wlr_primary_selection_offer *offer = source_send_offer( + seat_client->seat->primary_selection_source, seat_client); + if (offer == NULL) { + return; + } + + struct wl_resource *resource; + wl_resource_for_each(resource, &seat_client->primary_selection_devices) { + gtk_primary_selection_device_send_selection(resource, offer->resource); + } + } else { + struct wl_resource *resource; + wl_resource_for_each(resource, &seat_client->primary_selection_devices) { + gtk_primary_selection_device_send_selection(resource, NULL); + } + } +} + +static void seat_client_primary_selection_source_destroy( + struct wl_listener *listener, void *data) { + struct wlr_seat *seat = + wl_container_of(listener, seat, primary_selection_source_destroy); + struct wlr_seat_client *seat_client = seat->keyboard_state.focused_client; + + if (seat_client && seat->keyboard_state.focused_surface) { + struct wl_resource *resource; + wl_resource_for_each(resource, &seat_client->primary_selection_devices) { + gtk_primary_selection_device_send_selection(resource, NULL); + } + } + + seat->primary_selection_source = NULL; + + wl_signal_emit(&seat->events.primary_selection, seat); +} + +void wlr_seat_set_primary_selection(struct wlr_seat *seat, + struct wlr_primary_selection_source *source, uint32_t serial) { + if (seat->primary_selection_source && + seat->primary_selection_serial - serial < UINT32_MAX / 2) { + return; + } + + if (seat->primary_selection_source) { + seat->primary_selection_source->cancel(seat->primary_selection_source); + seat->primary_selection_source = NULL; + wl_list_remove(&seat->primary_selection_source_destroy.link); + } + + seat->primary_selection_source = source; + seat->primary_selection_serial = serial; + + struct wlr_seat_client *focused_client = + seat->keyboard_state.focused_client; + if (focused_client) { + wlr_seat_client_send_primary_selection(focused_client); + } + + wl_signal_emit(&seat->events.primary_selection, seat); + + if (source) { + seat->primary_selection_source_destroy.notify = + seat_client_primary_selection_source_destroy; + wl_signal_add(&source->events.destroy, + &seat->primary_selection_source_destroy); + } +} + +static void device_handle_set_selection(struct wl_client *client, + struct wl_resource *resource, struct wl_resource *source_resource, + uint32_t serial) { + struct wlr_primary_selection_source *source = NULL; + if (source_resource != NULL) { + source = wl_resource_get_user_data(source_resource); + } + + struct wlr_seat_client *seat_client = + wl_resource_get_user_data(resource); + + // TODO: store serial and check against incoming serial here + wlr_seat_set_primary_selection(seat_client->seat, source, serial); +} + +static void device_handle_destroy(struct wl_client *client, + struct wl_resource *resource) { + wl_resource_destroy(resource); +} + +static const struct gtk_primary_selection_device_interface device_impl = { + .set_selection = device_handle_set_selection, + .destroy = device_handle_destroy, +}; + +static void device_resource_handle_destroy(struct wl_resource *resource) { + wl_list_remove(wl_resource_get_link(resource)); +} + + +static void device_manager_handle_create_source(struct wl_client *client, + struct wl_resource *manager_resource, uint32_t id) { + struct wlr_primary_selection_source *source = + calloc(1, sizeof(struct wlr_primary_selection_source)); + if (source == NULL) { + wl_client_post_no_memory(client); + return; + } + + int version = wl_resource_get_version(manager_resource); + source->resource = wl_resource_create(client, + >k_primary_selection_source_interface, version, id); + if (source->resource == NULL) { + free(source); + wl_client_post_no_memory(client); + return; + } + wl_resource_set_implementation(source->resource, &source_impl, source, + source_resource_handle_destroy); + + source->send = client_source_send; + source->cancel = client_source_cancel; + + wl_array_init(&source->mime_types); + wl_signal_init(&source->events.destroy); +} + +void device_manager_handle_get_device(struct wl_client *client, + struct wl_resource *manager_resource, uint32_t id, + struct wl_resource *seat_resource) { + struct wlr_seat_client *seat_client = + wl_resource_get_user_data(seat_resource); + + uint32_t version = wl_resource_get_version(manager_resource); + struct wl_resource *resource = wl_resource_create(client, + >k_primary_selection_device_interface, version, id); + if (resource == NULL) { + wl_resource_post_no_memory(manager_resource); + return; + } + wl_resource_set_implementation(resource, &device_impl, seat_client, + &device_resource_handle_destroy); + wl_list_insert(&seat_client->primary_selection_devices, + wl_resource_get_link(resource)); +} + +static void device_manager_handle_destroy(struct wl_client *client, + struct wl_resource *manager_resource) { + wl_resource_destroy(manager_resource); +} + +static const struct gtk_primary_selection_device_manager_interface +device_manager_impl = { + .create_source = device_manager_handle_create_source, + .get_device = device_manager_handle_get_device, + .destroy = device_manager_handle_destroy, +}; + + +static void primary_selection_device_manager_bind(struct wl_client *client, + void *data, uint32_t version, uint32_t id) { + struct wlr_primary_selection_device_manager *manager = data; + + struct wl_resource *resource = wl_resource_create(client, + >k_primary_selection_device_manager_interface, version, id); + if (resource == NULL) { + wl_client_post_no_memory(client); + return; + } + wl_resource_set_implementation(resource, &device_manager_impl, manager, + NULL); +} + +static void handle_display_destroy(struct wl_listener *listener, void *data) { + struct wlr_primary_selection_device_manager *manager = + wl_container_of(listener, manager, display_destroy); + wlr_primary_selection_device_manager_destroy(manager); +} + +struct wlr_primary_selection_device_manager * + wlr_primary_selection_device_manager_create( + struct wl_display *display) { + struct wlr_primary_selection_device_manager *manager = + calloc(1, sizeof(struct wlr_primary_selection_device_manager)); + if (manager == NULL) { + return NULL; + } + manager->global = wl_global_create(display, + >k_primary_selection_device_manager_interface, 1, manager, + primary_selection_device_manager_bind); + if (manager->global == NULL) { + free(manager); + return NULL; + } + + manager->display_destroy.notify = handle_display_destroy; + wl_display_add_destroy_listener(display, &manager->display_destroy); + + return manager; +} + +void wlr_primary_selection_device_manager_destroy( + struct wlr_primary_selection_device_manager *manager) { + if (manager == NULL) { + return; + } + wl_list_remove(&manager->display_destroy.link); + // TODO: free wl_resources + wl_global_destroy(manager->global); + free(manager); +} diff --git a/types/wlr_seat.c b/types/wlr_seat.c index 25b1fcd4..c1401b4a 100644 --- a/types/wlr_seat.c +++ b/types/wlr_seat.c @@ -8,6 +8,7 @@ #include #include #include +#include static void resource_destroy(struct wl_client *client, struct wl_resource *resource) { @@ -198,6 +199,9 @@ static void wlr_seat_client_resource_destroy(struct wl_resource *seat_resource) wl_resource_for_each_safe(resource, tmp, &client->data_devices) { wl_resource_destroy(resource); } + wl_resource_for_each_safe(resource, tmp, &client->primary_selection_devices) { + wl_resource_destroy(resource); + } wl_list_remove(&client->link); free(client); @@ -234,6 +238,7 @@ static void wl_seat_bind(struct wl_client *client, void *_wlr_seat, wl_list_init(&seat_client->keyboards); wl_list_init(&seat_client->touches); wl_list_init(&seat_client->data_devices); + wl_list_init(&seat_client->primary_selection_devices); wl_resource_set_implementation(seat_client->wl_resource, &wl_seat_impl, seat_client, wlr_seat_client_resource_destroy); wl_list_insert(&wlr_seat->clients, &seat_client->link); @@ -437,6 +442,7 @@ struct wlr_seat *wlr_seat_create(struct wl_display *display, const char *name) { wl_signal_init(&wlr_seat->events.request_set_cursor); wl_signal_init(&wlr_seat->events.selection); + wl_signal_init(&wlr_seat->events.primary_selection); wl_signal_init(&wlr_seat->events.pointer_grab_begin); wl_signal_init(&wlr_seat->events.pointer_grab_end); @@ -882,6 +888,7 @@ void wlr_seat_keyboard_enter(struct wlr_seat *seat, wl_array_release(&keys); wlr_seat_client_send_selection(client); + wlr_seat_client_send_primary_selection(client); } // reinitialize the focus destroy events