labwc/src/xdg.c

583 lines
17 KiB
C
Raw Normal View History

2021-09-24 21:45:48 +01:00
// SPDX-License-Identifier: GPL-2.0-only
2020-09-28 20:53:59 +01:00
#include <assert.h>
#include "common/mem.h"
2019-12-26 21:37:31 +00:00
#include "labwc.h"
#include "node.h"
#include "view.h"
#include "view-impl-common.h"
2022-06-15 02:02:50 +02:00
#include "workspaces.h"
2019-12-26 21:37:31 +00:00
#define CONFIGURE_TIMEOUT_MS 100
static struct xdg_toplevel_view *
xdg_toplevel_view_from_view(struct view *view)
{
assert(view->type == LAB_XDG_SHELL_VIEW);
return (struct xdg_toplevel_view *)view;
}
struct wlr_xdg_surface *
xdg_surface_from_view(struct view *view)
{
assert(view->type == LAB_XDG_SHELL_VIEW);
struct xdg_toplevel_view *xdg_toplevel_view =
(struct xdg_toplevel_view *)view;
assert(xdg_toplevel_view->xdg_surface);
return xdg_toplevel_view->xdg_surface;
}
static struct wlr_xdg_toplevel *
xdg_toplevel_from_view(struct view *view)
{
struct wlr_xdg_surface *xdg_surface = xdg_surface_from_view(view);
assert(xdg_surface->role == WLR_XDG_SURFACE_ROLE_TOPLEVEL);
assert(xdg_surface->toplevel);
return xdg_surface->toplevel;
}
2021-01-09 22:51:20 +00:00
static void
handle_new_xdg_popup(struct wl_listener *listener, void *data)
{
struct xdg_toplevel_view *xdg_toplevel_view =
wl_container_of(listener, xdg_toplevel_view, new_popup);
struct view *view = &xdg_toplevel_view->base;
2021-01-09 22:51:20 +00:00
struct wlr_xdg_popup *wlr_popup = data;
xdg_popup_create(view, wlr_popup);
}
static bool
has_ssd(struct view *view)
2020-08-06 14:51:45 +01:00
{
if (!rc.xdg_shell_server_side_deco) {
2020-08-06 14:51:45 +01:00
return false;
}
2020-08-06 14:51:45 +01:00
/*
* Some XDG shells refuse to disable CSD in which case their
2021-03-02 20:53:03 +00:00
* geometry.{x,y} seems to be greater than zero. We filter on that
* on the assumption that this will remain true.
2020-08-06 14:51:45 +01:00
*/
struct wlr_xdg_surface_state *current =
&xdg_surface_from_view(view)->current;
2022-04-04 20:53:36 +01:00
if (current->geometry.x || current->geometry.y) {
2020-08-06 14:51:45 +01:00
return false;
}
2020-08-06 14:51:45 +01:00
return true;
}
static void
handle_commit(struct wl_listener *listener, void *data)
{
struct view *view = wl_container_of(listener, view, commit);
struct wlr_xdg_surface *xdg_surface = xdg_surface_from_view(view);
2020-09-28 20:53:59 +01:00
assert(view->surface);
struct wlr_box size;
wlr_xdg_surface_get_geometry(xdg_surface, &size);
struct wlr_box *current = &view->current;
bool update_required = current->width != size.width
|| current->height != size.height;
uint32_t serial = view->pending_configure_serial;
if (serial > 0 && serial == xdg_surface->current.configure_serial) {
assert(view->pending_configure_timeout);
wl_event_source_remove(view->pending_configure_timeout);
view->pending_configure_serial = 0;
view->pending_configure_timeout = NULL;
update_required = true;
}
if (update_required) {
view_impl_apply_geometry(view, size.width, size.height);
}
}
static int
handle_configure_timeout(void *data)
{
struct view *view = data;
assert(view->pending_configure_serial > 0);
assert(view->pending_configure_timeout);
const char *app_id = view_get_string_prop(view, "app_id");
wlr_log(WLR_INFO, "client (%s) did not respond to configure request "
"in %d ms", app_id, CONFIGURE_TIMEOUT_MS);
wl_event_source_remove(view->pending_configure_timeout);
view->pending_configure_serial = 0;
view->pending_configure_timeout = NULL;
view_impl_apply_geometry(view, view->current.width,
view->current.height);
return 0; /* ignored per wl_event_loop docs */
}
static void
set_pending_configure_serial(struct view *view, uint32_t serial)
{
view->pending_configure_serial = serial;
if (!view->pending_configure_timeout) {
view->pending_configure_timeout =
wl_event_loop_add_timer(view->server->wl_event_loop,
handle_configure_timeout, view);
}
wl_event_source_timer_update(view->pending_configure_timeout,
CONFIGURE_TIMEOUT_MS);
}
static void
handle_map(struct wl_listener *listener, void *data)
2019-12-26 21:37:31 +00:00
{
2019-12-27 20:48:58 +00:00
struct view *view = wl_container_of(listener, view, map);
view->impl->map(view);
2019-12-26 21:37:31 +00:00
}
static void
handle_unmap(struct wl_listener *listener, void *data)
2019-12-26 21:37:31 +00:00
{
2019-12-27 20:48:58 +00:00
struct view *view = wl_container_of(listener, view, unmap);
view->impl->unmap(view);
2019-12-26 21:37:31 +00:00
}
static void
handle_destroy(struct wl_listener *listener, void *data)
2019-12-26 21:37:31 +00:00
{
2019-12-27 20:48:58 +00:00
struct view *view = wl_container_of(listener, view, destroy);
assert(view->type == LAB_XDG_SHELL_VIEW);
struct xdg_toplevel_view *xdg_toplevel_view =
xdg_toplevel_view_from_view(view);
xdg_toplevel_view->xdg_surface->data = NULL;
xdg_toplevel_view->xdg_surface = NULL;
/* Remove xdg-shell view specific listeners */
wl_list_remove(&xdg_toplevel_view->set_app_id.link);
wl_list_remove(&xdg_toplevel_view->new_popup.link);
if (view->pending_configure_timeout) {
wl_event_source_remove(view->pending_configure_timeout);
view->pending_configure_timeout = NULL;
}
view_destroy(view);
2019-12-26 21:37:31 +00:00
}
static void
handle_request_move(struct wl_listener *listener, void *data)
2019-12-26 21:37:31 +00:00
{
2021-09-21 22:05:56 +01:00
/*
* This event is raised when a client would like to begin an interactive
2019-12-26 21:37:31 +00:00
* move, typically because the user clicked on their client-side
2019-12-27 21:22:45 +00:00
* decorations. Note that a more sophisticated compositor should check
* the provied serial against a list of button press serials sent to
* this
* client, to prevent the client from requesting this whenever they
2021-09-21 22:05:56 +01:00
* want.
*/
2019-12-27 20:48:58 +00:00
struct view *view = wl_container_of(listener, view, request_move);
2020-10-21 20:30:06 +01:00
interactive_begin(view, LAB_INPUT_STATE_MOVE, 0);
2019-12-26 21:37:31 +00:00
}
static void
handle_request_resize(struct wl_listener *listener, void *data)
2019-12-26 21:37:31 +00:00
{
2021-09-21 22:05:56 +01:00
/*
* This event is raised when a client would like to begin an interactive
2019-12-26 21:37:31 +00:00
* resize, typically because the user clicked on their client-side
2019-12-27 21:22:45 +00:00
* decorations. Note that a more sophisticated compositor should check
* the provied serial against a list of button press serials sent to
* this
* client, to prevent the client from requesting this whenever they
2021-09-21 22:05:56 +01:00
* want.
*/
2019-12-26 21:37:31 +00:00
struct wlr_xdg_toplevel_resize_event *event = data;
2019-12-27 20:48:58 +00:00
struct view *view = wl_container_of(listener, view, request_resize);
2020-10-21 20:30:06 +01:00
interactive_begin(view, LAB_INPUT_STATE_RESIZE, event->edges);
2019-12-26 21:37:31 +00:00
}
static void
handle_request_minimize(struct wl_listener *listener, void *data)
{
struct view *view = wl_container_of(listener, view, request_minimize);
view_minimize(view, xdg_toplevel_from_view(view)->requested.minimized);
}
static void
handle_request_maximize(struct wl_listener *listener, void *data)
{
struct view *view = wl_container_of(listener, view, request_maximize);
if (!view->mapped && !view->output) {
2023-02-28 11:46:48 -05:00
view_set_output(view, output_nearest_to_cursor(view->server));
}
view_maximize(view, xdg_toplevel_from_view(view)->requested.maximized,
/*store_natural_geometry*/ true);
}
static void
set_fullscreen_from_request(struct view *view,
struct wlr_xdg_toplevel_requested *requested)
{
if (!view->fullscreen && requested->fullscreen
&& requested->fullscreen_output) {
2023-02-28 11:46:48 -05:00
view_set_output(view, output_from_wlr_output(view->server,
requested->fullscreen_output));
}
view_set_fullscreen(view, requested->fullscreen);
}
2021-08-23 22:05:30 +01:00
static void
handle_request_fullscreen(struct wl_listener *listener, void *data)
{
struct view *view = wl_container_of(listener, view, request_fullscreen);
if (!view->mapped && !view->output) {
2023-02-28 11:46:48 -05:00
view_set_output(view, output_nearest_to_cursor(view->server));
}
set_fullscreen_from_request(view,
&xdg_toplevel_from_view(view)->requested);
2021-08-23 22:05:30 +01:00
}
static void
handle_set_title(struct wl_listener *listener, void *data)
{
struct view *view = wl_container_of(listener, view, set_title);
view_update_title(view);
}
static void
handle_set_app_id(struct wl_listener *listener, void *data)
{
struct xdg_toplevel_view *xdg_toplevel_view =
wl_container_of(listener, xdg_toplevel_view, set_app_id);
struct view *view = &xdg_toplevel_view->base;
view_update_app_id(view);
}
static void
xdg_toplevel_view_configure(struct view *view, struct wlr_box geo)
{
uint32_t serial = 0;
view_adjust_size(view, &geo.width, &geo.height);
/*
* We do not need to send a configure request unless the size
* changed (wayland has no notion of a global position). If the
* size is the same (and there is no pending configure request)
* then we can just move the view directly.
*/
if (geo.width != view->pending.width
|| geo.height != view->pending.height) {
serial = wlr_xdg_toplevel_set_size(xdg_toplevel_from_view(view),
geo.width, geo.height);
}
view->pending = geo;
if (serial > 0) {
set_pending_configure_serial(view, serial);
} else if (view->pending_configure_serial == 0) {
/*
* We can't assume here that view->current is equal to
* view->pending because some clients (e.g. terminals)
* refuse to accept the exact size we requested. To
* account for the size difference and avoid visual
* glitches during resize, we apply the same position
* adjustments here as in handle_commit().
*/
view_impl_apply_geometry(view, view->current.width,
view->current.height);
}
}
static void
xdg_toplevel_view_close(struct view *view)
2020-09-02 21:00:28 +01:00
{
wlr_xdg_toplevel_send_close(xdg_toplevel_from_view(view));
2020-09-02 21:00:28 +01:00
}
static void
xdg_toplevel_view_maximize(struct view *view, bool maximized)
{
wlr_xdg_toplevel_set_maximized(xdg_toplevel_from_view(view), maximized);
}
static void
xdg_toplevel_view_set_activated(struct view *view, bool activated)
{
wlr_xdg_toplevel_set_activated(xdg_toplevel_from_view(view), activated);
}
2021-08-23 22:05:30 +01:00
static void
xdg_toplevel_view_set_fullscreen(struct view *view, bool fullscreen)
{
wlr_xdg_toplevel_set_fullscreen(xdg_toplevel_from_view(view),
fullscreen);
2020-09-25 20:22:18 +01:00
}
static struct view *
lookup_view_by_xdg_toplevel(struct server *server,
struct wlr_xdg_toplevel *xdg_toplevel)
{
struct view *view;
wl_list_for_each(view, &server->views, link) {
if (view->type != LAB_XDG_SHELL_VIEW) {
continue;
}
if (xdg_toplevel_from_view(view) == xdg_toplevel) {
return view;
}
}
return NULL;
}
static void
position_xdg_toplevel_view(struct view *view)
{
struct wlr_xdg_toplevel *parent_xdg_toplevel =
xdg_toplevel_from_view(view)->parent;
if (!parent_xdg_toplevel) {
view_center(view, NULL);
} else {
/*
* If child-toplevel-views, we center-align relative to their
* parents
*/
struct view *parent = lookup_view_by_xdg_toplevel(
view->server, parent_xdg_toplevel);
assert(parent);
2023-02-28 11:46:48 -05:00
view_set_output(view, parent->output);
view_center(view, &parent->pending);
}
}
static const char *
xdg_toplevel_view_get_string_prop(struct view *view, const char *prop)
{
struct wlr_xdg_toplevel *xdg_toplevel = xdg_toplevel_from_view(view);
if (!strcmp(prop, "title")) {
return xdg_toplevel->title;
}
if (!strcmp(prop, "app_id")) {
return xdg_toplevel->app_id;
}
return "";
}
static void
xdg_toplevel_view_map(struct view *view)
{
if (view->mapped) {
return;
}
view->mapped = true;
if (!view->output) {
2023-02-28 11:46:48 -05:00
view_set_output(view, output_nearest_to_cursor(view->server));
}
struct wlr_xdg_surface *xdg_surface = xdg_surface_from_view(view);
view->surface = xdg_surface->surface;
wlr_scene_node_set_enabled(&view->scene_tree->node, true);
if (!view->been_mapped) {
struct wlr_xdg_toplevel_requested *requested =
&xdg_toplevel_from_view(view)->requested;
foreign_toplevel_handle_create(view);
view_set_decorations(view, has_ssd(view));
/*
* Set initial "pending" dimensions (may be modified by
* view_set_fullscreen/view_maximize() below). "Current"
* dimensions remain zero until handle_commit().
*/
if (wlr_box_empty(&view->pending)) {
struct wlr_box size;
wlr_xdg_surface_get_geometry(xdg_surface, &size);
view->pending.width = size.width;
view->pending.height = size.height;
}
/*
* Set initial "pending" position for floating views.
* Do this before view_set_fullscreen/view_maximize() so
* that the position is saved with the natural geometry.
*
* FIXME: the natural geometry is not saved if either
* handle_request_fullscreen/handle_request_maximize()
* is called before map (try "foot --maximized").
*/
if (view_is_floating(view)) {
position_xdg_toplevel_view(view);
}
if (!view->fullscreen && requested->fullscreen) {
set_fullscreen_from_request(view, requested);
} else if (!view->maximized && requested->maximized) {
view_maximize(view, true,
/*store_natural_geometry*/ true);
}
/*
* Set initial "current" position directly before
* calling view_moved() to reduce flicker
*/
view->current.x = view->pending.x;
view->current.y = view->pending.y;
view_moved(view);
view->been_mapped = true;
}
view->commit.notify = handle_commit;
wl_signal_add(&xdg_surface->surface->events.commit, &view->commit);
view_impl_map(view);
}
static void
xdg_toplevel_view_unmap(struct view *view)
{
if (view->mapped) {
view->mapped = false;
wlr_scene_node_set_enabled(&view->scene_tree->node, false);
wl_list_remove(&view->commit.link);
desktop_focus_topmost_mapped_view(view->server);
}
}
static const struct view_impl xdg_toplevel_view_impl = {
.configure = xdg_toplevel_view_configure,
.close = xdg_toplevel_view_close,
.get_string_prop = xdg_toplevel_view_get_string_prop,
.map = xdg_toplevel_view_map,
.set_activated = xdg_toplevel_view_set_activated,
2021-08-23 22:05:30 +01:00
.set_fullscreen = xdg_toplevel_view_set_fullscreen,
.unmap = xdg_toplevel_view_unmap,
.maximize = xdg_toplevel_view_maximize,
.move_to_front = view_impl_move_to_front,
};
Add xdg-activation protocol This PR allows applications to activate themselves *if they provide a valid xdg_activation token* (e.g. raise to the top and get keyboard focus). These tokens are given out by the xdg_activation protocol implemented by wlroots and can be configured by the client requesting the token in three ways: - an "empty" token (apparently used to tell the compositor about "urgency") - seat / input serial attached - surface attached Wlroots makes sure that - If the client attached the seat / input serial: those two are valid. - If the client attached a surface: that it has keyboard focus at the point where the request is finalized. There is a patch [1] pending for backport to wlroots 0.16 that also allows valid tokens when the supplied surface had cursor focus. - a token is only valid for 30 seconds after being given out The token can then be used by the client or given to other clients by unspecified means (e.g. via environment variable or dbus) which then may use the token on their own surface and request activation. We only handle the actual request activation part: - If the seat is set on the token we know wlroots validated seat and input serial - Thus, if no seat is set we deny the activation request so we don't have windows suddenly popping up and taking over the keyboard focus (focus stealing prevention) - We should also check for the surface being set but we can't do that with wlroots 0.16 as it will reset the surface to `NULL` when it is destroyed (which is something that usually happens for notifications). Once we move to wlroots 0.17.x we can add the missing surface check because it provides a `new_token` signal. We can use it to attach further details to the token which are then verified later when we decide if we allow the activate request or not. With this PR in place the following setup should activate windows: - launching an URL in foot should activate the target application if it is already running, foot requests a proper token and then sets it as `XDG_ACTIVATION_TOKEN` environment var before spawning `xdg-open` - clicking on a `mako` notification with a `default` action defined should request a proper token which is then given to the application starting the notification and can thus be used to activate itself This protocol is still very much in the process of being implemented / finalized all over the place (e.g. GTK / QT / Firefox / notification daemons, ..) but we should do our part and remove labwc from the puzzle of potential issues causing this not to work. [1] https://gitlab.freedesktop.org/wlroots/wlroots/-/commit/f6008ffff41f67e3c20bd8b3be8f216da6a4bb30)
2023-02-04 10:07:46 +01:00
void
xdg_activation_handle_request(struct wl_listener *listener, void *data)
{
const struct wlr_xdg_activation_v1_request_activate_event *event = data;
if (!wlr_surface_is_xdg_surface(event->surface)) {
return;
}
struct wlr_xdg_surface *xdg_surface =
wlr_xdg_surface_from_wlr_surface(event->surface);
struct view *view = xdg_surface ? xdg_surface->data : NULL;
if (!view) {
wlr_log(WLR_INFO, "Not activating surface - no view attached to surface");
return;
}
if (!event->token->seat) {
wlr_log(WLR_INFO, "Denying focus request, seat wasn't supplied");
return;
}
/*
* We do not check for event->token->surface here because it may already
* be destroyed and thus being NULL. With wlroots 0.17 we can hook into
* the `new_token` signal, attach further information to the token and
* then react to that information here instead. For now we just check
* for the seat / serial being correct and then allow the request.
*/
/*
* TODO: This is the exact same code as used in foreign.c.
* Refactor it into a public helper function somewhere.
*/
wlr_log(WLR_DEBUG, "Activating surface");
if (view->workspace != view->server->workspace_current) {
workspaces_switch_to(view->workspace);
}
desktop_focus_and_activate_view(&view->server->seat, view);
desktop_move_to_front(view);
}
/*
* We use the following struct user_data pointers:
* - wlr_xdg_surface->data = view
* for the wlr_xdg_toplevel_decoration_v1 implementation
* - wlr_surface->data = scene_tree
* to help the popups find their parent nodes
*/
void
xdg_surface_new(struct wl_listener *listener, void *data)
2019-12-26 21:37:31 +00:00
{
2019-12-27 20:48:58 +00:00
struct server *server =
2019-12-26 21:37:31 +00:00
wl_container_of(listener, server, new_xdg_surface);
struct wlr_xdg_surface *xdg_surface = data;
/*
* We deal with popups in xdg-popup.c and layers.c as they have to be
* treated differently
*/
if (xdg_surface->role != WLR_XDG_SURFACE_ROLE_TOPLEVEL) {
2019-12-26 21:37:31 +00:00
return;
}
2020-10-08 19:58:47 +01:00
wlr_xdg_surface_ping(xdg_surface);
2019-12-26 21:37:31 +00:00
struct xdg_toplevel_view *xdg_toplevel_view = znew(*xdg_toplevel_view);
struct view *view = &xdg_toplevel_view->base;
2019-12-26 21:37:31 +00:00
view->server = server;
view->type = LAB_XDG_SHELL_VIEW;
view->impl = &xdg_toplevel_view_impl;
xdg_toplevel_view->xdg_surface = xdg_surface;
2019-12-26 21:37:31 +00:00
2022-06-15 02:02:50 +02:00
view->workspace = server->workspace_current;
view->scene_tree = wlr_scene_tree_create(view->workspace->tree);
wlr_scene_node_set_enabled(&view->scene_tree->node, false);
2022-02-21 03:18:38 +01:00
struct wlr_scene_tree *tree = wlr_scene_xdg_surface_create(
view->scene_tree, xdg_surface);
if (!tree) {
/* TODO: might need further clean up */
wl_resource_post_no_memory(xdg_surface->resource);
free(xdg_toplevel_view);
return;
}
view->scene_node = &tree->node;
node_descriptor_create(&view->scene_tree->node,
LAB_NODE_DESC_VIEW, view);
/* In support of xdg_toplevel_decoration */
xdg_surface->data = view;
/* In support of xdg popups */
xdg_surface->surface->data = tree;
2020-09-03 21:40:27 +01:00
view->map.notify = handle_map;
2019-12-26 21:37:31 +00:00
wl_signal_add(&xdg_surface->events.map, &view->map);
2020-09-03 21:40:27 +01:00
view->unmap.notify = handle_unmap;
2019-12-26 21:37:31 +00:00
wl_signal_add(&xdg_surface->events.unmap, &view->unmap);
2020-09-03 21:40:27 +01:00
view->destroy.notify = handle_destroy;
2019-12-26 21:37:31 +00:00
wl_signal_add(&xdg_surface->events.destroy, &view->destroy);
struct wlr_xdg_toplevel *toplevel = xdg_surface->toplevel;
2020-09-03 21:40:27 +01:00
view->request_move.notify = handle_request_move;
2019-12-26 21:37:31 +00:00
wl_signal_add(&toplevel->events.request_move, &view->request_move);
2020-09-03 21:40:27 +01:00
view->request_resize.notify = handle_request_resize;
2019-12-26 21:37:31 +00:00
wl_signal_add(&toplevel->events.request_resize, &view->request_resize);
view->request_minimize.notify = handle_request_minimize;
wl_signal_add(&toplevel->events.request_minimize, &view->request_minimize);
view->request_maximize.notify = handle_request_maximize;
wl_signal_add(&toplevel->events.request_maximize, &view->request_maximize);
2021-08-23 22:05:30 +01:00
view->request_fullscreen.notify = handle_request_fullscreen;
wl_signal_add(&toplevel->events.request_fullscreen, &view->request_fullscreen);
view->set_title.notify = handle_set_title;
wl_signal_add(&toplevel->events.set_title, &view->set_title);
/* Events specific to XDG toplevel views */
xdg_toplevel_view->set_app_id.notify = handle_set_app_id;
wl_signal_add(&toplevel->events.set_app_id, &xdg_toplevel_view->set_app_id);
xdg_toplevel_view->new_popup.notify = handle_new_xdg_popup;
wl_signal_add(&xdg_surface->events.new_popup, &xdg_toplevel_view->new_popup);
2019-12-26 21:37:31 +00:00
wl_list_insert(&server->views, &view->link);
}