Files
SwayNotificationCenter/src/functions.vala
Erik Reider 86166a46b7 Grouped notifications (#345)
* Initial animation work

* Added custom icon

* Fixed collapse icon not being symbolic

* Centered collapse button

* Fixed group of 2 notifications being invisible

* Added back notification logic

Notifications are now separated into their own group depending on their provided name

* Added close all button

Also changed the notification close button icon to the new provided icon

* Fixed replacing notifications not working as expected

* Fixed group sensitivity not being set when auto collapsed

* Don't group notis with no provided app-name/desktop-file

Also adds parsing of desktop file in NotiModel which helps with getting and using the display name as the group name

* Remove testing notifications

* General fixes

* Added fade to cc viewport

* Added padding to cc viewport fade

* Call on_expand_change on close all button click

* Updated README

* Sort critical notification groups before regular groups

* Added group title icon

* Fixed not being able to navigate through single notifications

* Scroll to top of group on expand

* Fix non expanded single noti groups being clickable

* Fixed linting issues

* Added styling

* Fixed invalid style reload cast

* Set lower ordered notifications content opacity to 0

* Added hover effect to groups
2023-12-12 21:02:46 +01:00

368 lines
16 KiB
Vala

namespace SwayNotificationCenter {
public class Functions {
private static Gtk.CssProvider system_css_provider;
private static Gtk.CssProvider user_css_provider;
private Functions () {}
public static void init () {
system_css_provider = new Gtk.CssProvider ();
user_css_provider = new Gtk.CssProvider ();
// Init resources
var theme = Gtk.IconTheme.get_default ();
theme.add_resource_path ("/org/erikreider/swaync/icons");
}
public static void set_image_path (owned string path,
Gtk.Image img,
int icon_size,
bool file_exists) {
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);
return;
} catch (Error e) {
stderr.printf (e.message + "\n");
}
} else if (Gtk.IconTheme.get_default ().has_icon (path)) {
// Try as a freedesktop.org-compliant icon theme
img.set_from_icon_name (path, Notification.icon_size);
}
}
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);
}
/** Load the package provided CSS file as a base.
* Without this, an empty user CSS file would result in widgets
* with default GTK style properties
*/
public static bool load_css (string ? style_path) {
int css_priority = ConfigModel.instance.cssPriority.get_priority ();
// Load packaged CSS as backup
string system_css = get_style_path (null, true);
system_css = File.new_for_path (system_css).get_path () ?? system_css;
message ("Loading CSS: \"%s\"", system_css);
try {
system_css_provider.load_from_path (system_css);
Gtk.StyleContext.add_provider_for_screen (
Gdk.Screen.get_default (),
system_css_provider,
css_priority);
} catch (Error e) {
critical ("Load packaged CSS Error (\"%s\"):\n\t%s\n", system_css, e.message);
}
// Load user CSS
string user_css = get_style_path (style_path);
user_css = File.new_for_path (user_css).get_path () ?? user_css;
message ("Loading CSS: \"%s\"", user_css);
try {
user_css_provider.load_from_path (user_css);
Gtk.StyleContext.add_provider_for_screen (
Gdk.Screen.get_default (),
user_css_provider,
css_priority);
} catch (Error e) {
critical ("Load user CSS Error (\"%s\"):\n\t%s\n", user_css, e.message);
return false;
}
return true;
}
public static string get_style_path (owned string ? custom_path,
bool only_system = false) {
string[] paths = {};
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;
}
if (!only_system) {
paths += Path.build_path (Path.DIR_SEPARATOR.to_string (),
Environment.get_user_config_dir (),
"swaync/style.css");
}
foreach (var path in Environment.get_system_config_dirs ()) {
paths += Path.build_path (Path.DIR_SEPARATOR.to_string (),
path, "swaync/style.css");
}
// Fallback location. Specified in postinstall.py. Mostly for Debian
paths += "/usr/local/etc/xdg/swaync/style.css";
info ("Looking for CSS file in these directories:\n\t- %s",
string.joinv ("\n\t- ", paths));
string path = "";
foreach (string try_path in paths) {
if (File.new_for_path (try_path).query_exists ()) {
path = try_path;
break;
}
}
if (path == "") {
stderr.printf (
"COULD NOT FIND CSS FILE! REINSTALL THE PACKAGE!\n");
Process.exit (1);
}
return path;
}
public static string get_config_path (owned string ? custom_path) {
string[] paths = {};
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:];
}
if (File.new_for_path (custom_path).query_exists ()) {
paths += custom_path;
} else {
critical ("Custom config file \"%s\" not found, skipping...", custom_path);
}
}
paths += Path.build_path (Path.DIR_SEPARATOR.to_string (),
Environment.get_user_config_dir (),
"swaync/config.json");
foreach (var path in Environment.get_system_config_dirs ()) {
paths += Path.build_path (Path.DIR_SEPARATOR.to_string (),
path, "swaync/config.json");
}
// Fallback location. Specified in postinstall.py. Mostly for Debian
paths += "/usr/local/etc/xdg/swaync/config.json";
info ("Looking for config file in these directories:\n\t- %s",
string.joinv ("\n\t- ", paths));
string path = "";
foreach (string try_path in paths) {
if (File.new_for_path (try_path).query_exists ()) {
path = try_path;
break;
}
}
if (path == "") {
stderr.printf (
"COULD NOT FIND CONFIG FILE! REINSTALL THE PACKAGE!\n");
Process.exit (1);
}
return path;
}
public static string get_match_from_info (MatchInfo info) {
var all = info.fetch_all ();
if (all.length > 1 && all[1].length > 0) {
string img = all[1];
// Replaces "~/" with $HOME
if (img.index_of ("~/", 0) == 0) {
img = Environment.get_home_dir () +
img.slice (1, img.length);
}
return img;
}
return "";
}
/** Gets the base type of a type if it's derivited */
public static Type get_base_type (Type type) {
if (type.is_derived ()) {
while (type.is_derived ()) {
type = type.parent ();
}
}
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;
if (window_ratio > bg_ratio) { // Taller wallpaper than monitor
double scale = (double) buffer_width / width;
if (scale * height < buffer_height) {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf);
} else {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf);
}
} else { // Wider wallpaper than monitor
double scale = (double) buffer_height / height;
if (scale * width < buffer_width) {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf);
} else {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf);
}
}
cr.paint ();
cr.restore ();
scale_surf.finish ();
return Gdk.pixbuf_get_from_surface (surface, 0, 0, buffer_width, buffer_height);
}
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 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);
}
public delegate bool FilterFunc (char character);
public static string filter_string (string body, FilterFunc func) {
string result = "";
foreach (char char in (char[]) body.data) {
if (!func (char)) continue;
result += char.to_string ();
}
return result;
}
public static async bool execute_command (string cmd, string[] env_additions = {}, out string msg) {
msg = "";
try {
string[] spawn_env = Environ.get ();
// Export env variables
foreach (string additions in env_additions) {
spawn_env += additions;
}
string[] argvp = {};
Shell.parse_argv (cmd, out argvp);
Pid child_pid;
int std_input;
int std_output;
int std_err;
Process.spawn_async_with_pipes (
"/",
argvp,
spawn_env,
SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
null,
out child_pid,
out std_input,
out std_output,
out std_err);
// stdout:
string res = "";
IOChannel output = new IOChannel.unix_new (std_output);
output.add_watch (IOCondition.IN | IOCondition.HUP, (channel, condition) => {
if (condition == IOCondition.HUP) {
return false;
}
try {
channel.read_line (out res, null, null);
return true;
} catch (IOChannelError e) {
stderr.printf ("stdout: IOChannelError: %s\n", e.message);
return false;
} catch (ConvertError e) {
stderr.printf ("stdout: ConvertError: %s\n", e.message);
return false;
}
});
// Close the child when the spawned process is idling
int end_status = 0;
ChildWatch.add (child_pid, (pid, status) => {
Process.close_pid (pid);
end_status = status;
execute_command.callback ();
});
// Waits until `run_script.callback()` is called above
yield;
msg = res;
return end_status == 0;
} catch (Error e) {
stderr.printf ("Run_Script Error: %s\n", e.message);
msg = e.message;
return false;
}
}
}
}