diff --git a/data/Application.css b/data/Application.css index c21057c4..4358d01d 100644 --- a/data/Application.css +++ b/data/Application.css @@ -22,6 +22,10 @@ dock { opacity: 1; } +dock-window:not(.reduce-transparency) separator.vertical { + border-right-color: alpha(@highlight_color, 0.15); +} + dock-window { margin-top: 64px; /* Keep enough room so that icons don't clip when bouncing */ } @@ -60,6 +64,7 @@ launcher progressbar progress { min-width: 0; } +backgrounditem, icongroup { padding: 6px; padding-bottom: 0; @@ -88,6 +93,19 @@ icongroup .add-image { -gtk-icon-shadow: 0 1px 0 alpha(@highlight_color, 0.2); } +backgrounditem header { + padding: 0.5em 1em; +} + +/*Workaround for bug with headers in popover*/ +backgrounditem header .heading { + margin: 0; +} + +backgrounditem .close-button { + padding: 0.125em; +} + .reduce-transparency .add-image { color: @selected_fg_color; } diff --git a/src/AppSystem/Background/BackgroundApp.vala b/src/AppSystem/Background/BackgroundApp.vala new file mode 100644 index 00000000..969c51fe --- /dev/null +++ b/src/AppSystem/Background/BackgroundApp.vala @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * + * Authored by: Leonhard Kargl + */ + +public class Dock.BackgroundApp : Object { + public DesktopAppInfo app_info { get; construct; } + public Icon icon { get { return app_info.get_icon (); } } + public string? instance { get; construct; } + public string? message { get; construct; } + + public BackgroundApp (DesktopAppInfo app_info, string? instance, string? message) { + Object (app_info: app_info, instance: instance, message: message); + } + + public async void kill () throws Error { + var app_id = remove_desktop_suffix (app_info.get_id ()); + + try { + var object_path = "/" + app_id.replace (".", "/").replace ("-", "_"); + var parameters = new Variant ( + "(s@av@a{sv})", "quit", new Variant.array (VariantType.VARIANT, {}), + new Variant.array (new VariantType.dict_entry (VariantType.STRING, VariantType.VARIANT), {}) + ); + + var session_bus = yield Bus.get (SESSION, null); + + // DesktopAppInfo.launch_action only works for actions listed in the .desktop file + yield session_bus.call ( + app_id, object_path, "org.freedesktop.Application", + "ActivateAction", parameters, null, NONE, -1 + ); + + return; + } catch (Error e) { + debug ("Failed to quit app via action, try flatpak kill: %s", e.message); + } + + var process = new Subprocess (NONE, "flatpak", "kill", app_id); + if (!yield process.wait_check_async (null)) { + throw new IOError.FAILED ("Failed to kill app: %s", app_id); + } + } + + private string remove_desktop_suffix (string app_id) { + if (app_id.has_suffix (".desktop")) { + return app_id[0:app_id.length - ".desktop".length]; + } + + return app_id; + } +} diff --git a/src/AppSystem/Background/BackgroundAppRow.vala b/src/AppSystem/Background/BackgroundAppRow.vala new file mode 100644 index 00000000..924a9abd --- /dev/null +++ b/src/AppSystem/Background/BackgroundAppRow.vala @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * + * Authored by: Leonhard Kargl + */ + +public class Dock.BackgroundAppRow : Gtk.ListBoxRow { + public BackgroundApp app { get; construct; } + + private Gtk.Stack button_stack; + + public BackgroundAppRow (BackgroundApp app) { + Object (app: app); + } + + construct { + var icon = new Gtk.Image.from_gicon (app.app_info.get_icon ()) { + icon_size = LARGE + }; + + var name = new Gtk.Label (app.app_info.get_display_name ()) { + xalign = 0, + hexpand = true + }; + + var message = new Gtk.Label (app.message) { + xalign = 0, + hexpand = true + }; + message.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + message.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); + + var button = new Gtk.Button.from_icon_name ("window-close-symbolic") { + valign = CENTER, + tooltip_text = _("Quit"), + }; + button.add_css_class ("close-button"); + button.add_css_class (Granite.STYLE_CLASS_CIRCULAR); + button.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); + + var spinner = new Gtk.Spinner () { + spinning = true + }; + + button_stack = new Gtk.Stack () { + transition_type = CROSSFADE + }; + button_stack.add_named (button, "button"); + button_stack.add_named (spinner, "spinner"); + + var grid = new Gtk.Grid () { + column_spacing = 9, + row_spacing = 3 + }; + grid.attach (icon, 0, 0, 1, 2); + + if (app.message != null) { + grid.attach (name, 1, 0); + grid.attach (message, 1, 1); + } else { + grid.attach (name, 1, 0, 1, 2); + } + + grid.attach (button_stack, 2, 0, 1, 2); + + child = grid; + + button.clicked.connect (on_button_clicked); + } + + private async void on_button_clicked () { + button_stack.set_visible_child_name ("spinner"); + + try { + yield app.kill (); + } catch (Error e) { + button_stack.set_visible_child_name ("button"); + + var failed_notification = new GLib.Notification ( + "Failed to end app %s".printf (app.app_info.get_display_name ()) + ); + GLib.Application.get_default ().send_notification (null, failed_notification); + + return; + } + + Timeout.add_seconds (5, () => { + // Assume killing failed + button_stack.set_visible_child_name ("button"); + return Source.REMOVE; + }); + } +} diff --git a/src/AppSystem/Background/BackgroundItem.vala b/src/AppSystem/Background/BackgroundItem.vala new file mode 100644 index 00000000..e17a43ad --- /dev/null +++ b/src/AppSystem/Background/BackgroundItem.vala @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * + * Authored by: Leonhard Kargl + */ + +public class Dock.BackgroundItem : BaseIconGroup { + public signal void apps_appeared (); + + public BackgroundMonitor monitor { private get; construct; } + public bool has_apps { get { return monitor.background_apps.get_n_items () > 0; } } + + private Gtk.Popover popover; + + public BackgroundItem () { + var background_monitor = new BackgroundMonitor (); + Object ( + monitor: background_monitor, + icons: new Gtk.MapListModel (background_monitor.background_apps, (app) => { + return ((BackgroundApp) app).icon; + }), + disallow_dnd: true + ); + } + + construct { + var list_box = new Gtk.ListBox () { + selection_mode = BROWSE + }; + list_box.bind_model (monitor.background_apps, create_widget_func); + + var header_label = new Granite.HeaderLabel (_("Background Apps")) { + mnemonic_widget = list_box, + secondary_text = _("Apps running without a visible window.") + }; + + var box = new Gtk.Box (VERTICAL, 0); + box.append (header_label); + box.append (new Gtk.Separator (HORIZONTAL)); + box.append (list_box); + + popover = new Gtk.Popover () { + position = TOP, + child = box + }; + popover.add_css_class (Granite.STYLE_CLASS_MENU); + popover.set_parent (this); + + monitor.background_apps.items_changed.connect ((pos, n_removed, n_added) => { + if (monitor.background_apps.get_n_items () == 0) { + popover.popdown (); + removed (); + } else if (n_removed == 0 && n_added != 0 && n_added == monitor.background_apps.get_n_items ()) { + apps_appeared (); + } + }); + + gesture_click.released.connect (popover.popup); + } + + private Gtk.Widget create_widget_func (Object obj) { + var app = (BackgroundApp) obj; + return new BackgroundAppRow (app); + } + + public void load () { + monitor.load (); + } + + public override void cleanup () { + // Do nothing here since we reuse this item + } +} diff --git a/src/AppSystem/Background/BackgroundMonitor.vala b/src/AppSystem/Background/BackgroundMonitor.vala new file mode 100644 index 00000000..ca2faeca --- /dev/null +++ b/src/AppSystem/Background/BackgroundMonitor.vala @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * + * Authored by: Leonhard Kargl + */ + +[DBus (name = "org.freedesktop.background.Monitor")] +public interface Freedesktop.BackgroundMonitor : DBusProxy { + public abstract HashTable[] background_apps { owned get; } +} + +public class Dock.BackgroundMonitor : Object { + public ListStore background_apps { get; construct; } + + private Freedesktop.BackgroundMonitor? proxy; + + construct { + background_apps = new ListStore (typeof (BackgroundApp)); + } + + public void load () { + Bus.watch_name (SESSION, "org.freedesktop.background.Monitor", NONE, () => on_name_appeared.begin (), () => proxy = null); + } + + private async void on_name_appeared () { + try { + proxy = yield Bus.get_proxy ( + SESSION, + "org.freedesktop.background.Monitor", + "/org/freedesktop/background/monitor", + GET_INVALIDATED_PROPERTIES + ); + proxy.g_properties_changed.connect (update_background_apps); + + update_background_apps (); + } catch (Error e) { + warning ("Failed to get background monitor proxy: %s", e.message); + } + } + + private void update_background_apps () { + BackgroundApp[] apps = {}; + + foreach (var table in proxy.background_apps) { + DesktopAppInfo? app_info = null; + if ("app_id" in table) { + app_info = new DesktopAppInfo ((string) table["app_id"] + ".desktop"); + } + + if (app_info == null) { + continue; + } + + string? instance = null; + if ("instance" in table) { + instance = (string) table["instance"]; + } + + string? message = null; + if ("message" in table) { + message = (string) table["message"]; + } + + apps += new BackgroundApp (app_info, instance, message); + } + + background_apps.splice (0, background_apps.n_items, apps); + } +} diff --git a/src/BaseIconGroup.vala b/src/BaseIconGroup.vala new file mode 100644 index 00000000..690503bb --- /dev/null +++ b/src/BaseIconGroup.vala @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public abstract class Dock.BaseIconGroup : BaseItem { + private class EmptyWidget : Gtk.Widget {} + + private const int MAX_IN_ROW = 2; + private const int MAX_N_CHILDREN = MAX_IN_ROW * MAX_IN_ROW; + + public ListModel icons { get; construct; } + + private Gtk.Grid grid; + + class construct { + set_css_name ("icongroup"); + } + + construct { + grid = new Gtk.Grid () { + hexpand = true, + vexpand = true, + halign = CENTER, + valign = CENTER + }; + + var box = new Gtk.Box (VERTICAL, 0); + box.add_css_class ("icon-group-box"); + box.append (grid); + + overlay.child = box; + + update_icons (); + icons.items_changed.connect (update_icons); + notify["icon-size"].connect (update_icons); + + bind_property ("icon-size", box, "width-request", SYNC_CREATE); + bind_property ("icon-size", box, "height-request", SYNC_CREATE); + } + + private void update_icons () { + unowned Gtk.Widget? child; + while ((child = grid.get_first_child ()) != null) { + grid.remove (child); + } + + var grid_spacing = get_grid_spacing (); + grid.row_spacing = grid_spacing; + grid.column_spacing = grid_spacing; + + var new_pixel_size = get_pixel_size (); + int i; + for (i = 0; i < uint.min (icons.get_n_items (), 4); i++) { + var image = new Gtk.Image.from_gicon ((Icon) icons.get_item (i)) { + pixel_size = new_pixel_size + }; + + grid.attach (image, i % MAX_IN_ROW, i / MAX_IN_ROW, 1, 1); + } + + // We always need to attach at least 3 elements for grid to be square and properly aligned + for (; i < 3; i++) { + var empty_widget = new EmptyWidget (); + empty_widget.set_size_request (new_pixel_size, new_pixel_size); + + grid.attach (empty_widget, i % MAX_IN_ROW, i / MAX_IN_ROW, 1, 1); + } + } + + private int get_pixel_size () { + var pixel_size = 8; + + switch (icon_size) { + case 64: + pixel_size = 24; + break; + case 48: + pixel_size = 16; + break; + case 32: + pixel_size = 8; + break; + default: + pixel_size = (int) Math.round (icon_size / 3); + break; + } + + return pixel_size; + } + + private int get_grid_spacing () { + var pixel_size = get_pixel_size (); + + return (int) Math.round ((icon_size - pixel_size * MAX_IN_ROW) / 3); + } +} diff --git a/src/BaseItem.vala b/src/BaseItem.vala index 07b010ce..68fc4c8f 100644 --- a/src/BaseItem.vala +++ b/src/BaseItem.vala @@ -77,7 +77,7 @@ public class Dock.BaseItem : Gtk.Box { private int drag_offset_x = 0; private int drag_offset_y = 0; - private BaseItem () {} + protected BaseItem () {} construct { orientation = VERTICAL; @@ -135,9 +135,8 @@ public class Dock.BaseItem : Gtk.Box { reveal.done.connect (set_revealed_finish); - unowned var item_manager = ItemManager.get_default (); var animation_target = new Adw.CallbackAnimationTarget ((val) => { - item_manager.move (this, val, 0); + ItemManager.get_default ().move (this, val, 0); current_pos = val; }); @@ -184,15 +183,18 @@ public class Dock.BaseItem : Gtk.Box { // clip launcher to dock size until we finish animating overflow = HIDDEN; + fade.reverse = reveal.reverse = !revealed; + if (revealed) { + fade.duration = Granite.TRANSITION_DURATION_OPEN; + + reveal.duration = Granite.TRANSITION_DURATION_OPEN; reveal.easing = EASE_OUT_BACK; } else { fade.duration = Granite.TRANSITION_DURATION_CLOSE; - fade.reverse = true; reveal.duration = Granite.TRANSITION_DURATION_CLOSE; reveal.easing = EASE_IN_OUT_QUAD; - reveal.reverse = true; } fade.play (); diff --git a/src/ItemManager.vala b/src/ItemManager.vala index 336d650e..3233e6f1 100644 --- a/src/ItemManager.vala +++ b/src/ItemManager.vala @@ -15,23 +15,30 @@ private Adw.TimedAnimation resize_animation; private GLib.GenericArray launchers; // Only used to keep track of launcher indices - private GLib.GenericArray icon_groups; // Only used to keep track of icon group indices + private BackgroundItem background_item; + private GLib.GenericArray icon_groups; // Only used to keep track of icon group indices private DynamicWorkspaceIcon dynamic_workspace_item; + private Gtk.Separator separator; + static construct { settings = new Settings ("io.elementary.dock"); } construct { launchers = new GLib.GenericArray (); - icon_groups = new GLib.GenericArray (); + + background_item = new BackgroundItem (); + background_item.apps_appeared.connect (add_item); + + icon_groups = new GLib.GenericArray (); #if WORKSPACE_SWITCHER - // Idle is used here to because DynamicWorkspaceIcon depends on ItemManager - Idle.add_once (() => { - dynamic_workspace_item = new DynamicWorkspaceIcon (); - add_item (dynamic_workspace_item); - }); + dynamic_workspace_item = new DynamicWorkspaceIcon (); + + separator = new Gtk.Separator (VERTICAL); + settings.bind ("icon-size", separator, "height-request", GET); + put (separator, 0, 0); #endif overflow = VISIBLE; @@ -157,12 +164,13 @@ #if WORKSPACE_SWITCHER WorkspaceSystem.get_default ().workspace_added.connect ((workspace) => { - add_item (new IconGroup (workspace)); + add_item (new WorkspaceIconGroup (workspace)); }); #endif map.connect (() => { AppSystem.get_default ().load.begin (); + background_item.load (); #if WORKSPACE_SWITCHER WorkspaceSystem.get_default ().load.begin (); #endif @@ -175,6 +183,15 @@ position_item (launcher, ref index); } + if (background_item.has_apps) { + position_item (background_item, ref index); + } + +#if WORKSPACE_SWITCHER + var separator_y = (get_launcher_size () - separator.height_request) / 2; + move (separator, index * get_launcher_size () - 1, separator_y); +#endif + foreach (var icon_group in icon_groups) { position_item (icon_group, ref index); } @@ -210,8 +227,8 @@ if (item is Launcher) { launchers.add ((Launcher) item); - } else if (item is IconGroup) { - icon_groups.add ((IconGroup) item); + } else if (item is WorkspaceIconGroup) { + icon_groups.add ((WorkspaceIconGroup) item); } ulong reveal_cb = 0; @@ -231,10 +248,11 @@ private void remove_item (BaseItem item) { if (item is Launcher) { launchers.remove ((Launcher) item); - } else if (item is IconGroup) { - icon_groups.remove ((IconGroup) item); + } else if (item is WorkspaceIconGroup) { + icon_groups.remove ((WorkspaceIconGroup) item); } + item.removed.disconnect (remove_item); item.revealed_done.connect (remove_finish); item.set_revealed (false); } @@ -252,6 +270,7 @@ resize_animation.value_to = launchers.length * get_launcher_size (); resize_animation.play (); + item.revealed_done.disconnect (remove_finish); item.cleanup (); } @@ -260,9 +279,9 @@ double offset = 0; if (source is Launcher) { list = launchers; - } else if (source is IconGroup) { + } else if (source is WorkspaceIconGroup) { list = icon_groups; - offset = launchers.length * get_launcher_size (); + offset = (launchers.length + (background_item.has_apps ? 1 : 0)) * get_launcher_size (); // +1 for the background item } else { warning ("Tried to move neither launcher nor icon group"); return; @@ -298,9 +317,9 @@ } return 0; - } else if (item is IconGroup) { + } else if (item is WorkspaceIconGroup) { uint index; - if (icon_groups.find ((IconGroup) item, out index)) { + if (icon_groups.find ((WorkspaceIconGroup) item, out index)) { return (int) index; } diff --git a/src/WorkspaceSystem/IconGroup.vala b/src/WorkspaceSystem/IconGroup.vala index f99ab8b9..4501b4c1 100644 --- a/src/WorkspaceSystem/IconGroup.vala +++ b/src/WorkspaceSystem/IconGroup.vala @@ -3,51 +3,26 @@ * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ -public class Dock.IconGroup : BaseItem { - private class EmptyWidget : Gtk.Widget {} - - private const int MAX_IN_ROW = 2; - private const int MAX_N_CHILDREN = MAX_IN_ROW * MAX_IN_ROW; - +public class Dock.WorkspaceIconGroup : BaseIconGroup { public Workspace workspace { get; construct; } - private Gtk.Grid grid; - - class construct { - set_css_name ("icongroup"); - } - - public IconGroup (Workspace workspace) { - Object (workspace: workspace, group: Group.WORKSPACE); + public WorkspaceIconGroup (Workspace workspace) { + Object ( + workspace: workspace, + icons: new Gtk.MapListModel (workspace.windows, (window) => { + return ((Window) window).icon; + }), + group: Group.WORKSPACE + ); } construct { - grid = new Gtk.Grid () { - hexpand = true, - vexpand = true, - halign = CENTER, - valign = CENTER - }; - - var box = new Gtk.Box (VERTICAL, 0); - box.add_css_class ("icon-group-box"); - box.append (grid); - - overlay.child = box; - workspace.bind_property ("is-active-workspace", this, "state", SYNC_CREATE, (binding, from_value, ref to_value) => { var new_val = from_value.get_boolean () ? State.ACTIVE : State.HIDDEN; to_value.set_enum (new_val); return true; }); - update_icons (); - workspace.notify["windows"].connect (update_icons); - notify["icon-size"].connect (update_icons); - - bind_property ("icon-size", box, "width-request", SYNC_CREATE); - bind_property ("icon-size", box, "height-request", SYNC_CREATE); - workspace.removed.connect (() => removed ()); gesture_click.button = Gdk.BUTTON_PRIMARY; @@ -56,72 +31,9 @@ public class Dock.IconGroup : BaseItem { notify["moving"].connect (on_moving_changed); } - private void update_icons () { - unowned Gtk.Widget? child; - while ((child = grid.get_first_child ()) != null) { - grid.remove (child); - } - - var grid_spacing = get_grid_spacing (); - grid.row_spacing = grid_spacing; - grid.column_spacing = grid_spacing; - - var new_pixel_size = get_pixel_size (); - int i; - for (i = 0; i < int.min (workspace.windows.length, 4); i++) { - var image = new Gtk.Image.from_gicon (workspace.windows[i].icon) { - pixel_size = new_pixel_size - }; - - grid.attach (image, i % MAX_IN_ROW, i / MAX_IN_ROW, 1, 1); - } - - // We always need to attach at least 3 elements for grid to be square and properly aligned - for (; i < 3; i++) { - var empty_widget = new EmptyWidget (); - empty_widget.set_size_request (new_pixel_size, new_pixel_size); - - grid.attach (empty_widget, i % MAX_IN_ROW, i / MAX_IN_ROW, 1, 1); - } - } - - private int get_pixel_size () { - var pixel_size = 8; - - switch (icon_size) { - case 64: - pixel_size = 24; - break; - case 48: - pixel_size = 16; - break; - case 32: - pixel_size = 8; - break; - default: - pixel_size = (int) Math.round (icon_size / 3); - break; - } - - return pixel_size; - } - - private int get_grid_spacing () { - var pixel_size = get_pixel_size (); - - return (int) Math.round ((icon_size - pixel_size * MAX_IN_ROW) / 3); - } - private void on_moving_changed () { if (!moving) { workspace.reorder (ItemManager.get_default ().get_index_for_launcher (this)); } } - - /** - * {@inheritDoc} - */ - public override void cleanup () { - base.cleanup (); - } } diff --git a/src/WorkspaceSystem/Workspace.vala b/src/WorkspaceSystem/Workspace.vala index cf34ab8d..a180f32b 100644 --- a/src/WorkspaceSystem/Workspace.vala +++ b/src/WorkspaceSystem/Workspace.vala @@ -7,12 +7,17 @@ public class Dock.Workspace : GLib.Object { public signal void reordered (int new_index); public signal void removed (); - public GLib.GenericArray windows { get; owned set; } + private ListStore store; + public ListModel windows { get { return store; } } public int index { get; set; } public bool is_active_workspace { get; private set; } construct { - windows = new GLib.GenericArray (); + store = new ListStore (typeof (Window)); + } + + public void update_windows (GLib.GenericArray new_windows) { + store.splice (0, store.get_n_items (), new_windows.data); } public void remove () { diff --git a/src/WorkspaceSystem/WorkspaceSystem.vala b/src/WorkspaceSystem/WorkspaceSystem.vala index 9f2b6267..420b7f83 100644 --- a/src/WorkspaceSystem/WorkspaceSystem.vala +++ b/src/WorkspaceSystem/WorkspaceSystem.vala @@ -67,7 +67,7 @@ public class Dock.WorkspaceSystem : Object { workspace_window_list[i].sort (compare_func); - workspace.windows = workspace_window_list[i]; + workspace.update_windows (workspace_window_list[i]); workspace.index = i; workspace.update_active_workspace (); } diff --git a/src/meson.build b/src/meson.build index 34b5ca58..3bc51214 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,6 @@ sources = [ 'Application.vala', + 'BaseIconGroup.vala', 'BaseItem.vala', 'ItemManager.vala', 'MainWindow.vala', @@ -7,6 +8,10 @@ sources = [ 'AppSystem' / 'AppSystem.vala', 'AppSystem' / 'Launcher.vala', 'AppSystem' / 'PoofPopover.vala', + 'AppSystem' / 'Background' / 'BackgroundApp.vala', + 'AppSystem' / 'Background' / 'BackgroundAppRow.vala', + 'AppSystem' / 'Background' / 'BackgroundItem.vala', + 'AppSystem' / 'Background' / 'BackgroundMonitor.vala', 'DBus' / 'GalaDBus.vala', 'DBus' / 'ItemInterface.vala', 'DBus' / 'ShellKeyGrabber.vala',