// // Copyright (C) 2012 Tom Beckmann, Rico Tzschichholz // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. // namespace Gala { public class Utils { private struct CachedIcon { public Gdk.Pixbuf icon; public int icon_size; public int scale; } private static Gee.HashMap<int, Gdk.Pixbuf?>? resize_pixbufs = null; private static Gee.HashMultiMap<DesktopAppInfo, CachedIcon?> icon_cache; private static Gee.HashMap<Meta.Window, DesktopAppInfo> window_to_desktop_cache; private static Gee.ArrayList<CachedIcon?> unknown_icon_cache; private static AppCache app_cache; private static Gtk.IconTheme icon_theme; static construct { icon_theme = new Gtk.IconTheme (); icon_theme.set_custom_theme ("elementary"); icon_cache = new Gee.HashMultiMap<DesktopAppInfo, CachedIcon?> (); window_to_desktop_cache = new Gee.HashMap<Meta.Window, DesktopAppInfo> (); unknown_icon_cache = new Gee.ArrayList<CachedIcon?> (); app_cache = new AppCache (); app_cache.changed.connect (() => { icon_cache.clear (); window_to_desktop_cache.clear (); }); } public static Gdk.Pixbuf get_icon_for_window (Meta.Window window, int icon_size, int scale) { var transient_for = window.get_transient_for (); if (transient_for != null) { return get_icon_for_window (transient_for, icon_size, scale); } GLib.DesktopAppInfo? desktop_app = null; desktop_app = window_to_desktop_cache[window]; if (desktop_app != null) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { return icon; } } var sandbox_id = window.get_sandboxed_app_id (); var wm_instance = window.get_wm_class_instance (); desktop_app = app_cache.lookup_startup_wmclass (wm_instance); if (desktop_app != null && check_app_prefix (desktop_app, sandbox_id)) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { window_to_desktop_cache[window] = desktop_app; return icon; } } var wm_class = window.get_wm_class (); desktop_app = app_cache.lookup_startup_wmclass (wm_class); if (desktop_app != null && check_app_prefix (desktop_app, sandbox_id)) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { window_to_desktop_cache[window] = desktop_app; return icon; } } desktop_app = lookup_desktop_wmclass (wm_instance); if (desktop_app != null && check_app_prefix (desktop_app, sandbox_id)) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { window_to_desktop_cache[window] = desktop_app; return icon; } } desktop_app = lookup_desktop_wmclass (wm_class); if (desktop_app != null && check_app_prefix (desktop_app, sandbox_id)) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { window_to_desktop_cache[window] = desktop_app; return icon; } } desktop_app = get_app_from_id (sandbox_id); if (desktop_app != null) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { window_to_desktop_cache[window] = desktop_app; return icon; } } var gapplication_id = window.get_gtk_application_id (); desktop_app = get_app_from_id (gapplication_id); if (desktop_app != null) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { window_to_desktop_cache[window] = desktop_app; return icon; } } if (window.get_client_type () == Meta.WindowClientType.X11) { #if HAS_MUTTER46 unowned Meta.Group group = window.x11_get_group (); #else unowned Meta.Group group = window.get_group (); #endif if (group != null) { var group_windows = group.list_windows (); group_windows.foreach ((window) => { if (window.get_window_type () != Meta.WindowType.NORMAL) { return; } if (window_to_desktop_cache[window] != null) { desktop_app = window_to_desktop_cache[window]; } }); if (desktop_app != null) { var icon = get_icon_for_desktop_app_info (desktop_app, icon_size, scale); if (icon != null) { window_to_desktop_cache[window] = desktop_app; return icon; } } } } // Haven't been able to get an icon for the window at this point, look to see // if we've already cached "application-default-icon" at this size foreach (var icon in unknown_icon_cache) { if (icon.icon_size == icon_size && icon.scale == scale) { return icon.icon; } } // Construct a new "application-default-icon" and store it in the cache try { var icon = icon_theme.load_icon_for_scale ("application-default-icon", icon_size, scale, 0); unknown_icon_cache.add (CachedIcon () { icon = icon, icon_size = icon_size, scale = scale }); return icon; } catch (Error e) { var icon = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, icon_size * scale, icon_size * scale); icon.fill (0x00000000); return icon; } } private static bool check_app_prefix (GLib.DesktopAppInfo app, string? sandbox_id) { if (sandbox_id == null) { return true; } var prefix = "%s.".printf (sandbox_id); if (app.get_id ().has_prefix (prefix)) { return true; } return false; } public static void clear_window_cache (Meta.Window window) { var desktop = window_to_desktop_cache[window]; if (desktop != null) { icon_cache.remove_all (desktop); window_to_desktop_cache.unset (window); } } private static GLib.DesktopAppInfo? get_app_from_id (string? id) { if (id == null) { return null; } var desktop_file = "%s.desktop".printf (id); return app_cache.lookup_id (desktop_file); } private static GLib.DesktopAppInfo? lookup_desktop_wmclass (string? wm_class) { if (wm_class == null) { return null; } var desktop_info = get_app_from_id (wm_class); if (desktop_info != null) { return desktop_info; } var canonicalized = wm_class.ascii_down ().delimit (" ", '-'); return get_app_from_id (canonicalized); } private static Gdk.Pixbuf? get_icon_for_desktop_app_info (GLib.DesktopAppInfo desktop, int icon_size, int scale) { if (icon_cache.contains (desktop)) { foreach (var icon in icon_cache[desktop]) { if (icon.icon_size == icon_size && icon.scale == scale) { return icon.icon; } } } var icon = desktop.get_icon (); if (icon is GLib.ThemedIcon) { var icon_names = ((GLib.ThemedIcon)icon).get_names (); var icon_info = icon_theme.choose_icon_for_scale (icon_names, icon_size, scale, 0); if (icon_info == null) { return null; } try { var pixbuf = icon_info.load_icon (); icon_cache.@set (desktop, CachedIcon () { icon = pixbuf, icon_size = icon_size, scale = scale }); return pixbuf; } catch (Error e) { return null; } } else if (icon is GLib.FileIcon) { var file = ((GLib.FileIcon)icon).file; var size_with_scale = icon_size * scale; try { var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (file.read (), size_with_scale, size_with_scale, true); icon_cache.@set (desktop, CachedIcon () { icon = pixbuf, icon_size = icon_size, scale = scale }); return pixbuf; } catch (Error e) { return null; } } return null; } /** * Multiplies an integer by a floating scaling factor, and then * returns the result rounded to the nearest integer */ public static int scale_to_int (int value, float scale_factor) { return (int) (Math.round ((float)value * scale_factor)); } /** * Get the number of toplevel windows on a workspace excluding those that are * on all workspaces * * @param workspace The workspace on which to count the windows */ public static uint get_n_windows (Meta.Workspace workspace, bool on_primary = false) { var n = 0; foreach (unowned var window in workspace.list_windows ()) { if (window.on_all_workspaces) { continue; } if ( (window.window_type == Meta.WindowType.NORMAL || window.window_type == Meta.WindowType.DIALOG || window.window_type == Meta.WindowType.MODAL_DIALOG) && (!on_primary || (on_primary && window.is_on_primary_monitor ()))) { n ++; } } return n; } /** * Creates an actor showing the current contents of the given WindowActor. * * @param actor The actor from which to create a snapshot * @param inner_rect The inner (actually visible) rectangle of the window * * @return A copy of the actor at that time or %NULL */ public static Clutter.Actor? get_window_actor_snapshot ( Meta.WindowActor actor, #if HAS_MUTTER45 Mtk.Rectangle inner_rect #else Meta.Rectangle inner_rect #endif ) { Clutter.Content content; try { content = actor.paint_to_content (inner_rect); } catch (Error e) { warning ("Could not create window snapshot: %s", e.message); return null; } if (content == null) { warning ("Could not create window snapshot"); return null; } var container = new Clutter.Actor (); container.set_size (inner_rect.width, inner_rect.height); container.content = content; return container; } /** * DEPRECATED: When used with Mutter 44, this will always return 1. * Get the scaling factor for the monitor you are drawing to with * `Meta.Display.get_monitor_scale` instead */ [Version (deprecated = true, deprecated_since = "7.0.3", replacement = "Meta.Display.get_monitor_scale")] public static int get_ui_scaling_factor () { #if HAS_MUTTER44 return 1; #else return Meta.Backend.get_backend ().get_settings ().get_ui_scaling_factor (); #endif } /** * Returns the pixbuf that is used for resize buttons throughout gala at a * size of 36px * * @return the resize button pixbuf or null if it failed to load */ public static Gdk.Pixbuf? get_resize_button_pixbuf (float scale) { var height = scale_to_int (36, scale); if (resize_pixbufs == null) { resize_pixbufs = new Gee.HashMap<int, Gdk.Pixbuf?> (); } if (resize_pixbufs[height] == null) { try { resize_pixbufs[height] = new Gdk.Pixbuf.from_resource_at_scale ( Config.RESOURCEPATH + "/buttons/resize.svg", -1, height, true ); } catch (Error e) { warning (e.message); return null; } } return resize_pixbufs[height]; } /** * Creates a new reactive ClutterActor at 36px with the resize pixbuf * * @return The resize button actor */ public static Clutter.Actor create_resize_button (float scale) { var texture = new Clutter.Actor (); var pixbuf = get_resize_button_pixbuf (scale); texture.reactive = true; if (pixbuf != null) { try { var image = new Clutter.Image (); Cogl.PixelFormat pixel_format = (pixbuf.get_has_alpha () ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888); image.set_data (pixbuf.get_pixels (), pixel_format, pixbuf.width, pixbuf.height, pixbuf.rowstride); texture.set_content (image); texture.set_size (pixbuf.width, pixbuf.height); } catch (Error e) {} } else { // we'll just make this red so there's at least something as an // indicator that loading failed. Should never happen and this // works as good as some weird fallback-image-failed-to-load pixbuf var size = scale_to_int (36, scale); texture.set_size (size, size); texture.background_color = { 255, 0, 0, 255 }; } return texture; } } }