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 @@ - - - - - - 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 @@ - - - - - - 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 @@ - - - - - - - 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 "&lt;" without this - // &lt; -> < - 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 "&lt;" without this + // &lt; -> < + 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 @@ - - - - - - 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 @@ + + + + + + +