aboutsummaryrefslogtreecommitdiff
path: root/src/drag.c
blob: 6b05311af3120887d47149c50cc85ee9a78d3f23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/*
 * vim:ts=4:sw=4:expandtab
 *
 * i3 - an improved dynamic tiling window manager
 * © 2009 Michael Stapelberg and contributors (see also: LICENSE)
 *
 * drag.c: click and drag.
 *
 */
#include "all.h"

/* Custom data structure used to track dragging-related events. */
struct drag_x11_cb {
    ev_prepare prepare;

    /* Whether this modal event loop should be exited and with which result. */
    drag_result_t result;

    /* The container that is being dragged or resized, or NULL if this is a
     * drag of the resize handle. */
    Con *con;

    /* The original event that initiated the drag. */
    const xcb_button_press_event_t *event;

    /* The dimensions of con when the loop was started. */
    Rect old_rect;

    /* The callback to invoke after every pointer movement. */
    callback_t callback;

    /* Drag distance threshold exceeded. If use_threshold is not set, then
     * threshold_exceeded is always true. */
    bool threshold_exceeded;

    /* Cursor to set after the threshold is exceeded. */
    xcb_cursor_t xcursor;

    /* User data pointer for callback. */
    const void *extra;
};

static bool threshold_exceeded(uint32_t x1, uint32_t y1,
                               uint32_t x2, uint32_t y2) {
    const uint32_t threshold = 9;
    return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) > threshold * threshold;
}

static bool drain_drag_events(EV_P, struct drag_x11_cb *dragloop) {
    xcb_motion_notify_event_t *last_motion_notify = NULL;
    xcb_generic_event_t *event;

    while ((event = xcb_poll_for_event(conn)) != NULL) {
        if (event->response_type == 0) {
            xcb_generic_error_t *error = (xcb_generic_error_t *)event;
            DLOG("X11 Error received (probably harmless)! sequence 0x%x, error_code = %d\n",
                 error->sequence, error->error_code);
            free(event);
            continue;
        }

        /* Strip off the highest bit (set if the event is generated) */
        int type = (event->response_type & 0x7F);

        switch (type) {
            case XCB_BUTTON_RELEASE:
                dragloop->result = DRAG_SUCCESS;
                break;

            case XCB_KEY_PRESS:
                DLOG("A key was pressed during drag, reverting changes.\n");
                dragloop->result = DRAG_REVERT;
                handle_event(type, event);
                break;

            case XCB_UNMAP_NOTIFY: {
                xcb_unmap_notify_event_t *unmap_event = (xcb_unmap_notify_event_t *)event;
                Con *con = con_by_window_id(unmap_event->window);

                if (con != NULL) {
                    DLOG("UnmapNotify for window 0x%08x (container %p)\n", unmap_event->window, con);

                    if (con_get_workspace(con) == con_get_workspace(focused)) {
                        DLOG("UnmapNotify for a managed window on the current workspace, aborting\n");
                        dragloop->result = DRAG_ABORT;
                    }
                }

                handle_event(type, event);
                break;
            }

            case XCB_MOTION_NOTIFY:
                /* motion_notify events are saved for later */
                FREE(last_motion_notify);
                last_motion_notify = (xcb_motion_notify_event_t *)event;
                break;

            default:
                DLOG("Passing to original handler\n");
                handle_event(type, event);
                break;
        }

        if (last_motion_notify != (xcb_motion_notify_event_t *)event)
            free(event);

        if (dragloop->result != DRAGGING) {
            ev_break(EV_A_ EVBREAK_ONE);
            if (dragloop->result == DRAG_SUCCESS) {
                /* Ensure motion notify events are handled. */
                break;
            } else {
                free(last_motion_notify);
                return true;
            }
        }
    }

    if (last_motion_notify == NULL) {
        return true;
    }

    if (!dragloop->threshold_exceeded &&
        threshold_exceeded(last_motion_notify->root_x, last_motion_notify->root_y,
                           dragloop->event->root_x, dragloop->event->root_y)) {
        if (dragloop->xcursor != XCB_NONE) {
            xcb_change_active_pointer_grab(
                conn,
                dragloop->xcursor,
                XCB_CURRENT_TIME,
                XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION);
        }
        dragloop->threshold_exceeded = true;
    }

    /* Ensure that we are either dragging the resize handle (con is NULL) or that the
     * container still exists. The latter might not be true, e.g., if the window closed
     * for any reason while the user was dragging it. */
    if (dragloop->threshold_exceeded && (!dragloop->con || con_exists(dragloop->con))) {
        dragloop->callback(
            dragloop->con,
            &(dragloop->old_rect),
            last_motion_notify->root_x,
            last_motion_notify->root_y,
            dragloop->event,
            dragloop->extra);
    }
    FREE(last_motion_notify);

    xcb_flush(conn);
    return dragloop->result != DRAGGING;
}

