Unverified Commit 86166a46 authored by Erik Reider's avatar Erik Reider Committed by GitHub

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
parent 305ebf87
...@@ -57,6 +57,7 @@ Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotif ...@@ -57,6 +57,7 @@ Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotif
## Features ## Features
- Grouped notifications
- Keyboard shortcuts - Keyboard shortcuts
- Notification body markup with image support - Notification body markup with image support
- Inline replies - Inline replies
......
app_resources += gnome.compile_resources('icon-resources',
'swaync_icons.gresource.xml',
c_name: 'sway_notification_center_icons'
)
<?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>
<?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>
<?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', ...@@ -2,6 +2,8 @@ install_data('org.erikreider.swaync.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
) )
subdir('icons')
compile_schemas = find_program('glib-compile-schemas', required: false) compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found() if compile_schemas.found()
test('Validate schema file', compile_schemas, test('Validate schema file', compile_schemas,
......
...@@ -9,6 +9,9 @@ add_project_arguments(['--enable-gobject-tracing'], language: 'vala') ...@@ -9,6 +9,9 @@ add_project_arguments(['--enable-gobject-tracing'], language: 'vala')
add_project_arguments(['--enable-checking'], language: 'vala') add_project_arguments(['--enable-checking'], language: 'vala')
i18n = import('i18n') i18n = import('i18n')
gnome = import('gnome')
app_resources = []
subdir('data') subdir('data')
subdir('src') subdir('src')
......
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);
}
}
}
public class Constants { public class Constants {
public const string VERSION = @VERSION@; public const string VERSION = @VERSION@;
public const string VERSIONNUM = @VERSION_NUM@; public const string VERSIONNUM = @VERSION_NUM@;
public const uint ANIMATION_DURATION = 400;
} }
...@@ -35,24 +35,7 @@ ...@@ -35,24 +35,7 @@
<property name="can-focus">True</property> <property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property> <property name="hscrollbar-policy">never</property>
<child> <child>
<object class="GtkViewport" id="viewport"> <placeholder/>
<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>
</child> </child>
</object> </object>
<packing> <packing>
......
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 { ...@@ -8,6 +8,10 @@ namespace SwayNotificationCenter {
public static void init () { public static void init () {
system_css_provider = new Gtk.CssProvider (); system_css_provider = new Gtk.CssProvider ();
user_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, public static void set_image_path (owned string path,
......
...@@ -50,13 +50,16 @@ widget_sources = [ ...@@ -50,13 +50,16 @@ widget_sources = [
app_sources = [ app_sources = [
'main.vala', 'main.vala',
'animation/animation.vala',
'orderedHashTable/orderedHashTable.vala', 'orderedHashTable/orderedHashTable.vala',
'configModel/configModel.vala', 'configModel/configModel.vala',
'swayncDaemon/swayncDaemon.vala', 'swayncDaemon/swayncDaemon.vala',
'notiDaemon/notiDaemon.vala', 'notiDaemon/notiDaemon.vala',
'notiModel/notiModel.vala', 'notiModel/notiModel.vala',
'fadedViewport/fadedViewport.vala',
'notificationWindow/notificationWindow.vala', 'notificationWindow/notificationWindow.vala',
'notification/notification.vala', 'notification/notification.vala',
'notificationGroup/notificationGroup.vala',
'controlCenter/controlCenter.vala', 'controlCenter/controlCenter.vala',
widget_sources, widget_sources,
'blankWindow/blankWindow.vala', 'blankWindow/blankWindow.vala',
...@@ -119,15 +122,14 @@ args = [ ...@@ -119,15 +122,14 @@ args = [
] ]
sysconfdir = get_option('sysconfdir') 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', 'sway_notification_center.gresource.xml',
c_name: 'sway_notification_center' c_name: 'sway_notification_center'
) )
executable('swaync', executable('swaync',
app_sources, [ app_sources, app_resources ],
vala_args: args, vala_args: args,
dependencies: app_deps, dependencies: app_deps,
install: true, install: true,
......
...@@ -115,6 +115,12 @@ namespace SwayNotificationCenter { ...@@ -115,6 +115,12 @@ namespace SwayNotificationCenter {
public Array<Action> actions { get; set; } 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, public NotifyParams (uint32 applied_id,
string app_name, string app_name,
uint32 replaces_id, uint32 replaces_id,
...@@ -139,6 +145,32 @@ namespace SwayNotificationCenter { ...@@ -139,6 +145,32 @@ namespace SwayNotificationCenter {
parse_hints (); parse_hints ();
parse_actions (actions); 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 () { private void parse_hints () {
......
...@@ -281,7 +281,7 @@ ...@@ -281,7 +281,7 @@
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="icon-name">window-close-symbolic</property> <property name="icon-name">swaync-close-symbolic</property>
</object> </object>
</child> </child>
<style> <style>
......
...@@ -689,17 +689,8 @@ namespace SwayNotificationCenter { ...@@ -689,17 +689,8 @@ namespace SwayNotificationCenter {
if (img.storage_type == Gtk.ImageType.EMPTY) { if (img.storage_type == Gtk.ImageType.EMPTY) {
// Get the app icon // Get the app icon
Icon ? icon = null; Icon ? icon = null;
if (param.desktop_entry != null) { if (param.desktop_app_info != null
string entry = param.desktop_entry; && (icon = param.desktop_app_info.get_icon ()) != null) {
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) {
img.set_from_gicon (icon, icon_size); img.set_from_gicon (icon, icon_size);
} else if (image_visibility == ImageVisibility.ALWAYS) { } else if (image_visibility == ImageVisibility.ALWAYS) {
// Default icon // Default icon
......
...@@ -17,6 +17,38 @@ ...@@ -17,6 +17,38 @@
@define-color bg-selected rgb(0, 128, 255); @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 { .notification-row {
outline: none; outline: none;
} }
...@@ -35,19 +67,37 @@ ...@@ -35,19 +67,37 @@
} }
/* Uncomment to enable specific urgency colors /* Uncomment to enable specific urgency colors
.low { .notification.low {
background: yellow; background: yellow;
padding: 6px; padding: 6px;
border-radius: 12px; border-radius: 12px;
} }
.normal { .notification.normal {
background: green; background: green;
padding: 6px; padding: 6px;
border-radius: 12px; 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; background: red;
padding: 6px; padding: 6px;
border-radius: 12px; border-radius: 12px;
...@@ -151,6 +201,7 @@ ...@@ -151,6 +201,7 @@
} }
.image { .image {
-gtk-icon-effect: none;
} }
.body-image { .body-image {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment