Skip to content

Commit

Permalink
Implement DnD reordering (#181)
Browse files Browse the repository at this point in the history
Co-authored-by: Leo <[email protected]>
Co-authored-by: Ryan Kornheisl <[email protected]>
  • Loading branch information
3 people committed Nov 11, 2023
1 parent 0125900 commit 47c02f8
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 4 deletions.
28 changes: 28 additions & 0 deletions data/Launcher.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions data/dock.gresource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<gresource prefix="/io/elementary/dock">
<file compressed="true">MainWindow.css</file>
<file compressed="true">Launcher.css</file>
<file compressed="true">poof.svg</file>
</gresource>
</gresources>
114 changes: 114 additions & 0 deletions data/poof.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
141 changes: 139 additions & 2 deletions src/Launcher.vala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@
*/

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; }

public GLib.List<AppWindow> windows { get; private owned set; }

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) {
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -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<AppWindow>? new_windows) {
if (new_windows == null) {
windows = new GLib.List<AppWindow> ();
Expand All @@ -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;
}
}
54 changes: 53 additions & 1 deletion src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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<Dock.DesktopIntegration> (
GLib.BusType.SESSION,
Expand Down Expand Up @@ -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 ())) {
Expand Down
56 changes: 56 additions & 0 deletions src/PoofPopover.vala
Original file line number Diff line number Diff line change
@@ -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;
}
});
}
}
3 changes: 2 additions & 1 deletion src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ sources = [
'AppWindow.vala',
'DesktopIntegration.vala',
'Launcher.vala',
'MainWindow.vala'
'MainWindow.vala',
'PoofPopover.vala'
]

executable(
Expand Down

0 comments on commit 47c02f8

Please sign in to comment.