static void xcb_drag_prepare_cb(EV_P_ ev_prepare *w, int revents) {
    struct drag_x11_cb *dragloop = (struct drag_x11_cb *)w->data;
    while (!drain_drag_events(EV_A, dragloop)) {
        /* repeatedly drain events: draining might produce additional ones */
    }
}

/*
 * This function grabs your pointer and keyboard and lets you drag stuff around
 * (borders). Every time you move your mouse, an XCB_MOTION_NOTIFY event will
 * be received and the given callback will be called with the parameters
 * specified (client, the original event), the original rect of the client,
 * and the new coordinates (x, y).
 *
 * If use_threshold is set, dragging only starts after the user moves the
 * pointer past a certain threshold. That is, the cursor will not be set and the
 * callback will not be called until then.
 *
 */
drag_result_t drag_pointer(Con *con, const xcb_button_press_event_t *event,
                           xcb_window_t confine_to, int cursor,
                           bool use_threshold, callback_t callback,
                           const void *extra) {
    xcb_cursor_t xcursor = (cursor && xcursor_supported) ? xcursor_get_cursor(cursor) : XCB_NONE;

    /* Grab the pointer */
    xcb_grab_pointer_cookie_t cookie;
    xcb_grab_pointer_reply_t *reply;
    xcb_generic_error_t *error;

    cookie = xcb_grab_pointer(conn,
                              false,                                                         /* get all pointer events specified by the following mask */
                              root,                                                          /* grab the root window */
                              XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION, /* which events to let through */
                              XCB_GRAB_MODE_ASYNC,                                           /* pointer events should continue as normal */
                              XCB_GRAB_MODE_ASYNC,                                           /* keyboard mode */
                              confine_to,                                                    /* confine_to = in which window should the cursor stay */
                              use_threshold ? XCB_NONE : xcursor,                            /* possibly display a special cursor */
                              XCB_CURRENT_TIME);

    if ((reply = xcb_grab_pointer_reply(conn, cookie, &error)) == NULL) {
        ELOG("Could not grab pointer (error_code = %d)\n", error->error_code);
        free(error);
        return DRAG_ABORT;
    }

    free(reply);

    /* Grab the keyboard */
    xcb_grab_keyboard_cookie_t keyb_cookie;
    xcb_grab_keyboard_reply_t *keyb_reply;

    keyb_cookie = xcb_grab_keyboard(conn,
                                    false, /* get all keyboard events */
                                    root,  /* grab the root window */
                                    XCB_CURRENT_TIME,
                                    XCB_GRAB_MODE_ASYNC, /* continue processing pointer events as normal */
                                    XCB_GRAB_MODE_ASYNC  /* keyboard mode */
    );

    if ((keyb_reply = xcb_grab_keyboard_reply(conn, keyb_cookie, &error)) == NULL) {
        ELOG("Could not grab keyboard (error_code = %d)\n", error->error_code);
        free(error);
        xcb_ungrab_pointer(conn, XCB_CURRENT_TIME);
        return DRAG_ABORT;
    }

    free(keyb_reply);

    /* Go into our own event loop */
    struct drag_x11_cb loop = {
        .result = DRAGGING,
        .con = con,
        .event = event,
        .callback = callback,
        .threshold_exceeded = !use_threshold,
        .xcursor = xcursor,
        .extra = extra,
    };
    ev_prepare *prepare = &loop.prepare;
    if (con)
        loop.old_rect = con->rect;
    ev_prepare_init(prepare, xcb_drag_prepare_cb);
    prepare->data = &loop;
    main_set_x11_cb(false);
    ev_prepare_start(main_loop, prepare);

    ev_loop(main_loop, 0);

    ev_prepare_stop(main_loop, prepare);
    main_set_x11_cb(true);

    xcb_ungrab_keyboard(conn, XCB_CURRENT_TIME);
    xcb_ungrab_pointer(conn, XCB_CURRENT_TIME);
    xcb_flush(conn);

    return loop.result;
}