Skip to content
Draft
5 changes: 5 additions & 0 deletions metadata/core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
<_long>Whether to pass buttons through to the client when switching the focus via clicking</_long>
<default>true</default>
</option>
<option name="focus_on_map" type="bool">
<_short>Focus newly opened views</_short>
<_long>If unset, newly opened views will only be focused by using the xdg-activation protocol.</_long>
<default>true</default>
</option>
<option name="exit" type="key">
<_short>Wayfire Shutdown</_short>
<_long>Calls the shutdown routines for wayfire.</_long>
Expand Down
5 changes: 5 additions & 0 deletions metadata/xdg-activation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
<_long>Whether to reject activation requests if a newer request has arrived since their creation.</_long>
<default>false</default>
</option>
<option name="focus_stealing_prevention" type="bool">
<_short>Prevent stealing focus by an activation request</_short>
<_long>Whether to reject an activation request if the user interacted with a different view since its creation.</_long>
<default>true</default>
</option>
<option name="timeout" type="int">
<_short>Timeout for activation (in seconds)</_short>
<_long>Focus requests will be ignored if at least this amount of time has elapsed between creating and using it.</_long>
Expand Down
192 changes: 181 additions & 11 deletions plugins/protocols/xdg-activation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <wayfire/nonstd/wlroots-full.hpp>
#include <wayfire/window-manager.hpp>
#include <wayfire/util.hpp>
#include <wayfire/seat.hpp>
#include "config.h"

class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
Expand All @@ -28,14 +29,25 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t

xdg_activation_request_activate.connect(&xdg_activation->events.request_activate);
xdg_activation_new_token.connect(&xdg_activation->events.new_token);
wf::get_core().connect(&on_run_command);
}

void fini() override
{
xdg_activation_request_activate.disconnect();
xdg_activation_new_token.disconnect();
xdg_activation_token_destroy.disconnect();
xdg_activation_token_self_destroy.disconnect();
on_view_mapped.disconnect();
last_token = nullptr;
if (last_view)
{
last_view->disconnect(&on_view_unmapped);
last_view->disconnect(&on_view_deactivated);
last_view = nullptr;
}

wf::get_core().disconnect(&on_run_command);
}

bool is_unloadable() override
Expand All @@ -50,36 +62,67 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
{
auto event = static_cast<const struct wlr_xdg_activation_v1_request_activate_event*>(data);

if (!event->token->seat)
if (event->token != last_self_token)
{
LOGI("Denying focus request, token was rejected at creation");
return;
if (!event->token->seat)
{
LOGI("Denying focus request, token was rejected at creation");
return;
}

if (only_last_token && (event->token != last_token))
{
LOGI("Denying focus request, token is expired");
return;
}
}

if (only_last_token && (event->token != last_token))
last_token = nullptr; // avoid reusing the same token
last_self_token = nullptr;

if (prevent_focus_stealing && !last_view)
{
LOGI("Denying focus request, token is expired");
LOGI("Denying focus request, requesting view has been deactivated");
return;
}

last_token = nullptr; // avoid reusing the same token

bool should_focus = true;
wayfire_view view = wf::wl_surface_to_wayfire_view(event->surface->resource);
if (!view)
{
LOGE("Could not get view");
return;
should_focus = false;
}

auto toplevel = wf::toplevel_cast(view);
if (!toplevel)
{
LOGE("Could not get toplevel view");
return;
should_focus = false;
}

if (should_focus)
{
if (toplevel->toplevel()->current().mapped)
{
LOGD("Activating view");
wf::get_core().default_wm->focus_request(toplevel);
} else
{
/* This toplevel is not mapped yet, we want to focus it
* when it it first mapped. */
on_view_mapped.disconnect();
view->connect(&on_view_mapped);
return; // avoid disconnecting last_view's signals
}
}

LOGD("Activating view");
wf::get_core().default_wm->focus_request(toplevel);
if (last_view)
{
last_view->disconnect(&on_view_unmapped);
last_view->disconnect(&on_view_deactivated);
last_view = nullptr;
}
});

xdg_activation_new_token.set_callback([this] (void *data)
Expand All @@ -100,6 +143,29 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
return;
}

// unset any previously saved view
if (last_view)
{
last_view->disconnect(&on_view_unmapped);
last_view->disconnect(&on_view_deactivated);
last_view = nullptr;
}

wayfire_view view = token->surface ? wf::wl_surface_to_wayfire_view(
token->surface->resource) : nullptr;
if (view)
{
last_view = wf::toplevel_cast(view); // might return nullptr
//!! does not work for:
// (1) layer-shell views
// (2) (some) menus
if (last_view)
{
last_view->connect(&on_view_unmapped);
last_view->connect(&on_view_deactivated);
}
}

// update our token and connect its destroy signal
last_token = token;
xdg_activation_token_destroy.disconnect();
Expand All @@ -113,6 +179,13 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
xdg_activation_token_destroy.disconnect();
});

xdg_activation_token_self_destroy.set_callback([this] (void *data)
{
last_self_token = nullptr;

xdg_activation_token_self_destroy.disconnect();
});

timeout.set_callback(timeout_changed);
}

Expand All @@ -125,14 +198,111 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
}
};

wf::signal::connection_t<wf::view_unmapped_signal> on_view_unmapped = [this] (auto)
{
last_view->disconnect(&on_view_unmapped);
last_view->disconnect(&on_view_deactivated);
// handle the case when last_view was a dialog that is closed by user interaction
last_view = last_view->parent;
if (last_view)
{
last_view->connect(&on_view_unmapped);
last_view->connect(&on_view_deactivated);
}
};

