diff --git a/README.md b/README.md
index 186ea9a..9ccdc9e 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotif
## Features
+- Grouped notifications
- Keyboard shortcuts
- Notification body markup with image support
- Inline replies
diff --git a/data/icons/meson.build b/data/icons/meson.build
new file mode 100644
index 0000000..0f93645
--- /dev/null
+++ b/data/icons/meson.build
@@ -0,0 +1,4 @@
+app_resources += gnome.compile_resources('icon-resources',
+ 'swaync_icons.gresource.xml',
+ c_name: 'sway_notification_center_icons'
+)
diff --git a/data/icons/swaync-close-symbolic.svg b/data/icons/swaync-close-symbolic.svg
new file mode 100644
index 0000000..f68a861
--- /dev/null
+++ b/data/icons/swaync-close-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/data/icons/swaync-collapse-symbolic.svg b/data/icons/swaync-collapse-symbolic.svg
new file mode 100644
index 0000000..1e7f5c5
--- /dev/null
+++ b/data/icons/swaync-collapse-symbolic.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/data/icons/swaync_icons.gresource.xml b/data/icons/swaync_icons.gresource.xml
new file mode 100644
index 0000000..7e681b2
--- /dev/null
+++ b/data/icons/swaync_icons.gresource.xml
@@ -0,0 +1,7 @@
+
+
+
+ swaync-collapse-symbolic.svg
+ swaync-close-symbolic.svg
+
+
diff --git a/data/meson.build b/data/meson.build
index 01f1eb7..699cddc 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -2,6 +2,8 @@ install_data('org.erikreider.swaync.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
+subdir('icons')
+
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
diff --git a/meson.build b/meson.build
index 170520e..f8249a0 100644
--- a/meson.build
+++ b/meson.build
@@ -9,6 +9,9 @@ add_project_arguments(['--enable-gobject-tracing'], language: 'vala')
add_project_arguments(['--enable-checking'], language: 'vala')
i18n = import('i18n')
+gnome = import('gnome')
+
+app_resources = []
subdir('data')
subdir('src')
diff --git a/src/animation/animation.vala b/src/animation/animation.vala
new file mode 100644
index 0000000..2068b7e
--- /dev/null
+++ b/src/animation/animation.vala
@@ -0,0 +1,150 @@
+namespace SwayNotificationCenter {
+ public class Animation : Object {
+
+ private unowned Gtk.Widget widget;
+
+ double value;
+
+ double value_from;
+ double value_to;
+ int64 duration;
+
+ int64 start_time;
+ uint tick_cb_id;
+ ulong unmap_cb_id;
+
+ unowned AnimationEasingFunc easing_func;
+ unowned AnimationValueCallback value_cb;
+ unowned AnimationDoneCallback done_cb;
+
+ bool is_done;
+
+
+ public delegate void AnimationValueCallback (double value);
+
+ public delegate void AnimationDoneCallback ();
+
+ public delegate double AnimationEasingFunc (double t);
+
+ public Animation (Gtk.Widget widget, int64 duration,
+ AnimationEasingFunc easing_func,
+ AnimationValueCallback value_cb,
+ AnimationDoneCallback done_cb) {
+ this.widget = widget;
+ this.duration = duration;
+ this.easing_func = easing_func;
+ this.value_cb = value_cb;
+ this.done_cb = done_cb;
+
+ this.is_done = false;
+ }
+
+ ~Animation () {
+ stop ();
+ }
+
+ void set_value (double value) {
+ this.value = value;
+ this.value_cb (value);
+ }
+
+ void done () {
+ if (is_done) return;
+
+ is_done = true;
+ done_cb ();
+ }
+
+ bool tick_cb (Gtk.Widget widget, Gdk.FrameClock frame_clock) {
+ int64 frame_time = frame_clock.get_frame_time () / 1000; /* ms */
+ double t = (double) (frame_time - start_time) / duration;
+
+ if (t >= 1) {
+ tick_cb_id = 0;
+
+ set_value (value_to);
+
+ if (unmap_cb_id != 0) {
+ SignalHandler.disconnect (widget, unmap_cb_id);
+ unmap_cb_id = 0;
+ }
+
+ done ();
+
+ return Source.REMOVE;
+ }
+
+ set_value (lerp (value_from, value_to, easing_func (t)));
+
+ return Source.CONTINUE;
+ }
+
+ public void start (double from, double to) {
+ this.value_from = from;
+ this.value_to = to;
+ this.value = from;
+ this.is_done = false;
+
+ unowned Gtk.Settings ? gsettings = Gtk.Settings.get_default ();
+ bool animations_enabled =
+ gsettings != null ? gsettings.gtk_enable_animations : true;
+ if (animations_enabled != true ||
+ !widget.get_mapped () || duration <= 0) {
+ set_value (value_to);
+
+ done ();
+ return;
+ }
+
+
+ start_time = widget.get_frame_clock ().get_frame_time () / 1000;
+
+ if (tick_cb_id != 0) return;
+
+ unmap_cb_id =
+ Signal.connect_swapped (widget, "unmap", (Callback) stop, this);
+ tick_cb_id = widget.add_tick_callback (tick_cb);
+ }
+
+ public void stop () {
+ if (tick_cb_id != 0) {
+ widget.remove_tick_callback (tick_cb_id);
+ tick_cb_id = 0;
+ }
+
+ if (unmap_cb_id != 0) {
+ SignalHandler.disconnect (widget, unmap_cb_id);
+ unmap_cb_id = 0;
+ }
+
+ done ();
+ }
+
+ public double get_value () {
+ return value;
+ }
+
+ public static double lerp (double a, double b, double t) {
+ return a * (1.0 - t) + b * t;
+ }
+
+ public static double ease_out_cubic (double t) {
+ double p = t - 1;
+ return p * p * p + 1;
+ }
+
+ public static double ease_in_cubic (double t) {
+ return t * t * t;
+ }
+
+ public static double ease_in_out_cubic (double t) {
+ double p = t * 2;
+
+ if (p < 1) return 0.5 * p * p * p;
+
+ p -= 2;
+
+ return 0.5 * (p * p * p + 2);
+ }
+ }
+}
diff --git a/src/constants.vala.in b/src/constants.vala.in
index 28cfa2c..31779bc 100644
--- a/src/constants.vala.in
+++ b/src/constants.vala.in
@@ -1,4 +1,5 @@
public class Constants {
public const string VERSION = @VERSION@;
public const string VERSIONNUM = @VERSION_NUM@;
+ public const uint ANIMATION_DURATION = 400;
}
diff --git a/src/controlCenter/controlCenter.ui b/src/controlCenter/controlCenter.ui
index fe11730..07d230a 100644
--- a/src/controlCenter/controlCenter.ui
+++ b/src/controlCenter/controlCenter.ui
@@ -35,24 +35,7 @@
True
never
-
+
diff --git a/src/controlCenter/controlCenter.vala b/src/controlCenter/controlCenter.vala
index 28acdd2..15c2ab1 100644
--- a/src/controlCenter/controlCenter.vala
+++ b/src/controlCenter/controlCenter.vala
@@ -1,20 +1,30 @@
namespace SwayNotificationCenter {
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
public class ControlCenter : Gtk.ApplicationWindow {
-
[GtkChild]
unowned Gtk.Box notifications_box;
[GtkChild]
- unowned Gtk.ScrolledWindow scrolled_window;
- [GtkChild]
- unowned Gtk.Viewport viewport;
- [GtkChild]
unowned Gtk.Stack stack;
[GtkChild]
- unowned Gtk.ListBox list_box;
+ unowned Gtk.ScrolledWindow scrolled_window;
+ FadedViewport viewport = new FadedViewport (20);
+ Gtk.ListBox list_box = new Gtk.ListBox ();
+
[GtkChild]
unowned Gtk.Box box;
+ unowned NotificationGroup ? expanded_group = null;
+ private double fade_animation_progress = 1.0;
+ private Animation ? notification_fade_animation;
+ private double scroll_animation_progress = 1.0;
+ private Animation ? scroll_animation;
+
+ HashTable noti_groups_id =
+ new HashTable (direct_hash, direct_equal);
+ /** NOTE: Only includes groups with ids with length of > 0 */
+ HashTable noti_groups_name =
+ new HashTable (str_hash, str_equal);
+
const string STACK_NOTIFICATIONS_PAGE = "notifications-list";
const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
@@ -25,9 +35,8 @@ namespace SwayNotificationCenter {
private SwayncDaemon swaync_daemon;
private NotiDaemon noti_daemon;
- private uint list_position = 0;
+ private int list_position = 0;
- private double last_upper = 0;
private bool list_reverse = false;
private Gtk.Align list_align = Gtk.Align.START;
@@ -40,6 +49,18 @@ namespace SwayNotificationCenter {
this.swaync_daemon.reloading_css.connect (reload_notifications_style);
+ viewport.set_visible (true);
+ viewport.set_vexpand (true);
+ viewport.set_shadow_type (Gtk.ShadowType.NONE);
+ scrolled_window.add (viewport);
+
+ list_box.set_visible (true);
+ list_box.set_valign (Gtk.Align.END);
+ list_box.set_selection_mode (Gtk.SelectionMode.NONE);
+ list_box.set_activate_on_single_click (false);
+ list_box.get_style_context ().add_class ("control-center-list");
+ viewport.add (list_box);
+
if (swaync_daemon.use_layer_shell) {
if (!GtkLayerShell.is_supported ()) {
stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
@@ -56,8 +77,6 @@ namespace SwayNotificationCenter {
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
}
- viewport.size_allocate.connect (size_alloc);
-
this.map.connect (() => {
set_anchor ();
// Wait until the layer has attached
@@ -154,71 +173,7 @@ namespace SwayNotificationCenter {
return true;
});
- this.key_press_event.connect ((w, event_key) => {
- if (this.get_focus () is Gtk.Entry) return false;
- if (event_key.type == Gdk.EventType.KEY_PRESS) {
- var children = list_box.get_children ();
- Notification noti = (Notification)
- list_box.get_focus_child ();
- switch (Gdk.keyval_name (event_key.keyval)) {
- case "Return":
- if (noti != null) noti.click_default_action ();
- break;
- case "Delete":
- case "BackSpace":
- if (noti != null) {
- if (children.length () == 0) break;
- if (list_reverse &&
- children.first ().data != noti) {
- list_position--;
- } else if (children.last ().data == noti) {
- if (list_position > 0) list_position--;
- }
- close_notification (noti.param.applied_id, true);
- }
- break;
- case "C":
- close_all_notifications ();
- break;
- case "D":
- try {
- swaync_daemon.toggle_dnd ();
- } catch (Error e) {
- error ("Error: %s\n", e.message);
- }
- break;
- case "Down":
- if (list_position + 1 < children.length ()) {
- ++list_position;
- }
- break;
- case "Up":
- if (list_position > 0) --list_position;
- break;
- case "Home":
- list_position = 0;
- break;
- case "End":
- list_position = children.length () - 1;
- if (list_position == uint.MAX) list_position = 0;
- break;
- default:
- // Pressing 1-9 to activate a notification action
- for (int i = 0; i < 9; i++) {
- uint keyval = Gdk.keyval_from_name (
- (i + 1).to_string ());
- if (event_key.keyval == keyval) {
- if (noti != null) noti.click_alt_action (i);
- break;
- }
- }
- break;
- }
- navigate_list (list_position);
- }
- // Override the builtin list navigation
- return true;
- });
+ key_press_event.connect (key_press_event_cb);
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
// Switches the stack page depending on the amount of notifications
@@ -232,6 +187,171 @@ namespace SwayNotificationCenter {
});
add_widgets ();
+
+ notification_fade_animation = new Animation (this, Constants.ANIMATION_DURATION,
+ Animation.ease_in_out_cubic,
+ fade_animation_value_cb,
+ fade_animation_done_cb);
+ scroll_animation = new Animation (this, Constants.ANIMATION_DURATION,
+ Animation.ease_in_out_cubic,
+ scroll_animation_value_cb,
+ scroll_animation_done_cb);
+ list_box.draw.connect (list_box_draw_cb);
+ }
+
+ void fade_animation_value_cb (double progress) {
+ this.fade_animation_progress = progress;
+
+ this.queue_draw ();
+ }
+
+ void fade_animation_done_cb () {}
+
+ void fade_animate (double to) {
+ notification_fade_animation.stop ();
+ notification_fade_animation.start (fade_animation_progress, to);
+ }
+
+ void scroll_animation_value_cb (double progress) {
+ this.scroll_animation_progress = progress;
+
+ // Scroll to the top of the group
+ if (scroll_animation_progress > 0) {
+ scrolled_window.vadjustment.set_value (scroll_animation_progress);
+ }
+ }
+
+ void scroll_animation_done_cb () {
+ int y = expanded_group.get_relative_y (list_box);
+ if (y > 0) {
+ scrolled_window.vadjustment.set_value (y);
+ }
+ }
+
+ void scroll_animate (double to) {
+ scroll_animation.stop ();
+ scroll_animation.start (scroll_animation_progress, to);
+ }
+
+ /// Fade non-expanded groups when one group is expanded
+ private bool list_box_draw_cb (Cairo.Context cr) {
+ Cairo.Pattern fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
+ fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 1 - fade_animation_progress - 0.5);
+
+ foreach (unowned Gtk.Widget widget in list_box.get_children ()) {
+ Gtk.Allocation alloc;
+ widget.get_allocated_size (out alloc, null);
+
+ cr.save ();
+ cr.translate (0, alloc.y);
+
+ cr.push_group ();
+ widget.draw (cr);
+
+ cr.scale (alloc.width, alloc.height);
+ if (widget != expanded_group) {
+ cr.set_source (fade_gradient);
+ cr.rectangle (0, 0, alloc.width, alloc.height);
+ cr.set_operator (Cairo.Operator.DEST_OUT);
+ cr.fill ();
+ }
+
+ cr.pop_group_to_source ();
+ cr.paint ();
+
+ cr.restore ();
+ }
+ return true;
+ }
+
+ private bool key_press_event_cb (Gdk.EventKey event_key) {
+ if (this.get_focus () is Gtk.Entry) return false;
+ if (event_key.type == Gdk.EventType.KEY_PRESS) {
+ var children = list_box.get_children ();
+ var group = (NotificationGroup) list_box.get_focus_child ();
+ switch (Gdk.keyval_name (event_key.keyval)) {
+ case "Return":
+ if (group != null) {
+ var noti = group.get_latest_notification ();
+ if (group.only_single_notification () && noti != null) {
+ noti.click_default_action ();
+ break;
+ }
+ group.on_expand_change (group.toggle_expanded ());
+ }
+ break;
+ case "Delete":
+ case "BackSpace":
+ if (group != null) {
+ int len = (int) children.length ();
+ if (len == 0) break;
+ // Add a delta so that we select the next notification
+ // due to it not being gone from the list yet due to
+ // the fade transition
+ int delta = 2;
+ if (list_reverse) {
+ if (children.first ().data != group) {
+ delta = 0;
+ }
+ list_position--;
+ } else {
+ if (list_position > 0) list_position--;
+ if (children.last ().data == group) {
+ delta = 0;
+ }
+ }
+ var noti = group.get_latest_notification ();
+ if (group.only_single_notification () && noti != null) {
+ close_notification (noti.param.applied_id, true);
+ break;
+ }
+ group.close_all_notifications ();
+ navigate_list (list_position + delta);
+ return true;
+ }
+ break;
+ case "C":
+ close_all_notifications ();
+ break;
+ case "D":
+ try {
+ swaync_daemon.toggle_dnd ();
+ } catch (Error e) {
+ error ("Error: %s\n", e.message);
+ }
+ break;
+ case "Down":
+ if (list_position + 1 < children.length ()) {
+ ++list_position;
+ }
+ break;
+ case "Up":
+ if (list_position > 0) --list_position;
+ break;
+ case "Home":
+ list_position = 0;
+ break;
+ case "End":
+ list_position = ((int) children.length ()) - 1;
+ if (list_position == uint.MAX) list_position = 0;
+ break;
+ default:
+ // Pressing 1-9 to activate a notification action
+ for (int i = 0; i < 9; i++) {
+ uint keyval = Gdk.keyval_from_name (
+ (i + 1).to_string ());
+ if (event_key.keyval == keyval && group != null) {
+ var noti = group.get_latest_notification ();
+ noti.click_alt_action (i);
+ break;
+ }
+ }
+ break;
+ }
+ navigate_list (list_position);
+ }
+ // Override the builtin list navigation
+ return true;
}
/** Adds all custom widgets. Removes previous widgets */
@@ -358,28 +478,37 @@ namespace SwayNotificationCenter {
box.set_valign (align_y);
list_box.set_valign (list_align);
- list_box.set_sort_func ((w1, w2) => {
- var a = (Notification) w1;
- var b = (Notification) w2;
- if (a == null || b == null) return 0;
- // Sort the list in reverse if needed
- if (a.param.time == b.param.time) return 0;
- int val = list_reverse ? 1 : -1;
- return a.param.time > b.param.time ? val : val * -1;
- });
+ list_box.set_sort_func (list_box_sort_func);
// Always set the size request in all events.
box.set_size_request (ConfigModel.instance.control_center_width,
ConfigModel.instance.control_center_height);
}
- private void size_alloc () {
- var adj = viewport.vadjustment;
- double upper = adj.get_upper ();
- if (last_upper < upper) {
- scroll_to_start (list_reverse);
+ /**
+ * Returns < 0 if row1 should be before row2, 0 if they are equal
+ * and > 0 otherwise
+ */
+ private int list_box_sort_func (Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) {
+ int val = list_reverse ? 1 : -1;
+
+ var a_group = (NotificationGroup) row1;
+ var b_group = (NotificationGroup) row2;
+
+ // Check urgency before time
+ var a_urgency = a_group.get_is_urgent ();
+ var b_urgency = b_group.get_is_urgent ();
+ if (a_urgency != b_urgency) {
+ return a_urgency ? val : val * -1;
}
- last_upper = upper;
+
+ // Check time
+ var a_time = a_group.get_time ();
+ var b_time = b_group.get_time ();
+ if (a_time < 0 || b_time < 0) return 0;
+ // Sort the list in reverse if needed
+ if (a_time == b_time) return 0;
+ return a_time > b_time ? val : val * -1;
}
private void scroll_to_start (bool reverse) {
@@ -391,20 +520,26 @@ namespace SwayNotificationCenter {
}
public uint notification_count () {
- return list_box.get_children ().length ();
+ uint count = 0;
+ foreach (unowned Gtk.Widget widget in list_box.get_children ()) {
+ if (widget is NotificationGroup) {
+ count += ((NotificationGroup) widget).get_num_notifications ();
+ }
+ }
+ return count;
}
public void close_all_notifications () {
foreach (var w in list_box.get_children ()) {
- Notification noti = (Notification) w;
- if (noti != null) noti.close_notification (false);
+ NotificationGroup group = (NotificationGroup) w;
+ if (group != null) group.close_all_notifications ();
}
try {
swaync_daemon.subscribe_v2 (notification_count (),
- swaync_daemon.get_dnd (),
- get_visibility (),
- swaync_daemon.inhibited);
+ swaync_daemon.get_dnd (),
+ get_visibility (),
+ swaync_daemon.inhibited);
} catch (Error e) {
stderr.printf (e.message + "\n");
}
@@ -414,11 +549,20 @@ namespace SwayNotificationCenter {
}
}
- private void navigate_list (uint i) {
- var widget = list_box.get_children ().nth_data (i);
+ private void navigate_list (int i) {
+ unowned Gtk.Widget ? widget = list_box.get_children ().nth_data (i);
+ if (widget == null) {
+ // Try getting the last widget
+ if (list_reverse) {
+ widget = list_box.get_children ().nth_data (0);
+ } else {
+ int len = ((int) list_box.get_children ().length ()) - 1;
+ widget = list_box.get_children ().nth_data (len);
+ }
+ }
if (widget != null) {
- list_box.set_focus_child (widget);
widget.grab_focus ();
+ list_box.set_focus_child (widget);
}
}
@@ -431,20 +575,20 @@ namespace SwayNotificationCenter {
if (this.visible) {
// Focus the first notification
list_position = list_reverse ?
- (list_box.get_children ().length () - 1) : 0;
+ (((int) list_box.get_children ().length ()) - 1) : 0;
if (list_position == uint.MAX) list_position = 0;
list_box.grab_focus ();
navigate_list (list_position);
foreach (var w in list_box.get_children ()) {
- var noti = (Notification) w;
- if (noti != null) noti.set_time ();
+ var group = (NotificationGroup) w;
+ if (group != null) group.update ();
}
}
swaync_daemon.subscribe_v2 (notification_count (),
- noti_daemon.dnd,
- this.visible,
- swaync_daemon.inhibited);
+ noti_daemon.dnd,
+ this.visible,
+ swaync_daemon.inhibited);
}
public bool toggle_visibility () {
@@ -463,7 +607,9 @@ namespace SwayNotificationCenter {
}
public void close_notification (uint32 id, bool dismiss) {
- foreach (var w in list_box.get_children ()) {
+ unowned NotificationGroup group = null;
+ if (!noti_groups_id.lookup_extended (id, null, out group))return;
+ foreach (var w in group.get_notifications ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
if (!dismiss) {
@@ -471,21 +617,37 @@ namespace SwayNotificationCenter {
noti.destroy ();
} else {
noti.close_notification (false);
- list_box.remove (w);
+ group.remove_notification (noti);
}
+ noti_groups_id.remove (id);
break;
}
}
+ if (group.is_empty ()) {
+ if (group.name_id.length > 0) {
+ noti_groups_name.remove (group.name_id);
+ }
+ if (expanded_group == group) {
+ expanded_group = null;
+ fade_animate (1);
+ }
+ group.destroy ();
+ }
}
public void replace_notification (uint32 id, NotifyParams new_params) {
- foreach (var w in list_box.get_children ()) {
- var noti = (Notification) w;
- if (noti != null && noti.param.applied_id == id) {
- noti.replace_notification (new_params);
- // Position the notification in the beginning of the list
- list_box.invalidate_sort ();
- return;
+ unowned NotificationGroup group = null;
+ if (noti_groups_id.lookup_extended (id, null, out group)) {
+ foreach (var w in group.get_notifications ()) {
+ var noti = (Notification) w;
+ if (noti != null && noti.param.applied_id == id) {
+ noti_groups_id.remove (id);
+ noti_groups_id.set (new_params.applied_id, group);
+ noti.replace_notification (new_params);
+ // Position the notification in the beginning of the list
+ list_box.invalidate_sort ();
+ return;
+ }
}
}
@@ -498,19 +660,60 @@ namespace SwayNotificationCenter {
noti_daemon,
NotificationType.CONTROL_CENTER);
noti.grab_focus.connect ((w) => {
- uint i = list_box.get_children ().index (w);
+ int i = list_box.get_children ().index (w);
if (list_position != uint.MAX && list_position != i) {
list_position = i;
}
});
noti.set_time ();
- list_box.add (noti);
+
+ NotificationGroup ? group = null;
+ if (param.name_id.length > 0) {
+ noti_groups_name.lookup_extended (param.name_id, null, out group);
+ }
+ if (group == null) {
+ group = new NotificationGroup (param.name_id, param.display_name);
+ // Collapse other groups on expand
+ group.on_expand_change.connect ((expanded) => {
+ if (!expanded) {
+ fade_animate (1);
+ foreach (unowned Gtk.Widget child in list_box.get_children ()) {
+ child.set_sensitive (true);
+ }
+ return;
+ }
+ expanded_group = group;
+ expanded_group.set_sensitive (true);
+ fade_animate (0);
+ int y = expanded_group.get_relative_y (list_box);
+ if (y > 0) {
+ scroll_animate (y);
+ }
+ foreach (unowned Gtk.Widget child in list_box.get_children ()) {
+ NotificationGroup g = (NotificationGroup) child;
+ if (g != null && g != group) {
+ g.set_expanded (false);
+ if (g.only_single_notification ()) {
+ g.set_sensitive (false);
+ }
+ }
+ }
+ });
+ if (param.name_id.length > 0) {
+ noti_groups_name.set (param.name_id, group);
+ }
+ list_box.add (group);
+ }
+ noti_groups_id.set (param.applied_id, group);
+
+ group.add_notification (noti);
+ list_box.invalidate_sort ();
scroll_to_start (list_reverse);
try {
swaync_daemon.subscribe_v2 (notification_count (),
- swaync_daemon.get_dnd (),
- get_visibility (),
- swaync_daemon.inhibited);
+ swaync_daemon.get_dnd (),
+ get_visibility (),
+ swaync_daemon.inhibited);
} catch (Error e) {
stderr.printf (e.message + "\n");
}
@@ -527,8 +730,13 @@ namespace SwayNotificationCenter {
/** Forces each notification EventBox to reload its style_context #27 */
private void reload_notifications_style () {
foreach (var c in list_box.get_children ()) {
- Notification noti = (Notification) c;
- if (noti != null) noti.reload_style_context ();
+ NotificationGroup group = (NotificationGroup) c;
+ if (group != null) {
+ foreach (unowned var widget in group.get_notifications ()) {
+ Notification noti = (Notification) widget;
+ noti.reload_style_context ();
+ }
+ }
}
}
}
diff --git a/src/fadedViewport/fadedViewport.vala b/src/fadedViewport/fadedViewport.vala
new file mode 100644
index 0000000..cd5dfd4
--- /dev/null
+++ b/src/fadedViewport/fadedViewport.vala
@@ -0,0 +1,157 @@
+namespace SwayNotificationCenter {
+ public class FadedViewport : Gtk.Viewport {
+ private int fade_height = 30;
+
+ private FadedViewportChild container;
+
+ public FadedViewport (int fade_height) {
+ if (fade_height > 0) this.fade_height = fade_height;
+ this.container = new FadedViewportChild (this.fade_height);
+
+ base.add (container);
+ }
+
+ public override void add (Gtk.Widget widget) {
+ container.add (widget);
+ }
+
+ public override void remove (Gtk.Widget widget) {
+ container.remove (widget);
+ }
+
+ public override bool draw (Cairo.Context cr) {
+ Gtk.Allocation alloc;
+ get_allocated_size (out alloc, null);
+
+ Cairo.Pattern top_fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
+ top_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 1);
+ top_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 0);
+ Cairo.Pattern bottom_fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
+ bottom_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 0);
+ bottom_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 1);
+
+ cr.save ();
+ cr.push_group ();
+
+ // Draw widgets
+ base.draw (cr);
+
+ /// Draw vertical fade
+
+ // Top fade
+ cr.save ();
+ cr.scale (alloc.width, fade_height);
+ cr.rectangle (0, 0, alloc.width, fade_height);
+ cr.set_source (top_fade_gradient);
+ cr.set_operator (Cairo.Operator.DEST_OUT);
+ cr.fill ();
+ cr.restore ();
+ // Bottom fade
+ cr.save ();
+ cr.translate (0, alloc.height - fade_height);
+ cr.scale (alloc.width, fade_height);
+ cr.rectangle (0, 0, alloc.width, fade_height);
+ cr.set_source (bottom_fade_gradient);
+ cr.set_operator (Cairo.Operator.DEST_OUT);
+ cr.fill ();
+ cr.restore ();
+
+ cr.pop_group_to_source ();
+ cr.paint ();
+ cr.restore ();
+ return true;
+ }
+ }
+}
+
+private class FadedViewportChild : Gtk.Container {
+ private int y_padding;
+
+ private unowned Gtk.Widget _child;
+
+ public FadedViewportChild (int y_padding) {
+ base.set_has_window (false);
+ base.set_can_focus (true);
+ base.set_redraw_on_allocate (false);
+
+ this.y_padding = (int) (y_padding * 0.5);
+ this._child = null;
+
+ this.show ();
+ }
+
+ public override void add (Gtk.Widget widget) {
+ if (this._child == null) {
+ widget.set_parent (this);
+ this._child = widget;
+ }
+ }
+
+ public override void remove (Gtk.Widget widget) {
+ if (this._child == widget) {
+ widget.unparent ();
+ this._child = null;
+ if (this.get_visible () && widget.get_visible ()) {
+ this.queue_resize_no_redraw ();
+ }
+ }
+ }
+
+ public override void forall_internal (bool include_internals, Gtk.Callback callback) {
+ if (this._child != null) {
+ callback (this._child);
+ }
+ }
+
+ public override Gtk.SizeRequestMode get_request_mode () {
+ if (this._child != null) {
+ return this._child.get_request_mode ();
+ } else {
+ return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
+ }
+ }
+
+ public override void size_allocate (Gtk.Allocation allocation) {
+ Gtk.Allocation child_allocation = Gtk.Allocation ();
+ uint border_width = this.get_border_width ();
+ if (this._child != null && this._child.get_visible ()) {
+ child_allocation.x = allocation.x + (int) border_width;
+ child_allocation.y = allocation.y + y_padding + (int) border_width;
+ child_allocation.width = allocation.width - 2 * (int) border_width;
+ child_allocation.height = allocation.height + y_padding * 2
+ - 2 * (int) border_width;
+ this._child.size_allocate (child_allocation);
+ if (this.get_realized ()) {
+ this._child.show ();
+ }
+ }
+ if (this.get_realized ()) {
+ if (this._child != null) {
+ this._child.set_child_visible (true);
+ }
+ }
+ base.size_allocate (allocation);
+ }
+
+ public override void get_preferred_height_for_width (int width,
+ out int minimum_height,
+ out int natural_height) {
+ minimum_height = 0;
+ natural_height = 0;
+
+ if (_child != null && _child.get_visible ()) {
+ _child.get_preferred_height_for_width (width,
+ out minimum_height,
+ out natural_height);
+
+ minimum_height += y_padding * 2;
+ natural_height += y_padding * 2;
+ }
+
+ }
+
+ public override bool draw (Cairo.Context cr) {
+ base.draw (cr);
+ return false;
+ }
+}
diff --git a/src/functions.vala b/src/functions.vala
index 7ba9445..7b46f7b 100644
--- a/src/functions.vala
+++ b/src/functions.vala
@@ -8,6 +8,10 @@ namespace SwayNotificationCenter {
public static void init () {
system_css_provider = new Gtk.CssProvider ();
user_css_provider = new Gtk.CssProvider ();
+
+ // Init resources
+ var theme = Gtk.IconTheme.get_default ();
+ theme.add_resource_path ("/org/erikreider/swaync/icons");
}
public static void set_image_path (owned string path,
diff --git a/src/meson.build b/src/meson.build
index 8a62739..f7bc46f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -50,13 +50,16 @@ widget_sources = [
app_sources = [
'main.vala',
+ 'animation/animation.vala',
'orderedHashTable/orderedHashTable.vala',
'configModel/configModel.vala',
'swayncDaemon/swayncDaemon.vala',
'notiDaemon/notiDaemon.vala',
'notiModel/notiModel.vala',
+ 'fadedViewport/fadedViewport.vala',
'notificationWindow/notificationWindow.vala',
'notification/notification.vala',
+ 'notificationGroup/notificationGroup.vala',
'controlCenter/controlCenter.vala',
widget_sources,
'blankWindow/blankWindow.vala',
@@ -119,15 +122,14 @@ args = [
]
sysconfdir = get_option('sysconfdir')
-gnome = import('gnome')
-app_sources += gnome.compile_resources('sway_notification_center-resources',
+app_resources += gnome.compile_resources('sway_notification_center-resources',
'sway_notification_center.gresource.xml',
c_name: 'sway_notification_center'
)
executable('swaync',
- app_sources,
+ [ app_sources, app_resources ],
vala_args: args,
dependencies: app_deps,
install: true,
diff --git a/src/notiModel/notiModel.vala b/src/notiModel/notiModel.vala
index e647b71..9b218c1 100644
--- a/src/notiModel/notiModel.vala
+++ b/src/notiModel/notiModel.vala
@@ -115,6 +115,12 @@ namespace SwayNotificationCenter {
public Array actions { get; set; }
+ public DesktopAppInfo ? desktop_app_info = null;
+
+ public string name_id;
+
+ public string display_name;
+
public NotifyParams (uint32 applied_id,
string app_name,
uint32 replaces_id,
@@ -139,6 +145,32 @@ namespace SwayNotificationCenter {
parse_hints ();
parse_actions (actions);
+
+ // Try to get the desktop file
+ string[] entries = {};
+ if (desktop_entry != null) entries += desktop_entry.replace (".desktop", "");
+ if (app_name != null) entries += app_name.replace (".desktop", "");
+ foreach (string entry in entries) {
+ var app_info = new DesktopAppInfo ("%s.desktop".printf (entry));
+ // Checks if the .desktop file actually exists or not
+ if (app_info is DesktopAppInfo) {
+ desktop_app_info = app_info;
+ break;
+ }
+ }
+
+ // Set name_id
+ this.name_id = this.desktop_entry ?? this.app_name ?? "";
+
+ // Set display_name and make the first letter upper case
+ string ? display_name = this.desktop_entry ?? this.app_name;
+ if (desktop_app_info != null) {
+ display_name = desktop_app_info.get_display_name ();
+ }
+ if (display_name == null || display_name.length == 0) {
+ display_name = "Unknown";
+ }
+ this.display_name = display_name.splice (0, 1, display_name.up (1));
}
private void parse_hints () {
diff --git a/src/notification/notification.ui b/src/notification/notification.ui
index 7c38125..67e8ed2 100644
--- a/src/notification/notification.ui
+++ b/src/notification/notification.ui
@@ -281,7 +281,7 @@
True
False
- window-close-symbolic
+ swaync-close-symbolic