diff --git a/.gitignore b/.gitignore
index da59f957..93f1fd9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ SwayNotificationCenter
./src/swaync
./src/swaync*
swaync-git*
+subprojects
diff --git a/data/icons/meson.build b/data/icons/meson.build
new file mode 100644
index 00000000..0f93645b
--- /dev/null
+++ b/data/icons/meson.build
@@ -0,0 +1,4 @@
+app_resources += gnome.compile_resources('icon-resources',
+ 'swaync_icons.gresource.xml',
+ c_name: 'sway_notification_center_icons'
+)
diff --git a/data/icons/notifications-close-symbolic.svg b/data/icons/notifications-close-symbolic.svg
new file mode 100644
index 00000000..f68a861c
--- /dev/null
+++ b/data/icons/notifications-close-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/data/icons/notifications-placeholder-symbolic.svg b/data/icons/notifications-placeholder-symbolic.svg
new file mode 100644
index 00000000..bde5de16
--- /dev/null
+++ b/data/icons/notifications-placeholder-symbolic.svg
@@ -0,0 +1,10 @@
+
+
diff --git a/data/icons/swaync_icons.gresource.xml b/data/icons/swaync_icons.gresource.xml
new file mode 100644
index 00000000..9a9aabb0
--- /dev/null
+++ b/data/icons/swaync_icons.gresource.xml
@@ -0,0 +1,7 @@
+
+
+
+ notifications-placeholder-symbolic.svg
+ notifications-close-symbolic.svg
+
+
diff --git a/data/meson.build b/data/meson.build
index 01f1eb72..699cddcc 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -2,6 +2,8 @@ install_data('org.erikreider.swaync.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
+subdir('icons')
+
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
diff --git a/man/swaync.5.scd b/man/swaync.5.scd
index ff92f3e9..ab2dd128 100644
--- a/man/swaync.5.scd
+++ b/man/swaync.5.scd
@@ -304,24 +304,24 @@ config file to be able to detect config errors
widget-mpris-player ++
widget-mpris-title ++
widget-mpris-subtitle ++
+ widget-mpris-album-art ++
properties: ++
image-size: ++
type: integer ++
optional: true ++
default: 96 ++
description: The size of the album art. ++
- image-radius: ++
- type: integer ++
- optional: true ++
- default: 12 ++
- description: The border radius of the album art. ++
description: A widget that displays multiple music players. ++
*menubar*++
type: object ++
css classes: ++
widget-menubar ++
- .widget-menubar>box>.menu-button-bar ++
- name of element given after menu or buttons with # ++
+ widget-menubar-container (with additional classes "start" and "end") ++
+ widget-menubar-child (direct child of the container) ++
+ widget-menubar-buttons (group of buttons, "buttons" widget) ++
+ widget-menubar-button (each individual button) ++
+ widget-menubar-menu (the animated menu for the "menu" widget) ++
+ Name of individual buttons, ex: "button#power" would be "power" ++
patternProperties: ++
menu#: ++
type: object ++
@@ -391,7 +391,9 @@ config file to be able to detect config errors
description: A list of buttons to be displayed in the menu-button-bar ++
*buttons-grid*++
type: object ++
- css class: widget-buttons (access buttons with >flowbox>flowboxchild>button) ++
+ css class: ++
+ widget-buttons ++
+ widget-buttons-grid-button ++
properties: ++
actions: ++
type: array ++
@@ -545,16 +547,16 @@ config file to be able to detect config errors
},
...
]
+ },
+ "buttons": {
+ "actions": [
+ {
+ "label": "wifi",
+ "command": "rofi-wifi-menu"
+ },
+ ...
+ ]
}
- },
- "buttons": {
- "actions": [
- {
- "label": "wifi",
- "command": "rofi-wifi-menu"
- },
- ...
- ]
}
}
}
diff --git a/meson.build b/meson.build
index 2e5b85f8..a7a2a6fa 100644
--- a/meson.build
+++ b/meson.build
@@ -9,6 +9,9 @@ add_project_arguments(['--enable-gobject-tracing'], language: 'vala')
add_project_arguments(['--enable-checking'], language: 'vala')
i18n = import('i18n')
+gnome = import('gnome')
+
+app_resources = []
subdir('data')
subdir('src')
diff --git a/src/blankWindow/blankWindow.vala b/src/blankWindow/blankWindow.vala
deleted file mode 100644
index cd533a76..00000000
--- a/src/blankWindow/blankWindow.vala
+++ /dev/null
@@ -1,72 +0,0 @@
-namespace SwayNotificationCenter {
- public class BlankWindow : Gtk.Window {
- public unowned Gdk.Display display { get; private set; }
- public unowned Gdk.Monitor monitor { get; private set; }
- unowned SwayncDaemon daemon;
-
- Gtk.Button button;
-
- public BlankWindow (Gdk.Display disp,
- Gdk.Monitor mon,
- SwayncDaemon dae) {
- display = disp;
- monitor = mon;
- daemon = dae;
-
- // Use button click event instead of Window button_press_event due
- // to Gtk layer shell bug. This would grab focus instead of ControlCenter
- button = new Gtk.Button () {
- expand = true,
- opacity = 0,
- relief = Gtk.ReliefStyle.NONE,
- visible = true,
- };
- button.clicked.connect (() => {
- try {
- daemon.set_visibility (false);
- } catch (Error e) {
- stderr.printf ("BlankWindow Click Error: %s\n", e.message);
- }
- });
- add (button);
-
- if (!GtkLayerShell.is_supported ()) {
- stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
- stderr.printf ("Swaync only works on Wayland!\n");
- stderr.printf ("If running waylans session, try running:\n");
- stderr.printf ("\tGDK_BACKEND=wayland swaync\n");
- Process.exit (1);
- }
- GtkLayerShell.init_for_window (this);
- GtkLayerShell.set_namespace (this, "swaync-control-center");
- GtkLayerShell.set_monitor (this, monitor);
-
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.TOP, true);
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.LEFT, true);
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.RIGHT, true);
-
- GtkLayerShell.set_exclusive_zone (this, -1);
-
- GtkLayerShell.Layer layer;
- switch (ConfigModel.instance.control_center_layer) {
- case Layer.BACKGROUND:
- layer = GtkLayerShell.Layer.BACKGROUND;
- break;
- case Layer.BOTTOM:
- layer = GtkLayerShell.Layer.BOTTOM;
- break;
- case Layer.TOP:
- layer = GtkLayerShell.Layer.TOP;
- break;
- default:
- case Layer.OVERLAY:
- layer = GtkLayerShell.Layer.OVERLAY;
- break;
- }
- GtkLayerShell.set_layer (this, layer);
-
- get_style_context ().add_class ("blank-window");
- }
- }
-}
diff --git a/src/config.json.in b/src/config.json.in
index 99fb9cbc..c60b54a0 100644
--- a/src/config.json.in
+++ b/src/config.json.in
@@ -71,8 +71,7 @@
"text": "Label Text"
},
"mpris": {
- "image-size": 96,
- "image-radius": 12
+ "image-size": 96
}
}
}
diff --git a/src/configSchema.json b/src/configSchema.json
index 448a9ccb..f6bb543b 100644
--- a/src/configSchema.json
+++ b/src/configSchema.json
@@ -389,11 +389,6 @@
"type": "integer",
"description": "The size of the album art",
"default": 96
- },
- "image-radius": {
- "type": "integer",
- "description": "The border radius of the album art",
- "default": 12
}
}
},
diff --git a/src/controlCenter/controlCenter.ui b/src/controlCenter/controlCenter.ui
deleted file mode 100644
index 6f4dd749..00000000
--- a/src/controlCenter/controlCenter.ui
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
-
-
- control-center-window
- False
- False
- True
- True
- True
- False
- False
- north-east
- False
-
-
-
-
-
-
diff --git a/src/controlCenter/controlCenter.vala b/src/controlCenter/controlCenter.vala
index fa1c6fbe..7d8d830a 100644
--- a/src/controlCenter/controlCenter.vala
+++ b/src/controlCenter/controlCenter.vala
@@ -1,231 +1,135 @@
namespace SwayNotificationCenter {
- [GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
- public class ControlCenter : Gtk.ApplicationWindow {
+ public class ControlCenter : BlankWindow {
+ // TODO: Replace with Gtk ListView?
+ IterBox widgets_box = new IterBox (Gtk.Orientation.VERTICAL, 0);
- [GtkChild]
- unowned Gtk.ScrolledWindow scrolled_window;
- [GtkChild]
- unowned Gtk.Viewport viewport;
- [GtkChild]
- unowned Gtk.Stack stack;
- [GtkChild]
- unowned Gtk.ListBox list_box;
- [GtkChild]
- unowned Gtk.Box box;
+ private Gtk.EventControllerKey event_kb;
- const string STACK_NOTIFICATIONS_PAGE = "notifications-list";
- const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
+ private unowned NotiDaemon noti_daemon;
- private Gtk.GestureMultiPress blank_window_gesture;
- private bool blank_window_down = false;
- private bool blank_window_in = false;
-
- private SwayncDaemon swaync_daemon;
- private NotiDaemon noti_daemon;
-
- private uint list_position = 0;
-
- private double last_upper = 0;
- private bool list_reverse = false;
- private Gtk.Align list_align = Gtk.Align.START;
-
- private Array widgets = new Array ();
+ private Widgets.Notifications notification_widget
+ = new Widgets.Notifications (NotificationType.CONTROL_CENTER);
private const string[] DEFAULT_WIDGETS = { "title", "dnd", "notifications" };
public ControlCenter (SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
- this.swaync_daemon = swaync_daemon;
+ base (swaync_daemon);
this.noti_daemon = noti_daemon;
- this.swaync_daemon.reloading_css.connect (reload_notifications_style);
+ // Setup window
+ this.vexpand = true;
+ this.halign = Gtk.Align.FILL;
+ this.valign = Gtk.Align.FILL;
- if (swaync_daemon.use_layer_shell) {
- if (!GtkLayerShell.is_supported ()) {
- stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
- stderr.printf ("Swaync only works on Wayland!\n");
- stderr.printf ("If running waylans session, try running:\n");
- stderr.printf ("\tGDK_BACKEND=wayland swaync\n");
- Process.exit (1);
- }
- GtkLayerShell.init_for_window (this);
- GtkLayerShell.set_namespace (this, "swaync-control-center");
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.TOP, true);
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.LEFT, true);
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.RIGHT, true);
- GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
- }
+ set_child (widgets_box);
+ widgets_box.set_hexpand (true);
+ widgets_box.set_halign (Gtk.Align.FILL);
+ widgets_box.add_css_class ("control-center");
- viewport.size_allocate.connect (size_alloc);
+ this.swaync_daemon.reloading_css.connect (reload_notifications_style);
- this.map.connect (() => {
- set_anchor ();
+ this.map.connect_after (() => {
// Wait until the layer has attached
+ unowned Gdk.Surface surface = get_surface ();
+ if (!(surface is Gdk.Surface)) return;
ulong id = 0;
- id = notify["has-toplevel-focus"].connect (() => {
- disconnect (id);
- unowned Gdk.Monitor monitor = null;
- unowned Gdk.Window ? win = get_window ();
- if (win != null) {
- monitor = get_display ().get_monitor_at_window (win);
- }
- swaync_daemon.show_blank_windows (monitor);
+ id = surface.enter_monitor.connect ((surface, monitor) => {
+ surface.disconnect (id);
+ swaync_daemon.show_empty_windows (monitor);
});
});
- this.unmap.connect (swaync_daemon.hide_blank_windows);
+ this.unmap.connect (swaync_daemon.hide_empty_windows);
/*
- * Handling of bank window presses (pressing outside of ControlCenter)
+ * Handling of keyboard shortcuts
*/
- blank_window_gesture = new Gtk.GestureMultiPress (this);
- blank_window_gesture.set_touch_only (false);
- blank_window_gesture.set_exclusive (true);
- blank_window_gesture.set_button (Gdk.BUTTON_PRIMARY);
- blank_window_gesture.set_propagation_phase (Gtk.PropagationPhase.BUBBLE);
- blank_window_gesture.pressed.connect ((_gesture, _n_press, x, y) => {
- // Calculate if the clicked coords intersect the ControlCenter
- Gdk.Rectangle click_rectangle = Gdk.Rectangle () {
- width = 1,
- height = 1,
- x = (int) x,
- y = (int) y,
- };
- blank_window_in = !box.intersect (click_rectangle, null);
- blank_window_down = true;
- });
- blank_window_gesture.released.connect ((gesture, _n_press, _x, _y) => {
- // Emit released
- if (!blank_window_down) return;
- blank_window_down = false;
- if (blank_window_in) {
- try {
- swaync_daemon.set_visibility (false);
- } catch (Error e) {
- stderr.printf ("ControlCenter BlankWindow Click Error: %s\n",
- e.message);
- }
- }
-
- Gdk.EventSequence ? sequence = gesture.get_current_sequence ();
- if (sequence == null) {
- blank_window_in = false;
- }
- });
- blank_window_gesture.update.connect ((gesture, sequence) => {
- Gtk.GestureSingle gesture_single = (Gtk.GestureSingle) gesture;
- if (sequence != gesture_single.get_current_sequence ()) return;
- // Calculate if the clicked coords intersect the ControlCenter
- double x, y;
- gesture.get_point (sequence, out x, out y);
- Gdk.Rectangle click_rectangle = Gdk.Rectangle () {
- width = 1,
- height = 1,
- x = (int) x,
- y = (int) y,
- };
- if (box.intersect (click_rectangle, null)) {
- blank_window_in = false;
- }
- });
- blank_window_gesture.cancel.connect ((gesture, sequence) => {
- blank_window_down = false;
+ ((Gtk.Widget) this).add_controller (event_kb = new Gtk.EventControllerKey () {
+ propagation_phase = Gtk.PropagationPhase.CAPTURE,
});
// Only use release for closing notifications due to Escape key
// sometimes being passed through to unfucused application
// Ex: Firefox in a fullscreen YouTube video
- this.key_release_event.connect ((w, event_key) => {
- if (this.get_focus () is Gtk.Entry) {
- switch (Gdk.keyval_name (event_key.keyval)) {
+ event_kb.key_released.connect ((keyval, keycode, state) => {
+ print ("FOCUS: %s\n", this.get_focus ().name);
+ if (this.get_focus () is Gtk.Text) {
+ switch (Gdk.keyval_name (keyval)) {
case "Escape":
this.set_focus (null);
- return true;
+ return;
}
- return false;
+ return;
}
- if (event_key.type == Gdk.EventType.KEY_RELEASE) {
- switch (Gdk.keyval_name (event_key.keyval)) {
- case "Escape":
- case "Caps_Lock":
- this.set_visibility (false);
- return true;
- }
+ switch (Gdk.keyval_name (keyval)) {
+ case "Escape":
+ case "Caps_Lock":
+ this.set_visibility (false);
+ return;
}
- return true;
+ return;
});
- this.key_press_event.connect ((w, event_key) => {
- if (this.get_focus () is Gtk.Entry) return false;
- if (event_key.type == Gdk.EventType.KEY_PRESS) {
- var children = list_box.get_children ();
- Notification noti = (Notification)
- list_box.get_focus_child ();
- switch (Gdk.keyval_name (event_key.keyval)) {
- case "Return":
- if (noti != null) noti.click_default_action ();
- break;
- case "Delete":
- case "BackSpace":
- if (noti != null) {
- if (children.length () == 0) break;
- if (list_reverse &&
- children.first ().data != noti) {
- list_position--;
- } else if (children.last ().data == noti) {
- if (list_position > 0) list_position--;
- }
- close_notification (noti.param.applied_id);
- }
- 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);
- }
- return false;
- });
-
- // Switches the stack page depending on the
- list_box.add.connect (() => {
- stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE);
- });
-
- list_box.remove.connect ((container, _widget) => {
- if (container.get_children ().length () > 0) return;
- stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
- });
+ // event_controller_key.key_pressed.connect ((keyval, keycode, state) => {
+ // if (this.get_focus () is Gtk.Entry) return false;
+ // 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);
+ // }
+ // 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);
+ // return false;
+ // });
add_widgets ();
}
@@ -233,76 +137,58 @@ namespace SwayNotificationCenter {
/** Adds all custom widgets. Removes previous widgets */
public void add_widgets () {
// Remove all widgets
- foreach (var widget in widgets.data) {
- box.remove (widget);
+ foreach (var widget in widgets_box.get_children ()) {
+ widgets_box.remove (widget);
}
- widgets.remove_range (0, widgets.length);
string[] w = ConfigModel.instance.widgets.data;
if (w.length == 0) w = DEFAULT_WIDGETS;
bool has_notification = false;
foreach (string key in w) {
// Reposition the scrolled_window
+ // TODO: REDO with notifications
if (key == "notifications") {
has_notification = true;
- uint pos = box.get_children ().length ();
- box.reorder_child (scrolled_window, (int) (pos > 0 ? --pos : 0));
+ widgets_box.append (notification_widget);
+ // uint pos = widgets_box.get_children ().length ();
+ // TODO: pos should be reduced by 1
+ // widgets_box.reorder_child_after (notifications, (int) (pos > 0 ? --pos : 0));
continue;
}
// Add the widget if it is valid
Widgets.BaseWidget ? widget = Widgets.get_widget_from_key (
key, swaync_daemon, noti_daemon);
if (widget == null) continue;
- widgets.append_val (widget);
- box.pack_start (widgets.index (widgets.length - 1),
- false, true, 0);
+ widgets_box.append (widget);
}
if (!has_notification) {
warning ("Notification widget not included in \"widgets\" config. Using default bottom position");
- uint pos = box.get_children ().length ();
- box.reorder_child (scrolled_window, (int) (pos > 0 ? --pos : 0));
+ widgets_box.append (notification_widget);
}
}
+ public override Graphene.Rect? ignore_bounds () {
+ Graphene.Rect ? bounds = null;
+ bool result = widgets_box.compute_bounds (this, out bounds);
+ return result ? bounds : null;
+ }
+
/** Resets the UI positions */
- private void set_anchor () {
+ public override void set_custom_options () {
if (swaync_daemon.use_layer_shell) {
// Grabs the keyboard input until closed
bool keyboard_shortcuts = ConfigModel.instance.keyboard_shortcuts;
-#if HAVE_LATEST_GTK_LAYER_SHELL
var mode = keyboard_shortcuts ?
GtkLayerShell.KeyboardMode.EXCLUSIVE :
GtkLayerShell.KeyboardMode.NONE;
GtkLayerShell.set_keyboard_mode (this, mode);
-#else
- GtkLayerShell.set_keyboard_interactivity (this, keyboard_shortcuts);
-#endif
-
- // Set layer
- GtkLayerShell.Layer layer;
- switch (ConfigModel.instance.control_center_layer) {
- case Layer.BACKGROUND:
- layer = GtkLayerShell.Layer.BACKGROUND;
- break;
- case Layer.BOTTOM:
- layer = GtkLayerShell.Layer.BOTTOM;
- break;
- case Layer.TOP:
- layer = GtkLayerShell.Layer.TOP;
- break;
- default:
- case Layer.OVERLAY:
- layer = GtkLayerShell.Layer.OVERLAY;
- break;
- }
- GtkLayerShell.set_layer (this, layer);
}
// Set the box margins
- box.set_margin_top (ConfigModel.instance.control_center_margin_top);
- box.set_margin_start (ConfigModel.instance.control_center_margin_left);
- box.set_margin_end (ConfigModel.instance.control_center_margin_right);
- box.set_margin_bottom (ConfigModel.instance.control_center_margin_bottom);
+ widgets_box.set_margin_top (ConfigModel.instance.control_center_margin_top);
+ widgets_box.set_margin_start (ConfigModel.instance.control_center_margin_left);
+ widgets_box.set_margin_end (ConfigModel.instance.control_center_margin_right);
+ widgets_box.set_margin_bottom (ConfigModel.instance.control_center_margin_bottom);
// Anchor box to north/south edges as needed
Gtk.Align align_x = Gtk.Align.END;
@@ -327,72 +213,35 @@ namespace SwayNotificationCenter {
default:
case PositionY.TOP:
align_y = Gtk.Align.START;
- // Set cc widget position
- list_reverse = false;
- list_align = Gtk.Align.START;
break;
case PositionY.CENTER:
align_y = Gtk.Align.CENTER;
- // Set cc widget position
- list_reverse = false;
- list_align = Gtk.Align.START;
break;
case PositionY.BOTTOM:
align_y = Gtk.Align.END;
- // Set cc widget position
- list_reverse = true;
- list_align = Gtk.Align.END;
break;
}
+
+ // Refresh the positioning of the notifications list
+ notification_widget.set_list_orientation ();
+
// Fit the ControlCenter to the monitor height
if (ConfigModel.instance.fit_to_screen) align_y = Gtk.Align.FILL;
// Set the ControlCenter alignment
- box.set_halign (align_x);
- box.set_valign (align_y);
-
- list_box.set_valign (list_align);
- list_box.set_sort_func ((w1, w2) => {
- var a = (Notification) w1;
- var b = (Notification) w2;
- if (a == null || b == null) return 0;
- // Sort the list in reverse if needed
- if (a.param.time == b.param.time) return 0;
- int val = list_reverse ? 1 : -1;
- return a.param.time > b.param.time ? val : val * -1;
- });
+ widgets_box.set_halign (align_x);
+ widgets_box.set_valign (align_y);
// Always set the size request in all events.
- box.set_size_request (ConfigModel.instance.control_center_width,
- ConfigModel.instance.control_center_height);
- }
-
- private void size_alloc () {
- var adj = viewport.vadjustment;
- double upper = adj.get_upper ();
- if (last_upper < upper) {
- scroll_to_start (list_reverse);
- }
- last_upper = upper;
- }
-
- private void scroll_to_start (bool reverse) {
- Gtk.ScrollType scroll_type = Gtk.ScrollType.START;
- if (reverse) {
- scroll_type = Gtk.ScrollType.END;
- }
- scrolled_window.scroll_child (scroll_type, false);
+ widgets_box.set_size_request (ConfigModel.instance.control_center_width,
+ ConfigModel.instance.control_center_height);
}
public uint notification_count () {
- return list_box.get_children ().length ();
+ return notification_widget.notification_count;
}
public void close_all_notifications () {
- foreach (var w in list_box.get_children ()) {
- Notification noti = (Notification) w;
- if (noti != null) noti.close_notification (false);
- }
-
+ notification_widget.close_all_notifications ();
try {
swaync_daemon.subscribe_v2 (notification_count (),
swaync_daemon.get_dnd (),
@@ -407,32 +256,25 @@ namespace SwayNotificationCenter {
}
}
- private void navigate_list (uint i) {
- var widget = list_box.get_children ().nth_data (i);
- if (widget != null) {
- list_box.set_focus_child (widget);
- widget.grab_focus ();
- }
- }
+ // private void navigate_list (uint i) {
+ // var widget = (Notification) list_model.get_object (i);
+ // if (widget != null) {
+ // notification_list.set_focus_child (widget);
+ // widget.grab_focus ();
+ // }
+ // }
private void on_visibility_change () {
// Updates all widgets on visibility change
- foreach (var widget in widgets.data) {
- widget.on_cc_visibility_change (visible);
+ foreach (var widget in widgets_box.get_children ()) {
+ if (widget is Widgets.BaseWidget) {
+ widget.on_cc_visibility_change (visible);
+ }
}
if (this.visible) {
- // Focus the first notification
- list_position = list_reverse ?
- (list_box.get_children ().length () - 1) : 0;
- if (list_position == uint.MAX) list_position = 0;
-
- list_box.grab_focus ();
- navigate_list (list_position);
- foreach (var w in list_box.get_children ()) {
- var noti = (Notification) w;
- if (noti != null) noti.set_time ();
- }
+ notification_widget.navigate_to_start ();
+ notification_widget.refresh_notifications_time ();
}
swaync_daemon.subscribe_v2 (notification_count (),
noti_daemon.dnd,
@@ -456,35 +298,13 @@ namespace SwayNotificationCenter {
}
public void close_notification (uint32 id, bool replaces = false) {
- foreach (var w in list_box.get_children ()) {
- var noti = (Notification) w;
- if (noti != null && noti.param.applied_id == id) {
- if (replaces) {
- noti.remove_noti_timeout ();
- noti.destroy ();
- } else {
- noti.close_notification (false);
- list_box.remove (w);
- }
- break;
- }
- }
+ notification_widget.close_notification (id, replaces);
}
+ // FIX LARGE NOTIFICATIONS not scaling
public void add_notification (NotifyParams param,
NotiDaemon noti_daemon) {
- var noti = new Notification.regular (param,
- noti_daemon,
- NotificationType.CONTROL_CENTER);
- noti.grab_focus.connect ((w) => {
- uint i = list_box.get_children ().index (w);
- if (list_position != uint.MAX && list_position != i) {
- list_position = i;
- }
- });
- noti.set_time ();
- list_box.add (noti);
- scroll_to_start (list_reverse);
+ notification_widget.add_notification (param, noti_daemon);
try {
swaync_daemon.subscribe_v2 (notification_count (),
swaync_daemon.get_dnd (),
@@ -493,10 +313,6 @@ namespace SwayNotificationCenter {
} catch (Error e) {
stderr.printf (e.message + "\n");
}
-
- // Keep focus on currently focused notification
- list_box.grab_focus ();
- navigate_list (++list_position);
}
public bool get_visibility () {
@@ -504,11 +320,13 @@ namespace SwayNotificationCenter {
}
/** Forces each notification EventBox to reload its style_context #27 */
+ // TODO: Needed?
private void reload_notifications_style () {
- foreach (var c in list_box.get_children ()) {
- Notification noti = (Notification) c;
- if (noti != null) noti.reload_style_context ();
- }
+ // Functions.widget_children_foreach (list_box, (c) => {
+ // Notification noti = (Notification) c;
+ // if (noti != null) noti.reload_style_context ();
+ // return Source.CONTINUE;
+ // });
}
}
}
diff --git a/src/controlCenter/widgets/backlight/backlight.vala b/src/controlCenter/widgets/backlight/backlight.vala
index e25109e6..fb32ce26 100644
--- a/src/controlCenter/widgets/backlight/backlight.vala
+++ b/src/controlCenter/widgets/backlight/backlight.vala
@@ -8,7 +8,7 @@ namespace SwayNotificationCenter.Widgets {
}
}
- BacklightUtil client;
+ BacklightUtil ? client;
Gtk.Label label_widget = new Gtk.Label (null);
Gtk.Scale slider = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
@@ -27,10 +27,11 @@ namespace SwayNotificationCenter.Widgets {
switch (subsystem) {
default:
case "backlight":
- if (subsystem != "backlight")
+ if (subsystem != "backlight") {
info ("Invalid subsystem %s for device %s. " +
"Use 'backlight' or 'leds'. Using default: 'backlight'",
subsystem, device);
+ }
client = new BacklightUtil ("backlight", device);
slider.set_range (min, 100);
break;
@@ -41,6 +42,11 @@ namespace SwayNotificationCenter.Widgets {
}
}
+ if (client == null) {
+ hide ();
+ return;
+ }
+
this.client.brightness_change.connect ((percent) => {
if (percent < 0) { // invalid device path
hide ();
@@ -50,23 +56,21 @@ namespace SwayNotificationCenter.Widgets {
});
slider.set_draw_value (false);
+ slider.set_hexpand (true);
slider.set_round_digits (0);
slider.value_changed.connect (() => {
this.client.set_brightness ((float) slider.get_value ());
slider.tooltip_text = ((int) slider.get_value ()).to_string ();
});
- add (label_widget);
- pack_start (slider, true, true, 0);
-
- show_all ();
+ append (label_widget);
+ prepend (slider);
}
- public override void on_cc_visibility_change (bool val) {
- if (val) {
+ public override void on_cc_visibility_change (bool visible) {
+ if (client == null) return;
+ if (visible) {
this.client.start ();
- } else {
- this.client.close ();
}
}
}
diff --git a/src/controlCenter/widgets/backlight/backlightUtil.vala b/src/controlCenter/widgets/backlight/backlightUtil.vala
index 1c1a84c4..8ec8af41 100644
--- a/src/controlCenter/widgets/backlight/backlightUtil.vala
+++ b/src/controlCenter/widgets/backlight/backlightUtil.vala
@@ -10,7 +10,6 @@ namespace SwayNotificationCenter.Widgets {
string path_current;
string path_max;
File fd;
- FileMonitor monitor = null;
int max;
@@ -31,21 +30,16 @@ namespace SwayNotificationCenter.Widgets {
fd = File.new_for_path (path_current);
if (fd.query_exists ()) {
set_max_value ();
- try {
- monitor = fd.monitor (FileMonitorFlags.NONE, null);
- } catch (Error e) {
- error ("Error %s\n", e.message);
- }
} else {
this.brightness_change (-1);
warning ("Could not find device %s\n", path_current);
- close ();
}
try {
// setup DBus for setting brightness
login1 = Bus.get_proxy_sync (BusType.SYSTEM,
- "org.freedesktop.login1", "/org/freedesktop/login1/session/auto");
+ "org.freedesktop.login1",
+ "/org/freedesktop/login1/session/auto");
} catch (Error e) {
error ("Error %s\n", e.message);
}
@@ -55,30 +49,13 @@ namespace SwayNotificationCenter.Widgets {
if (fd.query_exists ()) {
// get changes made while controlCenter not shown
get_brightness ();
-
- connect_monitor ();
} else {
this.brightness_change (-1);
warning ("Could not find device %s\n", path_current);
- close ();
}
}
- private void connect_monitor () {
- if (monitor != null) {
- // connect monitor to monitor changes
- monitor.changed.connect ((src, dest, event) => {
- get_brightness ();
- });
- }
- }
-
- public void close () {
- if (monitor != null) monitor.cancel ();
- }
-
public void set_brightness (float percent) {
- this.close ();
try {
if (subsystem == "backlight") {
int actual = calc_actual (percent);
@@ -89,14 +66,13 @@ namespace SwayNotificationCenter.Widgets {
} catch (Error e) {
error ("Error %s\n", e.message);
}
- connect_monitor ();
}
// get current brightness and emit signal
private void get_brightness () {
try {
- var dis = new DataInputStream (fd.read (null));
- string data = dis.read_line (null);
+ var dis = new DataInputStream (fd.read ());
+ string data = dis.read_line ();
if (subsystem == "backlight") {
int val = calc_percent (int.parse (data));
this.brightness_change (val);
diff --git a/src/controlCenter/widgets/baseWidget.vala b/src/controlCenter/widgets/baseWidget.vala
index 2728485c..ebcba83f 100644
--- a/src/controlCenter/widgets/baseWidget.vala
+++ b/src/controlCenter/widgets/baseWidget.vala
@@ -1,7 +1,7 @@
using Posix;
namespace SwayNotificationCenter.Widgets {
- public abstract class BaseWidget : Gtk.Box {
+ public abstract class BaseWidget : IterBox {
public abstract string widget_name { get; }
public weak string css_class_name {
@@ -17,13 +17,17 @@ namespace SwayNotificationCenter.Widgets {
public unowned NotiDaemon noti_daemon;
protected BaseWidget (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
+ base (Gtk.Orientation.HORIZONTAL, 0);
this.suffix = suffix;
this.key = widget_name + (suffix.length > 0 ? "#%s".printf (suffix) : "");
this.swaync_daemon = swaync_daemon;
this.noti_daemon = noti_daemon;
- get_style_context ().add_class (css_class_name);
- if (suffix.length > 0) get_style_context ().add_class (suffix);
+ this.hexpand = true;
+ this.halign = Gtk.Align.FILL;
+
+ this.add_css_class (css_class_name);
+ if (suffix.length > 0) this.add_css_class (suffix);
}
protected Json.Object ? get_config (Gtk.Widget widget) {
@@ -33,7 +37,7 @@ namespace SwayNotificationCenter.Widgets {
Json.Object ? props = null;
bool result = config.lookup_extended (key, out orig_key, out props);
if (!result || orig_key == null || props == null) {
- critical ("%s: Config not found! Using default config...\n", key);
+ warning ("%s: Config not found! Using default config...\n", key);
return null;
}
return props;
diff --git a/src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala b/src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala
index d8f88b01..322adb86 100644
--- a/src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala
+++ b/src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala
@@ -20,20 +20,27 @@ namespace SwayNotificationCenter.Widgets {
if (a != null) actions = parse_actions (a);
}
- Gtk.FlowBox container = new Gtk.FlowBox ();
- container.set_selection_mode (Gtk.SelectionMode.NONE);
- pack_start (container, true, true, 0);
+ if (actions.length == 0) {
+ hide ();
+ return;
+ }
+
+ Gtk.FlowBox container = new Gtk.FlowBox () {
+ selection_mode = Gtk.SelectionMode.NONE,
+ hexpand = true,
+ };
+ prepend (container);
// add action to container
foreach (var act in actions) {
- Gtk.Button b = new Gtk.Button.with_label (act.label);
+ Gtk.Button button = new Gtk.Button.with_label (act.label) {
+ css_classes = { "widget-buttons-grid-button" },
+ };
- b.clicked.connect (() => execute_command (act.command));
+ button.clicked.connect (() => execute_command (act.command));
- container.insert (b, -1);
+ container.append (button);
}
-
- show_all ();
}
}
}
diff --git a/src/controlCenter/widgets/dnd/dnd.vala b/src/controlCenter/widgets/dnd/dnd.vala
index eab495f1..f466fae7 100644
--- a/src/controlCenter/widgets/dnd/dnd.vala
+++ b/src/controlCenter/widgets/dnd/dnd.vala
@@ -23,8 +23,11 @@ namespace SwayNotificationCenter.Widgets {
}
// Title
- title_widget = new Gtk.Label (title);
- add (title_widget);
+ title_widget = new Gtk.Label (title) {
+ halign = Gtk.Align.START,
+ hexpand = true,
+ };
+ prepend (title_widget);
// Dnd button
dnd_button = new Gtk.Switch () {
@@ -40,10 +43,9 @@ namespace SwayNotificationCenter.Widgets {
dnd_button.set_can_focus (false);
dnd_button.valign = Gtk.Align.CENTER;
// Backwards compatible towards older CSS stylesheets
- dnd_button.get_style_context ().add_class ("control-center-dnd");
- pack_end (dnd_button, false);
+ dnd_button.add_css_class ("control-center-dnd");
+ append (dnd_button);
- show_all ();
}
private bool state_set (Gtk.Widget widget, bool state) {
diff --git a/src/controlCenter/widgets/factory.vala b/src/controlCenter/widgets/factory.vala
index aea178d3..84e0b26e 100644
--- a/src/controlCenter/widgets/factory.vala
+++ b/src/controlCenter/widgets/factory.vala
@@ -7,6 +7,7 @@ namespace SwayNotificationCenter.Widgets {
if (key_seperated.length > 0) key = key_seperated[0];
if (key_seperated.length > 1) suffix = key_seperated[1];
BaseWidget widget;
+ message ("Loading widget: %s", key);
switch (key) {
case "title":
widget = new Title (suffix, swaync_daemon, noti_daemon);
@@ -39,7 +40,7 @@ namespace SwayNotificationCenter.Widgets {
warning ("Could not find widget: \"%s\"!", key);
return null;
}
- message ("Loading widget: %s", widget.widget_name);
+ message ("Finished loading widget: %s", widget.widget_name);
return widget;
}
}
diff --git a/src/controlCenter/widgets/inhibitors/inhibitors.vala b/src/controlCenter/widgets/inhibitors/inhibitors.vala
index f9e8f81a..eff12c85 100644
--- a/src/controlCenter/widgets/inhibitors/inhibitors.vala
+++ b/src/controlCenter/widgets/inhibitors/inhibitors.vala
@@ -7,7 +7,6 @@ namespace SwayNotificationCenter.Widgets {
}
Gtk.Label title_widget;
- Gtk.Button clear_all_button;
// Default config values
string title = "Inhibitors";
@@ -41,12 +40,15 @@ namespace SwayNotificationCenter.Widgets {
if (button_text != null) this.button_text = button_text;
}
- title_widget = new Gtk.Label (title);
- title_widget.show ();
- add (title_widget);
+ title_widget = new Gtk.Label (title) {
+ hexpand = true,
+ xalign = 0.0f,
+ yalign = 0.0f,
+ };
+ append (title_widget);
if (has_clear_all_button) {
- clear_all_button = new Gtk.Button.with_label (button_text);
+ Gtk.Button clear_all_button = new Gtk.Button.with_label (button_text);
clear_all_button.clicked.connect (() => {
try {
swaync_daemon.clear_inhibitors ();
@@ -57,7 +59,7 @@ namespace SwayNotificationCenter.Widgets {
clear_all_button.set_can_focus (false);
clear_all_button.valign = Gtk.Align.CENTER;
clear_all_button.show ();
- pack_end (clear_all_button, false);
+ append (clear_all_button);
}
hide ();
diff --git a/src/controlCenter/widgets/label/label.vala b/src/controlCenter/widgets/label/label.vala
index 4fe0e497..492c9615 100644
--- a/src/controlCenter/widgets/label/label.vala
+++ b/src/controlCenter/widgets/label/label.vala
@@ -29,18 +29,18 @@ namespace SwayNotificationCenter.Widgets {
label_widget.set_text (text);
label_widget.set_ellipsize (Pango.EllipsizeMode.END);
- label_widget.set_line_wrap (true);
+ label_widget.set_wrap (true);
label_widget.set_lines (max_lines);
// Without this and pack_start fill, the label would expand to
// the monitors full width... GTK bug!...
label_widget.set_max_width_chars (0);
- label_widget.set_line_wrap_mode (Pango.WrapMode.WORD_CHAR);
+ label_widget.set_wrap_mode (Pango.WrapMode.WORD_CHAR);
label_widget.set_justify (Gtk.Justification.LEFT);
- label_widget.set_alignment (0, 0);
+ label_widget.set_xalign (0);
+ label_widget.set_yalign (0);
- pack_start (label_widget, true, true, 0);
+ prepend (label_widget);
- show_all ();
}
}
}
diff --git a/src/controlCenter/widgets/menubar/menubar.vala b/src/controlCenter/widgets/menubar/menubar.vala
index a5d55e28..09b85d20 100644
--- a/src/controlCenter/widgets/menubar/menubar.vala
+++ b/src/controlCenter/widgets/menubar/menubar.vala
@@ -34,34 +34,44 @@ namespace SwayNotificationCenter.Widgets {
}
}
- Gtk.Box menus_container;
- Gtk.Box topbar_container;
+ Gtk.Box left_container;
+ Gtk.Box right_container;
List menu_objects;
public Menubar (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
base (suffix, swaync_daemon, noti_daemon);
+ set_orientation (Gtk.Orientation.VERTICAL);
Json.Object ? config = get_config (this);
if (config != null) {
parse_config_objects (config);
}
- menus_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
- topbar_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
- topbar_container.get_style_context ().add_class ("menu-button-bar");
-
- menus_container.add (topbar_container);
+ Gtk.Box topbar_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+ append (topbar_container);
+
+ left_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) {
+ css_classes = { "widget-menubar-container", "start" },
+ overflow = Gtk.Overflow.HIDDEN,
+ hexpand = true,
+ halign = Gtk.Align.START,
+ };
+ topbar_container.append (left_container);
+ right_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) {
+ css_classes = { "widget-menubar-container", "end" },
+ overflow = Gtk.Overflow.HIDDEN,
+ hexpand = true,
+ halign = Gtk.Align.END,
+ };
+ topbar_container.append (right_container);
for (int i = 0; i < menu_objects.length (); i++) {
unowned ConfigObject ? obj = menu_objects.nth_data (i);
add_menu (ref obj);
}
- pack_start (menus_container, true, true, 0);
- show_all ();
-
foreach (var obj in menu_objects) {
obj.revealer ?.set_reveal_child (false);
}
@@ -70,61 +80,82 @@ namespace SwayNotificationCenter.Widgets {
void add_menu (ref unowned ConfigObject ? obj) {
switch (obj.type) {
case MenuType.BUTTONS:
- Gtk.Box container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
- if (obj.name != null) container.get_style_context ().add_class (obj.name);
+ Gtk.Box container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) {
+ css_classes = { "widget-menubar-buttons", "widget-menubar-child" },
+ overflow = Gtk.Overflow.HIDDEN,
+ };
+ if (obj.name != null) container.add_css_class (obj.name);
- foreach (Action a in obj.actions) {
- Gtk.Button b = new Gtk.Button.with_label (a.label);
+ foreach (Action action in obj.actions) {
+ Gtk.Button button = new Gtk.Button.with_label (action.label);
+ button.add_css_class ("widget-menubar-button");
- b.clicked.connect (() => execute_command (a.command));
+ button.clicked.connect (() => execute_command (action.command));
- container.add (b);
+ container.append (button);
}
switch (obj.position) {
case Position.LEFT:
- topbar_container.pack_start (container, false, false, 0);
+ left_container.prepend (container);
break;
case Position.RIGHT:
- topbar_container.pack_end (container, false, false, 0);
+ right_container.append (container);
break;
}
break;
case MenuType.MENU:
- Gtk.Button show_button = new Gtk.Button.with_label (obj.label);
+ Gtk.ToggleButton show_button = new Gtk.ToggleButton.with_label (obj.label);
+ show_button.add_css_class ("widget-menubar-button");
+ show_button.add_css_class ("widget-menubar-child");
+ if (obj.name != null) show_button.add_css_class (obj.name);
Gtk.Box menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
- if (obj.name != null) menu.get_style_context ().add_class (obj.name);
+ print ("NAME: %s\n", obj.name);
- Gtk.Revealer r = new Gtk.Revealer ();
- r.add (menu);
- r.set_transition_duration (obj.animation_duration);
- r.set_transition_type (obj.animation_type);
- obj.revealer = r;
+ Gtk.Revealer revealer = new Gtk.Revealer () {
+ child = menu,
+ css_classes = { "widget-menubar-menu" },
+ hexpand = true,
+ transition_duration = obj.animation_duration,
+ transition_type = obj.animation_type
+ };
+ obj.revealer = revealer;
show_button.clicked.connect (() => {
- bool visible = !r.get_reveal_child ();
+ bool visible = !revealer.get_reveal_child ();
foreach (var o in menu_objects) {
o.revealer ?.set_reveal_child (false);
}
- r.set_reveal_child (visible);
+ if (visible) {
+ // revealer.show ();
+ revealer.set_reveal_child (true);
+ } else {
+ revealer.set_reveal_child (false);
+ Timeout.add_once (revealer.transition_duration, () => {
+ // revealer.hide ();
+ return Source.REMOVE;
+ });
+ }
});
foreach (var a in obj.actions) {
Gtk.Button b = new Gtk.Button.with_label (a.label);
b.clicked.connect (() => execute_command (a.command));
- menu.pack_start (b, true, true, 0);
+ menu.prepend (b);
}
switch (obj.position) {
case Position.RIGHT:
- topbar_container.pack_end (show_button, false, false, 0);
+ show_button.halign = Gtk.Align.START;
+ right_container.append (show_button);
break;
case Position.LEFT:
- topbar_container.pack_start (show_button, false, false, 0);
+ show_button.halign = Gtk.Align.END;
+ left_container.prepend (show_button);
break;
}
- menus_container.add (r);
+ append (revealer);
break;
}
}
@@ -140,21 +171,35 @@ namespace SwayNotificationCenter.Widgets {
if (obj == null) continue;
string[] key = e.split ("#");
- string t = key[0];
MenuType type = MenuType.BUTTONS;
- if (t == "buttons") type = MenuType.BUTTONS;
- else if (t == "menu") type = MenuType.MENU;
- else info ("Invalid type for menu-object - valid options: 'menu' || 'buttons' using default");
+ switch (key[0]) {
+ case "buttons":
+ type = MenuType.BUTTONS;
+ break;
+ case "menu":
+ type = MenuType.MENU;
+ break;
+ default:
+ info ("Invalid type for menu-object - valid options: 'menu' || 'buttons' using default");
+ break;
+ }
string name = key[1];
- string ? p = get_prop (obj, "position");
- Position pos;
- if (p != "left" && p != "right") {
- pos = Position.RIGHT;
- info ("No position for menu-object given using default");
- } else if (p == "right") pos = Position.RIGHT;
- else pos = Position.LEFT;
+ string ? config_pos = get_prop (obj, "position");
+ Position pos = Position.RIGHT;
+ switch (config_pos) {
+ case "right":
+ pos = Position.RIGHT;
+ break;
+ case "left":
+ pos = Position.LEFT;
+ break;
+ default:
+ pos = Position.RIGHT;
+ info ("No position for menu-object given using default");
+ break;
+ }
Json.Array ? actions = get_prop_array (obj, "actions");
if (actions == null) {
diff --git a/src/controlCenter/widgets/mpris/mpris.vala b/src/controlCenter/widgets/mpris/mpris.vala
index 3bac6c9b..c92aba47 100644
--- a/src/controlCenter/widgets/mpris/mpris.vala
+++ b/src/controlCenter/widgets/mpris/mpris.vala
@@ -1,9 +1,4 @@
namespace SwayNotificationCenter.Widgets.Mpris {
- public struct Config {
- int image_size;
- int image_radius;
- }
-
public class Mpris : BaseWidget {
public override string widget_name {
get {
@@ -19,14 +14,13 @@ namespace SwayNotificationCenter.Widgets.Mpris {
Gtk.Button button_prev;
Gtk.Button button_next;
Gtk.Box carousel_box;
- Hdy.Carousel carousel;
- Hdy.CarouselIndicatorDots carousel_dots;
+ Adw.Carousel carousel;
+ Adw.CarouselIndicatorDots carousel_dots;
+
+ bool starting = true;
// Default config values
- Config mpris_config = Config () {
- image_size = 96,
- image_radius = 12,
- };
+ int image_size = 96;
public Mpris (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
base (suffix, swaync_daemon, noti_daemon);
@@ -34,63 +28,48 @@ namespace SwayNotificationCenter.Widgets.Mpris {
set_valign (Gtk.Align.START);
set_vexpand (false);
- carousel_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) {
- visible = true,
- };
+ carousel_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
- button_prev = new Gtk.Button.from_icon_name ("go-previous", Gtk.IconSize.BUTTON) {
- relief = Gtk.ReliefStyle.NONE,
+ button_prev = new Gtk.Button.from_icon_name ("go-previous") {
visible = false,
};
button_prev.clicked.connect (() => change_carousel_position (-1));
- button_next = new Gtk.Button.from_icon_name ("go-next", Gtk.IconSize.BUTTON) {
- relief = Gtk.ReliefStyle.NONE,
+ button_next = new Gtk.Button.from_icon_name ("go-next") {
visible = false,
};
button_next.clicked.connect (() => change_carousel_position (1));
- carousel = new Hdy.Carousel () {
- visible = true,
+ carousel = new Adw.Carousel () {
+ allow_scroll_wheel = true,
+ hexpand = true,
};
-#if HAVE_LATEST_LIBHANDY
- carousel.allow_scroll_wheel = true;
-#endif
carousel.page_changed.connect ((index) => {
- GLib.List children = carousel.get_children ();
- int children_length = (int) children.length ();
- if (children_length <= 1) {
+ if (carousel.n_pages <= 1) {
button_prev.sensitive = false;
button_next.sensitive = false;
return;
}
button_prev.sensitive = index > 0;
- button_next.sensitive = index < children_length - 1;
+ button_next.sensitive = index < carousel.n_pages - 1;
});
- carousel_box.add (button_prev);
- carousel_box.add (carousel);
- carousel_box.add (button_next);
- add (carousel_box);
+ carousel_box.append (button_prev);
+ carousel_box.append (carousel);
+ carousel_box.append (button_next);
+ append (carousel_box);
- carousel_dots = new Hdy.CarouselIndicatorDots ();
+ carousel_dots = new Adw.CarouselIndicatorDots ();
carousel_dots.set_carousel (carousel);
carousel_dots.show ();
- add (carousel_dots);
+ append (carousel_dots);
// Config
Json.Object ? config = get_config (this);
if (config != null) {
// Get image-size
int? image_size = get_prop (config, "image-size");
- if (image_size != null) mpris_config.image_size = image_size;
-
- // Get image-border-radius
- int? image_radius = get_prop (config, "image-radius");
- if (image_radius != null) mpris_config.image_radius = image_radius;
- // Clamp the radius
- mpris_config.image_radius = mpris_config.image_radius.clamp (
- 0, (int) (mpris_config.image_size * 0.5));
+ if (image_size != null) this.image_size = image_size;
}
hide ();
@@ -99,19 +78,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
} catch (Error e) {
error ("MPRIS Widget error: %s", e.message);
}
- }
-
- /**
- * Forces the carousel to reload its style_context.
- * Fixes carousel items not redrawing when window isn't visible.
- * Probably related to: https://gitlab.gnome.org/GNOME/libhandy/-/issues/363
- */
- public override void on_cc_visibility_change (bool value) {
- if (!value) return;
- carousel.get_style_context ().changed ();
- foreach (var child in carousel.get_children ()) {
- child.get_style_context ().changed ();
- }
+ starting = false;
}
private void setup_mpris () throws Error {
@@ -147,17 +114,17 @@ namespace SwayNotificationCenter.Widgets.Mpris {
}
private void add_player (string name, MprisSource source) {
- MprisPlayer player = new MprisPlayer (source, mpris_config);
- player.get_style_context ().add_class ("%s-player".printf (css_class_name));
+ MprisPlayer player = new MprisPlayer (source, image_size, css_class_name);
carousel.prepend (player);
players.set (name, player);
if (!visible) show ();
// Scroll to the new player
- carousel.scroll_to (player);
- uint children_length = carousel.get_children ().length ();
- if (children_length > 1) {
+ // TODO: Open issue about scroll_to not being run before the window is shown
+ // Also affects notifications
+ carousel.scroll_to (player, !starting);
+ if (carousel.n_pages > 1) {
button_prev.show ();
button_next.show ();
}
@@ -171,24 +138,21 @@ namespace SwayNotificationCenter.Widgets.Mpris {
player.before_destroy ();
player.destroy ();
players.remove (name);
+ carousel.remove (player);
- uint children_length = carousel.get_children ().length ();
- if (children_length == 0) {
+ if (carousel.n_pages == 0) {
hide ();
- }
- if (children_length <= 1) {
+ } else if (carousel.n_pages <= 1) {
button_prev.hide ();
button_next.hide ();
}
}
private void change_carousel_position (int delta) {
- GLib.List children = carousel.get_children ();
- int children_length = (int) children.length ();
- if (children_length == 0) return;
+ if (carousel.n_pages == 0) return;
int position = ((int) carousel.position + delta).clamp (
- 0, children_length - 1);
- carousel.scroll_to (children.nth_data (position));
+ 0, (int) carousel.n_pages - 1);
+ carousel.scroll_to (carousel.get_nth_page (position), true);
}
}
}
diff --git a/src/controlCenter/widgets/mpris/mpris_player.ui b/src/controlCenter/widgets/mpris/mpris_player.ui
deleted file mode 100644
index 9b322bbf..00000000
--- a/src/controlCenter/widgets/mpris/mpris_player.ui
+++ /dev/null
@@ -1,266 +0,0 @@
-
-
-
-
-
- True
- False
- start
- True
- vertical
- 4
-
-
- True
- False
- 12
-
-
- True
- False
- 96
- audio-x-generic-symbolic
- True
- 0
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- center
- True
- vertical
- 4
-
-
- True
- False
- True
- word-char
- end
- 0
- 0
- 0
- 0
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- True
- word-char
- end
- 0
- 0
- 0
- 0
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- center
- 6
- True
-
-
- True
- False
- True
- center
- center
- none
-
-
- True
- False
- 4
- 4
- 20
- media-playlist-shuffle-symbolic
- 0
-
-
-
-
-
- True
- True
- 0
-
-
-
-
- True
- False
- True
- center
- center
- none
-
-
- True
- False
- 4
- 4
- 20
- media-seek-backward-symbolic
- 0
-
-
-
-
-
- True
- True
- 1
-
-
-
-
- True
- False
- True
- center
- center
- none
-
-
- True
- False
- 4
- 4
- 20
- media-playback-pause-symbolic
- 0
-
-
-
-
-
- True
- True
- 2
-
-
-
-
- True
- False
- True
- center
- center
- none
-
-
- True
- False
- 4
- 4
- 20
- media-seek-forward-symbolic
- 0
-
-
-
-
-
- True
- True
- 3
-
-
-
-
- True
- False
- True
- center
- center
- none
-
-
- True
- False
- 4
- 4
- 20
- media-playlist-repeat-symbolic
- 0
-
-
-
-
-
- True
- True
- 4
-
-
-
-
- False
- True
- 2
-
-
-
-
diff --git a/src/controlCenter/widgets/mpris/mpris_player.vala b/src/controlCenter/widgets/mpris/mpris_player.vala
index cec23224..9b28c805 100644
--- a/src/controlCenter/widgets/mpris/mpris_player.vala
+++ b/src/controlCenter/widgets/mpris/mpris_player.vala
@@ -1,30 +1,20 @@
namespace SwayNotificationCenter.Widgets.Mpris {
- [GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/widgets/mpris/mpris_player.ui")]
public class MprisPlayer : Gtk.Box {
- [GtkChild]
- unowned Gtk.Label title;
- [GtkChild]
- unowned Gtk.Label sub_title;
-
- [GtkChild]
- unowned Gtk.Image album_art;
-
- [GtkChild]
- unowned Gtk.Button button_shuffle;
- [GtkChild]
- unowned Gtk.Button button_prev;
- [GtkChild]
- unowned Gtk.Button button_play_pause;
- [GtkChild]
- unowned Gtk.Image button_play_pause_img;
- [GtkChild]
- unowned Gtk.Button button_next;
- [GtkChild]
- unowned Gtk.Button button_repeat;
- [GtkChild]
- unowned Gtk.Image button_repeat_img;
+ public Gtk.Label title;
+ Gtk.Label sub_title;
+
+ ScaledImage album_art;
+
+ Gtk.Button button_shuffle;
+ Gtk.Button button_prev;
+ Gtk.Button button_play_pause;
+ Gtk.Image button_play_pause_img;
+ Gtk.Button button_next;
+ Gtk.Button button_repeat;
+ Gtk.Image button_repeat_img;
public MprisSource source { construct; get; }
+ public string css_class_name { construct; get; }
private const double UNSELECTED_OPACITY = 0.5;
@@ -34,15 +24,110 @@ namespace SwayNotificationCenter.Widgets.Mpris {
public const string ICON_PLAY = "media-playback-start-symbolic";
public const string ICON_PAUSE = "media-playback-pause-symbolic";
+ private const string[] BUTTON_CSS_CLASSES = { "circular", "image-button", "flat" };
+
private Cancellable album_art_cancellable = new Cancellable ();
private string prev_art_url;
private DesktopAppInfo ? desktop_entry = null;
- private unowned Config mpris_config;
+ private int album_art_size = 96;
+
+ construct {
+ set_orientation (Gtk.Orientation.VERTICAL);
+ this.hexpand = true;
+ add_css_class ("%s-player".printf (css_class_name));
+
+ var top_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12);
+ append (top_box);
+
+ top_box.append (album_art = new ScaledImage () {
+ css_classes = { "%s-album-art".printf (css_class_name) },
+ });
+
+ var info_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 4);
+ info_box.append (title = new Gtk.Label (null) {
+ wrap = true,
+ ellipsize = Pango.EllipsizeMode.END,
+ css_classes = { "%s-title".printf (css_class_name) },
+ halign = Gtk.Align.FILL,
+ xalign = 0,
+ yalign = 0,
+ width_chars = 0,
+ max_width_chars = 0,
+ });
+ info_box.append (sub_title = new Gtk.Label (null) {
+ wrap = true,
+ ellipsize = Pango.EllipsizeMode.END,
+ css_classes = { "%s-subtitle".printf (css_class_name) },
+ halign = Gtk.Align.FILL,
+ xalign = 0,
+ yalign = 0,
+ width_chars = 0,
+ max_width_chars = 0,
+ });
+ top_box.append (info_box);
+
+ // Add all of the buttons
+ var button_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) {
+ homogeneous = true,
+ halign = Gtk.Align.CENTER,
+ };
+ append (button_box);
+ button_box.append (button_shuffle = new Gtk.Button () {
+ css_classes = BUTTON_CSS_CLASSES,
+ child = new Gtk.Image () {
+ icon_name = "media-playlist-shuffle-symbolic",
+ margin_start = 4,
+ margin_end = 4,
+ margin_top = 4,
+ margin_bottom = 4,
+ },
+ });
+ button_box.append (button_prev = new Gtk.Button () {
+ css_classes = BUTTON_CSS_CLASSES,
+ child = new Gtk.Image () {
+ icon_name = "media-seek-backward-symbolic",
+ margin_start = 4,
+ margin_end = 4,
+ margin_top = 4,
+ margin_bottom = 4,
+ },
+ });
+ button_box.append (button_play_pause = new Gtk.Button () {
+ css_classes = BUTTON_CSS_CLASSES,
+ child = (button_play_pause_img = new Gtk.Image () {
+ icon_name = ICON_PAUSE,
+ margin_start = 4,
+ margin_end = 4,
+ margin_top = 4,
+ margin_bottom = 4,
+ }),
+ });
+ button_box.append (button_next = new Gtk.Button () {
+ css_classes = BUTTON_CSS_CLASSES,
+ child = new Gtk.Image () {
+ icon_name = "media-seek-forward-symbolic",
+ margin_start = 4,
+ margin_end = 4,
+ margin_top = 4,
+ margin_bottom = 4,
+ },
+ });
+ button_box.append (button_repeat = new Gtk.Button () {
+ css_classes = BUTTON_CSS_CLASSES,
+ child = (button_repeat_img = new Gtk.Image () {
+ icon_name = ICON_REPEAT,
+ margin_start = 4,
+ margin_end = 4,
+ margin_top = 4,
+ margin_bottom = 4,
+ }),
+ });
+ }
- public MprisPlayer (MprisSource source, Config mpris_config) {
- Object (source: source);
- this.mpris_config = mpris_config;
+ public MprisPlayer (MprisSource source, int album_art_size, string css_class_name) {
+ Object (source: source, css_class_name: css_class_name);
+ this.album_art_size = album_art_size;
source.properties_changed.connect (properties_changed);
@@ -239,22 +324,20 @@ namespace SwayNotificationCenter.Widgets.Mpris {
}
private async void update_album_art (HashTable metadata) {
+ album_art.set_pixel_size (album_art_size);
if ("mpris:artUrl" in metadata) {
string url = metadata["mpris:artUrl"].get_string ();
if (url == prev_art_url) return;
prev_art_url = url;
- int scale = get_style_context ().get_scale ();
-
Gdk.Pixbuf ? pixbuf = null;
// Cancel previous download, reset the state and download again
album_art_cancellable.cancel ();
album_art_cancellable.reset ();
try {
File file = File.new_for_uri (url);
- InputStream stream = yield file.read_async (Priority.DEFAULT,
- album_art_cancellable);
-
+ InputStream stream = yield file.read_async (
+ Priority.DEFAULT, album_art_cancellable);
pixbuf = yield new Gdk.Pixbuf.from_stream_async (
stream, album_art_cancellable);
} catch (Error e) {
@@ -262,27 +345,20 @@ namespace SwayNotificationCenter.Widgets.Mpris {
source.media_player.identity);
}
if (pixbuf != null) {
- pixbuf = Functions.scale_round_pixbuf (pixbuf,
- mpris_config.image_size,
- mpris_config.image_size,
- scale,
- mpris_config.image_radius);
album_art.set_from_pixbuf (pixbuf);
- album_art.get_style_context ().set_scale (1);
return;
}
}
// Get the app icon
- Icon ? icon = null;
+ unowned Icon ? icon = null;
if (desktop_entry is DesktopAppInfo) {
icon = desktop_entry.get_icon ();
}
if (icon != null) {
- album_art.set_from_gicon (icon, mpris_config.image_size);
+ album_art.set_from_gicon (icon);
} else {
// Default icon
- album_art.set_from_icon_name ("audio-x-generic-symbolic",
- mpris_config.image_size);
+ album_art.set_from_icon_name ("audio-x-generic-symbolic");
}
}
@@ -368,9 +444,8 @@ namespace SwayNotificationCenter.Widgets.Mpris {
icon_name = ICON_REPEAT_SONG;
break;
}
- unowned Gtk.StyleContext ctx = button_repeat.get_style_context ();
- if (remove_flat_css_class) ctx.remove_class ("flat");
- else ctx.add_class ("flat");
+ if (remove_flat_css_class) remove_css_class ("flat");
+ else add_css_class ("flat");
button_repeat.get_child ().opacity = opacity;
button_repeat.sensitive = true;
button_repeat_img.icon_name = icon_name;
diff --git a/src/controlCenter/widgets/notifications/notifications.vala b/src/controlCenter/widgets/notifications/notifications.vala
new file mode 100644
index 00000000..29ecb414
--- /dev/null
+++ b/src/controlCenter/widgets/notifications/notifications.vala
@@ -0,0 +1,357 @@
+namespace SwayNotificationCenter.Widgets {
+ public class NotiListItemModel : Object {
+ public unowned Notification notification;
+ public unowned NotiDaemon noti_daemon;
+ public NotifyParams param;
+ public NotificationType notification_type;
+ }
+
+ public class Notifications : Gtk.Widget {
+ public uint notification_count {
+ get {
+ return list_model.n_items;
+ }
+ }
+
+ // CustomScrolledWindow scrolled_window = new CustomScrolledWindow ();
+ Gtk.ScrolledWindow scrolled_window;
+ Gtk.Stack stack;
+
+ // Gtk.ListBox notification_list = new Gtk.ListBox ();
+ Gtk.ListView notification_list;
+ private uint list_position = 0;
+ private double last_upper = 0;
+ private bool list_reverse = false;
+ private Gtk.Align list_align = Gtk.Align.START;
+
+ private List visible_models = new List ();
+ private ListStore list_model = new ListStore (typeof (NotiListItemModel));
+
+ NotificationType notification_type { get; private set; }
+
+ const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
+ const string STACK_NOTIFICATIONS_PAGE = "notifications-list";
+
+ public Notifications (NotificationType notification_type) {
+ this.notification_type = notification_type;
+
+ this.vexpand = true;
+ this.valign = Gtk.Align.FILL;
+
+ stack = new Gtk.Stack () {
+ vhomogeneous = false,
+ transition_type = Gtk.StackTransitionType.CROSSFADE,
+ };
+ stack.set_parent (this);
+
+ // Placeholder
+ Gtk.CenterBox placeholder = new Gtk.CenterBox () {
+ valign = Gtk.Align.CENTER,
+ };
+ stack.add_named (placeholder, STACK_PLACEHOLDER_PAGE);
+ Gtk.Box placeholder_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12) {
+ css_classes = { "control-center-list-placeholder" }
+ };
+ placeholder.set_center_widget (placeholder_box);
+ placeholder_box.append (new Gtk.Image () {
+ icon_name = "notifications-placeholder-symbolic",
+ pixel_size = 96
+ });
+ placeholder_box.append (new Gtk.Label ("No Notifications"));
+
+ // Notifications
+ stack.add_named (scrolled_window = new Gtk.ScrolledWindow () {
+ hexpand = true,
+ valign = Gtk.Align.FILL,
+ hscrollbar_policy = Gtk.PolicyType.NEVER,
+ }, STACK_NOTIFICATIONS_PAGE);
+ var factory = new Gtk.SignalListItemFactory ();
+ factory.setup.connect (item_factory_setup_cb);
+ factory.bind.connect (item_factory_bind_cb);
+ factory.unbind.connect (item_factory_unbind_cb);
+ // TODO: Use single selection for keyboard navigation?
+ var selection_model = new Gtk.NoSelection (list_model);
+ notification_list = new Gtk.ListView (selection_model, factory) {
+ single_click_activate = false,
+ };
+ notification_list.add_css_class ("control-center-list");
+ scrolled_window.set_child (notification_list);
+
+ // Switches the stack page depending on the
+ list_model.items_changed.connect (list_model_items_changed_cb);
+
+ map.connect_after (() => {
+ print ("MAP!\n");
+ // WORKS THE SECOND TIME...
+ foreach (unowned NotiListItemModel model in visible_models) {
+ print ("MODEL: %s\n", model.param.summary);
+ model.notification.queue_resize ();
+ }
+ notification_list.queue_resize ();
+ });
+ }
+
+ /*
+ * Callbacks
+ */
+
+ /**
+ * Emitted to set up permanent things on the listitem. This usually
+ * means constructing the widgets used in the row and adding them to
+ * the listitem.
+ */
+ private void item_factory_setup_cb (Gtk.SignalListItemFactory factory,
+ Gtk.ListItem item) {
+ Notification noti = new Notification ();
+ item.set_child (noti);
+ // noti.queue_resize ();
+ }
+
+ /**
+ * Emitted to bind the item passed via [ property@Gtk.ListItem:item]
+ * to the widgets that have been created in step 1 or to add
+ * item-specific widgets. Signals are connected to listen to
+ * changes - both to changes in the item to update the widgets or to
+ * changes in the widgets to update the item. After this signal has been
+ * called, the listitem may be shown in a list widget.
+ */
+ private void item_factory_bind_cb (Gtk.SignalListItemFactory factory,
+ Gtk.ListItem item) {
+ unowned Notification noti = (Notification) item.get_child ();
+ unowned NotiListItemModel model = (NotiListItemModel) item.get_item ();
+ model.notification = noti;
+ noti.construct_notification (model.param,
+ model.noti_daemon,
+ model.notification_type);
+ noti.set_time ();
+ // TODO: Fix some large notifications being clipped for some reason...
+
+ visible_models.append (model);
+ }
+
+ private void item_factory_unbind_cb (Gtk.SignalListItemFactory factory,
+ Gtk.ListItem item) {
+ unowned NotiListItemModel model = (NotiListItemModel) item.get_item ();
+ visible_models.remove (model);
+ }
+
+ private void list_model_items_changed_cb () {
+ switch (notification_count) {
+ case 0:
+ stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
+ break;
+ default:
+ stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE);
+ break;
+ }
+ }
+
+ /*
+ * Private methods
+ */
+
+ private int model_sort_func (Object w1, Object w2) {
+ var a = (NotiListItemModel) w1;
+ var b = (NotiListItemModel) 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;
+ }
+
+ private void scroll_to_start (bool reverse) {
+ Gtk.ScrollType scroll_type = Gtk.ScrollType.START;
+ if (reverse) {
+ scroll_type = Gtk.ScrollType.END;
+ }
+ scrolled_window.scroll_child (scroll_type, false);
+ }
+
+ /*
+ * Overrides
+ */
+
+ public override Gtk.SizeRequestMode get_request_mode () {
+ return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
+ }
+
+ public override void measure (Gtk.Orientation orientation, int for_size,
+ out int minimum, out int natural,
+ out int minimum_baseline, out int natural_baseline) {
+ minimum = 0;
+ natural = 0;
+ minimum_baseline = -1;
+ natural_baseline = -1;
+
+ for (Gtk.Widget child = get_first_child ();
+ child != null;
+ child = child.get_next_sibling ()) {
+ int child_min = 0;
+ int child_nat = 0;
+ int child_min_baseline = -1;
+ int child_nat_baseline = -1;
+
+ child.measure (orientation, for_size,
+ out child_min, out child_nat,
+ out child_min_baseline, out child_nat_baseline);
+
+ minimum = int.max (minimum, child_min);
+ natural = int.max (natural, child_nat);
+
+ if (child_min_baseline > -1) {
+ minimum_baseline = int.max (minimum_baseline, child_min_baseline);
+ }
+ if (child_nat_baseline > -1) {
+ natural_baseline = int.max (natural_baseline, child_nat_baseline);
+ }
+ }
+ }
+
+ public override void size_allocate (int width, int height, int baseline) {
+ for (Gtk.Widget child = get_first_child ();
+ child != null;
+ child = child.get_next_sibling ()) {
+ if (!child.should_layout ()) continue;
+
+ child.allocate (width, height, baseline, null);
+ }
+
+ // Scroll to the top/latest notification
+ var adj = notification_list.vadjustment;
+ double upper = adj.get_upper ();
+ if (last_upper < upper) {
+ scroll_to_start (list_reverse);
+ }
+ last_upper = upper;
+ }
+
+ /*
+ * Public methods
+ */
+
+ public void set_list_orientation () {
+ PositionY pos_y = PositionY.NONE;
+ if (notification_type == NotificationType.CONTROL_CENTER)
+ pos_y = ConfigModel.instance.control_center_positionY;
+ if (pos_y == PositionY.NONE) pos_y = ConfigModel.instance.positionY;
+ switch (pos_y) {
+ default:
+ case PositionY.TOP:
+ list_reverse = false;
+ list_align = Gtk.Align.START;
+ break;
+ case PositionY.CENTER:
+ list_reverse = false;
+ list_align = Gtk.Align.START;
+ break;
+ case PositionY.BOTTOM:
+ list_reverse = true;
+ list_align = Gtk.Align.END;
+ break;
+ }
+
+ notification_list.set_valign (list_align);
+ }
+
+ public void close_all_notifications () {
+ while (list_model.n_items > 0) {
+ NotiListItemModel model = (NotiListItemModel) list_model.get_object (0);
+ if (model != null && model.notification != null) {
+ model.notification.close_notification (false);
+ }
+ list_model.remove (0);
+ }
+ }
+
+ public void close_notification (uint32 id, bool replaces = false) {
+ for (uint i = 0; i < notification_count; i++) {
+ NotiListItemModel model = (NotiListItemModel) list_model.get_object (i);
+ if (model != null && model.param.applied_id == id) {
+ unowned Notification noti = model.notification;
+ if (replaces) {
+ noti.remove_noti_timeout ();
+ } else {
+ noti.close_notification (false);
+ }
+ list_model.remove (i);
+ break;
+ }
+ }
+ }
+
+ public void add_notification (NotifyParams param,
+ NotiDaemon noti_daemon) {
+ NotiListItemModel model = new NotiListItemModel () {
+ noti_daemon = noti_daemon,
+ param = param,
+ notification_type = NotificationType.CONTROL_CENTER,
+ };
+
+ // TODO: Make sure that this works
+ // noti.focus_event.enter.connect ((w) => {
+ // uint i = 0;
+ // if (list_model.find (noti, out i) &&
+ // list_position != uint.MAX && list_position != i) {
+ // list_position = i;
+ // }
+ // });
+ // noti.set_time ();
+
+ list_model.insert_sorted (model, model_sort_func);
+ scroll_to_start (list_reverse);
+
+ // Keep focus on currently focused notification
+ grab_list_focus ();
+
+ if (notification_type == NotificationType.CONTROL_CENTER) {
+ navigate_list (++list_position);
+ }
+ }
+
+ public void navigate_list (uint i) {
+ var model = (NotiListItemModel) list_model.get_object (i);
+ if (model != null && model.notification != null) {
+ notification_list.set_focus_child (model.notification);
+ model.notification.grab_focus ();
+ }
+ }
+
+ /** Focus the first notification */
+ public void navigate_to_start () {
+ list_position = list_reverse ? (list_model.n_items - 1) : 0;
+ if (list_position == uint.MAX) list_position = 0;
+
+ grab_list_focus ();
+ navigate_list (list_position);
+ }
+
+ public void grab_list_focus () {
+ notification_list.grab_focus ();
+ }
+
+ // TODO: MAke sure that this works automatically in ::bind
+ public void refresh_notifications_time () {
+ for (uint i = 0; i < notification_count; i++) {
+ NotiListItemModel model = (NotiListItemModel) list_model.get_object (i);
+ if (model != null && model.notification != null) {
+ model.notification.set_time ();
+ }
+ }
+ }
+
+ public unowned NotiListItemModel ? get_latest_notification () {
+ Object ? object = null;
+ if (list_reverse) {
+ // last
+ object = list_model.get_item (uint.min (0, list_model.get_n_items () - 1));
+ } else {
+ // first
+ object = list_model.get_item (0);
+ }
+
+ if (object == null || !(object is NotiListItemModel)) return null;
+ return (NotiListItemModel) object;
+ }
+ }
+}
diff --git a/src/controlCenter/widgets/title/title.vala b/src/controlCenter/widgets/title/title.vala
index c031c64e..58ce5ed2 100644
--- a/src/controlCenter/widgets/title/title.vala
+++ b/src/controlCenter/widgets/title/title.vala
@@ -33,7 +33,9 @@ namespace SwayNotificationCenter.Widgets {
}
title_widget = new Gtk.Label (title);
- add (title_widget);
+ title_widget.halign = Gtk.Align.START;
+ title_widget.hexpand = true;
+ prepend (title_widget);
if (has_clear_all_button) {
clear_all_button = new Gtk.Button.with_label (button_text);
@@ -47,11 +49,9 @@ namespace SwayNotificationCenter.Widgets {
clear_all_button.set_can_focus (false);
clear_all_button.valign = Gtk.Align.CENTER;
// Backwards compatible towards older CSS stylesheets
- clear_all_button.get_style_context ().add_class ("control-center-clear-all");
- pack_end (clear_all_button, false);
+ clear_all_button.add_css_class ("control-center-clear-all");
+ append (clear_all_button);
}
-
- show_all ();
}
}
}
diff --git a/src/controlCenter/widgets/volume/sinkInputRow.vala b/src/controlCenter/widgets/volume/sinkInputRow.vala
index 71593be3..3f732904 100644
--- a/src/controlCenter/widgets/volume/sinkInputRow.vala
+++ b/src/controlCenter/widgets/volume/sinkInputRow.vala
@@ -15,16 +15,16 @@ namespace SwayNotificationCenter.Widgets {
update (sink_input);
scale.draw_value = false;
+ scale.hexpand = true;
icon.pixel_size = icon_size;
+ icon.set_tooltip_text (sink_input.get_display_name ());
container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+ container.append (icon);
+ container.append (scale);
- container.add (icon);
-
- container.pack_start (scale);
-
- add (container);
+ set_child (container);
scale.value_changed.connect (() => {
client.set_sink_input_volume (sink_input, (float) scale.get_value ());
@@ -35,15 +35,12 @@ namespace SwayNotificationCenter.Widgets {
public void update (PulseSinkInput sink_input) {
this.sink_input = sink_input;
+ icon.set_pixel_size (64);
icon.set_from_icon_name (
- sink_input.application_icon_name ?? "application-x-executable",
- Gtk.IconSize.DIALOG
- );
+ sink_input.application_icon_name ?? "application-x-executable");
scale.set_value (sink_input.volume);
scale.tooltip_text = ((int) scale.get_value ()).to_string ();
-
- this.show_all ();
}
}
}
diff --git a/src/controlCenter/widgets/volume/volume.vala b/src/controlCenter/widgets/volume/volume.vala
index 58306029..17795571 100644
--- a/src/controlCenter/widgets/volume/volume.vala
+++ b/src/controlCenter/widgets/volume/volume.vala
@@ -10,11 +10,11 @@ namespace SwayNotificationCenter.Widgets {
Gtk.Label label_widget = new Gtk.Label (null);
Gtk.Scale slider = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
- // Per app volume controll
- Gtk.ListBox levels_listbox;
+ // Per app volume control
+ List levels_rows = new List ();
+ Gtk.ListBox levels_listbox = new Gtk.ListBox ();
Gtk.Button reveal_button;
Gtk.Revealer revealer;
- Gtk.Label no_sink_inputs_label;
string empty_label = "No active sink input";
string expand_label = "⇧";
@@ -86,27 +86,25 @@ namespace SwayNotificationCenter.Widgets {
this.orientation = Gtk.Orientation.VERTICAL;
slider.draw_value = false;
+ slider.hexpand = true;
- main_volume_slider_container.add (label_widget);
- main_volume_slider_container.pack_start (slider, true, true, 0);
- add (main_volume_slider_container);
+ main_volume_slider_container.append (label_widget);
+ main_volume_slider_container.append (slider);
+ append (main_volume_slider_container);
if (show_per_app) {
reveal_button = new Gtk.Button.with_label (expand_label);
revealer = new Gtk.Revealer ();
revealer.transition_type = revealer_type;
revealer.transition_duration = revealer_duration;
- levels_listbox = new Gtk.ListBox ();
- levels_listbox.get_style_context ().add_class ("per-app-volume");
- revealer.add (levels_listbox);
-
- if (this.client.active_sinks.size == 0) {
- no_sink_inputs_label = new Gtk.Label (empty_label);
- levels_listbox.add (no_sink_inputs_label);
- }
+ levels_listbox.add_css_class ("per-app-volume");
+ levels_listbox.set_placeholder (new Gtk.Label (empty_label));
+ revealer.set_child (levels_listbox);
foreach (var item in this.client.active_sinks.values) {
- levels_listbox.add (new SinkInputRow (item, client, icon_size));
+ var row = new SinkInputRow (item, client, icon_size);
+ levels_rows.append (row);
+ levels_listbox.append (levels_rows.last ().data);
}
this.client.change_active_sink.connect (active_sink_change);
@@ -123,11 +121,9 @@ namespace SwayNotificationCenter.Widgets {
}
});
- main_volume_slider_container.pack_end (reveal_button, false, false, 0);
- add (revealer);
+ main_volume_slider_container.append (reveal_button);
+ append (revealer);
}
-
- show_all ();
}
public override void on_cc_visibility_change (bool val) {
@@ -147,39 +143,32 @@ namespace SwayNotificationCenter.Widgets {
}
private void active_sink_change (PulseSinkInput sink) {
- foreach (var row in levels_listbox.get_children ()) {
- if (row == null) continue;
- var s = (SinkInputRow) row;
- if (s.sink_input.cmp (sink)) {
- s.update (sink);
+ foreach (var widget in levels_rows) {
+ if (!(widget is SinkInputRow)) continue;
+ var row = (SinkInputRow) widget;
+ if (row.sink_input.cmp (sink)) {
+ row.update (sink);
break;
}
}
}
private void active_sink_added (PulseSinkInput sink) {
- // one element added -> remove the empty label
- if (this.client.active_sinks.size == 1) {
- var label = levels_listbox.get_children ().first ().data;
- levels_listbox.remove ((Gtk.Widget) label);
- }
- levels_listbox.add (new SinkInputRow (sink, client, icon_size));
- show_all ();
+ var row = new SinkInputRow (sink, client, icon_size);
+ levels_rows.append (row);
+ levels_listbox.append (row);
}
private void active_sink_removed (PulseSinkInput sink) {
- foreach (var row in levels_listbox.get_children ()) {
- if (row == null) continue;
- var s = (SinkInputRow) row;
- if (s.sink_input.cmp (sink)) {
+ foreach (var widget in levels_rows) {
+ if (!(widget is SinkInputRow)) continue;
+ var row = (SinkInputRow) widget;
+ if (row.sink_input.cmp (sink)) {
+ levels_rows.remove (row);
levels_listbox.remove (row);
break;
}
}
- if (levels_listbox.get_children ().length () == 0) {
- levels_listbox.add (no_sink_inputs_label);
- show_all ();
- }
}
}
}
diff --git a/src/customWidgets/blankWindow.vala b/src/customWidgets/blankWindow.vala
new file mode 100644
index 00000000..a459682b
--- /dev/null
+++ b/src/customWidgets/blankWindow.vala
@@ -0,0 +1,118 @@
+namespace SwayNotificationCenter {
+ public abstract class BlankWindow : Gtk.Window {
+ public unowned SwayncDaemon swaync_daemon;
+
+ private Gtk.GestureClick gesture_click;
+ private bool blank_window_down = false;
+ private bool blank_window_in = false;
+
+ protected BlankWindow (SwayncDaemon swaync_daemon) {
+ this.swaync_daemon = swaync_daemon;
+
+ add_css_class ("blank-window");
+ set_decorated (false);
+
+ if (swaync_daemon.use_layer_shell) {
+ if (!GtkLayerShell.is_supported ()) {
+ stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
+ stderr.printf ("Swaync only works on Wayland!\n");
+ stderr.printf ("If running wayland session, try running:\n");
+ stderr.printf ("\tGDK_BACKEND=wayland swaync\n");
+ Process.exit (1);
+ }
+ GtkLayerShell.init_for_window (this);
+ GtkLayerShell.set_namespace (this, "swaync-control-center");
+ GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.TOP, true);
+ GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.LEFT, true);
+ GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.RIGHT, true);
+ GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
+
+ set_layer_options ();
+ }
+
+ ((Gtk.Widget) this).realize.connect (() => {
+ set_layer_options ();
+ });
+
+ /*
+ * Handling of bank window presses (pressing outside of ControlCenter)
+ */
+ ((Gtk.Widget) this).add_controller (gesture_click = new Gtk.GestureClick () {
+ touch_only = false,
+ exclusive = true,
+ button = Gdk.BUTTON_PRIMARY,
+ propagation_phase = Gtk.PropagationPhase.BUBBLE,
+ });
+ gesture_click.pressed.connect ((n_press, x, y) => {
+ // Calculate if the clicked coords intersect the ControlCenter
+ Graphene.Point click_point = Graphene.Point ()
+ .init ((float) x, (float) y);
+ Graphene.Rect ? bounds = ignore_bounds ();
+ blank_window_in = !(bounds != null && bounds.contains_point (click_point));
+ blank_window_down = true;
+ });
+ gesture_click.released.connect ((n_press, x, y) => {
+ // Emit released
+ if (!blank_window_down) return;
+ blank_window_down = false;
+ if (blank_window_in) {
+ try {
+ swaync_daemon.set_visibility (false);
+ } catch (Error e) {
+ stderr.printf ("ControlCenter BlankWindow Click Error: %s\n",
+ e.message);
+ }
+ }
+
+ if (gesture_click.get_current_sequence () == null) {
+ blank_window_in = false;
+ }
+ });
+ gesture_click.update.connect ((gesture, sequence) => {
+ Gtk.GestureSingle gesture_single = (Gtk.GestureSingle) gesture;
+ if (sequence != gesture_single.get_current_sequence ()) return;
+ // Calculate if the clicked coords intersect the ControlCenter
+ double x, y;
+ gesture.get_point (sequence, out x, out y);
+ Graphene.Point click_point = Graphene.Point ()
+ .init ((float) x, (float) y);
+ Graphene.Rect ? bounds = ignore_bounds ();
+ if (bounds != null && bounds.contains_point (click_point)) {
+ blank_window_in = false;
+ }
+ });
+ gesture_click.cancel.connect (() => {
+ blank_window_down = false;
+ });
+ }
+
+ public abstract Graphene.Rect ? ignore_bounds ();
+
+ /** Called by `set_layer_options` */
+ public abstract void set_custom_options ();
+
+ protected void set_layer_options () {
+ if (swaync_daemon.use_layer_shell) {
+ GtkLayerShell.Layer layer;
+ switch (ConfigModel.instance.control_center_layer) {
+ case Layer.BACKGROUND:
+ layer = GtkLayerShell.Layer.BACKGROUND;
+ break;
+ case Layer.BOTTOM:
+ layer = GtkLayerShell.Layer.BOTTOM;
+ break;
+ case Layer.TOP:
+ layer = GtkLayerShell.Layer.TOP;
+ break;
+ default:
+ case Layer.OVERLAY:
+ layer = GtkLayerShell.Layer.OVERLAY;
+ break;
+ }
+ GtkLayerShell.set_layer (this, layer);
+ }
+
+ set_custom_options ();
+ }
+ }
+}
diff --git a/src/customWidgets/dismissibleWidget.vala b/src/customWidgets/dismissibleWidget.vala
new file mode 100644
index 00000000..d60d181b
--- /dev/null
+++ b/src/customWidgets/dismissibleWidget.vala
@@ -0,0 +1,197 @@
+namespace SwayNotificationCenter {
+ public enum SwipeDirection {
+ SWIPE_LEFT, SWIPE_RIGHT;
+ }
+
+ public class DismissibleWidget : Gtk.Widget, Adw.Swipeable {
+ unowned Gtk.Widget child;
+
+ // Animation
+ Adw.SpringAnimation animation;
+ Adw.AnimationTarget target;
+
+ // Swipe Gesture
+ Adw.SwipeTracker swipe_tracker;
+
+ bool transition_running = false;
+ bool gesture_active = false;
+ double child_offset = 0;
+ double swipe_progress = 0.0;
+
+ SwipeDirection swipe_direction = SwipeDirection.SWIPE_RIGHT;
+
+ public DismissibleWidget (Gtk.Widget child) {
+ this.child = child;
+ child.set_parent (this);
+
+ swipe_tracker = new Adw.SwipeTracker (this);
+ swipe_tracker.set_orientation (Gtk.Orientation.HORIZONTAL);
+ swipe_tracker.set_reversed (true);
+ swipe_tracker.set_allow_mouse_drag (true);
+
+ swipe_tracker.prepare.connect (swipe_prepare_cb);
+ swipe_tracker.update_swipe.connect (swipe_update_swipe_cb);
+ swipe_tracker.end_swipe.connect (swipe_end_swipe_cb);
+
+ double[] snap_dir = get_snap_points ();
+ target = new Adw.CallbackAnimationTarget (animate_value_cb);
+ animation = new Adw.SpringAnimation (this, snap_dir[0], snap_dir[1],
+ new Adw.SpringParams (1, 0.5, 500),
+ target);
+ animation.set_clamp (true);
+ animation.done.connect (animation_done_cb);
+ }
+
+ public signal void dismissed ();
+
+ /*
+ * Overrides
+ */
+
+ public override Gtk.SizeRequestMode get_request_mode () {
+ return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
+ }
+
+ public override void measure (Gtk.Orientation orientation, int for_size,
+ out int minimum, out int natural,
+ out int minimum_baseline, out int natural_baseline) {
+ minimum = 0;
+ natural = 0;
+ minimum_baseline = -1;
+ natural_baseline = -1;
+
+ int child_min, child_nat;
+
+ if (!child.visible) return;
+
+ child.measure (orientation, for_size,
+ out child_min, out child_nat, null, null);
+
+ minimum = int.max (minimum, child_min);
+ natural = int.max (natural, child_nat);
+ }
+
+ public override void size_allocate (int width, int height, int baseline) {
+ if (!child.visible) return;
+
+ int child_width, child_height;
+ int min = 0, nat = 0;
+
+ child.measure (swipe_tracker.orientation,
+ height, out min, out nat, null, null);
+
+ int size = width;
+ if (!child.hexpand) {
+ size = nat.clamp (min, width);
+ }
+
+ child_width = size;
+ child_height = height;
+
+ double x = 0;
+ if (get_direction () == Gtk.TextDirection.RTL) {
+ x -= ((size * swipe_progress) - (width - child_width) / 2.0)
+ + (size * child_offset * 2);
+ } else {
+ x -= - ((size * swipe_progress) - (width - child_width) / 2.0)
+ - (size * child_offset * 2);
+ }
+
+ Gsk.Transform transform = new Gsk.Transform ()
+ .translate (Graphene.Point ().init ((float) x, 0));
+
+ child.allocate (child_width, child_height, baseline, transform);
+ }
+
+ /*
+ * Callbacks
+ */
+
+ private void animate_value_cb (double value) {
+ set_position (value);
+ }
+
+ private void animation_done_cb () {
+ transition_running = false;
+ if (swipe_progress != 0) {
+ dismissed ();
+ }
+ }
+
+ private void swipe_prepare_cb (Adw.NavigationDirection direction) {
+ gesture_active = true;
+ if (transition_running) {
+ animation.pause ();
+ } else {
+ transition_running = true;
+ }
+ }
+
+ private void swipe_update_swipe_cb (double distance) {
+ set_position (distance);
+ }
+
+ private void swipe_end_swipe_cb (double velocity, double to) {
+ if (!gesture_active) return;
+
+ animation.set_value_from (swipe_progress);
+ animation.set_value_to (to);
+ animation.set_initial_velocity (velocity);
+
+ animation.play ();
+
+ gesture_active = false;
+ }
+
+ /*
+ * Methods
+ */
+
+ private void set_position (double value) {
+ this.swipe_progress = value;
+ queue_allocate ();
+ }
+
+ public void set_gesture_direction (SwipeDirection swipe_direction) {
+ this.swipe_direction = swipe_direction;
+ }
+
+ /*
+ * Swipe gesture
+ */
+
+ /** Gets the progress this will snap back to after the gesture is canceled. */
+ public double get_cancel_progress () {
+ return 0;
+ }
+ /** Gets the swipe distance of this. */
+ public double get_distance () {
+ return get_width ();
+ }
+ /** Gets the current progress of this. */
+ public double get_progress () {
+ if (!transition_running) return 0;
+ return this.swipe_progress;
+ }
+ /** Gets the snap points of this. */
+ public double[] get_snap_points () {
+ switch (swipe_direction) {
+ case SwayNotificationCenter.SwipeDirection.SWIPE_LEFT:
+ return new double[] { -1, 0 };
+ default:
+ case SwayNotificationCenter.SwipeDirection.SWIPE_RIGHT:
+ return new double[] { 0, 1 };
+ }
+ }
+ /**
+ * Gets the area this can start a swipe from for the given direction
+ * and gesture type.
+ */
+ public Gdk.Rectangle get_swipe_area (Adw.NavigationDirection direction,
+ bool is_drag) {
+ Gtk.Allocation alloc;
+ this.get_allocation (out alloc);
+ return alloc;
+ }
+ }
+}
diff --git a/src/customWidgets/iterBox.vala b/src/customWidgets/iterBox.vala
new file mode 100644
index 00000000..761ab0c7
--- /dev/null
+++ b/src/customWidgets/iterBox.vala
@@ -0,0 +1,40 @@
+namespace SwayNotificationCenter {
+ public class IterBox : Gtk.Box {
+ public uint length { get; private set; default = 0; }
+
+ private List children = new List ();
+
+ public IterBox (Gtk.Orientation orientation, int spacing) {
+ Object (orientation: orientation, spacing: spacing);
+ }
+
+ private void on_add (Gtk.Widget child) {
+ length++;
+ child.destroy.connect (() => {
+ children.remove (child);
+ });
+ }
+
+ public List get_children () {
+ return children.copy ();
+ }
+
+ public new void append (Gtk.Widget child) {
+ children.append (child);
+ base.append (children.last ().data);
+ on_add (child);
+ }
+
+ public new void prepend (Gtk.Widget child) {
+ children.prepend (child);
+ base.prepend (children.first ().data);
+ on_add (child);
+ }
+
+ public new void remove (Gtk.Widget child) {
+ children.remove (child);
+ base.remove (child);
+ length--;
+ }
+ }
+}
diff --git a/src/customWidgets/scaledImage.vala b/src/customWidgets/scaledImage.vala
new file mode 100644
index 00000000..93ab6f47
--- /dev/null
+++ b/src/customWidgets/scaledImage.vala
@@ -0,0 +1,54 @@
+namespace SwayNotificationCenter {
+ public class ScaledImage : Gtk.Widget {
+ private Gdk.Texture ? texture;
+ private Gtk.Image image;
+
+ public ScaledImage () {
+ this.set_overflow (Gtk.Overflow.HIDDEN);
+
+ this.image = new Gtk.Image ();
+ this.image.set_parent (this);
+
+ this.layout_manager = new Gtk.BinLayout ();
+ }
+
+ public override void snapshot (Gtk.Snapshot snap) {
+ if (texture == null) {
+ base.snapshot (snap);
+ return;
+ }
+
+ Functions.snapshot_apply_scaled_texture (snap, texture,
+ get_width (), get_height (),
+ scale_factor);
+ }
+
+ public void set_pixel_size (int pixel_size) {
+ image.set_pixel_size (pixel_size);
+ }
+
+ public void set_from_texture (owned Gdk.Texture texture) {
+ this.texture = texture;
+ queue_draw ();
+ }
+
+ public void set_from_pixbuf (Gdk.Pixbuf ? pixbuf) {
+ if (pixbuf != null) {
+ this.texture = Gdk.Texture.for_pixbuf (pixbuf);
+ } else {
+ this.texture = null;
+ }
+ queue_draw ();
+ }
+
+ public void set_from_gicon (Icon icon) {
+ texture = null;
+ image.set_from_gicon (icon);
+ }
+
+ public void set_from_icon_name (string ? icon_name) {
+ texture = null;
+ image.set_from_icon_name (icon_name);
+ }
+ }
+}
diff --git a/src/emptyWindow/emptyWindow.vala b/src/emptyWindow/emptyWindow.vala
new file mode 100644
index 00000000..7f7aaf5d
--- /dev/null
+++ b/src/emptyWindow/emptyWindow.vala
@@ -0,0 +1,19 @@
+namespace SwayNotificationCenter {
+ public class EmptyWindow : BlankWindow {
+ public unowned Gdk.Monitor monitor { get; private set; }
+
+ // TODO: Fix fully transparent windows not being shown...
+ public EmptyWindow (Gdk.Monitor mon, SwayncDaemon swaync_daemon) {
+ base (swaync_daemon);
+ monitor = mon;
+ }
+
+ public override void set_custom_options () {
+ GtkLayerShell.set_monitor (this, monitor);
+ }
+
+ public override Graphene.Rect? ignore_bounds () {
+ return null;
+ }
+ }
+}
diff --git a/src/functions.vala b/src/functions.vala
index 36a0f12f..06f3af57 100644
--- a/src/functions.vala
+++ b/src/functions.vala
@@ -8,61 +8,45 @@ namespace SwayNotificationCenter {
public static void init () {
system_css_provider = new Gtk.CssProvider ();
user_css_provider = new Gtk.CssProvider ();
+
+ // Init resources
+ var theme = Gtk.IconTheme.get_for_display (Gdk.Display.get_default ());
+ theme.add_resource_path ("/org/erikreider/swaync/icons");
}
public static void set_image_path (owned string path,
Gtk.Image img,
- int icon_size,
bool file_exists) {
+ // img.set_pixel_size (Notification.icon_size);
if ((path.length > 6 && path.slice (0, 7) == "file://") || file_exists) {
// Try as a URI (file:// is the only URI schema supported right now)
try {
if (!file_exists) path = path.slice (7, path.length);
-
- var pixbuf = new Gdk.Pixbuf.from_file_at_scale (
- path,
- icon_size * img.scale_factor,
- icon_size * img.scale_factor,
- true);
- var surface = Gdk.cairo_surface_create_from_pixbuf (
- pixbuf,
- img.scale_factor,
- img.get_window ());
- img.set_from_surface (surface);
+ Gdk.Texture texture = Gdk.Texture.from_filename (path);
+ img.set_from_paintable (texture);
return;
} catch (Error e) {
stderr.printf (e.message + "\n");
}
- } else if (Gtk.IconTheme.get_default ().has_icon (path)) {
+ } else if (Gtk.IconTheme.get_for_display (img.get_display ()).has_icon (path)) {
// Try as a freedesktop.org-compliant icon theme
- img.set_from_icon_name (path, Notification.icon_size);
+ img.set_from_icon_name (path);
} else {
- img.set_from_icon_name (
- "image-missing",
- Notification.icon_size);
+ img.set_from_icon_name ("image-missing");
}
}
- public static void set_image_data (ImageData data, Gtk.Image img, int icon_size) {
- // Rebuild and scale the image
- var pixbuf = new Gdk.Pixbuf.with_unowned_data (data.data,
- Gdk.Colorspace.RGB,
- data.has_alpha,
- data.bits_per_sample,
- data.width,
- data.height,
- data.rowstride,
- null);
-
- pixbuf = pixbuf.scale_simple (
- icon_size * img.scale_factor,
- icon_size * img.scale_factor,
- Gdk.InterpType.BILINEAR);
- var surface = Gdk.cairo_surface_create_from_pixbuf (
- pixbuf,
- img.scale_factor,
- img.get_window ());
- img.set_from_surface (surface);
+ public static void set_image_data (ImageData data, Gtk.Image img) {
+ Gdk.MemoryFormat format = Gdk.MemoryFormat.R8G8B8;
+ if (data.has_alpha) {
+ format = Gdk.MemoryFormat.R8G8B8A8;
+ }
+ // TODO: Handle images with more channels?
+ var texture = new Gdk.MemoryTexture (data.width, data.height,
+ format,
+ new Bytes.static (data.data),
+ data.rowstride);
+ img.set_from_paintable (texture);
}
/** Load the package provided CSS file as a base.
@@ -72,46 +56,44 @@ namespace SwayNotificationCenter {
public static bool load_css (string ? style_path) {
int css_priority = ConfigModel.instance.cssPriority.get_priority ();
- try {
- // Load packaged CSS as backup
+ // Load packaged CSS as backup
+ if (!no_base_css) {
string system_css = get_style_path (null, true);
system_css_provider.load_from_path (system_css);
- Gtk.StyleContext.add_provider_for_screen (
- Gdk.Screen.get_default (),
+ Gtk.StyleContext.add_provider_for_display (
+ Gdk.Display.get_default (),
system_css_provider,
css_priority);
- } catch (Error e) {
- print ("Load packaged CSS Error: %s\n", e.message);
- return false;
}
- try {
- // Load user CSS
- string user_css = get_style_path (style_path);
- user_css_provider.load_from_path (user_css);
- Gtk.StyleContext.add_provider_for_screen (
- Gdk.Screen.get_default (),
- user_css_provider,
- css_priority);
- return true;
- } catch (Error e) {
- print ("Load user CSS Error: %s\n", e.message);
- return false;
+ // Load user CSS
+ string user_css = get_style_path (style_path);
+ user_css_provider.load_from_path (user_css);
+ Gtk.StyleContext.add_provider_for_display (
+ Gdk.Display.get_default (),
+ user_css_provider,
+ css_priority);
+ return true;
+ }
+
+ public static string clean_path (owned string path) {
+ // Replaces the home directory relative path with a absolute path
+ if (path.get (0) == '~') {
+ path = Environment.get_home_dir () + path[1 :];
}
+ return path;
}
public static string get_style_path (owned string ? custom_path,
bool only_system = false) {
string[] paths = {
// Fallback location. Specified in postinstall.py
+ "/usr/etc/xdg/swaync/style.css",
"/usr/local/etc/xdg/swaync/style.css"
};
if (custom_path != null && custom_path.length > 0) {
// Replaces the home directory relative path with a absolute path
- if (custom_path.get (0) == '~') {
- custom_path = Environment.get_home_dir () + custom_path[1:];
- }
- paths += custom_path;
+ paths += clean_path (custom_path);
}
if (!only_system) {
paths += Path.build_path (Path.DIR_SEPARATOR.to_string (),
@@ -147,7 +129,7 @@ namespace SwayNotificationCenter {
if (custom_path != null && (custom_path = custom_path.strip ()).length > 0) {
// Replaces the home directory relative path with a absolute path
if (custom_path.get (0) == '~') {
- custom_path = Environment.get_home_dir () + custom_path[1:];
+ custom_path = Environment.get_home_dir () + custom_path[1 :];
}
if (File.new_for_path (custom_path).query_exists ()) {
@@ -203,82 +185,60 @@ namespace SwayNotificationCenter {
return type;
}
- /** Scales the pixbuf to fit the given dimensions */
- public static Gdk.Pixbuf scale_round_pixbuf (Gdk.Pixbuf pixbuf,
- int buffer_width,
- int buffer_height,
- int img_scale,
- int radius) {
- Cairo.Surface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32,
- buffer_width,
- buffer_height);
- var cr = new Cairo.Context (surface);
-
- // Border radius
- const double DEGREES = Math.PI / 180.0;
- cr.new_sub_path ();
- cr.arc (buffer_width - radius, radius, radius, -90 * DEGREES, 0 * DEGREES);
- cr.arc (buffer_width - radius, buffer_height - radius, radius, 0 * DEGREES, 90 * DEGREES);
- cr.arc (radius, buffer_height - radius, radius, 90 * DEGREES, 180 * DEGREES);
- cr.arc (radius, radius, radius, 180 * DEGREES, 270 * DEGREES);
- cr.close_path ();
- cr.set_source_rgb (0, 0, 0);
- cr.clip ();
- cr.paint ();
-
- cr.save ();
- Cairo.Surface scale_surf = Gdk.cairo_surface_create_from_pixbuf (pixbuf,
- img_scale,
- null);
- int width = pixbuf.width / img_scale;
- int height = pixbuf.height / img_scale;
- double window_ratio = (double) buffer_width / buffer_height;
- double bg_ratio = width / height;
+ /** Scales and applies a scaled texture to fit the given dimensions */
+ public static void snapshot_apply_scaled_texture (Gtk.Snapshot snap,
+ Gdk.Texture texture,
+ float buffer_width,
+ float buffer_height,
+ float img_scale) {
+ float width = texture.width / img_scale;
+ float height = texture.height / img_scale;
+ float window_ratio = buffer_width / buffer_height;
+ float bg_ratio = width / height;
+ snap.save ();
if (window_ratio > bg_ratio) { // Taller wallpaper than monitor
- double scale = (double) buffer_width / width;
+ float scale = buffer_width / width;
if (scale * height < buffer_height) {
- draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf);
+ translate_wide (buffer_width, width, buffer_height, height, snap);
} else {
- draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf);
+ translate_tall (buffer_width, width, buffer_height, height, snap);
}
} else { // Wider wallpaper than monitor
- double scale = (double) buffer_height / height;
+ float scale = buffer_height / height;
if (scale * width < buffer_width) {
- draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf);
+ translate_tall (buffer_width, width, buffer_height, height, snap);
} else {
- draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf);
+ translate_wide (buffer_width, width, buffer_height, height, snap);
}
}
- cr.paint ();
- cr.restore ();
-
- scale_surf.finish ();
- return Gdk.pixbuf_get_from_surface (surface, 0, 0, buffer_width, buffer_height);
+ snap.append_scaled_texture (
+ texture,
+ Gsk.ScalingFilter.TRILINEAR,
+ Graphene.Rect ().init (0, 0, width, height)
+ );
+ snap.restore ();
}
- private static void draw_scale_tall (int buffer_width,
- int width,
- int buffer_height,
- int height,
- Cairo.Context cr,
- Cairo.Surface surface) {
- double scale = (double) buffer_width / width;
- cr.scale (scale, scale);
- cr.set_source_surface (surface,
- 0, (double) buffer_height / 2 / scale - height / 2);
+ private static void translate_tall (float buffer_width,
+ float width,
+ float buffer_height,
+ float height,
+ Gtk.Snapshot snap) {
+ float scale = buffer_width / width;
+ snap.scale (scale, scale);
+ snap.translate (Graphene.Point ().init (
+ 0, buffer_height / 2 / scale - height / 2));
}
- private static void draw_scale_wide (int buffer_width,
- int width,
- int buffer_height,
- int height,
- Cairo.Context cr,
- Cairo.Surface surface) {
- double scale = (double) buffer_height / height;
- cr.scale (scale, scale);
- cr.set_source_surface (
- surface,
- (double) buffer_width / 2 / scale - width / 2, 0);
+ private static void translate_wide (float buffer_width,
+ float width,
+ float buffer_height,
+ float height,
+ Gtk.Snapshot snap) {
+ float scale = (float) buffer_height / height;
+ snap.scale (scale, scale);
+ snap.translate (Graphene.Point ().init (
+ (float) buffer_width / 2 / scale - width / 2, 0));
}
public delegate bool FilterFunc (char character);
diff --git a/src/main.vala b/src/main.vala
index 057b988c..2af87173 100644
--- a/src/main.vala
+++ b/src/main.vala
@@ -1,19 +1,16 @@
namespace SwayNotificationCenter {
static SwayncDaemon swaync_daemon;
+ // Args
static string ? style_path;
static string ? config_path;
+ // Dev args
+ static bool no_base_css = false;
static uint layer_shell_protocol_version = 3;
static Settings self_settings;
public void main (string[] args) {
- Gtk.init (ref args);
- Hdy.init ();
- Functions.init ();
-
- self_settings = new Settings ("org.erikreider.swaync");
-
if (args.length > 0) {
for (uint i = 1; i < args.length; i++) {
string arg = args[i];
@@ -22,6 +19,16 @@ namespace SwayNotificationCenter {
case "--style":
style_path = args[++i];
break;
+ case "-D":
+ string dev_arg = args[++i];
+ switch (dev_arg) {
+ case "no-base-css":
+ no_base_css = true;
+ break;
+ default:
+ break;
+ }
+ break;
case "-c":
case "--config":
config_path = args[++i];
@@ -39,25 +46,40 @@ namespace SwayNotificationCenter {
}
}
- ConfigModel.init (config_path);
- Functions.load_css (style_path);
+ Adw.init ();
+ Gtk.init ();
+ Functions.init ();
- if (ConfigModel.instance.layer_shell) {
- layer_shell_protocol_version = GtkLayerShell.get_protocol_version ();
- }
+ var app = new Gtk.Application ("org.erikreider.swaync", ApplicationFlags.DEFAULT_FLAGS);
- swaync_daemon = new SwayncDaemon ();
- Bus.own_name (BusType.SESSION, "org.erikreider.swaync.cc",
- BusNameOwnerFlags.NONE,
- on_cc_bus_aquired,
- () => {},
- () => {
- stderr.printf (
- "Could not acquire swaync name!...\n");
- Process.exit (1);
+ app.activate.connect (() => {
+ self_settings = new Settings ("org.erikreider.swaync");
+
+ ConfigModel.init (config_path);
+ Functions.load_css (style_path);
+
+ if (ConfigModel.instance.layer_shell) {
+ layer_shell_protocol_version = GtkLayerShell.get_protocol_version ();
+ }
+
+ swaync_daemon = new SwayncDaemon ();
+ // TODO: Remove ".cc"/"/cc" for all servers and client
+ Bus.own_name (BusType.SESSION, "org.erikreider.swaync.cc",
+ BusNameOwnerFlags.NONE,
+ on_cc_bus_aquired,
+ () => {},
+ () => {
+ stderr.printf (
+ "Could not acquire swaync name!...\n");
+ Process.exit (1);
+ });
+ app.add_window (swaync_daemon.noti_daemon.control_center);
});
- Gtk.main ();
+ // Gtk.init ();
+
+ // new MainLoop ().run ();
+ app.run (null);
}
void on_cc_bus_aquired (DBusConnection conn) {
diff --git a/src/meson.build b/src/meson.build
index 78ce9f1e..cedea096 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -26,6 +26,8 @@ widget_sources = [
# Helpers
'controlCenter/widgets/baseWidget.vala',
'controlCenter/widgets/factory.vala',
+ # Widget: Notifications
+ 'controlCenter/widgets/notifications/notifications.vala',
# Widget: Title
'controlCenter/widgets/title/title.vala',
# Widget: Dnd
@@ -55,27 +57,107 @@ widget_sources = [
app_sources = [
'main.vala',
- 'orderedHashTable/orderedHashTable.vala',
+ 'functions.vala',
'configModel/configModel.vala',
+ 'orderedHashTable/orderedHashTable.vala',
+ 'customWidgets/iterBox.vala',
+ 'customWidgets/blankWindow.vala',
+ 'customWidgets/dismissibleWidget.vala',
+ 'customWidgets/scaledImage.vala',
'swayncDaemon/swayncDaemon.vala',
'notiDaemon/notiDaemon.vala',
'notiModel/notiModel.vala',
'notificationWindow/notificationWindow.vala',
'notification/notification.vala',
+ 'notification/notificationContent.vala',
'controlCenter/controlCenter.vala',
+ 'emptyWindow/emptyWindow.vala',
widget_sources,
- 'blankWindow/blankWindow.vala',
- 'functions.vala',
constants,
]
+# vapi_dir = meson.current_source_dir() / 'vapi'
+
+libadwaita_version = '>= 1.3.2'
+libadwaita_dep = dependency(
+ 'libadwaita-1',
+ # 'libadwaita-0',
+ version: libadwaita_version,
+ required: false,
+)
+if not libadwaita_dep.found()
+ libadwaita_project = subproject(
+ 'libadwaita',
+ version: libadwaita_version,
+ )
+ libadwaita_dep = declare_dependency(
+ dependencies: [
+ libadwaita_project.get_variable('libadwaita_dep'),
+ libadwaita_project.get_variable('libadwaita_vapi'),
+ ],
+ )
+ libadwaita_vapi = meson.build_root() / 'subprojects' / 'libadwaita' / 'src'
+ add_project_arguments(['--vapidir', libadwaita_vapi], language: 'vala')
+endif
+
+# libadwaita = dependency(
+# 'libadwaita-1',
+# version: '>= 1.3.2',
+# fallback: ['libadwaita', 'libadwaita_dep'],
+# )
+
+layershell_version = '>= 1.0.0'
+# layershell_proj = subproject(
+# 'gtk4-layer-shell',
+# required: false,
+# version: layershell_version,
+# )
+# layershell_dep = dependency(
+# 'gtk4-layer-shell-0',
+# fallback: ['gtk4-layer-shell', 'gtk_layer_shell'],
+# version: layershell_version,
+# required: false,
+# )
+# layershell_vapi = ''
+# if not layershell.found()
+# layershell_project = subproject(
+# 'gtk4-layer-shell',
+# version: layershell_version,
+# )
+# layershell = declare_dependency(
+# dependencies: [
+# layershell_project.get_variable('gtk_layer_shell'),
+# layershell_project.get_variable('vapi'),
+# ],
+# )
+# layershell_vapi = meson.build_root() / 'subprojects' / 'gtk4-layer-shell' / 'src'
+# endif
+
+# if not layershell_dep.found()
+# layershell_proj = subproject(
+# 'gtk4-layer-shell',
+# required: false,
+# version: layershell_version,
+# )
+# # layershell_lib = layershell_proj.get_variable('gtk_layer_shell_lib')
+# layershell_dep = layershell_proj.get_variable('gtk_layer_shell')
+# layershell_vapi = layershell_proj.get_variable('vapi')
+# # layershell = dependency('gtk4-layer-shell-0', version: layershell_version)
+# endif
+
app_deps = [
+ # layershell_lib,
+ # layershell_dep,
+ # layershell_vapi,
dependency('gio-2.0', version: '>= 2.50'),
dependency('gio-unix-2.0', version: '>= 2.50'),
- dependency('gtk+-3.0', version: '>= 3.22'),
+ dependency('gtk4', version: '>= 4.11.3'),
+ libadwaita_dep,
+ # libadwaita,
+ # dependency('libadwaita-1', version: '>= 1.3.2'),
dependency('json-glib-1.0', version: '>= 1.0'),
- dependency('libhandy-1', version: '>= 1.2.3'),
- meson.get_compiler('c').find_library('gtk-layer-shell'),
+ dependency('gtk4-layer-shell-0', version: layershell_version),
+ # dependency('libhandy-1', version: '>= 1.2.3'),
meson.get_compiler('c').find_library('m', required : true),
meson.get_compiler('vala').find_library('posix'),
dependency('gee-0.8'),
@@ -89,37 +171,41 @@ if get_option('scripting')
endif
# Detect libhandy version
-libhandy = dependency('libhandy-1')
-if libhandy.version() >= '1.3.9'
- add_project_arguments('-D', 'HAVE_LATEST_LIBHANDY', language: 'vala')
-endif
+# libhandy = dependency('libhandy-1')
+# if libhandy.version() >= '1.3.9'
+# add_project_arguments('-D', 'HAVE_LATEST_LIBHANDY', language: 'vala')
+# endif
# Detect gtk-layer-shell version
-gtk_layer_shell = dependency(
- 'gtk-layer-shell-0',
- fallback: ['gtk-layer-shell-0', 'gtk-layer-shell'],
-)
-if gtk_layer_shell.version() >= '0.6.0'
- add_project_arguments('-D', 'HAVE_LATEST_GTK_LAYER_SHELL', language: 'vala')
-endif
+# gtk_layer_shell = dependency(
+# 'gtk-layer-shell-0',
+# fallback: ['gtk-layer-shell-0', 'gtk-layer-shell'],
+# )
+# if gtk_layer_shell.version() >= '0.6.0'
+# add_project_arguments('-D', 'HAVE_LATEST_GTK_LAYER_SHELL', language: 'vala')
+# endif
args = [
- '--target-glib=2.50',
- '--pkg=GtkLayerShell-0.1',
+ '--target-glib=2.74',
+ # '--pkg=GtkLayerShell-0.1',
+ '--library=gtk4',
+ '--library=libadwaita-1',
]
sysconfdir = get_option('sysconfdir')
-gnome = import('gnome')
-app_sources += gnome.compile_resources('sway_notification_center-resources',
- 'sway_notification_center.gresource.xml',
+app_resources += gnome.compile_resources('sway_notification_center-resources',
+ 'swaync_template.gresource.xml',
c_name: 'sway_notification_center'
)
+# add_project_arguments(['--vapidir', vapi_dir], language: 'vala')
+
executable('swaync',
- app_sources,
+ [ app_sources, app_resources ],
vala_args: args,
dependencies: app_deps,
+ # link_with: [layershell_lib],
install: true,
)
diff --git a/src/notiDaemon/notiDaemon.vala b/src/notiDaemon/notiDaemon.vala
index e8347712..a0238164 100644
--- a/src/notiDaemon/notiDaemon.vala
+++ b/src/notiDaemon/notiDaemon.vala
@@ -9,6 +9,7 @@ namespace SwayNotificationCenter {
new HashTable (str_hash, str_equal);
public ControlCenter control_center;
+ public NotificationWindow notification_window;
public unowned SwayncDaemon swaync_daemon;
@@ -18,8 +19,8 @@ namespace SwayNotificationCenter {
this.notify["dnd"].connect (() => on_dnd_toggle (dnd));
on_dnd_toggle.connect ((dnd) => {
- if (!dnd || NotificationWindow.is_null) return;
- NotificationWindow.instance.close_all_notifications ((noti) => {
+ if (!dnd) return;
+ notification_window.close_all_notifications ((noti) => {
return noti.param.urgency != UrgencyLevels.CRITICAL;
});
});
@@ -27,6 +28,7 @@ namespace SwayNotificationCenter {
// Init dnd from gsettings
self_settings.bind ("dnd-state", this, "dnd", SettingsBindFlags.DEFAULT);
+ this.notification_window = new NotificationWindow (swaync_daemon, this);
this.control_center = new ControlCenter (swaync_daemon, this);
}
@@ -34,12 +36,13 @@ namespace SwayNotificationCenter {
* Changes the popup-notification window visibility.
* Closes all notifications and hides window if `value` is false
*/
- public void set_noti_window_visibility (bool value)
- throws DBusError, IOError {
- NotificationWindow.instance.change_visibility (value);
+ [DBus (visible = false)]
+ public void hide_notification_window (bool value) throws DBusError, IOError {
+ notification_window.change_visibility (value);
}
/** Toggles the current Do Not Disturb state */
+ [DBus (visible = false)]
public bool toggle_dnd () throws DBusError, IOError {
return dnd = !dnd;
}
@@ -62,7 +65,7 @@ namespace SwayNotificationCenter {
/** Method to close notification and send DISMISSED signal */
public void manually_close_notification (uint32 id, bool timeout)
throws DBusError, IOError {
- NotificationWindow.instance.close_notification (id, false);
+ notification_window.close_notification (id, false);
if (!timeout) {
control_center.close_notification (id);
NotificationClosed (id, ClosedReasons.DISMISSED);
@@ -76,14 +79,14 @@ namespace SwayNotificationCenter {
/** Closes all popup and controlcenter notifications */
public void close_all_notifications () throws DBusError, IOError {
- NotificationWindow.instance.close_all_notifications ();
+ notification_window.close_all_notifications ();
control_center.close_all_notifications ();
}
/** Closes latest popup notification */
public void hide_latest_notification (bool close)
throws DBusError, IOError {
- uint32 ? id = NotificationWindow.instance.get_latest_notification ();
+ uint32 ? id = notification_window.get_latest_notification ();
if (id == null) return;
manually_close_notification (id, !close);
}
@@ -173,7 +176,7 @@ namespace SwayNotificationCenter {
// Replace notification logic
if (id == replaces_id) {
param.replaces = true;
- NotificationWindow.instance.close_notification (id, true);
+ notification_window.close_notification (id, true);
control_center.close_notification (id, true);
} else if (param.synchronous != null
&& param.synchronous.length > 0) {
@@ -184,7 +187,7 @@ namespace SwayNotificationCenter {
param.synchronous, null, out r_id)) {
param.replaces = true;
// Close the notification
- NotificationWindow.instance.close_notification (r_id, true);
+ notification_window.close_notification (r_id, true);
control_center.close_notification (r_id, true);
}
synchronous_ids.set (param.synchronous, id);
@@ -197,7 +200,7 @@ namespace SwayNotificationCenter {
if (param.urgency == UrgencyLevels.CRITICAL ||
(!dnd && !swaync_daemon.inhibited
&& param.urgency != UrgencyLevels.CRITICAL)) {
- NotificationWindow.instance.add_notification (param, this);
+ notification_window.add_notification (param, this);
}
}
// Only add notification to CC if it isn't IGNORED and not transient/TRANSIENT
@@ -292,7 +295,7 @@ namespace SwayNotificationCenter {
*/
[DBus (name = "CloseNotification")]
public void close_notification (uint32 id) throws DBusError, IOError {
- NotificationWindow.instance.close_notification (id, false);
+ notification_window.close_notification (id, false);
control_center.close_notification (id);
NotificationClosed (id, ClosedReasons.CLOSED_BY_CLOSENOTIFICATION);
}
diff --git a/src/notification/notification.ui b/src/notification/notification.ui
deleted file mode 100644
index 7c38125e..00000000
--- a/src/notification/notification.ui
+++ /dev/null
@@ -1,317 +0,0 @@
-
-
-
-
-
-
- True
- True
-
-
- True
- False
- crossfade
-
-
- True
- False
-
-
- True
- False
- True
-
-
-
-
-
-
-
- True
- False
- True
-
-
- True
- False
- vertical
-
-
- True
- False
-
-
- True
- False
- vertical
-
-
- True
- False
- GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_STRUCTURE_MASK
-
-
- True
- False
- vertical
- 8
-
-
- True
- False
-
-
- True
- False
- center
- 12
- 6
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- center
- 14
- True
- True
- vertical
-
-
- True
- False
- center
-
-
- True
- False
- end
- 0
- 0
-
-
-
- True
- True
- 0
-
-
-
-
- True
- False
- start
- 6
- False
-
-
-
- False
- True
- end
- 1
-
-
-
-
- False
- False
- 0
-
-
-
-
- True
- False
- True
- True
- word-char
- end
- 1
- 0
- 0
-
-
-
- False
- False
- 1
-
-
-
-
- True
- True
- 1
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
-
-
- False
- True
- 1
-
-
-
-
- False
- center
-
-
-
- False
- False
- 2
-
-
-
-
- False
-
-
- True
- True
- GTK_INPUT_HINT_SPELLCHECK | GTK_INPUT_HINT_EMOJI | GTK_INPUT_HINT_NONE
-
-
-
- True
- True
- 0
-
-
-
-
- True
- True
- True
-
-
-
- False
- True
- 1
-
-
-
-
-
- False
- True
- 3
-
-
-
-
-
-
-
-
- False
- True
- 0
-
-
-
-
-
- -1
-
-
-
-
- True
- False
- end
- start
- crossfade
-
-
- True
- True
- False
- center
- center
- 4
- 4
- 2
- 2
- none
-
-
- True
- False
- window-close-symbolic
-
-
-
-
-
-
-
-
-
- False
- True
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/notification/notification.vala b/src/notification/notification.vala
index b5b2db89..2dc11bcb 100644
--- a/src/notification/notification.vala
+++ b/src/notification/notification.vala
@@ -1,61 +1,15 @@
namespace SwayNotificationCenter {
-
public enum NotificationType { CONTROL_CENTER, POPUP }
- [GtkTemplate (ui = "/org/erikreider/sway-notification-center/notification/notification.ui")]
- public class Notification : Gtk.ListBoxRow {
- [GtkChild]
- unowned Gtk.Revealer revealer;
- [GtkChild]
- unowned Hdy.Carousel carousel;
-
- [GtkChild]
- unowned Gtk.EventBox event_box;
+ public class Notification : Gtk.Widget {
+ Gtk.Revealer revealer;
- [GtkChild]
- unowned Gtk.EventBox default_action;
+ DismissibleWidget dismissible_widget;
+ NotificationContent notification_content;
/** The default_action gesture. Allows clicks while not in swipe gesture. */
- private Gtk.GestureMultiPress gesture;
-
- [GtkChild]
- unowned Gtk.ProgressBar progress_bar;
-
- [GtkChild]
- unowned Gtk.Box base_box;
-
- [GtkChild]
- unowned Gtk.Revealer close_revealer;
- [GtkChild]
- unowned Gtk.Button close_button;
-
- private Gtk.ButtonBox alt_actions_box = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL);
-
- [GtkChild]
- unowned Gtk.Label summary;
- [GtkChild]
- unowned Gtk.Label time;
- [GtkChild]
- unowned Gtk.Label body;
- [GtkChild]
- unowned Gtk.Image img;
- [GtkChild]
- unowned Gtk.Image body_image;
-
- // Inline Reply
- [GtkChild]
- unowned Gtk.Box inline_reply_box;
- [GtkChild]
- unowned Gtk.Entry inline_reply_entry;
- [GtkChild]
- unowned Gtk.Button inline_reply_button;
-
- private bool default_action_down = false;
- private bool default_action_in = false;
-
- public static Gtk.IconSize icon_size = Gtk.IconSize.INVALID;
- private int notification_icon_size { get; default = ConfigModel.instance.notification_icon_size; }
+ public Gtk.EventControllerFocus focus_event = new Gtk.EventControllerFocus ();
private int notification_body_image_height {
get;
@@ -68,526 +22,167 @@ namespace SwayNotificationCenter {
private uint timeout_id = 0;
- public bool is_timed { get; construct; default = false; }
-
- public NotifyParams param { get; construct; }
- public NotiDaemon noti_daemon { get; construct; }
+ public NotifyParams param { get; private set; }
+ public NotiDaemon noti_daemon { get; private set; }
public NotificationType notification_type {
get;
- construct;
+ private set;
default = NotificationType.POPUP;
}
- public uint timeout_delay { get; construct; }
- public uint timeout_low_delay { get; construct; }
- public uint timeout_critical_delay { get; construct; }
-
- public int transition_time { get; construct; }
-
- public int number_of_body_lines { get; construct; default = 10; }
-
- public bool has_inline_reply { get; private set; default = false; }
-
- private int carousel_empty_widget_index = 0;
-
- private static Regex code_regex;
+ public uint timeout_delay { get; private set; }
+ public uint timeout_low_delay { get; private set; }
+ public uint timeout_critical_delay { get; private set; }
- private static Regex tag_regex;
- private static Regex tag_unescape_regex;
- private static Regex img_tag_regex;
- private const string[] TAGS = { "b", "u", "i" };
- private const string[] UNESCAPE_CHARS = {
- "lt;", "#60;", "#x3C;", "#x3c;", // <
- "gt;", "#62;", "#x3E;", "#x3e;", // >
- "apos;", "#39;", // '
- "quot;", "#34;", // "
- "amp;" // &
- };
-
- private Notification () {}
-
- /** Show a non-timed notification */
- public Notification.regular (NotifyParams param,
- NotiDaemon noti_daemon,
- NotificationType notification_type) {
- Object (noti_daemon: noti_daemon,
- param: param,
- notification_type: notification_type);
+ public int transition_time {
+ get;
+ private set;
+ default = ConfigModel.instance.transition_time;
}
- /** Show a timed notification */
- public Notification.timed (NotifyParams param,
- NotiDaemon noti_daemon,
- NotificationType notification_type,
- uint timeout,
- uint timeout_low,
- uint timeout_critical) {
- Object (noti_daemon: noti_daemon,
- param: param,
- notification_type: notification_type,
- is_timed: true,
- timeout_delay: timeout,
- timeout_low_delay: timeout_low,
- timeout_critical_delay: timeout_critical,
- number_of_body_lines: 5
- );
+ public bool has_inline_reply {
+ get { return notification_content.has_inline_reply; }
}
- construct {
- try {
- code_regex = new Regex ("(?<= |^)(\\d{3}(-| )\\d{3}|\\d{4,7})(?= |$|\\.|,)",
- RegexCompileFlags.MULTILINE);
- string joined_tags = string.joinv ("|", TAGS);
- tag_regex = new Regex ("<(/?(?:%s))>".printf (joined_tags));
- string unescaped = string.joinv ("|", UNESCAPE_CHARS);
- tag_unescape_regex = new Regex ("&(?=%s)".printf (unescaped));
- img_tag_regex = new Regex ("""]* src=\"([^\"]*)\"[^>]*>""");
- } catch (Error e) {
- stderr.printf ("Invalid regex: %s", e.message);
- }
+ public bool is_constructed { get; private set; default = false; }
- // Build the default_action gesture. Makes clickes compatible with
- // the Hdy Swipe gesture unlike a regular ::button_release_event
- gesture = new Gtk.GestureMultiPress (default_action);
- gesture.set_touch_only (false);
- gesture.set_exclusive (true);
- gesture.set_button (Gdk.BUTTON_PRIMARY);
- gesture.set_propagation_phase (Gtk.PropagationPhase.BUBBLE);
- gesture.pressed.connect ((_gesture, _n_press, _x, _y) => {
- default_action_in = true;
- default_action_down = true;
- default_action_update_state ();
- });
- gesture.released.connect ((gesture, _n_press, _x, _y) => {
- // Emit released
- if (!default_action_down) return;
- default_action_down = false;
- if (default_action_in) {
- click_default_action ();
- }
+ public Notification () {
+ add_css_class ("notification-row");
+ (revealer = new Gtk.Revealer () {
+ reveal_child = false,
+ // TODO: Add config option?
+ transition_type = Gtk.RevealerTransitionType.CROSSFADE
+ }).set_parent (this);
+ notification_content = new NotificationContent (this);
+ dismissible_widget = new DismissibleWidget (notification_content);
+ revealer.set_child (dismissible_widget);
- Gdk.EventSequence ? sequence = gesture.get_current_sequence ();
- if (sequence == null) {
- default_action_in = false;
- default_action_update_state ();
- }
- });
- 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;
-
- default_action.get_allocation (out allocation);
- gesture.get_point (sequence, out x, out y);
- bool in_button = (x >= 0 && y >= 0 && x < allocation.width && y < allocation.height);
- if (default_action_in != in_button) {
- default_action_in = in_button;
- default_action_update_state ();
- }
- });
- gesture.cancel.connect ((_gesture, _sequence) => {
- if (default_action_down) {
- default_action_down = false;
- default_action_update_state ();
+ add_controller (focus_event);
+
+ // Remove notification when it has been swiped
+ dismissible_widget.dismissed.connect (() => {
+ remove_noti_timeout ();
+ try {
+ noti_daemon.manually_close_notification (
+ param.applied_id, false);
+ } catch (Error e) {
+ printerr ("Error: %s\n", e.message);
+ this.destroy ();
}
});
-
- this.transition_time = ConfigModel.instance.transition_time;
- build_noti ();
-
- if (is_timed) {
- add_notification_timeout ();
- this.size_allocate.connect (on_size_allocation);
- }
- }
-
- private void default_action_update_state () {
- bool pressed = default_action_in && default_action_down;
-
- Gtk.StateFlags flags = default_action.get_state_flags () &
- ~(Gtk.StateFlags.PRELIGHT | Gtk.StateFlags.ACTIVE);
-
- if (default_action_in) flags |= Gtk.StateFlags.PRELIGHT;
- if (pressed) flags |= Gtk.StateFlags.ACTIVE;
-
- default_action.set_state_flags (flags, true);
}
- private void on_size_allocation (Gtk.Allocation _ignored) {
- // Force recomputing the allocated size of the wrapped GTK label in the body.
- // `queue_resize` alone DOES NOT WORK because it does not properly invalidate
- // the cache, this is a GTK bug!
- // See https://gitlab.gnome.org/GNOME/gtk/-/issues/2556
- if (body != null) {
- body.set_size_request (-1, body.get_allocated_height ());
+ public void construct_notification (NotifyParams param,
+ NotiDaemon noti_daemon,
+ NotificationType notification_type) {
+ if (is_constructed) {
+ // TODO: remove this
+ int height = get_allocated_height ();
+ if (height > 0) set_size_request (-1, height);
+ queue_resize ();
+ return;
}
- }
-
- private void build_noti () {
- this.body.set_line_wrap (true);
- this.body.set_line_wrap_mode (Pango.WrapMode.WORD_CHAR);
- this.body.set_ellipsize (Pango.EllipsizeMode.END);
-
- this.summary.set_line_wrap (false);
- this.summary.set_text (param.summary ?? param.app_name);
- this.summary.set_ellipsize (Pango.EllipsizeMode.END);
-
- this.button_press_event.connect ((event) => {
- if (event.button != Gdk.BUTTON_SECONDARY) return false;
- // Right click
- this.close_notification ();
- return true;
- });
-
- // Adds CSS :hover selector to EventBox
- default_action.enter_notify_event.connect ((event) => {
- if (event.detail != Gdk.NotifyType.INFERIOR
- && event.window == default_action.get_window ()) {
- default_action_in = true;
- default_action_update_state ();
- }
- return true;
- });
- default_action.leave_notify_event.connect ((event) => {
- if (event.detail != Gdk.NotifyType.INFERIOR
- && event.window == default_action.get_window ()) {
- default_action_in = false;
- default_action_update_state ();
- }
- return true;
- });
-
- default_action.unmap.connect (() => default_action_in = false);
-
- close_revealer.set_transition_duration (this.transition_time);
-
- close_button.clicked.connect (() => close_notification ());
-
- this.event_box.enter_notify_event.connect ((event) => {
- close_revealer.set_reveal_child (true);
- remove_noti_timeout ();
- return false;
- });
- this.event_box.leave_notify_event.connect ((event) => {
- if (event.detail == Gdk.NotifyType.INFERIOR) return true;
- close_revealer.set_reveal_child (false);
- add_notification_timeout ();
- return false;
- });
+ this.param = param;
+ this.noti_daemon = noti_daemon;
+ this.notification_type = notification_type;
- this.revealer.set_transition_duration (this.transition_time);
-
- this.carousel.set_animation_duration (this.transition_time);
// Changes the swipe direction depending on the notifications X position
+ PositionX pos_x = PositionX.NONE;
+ if (notification_type == NotificationType.CONTROL_CENTER)
+ pos_x = ConfigModel.instance.control_center_positionX;
+ if (pos_x == PositionX.NONE) pos_x = ConfigModel.instance.positionX;
switch (ConfigModel.instance.positionX) {
case PositionX.LEFT:
- this.carousel.reorder (event_box, 0);
- this.carousel_empty_widget_index = 1;
+ dismissible_widget.set_gesture_direction (SwipeDirection.SWIPE_LEFT);
break;
default:
case PositionX.RIGHT:
case PositionX.CENTER:
- this.carousel.scroll_to (event_box);
- this.carousel_empty_widget_index = 0;
+ dismissible_widget.set_gesture_direction (SwipeDirection.SWIPE_RIGHT);
break;
}
- this.carousel.page_changed.connect ((_, i) => {
- if (i != this.carousel_empty_widget_index) return;
- remove_noti_timeout ();
- try {
- noti_daemon.manually_close_notification (
- param.applied_id, false);
- } catch (Error e) {
- print ("Error: %s\n", e.message);
- this.destroy ();
- }
- });
-#if HAVE_LATEST_LIBHANDY
- this.carousel.allow_scroll_wheel = false;
-#endif
-
- if (this.progress_bar.visible = param.has_synch) {
- this.progress_bar.set_fraction (param.value * 0.01);
- }
- set_body ();
- set_icon ();
- set_inline_reply ();
- set_actions ();
- set_style_urgency ();
+ this.timeout_delay = ConfigModel.instance.timeout;
+ this.timeout_low_delay = ConfigModel.instance.timeout_low;
+ this.timeout_critical_delay = ConfigModel.instance.timeout_critical;
- this.show ();
+ this.transition_time = ConfigModel.instance.transition_time;
+ this.revealer.set_transition_duration (transition_time);
if (param.replaces) {
this.revealer.set_reveal_child (true);
} else {
- Timeout.add (0, () => {
+ // Show the reveal transition when the notification appears
+ Idle.add (() => {
this.revealer.set_reveal_child (true);
return Source.REMOVE;
});
}
- }
-
- private void set_body () {
- string text = param.body ?? "";
- this.body.set_lines (this.number_of_body_lines);
+ notification_content.build_notification ();
- // Removes all image tags and adds them to an array
- if (text.length > 0) {
- try {
- // Get src paths from images
- string[] img_paths = {};
- MatchInfo info;
- if (img_tag_regex.match (text, 0, out info)) {
- img_paths += Functions.get_match_from_info (info);
- while (info.next ()) {
- img_paths += Functions.get_match_from_info (info);
- }
- }
-
- // Remove all images
- text = img_tag_regex.replace (text, text.length, 0, "");
-
- // Set the image if exists and is valid
- if (img_paths.length > 0) {
- var img = img_paths[0];
- var file = File.new_for_path (img);
- if (img.length > 0 && file.query_exists ()) {
- var buf = new Gdk.Pixbuf.from_file_at_scale (
- file.get_path (),
- notification_body_image_width,
- notification_body_image_height,
- true);
- this.body_image.set_from_pixbuf (buf);
- this.body_image.show ();
- }
- }
- } catch (Error e) {
- stderr.printf (e.message);
- }
+ if (notification_type == NotificationType.POPUP) {
+ add_notification_timeout ();
}
- // Markup
- try {
- // Escapes all characters
- string escaped = Markup.escape_text (text);
- // Replace all valid tags brackets with <,,> so that the
- // markup parser only parses valid tags
- // Ex: <b>BOLD</b> -> BOLD
- escaped = tag_regex.replace (escaped, escaped.length, 0, "<\\1>");
-
- // Unescape a few characters that may have been double escaped
- // Sending "<" in Discord would result in "<" without this
- // < -> <
- escaped = tag_unescape_regex.replace_literal (escaped, escaped.length, 0, "&");
-
- // Turns it back to markdown, defaults to original if not valid
- Pango.AttrList ? attr = null;
- string ? buf = null;
- Pango.parse_markup (escaped, -1, 0, out attr, out buf, null);
-
- this.body.set_text (buf);
- if (attr != null) this.body.set_attributes (attr);
- } catch (Error e) {
- stderr.printf ("Could not parse Pango markup %s: %s\n",
- text, e.message);
- // Sets the original text
- this.body.set_text (text);
- }
+ is_constructed = true;
}
- /** Returns the first code found, else null */
- private string ? parse_body_codes () {
- if (!ConfigModel.instance.notification_2fa_action) return null;
- string body = this.body.get_text ().strip ();
- if (body.length == 0) return null;
-
- MatchInfo info;
- var result = code_regex.match (body, RegexMatchFlags.NOTEMPTY, out info);
- string ? match = info.fetch (0);
- if (!result || match == null) return null;
+ /**
+ * Overrides
+ */
- return Functions.filter_string (
- match.strip (), (c) => c.isdigit () || c.isspace ()).strip ();
+ public override Gtk.SizeRequestMode get_request_mode () {
+ return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
- public void click_default_action () {
- action_clicked (param.default_action, true);
- }
+ public override void measure (Gtk.Orientation orientation,
+ int for_size,
+ out int minimum, out int natural,
+ out int minimum_baseline, out int natural_baseline) {
+ minimum = 0;
+ natural = 0;
+ minimum_baseline = -1;
+ natural_baseline = -1;
- public void click_alt_action (uint index) {
- List ? children = alt_actions_box.get_children ();
- uint length = children.length ();
- if (length == 0 || index >= length) return;
+ // Force recomputing the allocated size of the wrapped GTK label in the body.
+ // `queue_resize` alone DOES NOT WORK because it does not properly invalidate
+ // the cache, this is a GTK bug!
+ // See https://gitlab.gnome.org/GNOME/gtk/-/issues/2556
+ // , https://gitlab.gnome.org/GNOME/gtk/-/issues/5868
+ // , and https://gitlab.gnome.org/GNOME/gtk/-/issues/5885
+ // TODO: Use a default Bin layout_manager when this issue is fixed
+ notification_content.refresh_body_height ();
- unowned Gtk.Widget button = children.nth_data (index);
- if (button is Gtk.Button) {
- ((Gtk.Button) button).clicked ();
- return;
- }
- // Backup if the above fails
- action_clicked (param.actions.index (index));
- }
+ // This works for some reason...
+ // this.queue_resize ();
- private void action_clicked (Action ? action, bool is_default = false) {
- noti_daemon.run_scripts (param, ScriptRunOnType.ACTION);
- if (action != null
- && action.identifier != null
- && action.identifier != "") {
- noti_daemon.ActionInvoked (param.applied_id, action.identifier);
- if (ConfigModel.instance.hide_on_action) {
- try {
- swaync_daemon.set_visibility (false);
- } catch (Error e) {
- print ("Error: %s\n", e.message);
- }
- }
- }
- if (!param.resident) close_notification ();
- }
+ int child_min = 0;
+ int child_nat = 0;
+ int child_min_baseline = -1;
+ int child_nat_baseline = -1;
- private void set_style_urgency () {
- switch (param.urgency) {
- case UrgencyLevels.LOW:
- base_box.get_style_context ().add_class ("low");
- break;
- case UrgencyLevels.NORMAL:
- default:
- base_box.get_style_context ().add_class ("normal");
- break;
- case UrgencyLevels.CRITICAL:
- base_box.get_style_context ().add_class ("critical");
- break;
- }
- }
-
- private void set_inline_reply () {
- // Only show inline replies in popup notifications if the compositor
- // supports ON_DEMAND layer shell keyboard interactivity
- if (!ConfigModel.instance.notification_inline_replies
- || (ConfigModel.instance.layer_shell
- && layer_shell_protocol_version < 4
- && notification_type == NotificationType.POPUP)) {
- return;
- }
- if (param.inline_reply == null) return;
-
- has_inline_reply = true;
-
- inline_reply_box.show ();
-
- inline_reply_entry.set_placeholder_text (
- param.inline_reply_placeholder ?? "Enter Text");
- // Set reply Button sensitivity to disabled if Entry text is empty
- inline_reply_entry.bind_property (
- "text",
- inline_reply_button, "sensitive",
- BindingFlags.SYNC_CREATE,
- (binding, srcval, ref targetval) => {
- targetval.set_boolean (((string) srcval).strip ().length > 0);
- return true;
- },
- null);
-
- inline_reply_entry.key_release_event.connect ((w, event_key) => {
- switch (Gdk.keyval_name (event_key.keyval)) {
- case "Return":
- inline_reply_button.clicked ();
- return true;
- default:
- return false;
- }
- });
+ get_first_child ().measure (orientation, for_size,
+ out child_min, out child_nat,
+ out child_min_baseline, out child_nat_baseline);
- inline_reply_button.set_label (param.inline_reply.name ?? "Reply");
- inline_reply_button.clicked.connect (() => {
- string text = inline_reply_entry.get_text ().strip ();
- if (text.length == 0) return;
- noti_daemon.NotificationReplied (param.applied_id, text);
- // Dismiss notification without activating Action
- action_clicked (null);
- });
- }
-
- private void set_actions () {
- // Check for security codes
- string ? code = parse_body_codes ();
- if (param.actions.length > 0 || code != null) {
- var viewport = new Gtk.Viewport (null, null);
- var scroll = new Gtk.ScrolledWindow (null, null);
- alt_actions_box.set_homogeneous (true);
- alt_actions_box.set_layout (Gtk.ButtonBoxStyle.EXPAND);
-
- // Add "Copy code" Action if available and copy it to clipboard when clicked
- if (code != null && code.length > 0) {
- string action_name = "COPY \"%s\"".printf (code);
- var action_button = new Gtk.Button.with_label (action_name);
- action_button.clicked.connect (() => {
- // Copy to clipboard
- get_clipboard (Gdk.SELECTION_CLIPBOARD).set_text (code, -1);
- // Dismiss notification
- action_clicked (null);
- });
- action_button
- .get_style_context ().add_class ("notification-action");
- action_button.set_can_focus (false);
- alt_actions_box.add (action_button);
- }
+ minimum = int.max (minimum, child_min);
+ natural = int.max (natural, child_nat);
- // Add notification specified actions
- foreach (var action in param.actions.data) {
- var action_button = new Gtk.Button.with_label (action.name);
- action_button.clicked.connect (() => action_clicked (action));
- action_button
- .get_style_context ().add_class ("notification-action");
- action_button.set_can_focus (false);
- alt_actions_box.add (action_button);
- }
- viewport.add (alt_actions_box);
- scroll.add (viewport);
- base_box.add (scroll);
- scroll.show_all ();
+ if (child_min_baseline > -1) {
+ minimum_baseline = int.max (minimum_baseline, child_min_baseline);
+ }
+ if (child_nat_baseline > -1) {
+ natural_baseline = int.max (natural_baseline, child_nat_baseline);
}
}
- public void set_time () {
- this.time.set_text (get_readable_time ());
- }
-
- private string get_readable_time () {
- string value = "";
-
- double diff = (get_real_time () * 0.000001) - param.time;
- double secs = diff / 60;
- double hours = secs / 60;
- double days = hours / 24;
- if (secs < 1) {
- value = "Now";
- } else if (secs >= 1 && hours < 1) {
- // 1m - 1h
- var val = Math.floor (secs);
- value = val.to_string () + " min";
- if (val > 1) value += "s";
- value += " ago";
- } else if (hours >= 1 && hours < 24) {
- // 1h - 24h
- var val = Math.floor (hours);
- value = val.to_string () + " hour";
- if (val > 1) value += "s";
- value += " ago";
- } else {
- // Days
- var val = Math.floor (days);
- value = val.to_string () + " day";
- if (val > 1) value += "s";
- value += " ago";
- }
- return value;
+ public override void size_allocate (int width, int height, int baseline) {
+ Gtk.Widget child = get_first_child ();
+ if (!child.should_layout ()) return;
+ child.allocate (width, height, baseline, null);
}
public void close_notification (bool is_timeout = false) {
@@ -598,71 +193,15 @@ namespace SwayNotificationCenter {
noti_daemon.manually_close_notification (param.applied_id,
is_timeout);
} catch (Error e) {
- print ("Error: %s\n", e.message);
+ printerr ("Error: %s\n", e.message);
this.destroy ();
}
return Source.REMOVE;
});
}
- private void set_icon () {
- var image_visibility = ConfigModel.instance.image_visibility;
- if (image_visibility == ImageVisibility.NEVER) {
- img.set_visible (false);
- return;
- }
-
- img.set_pixel_size (notification_icon_size);
- img.height_request = notification_icon_size;
- img.width_request = notification_icon_size;
-
- var img_path_exists = File.new_for_path (
- param.image_path ?? "").query_exists ();
- var app_icon_exists = File.new_for_path (
- param.app_icon ?? "").query_exists ();
-
- if (param.image_data.is_initialized) {
- Functions.set_image_data (param.image_data, img,
- notification_icon_size);
- } else if (param.image_path != null &&
- param.image_path != "" &&
- img_path_exists) {
- Functions.set_image_path (param.image_path, img,
- notification_icon_size,
- img_path_exists);
- } else if (param.app_icon != null && param.app_icon != "") {
- Functions.set_image_path (param.app_icon, img,
- notification_icon_size,
- app_icon_exists);
- } else if (param.icon_data.is_initialized) {
- Functions.set_image_data (param.icon_data, img,
- notification_icon_size);
- } else {
- // Get the app icon
- Icon ? icon = null;
- if (param.desktop_entry != null) {
- string entry = param.desktop_entry;
- entry = entry.replace (".desktop", "");
- DesktopAppInfo entry_info = new DesktopAppInfo (
- "%s.desktop".printf (entry));
- // Checks if the .desktop file actually exists or not
- if (entry_info is DesktopAppInfo) {
- icon = entry_info.get_icon ();
- }
- }
- if (icon != null) {
- img.set_from_gicon (icon, icon_size);
- } else if (image_visibility == ImageVisibility.ALWAYS) {
- // Default icon
- img.set_from_icon_name ("image-missing", icon_size);
- } else {
- img.set_visible (false);
- }
- }
- }
-
public void add_notification_timeout () {
- if (!this.is_timed) return;
+ if (notification_type != NotificationType.POPUP) return;
// Removes the previous timeout
remove_noti_timeout ();
@@ -700,8 +239,12 @@ namespace SwayNotificationCenter {
/** Forces the EventBox to reload its style_context #27 */
public void reload_style_context () {
- event_box.get_style_context ().changed ();
- default_action.get_style_context ().changed ();
+ // overlay.get_style_context ().changed ();
+ // default_action.get_style_context ().changed ();
+ }
+
+ public void set_time () {
+ notification_content.set_time ();
}
}
}
diff --git a/src/notification/notificationContent.vala b/src/notification/notificationContent.vala
new file mode 100644
index 00000000..9174167f
--- /dev/null
+++ b/src/notification/notificationContent.vala
@@ -0,0 +1,549 @@
+namespace SwayNotificationCenter {
+ [GtkTemplate (ui = "/org/erikreider/swaync/templates/notificationContent.ui")]
+ public class NotificationContent : Adw.Bin {
+ [GtkChild]
+ unowned Gtk.Overlay overlay;
+
+ [GtkChild]
+ unowned Gtk.Box default_action;
+
+
+ /** The default_action gesture. Allows clicks while not in swipe gesture. */
+ public Gtk.EventControllerFocus focus_event = new Gtk.EventControllerFocus ();
+ private Gtk.EventControllerMotion motion_event = new Gtk.EventControllerMotion ();
+ private Gtk.GestureClick secondary_gesture_click = new Gtk.GestureClick ();
+ private Gtk.GestureClick default_action_gesture_click = new Gtk.GestureClick ();
+ private Gtk.EventControllerKey inline_reply_key_event = new Gtk.EventControllerKey ();
+
+ [GtkChild]
+ unowned Gtk.ProgressBar progress_bar;
+ // TODO: REPLACE WITH Gtk.LevelBar
+
+ [GtkChild]
+ unowned Gtk.Box base_box;
+
+ [GtkChild]
+ unowned Gtk.Revealer close_revealer;
+ [GtkChild]
+ unowned Gtk.Button close_button;
+
+ private IterBox alt_actions_box = new IterBox (Gtk.Orientation.HORIZONTAL, 0);
+
+ [GtkChild]
+ unowned Gtk.Label summary_label;
+ [GtkChild]
+ unowned Gtk.Label time_label;
+ [GtkChild]
+ unowned Gtk.Label body_label;
+ [GtkChild]
+ unowned Gtk.Image img;
+ [GtkChild]
+ unowned Gtk.Image body_image;
+
+ // Inline Reply
+ [GtkChild]
+ unowned Gtk.Box inline_reply_box;
+ [GtkChild]
+ unowned Gtk.Entry inline_reply_entry;
+ [GtkChild]
+ unowned Gtk.Button inline_reply_button;
+
+ private bool default_action_down = false;
+ private bool default_action_in = false;
+
+ private int notification_body_image_height {
+ get;
+ default = ConfigModel.instance.notification_body_image_height;
+ }
+ private int notification_body_image_width {
+ get;
+ default = ConfigModel.instance.notification_body_image_width;
+ }
+
+ public unowned Notification notification { get; construct; }
+ public unowned NotifyParams param {
+ get { return notification.param; }
+ }
+ public unowned NotiDaemon noti_daemon {
+ get { return notification.noti_daemon; }
+ }
+
+ public bool has_inline_reply { get; private set; default = false; }
+
+ public int number_of_body_lines { get; construct; }
+
+ public bool is_constructed { get; private set; default = false; }
+
+ private static Regex code_2fa_regex;
+ private static Regex tag_regex;
+ private static Regex tag_unescape_regex;
+ private static Regex img_tag_regex;
+ private const string[] TAGS = { "b", "u", "i" };
+ private const string[] UNESCAPE_CHARS = {
+ "lt;", "#60;", "#x3C;", "#x3c;", // <
+ "gt;", "#62;", "#x3E;", "#x3e;", // >
+ "apos;", "#39;", // '
+ "quot;", "#34;", // "
+ "amp;" // &
+ };
+
+ construct {
+ hexpand = true;
+ vexpand = true;
+
+ try {
+ code_2fa_regex = new Regex ("(?<= |^)(\\d{3}(-| )\\d{3}|\\d{4,7})(?= |$|\\.|,)",
+ RegexCompileFlags.MULTILINE);
+ string joined_tags = string.joinv ("|", TAGS);
+ tag_regex = new Regex ("<(/?(?:%s))>".printf (joined_tags));
+ string unescaped = string.joinv ("|", UNESCAPE_CHARS);
+ tag_unescape_regex = new Regex ("&(?=%s)".printf (unescaped));
+ img_tag_regex = new Regex ("""]* src=\"([^\"]*)\"[^>]*>""");
+ } catch (Error e) {
+ stderr.printf ("Invalid regex: %s", e.message);
+ }
+
+ // Build the default_action gesture. Makes clickes compatible with
+ // the Hdy Swipe gesture unlike a regular ::button_release_event
+ default_action.add_controller (default_action_gesture_click);
+ default_action_gesture_click.set_touch_only (false);
+ default_action_gesture_click.set_button (Gdk.BUTTON_PRIMARY);
+ default_action_gesture_click.pressed.connect ((_gesture, _n_press, _x, _y) => {
+ default_action_in = true;
+ default_action_down = true;
+ default_action_update_state ();
+ });
+ default_action_gesture_click.released.connect ((gesture, _n_press, _x, _y) => {
+ // Emit released
+ if (!default_action_down) return;
+ default_action_down = false;
+ if (default_action_in) {
+ click_default_action ();
+ }
+
+ Gdk.EventSequence ? sequence = gesture.get_current_sequence ();
+ if (sequence == null) {
+ default_action_in = false;
+ default_action_update_state ();
+ }
+ });
+ default_action_gesture_click.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;
+
+ default_action.get_allocation (out allocation);
+ gesture.get_point (sequence, out x, out y);
+ bool in_button = (x >= 0 && y >= 0 && x < allocation.width && y < allocation.height);
+ if (default_action_in != in_button) {
+ default_action_in = in_button;
+ default_action_update_state ();
+ }
+ });
+ default_action_gesture_click.cancel.connect ((_gesture, _sequence) => {
+ if (default_action_down) {
+ default_action_down = false;
+ default_action_update_state ();
+ }
+ });
+
+ // Right click to close
+ add_controller (secondary_gesture_click);
+ secondary_gesture_click.set_touch_only (false);
+ secondary_gesture_click.set_exclusive (false);
+ secondary_gesture_click.set_button (Gdk.BUTTON_SECONDARY);
+ secondary_gesture_click.set_propagation_phase (Gtk.PropagationPhase.CAPTURE);
+ secondary_gesture_click.released.connect ((controller, n, x, y) => {
+ var event = (Gdk.ButtonEvent) controller.get_current_event ();
+ if (event.get_button () == Gdk.BUTTON_SECONDARY) {
+ notification.close_notification ();
+ }
+ });
+ }
+
+ public NotificationContent (Notification notification) {
+ bool is_popup = notification.notification_type == NotificationType.POPUP;
+ Object (
+ notification: notification,
+ number_of_body_lines: (is_popup ? 5 : 10)
+ );
+
+ default_action.unmap.connect (() => default_action_in = false);
+
+ close_button.clicked.connect (() => {
+ notification.close_notification ();
+ });
+
+ add_controller (motion_event);
+ motion_event.enter.connect ((event) => {
+ close_revealer.set_reveal_child (true);
+ notification.remove_noti_timeout ();
+ });
+
+ motion_event.leave.connect ((event) => {
+ // if (event.detail == Gdk.NotifyType.INFERIOR) return true;
+ close_revealer.set_reveal_child (false);
+ notification.add_notification_timeout ();
+ });
+ }
+
+ public void refresh_body_height () {
+ body_label.set_size_request (-1, body_label.get_allocated_height ());
+ }
+
+ private void default_action_update_state () {
+ bool pressed = default_action_in && default_action_down;
+
+ Gtk.StateFlags flags = default_action.get_state_flags () &
+ ~(Gtk.StateFlags.PRELIGHT | Gtk.StateFlags.ACTIVE);
+
+ if (default_action_in) flags |= Gtk.StateFlags.PRELIGHT;
+ if (pressed) flags |= Gtk.StateFlags.ACTIVE;
+
+ default_action.set_state_flags (flags, true);
+ }
+
+ public void build_notification () {
+ if (is_constructed) return;
+
+ overlay.add_overlay (close_revealer);
+
+ this.body_label.set_wrap (true);
+ this.body_label.set_wrap_mode (Pango.WrapMode.WORD_CHAR);
+ this.body_label.set_ellipsize (Pango.EllipsizeMode.END);
+
+ this.summary_label.set_wrap (false);
+ this.summary_label.set_text (param.summary ?? param.app_name);
+ this.summary_label.set_ellipsize (Pango.EllipsizeMode.END);
+
+ close_revealer.set_transition_duration (notification.transition_time);
+
+ if (this.progress_bar.visible = param.has_synch) {
+ this.progress_bar.set_fraction (param.value * 0.01);
+ }
+
+ this.body_image.set_visible (false);
+ this.inline_reply_box.set_visible (false);
+
+ set_body ();
+ set_icon ();
+ set_inline_reply ();
+ set_actions ();
+ set_style_urgency ();
+
+ is_constructed = true;
+ }
+
+ /*
+ * Widgets
+ */
+
+ private void set_body () {
+ string text = param.body ?? "";
+
+ this.body_label.set_lines (this.number_of_body_lines);
+
+ // Removes all image tags and adds them to an array
+ if (text.length > 0) {
+ try {
+ // Get src paths from images
+ string[] img_paths = {};
+ MatchInfo info;
+ if (img_tag_regex.match (text, 0, out info)) {
+ img_paths += Functions.get_match_from_info (info);
+ while (info.next ()) {
+ img_paths += Functions.get_match_from_info (info);
+ }
+ }
+
+ // Remove all images
+ text = img_tag_regex.replace (text, text.length, 0, "");
+
+ // Set the image if exists and is valid
+ if (img_paths.length > 0) {
+ var img = img_paths[0];
+ var file = File.new_for_path (img);
+ if (img.length > 0 && file.query_exists ()) {
+ var buf = new Gdk.Pixbuf.from_file_at_scale (
+ file.get_path (),
+ notification_body_image_width,
+ notification_body_image_height,
+ true);
+ this.body_image.set_from_pixbuf (buf);
+ this.body_image.show ();
+ }
+ }
+ } catch (Error e) {
+ stderr.printf (e.message);
+ }
+ }
+
+ // Markup
+ try {
+ // Escapes all characters
+ string escaped = Markup.escape_text (text);
+ // Replace all valid tags brackets with <,,> so that the
+ // markup parser only parses valid tags
+ // Ex: <b>BOLD</b> -> BOLD
+ escaped = tag_regex.replace (escaped, escaped.length, 0, "<\\1>");
+
+ // Unescape a few characters that may have been double escaped
+ // Sending "<" in Discord would result in "<" without this
+ // < -> <
+ escaped = tag_unescape_regex.replace_literal (escaped, escaped.length, 0, "&");
+
+ // Turns it back to markdown, defaults to original if not valid
+ Pango.AttrList ? attr = null;
+ string ? buf = null;
+ Pango.parse_markup (escaped, -1, 0, out attr, out buf, null);
+
+ this.body_label.set_text (buf);
+ if (attr != null) this.body_label.set_attributes (attr);
+ } catch (Error e) {
+ stderr.printf ("Could not parse Pango markup %s: %s\n",
+ text, e.message);
+ // Sets the original text
+ this.body_label.set_text (text);
+ }
+ }
+
+ /** Returns the first code found, else null */
+ private string ? parse_body_codes () {
+ if (!ConfigModel.instance.notification_2fa_action) return null;
+ string body = this.body_label.get_text ().strip ();
+ if (body.length == 0) return null;
+
+ MatchInfo info;
+ var result = code_2fa_regex.match (body, RegexMatchFlags.NOTEMPTY, out info);
+ string ? match = info.fetch (0);
+ if (!result || match == null) return null;
+
+ return Functions.filter_string (
+ match.strip (), (c) => c.isdigit () || c.isspace ()).strip ();
+ }
+
+ public void click_default_action () {
+ action_clicked (param.default_action, true);
+ }
+
+ public void click_alt_action (uint index) {
+ List ? children = alt_actions_box.get_children ();
+ if (alt_actions_box.length == 0 || index >= alt_actions_box.length) return;
+
+ unowned Gtk.Widget button = children.nth_data (index);
+ if (button is Gtk.Button) {
+ button.clicked ();
+ return;
+ }
+ // Backup if the above fails
+ action_clicked (param.actions.index (index));
+ }
+
+ private void action_clicked (Action ? action, bool is_default = false) {
+ noti_daemon.run_scripts (param, ScriptRunOnType.ACTION);
+ if (action != null
+ && action.identifier != null
+ && action.identifier != "") {
+ noti_daemon.ActionInvoked (param.applied_id, action.identifier);
+ if (ConfigModel.instance.hide_on_action) {
+ try {
+ swaync_daemon.set_visibility (false);
+ } catch (Error e) {
+ printerr ("Error: %s\n", e.message);
+ }
+ }
+ }
+ if (!param.resident) notification.close_notification ();
+ }
+
+ private void set_style_urgency () {
+ switch (param.urgency) {
+ case UrgencyLevels.LOW:
+ base_box.add_css_class ("low");
+ break;
+ case UrgencyLevels.NORMAL:
+ default:
+ base_box.add_css_class ("normal");
+ break;
+ case UrgencyLevels.CRITICAL:
+ base_box.add_css_class ("critical");
+ break;
+ }
+ }
+
+ private void set_inline_reply () {
+ // Only show inline replies in popup notifications if the compositor
+ // supports ON_DEMAND layer shell keyboard interactivity
+ if (!ConfigModel.instance.notification_inline_replies
+ || (ConfigModel.instance.layer_shell
+ && layer_shell_protocol_version < 4
+ && notification.notification_type == NotificationType.POPUP)) {
+ return;
+ }
+ if (notification.param.inline_reply == null) return;
+
+ has_inline_reply = true;
+
+ inline_reply_box.show ();
+
+ inline_reply_entry.set_placeholder_text (
+ notification.param.inline_reply_placeholder ?? "Enter Text");
+ // Set reply Button sensitivity to disabled if Entry text is empty
+ inline_reply_entry.bind_property (
+ "text",
+ inline_reply_button, "sensitive",
+ BindingFlags.SYNC_CREATE,
+ (binding, srcval, ref targetval) => {
+ targetval.set_boolean (((string) srcval).strip ().length > 0);
+ return true;
+ },
+ null);
+
+ inline_reply_entry.add_controller (inline_reply_key_event);
+ inline_reply_key_event.key_released.connect ((keyval, keycode, state) => {
+ switch (Gdk.keyval_name (keyval)) {
+ case "Return":
+ inline_reply_button.clicked ();
+ break;
+ default:
+ break;
+ }
+ });
+
+ inline_reply_button.set_label (param.inline_reply.name ?? "Reply");
+ inline_reply_button.clicked.connect (() => {
+ string text = inline_reply_entry.get_text ().strip ();
+ if (text.length == 0) return;
+ noti_daemon.NotificationReplied (param.applied_id, text);
+ // Dismiss notification without activating Action
+ action_clicked (null);
+ });
+ }
+
+ private void set_actions () {
+ // Check for security codes
+ string ? code = parse_body_codes ();
+ if (param.actions.length > 0 || code != null) {
+ var viewport = new Gtk.Viewport (null, null);
+ var scroll = new Gtk.ScrolledWindow ();
+ alt_actions_box.set_homogeneous (true);
+ // alt_actions_box.set_layout (Gtk.ButtonBoxStyle.EXPAND);
+
+ // Add "Copy code" Action if available and copy it to clipboard when clicked
+ if (code != null && code.length > 0) {
+ string action_name = "COPY \"%s\"".printf (code);
+ var action_button = new Gtk.Button.with_label (action_name);
+ action_button.clicked.connect (() => {
+ // Copy to clipboard
+ get_clipboard ().set_text (code);
+ // Dismiss notification
+ action_clicked (null);
+ });
+ action_button.add_css_class ("notification-action");
+ action_button.set_can_focus (false);
+ alt_actions_box.append (action_button);
+ }
+
+ // Add notification specified actions
+ foreach (var action in param.actions.data) {
+ var action_button = new Gtk.Button.with_label (action.name);
+ action_button.clicked.connect (() => action_clicked (action));
+ action_button.add_css_class ("notification-action");
+ action_button.set_can_focus (false);
+ alt_actions_box.append (action_button);
+ }
+ viewport.set_child (alt_actions_box);
+ scroll.set_child (viewport);
+ base_box.append (scroll);
+ }
+ }
+
+ public void set_time () {
+ this.time_label.set_text (get_readable_time ());
+ }
+
+ private string get_readable_time () {
+ string value = "";
+
+ double diff = (get_real_time () * 0.000001) - param.time;
+ double secs = diff / 60;
+ double hours = secs / 60;
+ double days = hours / 24;
+ if (secs < 1) {
+ value = "Now";
+ } else if (secs >= 1 && hours < 1) {
+ // 1m - 1h
+ var val = Math.floor (secs);
+ value = val.to_string () + " min";
+ if (val > 1) value += "s";
+ value += " ago";
+ } else if (hours >= 1 && hours < 24) {
+ // 1h - 24h
+ var val = Math.floor (hours);
+ value = val.to_string () + " hour";
+ if (val > 1) value += "s";
+ value += " ago";
+ } else {
+ // Days
+ var val = Math.floor (days);
+ value = val.to_string () + " day";
+ if (val > 1) value += "s";
+ value += " ago";
+ }
+ return value;
+ }
+
+ private void set_icon () {
+ var image_visibility = ConfigModel.instance.image_visibility;
+ if (image_visibility == ImageVisibility.NEVER) {
+ img.set_visible (false);
+ return;
+ }
+
+ img.set_pixel_size (ConfigModel.instance.notification_icon_size);
+ img.height_request = img.pixel_size;
+ img.width_request = img.pixel_size;
+
+ var img_path_exists = File.new_for_path (
+ param.image_path ?? "").query_exists ();
+ var app_icon_exists = File.new_for_path (
+ param.app_icon ?? "").query_exists ();
+
+ if (param.image_data.is_initialized) {
+ Functions.set_image_data (param.image_data, img);
+ } else if (param.image_path != null &&
+ param.image_path != "" &&
+ img_path_exists) {
+ Functions.set_image_path (param.image_path, img, img_path_exists);
+ } else if (param.app_icon != null && param.app_icon != "") {
+ Functions.set_image_path (param.app_icon, img, app_icon_exists);
+ } else if (param.icon_data.is_initialized) {
+ Functions.set_image_data (param.icon_data, img);
+ } else {
+ // Get the app icon
+ Icon ? icon = null;
+ if (param.desktop_entry != null) {
+ string entry = param.desktop_entry;
+ entry = entry.replace (".desktop", "");
+ DesktopAppInfo entry_info = new DesktopAppInfo (
+ "%s.desktop".printf (entry));
+ // Checks if the .desktop file actually exists or not
+ if (entry_info is DesktopAppInfo) {
+ icon = entry_info.get_icon ();
+ }
+ }
+ // TODO: Make sure that pixel size is used
+ if (icon != null) {
+ img.set_from_gicon (icon);
+ } else if (image_visibility == ImageVisibility.ALWAYS) {
+ // Default icon
+ img.set_from_icon_name ("image-missing");
+ } else {
+ img.set_visible (false);
+ }
+ }
+ }
+ }
+}
diff --git a/src/notificationWindow/notificationWindow.ui b/src/notificationWindow/notificationWindow.ui
deleted file mode 100644
index 2eab4c09..00000000
--- a/src/notificationWindow/notificationWindow.ui
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
- False
- NotificationWindow
- False
- notification
- True
- True
- True
- False
- False
- north-east
- False
-
-
- True
- False
- never
- top-right
- True
-
-
- True
- False
- True
- none
-
-
- True
- False
- vertical
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/notificationWindow/notificationWindow.vala b/src/notificationWindow/notificationWindow.vala
index ad0a08c6..1efcfed8 100644
--- a/src/notificationWindow/notificationWindow.vala
+++ b/src/notificationWindow/notificationWindow.vala
@@ -1,40 +1,10 @@
namespace SwayNotificationCenter {
- [GtkTemplate (ui = "/org/erikreider/sway-notification-center/notificationWindow/notificationWindow.ui")]
- public class NotificationWindow : Gtk.ApplicationWindow {
- private static NotificationWindow ? window = null;
- /**
- * A NotificationWindow singleton due to a nasty notification
- * enter_notify_event bug where GTK still thinks that the cursor is at
- * that location after closing the last notification. The next notification
- * would sometimes automatically be hovered...
- * The only way to "solve" this is to close the window and reopen a new one.
- */
- public static NotificationWindow instance {
- get {
- if (window == null) {
- window = new NotificationWindow ();
- } else if (!window.get_mapped () ||
- !window.get_realized () ||
- !(window.get_child () is Gtk.Widget)) {
- window.destroy ();
- window = new NotificationWindow ();
- }
- return window;
- }
- }
-
- public static bool is_null {
- get {
- return window == null;
- }
- }
+ public class NotificationWindow : Gtk.Window {
+ private const int MAX_HEIGHT = 600;
- [GtkChild]
- unowned Gtk.ScrolledWindow scrolled_window;
- [GtkChild]
- unowned Gtk.Viewport viewport;
- [GtkChild]
- unowned Gtk.Box box;
+ Gtk.ScrolledWindow scrolled_window;
+ Gtk.Viewport viewport;
+ IterBox box = new IterBox (Gtk.Orientation.VERTICAL, 0);
private bool list_reverse = false;
@@ -42,14 +12,31 @@ namespace SwayNotificationCenter {
Gee.HashSet inline_reply_notifications = new Gee.HashSet ();
- private const int MAX_HEIGHT = 600;
+ private unowned NotiDaemon noti_daemon;
+ private unowned SwayncDaemon swaync_daemon;
+
+ public NotificationWindow (SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
+ this.noti_daemon = noti_daemon;
+ this.swaync_daemon = swaync_daemon;
+
+ // Build widget
+ add_css_class ("floating-notifications");
+
+ // set_child (scrolled_window = new CustomScrolledWindow.propagate (MAX_HEIGHT));
+ // scrolled_window.set_scrollable (viewport = new Gtk.Viewport (null, null));
+ set_child (scrolled_window = new Gtk.ScrolledWindow ());
+ scrolled_window.set_child (viewport = new Gtk.Viewport (null, null));
+ scrolled_window.set_min_content_height (-1);
+ scrolled_window.set_max_content_height (MAX_HEIGHT);
+ scrolled_window.set_propagate_natural_height (true);
+ scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER;
+ viewport.set_child (box);
- private NotificationWindow () {
if (swaync_daemon.use_layer_shell) {
if (!GtkLayerShell.is_supported ()) {
stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
stderr.printf ("Swaync only works on Wayland!\n");
- stderr.printf ("If running waylans session, try running:\n");
+ stderr.printf ("If running wayland session, try running:\n");
stderr.printf ("\tGDK_BACKEND=wayland swaync\n");
Process.exit (1);
}
@@ -58,13 +45,6 @@ namespace SwayNotificationCenter {
}
this.set_anchor ();
- // -1 should set it to the content size unless it exceeds max_height
- scrolled_window.set_min_content_height (-1);
- scrolled_window.set_max_content_height (MAX_HEIGHT);
- scrolled_window.set_propagate_natural_height (true);
-
- viewport.size_allocate.connect (size_alloc);
-
this.default_width = ConfigModel.instance.notification_window_width;
}
@@ -115,31 +95,72 @@ namespace SwayNotificationCenter {
this, GtkLayerShell.Edge.BOTTOM, false);
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.TOP, true);
+ scrolled_window.set_valign (Gtk.Align.START);
break;
case PositionY.CENTER:
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.BOTTOM, false);
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.TOP, false);
+ scrolled_window.set_valign (Gtk.Align.CENTER);
break;
case PositionY.BOTTOM:
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.TOP, false);
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.BOTTOM, true);
+ scrolled_window.set_valign (Gtk.Align.END);
break;
}
}
list_reverse = ConfigModel.instance.positionY == PositionY.BOTTOM;
}
- private void size_alloc () {
+ public override Gtk.SizeRequestMode get_request_mode () {
+ return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
+ }
+
+ public override void measure (Gtk.Orientation orientation, int for_size,
+ out int minimum_size, out int natural_size,
+ out int minimum_baseline, out int natural_baseline) {
+ minimum_size = 1;
+ natural_size = 1;
+ minimum_baseline = -1;
+ natural_baseline = -1;
+
+ int child_min = 0;
+ int child_nat = 0;
+ int child_min_baseline = -1;
+ int child_nat_baseline = -1;
+ scrolled_window.measure (orientation, for_size,
+ out child_min, out child_nat,
+ out child_min_baseline, out child_nat_baseline);
+
+ minimum_size = int.min (MAX_HEIGHT, int.max (minimum_size, child_min));
+ natural_size = int.min (MAX_HEIGHT, int.max (natural_size, child_nat));
+
+ if (child_min_baseline > -1) {
+ minimum_baseline = int.max (minimum_baseline, child_min_baseline);
+ }
+ if (child_nat_baseline > -1) {
+ natural_baseline = int.max (natural_baseline, child_nat_baseline);
+ }
+
+ // Input region not being resized unless default_height is set to -1
+ // Layer shell issue?
+ default_height = -1;
+ }
+
+ public override void size_allocate (int width, int height, int baseline) {
+ // Scroll to the top/latest notification
var adj = viewport.vadjustment;
double upper = adj.get_upper ();
if (last_upper < upper) {
scroll_to_start (list_reverse);
}
last_upper = upper;
+
+ base.size_allocate (width, height, baseline);
}
private void scroll_to_start (bool reverse) {
@@ -153,6 +174,7 @@ namespace SwayNotificationCenter {
close_all_notifications ();
} else {
this.set_anchor ();
+ this.show ();
}
}
@@ -173,64 +195,50 @@ namespace SwayNotificationCenter {
private void remove_notification (Notification ? noti, bool replaces) {
// Remove notification and its destruction timeout
if (noti != null) {
-#if HAVE_LATEST_GTK_LAYER_SHELL
if (noti.has_inline_reply) {
inline_reply_notifications.remove (noti.param.applied_id);
- if (inline_reply_notifications.size == 0
+ if (swaync_daemon.use_layer_shell
+ && inline_reply_notifications.size == 0
&& GtkLayerShell.get_keyboard_mode (this)
!= GtkLayerShell.KeyboardMode.NONE) {
GtkLayerShell.set_keyboard_mode (
this, GtkLayerShell.KeyboardMode.NONE);
}
}
-#endif
noti.remove_noti_timeout ();
- noti.destroy ();
+ box.remove (noti);
}
- if (!replaces
- && (!get_realized ()
- || !get_mapped ()
- || !(get_child () is Gtk.Widget)
- || box.get_children ().length () == 0)) {
- close ();
- return;
+ if (!replaces && box.length == 0) {
+ hide ();
+ // Reset to 0 due to Sway asserting that the size is a positive value
+ default_height = 0;
}
}
public void add_notification (NotifyParams param,
NotiDaemon noti_daemon) {
- var noti = new Notification.timed (param,
- noti_daemon,
- NotificationType.POPUP,
- ConfigModel.instance.timeout,
- ConfigModel.instance.timeout_low,
- ConfigModel.instance.timeout_critical);
-#if HAVE_LATEST_GTK_LAYER_SHELL
- if (noti.has_inline_reply) {
+ Notification notification = new Notification ();
+ notification.construct_notification (param, noti_daemon, NotificationType.POPUP);
+ if (notification.has_inline_reply) {
inline_reply_notifications.add (param.applied_id);
- if (GtkLayerShell.get_keyboard_mode (this)
+ if (swaync_daemon.use_layer_shell
+ && GtkLayerShell.get_keyboard_mode (this)
!= GtkLayerShell.KeyboardMode.ON_DEMAND) {
GtkLayerShell.set_keyboard_mode (
this, GtkLayerShell.KeyboardMode.ON_DEMAND);
}
}
-#endif
if (list_reverse) {
- box.pack_start (noti);
+ box.append (notification);
} else {
- box.pack_end (noti);
- }
- this.grab_focus ();
- if (!this.get_mapped () || !this.get_realized ()) {
- this.set_anchor ();
- this.show ();
+ box.prepend (notification);
}
- // IMPORTANT: queue a resize event to force the layout to be recomputed
- noti.queue_resize ();
+ change_visibility (true);
+
scroll_to_start (list_reverse);
}
@@ -245,14 +253,11 @@ namespace SwayNotificationCenter {
}
public uint32 ? get_latest_notification () {
- List children = box.get_children ();
- if (children.is_empty ()) return null;
-
Gtk.Widget ? child = null;
if (list_reverse) {
- child = children.last ().data;
+ child = box.get_last_child ();
} else {
- child = children.first ().data;
+ child = box.get_first_child ();
}
if (child == null || !(child is Notification)) return null;
diff --git a/src/style.css b/src/style.css
index 967a069e..0f35f900 100644
--- a/src/style.css
+++ b/src/style.css
@@ -18,11 +18,17 @@
@define-color bg-selected rgb(0, 128, 255);
.notification-row {
+ transition: all 200ms ease;
outline: none;
+ background: transparent;
}
-
-.notification-row:focus,
.notification-row:hover {
+ background: transparent;
+}
+
+.control-center .notification-row:focus,
+.control-center .notification-row:hover {
+ /* TODO: Check that focus bg works */
background: @noti-bg-focus;
}
@@ -94,7 +100,7 @@
.notification-default-action:hover,
.notification-action:hover {
- -gtk-icon-effect: none;
+ -gtk-icon-filter: none;
background: @noti-bg-hover;
}
@@ -202,7 +208,8 @@
/* Window behind control center and on all other monitors */
.blank-window {
- background: alpha(black, 0.25);
+ /* background: alpha(black, 0.25); */
+ background: transparent;
}
/*** Widgets ***/
@@ -261,6 +268,9 @@
padding: 8px;
margin: 8px;
}
+.widget-mpris-album-art {
+ border-radius: 12px;
+}
.widget-mpris-title {
font-weight: bold;
font-size: 1.25rem;
@@ -277,40 +287,67 @@
background-color: @noti-bg;
}
-.widget-buttons-grid>flowbox>flowboxchild>button{
- background: @noti-bg;
+.widget-buttons-grid-button {
border-radius: 12px;
}
-.widget-buttons-grid>flowbox>flowboxchild>button:hover {
+.widget-buttons-grid-button:hover {
background: @noti-bg-hover;
}
/* Menubar widget */
-.widget-menubar>box>.menu-button-bar>button {
- border: none;
- background: transparent;
-}
-
-/* .AnyName { Name defined in config after #
- background-color: @noti-bg;
- padding: 8px;
+.widget-menubar {
margin: 8px;
+ background: @noti-bg;
border-radius: 12px;
}
-
-.AnyName>button {
- background: transparent;
- border: none;
+.widget-menubar > box {
+ padding: 8px 0;
}
-
-.AnyName>button:hover {
- background-color: @noti-bg-hover;
-} */
-
-.topbar-buttons>button { /* Name defined in config after # */
- border: none;
- background: transparent;
+/* The left/right button container */
+.widget-menubar-container {
+}
+/* The left button container */
+.widget-menubar-container.start {
+ margin-left: 8px;
+}
+/* The right button container */
+.widget-menubar-container.end {
+ margin-right: 8px;
+}
+/* Each child element of the container (buttons box/toggle button) */
+.widget-menubar-child {
+ margin: 0 4px;
+ border-radius: 12px;
+}
+.widget-menubar-child:first-child {
+ margin-left: 0;
+}
+.widget-menubar-child:last-child {
+ margin-right: 0;
+}
+/* Each clickable button */
+.widget-menubar-button {
+}
+.widget-menubar-button:hover {
+ background: @noti-bg-hover;
+}
+.widget-menubar-button:checked {
+ background: @noti-bg-darker;
+}
+/* The container for each menubar "buttons" widget */
+.widget-menubar-buttons {
+}
+.widget-menubar-buttons > * {
+ border-radius: 0;
+}
+/* The menu revealer widget */
+.widget-menubar-menu {
+ padding: 0px 8px;
+}
+/* Each button in the revealer */
+.widget-menubar-menu > box > * {
+ margin-bottom: 8px;
}
/* Volume widget */
diff --git a/src/sway_notification_center.gresource.xml b/src/sway_notification_center.gresource.xml
deleted file mode 100644
index 584e488a..00000000
--- a/src/sway_notification_center.gresource.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- notificationWindow/notificationWindow.ui
- notification/notification.ui
- controlCenter/controlCenter.ui
- controlCenter/widgets/mpris/mpris_player.ui
-
-
diff --git a/src/swayncDaemon/swayncDaemon.vala b/src/swayncDaemon/swayncDaemon.vala
index 4efbd3f8..3b475f1e 100644
--- a/src/swayncDaemon/swayncDaemon.vala
+++ b/src/swayncDaemon/swayncDaemon.vala
@@ -15,7 +15,7 @@ namespace SwayNotificationCenter {
[DBus (visible = false)]
public signal void inhibited_changed (uint length);
- private Array blank_windows = new Array ();
+ private Array empty_windows = new Array ();
private unowned Gdk.Display ? display = Gdk.Display.get_default ();
[DBus (visible = false)]
@@ -65,26 +65,13 @@ namespace SwayNotificationCenter {
/// Blank windows
if (display == null) return;
- init_blank_windows (false);
+ unowned ListModel monitors = display.get_monitors ();
- display.closed.connect ((is_error) => {
- clear_blank_windows ();
- if (is_error) stderr.printf ("Display closed due to error!\n");
- });
-
- display.opened.connect ((d) => {
- bool visibility = noti_daemon.control_center.get_visibility ();
- init_blank_windows (visibility);
- });
-
- display.monitor_added.connect ((d, m) => {
- bool visibility = noti_daemon.control_center.get_visibility ();
- add_blank_window (d, m, visibility);
- });
+ init_empty_windows (monitors, false);
- display.monitor_removed.connect ((monitor) => {
+ monitors.items_changed.connect (() => {
bool visibility = noti_daemon.control_center.get_visibility ();
- init_blank_windows (visibility);
+ init_empty_windows (monitors, visibility);
});
}
@@ -98,45 +85,46 @@ namespace SwayNotificationCenter {
}
}
- private void add_blank_window (Gdk.Display display,
- Gdk.Monitor monitor,
+ private void add_empty_window (Gdk.Monitor monitor,
bool visible) {
- var win = new BlankWindow (display, monitor, this);
+ var win = new EmptyWindow (monitor, this);
win.set_visible (visible);
- blank_windows.append_val (win);
+ empty_windows.append_val (win);
}
- private void init_blank_windows (bool visible) {
- clear_blank_windows ();
+ private void init_empty_windows (ListModel monitors, bool visible) {
+ clear_empty_windows ();
// Add a window to all monitors
- for (int i = 0; i < display.get_n_monitors (); i++) {
- unowned Gdk.Monitor ? monitor = display.get_monitor (i);
+ for (int i = 0; i < monitors.get_n_items (); i++) {
+ GLib.Object ? obj = monitors.get_item (i);
+ if (obj == null || !(obj is Gdk.Monitor)) continue;
+ unowned Gdk.Monitor monitor = (Gdk.Monitor) obj;
if (monitor == null) continue;
- add_blank_window (display, monitor, visible);
+ add_empty_window (monitor, visible);
}
}
- private void clear_blank_windows () {
- while (blank_windows.length > 0) {
- uint i = blank_windows.length - 1;
- unowned BlankWindow ? win = blank_windows.index (i);
+ private void clear_empty_windows () {
+ while (empty_windows.length > 0) {
+ uint i = empty_windows.length - 1;
+ unowned EmptyWindow ? win = empty_windows.index (i);
win.close ();
- blank_windows.remove_index (i);
+ empty_windows.remove_index (i);
}
}
[DBus (visible = false)]
- public void show_blank_windows (Gdk.Monitor ? monitor) {
+ public void show_empty_windows (Gdk.Monitor ? monitor) {
if (!use_layer_shell) return;
- foreach (unowned BlankWindow win in blank_windows.data) {
+ foreach (unowned EmptyWindow win in empty_windows.data) {
if (win.monitor != monitor) win.show ();
}
}
[DBus (visible = false)]
- public void hide_blank_windows () {
+ public void hide_empty_windows () {
if (!use_layer_shell) return;
- foreach (unowned BlankWindow win in blank_windows.data) {
+ foreach (unowned EmptyWindow win in empty_windows.data) {
win.hide ();
}
}
@@ -224,14 +212,14 @@ namespace SwayNotificationCenter {
/** Toggles the visibility of the controlcenter */
public void toggle_visibility () throws DBusError, IOError {
if (noti_daemon.control_center.toggle_visibility ()) {
- noti_daemon.set_noti_window_visibility (false);
+ noti_daemon.hide_notification_window (false);
}
}
/** Sets the visibility of the controlcenter */
public void set_visibility (bool visibility) throws DBusError, IOError {
noti_daemon.control_center.set_visibility (visibility);
- if (visibility) noti_daemon.set_noti_window_visibility (false);
+ if (visibility) noti_daemon.hide_notification_window (false);
}
/** Toggles the current Do Not Disturb state */
diff --git a/src/swaync_template.gresource.xml b/src/swaync_template.gresource.xml
new file mode 100644
index 00000000..322644f6
--- /dev/null
+++ b/src/swaync_template.gresource.xml
@@ -0,0 +1,6 @@
+
+
+
+ templates/notificationContent.ui
+
+
diff --git a/src/templates/SwayNotificationCenter.cmb b/src/templates/SwayNotificationCenter.cmb
new file mode 100644
index 00000000..3a321a28
--- /dev/null
+++ b/src/templates/SwayNotificationCenter.cmb
@@ -0,0 +1,83 @@
+
+
+
+
+ (2,100,None,"notificationContent.ui",None,None,None,None,None,None,None)
+
+
+ (2,100,"AdwBin","SwayNotificationCenterNotificationContent",None,None,None,None,None,None),
+ (2,101,"GtkOverlay","overlay",100,None,None,None,None,None),
+ (2,102,"GtkRevealer","close_revealer",101,None,None,None,None,None),
+ (2,103,"GtkButton","close_button",102,None,None,None,None,None),
+ (2,104,"GtkImage",None,103,None,None,None,None,None),
+ (2,105,"GtkBox","base_box",101,None,None,None,1,None),
+ (2,106,"GtkBox","default_action",105,None,None,None,None,None),
+ (2,107,"GtkBox",None,106,None,None,None,None,None),
+ (2,108,"GtkImage","img",107,None,None,None,None,None),
+ (2,109,"GtkBox",None,107,None,None,None,1,None),
+ (2,110,"GtkBox",None,109,None,None,None,None,None),
+ (2,111,"GtkLabel","summary_label",110,None,None,None,None,None),
+ (2,112,"GtkLabel","time_label",110,None,None,None,1,None),
+ (2,113,"GtkLabel","body_label",109,None,None,None,1,None),
+ (2,114,"GtkImage","body_image",106,None,None,None,1,None),
+ (2,115,"GtkProgressBar","progress_bar",106,None,None,None,2,None),
+ (2,116,"GtkBox","inline_reply_box",106,None,None,None,3,None),
+ (2,118,"GtkButton","inline_reply_button",116,None,None,None,1,None),
+ (2,120,"GtkEntry","inline_reply_entry",116,None,None,None,None,None)
+
+
+ (2,101,"GtkWidget","css-classes","notification-background",None,None,None,None,None,None,None,None,None),
+ (2,101,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,102,"GtkRevealer","child",None,None,None,None,None,103,None,None,None,None),
+ (2,102,"GtkRevealer","transition-type","crossfade",None,None,None,None,None,None,None,None,None),
+ (2,102,"GtkWidget","halign","end",None,None,None,None,None,None,None,None,None),
+ (2,102,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,102,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None),
+ (2,102,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,103,"GtkButton","child",None,None,None,None,None,104,None,None,None,None),
+ (2,103,"GtkWidget","css-classes","close-button\ncircular",None,None,None,None,None,None,None,None,None),
+ (2,103,"GtkWidget","margin-bottom","2",None,None,None,None,None,None,None,None,None),
+ (2,103,"GtkWidget","margin-end","4",None,None,None,None,None,None,None,None,None),
+ (2,103,"GtkWidget","margin-start","4",None,None,None,None,None,None,None,None,None),
+ (2,103,"GtkWidget","margin-top","2",None,None,None,None,None,None,None,None,None),
+ (2,104,"GtkImage","icon-name","notifications-close-symbolic",None,None,None,None,None,None,None,None,None),
+ (2,104,"GtkImage","icon-size","normal",None,None,None,None,None,None,None,None,None),
+ (2,105,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None),
+ (2,105,"GtkWidget","css-classes","notification",None,None,None,None,None,None,None,None,None),
+ (2,106,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None),
+ (2,106,"GtkWidget","css-classes","notification-content\nnotification-default-action",None,None,None,None,None,None,None,None,None),
+ (2,108,"GtkImage","pixel-size","64",None,None,None,None,None,None,None,None,None),
+ (2,108,"GtkWidget","css-classes","image",None,None,None,None,None,None,None,None,None),
+ (2,108,"GtkWidget","margin-end","12",None,None,None,None,None,None,None,None,None),
+ (2,108,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None),
+ (2,109,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None),
+ (2,109,"GtkWidget","margin-end","14",None,None,None,None,None,None,None,None,None),
+ (2,111,"GtkLabel","ellipsize","end",None,None,None,None,None,None,None,None,None),
+ (2,111,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None),
+ (2,111,"GtkWidget","css-classes","summary",None,None,None,None,None,None,None,None,None),
+ (2,111,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,112,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None),
+ (2,112,"GtkLabel","yalign","0.0",None,None,None,None,None,None,None,None,None),
+ (2,112,"GtkWidget","css-classes","time",None,None,None,None,None,None,None,None,None),
+ (2,112,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkLabel","ellipsize","end",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkLabel","natural-wrap-mode","word",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkLabel","wrap","True",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkLabel","wrap-mode","word-char",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkLabel","yalign","0.0",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkWidget","css-classes","body",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,113,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,114,"GtkWidget","css-classes","body-image",None,None,None,None,None,None,None,None,None),
+ (2,114,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None),
+ (2,115,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,116,"GtkWidget","css-classes","inline-reply",None,None,None,None,None,None,None,None,None),
+ (2,116,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None),
+ (2,118,"GtkWidget","css-classes","inline-reply-button",None,None,None,None,None,None,None,None,None),
+ (2,118,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None),
+ (2,120,"GtkEntry","input-hints","emoji | spellcheck | uppercase-sentences",None,None,None,None,None,None,None,None,None),
+ (2,120,"GtkWidget","css-classes","inline-reply-entry",None,None,None,None,None,None,None,None,None),
+ (2,120,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None)
+
+
diff --git a/src/templates/notificationContent.ui b/src/templates/notificationContent.ui
new file mode 100644
index 00000000..c94f78dc
--- /dev/null
+++ b/src/templates/notificationContent.ui
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+ notification-background
+ True
+
+
+
+
+
+
+ notifications-close-symbolic
+ normal
+
+
+ close-button
+circular
+ 2
+ 4
+ 4
+ 2
+
+
+ end
+ True
+ crossfade
+ start
+ True
+
+
+
+
+ notification
+ vertical
+
+
+ notification-content
+notification-default-action
+ vertical
+
+
+
+
+ image
+ 12
+ 64
+ center
+
+
+
+
+ 14
+ vertical
+
+
+
+
+ summary
+ end
+ True
+ 0.0
+
+
+
+
+ time
+ start
+ 0.0
+ 0.0
+
+
+
+
+
+
+ body
+ end
+ True
+ word
+ True
+ True
+ word-char
+ 0.0
+ 0.0
+
+
+
+
+
+
+
+
+ body-image
+ center
+
+
+
+
+ True
+
+
+
+
+ inline-reply
+ True
+
+
+ inline-reply-entry
+ True
+ emoji | spellcheck | uppercase-sentences
+
+
+
+
+ inline-reply-button
+ end
+
+
+
+
+
+
+
+
+
+
+
+