diff --git a/data/Application.css b/data/Application.css index 1387b2b4..66717447 100644 --- a/data/Application.css +++ b/data/Application.css @@ -60,6 +60,38 @@ launcher progressbar progress { min-width: 0; } +icongroup { + padding: 6px; + padding-bottom: 0; +} + +icongroup box { + background: alpha(@base_color, 0.4); + box-shadow: + inset 0 -1px 0 0 alpha(@highlight_color, 0.2), + inset 0 1px 0 0 alpha(@highlight_color, 0.3), + inset 1px 0 0 0 alpha(@highlight_color, 0.07), + inset -1px 0 0 0 alpha(@highlight_color, 0.07), + 0 0 0 1px alpha(@borders, 0.3), + 0 1px 1px alpha(@borders, 0.2), + 0 1px 4px alpha(@borders, 0.4); + border-radius: 6px; + border-spacing: 3px; +} + +.reduce-transparency icongroup box { + background: @base_color; +} + +icongroup .add-image { + color: alpha(@selected_fg_color, 0.75); + -gtk-icon-shadow: 0 1px 0 alpha(@highlight_color, 0.2); +} + +.reduce-transparency .add-image { + color: @selected_fg_color; +} + .running-indicator { color: @selected_fg_color; -gtk-icon-size: 9px; diff --git a/po/POTFILES b/po/POTFILES index 10fbdaf0..76ad6156 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -13,3 +13,7 @@ src/DBus/Unity.vala src/WindowSystem/DesktopIntegration.vala src/WindowSystem/Window.vala src/WindowSystem/WindowSystem.vala +src/WorkspaceSystem/DynamicWorkspaceItem.vala +src/WorkspaceSystem/IconGroup.vala +src/WorkspaceSystem/Workspace.vala +src/WorkspaceSystem/WorkspaceSystem.vala diff --git a/src/ItemManager.vala b/src/ItemManager.vala index 7d6e6ee7..3910ad91 100644 --- a/src/ItemManager.vala +++ b/src/ItemManager.vala @@ -15,6 +15,8 @@ private Adw.TimedAnimation resize_animation; private List launchers; // Only used to keep track of launcher indices + private List icon_groups; // Only used to keep track of icon group indices + private DynamicWorkspaceIcon dynamic_workspace_item; static construct { settings = new Settings ("io.elementary.dock"); @@ -22,6 +24,13 @@ construct { launchers = new List (); + icon_groups = new List (); + + // Idle is used here to because DynamicWorkspaceIcon depends on ItemManager + Idle.add_once (() => { + dynamic_workspace_item = new DynamicWorkspaceIcon (); + add_item (dynamic_workspace_item); + }); overflow = VISIBLE; @@ -110,25 +119,40 @@ add_item (launcher); }); - map.connect (AppSystem.get_default ().load); + WorkspaceSystem.get_default ().workspace_added.connect ((workspace) => { + add_item (new IconGroup (workspace)); + }); + + map.connect (() => { + AppSystem.get_default ().load.begin (); + WorkspaceSystem.get_default ().load.begin (); + }); } private void reposition_items () { - var launcher_size = get_launcher_size (); - int index = 0; foreach (var launcher in launchers) { - var position = index * launcher_size; + position_item (launcher, ref index); + } - if (launcher.parent != this) { - put (launcher, position, 0); - launcher.current_pos = position; - } else { - launcher.animate_move (position); - } + foreach (var icon_group in icon_groups) { + position_item (icon_group, ref index); + } + + position_item (dynamic_workspace_item, ref index); + } + + private void position_item (BaseItem item, ref int index) { + var position = get_launcher_size () * index; - index++; + if (item.parent != this) { + put (item, position, 0); + item.current_pos = position; + } else { + item.animate_move (position); } + + index++; } private void add_launcher_via_dnd (Launcher launcher, int index) { @@ -144,6 +168,8 @@ if (item is Launcher) { launchers.append ((Launcher) item); + } else if (item is IconGroup) { + icon_groups.append ((IconGroup) item); } resize_animation.easing = EASE_OUT_BACK; @@ -163,6 +189,8 @@ private void remove_item (BaseItem item) { if (item is Launcher) { launchers.remove ((Launcher) item); + } else if (item is IconGroup) { + icon_groups.remove ((IconGroup) item); } item.set_revealed (false); diff --git a/src/WindowSystem/DesktopIntegration.vala b/src/WindowSystem/DesktopIntegration.vala index f39465c1..1ca833c3 100644 --- a/src/WindowSystem/DesktopIntegration.vala +++ b/src/WindowSystem/DesktopIntegration.vala @@ -20,9 +20,14 @@ public interface Dock.DesktopIntegration : GLib.Object { public signal void running_applications_changed (); public signal void windows_changed (); + public signal void active_workspace_changed (); + public signal void workspace_removed (int index); public abstract async RunningApplication[] get_running_applications () throws GLib.DBusError, GLib.IOError; public abstract async Window[] get_windows () throws GLib.DBusError, GLib.IOError; public abstract async void show_windows_for (string app_id) throws GLib.DBusError, GLib.IOError; public abstract async void focus_window (uint64 uid) throws GLib.DBusError, GLib.IOError; + public abstract async void activate_workspace (int index) throws GLib.DBusError, GLib.IOError; + public abstract async int get_n_workspaces () throws GLib.DBusError, GLib.IOError; + public abstract async int get_active_workspace () throws GLib.DBusError, GLib.IOError; } diff --git a/src/WindowSystem/Window.vala b/src/WindowSystem/Window.vala index ffe9d8a4..78b08872 100644 --- a/src/WindowSystem/Window.vala +++ b/src/WindowSystem/Window.vala @@ -9,8 +9,11 @@ public class Dock.Window : GLib.Object { public string app_id { get; private set; default = ""; } public bool has_focus { get; private set; default = false; } + public int workspace_index { get; private set; default = 0; } public bool on_active_workspace { get; private set; default = false; } + public GLib.Icon icon { get; private set; default = new GLib.ThemedIcon ("application-default-icon"); } + public Window (uint64 uid) { Object (uid: uid); } @@ -24,8 +27,20 @@ public class Dock.Window : GLib.Object { has_focus = (bool) properties["has-focus"]; } + if ("workspace-index" in properties) { + workspace_index = (int) properties["workspace-index"]; + } + if ("on-active-workspace" in properties) { on_active_workspace = (bool) properties["on-active-workspace"]; } + + var app_info = new GLib.DesktopAppInfo (app_id); + if (app_info != null) { + var icon = app_info.get_icon (); + if (icon != null && Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).has_gicon (icon)) { + this.icon = icon; + } + } } } diff --git a/src/WindowSystem/WindowSystem.vala b/src/WindowSystem/WindowSystem.vala index ce4013f2..f8fe750a 100644 --- a/src/WindowSystem/WindowSystem.vala +++ b/src/WindowSystem/WindowSystem.vala @@ -9,8 +9,11 @@ return instance.once (() => { return new WindowSystem (); }); } + public signal void workspace_removed (int index); + public DesktopIntegration? desktop_integration { get; private set; } public Gee.List windows { get; private owned set; } + public int active_workspace { get; private set; default = 0; } private WindowSystem () {} @@ -28,8 +31,11 @@ ); yield sync_windows (); + yield sync_active_workspace (); desktop_integration.windows_changed.connect (sync_windows); + desktop_integration.active_workspace_changed.connect (sync_active_workspace); + desktop_integration.workspace_removed.connect ((index) => workspace_removed (index)); } catch (Error e) { critical ("Failed to get desktop integration: %s", e.message); } @@ -64,4 +70,12 @@ windows = new_windows; } + + private async void sync_active_workspace () requires (desktop_integration != null) { + try { + active_workspace = yield desktop_integration.get_active_workspace (); + } catch (Error e) { + critical (e.message); + } + } } diff --git a/src/WorkspaceSystem/DynamicWorkspaceItem.vala b/src/WorkspaceSystem/DynamicWorkspaceItem.vala new file mode 100644 index 00000000..63982dbe --- /dev/null +++ b/src/WorkspaceSystem/DynamicWorkspaceItem.vala @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Dock.DynamicWorkspaceIcon : BaseItem { + class construct { + set_css_name ("icongroup"); + } + + public DynamicWorkspaceIcon () { + Object (); + } + + construct { + var add_image = new Gtk.Image.from_icon_name ("list-add-symbolic") { + hexpand = true, + vexpand = true + }; + add_image.add_css_class ("add-image"); + + // Gtk.Box is used here to keep css nodes consistent with IconGroup + var box = new Gtk.Box (VERTICAL, 0); + box.append (add_image); + + overlay.child = box; + + WorkspaceSystem.get_default ().workspace_added.connect (update_running_indicator_visibility); + WorkspaceSystem.get_default ().workspace_removed.connect (update_running_indicator_visibility); + WindowSystem.get_default ().notify["active-workspace"].connect (update_running_indicator_visibility); + + dock_settings.bind ("icon-size", box, "width-request", DEFAULT); + dock_settings.bind ("icon-size", box, "height-request", DEFAULT); + + dock_settings.bind_with_mapping ( + "icon-size", add_image, "pixel_size", DEFAULT | GET, + (value, variant, user_data) => { + var icon_size = variant.get_int32 (); + value.set_int (icon_size / 2); + return true; + }, + (value, expected_type, user_data) => { + return new Variant.maybe (null, null); + }, + null, null + ); + + gesture_click.button = Gdk.BUTTON_PRIMARY; + gesture_click.released.connect (switch_to_new_workspace); + } + + private void update_running_indicator_visibility () { + unowned var workspace_system = WorkspaceSystem.get_default (); + unowned var window_system = WindowSystem.get_default (); + running_revealer.reveal_child = workspace_system.workspaces.size == window_system.active_workspace; + } + + private async void switch_to_new_workspace () { + var n_workspaces = WorkspaceSystem.get_default ().workspaces.size; + + try { + yield WindowSystem.get_default ().desktop_integration.activate_workspace (n_workspaces); + } catch (Error e) { + warning ("Couldn't switch to new workspace: %s", e.message); + } + } +} diff --git a/src/WorkspaceSystem/IconGroup.vala b/src/WorkspaceSystem/IconGroup.vala new file mode 100644 index 00000000..914b158a --- /dev/null +++ b/src/WorkspaceSystem/IconGroup.vala @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * 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 Workspace workspace { get; construct; } + + private Gtk.Grid grid; + + class construct { + set_css_name ("icongroup"); + } + + public IconGroup (Workspace workspace) { + Object (workspace: workspace); + } + + construct { + grid = new Gtk.Grid () { + hexpand = true, + vexpand = true, + halign = CENTER, + valign = CENTER + }; + + var box = new Gtk.Box (VERTICAL, 0); + box.append (grid); + + overlay.child = box; + + workspace.bind_property ("is-active-workspace", running_revealer, "reveal-child", SYNC_CREATE); + + 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; + gesture_click.released.connect (workspace.activate); + } + + 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.size, 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); + } + + /** + * {@inheritDoc} + */ + public override void cleanup () { + base.cleanup (); + } +} diff --git a/src/WorkspaceSystem/Workspace.vala b/src/WorkspaceSystem/Workspace.vala new file mode 100644 index 00000000..7aace7a2 --- /dev/null +++ b/src/WorkspaceSystem/Workspace.vala @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Dock.Workspace : GLib.Object { + public signal void removed (); + + public Gee.List windows { get; owned set; } + public int index { get; set; } + public bool is_active_workspace { get; private set; } + + construct { + windows = new Gee.LinkedList (); + } + + public void remove () { + removed (); + } + + public void update_active_workspace () { + is_active_workspace = index == WindowSystem.get_default ().active_workspace; + } + + public void activate () { + WindowSystem.get_default ().desktop_integration.activate_workspace.begin (index); + } +} diff --git a/src/WorkspaceSystem/WorkspaceSystem.vala b/src/WorkspaceSystem/WorkspaceSystem.vala new file mode 100644 index 00000000..9c66a78b --- /dev/null +++ b/src/WorkspaceSystem/WorkspaceSystem.vala @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + */ + +public class Dock.WorkspaceSystem : Object { + private static GLib.Once instance; + public static unowned WorkspaceSystem get_default () { + return instance.once (() => { return new WorkspaceSystem (); }); + } + + public signal void workspace_added (Workspace workspace); + public signal void workspace_removed (); + + public Gee.List workspaces { get; private owned set; } + + private WorkspaceSystem () { } + + construct { + workspaces = new Gee.ArrayList (); + } + + public async void load () { + yield sync_windows (); + + WindowSystem.get_default ().notify["windows"].connect (sync_windows); + WindowSystem.get_default ().notify["active-workspace"].connect (sync_active_workspace); + WindowSystem.get_default ().workspace_removed.connect (remove_workspace); + } + + private Workspace add_workspace () { + var workspace = new Workspace (); + workspaces.add (workspace); + workspace_added (workspace); + return workspace; + } + + private async void sync_windows () { + // We subtract 1 because we have separate button for dynamic workspace + var n_workspaces = (yield get_n_workspaces ()) - 1; + + var workspace_window_list = new Gee.ArrayList> (); + for (var i = 0; i < n_workspaces; i++) { + workspace_window_list.add (new Gee.LinkedList ()); + } + + foreach (var window in WindowSystem.get_default ().windows) { + var workspace_index = window.workspace_index; + + if (workspace_index < 0 || workspace_index >= n_workspaces) { + warning ("WorkspaceSystem.sync_windows: Unexpected window workspace index: %d", workspace_index); + continue; + } + + workspace_window_list[workspace_index].add (window); + } + + // update windows in existing workspaces + for (var i = 0; i < n_workspaces; i++) { + Workspace workspace; + if (i < workspaces.size) { + workspace = workspaces[i]; + } else { + workspace = add_workspace (); + } + + workspace.windows = workspace_window_list[i]; + workspace.index = i; + workspace.update_active_workspace (); + } + } + + private async void sync_active_workspace () { + foreach (var workspace in workspaces) { + workspace.update_active_workspace (); + } + } + + private async void remove_workspace (int index) { + if (index == (yield get_n_workspaces ())) { + index--; + } + + workspaces[index].remove (); + workspaces.remove_at (index); + workspace_removed (); + } + + private async int get_n_workspaces () { + if (WindowSystem.get_default ().desktop_integration == null) { + critical ("DesktopIntegration is null"); + return 0; + } + + try { + return yield WindowSystem.get_default ().desktop_integration.get_n_workspaces (); + } catch (Error e) { + critical (e.message); + return 0; + } + } +} diff --git a/src/meson.build b/src/meson.build index 83240a98..125b77a0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -13,7 +13,11 @@ sources = [ 'DBus' / 'Unity.vala', 'WindowSystem' / 'DesktopIntegration.vala', 'WindowSystem' / 'Window.vala', - 'WindowSystem' / 'WindowSystem.vala' + 'WindowSystem' / 'WindowSystem.vala', + 'WorkspaceSystem' / 'DynamicWorkspaceItem.vala', + 'WorkspaceSystem' / 'IconGroup.vala', + 'WorkspaceSystem' / 'Workspace.vala', + 'WorkspaceSystem' / 'WorkspaceSystem.vala' ] executable(