1 /* GTK - The GIMP Toolkit
2 * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * SPDX-License-Identifier: LGPL-2.1+
18 */
19
20 /*
21 * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS
22 * file for a list of people on the GTK+ Team. See the ChangeLog
23 * files for a list of changes. These files are distributed with
24 * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
25 */
26
27 /* Most of the file is based on bits of code from GtkWindow */
28
29 #include "config.h"
30
31 #include "gtk-window-private.h"
32 #include "hdy-window-handle-controller-private.h"
33
34 #include <glib/gi18n-lib.h>
35
36 /**
37 * PRIVATE:hdy-window-handle-controller
38 * @short_description: An oblect that makes widgets behave like titlebars.
39 * @Title: HdyWindowHandleController
40 * @See_also: #HdyHeaderBar, #HdyWindowHandle
41 * @stability: Private
42 *
43 * When HdyWindowHandleController is added to the widget, dragging that widget
44 * will move the window, and right click, double click and middle click will be
45 * handled as if that widget was a titlebar. Currently it's used to implement
46 * these properties in #HdyWindowHandle and #HdyHeaderBar
47 *
48 * Since: 1.0
49 */
50
51 struct _HdyWindowHandleController
52 {
53 GObject parent;
54
55 GtkWidget *widget;
56 GtkGesture *multipress_gesture;
57 GtkWidget *fallback_menu;
58 gboolean keep_above;
59 };
60
61 G_DEFINE_TYPE (HdyWindowHandleController, hdy_window_handle_controller, G_TYPE_OBJECT);
62
63 static GtkWindow *
get_window(HdyWindowHandleController * self)64 get_window (HdyWindowHandleController *self)
65 {
66 GtkWidget *toplevel = gtk_widget_get_toplevel (self->widget);
67
68 if (GTK_IS_WINDOW (toplevel))
69 return GTK_WINDOW (toplevel);
70
71 return NULL;
72 }
73
74 static void
popup_menu_detach(GtkWidget * widget,GtkMenu * menu)75 popup_menu_detach (GtkWidget *widget,
76 GtkMenu *menu)
77 {
78 HdyWindowHandleController *self;
79
80 self = g_object_steal_data (G_OBJECT (menu), "hdywindowhandlecontroller");
81
82 self->fallback_menu = NULL;
83 }
84
85 static void
restore_window_cb(GtkMenuItem * menuitem,HdyWindowHandleController * self)86 restore_window_cb (GtkMenuItem *menuitem,
87 HdyWindowHandleController *self)
88 {
89 GtkWindow *window = get_window (self);
90 GdkWindowState state;
91
92 if (!window)
93 return;
94
95 if (gtk_window_is_maximized (window)) {
96 gtk_window_unmaximize (window);
97 return;
98 }
99
100 state = hdy_gtk_window_get_state (window);
101
102 if (state & GDK_WINDOW_STATE_ICONIFIED)
103 gtk_window_deiconify (window);
104 }
105
106 static void
move_window_cb(GtkMenuItem * menuitem,HdyWindowHandleController * self)107 move_window_cb (GtkMenuItem *menuitem,
108 HdyWindowHandleController *self)
109 {
110 GtkWindow *window = get_window (self);
111
112 if (!window)
113 return;
114
115 gtk_window_begin_move_drag (window,
116 0, /* 0 means "use keyboard" */
117 0, 0,
118 GDK_CURRENT_TIME);
119 }
120
121 static void
resize_window_cb(GtkMenuItem * menuitem,HdyWindowHandleController * self)122 resize_window_cb (GtkMenuItem *menuitem,
123 HdyWindowHandleController *self)
124 {
125 GtkWindow *window = get_window (self);
126
127 if (!window)
128 return;
129
130 gtk_window_begin_resize_drag (window,
131 0,
132 0, /* 0 means "use keyboard" */
133 0, 0,
134 GDK_CURRENT_TIME);
135 }
136
137 static void
minimize_window_cb(GtkMenuItem * menuitem,HdyWindowHandleController * self)138 minimize_window_cb (GtkMenuItem *menuitem,
139 HdyWindowHandleController *self)
140 {
141 GtkWindow *window = get_window (self);
142
143 if (!window)
144 return;
145
146 /* Turns out, we can't iconify a maximized window */
147 if (gtk_window_is_maximized (window))
148 gtk_window_unmaximize (window);
149
150 gtk_window_iconify (window);
151 }
152
153 static void
maximize_window_cb(GtkMenuItem * menuitem,HdyWindowHandleController * self)154 maximize_window_cb (GtkMenuItem *menuitem,
155 HdyWindowHandleController *self)
156 {
157 GtkWindow *window = get_window (self);
158 GdkWindowState state;
159
160 if (!window)
161 return;
162
163 state = hdy_gtk_window_get_state (window);
164
165 if (state & GDK_WINDOW_STATE_ICONIFIED)
166 gtk_window_deiconify (window);
167
168 gtk_window_maximize (window);
169 }
170
171 static void
ontop_window_cb(GtkMenuItem * menuitem,HdyWindowHandleController * self)172 ontop_window_cb (GtkMenuItem *menuitem,
173 HdyWindowHandleController *self)
174 {
175 GtkWindow *window = get_window (self);
176
177 if (!window)
178 return;
179
180 /*
181 * FIXME: It will go out of sync if something else calls
182 * gtk_window_set_keep_above(), so we need to actually track it.
183 * For some reason this doesn't seem to be reflected in the
184 * window state.
185 */
186 self->keep_above = !self->keep_above;
187 gtk_window_set_keep_above (window, self->keep_above);
188 }
189
190 static void
close_window_cb(GtkMenuItem * menuitem,HdyWindowHandleController * self)191 close_window_cb (GtkMenuItem *menuitem,
192 HdyWindowHandleController *self)
193 {
194 GtkWindow *window = get_window (self);
195
196 if (!window)
197 return;
198
199 gtk_window_close (window);
200 }
201
202 static void
do_popup(HdyWindowHandleController * self,GdkEventButton * event)203 do_popup (HdyWindowHandleController *self,
204 GdkEventButton *event)
205 {
206 GtkWindow *window = get_window (self);
207 GtkWidget *menuitem;
208 GdkWindowState state;
209 gboolean maximized, iconified, resizable;
210 GdkWindowTypeHint type_hint;
211
212 if (!window)
213 return;
214
215 if (gdk_window_show_window_menu (gtk_widget_get_window (GTK_WIDGET (window)),
216 (GdkEvent *) event))
217 return;
218
219 if (self->fallback_menu)
220 gtk_widget_destroy (self->fallback_menu);
221
222 state = hdy_gtk_window_get_state (window);
223
224 iconified = (state & GDK_WINDOW_STATE_ICONIFIED) == GDK_WINDOW_STATE_ICONIFIED;
225 maximized = gtk_window_is_maximized (window) && !iconified;
226 resizable = gtk_window_get_resizable (window);
227 type_hint = gtk_window_get_type_hint (window);
228
229 self->fallback_menu = gtk_menu_new ();
230 gtk_style_context_add_class (gtk_widget_get_style_context (self->fallback_menu),
231 GTK_STYLE_CLASS_CONTEXT_MENU);
232
233 /* We can't pass self to popup_menu_detach, so will have to use custom data */
234 g_object_set_data (G_OBJECT (self->fallback_menu),
235 "hdywindowhandlecontroller", self);
236
237 gtk_menu_attach_to_widget (GTK_MENU (self->fallback_menu),
238 self->widget,
239 popup_menu_detach);
240
241 menuitem = gtk_menu_item_new_with_label (_("Restore"));
242 gtk_widget_show (menuitem);
243 /* "Restore" means "Unmaximize" or "Unminimize"
244 * (yes, some WMs allow window menu to be shown for minimized windows).
245 * Not restorable:
246 * - visible windows that are not maximized or minimized
247 * - non-resizable windows that are not minimized
248 * - non-normal windows
249 */
250 if ((gtk_widget_is_visible (GTK_WIDGET (window)) &&
251 !(maximized || iconified)) ||
252 (!iconified && !resizable) ||
253 type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
254 gtk_widget_set_sensitive (menuitem, FALSE);
255 g_signal_connect (G_OBJECT (menuitem), "activate",
256 G_CALLBACK (restore_window_cb), self);
257 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
258
259 menuitem = gtk_menu_item_new_with_label (_("Move"));
260 gtk_widget_show (menuitem);
261 if (maximized || iconified)
262 gtk_widget_set_sensitive (menuitem, FALSE);
263 g_signal_connect (G_OBJECT (menuitem), "activate",
264 G_CALLBACK (move_window_cb), self);
265 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
266
267 menuitem = gtk_menu_item_new_with_label (_("Resize"));
268 gtk_widget_show (menuitem);
269 if (!resizable || maximized || iconified)
270 gtk_widget_set_sensitive (menuitem, FALSE);
271 g_signal_connect (G_OBJECT (menuitem), "activate",
272 G_CALLBACK (resize_window_cb), self);
273 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
274
275 menuitem = gtk_menu_item_new_with_label (_("Minimize"));
276 gtk_widget_show (menuitem);
277 if (iconified ||
278 type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
279 gtk_widget_set_sensitive (menuitem, FALSE);
280 g_signal_connect (G_OBJECT (menuitem), "activate",
281 G_CALLBACK (minimize_window_cb), self);
282 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
283
284 menuitem = gtk_menu_item_new_with_label (_("Maximize"));
285 gtk_widget_show (menuitem);
286 if (maximized ||
287 !resizable ||
288 type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
289 gtk_widget_set_sensitive (menuitem, FALSE);
290 g_signal_connect (G_OBJECT (menuitem), "activate",
291 G_CALLBACK (maximize_window_cb), self);
292 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
293
294 menuitem = gtk_separator_menu_item_new ();
295 gtk_widget_show (menuitem);
296 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
297
298 menuitem = gtk_check_menu_item_new_with_label (_("Always on Top"));
299 gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menuitem), self->keep_above);
300 if (maximized)
301 gtk_widget_set_sensitive (menuitem, FALSE);
302 gtk_widget_show (menuitem);
303 g_signal_connect (G_OBJECT (menuitem), "activate",
304 G_CALLBACK (ontop_window_cb), self);
305 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
306
307 menuitem = gtk_separator_menu_item_new ();
308 gtk_widget_show (menuitem);
309 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
310
311 menuitem = gtk_menu_item_new_with_label (_("Close"));
312 gtk_widget_show (menuitem);
313 if (!gtk_window_get_deletable (window))
314 gtk_widget_set_sensitive (menuitem, FALSE);
315 g_signal_connect (G_OBJECT (menuitem), "activate",
316 G_CALLBACK (close_window_cb), self);
317 gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
318 gtk_menu_popup_at_pointer (GTK_MENU (self->fallback_menu), (GdkEvent *) event);
319 }
320
321 static gboolean
titlebar_action(HdyWindowHandleController * self,const GdkEvent * event,guint button)322 titlebar_action (HdyWindowHandleController *self,
323 const GdkEvent *event,
324 guint button)
325 {
326 GtkSettings *settings;
327 g_autofree gchar *action = NULL;
328 GtkWindow *window = get_window (self);
329
330 if (!window)
331 return FALSE;
332
333 settings = gtk_widget_get_settings (GTK_WIDGET (window));
334
335 switch (button) {
336 case GDK_BUTTON_PRIMARY:
337 g_object_get (settings, "gtk-titlebar-double-click", &action, NULL);
338 break;
339
340 case GDK_BUTTON_MIDDLE:
341 g_object_get (settings, "gtk-titlebar-middle-click", &action, NULL);
342 break;
343
344 case GDK_BUTTON_SECONDARY:
345 g_object_get (settings, "gtk-titlebar-right-click", &action, NULL);
346 break;
347
348 default:
349 g_assert_not_reached ();
350 }
351
352 if (action == NULL)
353 return FALSE;
354
355 if (g_str_equal (action, "none"))
356 return FALSE;
357
358 if (g_str_has_prefix (action, "toggle-maximize")) {
359 /*
360 * gtk header bar won't show the maximize button if the following
361 * properties are not met, apply the same to title bar actions for
362 * consistency.
363 */
364 if (gtk_window_get_resizable (window) &&
365 gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL)
366 hdy_gtk_window_toggle_maximized (window);
367
368 return TRUE;
369 }
370
371 if (g_str_equal (action, "lower")) {
372 gdk_window_lower (gtk_widget_get_window (GTK_WIDGET (window)));
373
374 return TRUE;
375 }
376
377 if (g_str_equal (action, "minimize")) {
378 gdk_window_iconify (gtk_widget_get_window (GTK_WIDGET (window)));
379
380 return TRUE;
381 }
382
383 if (g_str_equal (action, "menu")) {
384 do_popup (self, (GdkEventButton*) event);
385
386 return TRUE;
387 }
388
389 g_warning ("Unsupported titlebar action %s", action);
390
391 return FALSE;
392 }
393
394 static void
pressed_cb(GtkGestureMultiPress * gesture,gint n_press,gdouble x,gdouble y,HdyWindowHandleController * self)395 pressed_cb (GtkGestureMultiPress *gesture,
396 gint n_press,
397 gdouble x,
398 gdouble y,
399 HdyWindowHandleController *self)
400 {
401 GtkWidget *window = gtk_widget_get_toplevel (self->widget);
402 GdkEventSequence *sequence =
403 gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
404 const GdkEvent *event =
405 gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);
406 guint button =
407 gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
408
409 if (!event)
410 return;
411
412 if (gdk_display_device_is_grabbed (gtk_widget_get_display (window),
413 gtk_gesture_get_device (GTK_GESTURE (gesture))))
414 return;
415
416 switch (button) {
417 case GDK_BUTTON_PRIMARY:
418 gdk_window_raise (gtk_widget_get_window (window));
419
420 if (n_press == 2)
421 titlebar_action (self, event, button);
422
423 if (gtk_widget_has_grab (window))
424 gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
425 sequence, GTK_EVENT_SEQUENCE_CLAIMED);
426
427 break;
428
429 case GDK_BUTTON_SECONDARY:
430 if (titlebar_action (self, event, button))
431 gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
432 sequence, GTK_EVENT_SEQUENCE_CLAIMED);
433
434 gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
435 break;
436
437 case GDK_BUTTON_MIDDLE:
438 if (titlebar_action (self, event, button))
439 gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
440 sequence, GTK_EVENT_SEQUENCE_CLAIMED);
441 break;
442
443 default:
444 break;
445 }
446 }
447
448 static void
hdy_window_handle_controller_finalize(GObject * object)449 hdy_window_handle_controller_finalize (GObject *object)
450 {
451 HdyWindowHandleController *self = (HdyWindowHandleController *)object;
452
453 self->widget = NULL;
454 g_clear_object (&self->multipress_gesture);
455 g_clear_object (&self->fallback_menu);
456
457 G_OBJECT_CLASS (hdy_window_handle_controller_parent_class)->finalize (object);
458 }
459
460 static void
hdy_window_handle_controller_class_init(HdyWindowHandleControllerClass * klass)461 hdy_window_handle_controller_class_init (HdyWindowHandleControllerClass *klass)
462 {
463 GObjectClass *object_class = G_OBJECT_CLASS (klass);
464
465 object_class->finalize = hdy_window_handle_controller_finalize;
466 }
467
468 static void
hdy_window_handle_controller_init(HdyWindowHandleController * self)469 hdy_window_handle_controller_init (HdyWindowHandleController *self)
470 {
471 }
472
473 /**
474 * hdy_window_handle_controller_new:
475 * @widget: The widget to create a controller for
476 *
477 * Creates a new #HdyWindowHandleController for @widget.
478 *
479 * Returns: (transfer full): a newly created #HdyWindowHandleController
480 *
481 * Since: 1.0
482 */
483 HdyWindowHandleController *
hdy_window_handle_controller_new(GtkWidget * widget)484 hdy_window_handle_controller_new (GtkWidget *widget)
485 {
486 HdyWindowHandleController *self;
487
488 g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
489
490 self = g_object_new (HDY_TYPE_WINDOW_HANDLE_CONTROLLER, NULL);
491
492 /* The object is intended to have the same life cycle as the widget,
493 * so we don't ref it. */
494 self->widget = widget;
495 self->multipress_gesture = g_object_new (GTK_TYPE_GESTURE_MULTI_PRESS,
496 "widget", widget,
497 "button", 0,
498 NULL);
499 g_signal_connect_object (self->multipress_gesture,
500 "pressed",
501 G_CALLBACK (pressed_cb),
502 self,
503 0);
504
505 gtk_widget_add_events (widget,
506 GDK_BUTTON_PRESS_MASK |
507 GDK_BUTTON_RELEASE_MASK |
508 GDK_BUTTON_MOTION_MASK |
509 GDK_TOUCH_MASK);
510
511 gtk_style_context_add_class (gtk_widget_get_style_context (widget),
512 "windowhandle");
513
514 return self;
515 }
516