diff --git a/src/server/frontend_wayland/CMakeLists.txt b/src/server/frontend_wayland/CMakeLists.txt index 1d0e325594a..954da2b643b 100644 --- a/src/server/frontend_wayland/CMakeLists.txt +++ b/src/server/frontend_wayland/CMakeLists.txt @@ -65,6 +65,7 @@ set( ${PROJECT_SOURCE_DIR}/src/include/server/mir/frontend/buffer_stream.h wp_viewporter.cpp wp_viewporter.h fractional_scale_v1.cpp fractional_scale_v1.h + xdg_activation_v1.cpp xdg_activation_v1.h ) add_custom_command( @@ -113,5 +114,6 @@ target_link_libraries(mirfrontend-wayland mircore PRIVATE PkgConfig::GIOUnix + PkgConfig::UUID ) diff --git a/src/server/frontend_wayland/wayland_connector.cpp b/src/server/frontend_wayland/wayland_connector.cpp index f6486d70b0f..dd88de115b1 100644 --- a/src/server/frontend_wayland/wayland_connector.cpp +++ b/src/server/frontend_wayland/wayland_connector.cpp @@ -241,7 +241,8 @@ mf::WaylandConnector::WaylandConnector( WaylandProtocolExtensionFilter const& extension_filter, bool enable_key_repeat, std::shared_ptr const& session_lock, - std::shared_ptr const& decoration_strategy) + std::shared_ptr const& decoration_strategy, + std::shared_ptr const& session_coordinator) : extension_filter{extension_filter}, display{wl_display_create(), &cleanup_display}, pause_signal{eventfd(0, EFD_CLOEXEC | EFD_SEMAPHORE)}, @@ -328,7 +329,9 @@ mf::WaylandConnector::WaylandConnector( main_loop, desktop_file_manager, session_lock_, - decoration_strategy}); + decoration_strategy, + session_coordinator, + keyboard_observer_registrar}); shm_global = std::make_unique(display.get(), executor); diff --git a/src/server/frontend_wayland/wayland_connector.h b/src/server/frontend_wayland/wayland_connector.h index 096cc52eb28..970355f00fe 100644 --- a/src/server/frontend_wayland/wayland_connector.h +++ b/src/server/frontend_wayland/wayland_connector.h @@ -67,6 +67,7 @@ class IdleHub; class Surface; class TextInputHub; class SessionLock; +class SessionCoordinator; } namespace time { @@ -113,6 +114,8 @@ class WaylandExtensions std::shared_ptr desktop_file_manager; std::shared_ptr session_lock; std::shared_ptr decoration_strategy; + std::shared_ptr session_coordinator; + std::shared_ptr> keyboard_observer_registrar; }; WaylandExtensions() = default; @@ -165,7 +168,8 @@ class WaylandConnector : public Connector WaylandProtocolExtensionFilter const& extension_filter, bool enable_key_repeat, std::shared_ptr const& session_lock, - std::shared_ptr const& decoration_strategy); + std::shared_ptr const& decoration_strategy, + std::shared_ptr const& session_coordinator); ~WaylandConnector() override; diff --git a/src/server/frontend_wayland/wayland_default_configuration.cpp b/src/server/frontend_wayland/wayland_default_configuration.cpp index 9e9b66dab4b..fd76d7b1a18 100644 --- a/src/server/frontend_wayland/wayland_default_configuration.cpp +++ b/src/server/frontend_wayland/wayland_default_configuration.cpp @@ -43,6 +43,7 @@ #include "wl_seat.h" #include "wl_shell.h" #include "wlr_screencopy_v1.h" +#include "xdg_activation_v1.h" #include "xdg-decoration-unstable-v1_wrapper.h" #include "xdg_decoration_unstable_v1.h" #include "xdg_output_v1.h" @@ -226,6 +227,16 @@ std::vector const internal_extension_builders = { { return mf::create_fractional_scale_v1(ctx.display); }), + make_extension_builder([](auto const& ctx) + { + return mf::create_xdg_activation_v1( + ctx.display, + ctx.shell, + ctx.session_coordinator, + ctx.main_loop, + ctx.keyboard_observer_registrar, + *ctx.wayland_executor); + }), }; ExtensionBuilder const xwayland_builder { @@ -327,6 +338,7 @@ auto mf::get_standard_extensions() -> std::vector mw::TextInputManagerV3::interface_name, mw::MirShellV1::interface_name, mw::XdgDecorationManagerV1::interface_name, + mw::XdgActivationV1::interface_name, mw::FractionalScaleManagerV1::interface_name}; } @@ -384,7 +396,8 @@ std::shared_ptr wayland_extension_filter, enable_repeat, the_session_lock(), - the_decoration_strategy()); + the_decoration_strategy(), + the_session_coordinator()); }); } diff --git a/src/server/frontend_wayland/xdg_activation_v1.cpp b/src/server/frontend_wayland/xdg_activation_v1.cpp new file mode 100644 index 00000000000..dd77469d351 --- /dev/null +++ b/src/server/frontend_wayland/xdg_activation_v1.cpp @@ -0,0 +1,432 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 or 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ + +#include "xdg_activation_v1.h" +#include "wl_surface.h" +#include "wl_seat.h" +#include "wl_client.h" +#include "mir/main_loop.h" +#include "mir/input/keyboard_observer.h" +#include "mir/scene/surface.h" +#include "mir/scene/session_listener.h" +#include "mir/scene/session_coordinator.h" +#include "mir/shell/shell.h" +#include "mir/wayland/protocol_error.h" +#include "mir/log.h" +#include +#include +#include +#include + +namespace mf = mir::frontend; +namespace mw = mir::wayland; +namespace ms = mir::scene; +namespace msh = mir::shell; +namespace mi = mir::input; + +namespace +{ +/// Time in milliseconds that the compositor will wait before invalidating a token +auto constexpr timeout_ms = std::chrono::milliseconds(3000); + +std::string generate_token() +{ + uuid_t uuid; + uuid_generate(uuid); + return { reinterpret_cast(uuid), 4 }; +} +} + +namespace mir +{ +namespace frontend +{ +struct XdgActivationTokenData +{ + XdgActivationTokenData( + std::string const& token, + std::unique_ptr alarm_, + std::shared_ptr const& session) + : token{token}, + alarm{std::move(alarm_)}, + session{session} + { + alarm->reschedule_in(timeout_ms); + } + + std::string const token; + std::unique_ptr const alarm; + std::weak_ptr session; + + std::optional serial; + std::optional seat; +}; + + +class XdgActivationV1 : public wayland::XdgActivationV1::Global +{ +public: + XdgActivationV1( + struct wl_display* display, + std::shared_ptr const& shell, + std::shared_ptr const& session_coordinator, + std::shared_ptr const& main_loop, + std::shared_ptr> const& keyboard_observer_registrar, + Executor& wayland_executor); + ~XdgActivationV1(); + + std::shared_ptr const& create_token(std::shared_ptr const& session); + std::shared_ptr try_consume_token(std::string const& token); + void invalidate_all(); + void invalidate_if_not_from_session(std::shared_ptr const&); + +private: + class Instance : public wayland::XdgActivationV1 + { + public: + Instance( + struct wl_resource* resource, + mf::XdgActivationV1* xdg_activation_v1, + std::shared_ptr const& shell, + std::shared_ptr const& session_coordinator, + std::shared_ptr const& main_loop); + + private: + void get_activation_token(struct wl_resource* id) override; + + void activate(std::string const& token, struct wl_resource* surface) override; + + mf::XdgActivationV1* xdg_activation_v1; + std::shared_ptr shell; + std::shared_ptr const session_coordinator; + std::shared_ptr main_loop; + }; + + class KeyboardObserver: public input::KeyboardObserver + { + public: + KeyboardObserver(XdgActivationV1* xdg_activation_v1); + void keyboard_event(std::shared_ptr const& event) override; + void keyboard_focus_set(std::shared_ptr const& surface) override; + + private: + XdgActivationV1* xdg_activation_v1; + }; + + class SessionListener : public ms::SessionListener + { + public: + SessionListener(XdgActivationV1* xdg_activation_v1); + void starting(std::shared_ptr const&) override {} + void stopping(std::shared_ptr const&) override {} + void focused(std::shared_ptr const& session) override; + void unfocused() override {} + + void surface_created(ms::Session&, std::shared_ptr const&) override {} + void destroying_surface(ms::Session&, std::shared_ptr const&) override {} + + void buffer_stream_created( + ms::Session&, + std::shared_ptr const&) override {} + void buffer_stream_destroyed( + ms::Session&, + std::shared_ptr const&) override {} + + private: + XdgActivationV1* xdg_activation_v1; + }; + + void bind(wl_resource* resource) override; + + std::shared_ptr shell; + std::shared_ptr const session_coordinator; + std::shared_ptr main_loop; + std::shared_ptr> keyboard_observer_registrar; + std::shared_ptr keyboard_observer; + std::vector> pending_tokens; + std::mutex pending_tokens_mutex; +}; + +class XdgActivationTokenV1 : public wayland::XdgActivationTokenV1 +{ +public: + XdgActivationTokenV1( + struct wl_resource* resource, + std::shared_ptr const& token); + +private: + void set_serial(uint32_t serial, struct wl_resource* seat) override; + + void set_app_id(std::string const& app_id) override; + + void set_surface(struct wl_resource* surface) override; + + void commit() override; + + std::shared_ptr token; + bool used = false; +}; +} +} + +auto mf::create_xdg_activation_v1( + struct wl_display* display, + std::shared_ptr const& shell, + std::shared_ptr const& session_coordinator, + std::shared_ptr const& main_loop, + std::shared_ptr> const& keyboard_observer_registrar, + Executor& wayland_executor) + -> std::shared_ptr +{ + return std::make_shared(display, shell, session_coordinator, main_loop, keyboard_observer_registrar, wayland_executor); +} + +mf::XdgActivationV1::XdgActivationV1( + struct wl_display* display, + std::shared_ptr const& shell, + std::shared_ptr const& session_coordinator, + std::shared_ptr const& main_loop, + std::shared_ptr> const& keyboard_observer_registrar, + Executor& wayland_executor) + : Global(display, Version<1>()), + shell{shell}, + session_coordinator{session_coordinator}, + main_loop{main_loop}, + keyboard_observer_registrar{keyboard_observer_registrar}, + keyboard_observer{std::make_shared(this)} +{ + keyboard_observer_registrar->register_interest(keyboard_observer, wayland_executor); +} + +mf::XdgActivationV1::~XdgActivationV1() +{ + keyboard_observer_registrar->unregister_interest(*keyboard_observer); +} + +std::shared_ptr const& mf::XdgActivationV1::create_token(std::shared_ptr const& session) +{ + auto generated = generate_token(); + auto token = std::make_shared(generated, main_loop->create_alarm([this, generated]() + { + try_consume_token(generated); + }), session); + + { + std::lock_guard guard(pending_tokens_mutex); + pending_tokens.emplace_back(std::move(token)); + return pending_tokens.back(); + } +} + +std::shared_ptr mf::XdgActivationV1::try_consume_token(std::string const& token) +{ + { + std::lock_guard guard(pending_tokens_mutex); + for (auto it = pending_tokens.begin(); it != pending_tokens.end(); it++) + { + if (it->get()->token == token) + { + auto result = *it; + pending_tokens.erase(it); + return result; + } + } + } + + return nullptr; +} + +void mf::XdgActivationV1::invalidate_all() +{ + std::lock_guard guard(pending_tokens_mutex); + pending_tokens.clear(); +} + +void mf::XdgActivationV1::invalidate_if_not_from_session(std::shared_ptr const& session) +{ + std::lock_guard guard(pending_tokens_mutex); + std::erase_if(pending_tokens, [&](std::shared_ptr const& token) + { + return token->session.expired() || token->session.lock() != session; + }); +} + +void mf::XdgActivationV1::bind(struct wl_resource* resource) +{ + new Instance{resource, this, shell, session_coordinator, main_loop}; +} + +mf::XdgActivationV1::Instance::Instance( + struct wl_resource* resource, + mf::XdgActivationV1* xdg_activation_v1, + std::shared_ptr const& shell, + std::shared_ptr const& session_coordinator, + std::shared_ptr const& main_loop) + : XdgActivationV1(resource, Version<1>()), + xdg_activation_v1{xdg_activation_v1}, + shell{shell}, + session_coordinator{session_coordinator}, + main_loop{main_loop} +{ +} + +mf::XdgActivationV1::KeyboardObserver::KeyboardObserver(mir::frontend::XdgActivationV1* xdg_activation_v1) + : xdg_activation_v1{xdg_activation_v1} +{} + +void mf::XdgActivationV1::KeyboardObserver::keyboard_event(std::shared_ptr const& event) +{ + // If we encounter a key press event, then we invalidate pending activation tokens + if (event->type() != mir_event_type_input) + return; + + auto input_event = event->to_input(); + if (input_event->input_type() != mir_input_event_type_key) + return; + + auto keyboard_event = input_event->to_keyboard(); + if (keyboard_event->action() == mir_keyboard_action_down) + xdg_activation_v1->invalidate_all(); +} + +void mf::XdgActivationV1::KeyboardObserver::keyboard_focus_set(std::shared_ptr const&) +{ +} + +void mf::XdgActivationV1::SessionListener::focused(std::shared_ptr const& session) +{ + xdg_activation_v1->invalidate_if_not_from_session(session); +} + +void mf::XdgActivationV1::Instance::get_activation_token(struct wl_resource* id) +{ + new XdgActivationTokenV1(id, xdg_activation_v1->create_token(client->client_session())); +} + +void mf::XdgActivationV1::Instance::activate(std::string const& token, struct wl_resource* surface) +{ + // This function handles requests from clients for activation. + // This will fail if the client cannot find their token in the list. + // A client may not be able to find their token in the list if any + // of the following are true: + // + // 1. A surface other than the surface that originally requested the activation token + // received focus in the meantime, unless it was a layer shell surface that + // initially requested the focus. + // 2. The surface failed to use the token in the alotted period of time + // 3. A key was pressed down between the issuing of the token and the activation + // of the surface with that token + auto xdg_token = xdg_activation_v1->try_consume_token(token); + if (!xdg_token) + { + mir::log_error("XdgActivationV1::activate invalid token: %s", token.c_str()); + return; + } + + main_loop->enqueue(this, [ + surface=surface, + shell=shell, + xdg_token=xdg_token, + client=client, + session_coordinator=session_coordinator] + { + if (xdg_token->session.expired()) + { + mir::log_error("XdgActivationV1::activate requesting session has expired"); + return; + } + + auto const wl_surface = mf::WlSurface::from(surface); + if (!wl_surface) + { + mir::log_error("XdgActivationV1::activate wl_surface not found"); + return; + } + + auto const scene_surface_opt = wl_surface->scene_surface(); + if (!scene_surface_opt) + { + mir::log_error("XdgActivationV1::activate scene_surface_opt not found"); + return; + } + + // In the event of a failure, we send 'done' with a new, invalid token which cannot be used later. + if (xdg_token->seat && xdg_token->serial) + { + // First, assert that the seat still exists. + auto const wl_seat = mf::WlSeat::from(xdg_token->seat.value()); + if (!wl_seat) + mir::log_warning("XdgActivationTokenV1::activate wl_seat not found"); + + auto event = client->event_for(xdg_token->serial.value()); + if (event == std::nullopt) + mir::log_warning("XdgActivationTokenV1::activate serial not found"); + } + + auto const& scene_surface = scene_surface_opt.value(); + auto const now = std::chrono::steady_clock::now().time_since_epoch(); + auto ns = std::chrono::duration_cast(now).count(); + shell->raise_surface(wl_surface->session, scene_surface, ns); + }); +} + +mf::XdgActivationTokenV1::XdgActivationTokenV1( + struct wl_resource* resource, + std::shared_ptr const& token) + : wayland::XdgActivationTokenV1(resource, Version<1>()), + token{token} +{ +} + +void mf::XdgActivationTokenV1::set_serial(uint32_t serial_, struct wl_resource* seat_) +{ + token->serial = serial_; + token->seat = seat_; +} + +void mf::XdgActivationTokenV1::set_app_id(std::string const& app_id) +{ + // TODO: This is the application id of the surface that is coming up. + // Until it presents itself as a problem, we will ignore it for now. + // Most likely this is supposed to be used that the application who + // is using the application token is the application that we intend + // to use that token. + (void)app_id; +} + +void mf::XdgActivationTokenV1::set_surface(struct wl_resource* surface) +{ + // TODO: This is the requesting surface. Until it presents itself + // as a problem, we will ignore it or now. Instead, we only ensure + // that the same same session is focused between token request + // and activation. Or we simply ensure that focus hasn't changed at all. + (void)surface; +} + +void mf::XdgActivationTokenV1::commit() +{ + if (used) + { + BOOST_THROW_EXCEPTION(mw::ProtocolError( + resource, + Error::already_used, + "The activation token has already been used")); + return; + } + + used = true; + send_done_event(token->token); +} diff --git a/src/server/frontend_wayland/xdg_activation_v1.h b/src/server/frontend_wayland/xdg_activation_v1.h new file mode 100644 index 00000000000..388f9d1d193 --- /dev/null +++ b/src/server/frontend_wayland/xdg_activation_v1.h @@ -0,0 +1,55 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 or 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ + +#ifndef MIR_FRONTEND_XDG_ACTIVATION_UNSTABLE_V1_H +#define MIR_FRONTEND_XDG_ACTIVATION_UNSTABLE_V1_H + +#include "xdg-activation-v1_wrapper.h" +#include "mir/observer_registrar.h" + +struct wl_display; + +namespace mir +{ +class MainLoop; +namespace shell +{ +class Shell; +} +namespace scene +{ +class SessionCoordinator; +} +namespace input +{ +class KeyboardObserver; +} + +namespace frontend +{ +auto create_xdg_activation_v1( + struct wl_display* display, + std::shared_ptr const&, + std::shared_ptr const&, + std::shared_ptr const&, + std::shared_ptr> const&, + Executor& wayland_executor) -> + std::shared_ptr; +} +} + + +#endif //MIR_FRONTEND_XDG_ACTIVATION_UNSTABLE_V1_H diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 33e9f4b9118..e4820524fab 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -39,6 +39,7 @@ mir_generate_protocol_wrapper(mirwayland "zmir_" mir-shell-unstable-v1.xml) mir_generate_protocol_wrapper(mirwayland "z" xdg-decoration-unstable-v1.xml) mir_generate_protocol_wrapper(mirwayland "wp_" viewporter.xml) mir_generate_protocol_wrapper(mirwayland "wp_" fractional-scale-v1.xml) +mir_generate_protocol_wrapper(mirwayland "z" xdg-activation-v1.xml) target_link_libraries(mirwayland PUBLIC diff --git a/wayland-protocols/xdg-activation-v1.xml b/wayland-protocols/xdg-activation-v1.xml new file mode 100644 index 00000000000..5c3c4fed299 --- /dev/null +++ b/wayland-protocols/xdg-activation-v1.xml @@ -0,0 +1,200 @@ + + + + + Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org> + Copyright © 2020 Carlos Garnacho <carlosg@gnome.org> + + 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. + + + + The way for a client to pass focus to another toplevel is as follows. + + The client that intends to activate another toplevel uses the + xdg_activation_v1.get_activation_token request to get an activation token. + This token is then forwarded to the client, which is supposed to activate + one of its surfaces, through a separate band of communication. + + One established way of doing this is through the XDG_ACTIVATION_TOKEN + environment variable of a newly launched child process. The child process + should unset the environment variable again right after reading it out in + order to avoid propagating it to other child processes. + + Another established way exists for Applications implementing the D-Bus + interface org.freedesktop.Application, which should get their token under + activation-token on their platform_data. + + In general activation tokens may be transferred across clients through + means not described in this protocol. + + The client to be activated will then pass the token + it received to the xdg_activation_v1.activate request. The compositor can + then use this token to decide how to react to the activation request. + + The token the activating client gets may be ineffective either already at + the time it receives it, for example if it was not focused, for focus + stealing prevention. The activating client will have no way to discover + the validity of the token, and may still forward it to the to be activated + client. + + The created activation token may optionally get information attached to it + that can be used by the compositor to identify the application that we + intend to activate. This can for example be used to display a visual hint + about what application is being started. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + A global interface used for informing the compositor about applications + being activated or started, or for applications to request to be + activated. + + + + + Notify the compositor that the xdg_activation object will no longer be + used. + + The child objects created via this interface are unaffected and should + be destroyed separately. + + + + + + Creates an xdg_activation_token_v1 object that will provide + the initiating client with a unique token for this activation. This + token should be offered to the clients to be activated. + + + + + + + + Requests surface activation. It's up to the compositor to display + this information as desired, for example by placing the surface above + the rest. + + The compositor may know who requested this by checking the activation + token and might decide not to follow through with the activation if it's + considered unwanted. + + Compositors can ignore unknown activation tokens when an invalid + token is passed. + + + + + + + + + An object for setting up a token and receiving a token handle that can + be passed as an activation token to another client. + + The object is created using the xdg_activation_v1.get_activation_token + request. This object should then be populated with the app_id, surface + and serial information and committed. The compositor shall then issue a + done event with the token. In case the request's parameters are invalid, + the compositor will provide an invalid token. + + + + + + + + + Provides information about the seat and serial event that requested the + token. + + The serial can come from an input or focus event. For instance, if a + click triggers the launch of a third-party client, the launcher client + should send a set_serial request with the serial and seat from the + wl_pointer.button event. + + Some compositors might refuse to activate toplevels when the token + doesn't have a valid and recent enough event serial. + + Must be sent before commit. This information is optional. + + + + + + + + The requesting client can specify an app_id to associate the token + being created with it. + + Must be sent before commit. This information is optional. + + + + + + + This request sets the surface requesting the activation. Note, this is + different from the surface that will be activated. + + Some compositors might refuse to activate toplevels when the token + doesn't have a requesting surface. + + Must be sent before commit. This information is optional. + + + + + + + Requests an activation token based on the different parameters that + have been offered through set_serial, set_surface and set_app_id. + + + + + + The 'done' event contains the unique token of this activation request + and notifies that the provider is done. + + + + + + + Notify the compositor that the xdg_activation_token_v1 object will no + longer be used. The received token stays valid. + + + +