Grouped notifications (#345)

* Initial animation work

* Added custom icon

* Fixed collapse icon not being symbolic

* Centered collapse button

* Fixed group of 2 notifications being invisible

* Added back notification logic

Notifications are now separated into their own group depending on their provided name

* Added close all button

Also changed the notification close button icon to the new provided icon

* Fixed replacing notifications not working as expected

* Fixed group sensitivity not being set when auto collapsed

* Don't group notis with no provided app-name/desktop-file

Also adds parsing of desktop file in NotiModel which helps with getting and using the display name as the group name

* Remove testing notifications

* General fixes

* Added fade to cc viewport

* Added padding to cc viewport fade

* Call on_expand_change on close all button click

* Updated README

* Sort critical notification groups before regular groups

* Added group title icon

* Fixed not being able to navigate through single notifications

* Scroll to top of group on expand

* Fix non expanded single noti groups being clickable

* Fixed linting issues

* Added styling

* Fixed invalid style reload cast

* Set lower ordered notifications content opacity to 0

* Added hover effect to groups
This commit is contained in:
Erik Reider
2023-12-12 21:02:46 +01:00
committed by GitHub
parent 305ebf8734
commit 86166a46b7
19 changed files with 1342 additions and 157 deletions

View File

@@ -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

4
data/icons/meson.build Normal file
View File

@@ -0,0 +1,4 @@
app_resources += gnome.compile_resources('icon-resources',
'swaync_icons.gresource.xml',
c_name: 'sway_notification_center_icons'
)

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 4 4 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 2.28125 2.28125 l 2.3125 -2.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -2.28125 2.28125 l 2.25 2.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -2.28125 -2.28125 l -2.28125 2.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 2.28125 -2.25 l -2.28125 -2.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 767 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="M 4 2.5 C 4 2.234 4.106 1.98 4.293 1.793 C 4.684 1.402 5.317 1.402 5.707 1.793 L 8 4.086 L 10.293 1.793 C 10.684 1.402 11.317 1.402 11.707 1.793 C 11.895 1.98 12 2.234 12 2.5 C 12 2.766 11.895 3.019 11.707 3.207 L 8.707 6.207 C 8.317 6.598 7.684 6.598 7.293 6.207 L 4.293 3.207 C 4.106 3.019 4 2.766 4 2.5 Z" fill="#2e3436"/>
<path d="M 4 13.5 C 4 13.766 4.106 14.019 4.293 14.207 C 4.684 14.598 5.317 14.598 5.707 14.207 L 8 11.914 L 10.293 14.207 C 10.684 14.598 11.317 14.598 11.707 14.207 C 11.895 14.019 12 13.766 12 13.5 C 12 13.234 11.895 12.98 11.707 12.793 L 8.707 9.793 C 8.317 9.402 7.684 9.402 7.293 9.793 L 4.293 12.793 C 4.106 12.98 4 13.234 4 13.5 Z" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 830 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/erikreider/swaync/icons/scalable/actions">
<file preprocess="xml-stripblanks">swaync-collapse-symbolic.svg</file>
<file preprocess="xml-stripblanks">swaync-close-symbolic.svg</file>
</gresource>
</gresources>

View File

@@ -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,

View File

@@ -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')

View File

@@ -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);
}
}
}

View File

@@ -1,4 +1,5 @@
public class Constants {
public const string VERSION = @VERSION@;
public const string VERSIONNUM = @VERSION_NUM@;
public const uint ANIMATION_DURATION = 400;
}

View File

@@ -35,24 +35,7 @@
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkViewport" id="viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkListBox" id="list_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">end</property>
<property name="selection-mode">none</property>
<property name="activate-on-single-click">False</property>
<style>
<class name="control-center-list"/>
</style>
</object>
</child>
</object>
<placeholder/>
</child>
</object>
<packing>

View File

