diff --git a/.clang-format b/.clang-format
new file mode 100644
index 00000000..fd2e8008
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,5 @@
+UseTab: Always
+IndentWidth: 4
+TabWidth: 4
+ColumnLimit: 80
+AllowShortFunctionsOnASingleLine: None
diff --git a/.github/workflows/ubuntu-build.yml b/.github/workflows/ubuntu-build.yml
index 4f77aded..4630900e 100644
--- a/.github/workflows/ubuntu-build.yml
+++ b/.github/workflows/ubuntu-build.yml
@@ -15,7 +15,7 @@ on:
jobs:
ubuntu-build:
- container: ubuntu:23.10
+ container: ubuntu:24.10
runs-on: ubuntu-latest
env:
DEBIAN_FRONTEND: noninteractive
diff --git a/.gitignore b/.gitignore
index da59f957..92729738 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@ SwayNotificationCenter
./swaync*
./src/swaync
./src/swaync*
+.cache/
swaync-git*
diff --git a/meson.build b/meson.build
index 5bb7643f..546a1d63 100644
--- a/meson.build
+++ b/meson.build
@@ -8,13 +8,19 @@ add_project_arguments(['-Wno-error=int-conversion'], language: 'c')
add_project_arguments(['--enable-gobject-tracing'], language: 'vala')
add_project_arguments(['--enable-checking'], language: 'vala')
-i18n = import('i18n')
+cc = meson.get_compiler('c')
+vala = meson.get_compiler('vala')
+
gnome = import('gnome')
app_resources = []
config_path = join_paths(get_option('sysconfdir'), 'xdg', 'swaync')
+# Wayland protocols
+protocol_dep = []
+subdir('protocols')
+
subdir('data')
subdir('src')
diff --git a/protocols/meson.build b/protocols/meson.build
new file mode 100644
index 00000000..c2c570f4
--- /dev/null
+++ b/protocols/meson.build
@@ -0,0 +1,34 @@
+dep_scanner = dependency('wayland-scanner', native: true)
+prog_scanner = find_program(dep_scanner.get_variable(pkgconfig: 'wayland_scanner'))
+
+protocol_file = files(
+ 'xdg-activation-v1.xml',
+)
+
+protocol_sources = []
+protocol_sources += custom_target(
+ 'xdg-activation-v1-client-protocol.h',
+ command: [prog_scanner, 'client-header', '@INPUT@', '@OUTPUT@'],
+ input: protocol_file,
+ output: 'xdg-activation-v1-client-protocol.h',
+)
+
+output_type = 'private-code'
+if dep_scanner.version().version_compare('< 1.14.91')
+ output_type = 'code'
+endif
+protocol_sources += custom_target(
+ 'xdg-activation-protocol.c',
+ command: [prog_scanner, output_type, '@INPUT@', '@OUTPUT@'],
+ input: protocol_file,
+ output: 'xdg-activation-protocol.c',
+)
+
+protocol_dep += declare_dependency(
+ dependencies: [
+ vala.find_library('xdg-activation-v1', dirs: meson.current_source_dir()),
+ dependency('wayland-client'),
+ ],
+ include_directories: include_directories('.'),
+ sources: protocol_sources,
+)
diff --git a/protocols/xdg-activation-v1.deps b/protocols/xdg-activation-v1.deps
new file mode 100644
index 00000000..8289bf8e
--- /dev/null
+++ b/protocols/xdg-activation-v1.deps
@@ -0,0 +1 @@
+wayland-client
diff --git a/protocols/xdg-activation-v1.vapi b/protocols/xdg-activation-v1.vapi
new file mode 100644
index 00000000..d31e36ed
--- /dev/null
+++ b/protocols/xdg-activation-v1.vapi
@@ -0,0 +1,48 @@
+// vim: ft=vala
+
+namespace XDG.Activation {
+ [CCode (cheader_filename = "xdg-activation-v1-client-protocol.h", cname = "struct xdg_activation_v1", cprefix = "xdg_activation_v1_")]
+ public class Activation : Wl.Proxy {
+ [CCode (cheader_filename = "xdg-activation-v1-client-protocol.h", cname = "xdg_activation_v1_interface")]
+ public static Wl.Interface iface;
+ public void set_user_data (void * user_data);
+ public void * get_user_data ();
+ public uint32 get_version ();
+
+ public void destroy ();
+
+ public Token * get_activation_token ();
+ public void activate (string token, Wl.Surface surface);
+ }
+
+ [CCode (cheader_filename = "xdg-activation-v1-client-protocol.h", cname = "enum xdg_activation_token_v1_error", cprefix = "XDG_ACTIVATION_TOKEN_V1_ERROR_", has_type_id = false)]
+ public enum error {
+ ALREADY_USED = 0,
+ }
+
+ [CCode (cheader_filename = "xdg-activation-v1-client-protocol.h", cname = "struct xdg_activation_token_v1", cprefix = "xdg_activation_token_v1_")]
+ public class Token : Wl.Proxy {
+ [CCode (cheader_filename = "xdg-activation-v1-client-protocol.h", cname = "xdg_activation_token_v1_listener")]
+ public static Wl.Interface iface;
+ public void set_user_data (void * user_data);
+ public void * get_user_data ();
+ public uint32 get_version ();
+ public void destroy ();
+
+ public int add_listener (TokenListener listener, void * data);
+
+ public void set_serial (uint32 serial, Wl.Seat seat);
+ public void set_app_id (string app_id);
+ public void set_surface (Wl.Surface surface);
+ public void commit ();
+ }
+
+ [CCode (cname = "struct xdg_activation_token_v1_listener", has_type_id = false)]
+ public struct TokenListener {
+ public TokenListenerDone done;
+ }
+
+ [CCode (has_target = false, has_typedef = false)]
+ public delegate void TokenListenerDone (void * data, Token activation_token, string token);
+}
+
diff --git a/protocols/xdg-activation-v1.xml b/protocols/xdg-activation-v1.xml
new file mode 100644
index 00000000..9adcc274
--- /dev/null
+++ b/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.
+
+
+
+
diff --git a/src/WaylandUtils/WaylandUtils.c b/src/WaylandUtils/WaylandUtils.c
new file mode 100644
index 00000000..21ef67e4
--- /dev/null
+++ b/src/WaylandUtils/WaylandUtils.c
@@ -0,0 +1,27 @@
+#include
+#include
+#include
+#include
+#include
+
+/** GDK doesn't provide a vapi file for GDK Wayland... */
+
+#define PRINT_ERROR \
+ g_error("Gdk Display isn't a Wayland display! Only Wayland is supported")
+
+struct wl_display *get_wl_display() {
+ GdkDisplay *display = gdk_display_get_default();
+ if (GDK_IS_WAYLAND_DISPLAY(display)) {
+ return gdk_wayland_display_get_wl_display(display);
+ }
+ PRINT_ERROR;
+ return NULL;
+}
+
+struct wl_surface *get_wl_surface(GdkWindow *window) {
+ if (GDK_IS_WAYLAND_WINDOW(window)) {
+ return gdk_wayland_window_get_wl_surface(window);
+ }
+ PRINT_ERROR;
+ return NULL;
+}
diff --git a/src/WaylandUtils/WaylandUtils.vala b/src/WaylandUtils/WaylandUtils.vala
new file mode 100644
index 00000000..6a43a038
--- /dev/null
+++ b/src/WaylandUtils/WaylandUtils.vala
@@ -0,0 +1,2 @@
+extern Wl.Display * get_wl_display ();
+extern Wl.Surface * get_wl_surface (Gdk.Window window);
diff --git a/src/meson.build b/src/meson.build
index 2c26e4c0..805afafc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -64,10 +64,13 @@ app_sources = [
widget_sources,
'blankWindow/blankWindow.vala',
'functions.vala',
+ 'WaylandUtils/WaylandUtils.c',
+ 'WaylandUtils/WaylandUtils.vala',
+ 'xdg_activation.vala',
constants,
]
-assert(meson.get_compiler('vala').version() >= '0.56')
+assert(vala.version() >= '0.56')
app_deps = [
dependency('gio-2.0', version: '>= 2.50'),
@@ -80,9 +83,11 @@ app_deps = [
fallback: ['gtk-layer-shell-0', 'gtk-layer-shell'],
version: '>= 0.8.0'
),
- meson.get_compiler('c').find_library('m', required : true),
- meson.get_compiler('vala').find_library('posix'),
+ cc.find_library('m', required : true),
+ vala.find_library('posix'),
dependency('gee-0.8'),
+ dependency('wayland-client'),
+ protocol_dep,
]
# Checks if the user wants scripting enabled
diff --git a/src/notiDaemon/notiDaemon.vala b/src/notiDaemon/notiDaemon.vala
index 6bac7f45..99618e8a 100644
--- a/src/notiDaemon/notiDaemon.vala
+++ b/src/notiDaemon/notiDaemon.vala
@@ -327,7 +327,7 @@ namespace SwayNotificationCenter {
name = "SwayNotificationCenter";
vendor = "ErikReider";
version = Constants.VERSIONNUM;
- spec_version = "1.2";
+ spec_version = "1.3";
}
/**
@@ -358,6 +358,12 @@ namespace SwayNotificationCenter {
*/
public signal void ActionInvoked (uint32 id, string action_key);
+ /**
+ * This signal can be emitted before a ActionInvoked signal. It
+ * carries an activation token that can be used to activate a toplevel.
+ */
+ public signal void ActivationToken (uint32 id, string activation_token);
+
/** To be used by the non-standard "inline-reply" capability */
public signal void NotificationReplied (uint32 id, string text);
}
diff --git a/src/notification/notification.vala b/src/notification/notification.vala
index bb3bffce..0ae52bbe 100644
--- a/src/notification/notification.vala
+++ b/src/notification/notification.vala
@@ -454,7 +454,7 @@ namespace SwayNotificationCenter {
}
public void click_default_action () {
- action_clicked (param.default_action, true);
+ action_clicked (param.default_action);
}
public void click_alt_action (uint index) {
@@ -472,11 +472,18 @@ namespace SwayNotificationCenter {
action_clicked (param.actions.index (index));
}
- private void action_clicked (Action ? action, bool is_default = false) {
+ private void action_clicked (Action ? action) {
noti_daemon.run_scripts (param, ScriptRunOnType.ACTION);
if (action != null
&& action.identifier != null
&& action.identifier != "") {
+ // Try getting a XDG Activation token so that the application
+ // can request compositor focus
+ string ? token = swaync_daemon.xdg_activation.get_token (this);
+ if (token != null) {
+ noti_daemon.ActivationToken (param.applied_id, token);
+ }
+
noti_daemon.ActionInvoked (param.applied_id, action.identifier);
if (ConfigModel.instance.hide_on_action) {
try {
diff --git a/src/swayncDaemon/swayncDaemon.vala b/src/swayncDaemon/swayncDaemon.vala
index 70808758..fc808707 100644
--- a/src/swayncDaemon/swayncDaemon.vala
+++ b/src/swayncDaemon/swayncDaemon.vala
@@ -9,6 +9,7 @@ namespace SwayNotificationCenter {
[DBus (name = "org.erikreider.swaync.cc")]
public class SwayncDaemon : Object {
public NotiDaemon noti_daemon;
+ public XdgActivationHelper xdg_activation;
private GenericSet inhibitors = new GenericSet (str_hash, str_equal);
public bool inhibited { get; set; default = false; }
@@ -29,6 +30,7 @@ namespace SwayNotificationCenter {
// Init noti_daemon
this.use_layer_shell = ConfigModel.instance.layer_shell;
this.noti_daemon = new NotiDaemon (this);
+ this.xdg_activation = new XdgActivationHelper ();
Bus.own_name (BusType.SESSION, "org.freedesktop.Notifications",
BusNameOwnerFlags.NONE,
on_noti_bus_aquired,
diff --git a/src/xdg_activation.vala b/src/xdg_activation.vala
new file mode 100644
index 00000000..72b2fd66
--- /dev/null
+++ b/src/xdg_activation.vala
@@ -0,0 +1,77 @@
+using XDG.Activation;
+using SwayNotificationCenter;
+
+public class XdgActivationHelper : Object {
+ private static Wl.RegistryListener registry_listener = Wl.RegistryListener () {
+ global = registry_handle_global,
+ };
+ private Activation * xdg_activation = null;
+
+ public XdgActivationHelper () {
+ unowned Wl.Display wl_display = get_wl_display ();
+ var wl_registry = wl_display.get_registry ();
+ wl_registry.add_listener (registry_listener, this);
+
+ if (wl_display.roundtrip () < 0) {
+ return;
+ }
+ }
+
+ ~XdgActivationHelper () {
+ if (xdg_activation != null) {
+ xdg_activation->destroy ();
+ }
+ }
+
+ private void registry_handle_global (Wl.Registry wl_registry, uint32 name,
+ string @interface, uint32 version) {
+ if (@interface == "xdg_activation_v1") {
+ xdg_activation = wl_registry.bind (name, ref Activation.iface, version);
+ if (xdg_activation == null) {
+ GLib.warning ("Could not bind to xdg_activation_v1 iface!");
+ }
+ }
+ }
+
+ private static void handle_done (void * data, Token activation_token,
+ string token) {
+ Value * value = (Value *) data;
+ value->set_string (token.dup ());
+ }
+
+ private const TokenListener TOKEN_LISTENER = {
+ handle_done,
+ };
+
+ public string ? get_token (Gtk.Widget widget) {
+ if (xdg_activation == null) {
+ return null;
+ }
+
+ unowned Wl.Display wl_display = get_wl_display ();
+ unowned Gdk.Window ? window = widget.get_window ();
+ if (window == null) {
+ warning ("GDK Window is null");
+ return null;
+ }
+ unowned Wl.Surface wl_surface = get_wl_surface (window);
+
+ Value token_value = Value (typeof (string));
+ token_value.set_string (null);
+
+ Token * token = xdg_activation->get_activation_token ();
+ token->add_listener (TOKEN_LISTENER, &token_value);
+ token->set_surface (wl_surface);
+ token->commit ();
+ while (wl_display.dispatch () >= 0 && token_value.get_string () == null) {
+ // noop
+ }
+ token->destroy ();
+
+ unowned string token_str = token_value.get_string ();
+ if (token_str != null && token_str.length > 0) {
+ return token_str.dup ();
+ }
+ return null;
+ }
+}