diff --git a/meson.build b/meson.build index 3fb519173..5f264d007 100644 --- a/meson.build +++ b/meson.build @@ -148,6 +148,10 @@ if get_option('systemd') vala_flags += ['--define', 'WITH_SYSTEMD'] endif +if get_option('old-icon-groups') + vala_flags += ['--define', 'OLD_ICON_GROUPS'] +endif + if vala.version().version_compare('>= 0.56.17') vala_flags += ['--define', 'VALA_0_56_17'] endif diff --git a/meson_options.txt b/meson_options.txt index 54b3ac94c..b75854d90 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ option ('documentation', type : 'boolean', value : false) option ('systemd', type : 'boolean', value : true) option ('systemduserunitdir', type : 'string', value : '') +option ('old-icon-groups', type : 'boolean', value : false) diff --git a/src/ShellClients/PanelWindow.vala b/src/ShellClients/PanelWindow.vala index 3153d5013..bfb35c90b 100644 --- a/src/ShellClients/PanelWindow.vala +++ b/src/ShellClients/PanelWindow.vala @@ -118,8 +118,10 @@ public class Gala.PanelWindow : ShellWindow, RootTarget { } public void request_visible_in_multitasking_view () { +#if !OLD_ICON_GROUPS visible_in_multitasking_view = true; actor.add_action (new DragDropAction (DESTINATION, "multitaskingview-window")); +#endif } public void animate_start () { diff --git a/src/Widgets/MultitaskingView/IconGroups/IconGroup.vala b/src/Widgets/MultitaskingView/IconGroups/IconGroup.vala new file mode 100644 index 000000000..8c7874485 --- /dev/null +++ b/src/Widgets/MultitaskingView/IconGroups/IconGroup.vala @@ -0,0 +1,471 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2014 Tom Beckmann + * 2025 elementary, Inc. (https://elementary.io) + */ + +/** + * Container for WindowIconActors which takes care of the scaling and positioning. + * It also decides whether to draw the container shape, a plus sign or an ellipsis. + * Lastly it also includes the drawing code for the active highlight. + */ +public class Gala.IconGroup : CanvasActor { + public const int SIZE = 64; + + private const int PLUS_SIZE = 6; + private const int PLUS_WIDTH = 26; + private const int BACKDROP_ABSOLUTE_OPACITY = 40; + + /** + * The group has been clicked. The MultitaskingView should consider activating + * its workspace. + */ + public signal void selected (); + + private float _backdrop_opacity = 0.0f; + /** + * The opacity of the backdrop/highlight. + */ + public float backdrop_opacity { + get { + return _backdrop_opacity; + } + set { + _backdrop_opacity = value; + queue_redraw (); + } + } + + private DragDropAction drag_action; + + public Meta.Display display { get; construct; } + public Meta.Workspace workspace { get; construct; } + private float _scale_factor = 1.0f; + public float scale_factor { + get { return _scale_factor; } + set { + if (value != _scale_factor) { + _scale_factor = value; + resize (); + } + } + } + + private Clutter.Actor? prev_parent = null; + private Clutter.Actor icon_container; + private GLib.List windows_list = new GLib.List (); + + public IconGroup (Meta.Display display, Meta.Workspace workspace, float scale) { + Object (display: display, workspace: workspace, scale_factor: scale); + } + + construct { + reactive = true; + + drag_action = new DragDropAction (DragDropActionType.SOURCE | DragDropActionType.DESTINATION, "multitaskingview-window"); + drag_action.actor_clicked.connect (() => selected ()); + drag_action.drag_begin.connect (drag_begin); + drag_action.drag_end.connect (drag_end); + drag_action.drag_canceled.connect (drag_canceled); + drag_action.notify["dragging"].connect (redraw); + add_action (drag_action); + + icon_container = new Clutter.Actor (); + icon_container.width = width; + icon_container.height = height; + + add_child (icon_container); + + resize (); +#if HAS_MUTTER46 + icon_container.child_removed.connect_after (redraw); +#else + icon_container.actor_removed.connect_after (redraw); +#endif + } + + ~IconGroup () { +#if HAS_MUTTER46 + icon_container.child_removed.disconnect (redraw); +#else + icon_container.actor_removed.disconnect (redraw); +#endif + } + + private void resize () { + var size = Utils.scale_to_int (SIZE, scale_factor); + + width = size; + height = size; + } + + /** + * Override the paint handler to draw our backdrop if necessary + */ + public override void paint (Clutter.PaintContext context) { + if (backdrop_opacity == 0.0 || drag_action.dragging) { + base.paint (context); + return; + } + + var width = Utils.scale_to_int (100, scale_factor); + var x = (Utils.scale_to_int (SIZE, scale_factor) - width) / 2; + var y = -10; + var height = Utils.scale_to_int (WorkspaceClone.BOTTOM_OFFSET, scale_factor); + var backdrop_opacity_int = (uint8) (BACKDROP_ABSOLUTE_OPACITY * backdrop_opacity); + +#if HAS_MUTTER47 +//FIXME: TODO! +#else + Cogl.VertexP2T2C4 vertices[4]; + vertices[0] = { x, y + height, 0, 1, backdrop_opacity_int, backdrop_opacity_int, backdrop_opacity_int, backdrop_opacity_int }; + vertices[1] = { x, y, 0, 0, 0, 0, 0, 0 }; + vertices[2] = { x + width, y + height, 1, 1, backdrop_opacity_int, backdrop_opacity_int, backdrop_opacity_int, backdrop_opacity_int }; + vertices[3] = { x + width, y, 1, 0, 0, 0, 0, 0 }; + + var primitive = new Cogl.Primitive.p2t2c4 (context.get_framebuffer ().get_context (), Cogl.VerticesMode.TRIANGLE_STRIP, vertices); + var pipeline = new Cogl.Pipeline (context.get_framebuffer ().get_context ()); + primitive.draw (context.get_framebuffer (), pipeline); +#endif + base.paint (context); + } + + /** + * Remove all currently added WindowIconActors + */ + public void clear () { + icon_container.destroy_all_children (); + } + + /** + * Creates a WindowIconActor for the given window and adds it to the group + * + * @param window The MetaWindow for which to create the WindowIconActor + * @param no_redraw If you add multiple windows at once you may want to consider + * settings this to true and when done calling redraw() manually + * @param temporary Mark the WindowIconActor as temporary. Used for windows dragged over + * the group. + */ + public void add_window (Meta.Window window, bool no_redraw = false, bool temporary = false) { + if (!windows_list.find (window).is_empty ()) { + return; + } + + windows_list.append (window); + + var new_window = new WindowIconActor (window); + new_window.set_position (32, 32); + new_window.temporary = temporary; + + icon_container.add_child (new_window); + + if (!no_redraw) + redraw (); + } + + /** + * Remove the WindowIconActor for a MetaWindow from the group + * + * @param animate Whether to fade the icon out before removing it + */ + public void remove_window (Meta.Window window, bool animate = true) { + foreach (unowned var child in icon_container.get_children ()) { + unowned var icon = (WindowIconActor) child; + if (icon.window == window) { + if (animate) { + icon.save_easing_state (); + icon.set_easing_mode (Clutter.AnimationMode.LINEAR); + icon.set_easing_duration (Utils.get_animation_duration (200)); + icon.opacity = 0; + icon.restore_easing_state (); + + var transition = icon.get_transition ("opacity"); + if (transition != null) { + transition.completed.connect (() => { + icon.destroy (); + }); + } else { + icon.destroy (); + } + + } else { + icon.destroy (); + } + + // don't break here! If people spam hover events and we animate + // removal, we can actually multiple instances of the same window icon + } + } + } + + public void remove_all_windows () { + windows_list = new GLib.List (); + icon_container.remove_all_children (); + } + + /** + * Sets a hovered actor for the drag action. + */ + public void set_hovered_actor (Clutter.Actor actor) { + drag_action.hovered = actor; + } + + /** + * Trigger a redraw + */ + public void redraw () { + content.invalidate (); + } + + /** + * Draw the background or plus sign and do layouting. We won't lose performance here + * by relayouting in the same function, as it's only ever called when we invalidate it. + */ + protected override void draw (Cairo.Context cr, int cr_width, int cr_height) { + clear_effects (); + cr.set_operator (Cairo.Operator.CLEAR); + cr.paint (); + cr.set_operator (Cairo.Operator.OVER); + + var n_windows = icon_container.get_n_children (); + + // single icon => big icon + if (n_windows == 1) { + var icon = (WindowIconActor) icon_container.get_child_at_index (0); + icon.place (0, 0, 64, scale_factor); + + return; + } + + // more than one => we need a folder + Drawing.Utilities.cairo_rounded_rectangle ( + cr, + 0, + 0, + width, + height, + Utils.scale_to_int (5, scale_factor) + ); + + var shadow_effect = new ShadowEffect ("", scale_factor) { + border_radius = 6 + }; + + var style_manager = Drawing.StyleManager.get_instance (); + + if (style_manager.prefers_color_scheme == DARK) { + const double BG_COLOR = 35.0 / 255.0; + if (drag_action.dragging) { + cr.set_source_rgba (BG_COLOR, BG_COLOR, BG_COLOR, 0.8); + } else { + cr.set_source_rgba (BG_COLOR , BG_COLOR , BG_COLOR , 0.5); + shadow_effect.shadow_opacity = 200; + } + } else { + if (drag_action.dragging) { + cr.set_source_rgba (255, 255, 255, 0.8); + } else { + cr.set_source_rgba (255, 255, 255, 0.3); + shadow_effect.shadow_opacity = 100; + } + } + + if (drag_action.dragging) { + shadow_effect.css_class = "workspace-switcher-dnd"; + } else { + shadow_effect.css_class = "workspace-switcher"; + } + + add_effect (shadow_effect); + cr.fill_preserve (); + + // it's not safe to to call meta_workspace_index() here, we may be still animating something + // while the workspace is already gone, which would result in a crash. + unowned Meta.WorkspaceManager manager = workspace.get_display ().get_workspace_manager (); + int workspace_index = 0; + for (int i = 0; i < manager.get_n_workspaces (); i++) { + if (manager.get_workspace_by_index (i) == workspace) { + workspace_index = i; + break; + } + } + + var scaled_size = Utils.scale_to_int (SIZE, scale_factor); + + if (n_windows < 1) { + if (workspace_index != manager.get_n_workspaces () - 1) { + return; + } + + var buffer = new Drawing.BufferSurface (scaled_size, scaled_size); + var offset = scaled_size / 2 - Utils.scale_to_int (PLUS_WIDTH, scale_factor) / 2; + + buffer.context.rectangle ( + Utils.scale_to_int (PLUS_WIDTH / 2, scale_factor) - Utils.scale_to_int (PLUS_SIZE / 2, scale_factor) + offset, + offset, + Utils.scale_to_int (PLUS_SIZE, scale_factor), + Utils.scale_to_int (PLUS_WIDTH, scale_factor) + ); + + buffer.context.rectangle (offset, + Utils.scale_to_int (PLUS_WIDTH / 2, scale_factor) - Utils.scale_to_int (PLUS_SIZE / 2, scale_factor) + offset, + Utils.scale_to_int (PLUS_WIDTH, scale_factor), + Utils.scale_to_int (PLUS_SIZE, scale_factor) + ); + + if (style_manager.prefers_color_scheme == DARK) { + buffer.context.move_to (0, 1 * scale_factor); + buffer.context.set_source_rgb (0, 0, 0); + buffer.context.fill_preserve (); + buffer.exponential_blur (2); + + buffer.context.move_to (0, 0); + buffer.context.set_source_rgba (1, 1, 1, 0.95); + } else { + buffer.context.move_to (0, 1 * scale_factor); + buffer.context.set_source_rgba (1, 1, 1, 0.4); + buffer.context.fill_preserve (); + buffer.exponential_blur (1); + + buffer.context.move_to (0, 0); + buffer.context.set_source_rgba (0, 0, 0, 0.7); + } + + buffer.context.fill (); + + cr.set_source_surface (buffer.surface, 0, 0); + cr.paint (); + + return; + } + + int size; + if (n_windows < 5) + size = 24; + else + size = 16; + + var n_tiled_windows = uint.min (n_windows, 9); + var columns = (int) Math.ceil (Math.sqrt (n_tiled_windows)); + var rows = (int) Math.ceil (n_tiled_windows / (double) columns); + + int spacing = Utils.scale_to_int (6, scale_factor); + + var width = columns * Utils.scale_to_int (size, scale_factor) + (columns - 1) * spacing; + var height = rows * Utils.scale_to_int (size, scale_factor) + (rows - 1) * spacing; + var x_offset = scaled_size / 2 - width / 2; + var y_offset = scaled_size / 2 - height / 2; + + var show_ellipsis = false; + var n_shown_windows = n_windows; + // make place for an ellipsis + if (n_shown_windows > 9) { + n_shown_windows = 8; + show_ellipsis = true; + } + + var x = x_offset; + var y = y_offset; + for (var i = 0; i < n_windows; i++) { + var window = (WindowIconActor) icon_container.get_child_at_index (i); + + // draw an ellipsis at the 9th position if we need one + if (show_ellipsis && i == 8) { + int top_offset = Utils.scale_to_int (10, scale_factor); + int left_offset = Utils.scale_to_int (2, scale_factor); + int radius = Utils.scale_to_int (2, scale_factor); + int dot_spacing = Utils.scale_to_int (3, scale_factor); + cr.arc (left_offset + x, y + top_offset, radius, 0, 2 * Math.PI); + cr.arc (left_offset + x + radius + dot_spacing, y + top_offset, radius, 0, 2 * Math.PI); + cr.arc (left_offset + x + radius * 2 + dot_spacing * 2, y + top_offset, radius, 0, 2 * Math.PI); + + cr.set_source_rgb (0.3, 0.3, 0.3); + cr.fill (); + } + + if (i >= n_shown_windows) { + window.visible = false; + continue; + } + + window.place (x, y, size, scale_factor); + + x += Utils.scale_to_int (size, scale_factor) + spacing; + if (x + Utils.scale_to_int (size, scale_factor) >= scaled_size) { + x = x_offset; + y += Utils.scale_to_int (size, scale_factor) + spacing; + } + } + } + + private Clutter.Actor? drag_begin (float click_x, float click_y) { + unowned Meta.WorkspaceManager manager = workspace.get_display ().get_workspace_manager (); + if (icon_container.get_n_children () < 1 && + workspace.index () == manager.get_n_workspaces () - 1) { + return null; + } + + float abs_x, abs_y; + float prev_parent_x, prev_parent_y; + + prev_parent = get_parent (); + prev_parent.get_transformed_position (out prev_parent_x, out prev_parent_y); + + var stage = get_stage (); + var container = prev_parent as IconGroupContainer; + if (container != null) { + container.remove_group_in_place (this); + container.reset_thumbs (0); + } else { + prev_parent.remove_child (this); + } + + stage.add_child (this); + + get_transformed_position (out abs_x, out abs_y); + set_position (abs_x + prev_parent_x, abs_y + prev_parent_y); + + // disable reactivity so that workspace thumbs can get events + reactive = false; + +#if HAS_MUTTER48 + display.set_cursor (Meta.Cursor.MOVE); +#else + display.set_cursor (Meta.Cursor.DND_IN_DRAG); +#endif + + return this; + } + + private void drag_end (Clutter.Actor destination) { + if (destination is WorkspaceInsertThumb) { + get_parent ().remove_child (this); + + unowned WorkspaceInsertThumb inserter = (WorkspaceInsertThumb) destination; + unowned Meta.WorkspaceManager manager = workspace.get_display ().get_workspace_manager (); + manager.reorder_workspace (workspace, inserter.workspace_index); + + restore_group (); + } else { + drag_canceled (); + } + + display.set_cursor (Meta.Cursor.DEFAULT); + } + + private void drag_canceled () { + get_parent ().remove_child (this); + restore_group (); + + display.set_cursor (Meta.Cursor.DEFAULT); + } + + private void restore_group () { + var container = prev_parent as IconGroupContainer; + if (container != null) { + container.add_group (this); + container.request_reposition (false); + container.reset_thumbs (WorkspaceInsertThumb.EXPAND_DELAY); + } + } +} diff --git a/src/Widgets/MultitaskingView/IconGroups/IconGroupContainer.vala b/src/Widgets/MultitaskingView/IconGroups/IconGroupContainer.vala new file mode 100644 index 000000000..58245a0a0 --- /dev/null +++ b/src/Widgets/MultitaskingView/IconGroups/IconGroupContainer.vala @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2014 Tom Beckmann + * 2025 elementary, Inc. + */ + +/** + * This class contains the icon groups at the bottom and will take + * care of displaying actors for inserting windows between the groups + * once implemented + */ +public class Gala.IconGroupContainer : Clutter.Actor { + public const int SPACING = 48; + public const int GROUP_WIDTH = 64; + + public signal void request_reposition (bool animate); + + private float _scale_factor = 1.0f; + public float scale_factor { + get { + return _scale_factor; + } + set { + if (value != _scale_factor) { + _scale_factor = value; + reallocate (); + } + } + } + + public IconGroupContainer (float scale) { + Object (scale_factor: scale); + + layout_manager = new Clutter.BoxLayout (); + } + + private void reallocate () { + foreach (var child in get_children ()) { + unowned WorkspaceInsertThumb thumb = child as WorkspaceInsertThumb; + if (thumb != null) { + thumb.scale_factor = scale_factor; + } + } + } + + public void add_group (IconGroup group) { + var index = group.workspace.index (); + + insert_child_at_index (group, index * 2); + + var thumb = new WorkspaceInsertThumb (index, scale_factor); + thumb.notify["expanded"].connect_after (expanded_changed); + insert_child_at_index (thumb, index * 2); + + update_inserter_indices (); + } + + public void remove_group (IconGroup group) { + var thumb = (WorkspaceInsertThumb) group.get_previous_sibling (); + thumb.notify["expanded"].disconnect (expanded_changed); + remove_child (thumb); + + remove_child (group); + + update_inserter_indices (); + } + + /** + * Removes an icon group "in place". + * When initially dragging an icon group we remove + * it and it's previous WorkspaceInsertThumb. This would make + * the container immediately reallocate and fill the empty space + * with right-most IconGroups. + * + * We don't want that until the IconGroup + * leaves the expanded WorkspaceInsertThumb. + */ + public void remove_group_in_place (IconGroup group) { + var deleted_thumb = (WorkspaceInsertThumb) group.get_previous_sibling (); + var deleted_placeholder_thumb = (WorkspaceInsertThumb) group.get_next_sibling (); + + remove_group (group); + + /* + * We will account for that empty space + * by manually expanding the next WorkspaceInsertThumb with the + * width we deleted. Because the IconGroup is still hovering over + * the expanded thumb, we will also update the drag & drop action + * of IconGroup on that. + */ + if (deleted_placeholder_thumb != null) { + float deleted_width = deleted_thumb.get_width () + group.get_width (); + deleted_placeholder_thumb.expanded = true; + deleted_placeholder_thumb.width += deleted_width; + group.set_hovered_actor (deleted_placeholder_thumb); + } + } + + public void reset_thumbs (int delay) { + foreach (var child in get_children ()) { + unowned WorkspaceInsertThumb thumb = child as WorkspaceInsertThumb; + if (thumb != null) { + thumb.delay = delay; + thumb.destroy_all_children (); + } + } + } + + private void expanded_changed (ParamSpec param) { + request_reposition (true); + } + + /** + * Calculates the width that will be occupied taking currently running animations + * end states into account + */ + public float calculate_total_width () { + var spacing = Utils.scale_to_int (SPACING, scale_factor); + var group_width = Utils.scale_to_int (GROUP_WIDTH, scale_factor); + + var width = 0.0f; + foreach (var child in get_children ()) { + if (child is WorkspaceInsertThumb) { + if (((WorkspaceInsertThumb) child).expanded) + width += group_width + spacing * 2; + else + width += spacing; + } else + width += group_width; + } + + width += spacing; + + return width; + } + + public void force_reposition () { + var children = get_children (); + + foreach (var child in children) { + if (child is IconGroup) { + remove_group ((IconGroup) child); + } + } + + foreach (var child in children) { + if (child is IconGroup) { + add_group ((IconGroup) child); + } + } + } + + private void update_inserter_indices () { + var current_index = 0; + + foreach (var child in get_children ()) { + unowned WorkspaceInsertThumb thumb = child as WorkspaceInsertThumb; + if (thumb != null) { + thumb.workspace_index = current_index++; + } + } + } +} diff --git a/src/Widgets/MultitaskingView/IconGroups/WindowIconActor.vala b/src/Widgets/MultitaskingView/IconGroups/WindowIconActor.vala new file mode 100644 index 000000000..396616420 --- /dev/null +++ b/src/Widgets/MultitaskingView/IconGroups/WindowIconActor.vala @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2014 Tom Beckmann + * 2025 elementary, Inc. (https://elementary.io) + */ + +/** + * Private class which is basically just a container for the actual + * icon and takes care of blending the same icon in different sizes + * over each other and various animations related to the icons + */ +public class Gala.WindowIconActor : Clutter.Actor { + public Meta.Window window { get; construct; } + + private float cur_icon_scale = 1.0f; + private float desired_icon_scale = 1.0f; + + private int _icon_size; + /** + * The icon size of the WindowIcon. Once set the new icon will be + * faded over the old one and the actor animates to the new size. + */ + public int icon_size { + get { + return _icon_size; + } + private set { + if (value == _icon_size && cur_icon_scale == desired_icon_scale) { + return; + } + + _icon_size = value; + cur_icon_scale = desired_icon_scale; + + var scaled_size = Utils.scale_to_int (_icon_size, cur_icon_scale); + set_size (scaled_size, scaled_size); + + fade_new_icon (); + } + } + + private bool _temporary; + /** + * Mark the WindowIcon as temporary. Only effect of this is that a pulse + * animation will be played on the actor. Used while DnDing window thumbs + * over the group. + */ + public bool temporary { + get { + return _temporary; + } + set { + if (_temporary && !value) { + remove_transition ("pulse"); + } else if (!_temporary && value && Meta.Prefs.get_gnome_animations ()) { + var transition = new Clutter.TransitionGroup () { + duration = 800, + auto_reverse = true, + repeat_count = -1, + progress_mode = Clutter.AnimationMode.LINEAR + }; + + var opacity_transition = new Clutter.PropertyTransition ("opacity"); + opacity_transition.set_from_value (100); + opacity_transition.set_to_value (255); + opacity_transition.auto_reverse = true; + + var scale_x_transition = new Clutter.PropertyTransition ("scale-x"); + scale_x_transition.set_from_value (0.8); + scale_x_transition.set_to_value (1.1); + scale_x_transition.auto_reverse = true; + + var scale_y_transition = new Clutter.PropertyTransition ("scale-y"); + scale_y_transition.set_from_value (0.8); + scale_y_transition.set_to_value (1.1); + scale_y_transition.auto_reverse = true; + + transition.add_transition (opacity_transition); + transition.add_transition (scale_x_transition); + transition.add_transition (scale_y_transition); + + add_transition ("pulse", transition); + } + + _temporary = value; + } + } + + private WindowIcon? icon = null; + private WindowIcon? old_icon = null; + + public WindowIconActor (Meta.Window window) { + Object (window: window); + } + + construct { + set_pivot_point (0.5f, 0.5f); + + window.notify["on-all-workspaces"].connect (on_all_workspaces_changed); + } + + ~WindowIconActor () { + window.notify["on-all-workspaces"].disconnect (on_all_workspaces_changed); + } + + private void on_all_workspaces_changed () { + // we don't display windows that are on all workspaces + if (window.on_all_workspaces) + destroy (); + } + + /** + * Shortcut to set both position and size of the icon + * + * @param x The x coordinate to which to animate to + * @param y The y coordinate to which to animate to + * @param size The size to which to animate to and display the icon in + */ + public void place (float x, float y, int size, float scale) { + desired_icon_scale = scale; + set_position (x, y); + icon_size = size; + } + + /** + * Fades out the old icon and fades in the new icon + */ + private void fade_new_icon () { + var new_icon = new WindowIcon (window, icon_size, (int)Math.round (cur_icon_scale)); + new_icon.add_constraint (new Clutter.BindConstraint (this, Clutter.BindCoordinate.SIZE, 0)); + new_icon.opacity = 0; + + add_child (new_icon); + + new_icon.save_easing_state (); + new_icon.set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD); + new_icon.set_easing_duration (Utils.get_animation_duration (500)); + new_icon.restore_easing_state (); + + if (icon == null) { + icon = new_icon; + } else { + old_icon = icon; + } + + new_icon.opacity = 255; + + if (old_icon != null) { + old_icon.opacity = 0; + var transition = old_icon.get_transition ("opacity"); + if (transition != null) { + transition.completed.connect (() => { + old_icon.destroy (); + old_icon = null; + }); + } else { + old_icon.destroy (); + old_icon = null; + } + } + + icon = new_icon; + } +} diff --git a/src/Widgets/MultitaskingView/IconGroups/WorkspaceInsertThumb.vala b/src/Widgets/MultitaskingView/IconGroups/WorkspaceInsertThumb.vala new file mode 100644 index 000000000..583eb7134 --- /dev/null +++ b/src/Widgets/MultitaskingView/IconGroups/WorkspaceInsertThumb.vala @@ -0,0 +1,125 @@ +/* + * Copyright 2014 Tom Beckmann + * Copyright 2023 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class Gala.WorkspaceInsertThumb : Clutter.Actor { + public const int EXPAND_DELAY = 300; + + public int workspace_index { get; construct set; } + public bool expanded { get; set; default = false; } + public int delay { get; set; default = EXPAND_DELAY; } + private float _scale_factor = 1.0f; + public float scale_factor { + get { + return _scale_factor; + } + set { + if (value != _scale_factor) { + _scale_factor = value; + reallocate (); + } + } + } + + private uint expand_timeout = 0; + + public WorkspaceInsertThumb (int workspace_index, float scale) { + Object (workspace_index: workspace_index, scale_factor: scale); + + reallocate (); + opacity = 0; + set_pivot_point (0.5f, 0.5f); + reactive = true; + x_align = Clutter.ActorAlign.CENTER; + + var drop = new DragDropAction (DragDropActionType.DESTINATION, "multitaskingview-window"); + drop.crossed.connect ((target, hovered) => { + if (!hovered) { + if (expand_timeout != 0) { + Source.remove (expand_timeout); + expand_timeout = 0; + } + + transform (false); + } else { + expand_timeout = Timeout.add (delay, expand); + } + }); + + add_action (drop); + } + + private void reallocate () { + width = Utils.scale_to_int (IconGroupContainer.SPACING, scale_factor); + height = Utils.scale_to_int (IconGroupContainer.GROUP_WIDTH, scale_factor); + y = Utils.scale_to_int (IconGroupContainer.GROUP_WIDTH - IconGroupContainer.SPACING, scale_factor) / 2; + } + + public void set_window_thumb (Meta.Window window) { + destroy_all_children (); + + var icon = new WindowIcon (window, IconGroupContainer.GROUP_WIDTH, (int)Math.round (scale_factor)) { + x = IconGroupContainer.SPACING, + x_align = Clutter.ActorAlign.CENTER + }; + add_child (icon); + } + + private bool expand () { + expand_timeout = 0; + + transform (true); + + return Source.REMOVE; + } + + private new void transform (bool expand) { + save_easing_state (); + set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD); + set_easing_duration (Utils.get_animation_duration (200)); + + if (!expand) { + remove_transition ("pulse"); + opacity = 0; + width = Utils.scale_to_int (IconGroupContainer.SPACING, scale_factor); + expanded = false; + } else { + add_pulse_animation (); + opacity = 200; + width = Utils.scale_to_int (IconGroupContainer.GROUP_WIDTH + IconGroupContainer.SPACING * 2, scale_factor); + expanded = true; + } + + restore_easing_state (); + } + + private void add_pulse_animation () { + if (!Meta.Prefs.get_gnome_animations ()) { + return; + } + + var transition = new Clutter.TransitionGroup () { + duration = 800, + auto_reverse = true, + repeat_count = -1, + progress_mode = Clutter.AnimationMode.LINEAR + }; + + var scale_x_transition = new Clutter.PropertyTransition ("scale-x"); + scale_x_transition.set_from_value (0.8); + scale_x_transition.set_to_value (1.1); + scale_x_transition.auto_reverse = true; + + var scale_y_transition = new Clutter.PropertyTransition ("scale-y"); + scale_y_transition.set_from_value (0.8); + scale_y_transition.set_to_value (1.1); + scale_y_transition.auto_reverse = true; + + transition.add_transition (scale_x_transition); + transition.add_transition (scale_y_transition); + + add_transition ("pulse", transition); + } +} diff --git a/src/Widgets/MultitaskingView/MultitaskingView.vala b/src/Widgets/MultitaskingView/MultitaskingView.vala index 4ee643cc9..4ac4ba68f 100644 --- a/src/Widgets/MultitaskingView/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView/MultitaskingView.vala @@ -35,6 +35,9 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone private List window_containers_monitors; +#if OLD_ICON_GROUPS + private IconGroupContainer icon_groups; +#endif private ActorTarget workspaces; private Clutter.Actor primary_monitor_container; private Clutter.BrightnessContrastEffect brightness_effect; @@ -74,12 +77,19 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone workspaces_gesture_controller.enable_scroll (this, HORIZONTAL); add_gesture_controller (workspaces_gesture_controller); +#if OLD_ICON_GROUPS + icon_groups = new IconGroupContainer (display.get_monitor_scale (display.get_primary_monitor ())); +#endif + update_blurred_bg (); // Create a child container that will be sized to fit the primary monitor, to contain the "main" // multitasking view UI. The Clutter.Actor of this class has to be allowed to grow to the size of the // stage as it contains MonitorClones for each monitor. primary_monitor_container = new ActorTarget (); +#if OLD_ICON_GROUPS + primary_monitor_container.add_child (icon_groups); +#endif primary_monitor_container.add_child (workspaces); add_child (primary_monitor_container); @@ -142,6 +152,9 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone var primary_geometry = display.get_monitor_geometry (primary); var scale = display.get_monitor_scale (primary); +#if OLD_ICON_GROUPS + icon_groups.scale_factor = scale; +#endif primary_monitor_container.set_position (primary_geometry.x, primary_geometry.y); primary_monitor_container.set_size (primary_geometry.width, primary_geometry.height); @@ -179,6 +192,9 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone private void update_workspaces () { foreach (unowned var child in workspaces.get_children ()) { unowned var workspace_clone = (WorkspaceClone) child; +#if OLD_ICON_GROUPS + icon_groups.remove_group (workspace_clone.icon_group); +#endif workspace_clone.destroy (); } @@ -245,12 +261,26 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone wm.background_group.hide (); wm.window_group.hide (); wm.top_window_group.hide (); +#if OLD_ICON_GROUPS + icon_groups.show (); +#endif show (); grab_key_focus (); modal_proxy = wm.push_modal (get_stage (), false); modal_proxy.set_keybinding_filter (keybinding_filter); modal_proxy.allow_actions ({ MULTITASKING_VIEW, SWITCH_WORKSPACE, ZOOM }); + +#if OLD_ICON_GROUPS + var scale = display.get_monitor_scale (display.get_primary_monitor ()); + icon_groups.force_reposition (); + icon_groups.y = primary_monitor_container.height - Utils.scale_to_int (WorkspaceClone.BOTTOM_OFFSET - 20, scale); + reposition_icon_groups (false); + + if (action != MULTITASKING_VIEW) { + icon_groups.hide (); + } +#endif } else if (action == MULTITASKING_VIEW) { DragDropAction.cancel_all_by_id ("multitaskingview-window"); } @@ -307,6 +337,9 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone wm.background_group.show (); wm.window_group.show (); wm.top_window_group.show (); +#if OLD_ICON_GROUPS + icon_groups.hide (); +#endif hide (); wm.pop_modal (modal_proxy); @@ -319,14 +352,47 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone } } +#if OLD_ICON_GROUPS + private void reposition_icon_groups (bool animate) { + unowned Meta.WorkspaceManager manager = display.get_workspace_manager (); + var active_index = manager.get_active_workspace ().index (); + + if (animate) { + icon_groups.save_easing_state (); + icon_groups.set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD); + icon_groups.set_easing_duration (200); + } + + var scale = display.get_monitor_scale (display.get_primary_monitor ()); + // make sure the active workspace's icongroup is always visible + var icon_groups_width = icon_groups.calculate_total_width (); + if (icon_groups_width > primary_monitor_container.width) { + icon_groups.x = (-active_index * Utils.scale_to_int (IconGroupContainer.SPACING + IconGroup.SIZE, scale) + primary_monitor_container.width / 2) + .clamp (primary_monitor_container.width - icon_groups_width - Utils.scale_to_int (64, scale), Utils.scale_to_int (64, scale)); + } else + icon_groups.x = primary_monitor_container.width / 2 - icon_groups_width / 2; + + if (animate) { + icon_groups.restore_easing_state (); + } + } +#endif + private void add_workspace (int num) { unowned var manager = display.get_workspace_manager (); var scale = display.get_monitor_scale (display.get_primary_monitor ()); var workspace = new WorkspaceClone (wm, manager.get_workspace_by_index (num), scale); workspaces.insert_child_at_index (workspace, num); +#if OLD_ICON_GROUPS + icon_groups.add_group (workspace.icon_group); +#endif workspace.window_selected.connect (window_selected); + +#if OLD_ICON_GROUPS + reposition_icon_groups (false); +#endif } private void remove_workspace (int num) { @@ -352,8 +418,19 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone } workspace.window_selected.disconnect (window_selected); + +#if OLD_ICON_GROUPS + if (icon_groups.contains (workspace.icon_group)) { + icon_groups.remove_group (workspace.icon_group); + } +#endif + workspace.destroy (); +#if OLD_ICON_GROUPS + reposition_icon_groups (opened); +#endif + workspaces_gesture_controller.progress = -manager.get_active_workspace_index (); } @@ -362,6 +439,10 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone unowned var manager = display.get_workspace_manager (); workspaces_gesture_controller.progress = -manager.get_active_workspace_index (); } + +#if OLD_ICON_GROUPS + reposition_icon_groups (opened); +#endif } private void on_workspace_switched (int from, int to) { diff --git a/src/Widgets/MultitaskingView/WindowClone.vala b/src/Widgets/MultitaskingView/WindowClone.vala index c1f2f91dd..414efc32f 100644 --- a/src/Widgets/MultitaskingView/WindowClone.vala +++ b/src/Widgets/MultitaskingView/WindowClone.vala @@ -500,6 +500,7 @@ public class Gala.WindowClone : ActorTarget, RootTarget { } private void destination_crossed (Clutter.Actor destination, bool hovered) { +#if !OLD_ICON_GROUPS if (!(destination is Meta.WindowActor)) { return; } @@ -509,6 +510,55 @@ public class Gala.WindowClone : ActorTarget, RootTarget { } else { WindowDragProvider.get_instance ().notify_leave (); } +#else + var icon_group = destination as IconGroup; + var insert_thumb = destination as WorkspaceInsertThumb; + + // if we have don't dynamic workspace, we don't allow inserting + if (icon_group == null && insert_thumb == null + || (insert_thumb != null && !Meta.Prefs.get_dynamic_workspaces ())) { + return; + } + + // for an icon group, we only do animations if there is an actual movement possible + if (icon_group != null + && icon_group.workspace == window.get_workspace () + && window.is_on_primary_monitor ()) { + return; + } + + var scale = hovered ? 0.4 : 1.0; + var opacity = hovered ? 0 : 255; + uint duration = hovered && insert_thumb != null ? insert_thumb.delay : 100; + duration = Utils.get_animation_duration (duration); + + window_icon.save_easing_state (); + + window_icon.set_easing_mode (Clutter.AnimationMode.LINEAR); + window_icon.set_easing_duration (duration); + window_icon.set_scale (scale, scale); + window_icon.set_opacity (opacity); + + window_icon.restore_easing_state (); + + if (insert_thumb != null) { + insert_thumb.set_window_thumb (window); + } + + if (icon_group != null) { + if (hovered) { + icon_group.add_window (window, false, true); + } else { + icon_group.remove_window (window, false); + } + } + +#if HAS_MUTTER48 + wm.get_display ().set_cursor (hovered ? Meta.Cursor.MOVE : Meta.Cursor.NO_DROP); +#else + wm.get_display ().set_cursor (hovered ? Meta.Cursor.DND_MOVE : Meta.Cursor.DND_IN_DRAG); +#endif +#endif } private void destination_motion (Clutter.Actor destination, float x, float y) { diff --git a/src/Widgets/MultitaskingView/WorkspaceClone.vala b/src/Widgets/MultitaskingView/WorkspaceClone.vala index b6563b6b9..6d9132a15 100644 --- a/src/Widgets/MultitaskingView/WorkspaceClone.vala +++ b/src/Widgets/MultitaskingView/WorkspaceClone.vala @@ -95,7 +95,11 @@ public class Gala.WorkspaceClone : ActorTarget { /** * The offset of the scaled background to the bottom of the monitor bounds */ +#if OLD_ICON_GROUPS + public const int BOTTOM_OFFSET = 100; +#else private const int BOTTOM_OFFSET = 100; +#endif /** * The offset of the scaled background to the top of the monitor bounds @@ -118,6 +122,9 @@ public class Gala.WorkspaceClone : ActorTarget { public Meta.Workspace workspace { get; construct; } public float monitor_scale { get; construct set; } +#if OLD_ICON_GROUPS + public IconGroup icon_group { get; private set; } +#endif public WindowCloneContainer window_container { get; private set; } private BackgroundManager background; @@ -147,6 +154,15 @@ public class Gala.WorkspaceClone : ActorTarget { window_container.requested_close.connect (() => activate (true)); bind_property ("monitor-scale", window_container, "monitor-scale"); +#if OLD_ICON_GROUPS + icon_group = new IconGroup (display, workspace, monitor_scale); + icon_group.selected.connect (() => activate (true)); + bind_property ("monitor-scale", icon_group, "scale-factor"); + + var icons_drop_action = new DragDropAction (DragDropActionType.DESTINATION, "multitaskingview-window"); + icon_group.add_action (icons_drop_action); +#endif + var background_drop_action = new DragDropAction (DragDropActionType.DESTINATION, "multitaskingview-window"); background.add_action (background_drop_action); background_drop_action.crossed.connect ((target, hovered) => { @@ -170,6 +186,11 @@ public class Gala.WorkspaceClone : ActorTarget { add_child (background); add_child (window_container); +#if OLD_ICON_GROUPS + on_items_changed (windows, 0, 0, windows.get_n_items ()); + windows.items_changed.connect (on_items_changed); +#endif + unowned var monitor_manager = display.get_context ().get_backend ().get_monitor_manager (); monitor_manager.monitors_changed.connect (update_targets); notify["monitor-scale"].connect (update_targets); @@ -179,6 +200,9 @@ public class Gala.WorkspaceClone : ActorTarget { ~WorkspaceClone () { background.destroy (); window_container.destroy (); +#if OLD_ICON_GROUPS + icon_group.destroy (); +#endif } public void update_size (Mtk.Rectangle monitor_geometry) { @@ -188,6 +212,16 @@ public class Gala.WorkspaceClone : ActorTarget { } } +#if OLD_ICON_GROUPS + private void on_items_changed (ListModel windows, uint position, uint removed, uint added) { + icon_group.remove_all_windows (); + + for (var i = 0; i < windows.get_n_items (); i++) { + icon_group.add_window ((Meta.Window) windows.get_item (i)); + } + } +#endif + private void update_targets () { remove_all_targets (); @@ -214,6 +248,14 @@ public class Gala.WorkspaceClone : ActorTarget { window_container.padding_bottom = Utils.scale_to_int (BOTTOM_OFFSET, monitor_scale); } +#if OLD_ICON_GROUPS + public override void update_progress (GestureAction action, double progress) { + if (action == SWITCH_WORKSPACE) { + icon_group.backdrop_opacity = 1 - (float) (workspace.index () + progress).abs ().clamp (0, 1); + } + } +#endif + private void activate (bool close_view) { if (close_view && workspace.active) { wm.perform_action (SHOW_MULTITASKING_VIEW); diff --git a/src/meson.build b/src/meson.build index 08c81890f..d0503f355 100644 --- a/src/meson.build +++ b/src/meson.build @@ -69,6 +69,15 @@ gala_bin_sources = files( 'Widgets/WindowSwitcher/WindowSwitcherIcon.vala', ) +if get_option('old-icon-groups') + gala_bin_sources += [ + 'Widgets/MultitaskingView/IconGroups/IconGroup.vala', + 'Widgets/MultitaskingView/IconGroups/IconGroupContainer.vala', + 'Widgets/MultitaskingView/IconGroups/WindowIconActor.vala', + 'Widgets/MultitaskingView/IconGroups/WorkspaceInsertThumb.vala' + ] +endif + gala_bin = executable( 'gala', gala_bin_sources,