1 /*****************************************************************************
2 * Copyright 2003 - 2010 Craig Drummond <craig.p.drummond@gmail.com> *
3 * Copyright 2013 - 2015 Yichao Yu <yyc1992@gmail.com> *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU Lesser General Public License as *
7 * published by the Free Software Foundation; either version 2.1 of the *
8 * License, or (at your option) version 3, or any later version accepted *
9 * by the membership of KDE e.V. (or its successor approved by the *
10 * membership of KDE e.V.), which shall act as a proxy defined in *
11 * Section 6 of version 3 of the license. *
12 * *
13 * This program is distributed in the hope that it will be useful, *
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
16 * Lesser General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU Lesser General Public *
19 * License along with this library. If not, *
20 * see <http://www.gnu.org/licenses/>. *
21 *****************************************************************************/
22
23 #include "wmmove.h"
24
25 #include <qtcurve-utils/gtkprops.h>
26 #include <qtcurve-utils/x11wmmove.h>
27
28 #include <gdk/gdkx.h>
29 #include "helpers.h"
30 #include "qt_settings.h"
31 #include "tab.h"
32
33 namespace QtCurve {
34 namespace WMMove {
35
36 static int lastX = -1;
37 static int lastY = -1;
38 static int timer = 0;
39 static GtkWidget *dragWidget = nullptr;
40 //! keep track of the last rejected button event to reject it again if passed to some parent widget
41 /*! this spares some time (by not processing the same event twice), and prevents some bugs */
42 GdkEventButton *lastRejectedEvent = nullptr;
43
44 static int btnReleaseSignalId = 0;
45 static int btnReleaseHookId = 0;
46
47 static bool dragEnd();
48
49 static gboolean
btnReleaseHook(GSignalInvocationHint *,unsigned,const GValue *,void *)50 btnReleaseHook(GSignalInvocationHint*, unsigned, const GValue*, void*)
51 {
52 if (dragWidget)
53 dragEnd();
54 return true;
55 }
56
57 static void
registerBtnReleaseHook()58 registerBtnReleaseHook()
59 {
60 if (btnReleaseSignalId == 0 && btnReleaseHookId == 0) {
61 btnReleaseSignalId =
62 g_signal_lookup("button-release-event", GTK_TYPE_WIDGET);
63 if (btnReleaseSignalId) {
64 btnReleaseHookId =
65 g_signal_add_emission_hook(btnReleaseSignalId,
66 (GQuark)0, btnReleaseHook,
67 nullptr, nullptr);
68 }
69 }
70 }
71
72 static void
stopTimer()73 stopTimer()
74 {
75 if (timer)
76 g_source_remove(timer);
77 timer = 0;
78 }
79
80 static void
reset()81 reset()
82 {
83 lastX = -1;
84 lastY = -1;
85 dragWidget = nullptr;
86 lastRejectedEvent = nullptr;
87 stopTimer();
88 }
89
90 static void
store(GtkWidget * widget,GdkEventButton * event)91 store(GtkWidget *widget, GdkEventButton *event)
92 {
93 lastX = event ? event->x_root : -1;
94 lastY = event ? event->y_root : -1;
95 dragWidget = widget;
96 }
97
98 static void
trigger(GtkWidget * w,int x,int y)99 trigger(GtkWidget *w, int x, int y)
100 {
101 GtkWindow *topLevel = GTK_WINDOW(gtk_widget_get_toplevel(w));
102 xcb_window_t wid =
103 GDK_WINDOW_XID(gtk_widget_get_window(GTK_WIDGET(topLevel)));
104 qtcX11MoveTrigger(wid, x, y);
105 dragEnd();
106 }
107
108 static bool
withinWidget(GtkWidget * widget,GdkEventButton * event)109 withinWidget(GtkWidget *widget, GdkEventButton *event)
110 {
111 // get top level widget
112 GtkWidget *topLevel=gtk_widget_get_toplevel(widget);;
113 GdkWindow *window = topLevel ? gtk_widget_get_window(topLevel) : nullptr;
114
115 if (window) {
116 QtcRect allocation;
117 int wx=0, wy=0, nx=0, ny=0;
118
119 // translate widget position to topLevel
120 gtk_widget_translate_coordinates(widget, topLevel, wx, wy, &wx, &wy);
121
122 // translate to absolute coordinates
123 gdk_window_get_origin(window, &nx, &ny);
124 wx += nx;
125 wy += ny;
126
127 // get widget size.
128 // for notebooks, only consider the tabbar rect
129 if (GTK_IS_NOTEBOOK(widget)) {
130 QtcRect widgetAlloc = Widget::getAllocation(widget);
131 allocation = Tab::getTabbarRect(GTK_NOTEBOOK(widget));
132 allocation.x += wx - widgetAlloc.x;
133 allocation.y += wy - widgetAlloc.y;
134 } else {
135 allocation = Widget::getAllocation(widget);
136 allocation.x = wx;
137 allocation.y = wy;
138 }
139
140 return allocation.x<=event->x_root && allocation.y<=event->y_root &&
141 (allocation.x+allocation.width)>event->x_root && (allocation.y+allocation.height)>event->y_root;
142 }
143 return true;
144 }
145
146 static bool
isBlackListed(GObject * object)147 isBlackListed(GObject *object)
148 {
149 static const char *widgets[] = {
150 "GtkPizza", "GladeDesignLayout", "MetaFrames", "SPHRuler",
151 "SPVRuler", nullptr
152 };
153
154 for (int i = 0;widgets[i];i++) {
155 if (objectIsA(object, widgets[i])) {
156 return true;
157 }
158 }
159 return false;
160 }
161
162 static bool
childrenUseEvent(GtkWidget * widget,GdkEventButton * event,bool inNoteBook)163 childrenUseEvent(GtkWidget *widget, GdkEventButton *event, bool inNoteBook)
164 {
165 // accept, by default
166 bool usable = true;
167
168 // get children and check
169 GList *children = gtk_container_get_children(GTK_CONTAINER(widget));
170 for (GList *child = children;child && usable;child = g_list_next(child)) {
171 // cast child to GtkWidget
172 if (GTK_IS_WIDGET(child->data)) {
173 GtkWidget *childWidget = GTK_WIDGET(child->data);
174 GdkWindow *window = nullptr;
175
176 // check widget state and type
177 if (gtk_widget_get_state(childWidget) == GTK_STATE_PRELIGHT) {
178 // if widget is prelight, we don't need to check where event
179 // happen, any prelight widget indicate we can't do a move
180 usable = false;
181 continue;
182 }
183
184 window = gtk_widget_get_window(childWidget);
185 if (!(window && gdk_window_is_visible(window)))
186 continue;
187
188 if (GTK_IS_NOTEBOOK(childWidget))
189 inNoteBook = true;
190
191 if(!(event && withinWidget(childWidget, event)))
192 continue;
193
194 // check special cases for which grab should not be enabled
195 if((isBlackListed(G_OBJECT(childWidget))) ||
196 (GTK_IS_NOTEBOOK(widget) && Tab::isLabel(GTK_NOTEBOOK(widget),
197 childWidget)) ||
198 (GTK_IS_BUTTON(childWidget) &&
199 gtk_widget_get_state(childWidget) != GTK_STATE_INSENSITIVE) ||
200 (gtk_widget_get_events(childWidget) &
201 (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK)) ||
202 (GTK_IS_MENU_ITEM(childWidget)) ||
203 (GTK_IS_SCROLLED_WINDOW(childWidget) &&
204 (!inNoteBook || gtk_widget_is_focus(childWidget)))) {
205 usable = false;
206 }
207
208 // if child is a container and event has been accepted so far,
209 // also check it, recursively
210 if (usable && GTK_IS_CONTAINER(childWidget)) {
211 usable = childrenUseEvent(childWidget, event, inNoteBook);
212 }
213 }
214 }
215 if (children) {
216 g_list_free(children);
217 }
218 return usable;
219 }
220
221 static bool
useEvent(GtkWidget * widget,GdkEventButton * event)222 useEvent(GtkWidget *widget, GdkEventButton *event)
223 {
224 if(lastRejectedEvent && lastRejectedEvent==event)
225 return false;
226
227 if(!GTK_IS_CONTAINER(widget))
228 return true;
229
230 // if widget is a notebook, accept if there is no hovered tab
231 if (GTK_IS_NOTEBOOK(widget)) {
232 return (!Tab::hasVisibleArrows(GTK_NOTEBOOK(widget)) &&
233 Tab::currentHoveredIndex(widget) == -1 &&
234 childrenUseEvent(widget, event, false));
235 } else {
236 return childrenUseEvent(widget, event, false);
237 }
238 }
239
240 static gboolean
startDelayedDrag(void *)241 startDelayedDrag(void*)
242 {
243 if (dragWidget) {
244 gdk_threads_enter();
245 trigger(dragWidget, lastX, lastY);
246 gdk_threads_leave();
247 }
248 return false;
249 }
250
251 static bool
isWindowDragWidget(GtkWidget * widget,GdkEventButton * event)252 isWindowDragWidget(GtkWidget *widget, GdkEventButton *event)
253 {
254 if (opts.windowDrag && (!event || (withinWidget(widget, event) &&
255 useEvent(widget, event)))) {
256 store(widget, event);
257 // Start timer
258 stopTimer();
259 timer = g_timeout_add(qtSettings.startDragTime,
260 (GSourceFunc)startDelayedDrag, nullptr);
261 return true;
262 }
263 lastRejectedEvent=event;
264 return false;
265 }
266
267 static gboolean
buttonPress(GtkWidget * widget,GdkEventButton * event,void *)268 buttonPress(GtkWidget *widget, GdkEventButton *event, void*)
269 {
270 if (GDK_BUTTON_PRESS == event->type && 1 == event->button &&
271 isWindowDragWidget(widget, event)) {
272 dragWidget = widget;
273 return true;
274 }
275 return false;
276 }
277
278 static bool
dragEnd()279 dragEnd()
280 {
281 if (dragWidget) {
282 //gtk_grab_remove(widget);
283 gdk_pointer_ungrab(CurrentTime);
284 reset();
285 return true;
286 }
287
288 return false;
289 }
290
cleanup(GtkWidget * widget)291 static void cleanup(GtkWidget *widget)
292 {
293 GtkWidgetProps props(widget);
294 if (props->wmMoveHacked) {
295 if (widget == dragWidget)
296 reset();
297 props->wmMoveDestroy.disconn();
298 props->wmMoveStyleSet.disconn();
299 props->wmMoveMotion.disconn();
300 props->wmMoveLeave.disconn();
301 props->wmMoveButtonPress.disconn();
302 props->wmMoveHacked = false;
303 }
304 }
305
306 static gboolean
styleSet(GtkWidget * widget,GtkStyle *,void *)307 styleSet(GtkWidget *widget, GtkStyle*, void*)
308 {
309 cleanup(widget);
310 return false;
311 }
312
313 static gboolean
destroy(GtkWidget * widget,GdkEvent *,void *)314 destroy(GtkWidget *widget, GdkEvent*, void*)
315 {
316 cleanup(widget);
317 return false;
318 }
319
320 static gboolean
motion(GtkWidget * widget,GdkEventMotion * event,void *)321 motion(GtkWidget *widget, GdkEventMotion *event, void*)
322 {
323 if (dragWidget == widget) {
324 // check displacement with respect to drag start
325 const int distance = (std::abs(lastX - event->x_root) +
326 std::abs(lastY - event->y_root));
327
328 if (distance > 0)
329 stopTimer();
330
331 /* if (distance < qtSettings.startDragDist) */
332 /* return false; */
333 trigger(widget, event->x_root, event->y_root);
334 return true;
335 }
336 return false;
337 }
338
339 static gboolean
leave(GtkWidget *,GdkEventMotion *,void *)340 leave(GtkWidget*, GdkEventMotion*, void*)
341 {
342 return dragEnd();
343 }
344
345 void
setup(GtkWidget * widget)346 setup(GtkWidget *widget)
347 {
348 QTC_RET_IF_FAIL(widget);
349 GtkWidget *parent = nullptr;
350
351 if (GTK_IS_WINDOW(widget) &&
352 !gtk_window_get_decorated(GTK_WINDOW(widget))) {
353 return;
354 }
355
356 if (GTK_IS_EVENT_BOX(widget) &&
357 gtk_event_box_get_above_child(GTK_EVENT_BOX(widget)))
358 return;
359
360 parent = gtk_widget_get_parent(widget);
361
362 // widgets used in tabs also must be ignored (happens, unfortunately)
363 if (GTK_IS_NOTEBOOK(parent) && Tab::isLabel(GTK_NOTEBOOK(parent), widget))
364 return;
365
366 /*
367 check event mask (for now we only need to do that for GtkWindow)
368 The idea is that if the window has been set to receive button_press
369 and button_release events (which is not done by default), it likely
370 means that it does something with such events, in which case we should
371 not use them for grabbing
372 */
373 if (oneOf(gTypeName(widget), "GtkWindow") &&
374 (gtk_widget_get_events(widget) &
375 (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK)))
376 return;
377
378 GtkWidgetProps props(widget);
379 if (!isFakeGtk() && !props->wmMoveHacked) {
380 props->wmMoveHacked = true;
381 gtk_widget_add_events(widget, GDK_BUTTON_RELEASE_MASK |
382 GDK_BUTTON_PRESS_MASK | GDK_LEAVE_NOTIFY_MASK |
383 GDK_BUTTON1_MOTION_MASK);
384 registerBtnReleaseHook();
385 props->wmMoveDestroy.conn("destroy-event", destroy);
386 props->wmMoveStyleSet.conn("style-set", styleSet);
387 props->wmMoveMotion.conn("motion-notify-event", motion);
388 props->wmMoveLeave.conn("leave-notify-event", leave);
389 props->wmMoveButtonPress.conn("button-press-event", buttonPress);
390 }
391 }
392
393 }
394 }
395