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(