
* 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
368 lines
16 KiB
Vala
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;
|
|
}
|
|
}
|
|
}
|
|
}
|