diff --git a/src/AppSystem/AppCache.vala b/src/AppSystem/AppCache.vala new file mode 100644 index 00000000..dd29610e --- /dev/null +++ b/src/AppSystem/AppCache.vala @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Gala.AppCache : Object { + private const int DEFAULT_TIMEOUT_SECONDS = 3; + + private ListStore _apps; + public ListModel apps { get { return _apps; } } + + private HashTable id_to_app; + + private AppInfoMonitor app_info_monitor; + + private uint queued_update_id = 0; + + construct { + _apps = new ListStore (typeof (DesktopAppInfo)); + id_to_app = new HashTable (str_hash, str_equal); + + app_info_monitor = AppInfoMonitor.@get (); + app_info_monitor.changed.connect (queue_cache_update); + + rebuild_cache.begin (); + } + + private void queue_cache_update () { + if (queued_update_id != 0) { + return; + } + + queued_update_id = Timeout.add_seconds (DEFAULT_TIMEOUT_SECONDS, () => { + rebuild_cache.begin ();; + return Source.REMOVE; + }); + } + + private async void rebuild_cache () { + SourceFunc callback = rebuild_cache.callback; + + new Thread ("rebuild_cache", () => { + lock (id_to_app) { + id_to_app.remove_all (); + + var app_infos = AppInfo.get_all (); + + foreach (unowned var app in app_infos) { + id_to_app[app.get_id ()] = (DesktopAppInfo) app; + } + } + + Idle.add ((owned) callback); + }); + + yield; + + _apps.splice (0, _apps.n_items, id_to_app.get_values_as_ptr_array ().data); + queued_update_id = 0; + } + + public DesktopAppInfo get_app_by_id (string id) { + return id_to_app[id]; + } +} diff --git a/src/AppSystem/AppWidget.vala b/src/AppSystem/AppWidget.vala new file mode 100644 index 00000000..4a49ee22 --- /dev/null +++ b/src/AppSystem/AppWidget.vala @@ -0,0 +1,166 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +/** + * A widget that shows the app icon along with the unity badge and progress bar + * if they are requested by the app. + */ +public class Dock.AppWidget : Granite.Bin { + private static Settings? notify_settings; + + static construct { + if (SettingsSchemaSource.get_default ().lookup ("io.elementary.notifications", true) != null) { + notify_settings = new Settings ("io.elementary.notifications"); + } + } + + public App app { get; construct; } + + public int icon_size { set { image.pixel_size = value; } } + + private Gtk.Image image; + private Gtk.Label badge; + private Gtk.Revealer progress_revealer; + private Adw.TimedAnimation badge_fade; + private Adw.TimedAnimation badge_scale; + + public AppWidget (App app) { + Object (app: app); + } + + construct { + image = new Gtk.Image (); + + var icon = app.app_info.get_icon (); + if (icon != null && Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).has_gicon (icon)) { + image.gicon = icon; + } else { + image.gicon = new ThemedIcon ("application-default-icon"); + } + + badge = new Gtk.Label ("!"); + badge.add_css_class (Granite.STYLE_CLASS_BADGE); + app.bind_property ("current_count", badge, "label", SYNC_CREATE, + (binding, srcval, ref targetval) => { + var src = (int64) srcval; + + if (src > 0) { + targetval.set_string ("%lld".printf (src)); + } else { + targetval.set_string ("!"); + } + + return true; + }, null + ); + + var badge_container = new Granite.Bin () { + can_target = false, + child = badge, + halign = END, + valign = START, + overflow = VISIBLE + }; + + progress_revealer = new Gtk.Revealer () { + can_target = false, + transition_type = CROSSFADE + }; + + var overlay = new Gtk.Overlay () { + child = image + }; + overlay.add_overlay (badge_container); + overlay.add_overlay (progress_revealer); + + child = overlay; + + // We have to destroy the progressbar when it is not needed otherwise it will + // cause continuous layouting of the surface see https://github.com/elementary/dock/issues/279 + progress_revealer.notify["child-revealed"].connect (() => { + if (!progress_revealer.child_revealed) { + progress_revealer.child = null; + } + }); + + badge_scale = new Adw.TimedAnimation ( + badge, 0.25, 1, + Granite.TRANSITION_DURATION_OPEN, + new Adw.CallbackAnimationTarget ((val) => { + var height = badge_container.get_height (); + var width = badge_container.get_width (); + + var x = (float) (width - (val * width)) / 2; + var y = (float) (height - (val * height)) / 2; + + badge.allocate ( + width, height, -1, + new Gsk.Transform ().scale ((float) val, (float) val).translate (Graphene.Point ().init (x, y)) + ); + }) + ); + + badge_fade = new Adw.TimedAnimation ( + badge, 0, 1, + Granite.TRANSITION_DURATION_OPEN, + new Adw.CallbackAnimationTarget ((val) => { + badge.opacity = val; + }) + ) { + easing = EASE_IN_OUT_QUAD + }; + + app.notify["count-visible"].connect (update_badge_revealed); + update_badge_revealed (); + + if (notify_settings != null) { + notify_settings.changed["do-not-disturb"].connect (update_badge_revealed); + } + + app.notify["progress-visible"].connect (update_progress_revealer); + update_progress_revealer (); + } + + private void update_badge_revealed () { + badge_fade.skip (); + badge_scale.skip (); + + // Avoid a stutter at the beginning + badge.opacity = 0; + + if (app.count_visible && (notify_settings == null || !notify_settings.get_boolean ("do-not-disturb"))) { + badge_fade.duration = Granite.TRANSITION_DURATION_OPEN; + badge_fade.reverse = false; + + badge_scale.duration = Granite.TRANSITION_DURATION_OPEN; + badge_scale.easing = EASE_OUT_BACK; + badge_scale.reverse = false; + } else { + badge_fade.duration = Granite.TRANSITION_DURATION_CLOSE; + badge_fade.reverse = true; + + badge_scale.duration = Granite.TRANSITION_DURATION_CLOSE; + badge_scale.easing = EASE_OUT_QUAD; + badge_scale.reverse = true; + } + + badge_fade.play (); + badge_scale.play (); + } + + private void update_progress_revealer () { + progress_revealer.reveal_child = app.progress_visible; + + // See comment above and https://github.com/elementary/dock/issues/279 + if (progress_revealer.reveal_child && progress_revealer.child == null) { + var progress_bar = new Gtk.ProgressBar () { + valign = END + }; + app.bind_property ("progress", progress_bar, "fraction", SYNC_CREATE); + + progress_revealer.child = progress_bar; + } + } +} diff --git a/src/AppSystem/Launcher.vala b/src/AppSystem/Launcher.vala index e2b02411..6b4af614 100644 --- a/src/AppSystem/Launcher.vala +++ b/src/AppSystem/Launcher.vala @@ -10,14 +10,6 @@ public class Dock.Launcher : BaseItem { private const int DND_TIMEOUT = 1000; - private static Settings? notify_settings; - - static construct { - if (SettingsSchemaSource.get_default ().lookup ("io.elementary.notifications", true) != null) { - notify_settings = new Settings ("io.elementary.notifications"); - } - } - // Matches icon size and padding in Launcher.css public const int ICON_SIZE = 48; public const int PADDING = 6; @@ -25,12 +17,7 @@ public class Dock.Launcher : BaseItem { public App app { get; construct; } private Gtk.Box running_box; - private Gtk.Image image; - private Gtk.Label badge; - private Gtk.Revealer progress_revealer; private Gtk.Revealer running_revealer; - private Adw.TimedAnimation badge_fade; - private Adw.TimedAnimation badge_scale; private Adw.TimedAnimation bounce_up; private Adw.TimedAnimation bounce_down; private Adw.TimedAnimation shake; @@ -50,8 +37,6 @@ public class Dock.Launcher : BaseItem { } } - private Binding current_count_binding; - private int drag_offset_x = 0; private int drag_offset_y = 0; @@ -92,34 +77,10 @@ public class Dock.Launcher : BaseItem { tooltip_text = app.app_info.get_display_name (); - image = new Gtk.Image (); - - var icon = app.app_info.get_icon (); - if (icon != null && Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).has_gicon (icon)) { - image.gicon = icon; - } else { - image.gicon = new ThemedIcon ("application-default-icon"); - } - - badge = new Gtk.Label ("!"); - badge.add_css_class (Granite.STYLE_CLASS_BADGE); - - var badge_container = new Granite.Bin () { - can_target = false, - child = badge, - halign = END, - valign = START, - overflow = VISIBLE - }; - - progress_revealer = new Gtk.Revealer () { - can_target = false, - transition_type = CROSSFADE - }; + var app_widget = new AppWidget (app); + bind_property ("icon-size", app_widget, "icon_size", SYNC_CREATE); - overlay.child = image; - overlay.add_overlay (badge_container); - overlay.add_overlay (progress_revealer); + overlay.child = app_widget; var running_indicator = new Gtk.Image.from_icon_name ("pager-checked-symbolic"); running_indicator.add_css_class ("running-indicator"); @@ -139,14 +100,6 @@ public class Dock.Launcher : BaseItem { insert_child_after (running_revealer, bin); - // We have to destroy the progressbar when it is not needed otherwise it will - // cause continuous layouting of the surface see https://github.com/elementary/dock/issues/279 - progress_revealer.notify["child-revealed"].connect (() => { - if (!progress_revealer.child_revealed) { - progress_revealer.child = null; - } - }); - app.launched.connect (animate_launch); app.removed.connect (() => removed ()); @@ -205,33 +158,6 @@ public class Dock.Launcher : BaseItem { reverse = true }; - badge_scale = new Adw.TimedAnimation ( - badge, 0.25, 1, - Granite.TRANSITION_DURATION_OPEN, - new Adw.CallbackAnimationTarget ((val) => { - var height = badge_container.get_height (); - var width = badge_container.get_width (); - - var x = (float) (width - (val * width)) / 2; - var y = (float) (height - (val * height)) / 2; - - badge.allocate ( - width, height, -1, - new Gsk.Transform ().scale ((float) val, (float) val).translate (Graphene.Point ().init (x, y)) - ); - }) - ); - - badge_fade = new Adw.TimedAnimation ( - badge, 0, 1, - Granite.TRANSITION_DURATION_OPEN, - new Adw.CallbackAnimationTarget ((val) => { - badge.opacity = val; - }) - ) { - easing = EASE_IN_OUT_QUAD - }; - gesture_click.button = 0; gesture_click.released.connect (on_click_released); @@ -260,31 +186,6 @@ public class Dock.Launcher : BaseItem { return Gdk.EVENT_STOP; }); - bind_property ("icon-size", image, "pixel-size", SYNC_CREATE); - - app.notify["count-visible"].connect (update_badge_revealed); - update_badge_revealed (); - current_count_binding = app.bind_property ("current_count", badge, "label", SYNC_CREATE, - (binding, srcval, ref targetval) => { - var src = (int64) srcval; - - if (src > 0) { - targetval.set_string ("%lld".printf (src)); - } else { - targetval.set_string ("!"); - } - - return true; - }, null - ); - - if (notify_settings != null) { - notify_settings.changed["do-not-disturb"].connect (update_badge_revealed); - } - - app.notify["progress-visible"].connect (update_progress_revealer); - update_progress_revealer (); - app.notify["running-on-active-workspace"].connect (update_active_state); app.notify["running"].connect (update_active_state); update_active_state (); @@ -317,7 +218,6 @@ public class Dock.Launcher : BaseItem { bounce_down = null; bounce_up = null; shake = null; - current_count_binding.unbind (); remove_dnd_cycle (); } @@ -437,47 +337,6 @@ public class Dock.Launcher : BaseItem { } } - private void update_badge_revealed () { - badge_fade.skip (); - badge_scale.skip (); - - // Avoid a stutter at the beginning - badge.opacity = 0; - - if (app.count_visible && (notify_settings == null || !notify_settings.get_boolean ("do-not-disturb"))) { - badge_fade.duration = Granite.TRANSITION_DURATION_OPEN; - badge_fade.reverse = false; - - badge_scale.duration = Granite.TRANSITION_DURATION_OPEN; - badge_scale.easing = EASE_OUT_BACK; - badge_scale.reverse = false; - } else { - badge_fade.duration = Granite.TRANSITION_DURATION_CLOSE; - badge_fade.reverse = true; - - badge_scale.duration = Granite.TRANSITION_DURATION_CLOSE; - badge_scale.easing = EASE_OUT_QUAD; - badge_scale.reverse = true; - } - - badge_fade.play (); - badge_scale.play (); - } - - private void update_progress_revealer () { - progress_revealer.reveal_child = app.progress_visible; - - // See comment above and https://github.com/elementary/dock/issues/279 - if (progress_revealer.reveal_child && progress_revealer.child == null) { - var progress_bar = new Gtk.ProgressBar () { - valign = END - }; - app.bind_property ("progress", progress_bar, "fraction", SYNC_CREATE); - - progress_revealer.child = progress_bar; - } - } - private void update_active_state () { if (!app.running) { state = HIDDEN; diff --git a/src/meson.build b/src/meson.build index 1b84b18f..3936fa5b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,7 +8,9 @@ sources = [ 'MainWindow.vala', 'RenderNodeWalker.vala', 'AppSystem' / 'App.vala', + 'AppSystem' / 'AppCache.vala', 'AppSystem' / 'AppSystem.vala', + 'AppSystem' / 'AppWidget.vala', 'AppSystem' / 'Launcher.vala', 'AppSystem' / 'PoofPopover.vala', 'AppSystem' / 'Background' / 'BackgroundApp.vala',