@@ -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<uint32, unowned NotificationGroup> noti_groups_id =
new HashTable<uint32, unowned NotificationGroup> (direct_hash, direct_equal);
/** NOTE: Only includes groups with ids with length of > 0 */
HashTable<string, unowned NotificationGroup> noti_groups_name =
new HashTable<string, unowned NotificationGroup> (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 ();
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -115,6 +115,12 @@ namespace SwayNotificationCenter {
public Array<Action> 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 () {

View File

@@ -281,7 +281,7 @@
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">window-close-symbolic</property>
<property name="icon-name">swaync-close-symbolic</property>
</object>
</child>
<style>

View File

@@ -689,17 +689,8 @@ namespace SwayNotificationCenter {
if (img.storage_type == Gtk.ImageType.EMPTY) {
// Get the app icon
Icon ? icon = null;
if (param.desktop_entry != null) {
string entry = param.desktop_entry;
entry = entry.replace (".desktop", "");
DesktopAppInfo entry_info = new DesktopAppInfo (
"%s.desktop".printf (entry));
// Checks if the .desktop file actually exists or not
if (entry_info is DesktopAppInfo) {
icon = entry_info.get_icon ();
}
}
if (icon != null) {
if (param.desktop_app_info != null
&& (icon = param.desktop_app_info.get_icon ()) != null) {
img.set_from_gicon (icon, icon_size);
} else if (image_visibility == ImageVisibility.ALWAYS) {
// Default icon

View File

@@ -0,0 +1,580 @@
namespace SwayNotificationCenter {
public class NotificationGroup : Gtk.ListBoxRow {
const string STYLE_CLASS_URGENT = "critical";
const string STYLE_CLASS_COLLAPSED = "collapsed";
public string name_id;
private ExpandableGroup group;
private Gtk.Revealer revealer = new Gtk.Revealer ();
private Gtk.Image app_icon;
private Gtk.Label app_label;
private Gtk.GestureMultiPress gesture;
private bool gesture_down = false;
private bool gesture_in = false;
private HashTable<uint32, bool> urgent_notifications
= new HashTable<uint32, bool> (direct_hash, direct_equal);
public signal void on_expand_change (bool state);
public NotificationGroup (string name_id, string display_name) {
this.name_id = name_id;
get_style_context ().add_class ("notification-group");
Gtk.Box box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
revealer.set_transition_type (Gtk.RevealerTransitionType.SLIDE_UP);
revealer.set_reveal_child (false);
revealer.set_transition_duration (Constants.ANIMATION_DURATION);
// Add top controls
Gtk.Box controls_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4);
Gtk.Box end_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4);
end_box.set_halign (Gtk.Align.END);
end_box.get_style_context ().add_class ("notification-group-buttons");
// Collapse button
Gtk.Button collapse_button = new Gtk.Button.from_icon_name (
"swaync-collapse-symbolic", Gtk.IconSize.BUTTON);
collapse_button.get_style_context ().add_class ("flat");
collapse_button.get_style_context ().add_class ("circular");
collapse_button.get_style_context ().add_class ("notification-group-collapse-button");
collapse_button.set_relief (Gtk.ReliefStyle.NORMAL);
collapse_button.set_halign (Gtk.Align.END);
collapse_button.set_valign (Gtk.Align.CENTER);
collapse_button.clicked.connect (() => {
set_expanded (false);
on_expand_change (false);
});
end_box.add (collapse_button);
// Close all button
Gtk.Button close_all_button = new Gtk.Button.from_icon_name (
"swaync-close-symbolic", Gtk.IconSize.BUTTON);
close_all_button.get_style_context ().add_class ("flat");
close_all_button.get_style_context ().add_class ("circular");
close_all_button.get_style_context ().add_class ("notification-group-close-all-button");
close_all_button.set_relief (Gtk.ReliefStyle.NORMAL);
close_all_button.set_halign (Gtk.Align.END);
close_all_button.set_valign (Gtk.Align.CENTER);
close_all_button.clicked.connect (() => {
close_all_notifications ();
on_expand_change (false);
});
end_box.add (close_all_button);
// Group name label
Gtk.Box start_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4);
start_box.set_halign (Gtk.Align.START);
start_box.get_style_context ().add_class ("notification-group-headers");
// App Icon
app_icon = new Gtk.Image ();
app_icon.set_valign (Gtk.Align.CENTER);
app_icon.get_style_context ().add_class ("notification-group-icon");
start_box.add (app_icon);
// App Label
app_label = new Gtk.Label (display_name);
app_label.xalign = 0;
app_label.get_style_context ().add_class ("title-1");
app_label.get_style_context ().add_class ("notification-group-header");
start_box.add (app_label);
controls_box.pack_start (start_box);
controls_box.pack_end (end_box);
revealer.add (controls_box);
box.add (revealer);
set_activatable (false);
group = new ExpandableGroup (Constants.ANIMATION_DURATION, (state) => {
revealer.set_reveal_child (state);
// Change CSS Class
if (parent != null) {
set_classes ();
}
});
set_classes ();
box.add (group);
add (box);
show_all ();
/*
* Handling of group presses
*/
gesture = new Gtk.GestureMultiPress (this);
gesture.set_touch_only (false);
gesture.set_exclusive (true);
gesture.set_button (Gdk.BUTTON_PRIMARY);
gesture.set_propagation_phase (Gtk.PropagationPhase.CAPTURE);
gesture.pressed.connect ((_gesture, _n_press, x, y) => {
gesture_in = true;
gesture_down = true;
});
gesture.released.connect ((gesture, _n_press, _x, _y) => {
// Emit released
if (!gesture_down) return;
gesture_down = false;
if (gesture_in) {
bool single_noti = only_single_notification ();
if (!group.is_expanded && !single_noti) {
group.set_expanded (true);
on_expand_change (true);
}
group.set_sensitive (single_noti || group.is_expanded);
}
Gdk.EventSequence ? sequence = gesture.get_current_sequence ();
if (sequence == null) {
gesture_in = false;
}
});
gesture.update.connect ((gesture, sequence) => {
Gtk.GestureSingle gesture_single = (Gtk.GestureSingle) gesture;
if (sequence != gesture_single.get_current_sequence ()) return;
Gtk.Allocation allocation;
double x, y;
get_allocation (out allocation);
gesture.get_point (sequence, out x, out y);
bool intersects = (x >= 0 && y >= 0 && x < allocation.width && y < allocation.height);
if (gesture_in != intersects) {
gesture_in = intersects;
}
});
gesture.cancel.connect ((gesture, sequence) => {
if (gesture_down) {
gesture_down = false;
}
});
}
private void set_classes () {
unowned Gtk.StyleContext ctx = get_style_context ();
ctx.remove_class (STYLE_CLASS_COLLAPSED);
if (!group.is_expanded) {
if (!ctx.has_class (STYLE_CLASS_COLLAPSED)) {
ctx.add_class (STYLE_CLASS_COLLAPSED);
}
}
}
/// Returns if there's more than one notification
public bool only_single_notification () {
unowned Gtk.Widget ? widget = group.widgets.nth_data (1);
return widget == null;
}
public void set_expanded (bool state) {
group.set_expanded (state);
group.set_sensitive (only_single_notification () || group.is_expanded);
}
public bool toggle_expanded () {
bool state = !group.is_expanded;
set_expanded (state);
return state;
}
public void add_notification (Notification noti) {
if (noti.param.urgency == UrgencyLevels.CRITICAL) {
urgent_notifications.insert (noti.param.applied_id, true);
unowned Gtk.StyleContext ctx = get_style_context ();
if (!ctx.has_class (STYLE_CLASS_URGENT)) {
ctx.add_class (STYLE_CLASS_URGENT);
}
}
group.add (noti);
if (!only_single_notification ()) {
group.set_sensitive (false);
}
}
public void remove_notification (Notification noti) {
urgent_notifications.remove (noti.param.applied_id);
if (urgent_notifications.length == 0) {
get_style_context ().remove_class (STYLE_CLASS_URGENT);
}
group.remove (noti);
if (only_single_notification ()) {
set_expanded (false);
on_expand_change (false);
}
}
public List<weak Gtk.Widget> get_notifications () {
return group.widgets.copy ();
}
public unowned Notification ? get_latest_notification () {
return (Notification ?) group.widgets.last ().data;
}
public int64 get_time () {
if (group.widgets.is_empty ()) return -1;
return ((Notification) group.widgets.last ().data).param.time;
}
public bool get_is_urgent () {
return urgent_notifications.length > 0;
}
public uint get_num_notifications () {
return group.widgets.length ();
}
public bool is_empty () {
return group.widgets.is_empty ();
}
public void close_all_notifications () {
urgent_notifications.remove_all ();
foreach (unowned Gtk.Widget widget in group.widgets) {
var noti = (Notification) widget;
if (noti != null) noti.close_notification (false);
}
}
public void update () {
if (!is_empty ()) {
unowned Notification first = (Notification) group.widgets.first ().data;
unowned NotifyParams param = first.param;
// Get the app icon
Icon ? icon = null;
if (param.desktop_app_info != null
&& (icon = param.desktop_app_info.get_icon ()) != null) {
app_icon.set_from_gicon (icon, Gtk.IconSize.LARGE_TOOLBAR);
app_icon.show ();
} else {
app_icon.set_from_icon_name ("application-x-executable-symbolic",
Gtk.IconSize.LARGE_TOOLBAR);
// app_icon.hide ();
}
} else {
// app_icon.hide ();
}
foreach (unowned Gtk.Widget widget in group.widgets) {
var noti = (Notification) widget;
if (noti != null) noti.set_time ();
}
}
public int get_relative_y (Gtk.Widget parent) {
int dest_y;
translate_coordinates (parent, 0, 0, null, out dest_y);
return dest_y;
}
}
private class ExpandableGroup : Gtk.Container {
const int NUM_STACKED_NOTIFICATIONS = 3;
const int COLLAPSED_NOTIFICATION_OFFSET = 8;
public bool is_expanded { get; private set; default = true; }
private double animation_progress = 1.0;
private double animation_progress_inv {
get {
return (1 - animation_progress);
}
}
private Animation ? animation;
private unowned on_expand_change change_cb;
public List<unowned Gtk.Widget> widgets = new List<unowned Gtk.Widget> ();
public delegate void on_expand_change (bool state);
public ExpandableGroup (uint animation_duration, on_expand_change change_cb) {
base.set_has_window (false);
base.set_can_focus (true);
base.set_redraw_on_allocate (false);
this.change_cb = change_cb;
animation = new Animation (this, animation_duration,
Animation.ease_in_out_cubic,
animation_value_cb,
animation_done_cb);
this.show ();
set_expanded (false);
}
public void set_expanded (bool value) {
if (is_expanded == value) return;
is_expanded = value;
animate (is_expanded ? 1 : 0);
this.queue_resize ();
change_cb (is_expanded);
}
public override void add (Gtk.Widget widget) {
widget.set_parent (this);
widgets.append (widget);
}
public override void remove (Gtk.Widget widget) {
widget.unparent ();
widgets.remove (widget);
if (this.get_visible () && widget.get_visible ()) {
this.queue_resize_no_redraw ();
}
}
public override void forall_internal (bool include_internals, Gtk.Callback callback) {
foreach (unowned Gtk.Widget widget in widgets) {
callback (widget);
}
}
public override Gtk.SizeRequestMode get_request_mode () {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
public override void size_allocate (Gtk.Allocation allocation) {
base.size_allocate (allocation);
int length = (int) widgets.length ();
if (length == 0) return;
uint border_width = get_border_width ();
Gtk.Allocation prev_allocation = Gtk.Allocation ();
prev_allocation.y = allocation.y;
// The height of the most recent notification
unowned Gtk.Widget last = widgets.last ().data;
int target_height = 0;
last.get_preferred_height_for_width (allocation.width,
out target_height, null);
for (int i = length - 1; i >= 0; i--) {
unowned Gtk.Widget widget = widgets.nth_data (i);
if (widget != null && widget.get_visible ()) {
int height;
widget.get_preferred_height_for_width (allocation.width,
out height, null);
Gtk.Allocation alloc = Gtk.Allocation ();
alloc.x = allocation.x + (int) border_width;
alloc.y = (int) (prev_allocation.y +
animation_progress * prev_allocation.height +
border_width);
alloc.width = allocation.width - 2 * (int) border_width;
alloc.height = height;
// Expand smaller stacked notifications to the expected height
// But only when the animation has finished
if (target_height > height && !is_expanded && animation_progress == 0) {
alloc.height = target_height;
}
alloc.height -= 2 * (int) border_width;
// Add the collapsed offset to only stacked notifications.
// Excludes notifications index > NUM_STACKED_NOTIFICATIONS
if (i < length - 1 && length - 1 - i < NUM_STACKED_NOTIFICATIONS) {
alloc.y += (int) (animation_progress_inv * COLLAPSED_NOTIFICATION_OFFSET);
}
prev_allocation = alloc;
widget.size_allocate (alloc);
if (get_realized ()) {
widget.show ();
}
}
if (get_realized ()) {
widget.set_child_visible (true);
}
}
}
public override void get_preferred_height_for_width (int width,
out int minimum_height,
out int natural_height) {
minimum_height = 0;
natural_height = 0;
foreach (unowned Gtk.Widget widget in widgets) {
if (widget != null && widget.get_visible ()) {
int widget_minimum_height = 0;
int widget_natural_height = 0;
widget.get_preferred_height_for_width (width,
out widget_minimum_height,
out widget_natural_height);
minimum_height += widget_minimum_height;
natural_height += widget_natural_height;
}
}
int target_minimum_height;
int target_natural_height;
get_height_for_latest_notifications (width,
out target_minimum_height,
out target_natural_height);
minimum_height = (int) Animation.lerp (minimum_height,
target_minimum_height,
animation_progress_inv);
natural_height = (int) Animation.lerp (natural_height,
target_natural_height,
animation_progress_inv);
}
public override bool draw (Cairo.Context cr) {
int length = (int) widgets.length ();
if (length == 0) return true;
Gtk.Allocation alloc;
get_allocated_size (out alloc, null);
unowned Gtk.Widget latest = widgets.nth_data (length - 1);
Gtk.Allocation latest_alloc;
latest.get_allocated_size (out latest_alloc, null);
Cairo.Pattern hover_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
hover_gradient.add_color_stop_rgba (0, 1, 1, 1, 1);
hover_gradient.add_color_stop_rgba (1, 1, 1, 1, 1);
// Fades from the bottom at 0.5 -> top at 0.0 opacity
Cairo.Pattern fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
fade_gradient.add_color_stop_rgba (0, 1, 1, 1, animation_progress_inv);
fade_gradient.add_color_stop_rgba (1, 1, 1, 1, animation_progress_inv - 0.5);
// Cross-fades in the non visible stacked notifications when expanded
Cairo.Pattern cross_fade_pattern =
new Cairo.Pattern.rgba (1, 1, 1, 1.5 * animation_progress_inv);
int width = alloc.width;
for (int i = 0; i < length; i++) {
// Skip drawing excess notifications
if (!is_expanded &&
animation_progress == 0 &&
i < length - NUM_STACKED_NOTIFICATIONS) {
continue;
}
unowned Gtk.Widget widget = widgets.nth_data (i);
int preferred_height;
widget.get_preferred_height_for_width (width,
out preferred_height, null);
Gtk.Allocation widget_alloc;
widget.get_allocated_size (out widget_alloc, null);
int height_diff = latest_alloc.height - widget_alloc.height;
cr.save ();
// Translate to the widgets allocated y
double translate_y = widget_alloc.y - alloc.y;
// Move down even more if the height is larger than the latest
// in the stack (helps with only rendering the bottom portion)
translate_y += height_diff * animation_progress_inv;
cr.translate (0, translate_y);
// Scale down lower notifications in the stack
if (i + 1 != length) {
double scale = double.min (
animation_progress + Math.pow (0.95, length - 1 - i), 1);
// Moves the scaled notification to the center of X and bottom y
cr.translate ((widget_alloc.width - width * scale) * 0.5,
widget_alloc.height * (1 - scale));
cr.scale (scale, scale);
}
int lerped_y = (int) Animation.lerp (-height_diff, 0, animation_progress);
int lerped_height = (int) Animation.lerp (latest_alloc.height,
widget_alloc.height,
animation_progress);
// Clip to the size of the latest notification
// (fixes issue where a larger bottom notification would
// be visible above)
cr.rectangle (0, lerped_y, width, lerped_height);
cr.clip ();
// Draw patterns on the notification
cr.push_group ();
widget.draw (cr);
if (i + 1 != length) {
// Draw Fade Gradient
cr.save ();
cr.translate (0, lerped_y);
cr.scale (1, lerped_height * 0.75);
cr.set_source (fade_gradient);
cr.rectangle (0, 0, width, lerped_height * 0.75);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
}
// Draw notification cross-fade
if (i < length - NUM_STACKED_NOTIFICATIONS) {
cr.save ();
cr.translate (0, lerped_y);
cr.scale (1, lerped_height);
cr.set_source (cross_fade_pattern);
cr.rectangle (0, 0, width, lerped_height);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
}
cr.pop_group_to_source ();
cr.paint ();
cr.restore ();
}
return true;
}
/** Gets the collapsed height (first notification + stacked) */
private void get_height_for_latest_notifications (int width,
out int minimum_height,
out int natural_height) {
minimum_height = 0;
natural_height = 0;
uint length = widgets.length ();
if (length == 0) return;
int offset = 0;
for (uint i = 1;
i < length && i < NUM_STACKED_NOTIFICATIONS;
i++) {
offset += COLLAPSED_NOTIFICATION_OFFSET;
}
unowned Gtk.Widget last = widgets.last ().data;
last.get_preferred_height_for_width (width,
out minimum_height,
out natural_height);
minimum_height += offset;
natural_height += offset;
}
void animation_value_cb (double progress) {
this.animation_progress = progress;
this.queue_resize ();
}
void animation_done_cb () {
animation.dispose ();
this.queue_allocate ();
}
void animate (double to) {
animation.stop ();
animation.start (animation_progress, to);
}
}
}

View File

@@ -17,6 +17,38 @@
@define-color bg-selected rgb(0, 128, 255);
.notification-group {
}
/* Set lower stacked notifications opacity to 0 */
.notification-group.collapsed
.notification-row:not(:last-child)
.notification-action
*,
.notification-group.collapsed
.notification-row:not(:last-child)
.notification-default-action
* {
transition: opacity 400ms ease-in-out;
opacity: 0;
}
.notification-group-buttons,
.notification-group-headers {
margin: 0 16px;
color: @text-color;
}
.notification-group-icon {
}
.notification-group > widget {
}
.notification-group.collapsed:hover .notification-default-action,
.notification-group.collapsed:hover .notification-action {
background-color: @noti-bg-hover;
}
.notification-row {
outline: none;
}
@@ -35,19 +67,37 @@
}
/* Uncomment to enable specific urgency colors
.low {
.notification.low {
background: yellow;
padding: 6px;
border-radius: 12px;
}
.normal {
.notification.normal {
background: green;
padding: 6px;
border-radius: 12px;
}
.critical {
.notification.critical {
background: red;
padding: 6px;
border-radius: 12px;
}
.notification-group.low {
background: yellow;
padding: 6px;
border-radius: 12px;
}
.notification-group.normal {
background: green;
padding: 6px;
border-radius: 12px;
}
.notification-group.critical {
background: red;
padding: 6px;
border-radius: 12px;
@@ -151,6 +201,7 @@
}
.image {
-gtk-icon-effect: none;
}
.body-image {