From a127d09a226f176bcffacf6d19bd93adf2092d1f Mon Sep 17 00:00:00 2001 From: Cassidy James Blaede Date: Tue, 14 Jan 2020 14:59:01 -0700 Subject: [PATCH] Optionally support native notification windows (#552) --- data/gala.gschema.xml | 5 ++ plugins/notify/Main.vala | 59 +++++++++++--- src/NotificationStack.vala | 157 +++++++++++++++++++++++++++++++++++++ src/Settings.vala | 1 + src/WindowManager.vala | 36 +++++++-- src/meson.build | 1 + 6 files changed, 240 insertions(+), 19 deletions(-) create mode 100644 src/NotificationStack.vala diff --git a/data/gala.gschema.xml b/data/gala.gschema.xml index 44446675..80325833 100644 --- a/data/gala.gschema.xml +++ b/data/gala.gschema.xml @@ -57,6 +57,11 @@ Enable edge tiling when dropping windows on screen edges If enabled, dropping windows on vertical screen edges maximizes them vertically and resizes them horizontally to cover half of the available area. Dropping windows on the top screen edge maximizes them completely. + + false + If new notifications should be used + If new notifications UI should be used, requires io.elemenetary.notifications to be present and running + '' Panel main menu action diff --git a/plugins/notify/Main.vala b/plugins/notify/Main.vala index bafaa25d..d10b666b 100644 --- a/plugins/notify/Main.vala +++ b/plugins/notify/Main.vala @@ -22,13 +22,18 @@ namespace Gala.Plugins.Notify { public class Main : Gala.Plugin { + private GLib.Settings behavior_settings; Gala.WindowManager? wm = null; NotifyServer server; NotificationStack stack; + uint owner_id = 0U; + public override void initialize (Gala.WindowManager wm) { + behavior_settings = new GLib.Settings ("org.pantheon.desktop.gala.behavior"); + this.wm = wm; #if HAS_MUTTER330 unowned Meta.Display display = wm.get_display (); @@ -41,26 +46,40 @@ namespace Gala.Plugins.Notify #else stack = new NotificationStack (screen); #endif - wm.ui_group.add_child (stack); - track_actor (stack); - stack.animations_changed.connect ((running) => { freeze_track = running; }); - server = new NotifyServer (stack); - - update_position (); -#if HAS_MUTTER330 - unowned Meta.MonitorManager monitor_manager = Meta.MonitorManager.@get (); - monitor_manager.monitors_changed.connect (update_position); - display.workareas_changed.connect (update_position); -#else screen.monitors_changed.connect (update_position); screen.workareas_changed.connect (update_position); -#endif - Bus.own_name (BusType.SESSION, "org.freedesktop.Notifications", BusNameOwnerFlags.NONE, + server = new NotifyServer (stack); + + if (!behavior_settings.get_boolean ("use-new-notifications")) { + enable (); + } + + behavior_settings.changed["use-new-notifications"].connect (() => { + if (!behavior_settings.get_boolean ("use-new-notifications")) { + enable (); + } else { + disable (); + } + }); + } + + void enable () + { + if (owner_id != 0U) { + return; + } + + wm.ui_group.add_child (stack); + track_actor (stack); + + update_position (); + + owner_id = Bus.own_name (BusType.SESSION, "org.freedesktop.Notifications", BusNameOwnerFlags.REPLACE, (connection) => { try { connection.register_object ("/org/freedesktop/Notifications", server); @@ -76,6 +95,20 @@ namespace Gala.Plugins.Notify }); } + void disable () + { + if (owner_id == 0U) { + return; + } + + Bus.unown_name (owner_id); + + untrack_actor (stack); + wm.ui_group.remove_child (stack); + + owner_id = 0U; + } + void update_position () { #if HAS_MUTTER330 diff --git a/src/NotificationStack.vala b/src/NotificationStack.vala new file mode 100644 index 00000000..0daba71b --- /dev/null +++ b/src/NotificationStack.vala @@ -0,0 +1,157 @@ +/* + * Copyright 2020 elementary, Inc (https://elementary.io) + * 2014 Tom Beckmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +public class Gala.NotificationStack : Object { + // we need to keep a small offset to the top, because we clip the container to + // its allocations and the close button would be off for the first notification + private const int TOP_OFFSET = 2; + private const int ADDITIONAL_MARGIN = 12; + private const int MARGIN = 12; + + private const int WIDTH = 300; + + private int stack_x; + private int stack_y; + private int stack_width; + + public Meta.Screen screen { get; construct; } + + private Gee.ArrayList notifications; + + public NotificationStack (Meta.Screen screen) { + Object (screen: screen); + } + + construct { + notifications = new Gee.ArrayList (); + + screen.monitors_changed.connect (update_stack_allocation); + screen.workareas_changed.connect (update_stack_allocation); + update_stack_allocation (); + } + + public void show_notification (Meta.WindowActor notification) { + notification.set_pivot_point (0.5f, 0.5f); + + unowned Meta.Window window = notification.get_meta_window (); + window.stick (); + + var scale = Utils.get_ui_scaling_factor (); + + var entry = new Clutter.TransitionGroup (); + entry.remove_on_complete = true; + entry.duration = 400; + + var opacity_transition = new Clutter.PropertyTransition ("opacity"); + opacity_transition.set_from_value (0); + opacity_transition.set_to_value (255); + + var flip_transition = new Clutter.KeyframeTransition ("rotation-angle-x"); + flip_transition.set_from_value (90.0); + flip_transition.set_to_value (0.0); + flip_transition.set_key_frames ({ 0.6 }); + flip_transition.set_values ({ -10.0 }); + + entry.add_transition (opacity_transition); + entry.add_transition (flip_transition); + notification.add_transition ("entry", entry); + + /** + * We will make space for the incomming notification + * by shifting all current notifications by height + * and then add it to the notifications list. + */ + update_positions (notification.height); + + move_window (notification, stack_x, stack_y + TOP_OFFSET + ADDITIONAL_MARGIN * scale); + notifications.insert (0, notification); + } + + private void update_stack_allocation () { + var primary = screen.get_primary_monitor (); + var area = screen.get_active_workspace ().get_work_area_for_monitor (primary); + + var scale = Utils.get_ui_scaling_factor (); + stack_width = (WIDTH + MARGIN) * scale; + + stack_x = area.x + area.width - stack_width; + stack_y = area.y; + } + + private void update_positions (float add_y = 0.0f) { + var scale = Utils.get_ui_scaling_factor (); + var y = stack_y + TOP_OFFSET + add_y + ADDITIONAL_MARGIN * scale; + var i = notifications.size; + var delay_step = i > 0 ? 150 / i : 0; + foreach (var actor in notifications) { + actor.save_easing_state (); + actor.set_easing_mode (Clutter.AnimationMode.EASE_OUT_BACK); + actor.set_easing_duration (200); + actor.set_easing_delay ((i--) * delay_step); + + move_window (actor, -1, (int)y); + actor.restore_easing_state (); + + y += actor.height; + } + } + + public void destroy_notification (Meta.WindowActor notification) { + notification.save_easing_state (); + notification.set_easing_duration (100); + notification.set_easing_mode (Clutter.AnimationMode.EASE_IN_QUAD); + notification.opacity = 0; + + notification.x += stack_width; + notification.restore_easing_state (); + + notifications.remove (notification); + update_positions (); + } + + /** + * This function takes care of properly updating both the actor + * position and the actual window position. + * + * To enable animations for a window we first need to move it's frame + * in the compositor and then calculate & apply the coordinates for the window + * actor. + */ + private static void move_window (Meta.WindowActor actor, int x, int y) { + if (actor.is_destroyed ()) { + return; + } + + unowned Meta.Window window = actor.get_meta_window (); + if (window == null) { + return; + } + + var rect = window.get_frame_rect (); + + window.move_frame (false, x != -1 ? x : rect.x, y != -1 ? y : rect.y); + + /** + * move_frame does not guarantee that the frame rectangle + * will be updated instantly, get the buffer rectangle. + */ + rect = window.get_buffer_rect (); + actor.x = rect.x - ((actor.width - rect.width) / 2); + actor.y = rect.y - ((actor.height - rect.height) / 2); + } +} diff --git a/src/Settings.vala b/src/Settings.vala index c47b9083..0f7f8a2d 100644 --- a/src/Settings.vala +++ b/src/Settings.vala @@ -19,6 +19,7 @@ namespace Gala { public class BehaviorSettings : Granite.Services.Settings { public bool dynamic_workspaces { get; set; } public bool edge_tiling { get; set; } + public bool use_new_notifications { get; set; } public string panel_main_menu_action { get; set; } public string toggle_recording_action { get; set; } public string overlay_action { get; set; } diff --git a/src/WindowManager.vala b/src/WindowManager.vala index b2bd9c6e..cdba9f86 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -70,6 +70,8 @@ namespace Gala { private Meta.Window? moving; //place for the window that is being moved over Daemon? daemon_proxy = null; + + NotificationStack notification_stack; Gee.LinkedList modal_stack = new Gee.LinkedList (); @@ -159,6 +161,8 @@ namespace Gala { #endif KeyboardManager.init (display); + notification_stack = new NotificationStack (screen); + // Due to a bug which enables access to the stage when using multiple monitors // in the screensaver, we have to listen for changes and make sure the input area // is set to NONE when we are in locked mode @@ -986,7 +990,7 @@ namespace Gala { public override void show_window_menu (Meta.Window window, Meta.WindowMenuType menu, int x, int y) { switch (menu) { case Meta.WindowMenuType.WM: - if (daemon_proxy == null) { + if (daemon_proxy == null || window.get_window_type () == Meta.WindowType.NOTIFICATION) { return; } @@ -1527,6 +1531,13 @@ namespace Gala { window.is_attached_dialog ()) dim_window (window.find_root_ancestor (), true); + break; + case Meta.WindowType.NOTIFICATION: + if (BehaviorSettings.get_default ().use_new_notifications) { + notification_stack.show_notification (actor); + map_completed (actor); + } + break; default: map_completed (actor); @@ -1618,6 +1629,17 @@ namespace Gala { actor.opacity = 0U; actor.restore_easing_state (); + ulong destroy_handler_id = 0UL; + destroy_handler_id = actor.transitions_completed.connect (() => { + actor.disconnect (destroy_handler_id); + destroying.remove (actor); + destroy_completed (actor); + }); + break; + case Meta.WindowType.NOTIFICATION: + destroying.add (actor); + notification_stack.destroy_notification (actor); + ulong destroy_handler_id = 0UL; destroy_handler_id = actor.transitions_completed.connect (() => { actor.disconnect (destroy_handler_id); @@ -1930,11 +1952,13 @@ namespace Gala { parents.prepend (actor.get_parent ()); clutter_actor_reparent (actor, static_windows); - actor.set_translation (-clone_offset_x, -clone_offset_y, 0); - actor.save_easing_state (); - actor.set_easing_duration (300); - actor.opacity = 0; - actor.restore_easing_state (); + if (window.window_type != Meta.WindowType.NOTIFICATION) { + actor.set_translation (-clone_offset_x, -clone_offset_y, 0); + actor.save_easing_state (); + actor.set_easing_duration (300); + actor.opacity = 0; + actor.restore_easing_state (); + } } continue; diff --git a/src/meson.build b/src/meson.build index ef6200b5..c751c65a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ gala_bin_sources = files( 'DragDropAction.vala', 'InternalUtils.vala', 'KeyboardManager.vala', + 'NotificationStack.vala', 'Main.vala', 'MediaFeedback.vala', 'PluginManager.vala',