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
|
## Features
|
||||||
|
|
||||||
|
- Grouped notifications
|
||||||
- Keyboard shortcuts
|
- Keyboard shortcuts
|
||||||
- Notification body markup with image support
|
- Notification body markup with image support
|
||||||
- Inline replies
|
- 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')
|
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')
|
|||||||
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')
|
||||||
|
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 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 @@
|
|||||||
<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>
|
||||||
|
@@ -1,20 +1,30 @@
|
|||||||
namespace SwayNotificationCenter {
|
namespace SwayNotificationCenter {
|
||||||
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
|
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
|
||||||
public class ControlCenter : Gtk.ApplicationWindow {
|
public class ControlCenter : Gtk.ApplicationWindow {
|
||||||
|
|
||||||
[GtkChild]
|
[GtkChild]
|
||||||
unowned Gtk.Box notifications_box;
|
unowned Gtk.Box notifications_box;
|
||||||
[GtkChild]
|
[GtkChild]
|
||||||
unowned Gtk.ScrolledWindow scrolled_window;
|
|
||||||
[GtkChild]
|
|
||||||
unowned Gtk.Viewport viewport;
|
|
||||||
[GtkChild]
|
|
||||||
unowned Gtk.Stack stack;
|
unowned Gtk.Stack stack;
|
||||||
[GtkChild]
|
[GtkChild]
|
||||||
unowned Gtk.ListBox list_box;
|
unowned Gtk.ScrolledWindow scrolled_window;
|
||||||
|
FadedViewport viewport = new FadedViewport (20);
|
||||||
|
Gtk.ListBox list_box = new Gtk.ListBox ();
|
||||||
|
|
||||||
[GtkChild]
|
[GtkChild]
|
||||||
unowned Gtk.Box box;
|
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_NOTIFICATIONS_PAGE = "notifications-list";
|
||||||
const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
|
const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
|
||||||
|
|
||||||
@@ -25,9 +35,8 @@ namespace SwayNotificationCenter {
|
|||||||
private SwayncDaemon swaync_daemon;
|
private SwayncDaemon swaync_daemon;
|
||||||
private NotiDaemon noti_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 bool list_reverse = false;
|
||||||
private Gtk.Align list_align = Gtk.Align.START;
|
private Gtk.Align list_align = Gtk.Align.START;
|
||||||
|
|
||||||
@@ -40,6 +49,18 @@ namespace SwayNotificationCenter {
|
|||||||
|
|
||||||
this.swaync_daemon.reloading_css.connect (reload_notifications_style);
|
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 (swaync_daemon.use_layer_shell) {
|
||||||
if (!GtkLayerShell.is_supported ()) {
|
if (!GtkLayerShell.is_supported ()) {
|
||||||
stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
|
stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
|
||||||
@@ -56,8 +77,6 @@ namespace SwayNotificationCenter {
|
|||||||
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
|
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport.size_allocate.connect (size_alloc);
|
|
||||||
|
|
||||||
this.map.connect (() => {
|
this.map.connect (() => {
|
||||||
set_anchor ();
|
set_anchor ();
|
||||||
// Wait until the layer has attached
|
// Wait until the layer has attached
|
||||||
@@ -154,71 +173,7 @@ namespace SwayNotificationCenter {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.key_press_event.connect ((w, event_key) => {
|
key_press_event.connect (key_press_event_cb);
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
|
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
|
||||||
// Switches the stack page depending on the amount of notifications
|
// Switches the stack page depending on the amount of notifications
|
||||||
@@ -232,6 +187,171 @@ namespace SwayNotificationCenter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
add_widgets ();
|
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 */
|
/** Adds all custom widgets. Removes previous widgets */
|
||||||
@@ -358,28 +478,37 @@ namespace SwayNotificationCenter {
|
|||||||
box.set_valign (align_y);
|
box.set_valign (align_y);
|
||||||
|
|
||||||
list_box.set_valign (list_align);
|
list_box.set_valign (list_align);
|
||||||
list_box.set_sort_func ((w1, w2) => {
|
list_box.set_sort_func (list_box_sort_func);
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always set the size request in all events.
|
// Always set the size request in all events.
|
||||||
box.set_size_request (ConfigModel.instance.control_center_width,
|
box.set_size_request (ConfigModel.instance.control_center_width,
|
||||||
ConfigModel.instance.control_center_height);
|
ConfigModel.instance.control_center_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void size_alloc () {
|
/**
|
||||||
var adj = viewport.vadjustment;
|
* Returns < 0 if row1 should be before row2, 0 if they are equal
|
||||||
double upper = adj.get_upper ();
|
* and > 0 otherwise
|
||||||
if (last_upper < upper) {
|
*/
|
||||||
scroll_to_start (list_reverse);
|
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) {
|
private void scroll_to_start (bool reverse) {
|
||||||
@@ -391,20 +520,26 @@ namespace SwayNotificationCenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public uint notification_count () {
|
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 () {
|
public void close_all_notifications () {
|
||||||
foreach (var w in list_box.get_children ()) {
|
foreach (var w in list_box.get_children ()) {
|
||||||
Notification noti = (Notification) w;
|
NotificationGroup group = (NotificationGroup) w;
|
||||||
if (noti != null) noti.close_notification (false);
|
if (group != null) group.close_all_notifications ();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
swaync_daemon.subscribe_v2 (notification_count (),
|
swaync_daemon.subscribe_v2 (notification_count (),
|
||||||
swaync_daemon.get_dnd (),
|
swaync_daemon.get_dnd (),
|
||||||
get_visibility (),
|
get_visibility (),
|
||||||
swaync_daemon.inhibited);
|
swaync_daemon.inhibited);
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
stderr.printf (e.message + "\n");
|
stderr.printf (e.message + "\n");
|
||||||
}
|
}
|
||||||
@@ -414,11 +549,20 @@ namespace SwayNotificationCenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void navigate_list (uint i) {
|
private void navigate_list (int i) {
|
||||||
var widget = list_box.get_children ().nth_data (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) {
|
if (widget != null) {
|
||||||
list_box.set_focus_child (widget);
|
|
||||||
widget.grab_focus ();
|
widget.grab_focus ();
|
||||||
|
list_box.set_focus_child (widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,20 +575,20 @@ namespace SwayNotificationCenter {
|
|||||||
if (this.visible) {
|
if (this.visible) {
|
||||||
// Focus the first notification
|
// Focus the first notification
|
||||||
list_position = list_reverse ?
|
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;
|
if (list_position == uint.MAX) list_position = 0;
|
||||||
|
|
||||||
list_box.grab_focus ();
|
list_box.grab_focus ();
|
||||||
navigate_list (list_position);
|
navigate_list (list_position);
|
||||||
foreach (var w in list_box.get_children ()) {
|
foreach (var w in list_box.get_children ()) {
|
||||||
var noti = (Notification) w;
|
var group = (NotificationGroup) w;
|
||||||
if (noti != null) noti.set_time ();
|
if (group != null) group.update ();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
swaync_daemon.subscribe_v2 (notification_count (),
|
swaync_daemon.subscribe_v2 (notification_count (),
|
||||||
noti_daemon.dnd,
|
noti_daemon.dnd,
|
||||||
this.visible,
|
this.visible,
|
||||||
swaync_daemon.inhibited);
|
swaync_daemon.inhibited);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool toggle_visibility () {
|
public bool toggle_visibility () {
|
||||||
@@ -463,7 +607,9 @@ namespace SwayNotificationCenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void close_notification (uint32 id, bool dismiss) {
|
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;
|
var noti = (Notification) w;
|
||||||
if (noti != null && noti.param.applied_id == id) {
|
if (noti != null && noti.param.applied_id == id) {
|
||||||
if (!dismiss) {
|
if (!dismiss) {
|
||||||
@@ -471,21 +617,37 @@ namespace SwayNotificationCenter {
|
|||||||
noti.destroy ();
|
noti.destroy ();
|
||||||
} else {
|
} else {
|
||||||
noti.close_notification (false);
|
noti.close_notification (false);
|
||||||
list_box.remove (w);
|
group.remove_notification (noti);
|
||||||
}
|
}
|
||||||
|
noti_groups_id.remove (id);
|
||||||
break;
|
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) {
|
public void replace_notification (uint32 id, NotifyParams new_params) {
|
||||||
foreach (var w in list_box.get_children ()) {
|
unowned NotificationGroup group = null;
|
||||||
var noti = (Notification) w;
|
if (noti_groups_id.lookup_extended (id, null, out group)) {
|
||||||
if (noti != null && noti.param.applied_id == id) {
|
foreach (var w in group.get_notifications ()) {
|
||||||
noti.replace_notification (new_params);
|
var noti = (Notification) w;
|
||||||
// Position the notification in the beginning of the list
|
if (noti != null && noti.param.applied_id == id) {
|
||||||
list_box.invalidate_sort ();
|
noti_groups_id.remove (id);
|
||||||
return;
|
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,
|
noti_daemon,
|
||||||
NotificationType.CONTROL_CENTER);
|
NotificationType.CONTROL_CENTER);
|
||||||
noti.grab_focus.connect ((w) => {
|
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) {
|
if (list_position != uint.MAX && list_position != i) {
|
||||||
list_position = i;
|
list_position = i;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
noti.set_time ();
|
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);
|
scroll_to_start (list_reverse);
|
||||||
try {
|
try {
|
||||||
swaync_daemon.subscribe_v2 (notification_count (),
|
swaync_daemon.subscribe_v2 (notification_count (),
|
||||||
swaync_daemon.get_dnd (),
|
swaync_daemon.get_dnd (),
|
||||||
get_visibility (),
|
get_visibility (),
|
||||||
swaync_daemon.inhibited);
|
swaync_daemon.inhibited);
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
stderr.printf (e.message + "\n");
|
stderr.printf (e.message + "\n");
|
||||||
}
|
}
|
||||||
@@ -527,8 +730,13 @@ namespace SwayNotificationCenter {
|
|||||||
/** Forces each notification EventBox to reload its style_context #27 */
|
/** Forces each notification EventBox to reload its style_context #27 */
|
||||||
private void reload_notifications_style () {
|
private void reload_notifications_style () {
|
||||||
foreach (var c in list_box.get_children ()) {
|
foreach (var c in list_box.get_children ()) {
|
||||||
Notification noti = (Notification) c;
|
NotificationGroup group = (NotificationGroup) c;
|
||||||
if (noti != null) noti.reload_style_context ();
|
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 () {
|
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 = [
|
|||||||
|
|
||||||
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 = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 @@
|
|||||||
<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 {
|
|||||||
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
|
||||||
|
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);
|
@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 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 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 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
|
-gtk-icon-effect: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-image {
|
.body-image {
|
||||||
|
Reference in New Issue
Block a user