wf::signal::connection_t<wf::view_activated_state_signal> on_view_deactivated = [this] (auto)
{
if (last_view->activated)
{
// could be a spurious event, e.g. activating the parent
// view after closing a dialog
return;
}

last_view->disconnect(&on_view_unmapped);
last_view->disconnect(&on_view_deactivated);
last_view = nullptr;
};

wf::signal::connection_t<wf::view_mapped_signal> on_view_mapped = [this] (auto signal)
{
signal->view->disconnect(&on_view_mapped);

// re-check focus stealing prevention
if (last_view)
{
last_view->disconnect(&on_view_unmapped);
last_view->disconnect(&on_view_deactivated);
last_view = nullptr;
} else if (prevent_focus_stealing)
{
LOGI("Denying focus request, requesting view has been deactivated");
return;
}

LOGD("Activating view");
wf::get_core().default_wm->focus_request(signal->view);
};

wf::signal::connection_t<wf::command_run_signal> on_run_command = [this] (auto signal)
{
if (wf::get_core().default_wm->focus_on_map)
{
// no need to do anything if views are focused anyway
return;
}

if (last_self_token)
{
// TODO: invalidate our last token !
last_self_token = nullptr;
}

auto active_view = wf::get_core().seat->get_active_view();
if (active_view && (active_view->role == wf::VIEW_ROLE_DESKTOP_ENVIRONMENT))
{
active_view = nullptr;
}

auto active_toplevel = active_view ? wf::toplevel_cast(active_view) : nullptr;

if (!active_toplevel)
{
// if there is no active view, we don't need a token
return;
}

if (last_view)
{
// TODO: we need a separate last_view actually !
last_view->disconnect(&on_view_unmapped);
last_view->disconnect(&on_view_deactivated);
}

last_view = active_toplevel;
last_view->connect(&on_view_unmapped);
last_view->connect(&on_view_deactivated);
last_self_token = wlr_xdg_activation_token_v1_create(xdg_activation);
xdg_activation_token_self_destroy.disconnect();
xdg_activation_token_self_destroy.connect(&last_self_token->events.destroy);
const char *token_id = wlr_xdg_activation_token_v1_get_name(last_self_token);
signal->env.emplace_back("XDG_ACTIVATION_TOKEN", token_id);
signal->env.emplace_back("DESKTOP_STARTUP_ID", token_id);
};

struct wlr_xdg_activation_v1 *xdg_activation;
wf::wl_listener_wrapper xdg_activation_request_activate;
wf::wl_listener_wrapper xdg_activation_new_token;
wf::wl_listener_wrapper xdg_activation_token_destroy;
wf::wl_listener_wrapper xdg_activation_token_self_destroy;
struct wlr_xdg_activation_token_v1 *last_token = nullptr;
struct wlr_xdg_activation_token_v1 *last_self_token = nullptr;
wayfire_toplevel_view last_view = nullptr; // view that created the token

wf::option_wrapper_t<bool> check_surface{"xdg-activation/check_surface"};
wf::option_wrapper_t<bool> only_last_token{"xdg-activation/only_last_request"};
wf::option_wrapper_t<bool> prevent_focus_stealing{"xdg-activation/focus_stealing_prevention"};
wf::option_wrapper_t<int> timeout{"xdg-activation/timeout"};
};

Expand Down
15 changes: 15 additions & 0 deletions src/api/wayfire/signal-definitions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,21 @@ struct idle_inhibit_changed_signal
bool inhibit;
};

/**
* on: core
* when: before running a command in compositor_core_t::run().
* Plugins can listen to this signal and add environment variables to
* set in env.
*/
struct command_run_signal
{
const std::string& command;
std::vector<std::pair<std::string, std::string>> env;

command_run_signal(const std::string& cmd) : command(cmd)
{}
};

/**
* on: output, core(output-)
* when: Immediately after the output becomes focused.
Expand Down
11 changes: 11 additions & 0 deletions src/api/wayfire/toplevel-view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,17 @@ class toplevel_view_interface_t : public virtual wf::view_interface_t
* This function is useful for view implementations only.
*/
void set_toplevel(std::shared_ptr<wf::toplevel_t> toplevel);

/**
* Potentially focus this view on first map. Newly mapped views are
* focused if either:
* - the "core/focus_on_map" option is set
* - no toplevel view is currently focused (i.e. no view is focused
* or the currently focused view has role VIEW_ROLE_DESKTOP_ENVIRONMENT)
* - the newly mapped view is a dialog (i.e. has a non-null parent)
* and its parent is focused
*/
void focus_toplevel_on_map();
};

inline wayfire_toplevel_view toplevel_cast(wayfire_view view)
Expand Down
5 changes: 5 additions & 0 deletions src/api/wayfire/window-manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class window_manager_t
public:
virtual ~window_manager_t() = default;

/**
* Option that sets whether newly mapped views get focus by default.
*/
wf::option_wrapper_t<bool> focus_on_map{"core/focus_on_map"};

/**
* Update the remembered last windowed geometry.
*
Expand Down
8 changes: 8 additions & 0 deletions src/core/core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,9 @@ pid_t wf::compositor_core_impl_t::run(std::string command)
return 0;
}

wf::command_run_signal signal(command);
wf::get_core().emit(&signal);

/* The following is a "hack" for disowning the child processes,
* otherwise they will simply stay as zombie processes */
pid_t pid = fork();
Expand All @@ -520,6 +523,11 @@ pid_t wf::compositor_core_impl_t::run(std::string command)
}

#endif
for (const auto& var : signal.env)
{
setenv(var.first.c_str(), var.second.c_str(), 1);
}

if (discard_command_output)
{
int dev_null = open("/dev/null", O_WRONLY);
Expand Down
Loading
Loading