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:
@@ -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
4
data/icons/meson.build
Normal file
@@ -0,0 +1,4 @@
|
||||
app_resources += gnome.compile_resources('icon-resources',
|
||||
'swaync_icons.gresource.xml',
|
||||
c_name: 'sway_notification_center_icons'
|
||||
)
|
4
data/icons/swaync-close-symbolic.svg
Normal file
4
data/icons/swaync-close-symbolic.svg
Normal 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 |
5
data/icons/swaync-collapse-symbolic.svg
Normal file
5
data/icons/swaync-collapse-symbolic.svg
Normal 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 |
7
data/icons/swaync_icons.gresource.xml
Normal file
7
data/icons/swaync_icons.gresource.xml
Normal 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>
|
@@ -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,
|
||||
|
@@ -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')
|
||||
|
150
src/animation/animation.vala
Normal file
150
src/animation/animation.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
public class Constants {
|
||||
public const string VERSION = @VERSION@;
|
||||
public const string VERSIONNUM = @VERSION_NUM@;
|
||||
public const uint ANIMATION_DURATION = 400;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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 ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
157
src/fadedViewport/fadedViewport.vala
Normal file
157
src/fadedViewport/fadedViewport.vala
Normal 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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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 () {
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
580
src/notificationGroup/notificationGroup.vala
Normal file
580
src/notificationGroup/notificationGroup.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user