diff --git a/RELEASE-NOTES-4.21 b/RELEASE-NOTES-4.21 deleted file mode 100644 index 9d4c708e7..000000000 --- a/RELEASE-NOTES-4.21 +++ /dev/null @@ -1,74 +0,0 @@ - - ┌──────────────────────────────┐ - │ Release notes for i3 v4.21 │ - └──────────────────────────────┘ - -This is i3 v4.21. This version is considered stable. All users of i3 are -strongly encouraged to upgrade. - -The biggest change in this release is that you can now drag tiling windows -with your mouse (floating windows could already be dragged). For more details -on how to use this feature, please refer to the userguide: - -https://i3wm.org/docs/userguide.html#_moving_tiling_containers_with_the_mouse - -A big thank you goes out to our core i3 developer Orestis Floros who made this -feature possible, based on previous work from Michael Forster and Tony Crisci! - - ┌────────────────────────────┐ - │ Changes in i3 v4.21 │ - └────────────────────────────┘ - - • Allow dragging tiling windows with the mouse - • Add client.focused_tab_title color option - • Add support for multiple output names in the focus command, - allowing users to cycle focus between e.g. VGA1 and LVDS1 but not DVI0. - • Add a toggle option to the title_window_icon command - • i3 switched from the obsolete PCRE 8.x regular expression matching - library to the current PCRE2 10.x version. - - ┌────────────────────────────┐ - │ Bugfixes │ - └────────────────────────────┘ - - • docs/ipc: document all window_type values - • docs/userguide: clarify the difference between the “workspace N” and - “workspace number N” commands - • i3bar: fix default font not being applied to bars if defined after bar block - • i3-dmenu-desktop: add backslashes for the exec command, - which fixes opening some .desktop files (e.g. electrum) - • i3-sensible-pager: sanitize LESS environment variable to remove -E or -F - • testsuite: catch i3 crashes instead of hanging on crash - • Fix logging on machines with 256 GB of RAM - • Do not replace existing IPC socket on start, to prevent clobbering - the IPC socket when running i3 within i3 (e.g. in Xepyhr, for development) - • Refuse to start without a valid IPC socket - • Fix focus when moving container between outputs with mouse warp and - focus_follows_mouse - • Fix endless loop with transient_for windows - • Fix wrong “failed” IPC reply on move workspace to output - • Fix WM registration selection (from WM_S_S to WM_S) - • avoid graphics artifacts when changing the layout tree by - initializing surfaces to all black - • update parent split con titles when child container swaps position with - another child container - • Fix segfault if command in bindsym is empty - • Fix segfault with explicit mode "default" key bindings - • Fix crash if config contains nested variables. - • strip trailing whitespace in bar output names - • Fix crash with long commands - • Fix changing borders by restoring BS_NORMAL _MOTIF_WM_HINTS correctly - - ┌────────────────────────────┐ - │ Thanks! │ - └────────────────────────────┘ - -Thanks for testing, bugfixes, discussions and everything I forgot go out to: - -André Silva, Anton Älgmyr, Baptiste Daroussin, bodea, Chris Templin, George -Rodrigues, Gergely Risko, Ingo Bürk, Jakob Haufe, Jay Ta'ala, Jeff Smith, Jonta, -Josh Soref, Kjetil Torgrim Homme, lycurgus, mariano, Michael Forster, Orestis -Floros, paperluigis, Peder Stray, rvalieris, sergio, Tony Crisci, takelley1, Uli -Schlachter, viri, zhiv-git, zhrvn - --- Michael Stapelberg, 2021-10-19 diff --git a/RELEASE-NOTES-4.21.1 b/RELEASE-NOTES-4.21.1 new file mode 100644 index 000000000..4f8a9000c --- /dev/null +++ b/RELEASE-NOTES-4.21.1 @@ -0,0 +1,44 @@ + + ┌──────────────────────────────┐ + │ Release notes for i3 v4.21.1 │ + └──────────────────────────────┘ + +This is i3 v4.21.1. This version is considered stable. All users of i3 are +strongly encouraged to upgrade. + +This release fixes a few rough edges with regards to the newly-introduced +tiling drag feature, which is now configurable: +https://i3wm.org/docs/userguide.html#config_tiling_drag + + ┌────────────────────────────┐ + │ Changes in i3 v4.21.1 │ + └────────────────────────────┘ + + • tiling drag: allow configuration + • tiling drag: allow click immediately, to focus on decoration click + • tiling drag: fix cursor (wrong argument passed) + • tiling drag: increase drag threshold, run it through logical_px + • tiling drag: left-click needs threshold, mod-click doesn’t + • tiling drag: ignore scratchpad windows when locating drop targets + • tiling drag: only start when there are drop targets + • Raise floating windows when their border is clicked + + ┌────────────────────────────┐ + │ Bugfixes │ + └────────────────────────────┘ + + • docs/ipc: document sticky field of GET_TREE + • man/i3-config-wizard: escape ~ to prevent interpretation as subscript + • Motif hints: respect maximum border style configuration set by user + • i3-dmenu-desktop: fix quoting bug + • Fix segfault during config validation + + ┌────────────────────────────┐ + │ Thanks! │ + └────────────────────────────┘ + +Thanks for testing, bugfixes, discussions and everything I forgot go out to: + + Erich Heine, Matias Goldfeld, Orestis Floros, Tudor Brindus, bodea + +-- Michael Stapelberg, 2022-10-24 diff --git a/docs/userguide b/docs/userguide index 155a93c7d..d1bb55759 100644 --- a/docs/userguide +++ b/docs/userguide @@ -196,6 +196,7 @@ provided by the i3 https://github.com/i3/i3/blob/next/etc/config.keycodes[defaul Floating windows are always on top of tiling windows. +[[tiling_drag]] === Moving tiling containers with the mouse Since i3 4.21, it's possible to drag tiling containers using the mouse. The @@ -1402,6 +1403,29 @@ fullscreen toggle bindsym Mod1+F fullscreen toggle ------------------- +[[config_tiling_drag]] +=== Tiling drag + +You can configure how to initiate the tiling drag feature (see <>). + +*Syntax*: +-------------------------------- +tiling_drag off +tiling_drag modifier|titlebar [modifier|titlebar] +-------------------------------- + +*Examples*: +-------------------------------- +# Only initiate a tiling drag when the modifier is held: +tiling_drag modifier + +# Initiate a tiling drag on either titlebar click or held modifier: +tiling_drag modifier titlebar + +# Disable tiling drag altogether +tiling_drag off +-------------------------------- + == Configuring i3bar The bar at the bottom of your monitor is drawn by a separate process called diff --git a/etc/config b/etc/config index 51902f080..40076ca95 100644 --- a/etc/config +++ b/etc/config @@ -49,6 +49,10 @@ set $right semicolon # use Mouse+Mod1 to drag floating windows to their wanted position floating_modifier Mod1 +# move tiling windows via drag & drop by left-clicking into the title bar, +# or left-clicking anywhere into the window while holding the floating modifier. +tiling_drag modifier titlebar + # start a terminal bindsym Mod1+Return exec i3-sensible-terminal diff --git a/etc/config.keycodes b/etc/config.keycodes index 11a7d46fd..7bc5d60a7 100644 --- a/etc/config.keycodes +++ b/etc/config.keycodes @@ -43,6 +43,10 @@ bindsym XF86AudioMicMute exec --no-startup-id pactl set-source-mute @DEFAULT_SOU # Use Mouse+$mod to drag floating windows to their wanted position floating_modifier $mod +# move tiling windows via drag & drop by left-clicking into the title bar, +# or left-clicking anywhere into the window while holding the floating modifier. +tiling_drag modifier titlebar + # start a terminal bindcode $mod+36 exec i3-sensible-terminal diff --git a/i3-dmenu-desktop b/i3-dmenu-desktop index e43d95aaf..bb693350d 100755 --- a/i3-dmenu-desktop +++ b/i3-dmenu-desktop @@ -413,7 +413,7 @@ my $exec = $app->{Exec}; my $location = $app->{_Location}; # Quote as described by “The Exec key”: -# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html +# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html sub quote { my ($str) = @_; $str =~ s/("|`|\$|\\)/\\$1/g; @@ -425,6 +425,17 @@ $choice = quote($choice); $location = quote($location); $name = quote($name); +# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html: +# +# Note that the general escape rule for values of type string states that the +# backslash character can be escaped as ("\\") as well and that this escape rule +# is applied before the quoting rule. As such, to unambiguously represent a +# literal backslash character in a quoted argument in a desktop entry file +# requires the use of four successive backslash characters ("\\\\"). Likewise, a +# literal dollar sign in a quoted argument in a desktop entry file is +# unambiguously represented with ("\\$"). +$exec =~ s/\\\\/\\/g; + # Remove deprecated field codes, as the spec dictates. $exec =~ s/%[dDnNvm]//g; @@ -481,9 +492,10 @@ EOT # starts with a double quote ("), everything is parsed as-is until the next # double quote which is NOT preceded by a backslash (\). # - # Therefore, we escape all double quotes (") by replacing them with \" - $exec =~ s/\\"/\\\\\\"/g; - $exec =~ s/([^\\])"/$1\\"/g; + # Therefore, we escape all double quotes (") by replacing them with \". + # To not change the meaning of any double quote, backslashes need to be + # escaped as well. + $exec =~ s/(["\\])/\\$1/g; if (exists($app->{StartupNotify}) && !$app->{StartupNotify}) { $nosn = '--no-startup-id'; diff --git a/include/con.h b/include/con.h index b89d1177f..6577c2d6f 100644 --- a/include/con.h +++ b/include/con.h @@ -451,7 +451,7 @@ int con_border_style(Con *con); * floating window. * */ -void con_set_border_style(Con *con, int border_style, int border_width); +void con_set_border_style(Con *con, border_style_t border_style, int border_width); /** * This function changes the layout of a given container. Use it to handle diff --git a/include/config_directives.h b/include/config_directives.h index c6a600619..1a996cf14 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -68,6 +68,7 @@ CFGFUN(assign, const char *workspace, bool is_number); CFGFUN(no_focus); CFGFUN(ipc_socket, const char *path); CFGFUN(ipc_kill_timeout, const long timeout_ms); +CFGFUN(tiling_drag, const char *value); CFGFUN(restart_state, const char *path); CFGFUN(popup_during_fullscreen, const char *value); CFGFUN(color, const char *colorclass, const char *border, const char *background, const char *text, const char *indicator, const char *child_border); diff --git a/include/configuration.h b/include/configuration.h index 28bcaa42b..8d3b60d95 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -14,6 +14,7 @@ #include "queue.h" #include "i3.h" +#include "tiling_drag.h" typedef struct IncludedFile IncludedFile; typedef struct Config Config; @@ -266,6 +267,8 @@ struct Config { /* The number of currently parsed barconfigs */ int number_barconfigs; + tiling_drag_t tiling_drag; + /* Gap sizes */ gaps_t gaps; diff --git a/include/data.h b/include/data.h index 0dd097f3d..67a573c24 100644 --- a/include/data.h +++ b/include/data.h @@ -62,9 +62,11 @@ typedef enum { NO_ORIENTATION = 0, VERT } orientation_t; typedef enum { BEFORE, AFTER } position_t; -typedef enum { BS_NORMAL = 0, - BS_NONE = 1, - BS_PIXEL = 2 } border_style_t; +typedef enum { + BS_NONE = 0, + BS_PIXEL = 1, + BS_NORMAL = 2, +} border_style_t; /** parameter to specify whether tree_close_internal() and x_window_kill() should kill * only this specific window or the whole X11 client */ @@ -743,7 +745,16 @@ struct Con { * layout in workspace_layout and creates a new split container with that * layout whenever a new container is attached to the workspace. */ layout_t layout, last_split_layout, workspace_layout; + border_style_t border_style; + /* When the border style of a con changes because of motif hints, we don't + * want to set more decoration that the user wants. The user's preference is determined by these: + * 1. For new tiling windows, as set by `default_border` + * 2. For new floating windows, as set by `default_floating_border` + * 3. For all windows that the user runs the `border` command, whatever is + * the result of that command for that window. */ + border_style_t max_user_border_style; + /** floating? (= not in tiling layout) This cannot be simply a bool * because we want to keep track of whether the status was set by the * application (by setting _NET_WM_WINDOW_TYPE appropriately) or by the diff --git a/include/tiling_drag.h b/include/tiling_drag.h index ab002d432..3091b734e 100644 --- a/include/tiling_drag.h +++ b/include/tiling_drag.h @@ -9,8 +9,27 @@ */ #pragma once +#include "all.h" + +/** + * Tiling drag initiation modes. + */ +typedef enum { + TILING_DRAG_OFF = 0, + TILING_DRAG_MODIFIER = 1, + TILING_DRAG_TITLEBAR = 2, + TILING_DRAG_MODIFIER_OR_TITLEBAR = 3 +} tiling_drag_t; + +/** + * Returns whether there currently are any drop targets. + * Used to only initiate a drag when there is something to drop onto. + * + */ +bool has_drop_targets(void); + /** * Initiates a mouse drag operation on a tiled window. * */ -void tiling_drag(Con *con, xcb_button_press_event_t *event); +void tiling_drag(Con *con, xcb_button_press_event_t *event, bool use_threshold); diff --git a/man/i3-config-wizard.man b/man/i3-config-wizard.man index e8cce0078..8e8486dc2 100644 --- a/man/i3-config-wizard.man +++ b/man/i3-config-wizard.man @@ -35,7 +35,7 @@ used. == DESCRIPTION -i3-config-wizard is started by i3 in its default config, unless ~/.i3/config +i3-config-wizard is started by i3 in its default config, unless \~/.i3/config exists. i3-config-wizard creates a keysym based i3 config file (based on /etc/i3/config.keycodes) in ~/.i3/config. diff --git a/meson.build b/meson.build index 8c139c2a8..8815240a6 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,7 @@ project( 'i3', 'c', - version: '4.21', + version: '4.21.1', default_options: [ 'c_std=c11', 'warning_level=1', # enable all warnings (-Wall) diff --git a/parser-specs/config.spec b/parser-specs/config.spec index 0c90994fd..a0ac987fb 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -56,6 +56,7 @@ state INITIAL: 'ipc_kill_timeout' -> IPC_KILL_TIMEOUT 'restart_state' -> RESTART_STATE 'popup_during_fullscreen' -> POPUP_DURING_FULLSCREEN + 'tiling_drag' -> TILING_DRAG exectype = 'exec_always', 'exec' -> EXEC colorclass = 'client.background' -> COLOR_SINGLE @@ -362,6 +363,18 @@ state POPUP_DURING_FULLSCREEN: value = 'ignore', 'leave_fullscreen', 'smart' -> call cfg_popup_during_fullscreen($value) +state TILING_DRAG_MODE: + value = 'modifier', 'titlebar' + -> + end + -> call cfg_tiling_drag($value) + +state TILING_DRAG: + off = '0', 'no', 'false', 'off', 'disable', 'inactive' + -> call cfg_tiling_drag($off) + value = 'modifier', 'titlebar' + -> TILING_DRAG_MODE + # client.background state COLOR_SINGLE: color = word diff --git a/release-notes/bugfixes/1-motifs b/release-notes/bugfixes/1-motifs new file mode 100644 index 000000000..b5ae083e7 --- /dev/null +++ b/release-notes/bugfixes/1-motifs @@ -0,0 +1 @@ +motif hints: respect maximum border style configuration set by user diff --git a/release-notes/bugfixes/2-focus-click b/release-notes/bugfixes/2-focus-click new file mode 100644 index 000000000..30c096f4e --- /dev/null +++ b/release-notes/bugfixes/2-focus-click @@ -0,0 +1 @@ +tiling drag: allow click immediately, to focus on decoration click diff --git a/release-notes/bugfixes/3-drag-cursor b/release-notes/bugfixes/3-drag-cursor new file mode 100644 index 000000000..1902dc28d --- /dev/null +++ b/release-notes/bugfixes/3-drag-cursor @@ -0,0 +1 @@ +fix tiling drag cursor: should be “move”, accidentally was “top right corner” diff --git a/release-notes/bugfixes/4-drop-scratchpad b/release-notes/bugfixes/4-drop-scratchpad new file mode 100644 index 000000000..559abd18d --- /dev/null +++ b/release-notes/bugfixes/4-drop-scratchpad @@ -0,0 +1 @@ +tiling drag: ignore scratchpad windows when locating drop targets diff --git a/release-notes/changes/1-tiling-drag b/release-notes/changes/1-tiling-drag new file mode 100644 index 000000000..bc16308bf --- /dev/null +++ b/release-notes/changes/1-tiling-drag @@ -0,0 +1 @@ +tiling drag is now configurable, and defaults to “modifier” for existing configs diff --git a/release-notes/changes/2-tiling-drag-targets b/release-notes/changes/2-tiling-drag-targets new file mode 100644 index 000000000..9981a1997 --- /dev/null +++ b/release-notes/changes/2-tiling-drag-targets @@ -0,0 +1 @@ +tiling drag: only initiate when there are drop targets diff --git a/src/click.c b/src/click.c index 10af756e2..e262516f6 100644 --- a/src/click.c +++ b/src/click.c @@ -141,6 +141,12 @@ static bool tiling_resize(Con *con, xcb_button_press_event_t *event, const click return false; } +static void allow_replay_pointer(xcb_timestamp_t time) { + xcb_allow_events(conn, XCB_ALLOW_REPLAY_POINTER, time); + xcb_flush(conn); + tree_render(); +} + /* * Being called by handle_button_press, this function calls the appropriate * functions for resizing/dragging. @@ -152,8 +158,10 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo DLOG("type = %d, name = %s\n", con->type, con->name); /* don’t handle dockarea cons, they must not be focused */ - if (con->parent->type == CT_DOCKAREA) - goto done; + if (con->parent->type == CT_DOCKAREA) { + allow_replay_pointer(event->time); + return; + } /* if the user has bound an action to this click, it should override the * default behavior. */ @@ -173,7 +181,8 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo /* There is no default behavior for button release events so we are done. */ if (event->response_type == XCB_BUTTON_RELEASE) { - goto done; + allow_replay_pointer(event->time); + return; } /* Any click in a workspace should focus that workspace. If the @@ -184,8 +193,10 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo if (!ws) { ws = TAILQ_FIRST(&(output_get_content(con_get_output(con))->focus_head)); - if (!ws) - goto done; + if (!ws) { + allow_replay_pointer(event->time); + return; + } } /* get the floating con */ @@ -212,13 +223,19 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo Con *next = get_tree_next_sibling(current, direction); con_activate(con_descend_focused(next ? next : current)); - goto done; + allow_replay_pointer(event->time); + return; } /* 2: floating modifier pressed, initiate a drag */ - if (mod_pressed && event->detail == XCB_BUTTON_INDEX_1 && !floatingcon) { - tiling_drag(con, event); - goto done; + if (mod_pressed && is_left_click && !floatingcon && + (config.tiling_drag == TILING_DRAG_MODIFIER || + config.tiling_drag == TILING_DRAG_MODIFIER_OR_TITLEBAR) && + has_drop_targets()) { + const bool use_threshold = !mod_pressed; + tiling_drag(con, event, use_threshold); + allow_replay_pointer(event->time); + return; } /* 3: focus this con or one of its children. */ @@ -262,8 +279,10 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo is_left_or_right_click) { /* try tiling resize, but continue if it doesn’t work */ DLOG("tiling resize with fallback\n"); - if (tiling_resize(con, event, dest, dest == CLICK_DECORATION && !was_focused)) - goto done; + if (tiling_resize(con, event, dest, dest == CLICK_DECORATION && !was_focused)) { + allow_replay_pointer(event->time); + return; + } } if (dest == CLICK_DECORATION && is_right_click) { @@ -285,13 +304,20 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo return; } - goto done; + allow_replay_pointer(event->time); + return; } - /* 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; + /* 8: floating modifier pressed, or click in titlebar, initiate a drag */ + if (is_left_click && + ((config.tiling_drag == TILING_DRAG_TITLEBAR && dest == CLICK_DECORATION) || + (config.tiling_drag == TILING_DRAG_MODIFIER_OR_TITLEBAR && + (mod_pressed || dest == CLICK_DECORATION))) && + has_drop_targets()) { + allow_replay_pointer(event->time); + const bool use_threshold = !mod_pressed; + tiling_drag(con, event, use_threshold); + return; } /* 9: floating modifier pressed, initiate a resize */ @@ -312,10 +338,7 @@ static void route_click(Con *con, xcb_button_press_event_t *event, const bool mo tiling_resize(con, event, dest, dest == CLICK_DECORATION && !was_focused); } -done: - xcb_allow_events(conn, XCB_ALLOW_REPLAY_POINTER, event->time); - xcb_flush(conn); - tree_render(); + allow_replay_pointer(event->time); } /* diff --git a/src/commands.c b/src/commands.c index 07c6132ec..52c3fcd1e 100644 --- a/src/commands.c +++ b/src/commands.c @@ -747,6 +747,8 @@ void cmd_border(I3_CMD, const char *border_style_str, long border_width) { return; } + /* User changed the border */ + current->con->max_user_border_style = border_style; const int con_border_width = border_width_from_style(border_style, border_width, current->con); con_set_border_style(current->con, border_style, con_border_width); } diff --git a/src/con.c b/src/con.c index 996d6f3b9..51fdab918 100644 --- a/src/con.c +++ b/src/con.c @@ -41,7 +41,7 @@ Con *con_new_skeleton(Con *parent, i3Window *window) { TAILQ_INSERT_TAIL(&all_cons, new, all_cons); new->type = CT_CON; new->window = window; - new->border_style = config.default_border; + new->border_style = new->max_user_border_style = config.default_border; new->current_border_width = -1; new->window_icon_padding = -1; if (window) { @@ -1809,7 +1809,11 @@ int con_border_style(Con *con) { * floating window. * */ -void con_set_border_style(Con *con, int border_style, int border_width) { +void con_set_border_style(Con *con, border_style_t border_style, int border_width) { + if (border_style > con->max_user_border_style) { + border_style = con->max_user_border_style; + } + /* Handle the simple case: non-floating containerns */ if (!con_is_floating(con)) { con->border_style = border_style; @@ -1822,8 +1826,6 @@ void con_set_border_style(Con *con, int border_style, int border_width) { * con->rect represent the absolute position of the window (same for * parent). Then, we change the border style and subtract the new border * pixels. For the parent, we do the same also for the decoration. */ - DLOG("This is a floating container\n"); - Con *parent = con->parent; Rect bsr = con_border_style_rect(con); int deco_height = (con->border_style == BS_NORMAL ? render_deco_height() : 0); diff --git a/src/config.c b/src/config.c index 3b96269f0..7cbdad1f1 100644 --- a/src/config.c +++ b/src/config.c @@ -228,6 +228,8 @@ bool load_configuration(const char *override_configpath, config_load_t load_type config.focus_wrapping = FOCUS_WRAPPING_ON; + config.tiling_drag = TILING_DRAG_MODIFIER; + FREE(current_configpath); current_configpath = get_config_path(override_configpath, true); if (current_configpath == NULL) { @@ -281,11 +283,13 @@ bool load_configuration(const char *override_configpath, config_load_t load_type /* Make bar config blocks without a configured font use the i3-wide font. */ Barconfig *current; - TAILQ_FOREACH (current, &barconfigs, configs) { - if (current->font != NULL) { - continue; + if (load_type != C_VALIDATE) { + TAILQ_FOREACH (current, &barconfigs, configs) { + if (current->font != NULL) { + continue; + } + current->font = sstrdup(config.font.pattern); } - current->font = sstrdup(config.font.pattern); } if (load_type == C_RELOAD) { diff --git a/src/config_directives.c b/src/config_directives.c index 0fcee6888..4fd73297e 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -718,6 +718,20 @@ CFGFUN(ipc_kill_timeout, const long timeout_ms) { ipc_set_kill_timeout(timeout_ms / 1000.0); } +CFGFUN(tiling_drag, const char *value) { + if (strcmp(value, "modifier") == 0) { + config.tiling_drag = TILING_DRAG_MODIFIER; + } else if (strcmp(value, "titlebar") == 0) { + config.tiling_drag = TILING_DRAG_TITLEBAR; + } else if (strcmp(value, "modifier,titlebar") == 0 || + strcmp(value, "titlebar,modifier") == 0) { + /* Switch the above to strtok() or similar if we ever grow more options */ + config.tiling_drag = TILING_DRAG_MODIFIER_OR_TITLEBAR; + } else { + config.tiling_drag = TILING_DRAG_OFF; + } +} + /******************************************************************************* * Bar configuration (i3bar) ******************************************************************************/ diff --git a/src/drag.c b/src/drag.c index 67ccff405..582dbb171 100644 --- a/src/drag.c +++ b/src/drag.c @@ -42,7 +42,8 @@ struct drag_x11_cb { static bool threshold_exceeded(uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2) { - const uint32_t threshold = 9; + /* The threshold is about the height of one window decoration. */ + const uint32_t threshold = logical_px(15); return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) > threshold * threshold; } diff --git a/src/floating.c b/src/floating.c index cb31f8c55..5eaf639ce 100644 --- a/src/floating.c +++ b/src/floating.c @@ -347,8 +347,9 @@ bool floating_enable(Con *con, bool automatic) { con->floating = FLOATING_USER_ON; /* 4: set the border style as specified with new_float */ - if (automatic) - con->border_style = config.default_floating_border; + if (automatic) { + con->border_style = con->max_user_border_style = config.default_floating_border; + } /* Add pixels for the decoration. */ Rect border_style_rect = con_border_style_rect(con); @@ -698,6 +699,10 @@ void floating_resize_window(Con *con, const bool proportional, const xcb_button_press_event_t *event) { DLOG("floating_resize_window\n"); + /* Push changes before resizing, so that the window gets raised now and not + * after the user releases the mouse button */ + tree_render(); + /* corner saves the nearest corner to the original click. It contains * a bitmask of the nearest borders (BORDER_LEFT, BORDER_RIGHT, …) */ border_t corner = 0; diff --git a/src/manage.c b/src/manage.c index add6c8491..6125af34d 100644 --- a/src/manage.c +++ b/src/manage.c @@ -511,6 +511,13 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki if (nc->geometry.width == 0) nc->geometry = (Rect){geom->x, geom->y, geom->width, geom->height}; + if (want_floating) { + DLOG("geometry = %d x %d\n", nc->geometry.width, nc->geometry.height); + if (floating_enable(nc, true)) { + nc->floating = FLOATING_AUTO_ON; + } + } + if (has_mwm_hints) { DLOG("MOTIF_WM_HINTS specifies decorations (border_style = %d)\n", motif_border_style); if (want_floating) { @@ -520,17 +527,6 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki } } - if (want_floating) { - DLOG("geometry = %d x %d\n", nc->geometry.width, nc->geometry.height); - /* automatically set the border to the default value if a motif border - * was not specified */ - bool automatic_border = (motif_border_style == BS_NORMAL); - - if (floating_enable(nc, automatic_border)) { - nc->floating = FLOATING_AUTO_ON; - } - } - /* explicitly set the border width to the default */ if (nc->current_border_width == -1) { nc->current_border_width = (want_floating ? config.default_floating_border_width : config.default_border_width); diff --git a/src/tiling_drag.c b/src/tiling_drag.c index cce91f298..a35a66e62 100644 --- a/src/tiling_drag.c +++ b/src/tiling_drag.c @@ -27,6 +27,62 @@ static Rect con_rect_plus_deco_height(Con *con) { return rect; } +static bool is_tiling_drop_target(Con *con) { + if (!con_has_managed_window(con) || + con_is_floating(con) || + con_is_hidden(con)) { + return false; + } + Con *ws = con_get_workspace(con); + if (con_is_internal(ws)) { + /* Skip containers on i3-internal containers like the scratchpad, which are + technically visible on their pseudo-output. */ + return false; + } + if (!workspace_is_visible(ws)) { + return false; + } + Con *fs = con_get_fullscreen_covering_ws(ws); + if (fs != NULL && fs != con) { + /* Workspace is visible, but con is not visible because some other + container is in fullscreen. */ + return false; + } + return true; +} + +/* + * Returns whether there currently are any drop targets. + * Used to only initiate a drag when there is something to drop onto. + * + */ +bool has_drop_targets(void) { + int drop_targets = 0; + Con *con; + TAILQ_FOREACH (con, &all_cons, all_cons) { + if (!is_tiling_drop_target(con)) { + continue; + } + drop_targets++; + } + + /* In addition to tiling containers themselves, an visible but empty + * workspace (in a multi-monitor scenario) also is a drop target. */ + Con *output; + TAILQ_FOREACH (output, &(croot->focus_head), focused) { + if (con_is_internal(output)) { + continue; + } + Con *visible_ws = NULL; + GREP_FIRST(visible_ws, output_get_content(output), workspace_is_visible(child)); + if (visible_ws != NULL && con_num_children(visible_ws) == 0) { + drop_targets++; + } + } + + return drop_targets > 1; +} + /* * Return an appropriate target at given coordinates. * @@ -35,18 +91,13 @@ 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; + if (!rect_contains(rect, x, y) || + !is_tiling_drop_target(con)) { + continue; } + Con *ws = con_get_workspace(con); + Con *fs = con_get_fullscreen_covering_ws(ws); + return fs ? fs : con; } /* Couldn't find leaf container, get a workspace. */ @@ -267,7 +318,7 @@ static xcb_window_t create_drop_indicator(Rect rect) { * Initiates a mouse drag operation on a tiled window. * */ -void tiling_drag(Con *con, xcb_button_press_event_t *event) { +void tiling_drag(Con *con, xcb_button_press_event_t *event, bool use_threshold) { DLOG("Start dragging tiled container: con = %p\n", con); bool set_focus = (con == focused); bool set_fs = con->fullscreen_mode != CF_NONE; @@ -283,7 +334,7 @@ void tiling_drag(Con *con, xcb_button_press_event_t *event) { 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, ¶ms); + drag_result_t drag_result = drag_pointer(con, event, XCB_NONE, XCURSOR_CURSOR_MOVE, use_threshold, drag_callback, ¶ms); /* Dragging is done. We don't need the indicator window any more. */ xcb_destroy_window(conn, indicator); diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index d1446c4d3..bab650dee 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -552,6 +552,7 @@ my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: , ' ipc_kill_timeout restart_state popup_during_fullscreen + tiling_drag exec_always exec client.background diff --git a/testcases/t/318-i3-dmenu-desktop.t b/testcases/t/318-i3-dmenu-desktop.t new file mode 100644 index 000000000..75d983efc --- /dev/null +++ b/testcases/t/318-i3-dmenu-desktop.t @@ -0,0 +1,125 @@ +#!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) +# +# Verifies that i3-dmenu-desktop correctly parses Exec= lines in .desktop files +# and sends the command to i3 for execution. +# Ticket: #5152, #5156 +# Bug still in: 4.21-17-g389d555d +use i3test; +use i3test::Util qw(slurp); +use File::Temp qw(tempfile tempdir); +use POSIX qw(mkfifo); +use JSON::XS qw(decode_json); + +my $desktopdir = tempdir(CLEANUP => 1); + +$ENV{XDG_DATA_DIRS} = "$desktopdir"; + +mkdir("$desktopdir/applications"); + +# Create an i3-msg executable that dumps command line flags to a FIFO +my $tmpdir = tempdir(CLEANUP => 1); + +$ENV{PATH} = "$tmpdir:" . $ENV{PATH}; + +mkfifo("$tmpdir/fifo", 0600) or BAIL_OUT "Could not create FIFO: $!"; + +open(my $i3msg_dump, '>', "$tmpdir/i3-msg"); +say $i3msg_dump <', "$tmpdir/fifo"); +say \$f encode_json(\\\@ARGV); +close(\$f); +EOT +close($i3msg_dump); +chmod 0755, "$tmpdir/i3-msg"; + +my $testcnt = 0; +sub verify_exec { + my ($execline, $want_arg) = @_; + + $testcnt++; + + open(my $desktop, '>', "$desktopdir/applications/desktop$testcnt.desktop"); + say $desktop < 0; +use X11::XCB qw(:all); + +my $use_floating; +sub subtest_with_config { + my ($style, $cb) = @_; + my $some_other_style = $style eq "normal" ? "pixel" : "normal"; + + subtest 'with tiling', sub { + my $config = <(); + exit_gracefully($pid); + }; + + subtest 'with floating', sub { + my $config = <(); + exit_gracefully($pid); + }; +} + +sub _change_motif_property { + my ($window, $value) = @_; + $x->change_property( + PROP_MODE_REPLACE, + $window->id, + $x->atom(name => '_MOTIF_WM_HINTS')->id, + $x->atom(name => 'CARDINAL')->id, + 32, 5, + pack('L5', 2, 0, $value, 0, 0), + ); +} + +sub open_window_with_motifs { + my $value = shift; + + # we don't need other windows anymore, simplifies get_border_style + kill_all_windows; + + my $open = \&open_window; + if ($use_floating) { + $open = \&open_floating_window; + } + + my $window = $open->( + before_map => sub { + my ($window) = @_; + _change_motif_property($window, $value); + }, + ); + + sync_with_i3; + + return $window; +} + +my $window; +sub change_motif_property { + my $value = shift; + _change_motif_property($window, $value); + sync_with_i3; +} + +sub get_border_style { + if ($use_floating) { + my @floating = @{get_ws(focused_ws)->{floating_nodes}}; + return $floating[0]->{nodes}[0]->{border}; + } + + return @{get_ws(focused_ws)->{nodes}}[0]->{border}; +} + +sub is_border_style { + my ($expected, $extra_msg) = @_; + my $msg = "border style $expected"; + if (defined $extra_msg) { + $msg = "$msg: $extra_msg"; + } + + local $Test::Builder::Level = $Test::Builder::Level + 1; + is(get_border_style($window), $expected, $msg); +} + +############################################################################### +subtest 'with default_border normal', \&subtest_with_config, 'normal', +sub { +$window = open_window_with_motifs(0); +is_border_style('none'); + +$window = open_window_with_motifs(1 << 0); +is_border_style('normal'); + +$window = open_window_with_motifs(1 << 1); +is_border_style('pixel'); + +$window = open_window_with_motifs(1 << 3); +is_border_style('normal'); + +cmd 'border pixel'; +is_border_style('pixel', 'set by user'); + +change_motif_property(0); +is_border_style('none'); + +change_motif_property(1); +is_border_style('pixel', 'because of user maximum=pixel'); + +cmd 'border none'; +is_border_style('none', 'set by user'); + +change_motif_property(0); +is_border_style('none'); + +change_motif_property(1); +is_border_style('none', 'because of user maximum=none'); +}; + +subtest 'with default_border pixel', \&subtest_with_config, 'pixel', +sub { +$window = open_window_with_motifs(0); +is_border_style('none'); + +$window = open_window_with_motifs(1 << 0); +is_border_style('pixel'); + +$window = open_window_with_motifs(1 << 1); +is_border_style('pixel'); + +$window = open_window_with_motifs(1 << 3); +is_border_style('pixel'); + +cmd 'border normal'; +is_border_style('normal', 'set by user'); + +change_motif_property(0); +is_border_style('none'); + +change_motif_property(1); +is_border_style('normal', 'because of user maximum=normal'); +}; + +subtest 'with default_border none', \&subtest_with_config, 'none', +sub { +$window = open_window_with_motifs(0); +is_border_style('none'); + +$window = open_window_with_motifs(1 << 0); +is_border_style('none'); + +$window = open_window_with_motifs(1 << 1); +is_border_style('none'); + +$window = open_window_with_motifs(1 << 3); +is_border_style('none'); + +cmd 'border pixel'; +is_border_style('pixel', 'set by user'); + +change_motif_property(0); +is_border_style('none'); + +change_motif_property(1); +is_border_style('pixel', 'because of user maximum=pixel'); +}; + +done_testing;