diff --git a/metadata/core.xml b/metadata/core.xml
index 6bb03af7d..7a8e13e82 100644
--- a/metadata/core.xml
+++ b/metadata/core.xml
@@ -93,6 +93,11 @@
<_long>Whether to pass buttons through to the client when switching the focus via clicking
true
+
+ <_short>Focus newly opened views
+ <_long>If unset, newly opened views will only be focused by using the xdg-activation protocol.
+ true
+
<_short>Wayfire Shutdown
<_long>Calls the shutdown routines for wayfire.
diff --git a/metadata/xdg-activation.xml b/metadata/xdg-activation.xml
index 6eb8c5514..71fe62a12 100644
--- a/metadata/xdg-activation.xml
+++ b/metadata/xdg-activation.xml
@@ -14,6 +14,11 @@
<_long>Whether to reject activation requests if a newer request has arrived since their creation.
false
+
+ <_short>Prevent stealing focus by an activation request
+ <_long>Whether to reject an activation request if the user interacted with a different view since its creation.
+ true
+
<_short>Timeout for activation (in seconds)
<_long>Focus requests will be ignored if at least this amount of time has elapsed between creating and using it.
diff --git a/plugins/protocols/xdg-activation.cpp b/plugins/protocols/xdg-activation.cpp
index d87beee53..e8bbbf785 100644
--- a/plugins/protocols/xdg-activation.cpp
+++ b/plugins/protocols/xdg-activation.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include "config.h"
class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
@@ -28,6 +29,7 @@ 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
@@ -35,7 +37,17 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
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
@@ -50,36 +62,67 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
{
auto event = static_cast(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 || last_view != wf::get_core().seat->get_active_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;
}
- LOGD("Activating view");
- wf::get_core().default_wm->focus_request(toplevel);
+ 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
+ }
+ }
+
+ 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)
@@ -100,6 +143,24 @@ 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 = view;
+ last_view->connect(&on_view_unmapped);
+ LOGI("New token for view: ", view.get());
+ // last_view->connect(&on_view_deactivated);
+ }
+
// update our token and connect its destroy signal
last_token = token;
xdg_activation_token_destroy.disconnect();
@@ -113,6 +174,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);
}
@@ -125,14 +193,145 @@ class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t
}
};
+ wf::signal::connection_t on_view_unmapped = [this] (auto)
+ {
+ last_view->disconnect(&on_view_unmapped);
+ // last_view->disconnect(&on_view_deactivated);
+ LOGI("View closed: ", last_view.get());
+
+ // handle the case when last_view was a dialog that is closed by user interaction
+ auto toplevel = wf::toplevel_cast(last_view);
+ if (toplevel)
+ {
+ last_view = toplevel->parent;
+ LOGI("Setting parent: ", last_view.get());
+ } else
+ {
+ //!! does not work, it is already NULL when this is called
+ auto surface = last_view->get_wlr_surface();
+ auto popup = surface ? wlr_xdg_popup_try_from_wlr_surface (surface) : nullptr;
+ if (popup && popup->parent)
+ {
+ last_view = wf::wl_surface_to_wayfire_view (popup->parent->resource);
+ LOGI("Setting parent (from xdg-popup): ", last_view.get());
+ } else
+ {
+ //!! TODO: XWayland popups?
+ last_view = nullptr;
+ LOGI("No parent");
+ }
+ }
+
+ if (last_view)
+ {
+ last_view->connect(&on_view_unmapped);
+ // last_view->connect(&on_view_deactivated);
+ }
+ };
+
+/*
+ wf::signal::connection_t 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 on_view_mapped = [this] (auto signal)
+ {
+ signal->view->disconnect(&on_view_mapped);
+
+ bool should_focus = true;
+
+ // re-check focus stealing prevention
+ if (prevent_focus_stealing)
+ {
+ if (!last_view || last_view != wf::get_core().seat->get_active_view())
+ {
+ LOGI("Denying focus request, requesting view has been deactivated");
+ should_focus = false;
+ }
+ }
+ if (last_view)
+ {
+ last_view->disconnect(&on_view_unmapped);
+ // last_view->disconnect(&on_view_deactivated);
+ last_view = nullptr;
+ }
+
+ if (should_focus)
+ {
+ LOGD("Activating view");
+ wf::get_core().default_wm->focus_request(signal->view);
+ }
+ };
+
+ wf::signal::connection_t 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_view)
+ {
+ // 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_view;
+ 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_view last_view = nullptr; // view that created the token
wf::option_wrapper_t check_surface{"xdg-activation/check_surface"};
wf::option_wrapper_t only_last_token{"xdg-activation/only_last_request"};
+ wf::option_wrapper_t prevent_focus_stealing{"xdg-activation/focus_stealing_prevention"};
wf::option_wrapper_t timeout{"xdg-activation/timeout"};
};
diff --git a/src/api/wayfire/signal-definitions.hpp b/src/api/wayfire/signal-definitions.hpp
index 800068171..18653ce90 100644
--- a/src/api/wayfire/signal-definitions.hpp
+++ b/src/api/wayfire/signal-definitions.hpp
@@ -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> env;
+
+ command_run_signal(const std::string& cmd) : command(cmd)
+ {}
+};
+
/**
* on: output, core(output-)
* when: Immediately after the output becomes focused.
diff --git a/src/api/wayfire/toplevel-view.hpp b/src/api/wayfire/toplevel-view.hpp
index d40688e27..49ed29ca2 100644
--- a/src/api/wayfire/toplevel-view.hpp
+++ b/src/api/wayfire/toplevel-view.hpp
@@ -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 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)
diff --git a/src/api/wayfire/window-manager.hpp b/src/api/wayfire/window-manager.hpp
index 0a0f6df11..ed42ec3df 100644
--- a/src/api/wayfire/window-manager.hpp
+++ b/src/api/wayfire/window-manager.hpp
@@ -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 focus_on_map{"core/focus_on_map"};
+
/**
* Update the remembered last windowed geometry.
*
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 10aae38cb..aeb6961f2 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -524,6 +524,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();
@@ -545,6 +548,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);
diff --git a/src/view/toplevel-view.cpp b/src/view/toplevel-view.cpp
index 5f91f5507..3fd56f325 100644
--- a/src/view/toplevel-view.cpp
+++ b/src/view/toplevel-view.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include "view-impl.hpp"
#include "wayfire/core.hpp"
#include "wayfire/scene.hpp"
@@ -329,6 +330,35 @@ void wf::toplevel_view_interface_t::set_toplevel(
priv->toplevel = toplevel;
}
+void wf::toplevel_view_interface_t::focus_toplevel_on_map()
+{
+ /* We only focus a newly mapped view if the corresponding option is
+ * set or if there is no currently active view. */
+ wayfire_view active_view = nullptr;
+ bool should_focus = wf::get_core().default_wm->focus_on_map;
+ if (!should_focus)
+ {
+ active_view = wf::get_core().seat->get_active_view();
+ if (active_view && (active_view->role == wf::VIEW_ROLE_DESKTOP_ENVIRONMENT))
+ {
+ active_view = nullptr;
+ }
+
+ if (!active_view || (active_view == this->parent))
+ {
+ should_focus = true;
+ }
+ }
+
+ if (should_focus)
+ {
+ wf::get_core().default_wm->focus_request(self());
+ } else if (active_view)
+ {
+ wf::view_bring_to_front(active_view);
+ }
+}
+
wayfire_toplevel_view wf::find_view_for_toplevel(
std::shared_ptr toplevel)
{
diff --git a/src/view/xdg-shell/xdg-toplevel-view.cpp b/src/view/xdg-shell/xdg-toplevel-view.cpp
index c042ac7fd..7dec8efc1 100644
--- a/src/view/xdg-shell/xdg-toplevel-view.cpp
+++ b/src/view/xdg-shell/xdg-toplevel-view.cpp
@@ -310,7 +310,8 @@ void wf::xdg_toplevel_view_t::map()
adjust_view_output_on_map(this);
xdg_toplevel_view_base_t::map();
- wf::get_core().default_wm->focus_request(self());
+
+ focus_toplevel_on_map();
/* Might trigger repositioning */
set_toplevel_parent(this->parent);
diff --git a/src/view/xwayland/xwayland-toplevel-view.hpp b/src/view/xwayland/xwayland-toplevel-view.hpp
index 2f7252c58..70c6d8060 100644
--- a/src/view/xwayland/xwayland-toplevel-view.hpp
+++ b/src/view/xwayland/xwayland-toplevel-view.hpp
@@ -444,7 +444,7 @@ class wayfire_xwayland_view : public wf::toplevel_view_interface_t, public wayfi
if (wants_focus)
{
- wf::get_core().default_wm->focus_request(self());
+ focus_toplevel_on_map();
}
/* Might trigger repositioning */