summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOrestis Floros <orestisflo@gmail.com>2022-07-28 12:03:16 +0200
committerGitHub <noreply@github.com>2022-07-28 12:03:16 +0200
commitebcd1d43ea9fd08a1dbb1212fb61e42f05a22684 (patch)
tree5c4cb25d3b3b022501c05d6416398b08075004b7
parent807e972330b011de6afd227283cd49ebcf0ce1e7 (diff)
downloadi3-ebcd1d43ea9fd08a1dbb1212fb61e42f05a22684.tar.gz
i3-ebcd1d43ea9fd08a1dbb1212fb61e42f05a22684.zip
Allow dragging tiled windows with the mouse. (#3085)
Fixes #2643 Inner drop region behaves like move to mark. The outer region is close to the edge (currently 30px from the edge). This will place the container as a sibling in the given direction within the parent container. If the move direction goes against the orientation of the parent container, tree_move() is called. Contributors: Co-authored-by: Orestis Floros <orestisflo@gmail.com> See #3085 - Inner drop region behaves like move to mark - Handle workspaces - Fix crash when target closes - Initiate tiling drag from titlebar - Hide indicator until container is dragged outside of original position - Calculate outer_threshold using percentages instead of fixed pixel values - Emit 'move' event properly - Don't focus previously unfocused containers - Use tree_split() on different orientation - Fix redundant split containers - DT_PARENT - Readability & optimizations - Limit parent threshold by render_deco_height() - Tests - Fullscreen container handling - Initiate drag from title bar - Fix issue of EnterNotify events still triggering after drag_callback is called - Include decorations for drop target calculation Co-authored-by: Michael Forster <email@michael-forster.de> See #2178 - Original implementation of tiling drag + indicator window > A container can be dragged by the title bar to one of the four sides > of another container. That container will then be split either > horizontally or vertically. Co-authored-by: Tony Crisci <tony@dubstepdish.com> See #2653 - Original implementation of outer/inner drop region indicator: > There are two drop regions per direction. > > The inner region is closer to the center of the window. Dropping on > this region will split the target container and put the container > within the split at the given direction beside the target container. > > The outer region is close to the edge (currently 30px from the edge). > This will place the container as a sibling in the given direction within > the parent container. > > Dropping into the outer region moves the con beside the target. If the > move direction goes against the orientation of the parent container, the > con moves out of the row. - Fix crash: Ignore containers without a managed window (eg i3bar)
-rw-r--r--docs/userguide45
-rw-r--r--include/all.h1
-rw-r--r--include/con.h1
-rw-r--r--include/tiling_drag.h16
-rw-r--r--include/util.h12
-rw-r--r--meson.build1
-rw-r--r--release-notes/changes/3-tiling-drag1
-rw-r--r--src/click.c32
-rw-r--r--src/con.c26
-rw-r--r--src/tiling_drag.c380
-rw-r--r--src/util.c32
-rw-r--r--src/workspace.c4
-rw-r--r--testcases/t/316-drag-container.t321
13 files changed, 849 insertions, 23 deletions
diff --git a/docs/userguide b/docs/userguide
index e7ec29d6..7b233844 100644
--- a/docs/userguide
+++ b/docs/userguide
@@ -196,6 +196,48 @@ provided by the i3 https://github.com/i3/i3/blob/next/etc/config.keycodes[defaul
Floating windows are always on top of tiling windows.
+=== Moving tiling containers with the mouse
+
+Since i3 4.21, it's possible to drag tiling containers using the mouse. The
+drag can be initiated either by dragging the window's titlebar or by pressing
+the <<floating_modifier>> and dragging the container while holding the
+left-click button.
+
+Once the drag is initiated and the cursor has left the original container, drop
+indicators are created according to the position of the cursor relatively to
+the target container. These indicators help you understand what the resulting
+<<tree>> layout is going to be after you release the mouse button.
+
+The possible drop positions are:
+
+Drop on container::
+ This happens when the mouse is relatively near the center of a container.
+ If the mouse is released, the result is exactly as if you had run the
+ +move container to mark+ command. See <<move_to_mark>>.
+Drop as sibling::
+ This happens when the mouse is relatively near the edge of a container. If
+ the mouse is released, the dragged container will become a sibling of the
+ target container, placed left/right/up/down according to the position of
+ the indicator.
+ This might or might not create a new v-split or h-split according to the
+ previous layout of the target container. For example, if the target
+ container is in an h-split and you drop the dragged container below it, the
+ new layout will have to be a v-split.
+Drop to parent::
+ This happens when the mouse is relatively near the edge of a container (but
+ even closer to the border in comparison to the sibling case above) *and* if
+ that edge is also the parent container's edge. For example, if three
+ containers are in a horizontal layout then edges where this can happen is
+ the left edge of the left container, the right edge of the right container
+ and all bottom and top edges of all three containers.
+ If the mouse is released, the container is first dropped as a sibling to
+ the target container, like in the case above, and then is moved
+ directionally like with the +move left|right|down|up+ command. See
+ <<move_direction>>.
+
+The color of the indicator matches the +client.focused+ setting. See <<client_colors>>.
+
+[[tree]]
== Tree
i3 stores all information about the X11 outputs, workspaces and layout of the
@@ -1041,6 +1083,7 @@ workspace 5 output VGA1 LVDS1
workspace "2: vim" output VGA1
---------------------------
+[[client_colors]]
=== Changing colors
You can change all colors which i3 uses to draw the window decorations.
@@ -2253,6 +2296,7 @@ Note that you might not have a primary output configured yet. To do so, run:
xrandr --output <output> --primary
-------------------------
+[[move_direction]]
=== Moving containers
Use the +move+ command to move a container.
@@ -2537,6 +2581,7 @@ Note that you might not have a primary output configured yet. To do so, run:
xrandr --output <output> --primary
-------------------------
+[[move_to_mark]]
=== Moving containers/windows to marks
To move a container to another container with a specific mark (see <<vim_like_marks>>),
diff --git a/include/all.h b/include/all.h
index b9a1a7a9..5941b5e6 100644
--- a/include/all.h
+++ b/include/all.h
@@ -58,6 +58,7 @@
#include "match.h"
#include "xcursor.h"
#include "resize.h"
+#include "tiling_drag.h"
#include "sighandler.h"
#include "move.h"
#include "output.h"
diff --git a/include/con.h b/include/con.h
index 3cea6780..b8bff080 100644
--- a/include/con.h
+++ b/include/con.h
@@ -371,6 +371,7 @@ void con_move_to_output(Con *con, Output *output, bool fix_coordinates);
*/
bool con_move_to_output_name(Con *con, const char *name, bool fix_coordinates);
+bool con_move_to_target(Con *con, Con *target);
/**
* Moves the given container to the given mark.
*
diff --git a/include/tiling_drag.h b/include/tiling_drag.h
new file mode 100644
index 00000000..ab002d43
--- /dev/null
+++ b/include/tiling_drag.h
@@ -0,0 +1,16 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ * tiling_drag.h: Reposition tiled windows by dragging.
+ *
+ */
+#pragma once
+
+/**
+ * Initiates a mouse drag operation on a tiled window.
+ *
+ */
+void tiling_drag(Con *con, xcb_button_press_event_t *event);
diff --git a/include/util.h b/include/util.h
index 09ad941f..8525b6d9 100644
--- a/include/util.h
+++ b/include/util.h
@@ -183,3 +183,15 @@ position_t position_from_direction(direction_t direction);
*
*/
direction_t direction_from_orientation_position(orientation_t orientation, position_t position);
+
+/**
+ * Converts direction to a string representation.
+ *
+ */
+const char *direction_to_string(direction_t direction);
+
+/**
+ * Converts position to a string representation.
+ *
+ */
+const char *position_to_string(position_t position);
diff --git a/meson.build b/meson.build
index df2d86e0..5c6a6198 100644
--- a/meson.build
+++ b/meson.build
@@ -409,6 +409,7 @@ i3srcs = [
'src/sighandler.c',
'src/startup.c',
'src/sync.c',
+ 'src/tiling_drag.c',
'src/tree.c',
'src/util.c',
'src/version.c',
diff --git a/release-notes/changes/3-tiling-drag b/release-notes/changes/3-tiling-drag
new file mode 100644
index 00000000..195fcdbc
--- /dev/null
+++ b/release-notes/changes/3-tiling-drag
@@ -0,0 +1 @@
+Allow moving tiling windows with the mouse
diff --git a/src/click.c b/src/click.c
index df1c321c..10af756e 100644
--- a/src/click.c
+++ b/src/click.c
@@ -188,9 +188,6 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo
goto done;
}
- if (ws != focused_workspace)
- workspace_show(ws);
-
/* get the floating con */
Con *floatingcon = con_inside_floating(con);
const bool proportional = (event->state & XCB_KEY_BUT_MASK_SHIFT) == XCB_KEY_BUT_MASK_SHIFT;
@@ -218,7 +215,13 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo
goto done;
}
- /* 2: focus this con or one of its children. */
+ /* 2: floating modifier pressed, initiate a drag */
+ if (mod_pressed && event->detail == XCB_BUTTON_INDEX_1 && !floatingcon) {
+ tiling_drag(con, event);
+ goto done;
+ }
+
+ /* 3: focus this con or one of its children. */
Con *con_to_focus = con;
if (in_stacked && dest == CLICK_DECORATION) {
/* If the container is a tab/stacked container and the click happened
@@ -231,19 +234,22 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo
}
}
}
+ if (ws != focused_workspace) {
+ workspace_show(ws);
+ }
con_activate(con_to_focus);
- /* 3: For floating containers, we also want to raise them on click.
+ /* 4: For floating containers, we also want to raise them on click.
* We will skip handling events on floating cons in fullscreen mode */
Con *fs = con_get_fullscreen_covering_ws(ws);
if (floatingcon != NULL && fs != con) {
- /* 4: floating_modifier plus left mouse button drags */
+ /* 5: floating_modifier plus left mouse button drags */
if (mod_pressed && is_left_click) {
floating_drag_window(floatingcon, event, false);
return;
}
- /* 5: resize (floating) if this was a (left or right) click on the
+ /* 6: resize (floating) if this was a (left or right) click on the
* left/right/bottom border, or a right click on the decoration.
* also try resizing (tiling) if possible */
if (mod_pressed && is_right_click) {
@@ -272,7 +278,7 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo
return;
}
- /* 6: dragging, if this was a click on a decoration (which did not lead
+ /* 7: dragging, if this was a click on a decoration (which did not lead
* to a resize) */
if (dest == CLICK_DECORATION && is_left_click) {
floating_drag_window(floatingcon, event, !was_focused);
@@ -282,7 +288,13 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo
goto done;
}
- /* 7: floating modifier pressed, initiate a resize */
+ /* 8: floating modifier pressed, initiate a drag */
+ if ((mod_pressed || dest == CLICK_DECORATION) && event->detail == XCB_BUTTON_INDEX_1) {
+ tiling_drag(con, event);
+ goto done;
+ }
+
+ /* 9: floating modifier pressed, initiate a resize */
if (dest == CLICK_INSIDE && mod_pressed && is_right_click) {
if (floating_mod_on_tiled_client(con, event)) {
return;
@@ -293,7 +305,7 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo
xcb_flush(conn);
return;
}
- /* 8: otherwise, check for border/decoration clicks and resize */
+ /* 10: otherwise, check for border/decoration clicks and resize */
if ((dest == CLICK_BORDER || dest == CLICK_DECORATION) &&
is_left_or_right_click) {
DLOG("Trying to resize (tiling)\n");
diff --git a/src/con.c b/src/con.c
index 6de5cf0c..e7d18ff9 100644
--- a/src/con.c
+++ b/src/con.c
@@ -1389,17 +1389,7 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi
return true;
}
-/*
- * Moves the given container to the given mark.
- *
- */
-bool con_move_to_mark(Con *con, const char *mark) {
- Con *target = con_by_mark(mark);
- if (target == NULL) {
- DLOG("found no container with mark \"%s\"\n", mark);
- return false;
- }
-
+bool con_move_to_target(Con *con, Con *target) {
/* For target containers in the scratchpad, we just send the window to the scratchpad. */
if (con_get_workspace(target) == workspace_get("__i3_scratch")) {
DLOG("target container is in the scratchpad, moving container to scratchpad.\n");
@@ -1437,6 +1427,20 @@ bool con_move_to_mark(Con *con, const char *mark) {
}
/*
+ * Moves the given container to the given mark.
+ *
+ */
+bool con_move_to_mark(Con *con, const char *mark) {
+ Con *target = con_by_mark(mark);
+ if (target == NULL) {
+ DLOG("found no container with mark \"%s\"\n", mark);
+ return false;
+ }
+
+ return con_move_to_target(con, target);
+}
+
+/*
* Moves the given container to the currently focused container on the given
* workspace.
*
diff --git a/src/tiling_drag.c b/src/tiling_drag.c
new file mode 100644
index 00000000..83ce63c9
--- /dev/null
+++ b/src/tiling_drag.c
@@ -0,0 +1,380 @@
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ * tiling_drag.c: Reposition tiled windows by dragging.
+ *
+ */
+#include "all.h"
+static xcb_window_t create_drop_indicator(Rect rect);
+
+/*
+ * Includes decoration (container title) to the container's rect. This way we
+ * can find the correct drop target if the mouse is on a container's
+ * decoration.
+ *
+ */
+static Rect con_rect_plus_deco_height(Con *con) {
+ Rect rect = con->rect;
+ rect.height += con->deco_rect.height;
+ rect.y -= con->deco_rect.height;
+ return rect;
+}
+
+/*
+ * Return an appropriate target at given coordinates.
+ *
+ */
+static Con *find_drop_target(uint32_t x, uint32_t y) {
+ Con *con;
+ TAILQ_FOREACH (con, &all_cons, all_cons) {
+ Rect rect = con_rect_plus_deco_height(con);
+
+ if (rect_contains(rect, x, y) &&
+ con_has_managed_window(con) &&
+ !con_is_floating(con) &&
+ !con_is_hidden(con)) {
+ Con *ws = con_get_workspace(con);
+ if (!workspace_is_visible(ws)) {
+ continue;
+ }
+ Con *fs = con_get_fullscreen_covering_ws(ws);
+ return fs ? fs : con;
+ }
+ }
+
+ /* Couldn't find leaf container, get a workspace. */
+ Output *output = get_output_containing(x, y);
+ if (!output) {
+ return NULL;
+ }
+ Con *content = output_get_content(output->con);
+ /* Still descend because you can drag to the bar on an non-empty workspace. */
+ return con_descend_tiling_focused(content);
+}
+
+typedef enum { DT_SIBLING,
+ DT_CENTER,
+ DT_PARENT
+} drop_type_t;
+
+struct callback_params {
+ xcb_window_t *indicator;
+ Con **target;
+ direction_t *direction;
+ drop_type_t *drop_type;
+};
+
+static Rect adjust_rect(Rect rect, direction_t direction, uint32_t threshold) {
+ switch (direction) {
+ case D_LEFT:
+ rect.width = threshold;
+ break;
+ case D_UP:
+ rect.height = threshold;
+ break;
+ case D_RIGHT:
+ rect.x += (rect.width - threshold);
+ rect.width = threshold;
+ break;
+ case D_DOWN:
+ rect.y += (rect.height - threshold);
+ rect.height = threshold;
+ break;
+ }
+ return rect;
+}
+
+static bool con_on_side_of_parent(Con *con, direction_t direction) {
+ const orientation_t orientation = orientation_from_direction(direction);
+ direction_t reverse_direction;
+ switch (direction) {
+ case D_LEFT:
+ reverse_direction = D_RIGHT;
+ break;
+ case D_RIGHT:
+ reverse_direction = D_LEFT;
+ break;
+ case D_UP:
+ reverse_direction = D_DOWN;
+ break;
+ case D_DOWN:
+ reverse_direction = D_UP;
+ break;
+ }
+ return (con_orientation(con->parent) != orientation ||
+ con->parent->layout == L_STACKED || con->parent->layout == L_TABBED ||
+ con_descend_direction(con->parent, reverse_direction) == con);
+}
+
+/*
+ * The callback that is executed on every mouse move while dragging. On each
+ * invocation we determine the drop target and the direction in which to insert
+ * the dragged container. The indicator window is updated to show the new
+ * position of the dragged container. The target container and direction are
+ * passed out using the callback params.
+ *
+ */
+DRAGGING_CB(drag_callback) {
+ /* 30% of the container (minus the parent indicator) is used to drop the
+ * dragged container as a sibling to the target */
+ const double sibling_indicator_percent_of_rect = 0.3;
+ /* Use the base decoration height and add a few pixels. This makes the
+ * outer indicator generally thin but at least thick enough to cover
+ * container titles */
+ const double parent_indicator_max_size = render_deco_height() + logical_px(5);
+
+ Con *target = find_drop_target(new_x, new_y);
+ if (target == NULL) {
+ return;
+ }
+
+ Rect rect = con_rect_plus_deco_height(target);
+
+ direction_t direction = 0;
+ drop_type_t drop_type = DT_CENTER;
+ bool draw_window = true;
+ const struct callback_params *params = extra;
+
+ if (target->type == CT_WORKSPACE) {
+ goto create_indicator;
+ }
+
+ /* Define the thresholds in pixels. The drop type depends on the cursor
+ * position. */
+ const uint32_t min_rect_dimension = min(rect.width, rect.height);
+ const uint32_t sibling_indicator_size = max(logical_px(2), (uint32_t)(sibling_indicator_percent_of_rect * min_rect_dimension));
+ const uint32_t parent_indicator_size = min(
+ parent_indicator_max_size,
+ /* For small containers, start where the sibling indicator finishes.
+ * This is always at least 1 pixel. We use min() to not override the
+ * sibling indicator: */
+ sibling_indicator_size - 1);
+
+ /* Find which edge the cursor is closer to. */
+ const uint32_t d_left = new_x - rect.x;
+ const uint32_t d_top = new_y - rect.y;
+ const uint32_t d_right = rect.x + rect.width - new_x;
+ const uint32_t d_bottom = rect.y + rect.height - new_y;
+ const uint32_t d_min = min(min(d_left, d_right), min(d_top, d_bottom));
+ /* And move the container towards that direction. */
+ if (d_left == d_min) {
+ direction = D_LEFT;
+ } else if (d_top == d_min) {
+ direction = D_UP;
+ } else if (d_right == d_min) {
+ direction = D_RIGHT;
+ } else if (d_bottom == d_min) {
+ direction = D_DOWN;
+ } else {
+ /* Keep the compiler happy */
+ ELOG("min() is broken\n");
+ assert(false);
+ }
+ const bool target_parent = (d_min < parent_indicator_size &&
+ con_on_side_of_parent(target, direction));
+ const bool target_sibling = (d_min < sibling_indicator_size);
+ drop_type = target_parent ? DT_PARENT : (target_sibling ? DT_SIBLING : DT_CENTER);
+
+ /* target == con makes sense only when we are moving away from target's parent. */
+ if (drop_type != DT_PARENT && target == con) {
+ draw_window = false;
+ xcb_destroy_window(conn, *(params->indicator));
+ *(params->indicator) = 0;
+ goto create_indicator;
+ }
+
+ switch (drop_type) {
+ case DT_PARENT:
+ while (target->parent->type != CT_WORKSPACE && con_on_side_of_parent(target->parent, direction)) {
+ target = target->parent;
+ }
+ rect = adjust_rect(target->parent->rect, direction, parent_indicator_size);
+ break;
+ case DT_CENTER:
+ rect = target->rect;
+ rect.x += sibling_indicator_size;
+ rect.y += sibling_indicator_size;
+ rect.width -= sibling_indicator_size * 2;
+ rect.height -= sibling_indicator_size * 2;
+ break;
+ case DT_SIBLING:
+ rect = adjust_rect(target->rect, direction, sibling_indicator_size);
+ break;
+ }
+
+create_indicator:
+ if (draw_window) {
+ if (*(params->indicator) == 0) {
+ *(params->indicator) = create_drop_indicator(rect);
+ } else {
+ const uint32_t values[4] = {rect.x, rect.y, rect.width, rect.height};
+ const uint32_t mask = XCB_CONFIG_WINDOW_X |
+ XCB_CONFIG_WINDOW_Y |
+ XCB_CONFIG_WINDOW_WIDTH |
+ XCB_CONFIG_WINDOW_HEIGHT;
+ xcb_configure_window(conn, *(params->indicator), mask, values);
+ }
+ }
+ x_mask_event_mask(~XCB_EVENT_MASK_ENTER_WINDOW);
+ xcb_flush(conn);
+
+ *(params->target) = target;
+ *(params->direction) = direction;
+ *(params->drop_type) = drop_type;
+}
+
+/*
+ * Returns a new drop indicator window with the given initial coordinates.
+ *
+ */
+static xcb_window_t create_drop_indicator(Rect rect) {
+ uint32_t mask = 0;
+ uint32_t values[2];
+
+ mask = XCB_CW_BACK_PIXEL;
+ values[0] = config.client.focused.indicator.colorpixel;
+
+ mask |= XCB_CW_OVERRIDE_REDIRECT;
+ values[1] = 1;
+
+ xcb_window_t indicator = create_window(conn, rect, XCB_COPY_FROM_PARENT, XCB_COPY_FROM_PARENT,
+ XCB_WINDOW_CLASS_INPUT_OUTPUT, XCURSOR_CURSOR_MOVE, false, mask, values);
+ /* Change the window class to "i3-drag", so that it can be matched in a
+ * compositor configuration. Note that the class needs to be changed before
+ * mapping the window. */
+ xcb_change_property(conn,
+ XCB_PROP_MODE_REPLACE,
+ indicator,
+ XCB_ATOM_WM_CLASS,
+ XCB_ATOM_STRING,
+ 8,
+ (strlen("i3-drag") + 1) * 2,
+ "i3-drag\0i3-drag\0");
+ xcb_map_window(conn, indicator);
+ xcb_circulate_window(conn, XCB_CIRCULATE_RAISE_LOWEST, indicator);
+
+ return indicator;
+}
+
+/*
+ * Initiates a mouse drag operation on a tiled window.
+ *
+ */
+void tiling_drag(Con *con, xcb_button_press_event_t *event) {
+ DLOG("Start dragging tiled container: con = %p\n", con);
+ bool set_focus = (con == focused);
+ bool set_fs = con->fullscreen_mode != CF_NONE;
+
+ /* Don't change focus while dragging. */
+ x_mask_event_mask(~XCB_EVENT_MASK_ENTER_WINDOW);
+ xcb_flush(conn);
+
+ /* Indicate drop location while dragging. This blocks until the drag is completed. */
+ Con *target = NULL;
+ direction_t direction;
+ drop_type_t drop_type;
+ xcb_window_t indicator = 0;
+ const struct callback_params params = {&indicator, &target, &direction, &drop_type};
+
+ drag_result_t drag_result = drag_pointer(con, event, XCB_NONE, BORDER_TOP, XCURSOR_CURSOR_MOVE, drag_callback, &params);
+
+ /* Dragging is done. We don't need the indicator window any more. */
+ xcb_destroy_window(conn, indicator);
+
+ if (drag_result == DRAG_REVERT ||
+ target == NULL ||
+ (target == con && drop_type != DT_PARENT) ||
+ !con_exists(target)) {
+ DLOG("drop aborted\n");
+ return;
+ }
+
+ const orientation_t orientation = orientation_from_direction(direction);
+ const position_t position = position_from_direction(direction);
+ const layout_t layout = orientation == VERT ? L_SPLITV : L_SPLITH;
+ con_disable_fullscreen(con);
+ switch (drop_type) {
+ case DT_CENTER:
+ /* Also handles workspaces.*/
+ DLOG("drop to center of %p\n", target);
+ con_move_to_target(con, target);
+ break;
+ case DT_SIBLING:
+ DLOG("drop %s %p\n", position_to_string(position), target);
+ if (con_orientation(target->parent) != orientation) {
+ /* If con and target are the only children of the same parent, we can just change
+ * the parent's layout manually and then move con to the correct position.
+ * tree_split checks for a parent with only one child so it would create a new
+ * parent with the new layout. */
+ if (con->parent == target->parent && con_num_children(target->parent) == 2) {
+ target->parent->layout = layout;
+ } else {
+ tree_split(target, orientation);
+ }
+ }
+
+ insert_con_into(con, target, position);
+
+ ipc_send_window_event("move", con);
+ break;
+ case DT_PARENT: {
+ const bool parent_tabbed_or_stacked = (target->parent->layout == L_TABBED || target->parent->layout == L_STACKED);
+ DLOG("drop %s (%s) of %s%p\n",
+ direction_to_string(direction),
+ position_to_string(position),
+ parent_tabbed_or_stacked ? "tabbed/stacked " : "",
+ target);
+ if (parent_tabbed_or_stacked) {
+ /* When dealing with tabbed/stacked the target can be in the
+ * middle of the container. Thus, after a directional move, con
+ * will still be bound to the tabbed/stacked parent. */
+ if (position == BEFORE) {
+ target = TAILQ_FIRST(&(target->parent->nodes_head));
+ } else {
+ target = TAILQ_LAST(&(target->parent->nodes_head), nodes_head);
+ }
+ }
+ if (con != target) {
+ insert_con_into(con, target, position);
+ }
+ /* tree_move can change the focus */
+ Con *old_focus = focused;
+ tree_move(con, direction);
+ if (focused != old_focus) {
+ con_activate(old_focus);
+ }
+ break;
+ }
+ }
+ /* Warning: target might not exist anymore */
+ target = NULL;
+
+ /* Manage fullscreen status. */
+ if (set_focus || set_fs) {
+ Con *fs = con_get_fullscreen_covering_ws(con_get_workspace(con));
+ if (fs == con) {
+ ELOG("dragged container somehow got fullscreen again.\n");
+ assert(false);
+ } else if (fs && set_focus && set_fs) {
+ /* con was focused & fullscreen, disable other fullscreen container. */
+ con_disable_fullscreen(fs);
+ } else if (fs) {
+ /* con was not focused, prefer other fullscreen container. */
+ set_fs = set_focus = false;
+ } else if (!set_focus) {
+ /* con was not focused. If it was fullscreen and we are moving it to the focused
+ * workspace we must focus it. */
+ set_focus = (set_fs && con_get_workspace(focused) == con_get_workspace(con));
+ }
+ }
+ if (set_fs) {
+ con_enable_fullscreen(con, CF_OUTPUT);
+ }
+ if (set_focus) {
+ con_focus(con);
+ }
+ tree_render();
+}
diff --git a/src/util.c b/src/util.c
index f2c56222..7b148614 100644
--- a/src/util.c
+++ b/src/util.c
@@ -476,3 +476,35 @@ direction_t direction_from_orientation_position(orientation_t orientation, posit
return position == BEFORE ? D_UP : D_DOWN;
}
}
+
+/*
+ * Converts direction to a string representation.
+ *
+ */
+const char *direction_to_string(direction_t direction) {
+ switch (direction) {
+ case D_LEFT:
+ return "left";
+ case D_RIGHT:
+ return "right";
+ case D_UP:
+ return "up";
+ case D_DOWN:
+ return "down";
+ }
+ return "invalid";
+}
+
+/*
+ * Converts position to a string representation.
+ *
+ */
+const char *position_to_string(position_t position) {
+ switch (position) {
+ case BEFORE:
+ return "before";
+ case AFTER:
+ return "after";
+ }
+ return "invalid";
+}
diff --git a/src/workspace.c b/src/workspace.c
index 7a9d01e5..e1ac49d3 100644
--- a/src/workspace.c
+++ b/src/workspace.c
@@ -305,10 +305,10 @@ Con *create_workspace_on_output(Output *output, Con *content) {
*/
bool workspace_is_visible(Con *ws) {
Con *output = con_get_output(ws);
- if (output == NULL)
+ if (output == NULL) {
return false;
+ }
Con *fs = con_get_fullscreen_con(output, CF_OUTPUT);
- LOG("workspace visible? fs = %p, ws = %p\n", fs, ws);
return (fs == ws);
}
diff --git a/testcases/t/316-drag-container.t b/testcases/t/316-drag-container.t
new file mode 100644
index 00000000..7e2b8494
--- /dev/null
+++ b/testcases/t/316-drag-container.t
@@ -0,0 +1,321 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+#
+# Test dragging containers.
+
+my ($width, $height) = (1000, 500);
+
+my $config = <<"EOT";
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+focus_follows_mouse no
+floating_modifier Mod1
+
+# 2 side by side outputs
+fake-outputs ${width}x${height}+0+0P,${width}x${height}+${width}+0
+
+bar {
+ output primary
+}
+EOT
+use i3test i3_autostart => 0;
+use i3test::XTEST;
+my $pid = launch_with_config($config);
+
+sub start_drag {
+ my ($pos_x, $pos_y) = @_;
+ die "Drag outside of bounds!" unless $pos_x < $width * 2 && $pos_y < $height;
+
+ $x->root->warp_pointer($pos_x, $pos_y);
+ sync_with_i3;
+
+ xtest_key_press(64); # Alt_L
+ xtest_button_press(1, $pos_x, $pos_y);
+ xtest_sync_with_i3;
+}
+
+sub end_drag {
+ my ($pos_x, $pos_y) = @_;
+ die "Drag outside of bounds!" unless $pos_x < $width * 2 && $pos_y < $height;
+
+ $x->root->warp_pointer($pos_x, $pos_y);
+ sync_with_i3;
+
+ xtest_button_release(1, $pos_x, $pos_y);
+ xtest_key_release(64); # Alt_L
+ xtest_sync_with_i3;
+}
+
+my ($ws1, $ws2);
+my ($A, $B, $tmp);
+my ($A_id, $B_id);
+
+sub move_subtest {
+ my ($cb, $win) = @_;
+
+ my @events = events_for($cb, 'window');
+ my @move = grep { $_->{change} eq 'move' } @events;
+
+ is(scalar @move, 1, 'Received 1 window::move event');
+ is($move[0]->{container}->{window}, $A->{id}, "window id matches");
+}
+
+###############################################################################
+# Drag floating container onto an empty workspace.
+###############################################################################
+
+$ws2 = fresh_workspace(output => 1);
+$ws1 = fresh_workspace(output => 0);
+$A = open_floating_window(rect => [ 30, 30, 50, 50 ]);
+
+start_drag(40, 40);
+end_drag(1050, 50);
+
+is($x->input_focus, $A->id, 'Floating window moved to the right workspace');
+is($ws2, focused_ws, 'Empty workspace focused after floating window dragged to it');
+
+###############################################################################
+# Drag tiling container onto an empty workspace.
+###############################################################################
+
+subtest "Draging tiling container onto an empty workspace produces move event", \&move_subtest,
+sub {
+
+$ws2 = fresh_workspace(output => 1);
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+
+start_drag(50, 50);
+end_drag(1050, 50);
+
+is($x->input_focus, $A->id, 'Tiling window moved to the right workspace');
+is($ws2, focused_ws, 'Empty workspace focused after tiling window dragged to it');
+
+};
+
+###############################################################################
+# Drag tiling container onto a container that closes before the drag is
+# complete.
+###############################################################################
+
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+open_window;
+
+start_drag(600, 300); # Start dragging the second window.
+
+# Try to place it on the first window.
+$x->root->warp_pointer(50, 50);
+sync_with_i3;
+
+cmd '[id=' . $A->id . '] kill';
+sync_with_i3;
+end_drag(50, 50);
+
+is(@{get_ws_content($ws1)}, 1, 'One container left in ws1');
+
+###############################################################################
+# Drag tiling container onto a tiling container on an other workspace.
+###############################################################################
+
+subtest "Draging tiling container onto a tiling container on an other workspace produces move event", \&move_subtest,
+sub {
+
+$ws2 = fresh_workspace(output => 1);
+open_window;
+$B_id = get_focused($ws2);
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+$A_id = get_focused($ws1);
+
+start_drag(50, 50);
+end_drag(1500, 250); # Center of right output, inner region.
+
+is($ws2, focused_ws, 'Workspace focused after tiling window dragged to it');
+$ws2 = get_ws($ws2);
+is($ws2->{focus}[0], $A_id, 'A focused first, dragged container kept focus');
+is($ws2->{focus}[1], $B_id, 'B focused second');
+
+};
+
+###############################################################################
+# Drag tiling container onto a floating container on an other workspace.
+###############################################################################
+
+subtest "Draging tiling container onto a floating container on an other workspace produces move event", \&move_subtest,
+sub {
+
+$ws2 = fresh_workspace(output => 1);
+open_floating_window;
+$B_id = get_focused($ws2);
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+$A_id = get_focused($ws1);
+
+start_drag(50, 50);
+end_drag(1500, 250);
+
+is($ws2, focused_ws, 'Workspace with one floating container focused after tiling window dragged to it');
+$ws2 = get_ws($ws2);
+is($ws2->{focus}[0], $A_id, 'A focused first, dragged container kept focus');
+is($ws2->{floating_nodes}[0]->{nodes}[0]->{id}, $B_id, 'B exists & floating');
+
+};
+
+###############################################################################
+# Drag tiling container onto a bar.
+###############################################################################
+
+subtest "Draging tiling container onto a bar produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+open_window;
+$B_id = get_focused($ws1);
+$ws2 = fresh_workspace(output => 1);
+$A = open_window;
+$A_id = get_focused($ws2);
+
+start_drag(1500, 250);
+end_drag(1, 498); # Bar on bottom of left output.
+
+is($ws1, focused_ws, 'Workspace focused after tiling window dragged to its bar');
+$ws1 = get_ws($ws1);
+is($ws1->{focus}[0], $A_id, 'B focused first, dragged container kept focus');
+is($ws1->{focus}[1], $B_id, 'A focused second');
+
+};
+
+###############################################################################
+# Drag an unfocused tiling container onto it's self.
+###############################################################################
+
+$ws1 = fresh_workspace(output => 0);
+open_window;
+$A_id = get_focused($ws1);
+open_window;
+$B_id = get_focused($ws1);
+
+start_drag(50, 50);
+end_drag(450, 450);
+
+$ws1 = get_ws($ws1);
+is($ws1->{focus}[0], $B_id, 'B focused first, kept focus');
+is($ws1->{focus}[1], $A_id, 'A focused second, unfocused dragged container didn\'t gain focus');
+
+###############################################################################
+# Drag an unfocused tiling container onto an occupied workspace.
+###############################################################################
+
+subtest "Draging unfocused tiling container onto an occupied workspace produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+$A_id = get_focused($ws1);
+$ws2 = fresh_workspace(output => 1);
+open_window;
+$B_id = get_focused($ws2);
+
+start_drag(50, 50);
+end_drag(1500, 250); # Center of right output, inner region.
+
+is($ws2, focused_ws, 'Workspace remained focused after dragging unfocused container');
+$ws2 = get_ws($ws2);
+is($ws2->{focus}[0], $B_id, 'B focused first, kept focus');
+is($ws2->{focus}[1], $A_id, 'A focused second, unfocused container didn\'t steal focus');
+
+};
+
+###############################################################################
+# Drag fullscreen container onto window in same workspace.
+###############################################################################
+
+$ws1 = fresh_workspace(output => 0);
+open_window;
+$A = open_window;
+cmd 'fullscreen enable';
+
+start_drag(900, 100); # Second window
+end_drag(50, 50); # To first window
+
+is($ws1, focused_ws, 'Workspace remained focused after dragging fullscreen container');
+is_num_fullscreen($ws1, 1, 'Container still fullscreened');
+is($x->input_focus, $A->id, 'Fullscreen container still focused');
+
+###############################################################################
+# Drag unfocused fullscreen container onto window in other workspace.
+###############################################################################
+
+subtest "Draging unfocused fullscreen container onto window in other workspace produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+$A = open_window;
+cmd 'fullscreen enable';
+$ws2 = fresh_workspace(output => 1);
+open_window;
+open_window;
+
+start_drag(900, 100);
+end_drag(1000 + 500 * 0.15 + 10, 200); # left of leftmost window
+
+is($ws2, focused_ws, 'Workspace still focused after dragging fullscreen container to it');
+is_num_fullscreen($ws1, 0, 'No fullscreen container in first workspace');
+is_num_fullscreen($ws2, 1, 'Moved container still fullscreened');
+is($x->input_focus, $A->id, 'Fullscreen container now focused');
+$ws2 = get_ws($ws2);
+is($ws2->{nodes}->[0]->{window}, $A->id, 'Fullscreen container now leftmost window in second workspace');
+
+};
+
+###############################################################################
+# Drag unfocused fullscreen container onto left outter region of window in
+# other workspace. The container shouldn't end up in $ws2 because it was
+# dragged onto the outter region of the leftmost window. We must also check
+# that the focus remains on the other window.
+###############################################################################
+
+subtest "Draging unfocused fullscreen container onto left outter region of window in other workspace produces move event", \&move_subtest,
+sub {
+
+$ws1 = fresh_workspace(output => 0);
+open_window for (1..3);
+$A = open_window;
+$tmp = get_focused($ws1);
+cmd 'fullscreen enable';
+$ws2 = fresh_workspace(output => 1);
+$B = open_window;
+
+start_drag(990, 100); # rightmost of $ws1
+end_drag(1004, 100); # outter region of window of $ws2
+
+is($ws2, focused_ws, 'Workspace still focused after dragging fullscreen container to it');
+is_num_fullscreen($ws1, 1, 'Fullscreen container still in first workspace');
+is_num_fullscreen($ws2, 0, 'No fullscreen container in second workspace');
+is($x->input_focus, $B->id, 'Window of second workspace still has focus');
+is(get_focused($ws1), $tmp, 'Fullscreen container still focused in first workspace');
+$ws1 = get_ws($ws1);
+is($ws1->{nodes}->[3]->{window}, $A->id, 'Fullscreen container still rightmost window in first workspace');
+
+};
+
+exit_gracefully($pid);
+
+###############################################################################
+
+done_testing;