diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9ae7839c..a2c61467 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y libhandy-1-dev libcanberra-dev libcanberra-gtk3-dev libgranite-dev libgtk-3-dev meson valac + apt install -y libadwaita-1-dev libcanberra-dev libgranite-dev libgtk-4-dev meson valac - name: Build env: DESTDIR: out diff --git a/README.md b/README.md index 873851b4..2d4b260b 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,9 @@ a Gtk notification server for Pantheon You'll need the following dependencies: * libcanberra -* libcanberra-gtk3 * libgranite-dev (>=5) -* libgtk-3-dev -* libhandy-1-dev (>=0.90.0) +* libgtk-4-dev +* libadwaita-1-dev * meson * valac diff --git a/demo/Application.vala b/demo/Application.vala index 466e5cfd..ee416e9a 100644 --- a/demo/Application.vala +++ b/demo/Application.vala @@ -41,7 +41,7 @@ public class Application : Gtk.Application { }); var window = new MainWindow (this); - window.show_all (); + window.present (); } public static int main (string[] args) { diff --git a/demo/MainWindow.vala b/demo/MainWindow.vala index df0da500..d8f0f780 100644 --- a/demo/MainWindow.vala +++ b/demo/MainWindow.vala @@ -74,17 +74,20 @@ public class MainWindow : Gtk.ApplicationWindow { action_spinbutton = new Gtk.SpinButton.with_range (0, 3, 1); var send_button = new Gtk.Button.with_label ("Send Notification") { - can_default = true, + // can_default = true, halign = Gtk.Align.END, margin_top = 12 }; - send_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + send_button.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); var grid = new Gtk.Grid () { valign = Gtk.Align.CENTER, column_spacing = 12, row_spacing = 12, - margin = 12 + margin_start = 12, + margin_end = 12, + margin_top = 12, + margin_bottom = 12 }; grid.attach (title_entry, 0, 0, 2); grid.attach (body_entry, 0, 1, 2); @@ -96,15 +99,16 @@ public class MainWindow : Gtk.ApplicationWindow { grid.attach (action_spinbutton, 1, 5); grid.attach (send_button, 0, 6, 2); - var toast = new Granite.Widgets.Toast (""); + var toast = new Granite.Toast (""); - var overlay = new Gtk.Overlay (); - overlay.add (grid); + var overlay = new Gtk.Overlay () { + child = grid + }; overlay.add_overlay (toast); - add (overlay); + child = overlay; - send_button.has_default = true; + // send_button.has_default = true; send_button.clicked.connect (send_notification); var toast_action = new SimpleAction ("toast", VariantType.STRING); diff --git a/demo/meson.build b/demo/meson.build index 0988bd33..8201a019 100644 --- a/demo/meson.build +++ b/demo/meson.build @@ -3,8 +3,8 @@ executable( 'Application.vala', 'MainWindow.vala', dependencies : [ - dependency ('granite'), - dependency ('gtk+-3.0'), + dependency ('granite-7'), + dependency ('gtk4'), ], install : true ) diff --git a/meson.build b/meson.build index 47a994fa..f512d068 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,7 @@ executable( 'src/AbstractBubble.vala', 'src/Application.vala', 'src/Bubble.vala', + 'src/CanberraGtk4.vala', 'src/Confirmation.vala', 'src/DBus.vala', 'src/Notification.vala', @@ -24,12 +25,13 @@ executable( css_gresource, dependencies: [ dependency ('libcanberra'), - dependency ('libcanberra-gtk3'), + dependency ('gee-0.8'), + dependency ('gio-unix-2.0'), dependency ('glib-2.0'), dependency ('gobject-2.0'), - dependency ('granite', version: '>=5.4.0'), - dependency ('gtk+-3.0'), - dependency ('libhandy-1') + dependency ('granite-7'), + dependency ('gtk4'), + dependency ('libadwaita-1') ], install : true ) diff --git a/src/AbstractBubble.vala b/src/AbstractBubble.vala index f19e7766..f7fcfdd3 100644 --- a/src/AbstractBubble.vala +++ b/src/AbstractBubble.vala @@ -20,13 +20,15 @@ public class Notifications.AbstractBubble : Gtk.Window { public signal void closed (uint32 reason); + public signal void button_release_event (); + protected Gtk.EventControllerMotion bubble_motion_controller; protected Gtk.Stack content_area; protected Gtk.Grid draw_area; private Gtk.Revealer revealer; private uint timeout_id; - private Hdy.Carousel carousel; + private Adw.Carousel carousel; construct { content_area = new Gtk.Stack () { @@ -36,57 +38,69 @@ public class Notifications.AbstractBubble : Gtk.Window { draw_area = new Gtk.Grid () { hexpand = true, - margin = 16 + margin_start = 16, + margin_end = 16, + margin_top = 16, + margin_bottom = 16 }; - draw_area.get_style_context ().add_class ("draw-area"); + draw_area.add_css_class ("draw-area"); draw_area.attach (content_area, 0, 0); - var close_button = new Gtk.Button.from_icon_name ("window-close-symbolic", Gtk.IconSize.LARGE_TOOLBAR) { + var close_button = new Gtk.Image.from_icon_name ("window-close-symbolic") { halign = Gtk.Align.START, - valign = Gtk.Align.START + valign = Gtk.Align.START, + pixel_size = 24 }; - close_button.get_style_context ().add_class ("close"); + close_button.add_css_class ("close"); + + var close_button_controller = new Gtk.GestureClick (); + close_button.add_controller (close_button_controller); var close_revealer = new Gtk.Revealer () { reveal_child = false, transition_type = Gtk.RevealerTransitionType.CROSSFADE, halign = Gtk.Align.START, - valign = Gtk.Align.START + valign = Gtk.Align.START, + child = close_button }; - close_revealer.add (close_button); - var overlay = new Gtk.Overlay (); - overlay.add (draw_area); + var overlay = new Gtk.Overlay () { + child = draw_area + }; overlay.add_overlay (close_revealer); revealer = new Gtk.Revealer () { reveal_child = true, transition_duration = 195, - transition_type = Gtk.RevealerTransitionType.CROSSFADE + transition_type = Gtk.RevealerTransitionType.CROSSFADE, + child = overlay }; - revealer.add (overlay); - var label = new Gtk.Grid (); + var label = new Gtk.Grid () { + visible = false + }; - carousel = new Hdy.Carousel () { + carousel = new Adw.Carousel () { allow_mouse_drag = true, interactive = true, halign = Gtk.Align.END, hexpand = true }; - carousel.add (new Gtk.Grid ()); - carousel.add (revealer); - carousel.scroll_to (revealer); + // carousel.append (new Gtk.Grid ()); + carousel.append (revealer); + carousel.scroll_to (revealer, true); default_height = 0; default_width = 332; resizable = false; - type_hint = Gdk.WindowTypeHint.NOTIFICATION; + // type_hint = Gdk.WindowTypeHint.NOTIFICATION; get_style_context ().add_class ("notification"); // Prevent stealing focus when an app window is closed - set_accept_focus (false); + can_focus = false; set_titlebar (label); - add (carousel); + child = carousel; + // Set title to make it recognizable to window manager + title = "io.elementary.notifications"; carousel.page_changed.connect ((index) => { if (index == 0) { @@ -95,24 +109,34 @@ public class Notifications.AbstractBubble : Gtk.Window { } }); - close_button.button_release_event.connect (() => { + close_button_controller.released.connect (() => { closed (Notifications.Server.CloseReason.DISMISSED); dismiss (); - return Gdk.EVENT_STOP; + // return Gdk.EVENT_STOP; }); - enter_notify_event.connect (() => { + var bubble_gesture_controller = new Gtk.GestureClick (); + ((Gtk.Widget) this).add_controller (bubble_gesture_controller); + + bubble_gesture_controller.released.connect (() => { + button_release_event (); + }); + + bubble_motion_controller = new Gtk.EventControllerMotion (); + revealer.add_controller (bubble_motion_controller); + + bubble_motion_controller.enter.connect (() => { close_revealer.reveal_child = true; stop_timeout (); - return Gdk.EVENT_PROPAGATE; + // return Gdk.EVENT_PROPAGATE; }); - leave_notify_event.connect ((event) => { - if (event.detail == Gdk.NotifyType.INFERIOR) { - return Gdk.EVENT_STOP; - } + bubble_motion_controller.leave.connect ((event) => { + // if (event.detail == Gdk.NotifyType.INFERIOR) { + // return Gdk.EVENT_STOP; + // } close_revealer.reveal_child = false; - return Gdk.EVENT_PROPAGATE; + // return Gdk.EVENT_PROPAGATE; }); } diff --git a/src/Application.vala b/src/Application.vala index cd56e598..ea41742c 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -63,9 +63,9 @@ public class Notifications.Application : Gtk.Application { var css_provider = new Gtk.CssProvider (); css_provider.load_from_resource (resource_base_path + "/application.css"); - Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + Gtk.StyleContext.add_provider_for_display (Gdk.Display.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - unowned var context = CanberraGtk.context_get (); + unowned var context = CanberraGtk4.context_get (); context.change_props ( Canberra.PROP_APPLICATION_NAME, "Notifications", Canberra.PROP_APPLICATION_ID, application_id, diff --git a/src/Bubble.vala b/src/Bubble.vala index d5c613ce..d48db16b 100644 --- a/src/Bubble.vala +++ b/src/Bubble.vala @@ -34,12 +34,12 @@ public class Notifications.Bubble : AbstractBubble { construct { var contents = new Contents (notification); - content_area.add (contents); + content_area.add_child (contents); switch (notification.priority) { case GLib.NotificationPriority.HIGH: case GLib.NotificationPriority.URGENT: - content_area.get_style_context ().add_class ("urgent"); + content_area.add_css_class ("urgent"); break; default: start_timeout (4000); @@ -74,13 +74,13 @@ public class Notifications.Bubble : AbstractBubble { } } - return Gdk.EVENT_STOP; + // return Gdk.EVENT_STOP; }); - leave_notify_event.connect (() => { - if (notification.priority == GLib.NotificationPriority.HIGH || notification.priority == GLib.NotificationPriority.URGENT) { - return Gdk.EVENT_PROPAGATE; - } + bubble_motion_controller.leave.connect (() => { + // if (notification.priority == GLib.NotificationPriority.HIGH || notification.priority == GLib.NotificationPriority.URGENT) { + // return Gdk.EVENT_PROPAGATE; + // } start_timeout (4000); }); } @@ -89,14 +89,14 @@ public class Notifications.Bubble : AbstractBubble { start_timeout (4000); var new_contents = new Contents (new_notification); - new_contents.show_all (); + new_contents.show (); new_contents.action_invoked.connect ((action_key) => { action_invoked (action_key); dismiss (); }); - content_area.add (new_contents); + content_area.add_child (new_contents); content_area.visible_child = new_contents; } @@ -121,14 +121,14 @@ public class Notifications.Bubble : AbstractBubble { app_image.pixel_size = 24; app_image.halign = app_image.valign = Gtk.Align.END; - image_overlay.add (notification.image); + image_overlay.child = notification.image; image_overlay.add_overlay (app_image); } else { app_image.pixel_size = 48; - image_overlay.add (app_image); + image_overlay.child = app_image; if (notification.badge_icon != null) { - var badge_image = new Gtk.Image.from_gicon (notification.badge_icon, Gtk.IconSize.LARGE_TOOLBAR) { + var badge_image = new Gtk.Image.from_gicon (notification.badge_icon) { halign = Gtk.Align.END, valign = Gtk.Align.END, pixel_size = 24 @@ -144,7 +144,7 @@ public class Notifications.Bubble : AbstractBubble { width_chars = 33, xalign = 0 }; - title_label.get_style_context ().add_class ("title"); + title_label.add_css_class ("title"); var body_label = new Gtk.Label (notification.body) { ellipsize = Pango.EllipsizeMode.END, @@ -179,20 +179,23 @@ public class Notifications.Bubble : AbstractBubble { halign = Gtk.Align.END, homogeneous = true }; - action_area.get_style_context ().add_class ("buttonbox"); + action_area.add_css_class ("buttonbox"); bool action_area_packed = false; for (int i = 0; i < notification.actions.length; i += 2) { if (notification.actions[i] != "default") { - var button = new Gtk.Button.with_label (notification.actions[i + 1]); + var button = new Gtk.Button.with_label (notification.actions[i + 1]) { + width_request = 85, + height_request = 27 + }; var action = notification.actions[i].dup (); button.clicked.connect (() => { action_invoked (action); }); - action_area.pack_end (button); + action_area.prepend (button); if (!action_area_packed) { attach (action_area, 0, 2, 2); diff --git a/src/CanberraGtk4.vala b/src/CanberraGtk4.vala new file mode 100644 index 00000000..4a3a34c7 --- /dev/null +++ b/src/CanberraGtk4.vala @@ -0,0 +1,76 @@ +/* + * Copyright 2022 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/* code adapted from libcanberra */ + +namespace CanberraGtk4 { + private Canberra.Context? context = null; + + public unowned Canberra.Context? context_get () { + Canberra.Proplist proplist; + + if (context != null) { + return context; + } if (Canberra.Context.create (out context) != Canberra.SUCCESS) { + return null; + } if (Canberra.Proplist.create (out proplist) != Canberra.SUCCESS) { + return null; + } + + unowned var name = GLib.Environment.get_application_name (); + if (name != null) { + proplist.sets (Canberra.PROP_APPLICATION_NAME, name); + } else { + proplist.sets (Canberra.PROP_APPLICATION_NAME, "libcanberra-gtk"); + proplist.sets (Canberra.PROP_APPLICATION_VERSION, "%i.%i".printf (Canberra.MAJOR, Canberra.MINOR)); + proplist.sets (Canberra.PROP_APPLICATION_ID, "org.freedesktop.libcanberra.gtk"); + } + + unowned var icon = Gtk.Window.get_default_icon_name (); + if (icon != null) { + proplist.sets (Canberra.PROP_APPLICATION_ICON_NAME, icon); + } + + unowned var display = Gdk.Display.get_default (); + if (display is Gdk.X11.Display) { + unowned var display_name = display.get_name (); + if (display_name != null) { + proplist.sets (Canberra.PROP_WINDOW_X11_SCREEN, display_name); + } + + var screen = "%i".printf (((Gdk.X11.Display) display).get_screen ().get_screen_number ()); + proplist.sets (Canberra.PROP_WINDOW_X11_SCREEN, screen); + } + + context.change_props_full (proplist); + + var val = Value (typeof (string)); + if (display.get_setting ("gtk-sound-theme-name", val)) { + context.change_props (Canberra.PROP_CANBERRA_XDG_THEME_NAME, val.get_string ()); + } + + val = Value (typeof (bool)); + if (display.get_setting ("gtk-enable-event-sounds", val)) { + unowned var env = GLib.Environment.get_variable ("CANBERRA_FORCE_EVENT_SOUNDS"); + context.change_props (Canberra.PROP_CANBERRA_ENABLE, env != null ? true : val.get_boolean ()); + } + + display.setting_changed.connect ((setting) => { + Value new_val; + if (setting == "gtk-sound-theme-name") { + new_val = Value (typeof (string)); + display.get_setting ("gtk-sound-theme-name", new_val); + context.change_props (Canberra.PROP_CANBERRA_ENABLE, new_val.get_string ()); + } else if (setting == "gtk-enable-event-sounds") { + new_val = Value (typeof (bool)); + unowned var env = GLib.Environment.get_variable ("CANBERRA_FORCE_EVENT_SOUNDS"); + display.get_setting ("gtk-enable-event-sounds", new_val); + context.change_props (Canberra.PROP_CANBERRA_ENABLE, env != null ? true : new_val.get_boolean ()); + } + }); + + return context; + } +} diff --git a/src/Confirmation.vala b/src/Confirmation.vala index 68405823..58d109a4 100644 --- a/src/Confirmation.vala +++ b/src/Confirmation.vala @@ -30,7 +30,7 @@ public class Notifications.Confirmation : AbstractBubble { } construct { - var image = new Gtk.Image.from_icon_name (icon_name, Gtk.IconSize.DIALOG) { + var image = new Gtk.Image.from_icon_name (icon_name) { valign = Gtk.Align.START, pixel_size = 48 }; @@ -41,7 +41,7 @@ public class Notifications.Confirmation : AbstractBubble { margin_end = 6, width_request = 228 }; - progressbar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + progressbar.add_css_class (Granite.STYLE_CLASS_FLAT); var contents = new Gtk.Grid () { column_spacing = 6 @@ -49,9 +49,9 @@ public class Notifications.Confirmation : AbstractBubble { contents.attach (image, 0, 0); contents.attach (progressbar, 1, 0); - content_area.add (contents); + content_area.add_child (contents); - get_style_context ().add_class ("confirmation"); + add_css_class ("confirmation"); bind_property ("icon-name", image, "icon-name"); bind_property ("progress", progressbar, "fraction"); @@ -61,9 +61,9 @@ public class Notifications.Confirmation : AbstractBubble { start_timeout (2000); }); - leave_notify_event.connect (() => { + bubble_motion_controller.leave.connect (() => { start_timeout (2000); - return Gdk.EVENT_PROPAGATE; + // return Gdk.EVENT_PROPAGATE; }); } } diff --git a/src/DBus.vala b/src/DBus.vala index bee4807b..128a6d9c 100644 --- a/src/DBus.vala +++ b/src/DBus.vala @@ -122,7 +122,7 @@ public class Notifications.Server : Object { bubbles[id].replace (notification); } else { bubbles[id] = new Notifications.Bubble (notification, id); - bubbles[id].show_all (); + bubbles[id].present (); bubbles[id].action_invoked.connect ((action_key) => { action_invoked (id, action_key); @@ -177,15 +177,16 @@ public class Notifications.Server : Object { icon_name, progress_value ); - confirmation.destroy.connect (() => { + confirmation.close_request.connect (() => { confirmation = null; + return false; }); } else { confirmation.icon_name = icon_name; confirmation.progress = progress_value; } - confirmation.show_all (); + confirmation.present (); } private void send_sound (HashTable hints, string sound_name = "dialog-information") { @@ -203,7 +204,7 @@ public class Notifications.Server : Object { props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile"); props.sets (Canberra.PROP_EVENT_ID, sound_name); - CanberraGtk.context_get ().play_full (0, props); + CanberraGtk4.context_get ().play_full (0, props); } } diff --git a/src/Notification.vala b/src/Notification.vala index 3c8cfea6..856c0f64 100644 --- a/src/Notification.vala +++ b/src/Notification.vala @@ -91,7 +91,7 @@ public class Notifications.Notification : GLib.Object { var image_path = variant.get_string (); // GLib.Notification also sends icon names via this hint - if (Gtk.IconTheme.get_default ().has_icon (image_path) && image_path != app_icon) { + if (Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).has_icon (image_path) && image_path != app_icon) { badge_icon = new ThemedIcon (image_path); } else if (image_path.has_prefix ("/") || image_path.has_prefix ("file://")) { try { diff --git a/src/Widgets/MaskedImage.vala b/src/Widgets/MaskedImage.vala index 336e4119..1c6e312a 100644 --- a/src/Widgets/MaskedImage.vala +++ b/src/Widgets/MaskedImage.vala @@ -18,15 +18,20 @@ * */ -public class Notifications.MaskedImage : Gtk.Overlay { +public class Notifications.MaskedImage : Gtk.Widget { private const int ICON_SIZE = 48; + public Gtk.Overlay overlay_widget { get; set; } public Gdk.Pixbuf pixbuf { get; construct; } public MaskedImage (Gdk.Pixbuf pixbuf) { Object (pixbuf: pixbuf); } + static construct { + set_layout_manager_type (typeof (Gtk.BinLayout)); + } + construct { var mask = new Gtk.Image.from_resource ("/io/elementary/notifications/image-mask.svg"); mask.pixel_size = ICON_SIZE; @@ -37,8 +42,10 @@ public class Notifications.MaskedImage : Gtk.Overlay { image.gicon = mask_pixbuf (pixbuf, scale); image.pixel_size = ICON_SIZE; - add (image); - add_overlay (mask); + overlay_widget = new Gtk.Overlay (); + overlay_widget.child = image; + overlay_widget.add_overlay (mask); + overlay_widget.set_parent (this); } private static Gdk.Pixbuf? mask_pixbuf (Gdk.Pixbuf pixbuf, int scale) { @@ -54,7 +61,14 @@ public class Notifications.MaskedImage : Gtk.Overlay { var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, mask_size, mask_size); var cr = new Cairo.Context (surface); - Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, offset_x, offset_y, size, size, mask_offset); + // replace Granite.Drawing.Utilities.cairo_rounded_rectangle + cr.move_to (offset_x + mask_offset, offset_y); + cr.arc (offset_x + size - mask_offset, offset_y + mask_offset, mask_offset, Math.PI * 1.5, Math.PI * 2); + cr.arc (offset_x + size - mask_offset, offset_y + size - mask_offset, mask_offset, 0, Math.PI * 0.5); + cr.arc (offset_x + mask_offset, offset_y + size - mask_offset, mask_offset, Math.PI * 0.5, Math.PI); + cr.arc (offset_x + mask_offset, offset_y + mask_offset, mask_offset, Math.PI, Math.PI * 1.5); + cr.close_path (); + cr.clip (); Gdk.cairo_set_source_pixbuf (cr, input, offset_x, offset_y); @@ -62,4 +76,10 @@ public class Notifications.MaskedImage : Gtk.Overlay { return Gdk.pixbuf_get_from_surface (surface, 0, 0, mask_size, mask_size); } + + ~MaskedImage () { + while (get_last_child () != null) { + get_last_child ().unparent (); + } + } } diff --git a/vapi/libcanberra-gtk3.vapi b/vapi/libcanberra-gtk3.vapi deleted file mode 100644 index f788f8e0..00000000 --- a/vapi/libcanberra-gtk3.vapi +++ /dev/null @@ -1,37 +0,0 @@ -/*** - This file is part of libcanberra. - - Copyright 2009 Lennart Poettering - - libcanberra is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation, either version 2.1 of the - License, or (at your option) any later version. - - libcanberra is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with libcanberra. If not, see - . -***/ - -using Canberra; -using Gdk; -using Gtk; - -[CCode (cprefix = "CA_GTK_", lower_case_cprefix = "ca_gtk_", cheader_filename = "canberra-gtk.h")] -namespace CanberraGtk { - - public unowned Context? context_get(); - public unowned Context? context_get_for_screen(Gdk.Screen? screen); - - public int proplist_set_for_widget(Proplist p, Gtk.Widget w); - public int play_for_widget(Gtk.Widget w, uint32 id, ...); - public int proplist_set_for_event(Proplist p, Gdk.Event e); - public int play_for_event(Gdk.Event e, uint32 id, ...); - - public void widget_disable_sounds(Gtk.Widget w, bool enable = false); -}