diff --git a/data/Launcher.css b/data/Launcher.css index d049acb6..51be165f 100644 --- a/data/Launcher.css +++ b/data/Launcher.css @@ -24,3 +24,31 @@ launcher image { transform: translatey(-20px); } } + +.move-right { + animation: move-right 300ms; + animation-direction: normal; +} + +@keyframes move-right { + from { + transform: translatex(-60px); + } + to { + transform: translatex(0); + } +} + +.move-left { + animation: move-left 300ms; + animation-direction: normal; +} + +@keyframes move-left { + from { + transform: translatex(60px); + } + to { + transform: translatex(0); + } +} diff --git a/data/dock.gresource.xml b/data/dock.gresource.xml index 853ec9fa..b98c1a89 100644 --- a/data/dock.gresource.xml +++ b/data/dock.gresource.xml @@ -3,5 +3,6 @@ MainWindow.css Launcher.css + poof.svg diff --git a/data/poof.svg b/data/poof.svg new file mode 100644 index 00000000..d16bdf5e --- /dev/null +++ b/data/poof.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Launcher.vala b/src/Launcher.vala index 2c1cfa19..d5528147 100644 --- a/src/Launcher.vala +++ b/src/Launcher.vala @@ -4,6 +4,10 @@ */ public class Dock.Launcher : Gtk.Button { + // Matches icon size and padding in Launcher.css + public const int ICON_SIZE = 48; + public const int PADDING = 6; + public GLib.DesktopAppInfo app_info { get; construct; } public bool pinned { get; set; } @@ -11,6 +15,12 @@ public class Dock.Launcher : Gtk.Button { private static Gtk.CssProvider css_provider; + private Gtk.Image image; + private int drag_offset_x = 0; + private int drag_offset_y = 0; + private string animate_css_class_name = ""; + private uint animate_timeout_id = 0; + private Gtk.PopoverMenu popover; public Launcher (GLib.DesktopAppInfo app_info) { @@ -56,14 +66,34 @@ public class Dock.Launcher : Gtk.Button { }; popover.set_parent (this); - var image = new Gtk.Image () { + image = new Gtk.Image () { gicon = app_info.get_icon () }; image.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - child = image; + // Needed to work around DnD bug where it + // would stop working once the button got clicked + var box = new Gtk.Box (VERTICAL, 0); + box.append (image); + + child = box; tooltip_text = app_info.get_display_name (); + var drag_source = new Gtk.DragSource () { + actions = MOVE + }; + box.add_controller (drag_source); + drag_source.prepare.connect (on_drag_prepare); + drag_source.drag_begin.connect (on_drag_begin); + drag_source.drag_cancel.connect (on_drag_cancel); + drag_source.drag_end.connect (() => image.gicon = app_info.get_icon ()); + + var drop_target = new Gtk.DropTarget (typeof (Launcher), MOVE) { + preload = true + }; + box.add_controller (drop_target); + drop_target.enter.connect (on_drop_enter); + notify["pinned"].connect (() => ((MainWindow) get_root ()).sync_pinned ()); var gesture_click = new Gtk.GestureClick () { @@ -103,6 +133,30 @@ public class Dock.Launcher : Gtk.Button { }); } + public void animate_move (Gtk.DirectionType dir) { + if (animate_timeout_id != 0) { + Source.remove (animate_timeout_id); + animate_timeout_id = 0; + remove_css_class (animate_css_class_name); + } + + if (dir == LEFT) { + animate_css_class_name = "move-left"; + } else if (dir == RIGHT) { + animate_css_class_name = "move-right"; + } else { + warning ("Invalid direction type."); + return; + } + + add_css_class (animate_css_class_name); + animate_timeout_id = Timeout.add (300, () => { + remove_css_class (animate_css_class_name); + animate_timeout_id = 0; + return Source.REMOVE; + }); + } + public void update_windows (owned GLib.List? new_windows) { if (new_windows == null) { windows = new GLib.List (); @@ -128,4 +182,87 @@ public class Dock.Launcher : Gtk.Button { return null; } } + + private Gdk.ContentProvider? on_drag_prepare (double x, double y) { + drag_offset_x = (int) x; + drag_offset_y = (int) y; + + var val = Value (typeof (Launcher)); + val.set_object (this); + return new Gdk.ContentProvider.for_value (val); + } + + private void on_drag_begin (Gtk.DragSource drag_source, Gdk.Drag drag) { + var paintable = new Gtk.WidgetPaintable (image); //Maybe TODO How TF can I get a paintable from a gicon?!?!? + drag_source.set_icon (paintable.get_current_image (), drag_offset_x, drag_offset_y); + image.clear (); + } + + private bool on_drag_cancel (Gdk.Drag drag, Gdk.DragCancelReason reason) { + if (pinned && reason == NO_TARGET) { + var popover = new PoofPopover (); + + unowned var window = (MainWindow) get_root (); + popover.set_parent (window); + unowned var surface = window.get_surface (); + + double x, y; + surface.get_device_position (drag.device, out x, out y, null); + + var rect = Gdk.Rectangle () { + x = (int) x, + y = (int) y + }; + + popover.set_pointing_to (rect); + // 50 and -13 position the popover in a way that the cursor is in the top left corner. + // (TODO: I got this with trial and error and I very much doubt that will be the same everywhere + // and at different scalings so it needs testing.) + // Although the drag_offset is also measured from the top left corner it works + // the other way round (i.e it moves the cursor not the surface) + // than set_offset so we put a - in front. + popover.set_offset ( + 50 - (drag_offset_x * (popover.width_request / ICON_SIZE)), + -13 - (drag_offset_y * (popover.height_request / ICON_SIZE)) + ); + popover.popup (); + popover.start_animation (); + + var box = (Gtk.Box) parent; + if (!windows.is_empty ()) { + window.move_launcher_after (this, (Launcher) box.get_last_child ()); + } + + pinned = false; + + return true; + } else { + image.gicon = app_info.get_icon (); + return false; + } + } + + private Gdk.DragAction on_drop_enter (Gtk.DropTarget drop_target, double x, double y) { + var val = drop_target.get_value (); + if (val != null) { + var obj = val.get_object (); + + if (obj != null && obj is Launcher) { + Launcher source = (Launcher) obj; + Launcher target = this; + + if (source != target) { + if (((x > get_allocated_width () / 2) && get_next_sibling () == source) || + ((x < get_allocated_width () / 2) && get_prev_sibling () != source) + ) { + target = (Launcher) get_prev_sibling (); + } + + ((MainWindow) get_root ()).move_launcher_after (source, target); + } + } + } + + return MOVE; + } } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 32100d9d..1c9fcd17 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -12,6 +12,7 @@ public class Dock.MainWindow : Gtk.ApplicationWindow { public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; private static Gtk.CssProvider css_provider; + private static Settings settings; private Gtk.Box box; private Dock.DesktopIntegration desktop_integration; @@ -24,6 +25,8 @@ public class Dock.MainWindow : Gtk.ApplicationWindow { static construct { css_provider = new Gtk.CssProvider (); css_provider.load_from_resource ("/io/elementary/dock/MainWindow.css"); + + settings = new Settings ("io.elementary.dock"); } construct { @@ -41,7 +44,9 @@ public class Dock.MainWindow : Gtk.ApplicationWindow { resizable = false; set_titlebar (empty_title); - var settings = new Settings ("io.elementary.dock"); + // Fixes DnD reordering of launchers failing on a very small line between two launchers + var drop_target_launcher = new Gtk.DropTarget (typeof (Launcher), MOVE); + box.add_controller (drop_target_launcher); GLib.Bus.get_proxy.begin ( GLib.BusType.SESSION, @@ -142,6 +147,53 @@ public class Dock.MainWindow : Gtk.ApplicationWindow { }); } + public void move_launcher_after (Launcher source, Launcher? target) { + var before_source = source.get_prev_sibling (); + + box.reorder_child_after (source, target); + + /* + * should_animate toggles to true once either the launcher before the one + * that was moved is reached or once the one that was moved is reached + * and goes false again once the other one is reached. While true + * all launchers that are iterated over are animated to move in the appropriate + * direction. + */ + bool should_animate = false; + Gtk.DirectionType dir = UP; // UP is an invalid placeholder value + + // source was the first launcher in the box so we start animating right away + if (before_source == null) { + should_animate = true; + dir = LEFT; + } + + Launcher child = (Launcher) box.get_first_child (); + while (child != null) { + if (child == source) { + should_animate = !should_animate; + if (should_animate) { + dir = RIGHT; + } + } + + if (should_animate && child != source) { + child.animate_move (dir); + } + + if (child == before_source) { + should_animate = !should_animate; + if (should_animate) { + dir = LEFT; + } + } + + child = (Launcher) child.get_next_sibling (); + } + + sync_pinned (); + } + public void remove_launcher (Launcher launcher, bool from_map = true) { foreach (var action in list_actions ()) { if (action.has_prefix (launcher.app_info.get_id ())) { diff --git a/src/PoofPopover.vala b/src/PoofPopover.vala new file mode 100644 index 00000000..a5d457e6 --- /dev/null +++ b/src/PoofPopover.vala @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io) + */ + +public class Dock.PoofPopover : Gtk.Popover { + private Gtk.Adjustment vadjustment; + private int poof_frames; + + construct { + var texture = Gdk.Texture.from_resource ("/io/elementary/dock/poof.svg"); + + var poof_size = texture.width; + poof_frames = (int) Math.floor (texture.height / poof_size); + + var picture = new Gtk.Picture.for_paintable (texture) { + width_request = poof_size, + height_request = poof_size * poof_frames, + keep_aspect_ratio = true + }; + + var scrolled_window = new Gtk.ScrolledWindow () { + hexpand = true, + vexpand = true, + child = picture, + vscrollbar_policy = EXTERNAL + }; + + vadjustment = scrolled_window.get_vadjustment (); + + height_request = poof_size; + width_request = poof_size; + has_arrow = false; + remove_css_class ("background"); + child = scrolled_window; + } + + public void start_animation () { + var frame = 0; + Timeout.add (30, () => { + var adjustment_step = (int) vadjustment.get_upper () / poof_frames; + vadjustment.value = vadjustment.value + adjustment_step; + if (frame < poof_frames) { + frame++; + return Source.CONTINUE; + } else { + popdown (); + Idle.add (() => { + unparent (); + return Source.REMOVE; + }); + return Source.REMOVE; + } + }); + } +} diff --git a/src/meson.build b/src/meson.build index 30b4b390..82306818 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,7 +3,8 @@ sources = [ 'AppWindow.vala', 'DesktopIntegration.vala', 'Launcher.vala', - 'MainWindow.vala' + 'MainWindow.vala', + 'PoofPopover.vala' ] executable(