1/*
2 * Copyright (c) 2020 Alexander Mikhaylenko <alexm@gnome.org>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or (at your
7 * option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12 * License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program; if not, write to the Free Software Foundation,
16 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19#include "config.h"
20
21#include "gtkwindowhandle.h"
22
23#include "gtkactionmuxerprivate.h"
24#include "gtkbinlayout.h"
25#include "gtkbox.h"
26#include "gtkbuildable.h"
27#include "gtkdragsourceprivate.h"
28#include "gtkgestureclick.h"
29#include "gtkgesturedrag.h"
30#include "gtkgestureprivate.h"
31#include "gtkintl.h"
32#include "gtkmodelbuttonprivate.h"
33#include "gtknative.h"
34#include "gtkpopovermenuprivate.h"
35#include "gtkprivate.h"
36#include "gtkseparator.h"
37#include "gtkwidgetprivate.h"
38
39
40/**
41 * GtkWindowHandle:
42 *
43 * `GtkWindowHandle` is a titlebar area widget.
44 *
45 * When added into a window, it can be dragged to move the window, and handles
46 * right click, double click and middle click as expected of a titlebar.
47 *
48 * # CSS nodes
49 *
50 * `GtkWindowHandle` has a single CSS node with the name `windowhandle`.
51 *
52 * # Accessibility
53 *
54 * `GtkWindowHandle` uses the %GTK_ACCESSIBLE_ROLE_GROUP role.
55 */
56
57struct _GtkWindowHandle {
58 GtkWidget parent_instance;
59
60 GtkGesture *click_gesture;
61 GtkGesture *drag_gesture;
62
63 GtkWidget *child;
64 GtkWidget *fallback_menu;
65};
66
67enum {
68 PROP_0,
69 PROP_CHILD,
70 LAST_PROP
71};
72
73static GParamSpec *props[LAST_PROP] = { NULL, };
74
75static void gtk_window_handle_buildable_iface_init (GtkBuildableIface *iface);
76
77G_DEFINE_TYPE_WITH_CODE (GtkWindowHandle, gtk_window_handle, GTK_TYPE_WIDGET,
78 G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_window_handle_buildable_iface_init))
79
80static void
81lower_window (GtkWindowHandle *self)
82{
83 GdkSurface *surface =
84 gtk_native_get_surface (self: gtk_widget_get_native (GTK_WIDGET (self)));
85
86 gdk_toplevel_lower (toplevel: GDK_TOPLEVEL (ptr: surface));
87}
88
89static GtkWindow *
90get_window (GtkWindowHandle *self)
91{
92 GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
93
94 if (GTK_IS_WINDOW (root))
95 return GTK_WINDOW (root);
96
97 return NULL;
98}
99
100static void
101restore_window_clicked (GtkModelButton *button,
102 GtkWindowHandle *self)
103{
104 GtkWindow *window = get_window (self);
105
106 if (!window)
107 return;
108
109 if (gtk_window_is_maximized (window))
110 gtk_window_unmaximize (window);
111}
112
113static void
114minimize_window_clicked (GtkModelButton *button,
115 GtkWindowHandle *self)
116{
117 GtkWindow *window = get_window (self);
118
119 if (!window)
120 return;
121
122 /* Turns out, we can't minimize a maximized window */
123 if (gtk_window_is_maximized (window))
124 gtk_window_unmaximize (window);
125
126 gtk_window_minimize (window);
127}
128
129static void
130maximize_window_clicked (GtkModelButton *button,
131 GtkWindowHandle *self)
132{
133 GtkWindow *window = get_window (self);
134
135 if (window)
136 gtk_window_maximize (window);
137}
138
139static void
140close_window_clicked (GtkModelButton *button,
141 GtkWindowHandle *self)
142{
143 GtkWindow *window = get_window (self);
144
145 if (window)
146 gtk_window_close (window);
147}
148
149static void
150popup_menu_closed (GtkPopover *popover,
151 GtkWindowHandle *self)
152{
153 g_clear_pointer (&self->fallback_menu, gtk_widget_unparent);
154}
155
156static void
157do_popup_fallback (GtkWindowHandle *self,
158 GdkEvent *event)
159{
160 GdkRectangle rect = { 0, 0, 1, 1 };
161 GdkDevice *device;
162 GdkSeat *seat;
163 GtkWidget *box, *menuitem;
164 GtkWindow *window;
165 gboolean maximized, resizable, deletable;
166
167 g_clear_pointer (&self->fallback_menu, gtk_widget_unparent);
168
169 window = get_window (self);
170
171 if (window)
172 {
173 maximized = gtk_window_is_maximized (window);
174 resizable = gtk_window_get_resizable (window);
175 deletable = gtk_window_get_deletable (window);
176 }
177 else
178 {
179 maximized = FALSE;
180 resizable = FALSE;
181 deletable = FALSE;
182 }
183
184 self->fallback_menu = gtk_popover_menu_new ();
185 gtk_widget_set_parent (widget: self->fallback_menu, GTK_WIDGET (self));
186
187 gtk_popover_set_has_arrow (GTK_POPOVER (self->fallback_menu), FALSE);
188 gtk_widget_set_halign (widget: self->fallback_menu, align: GTK_ALIGN_START);
189
190
191 device = gdk_event_get_device (event);
192 seat = gdk_event_get_seat (event);
193
194 if (device == gdk_seat_get_keyboard (seat))
195 device = gdk_seat_get_pointer (seat);
196
197 if (device)
198 {
199 GtkNative *native;
200 GdkSurface *surface;
201 double px, py;
202 double nx, ny;
203
204 native = gtk_widget_get_native (GTK_WIDGET (self));
205 surface = gtk_native_get_surface (self: native);
206 gdk_surface_get_device_position (surface, device, x: &px, y: &py, NULL);
207 gtk_native_get_surface_transform (self: native, x: &nx, y: &ny);
208
209 gtk_widget_translate_coordinates (GTK_WIDGET (gtk_widget_get_native (GTK_WIDGET (self))),
210 GTK_WIDGET (self),
211 src_x: px - nx, src_y: py - ny,
212 dest_x: &px, dest_y: &py);
213 rect.x = px;
214 rect.y = py;
215 }
216
217 gtk_popover_set_pointing_to (GTK_POPOVER (self->fallback_menu), rect: &rect);
218
219 box = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 0);
220 gtk_popover_menu_add_submenu (GTK_POPOVER_MENU (self->fallback_menu), submenu: box, name: "main");
221
222 menuitem = gtk_model_button_new ();
223 g_object_set (object: menuitem, first_property_name: "text", _("Restore"), NULL);
224 gtk_widget_set_sensitive (widget: menuitem, sensitive: maximized && resizable);
225 g_signal_connect (G_OBJECT (menuitem), "clicked",
226 G_CALLBACK (restore_window_clicked), self);
227 gtk_box_append (GTK_BOX (box), child: menuitem);
228
229 menuitem = gtk_model_button_new ();
230 g_object_set (object: menuitem, first_property_name: "text", _("Minimize"), NULL);
231 g_signal_connect (G_OBJECT (menuitem), "clicked",
232 G_CALLBACK (minimize_window_clicked), self);
233 gtk_box_append (GTK_BOX (box), child: menuitem);
234
235 menuitem = gtk_model_button_new ();
236 g_object_set (object: menuitem, first_property_name: "text", _("Maximize"), NULL);
237 gtk_widget_set_sensitive (widget: menuitem, sensitive: resizable && !maximized);
238 g_signal_connect (G_OBJECT (menuitem), "clicked",
239 G_CALLBACK (maximize_window_clicked), self);
240 gtk_box_append (GTK_BOX (box), child: menuitem);
241
242 menuitem = gtk_separator_new (orientation: GTK_ORIENTATION_HORIZONTAL);
243 gtk_box_append (GTK_BOX (box), child: menuitem);
244
245 menuitem = gtk_model_button_new ();
246 g_object_set (object: menuitem, first_property_name: "text", _("Close"), NULL);
247 gtk_widget_set_sensitive (widget: menuitem, sensitive: deletable);
248 g_signal_connect (G_OBJECT (menuitem), "clicked",
249 G_CALLBACK (close_window_clicked), self);
250 gtk_box_append (GTK_BOX (box), child: menuitem);
251
252 g_signal_connect (self->fallback_menu, "closed",
253 G_CALLBACK (popup_menu_closed), self);
254 gtk_popover_popup (GTK_POPOVER (self->fallback_menu));
255}
256
257static void
258do_popup (GtkWindowHandle *self,
259 GdkEvent *event)
260{
261 GdkSurface *surface =
262 gtk_native_get_surface (self: gtk_widget_get_native (GTK_WIDGET (self)));
263
264 if (!gdk_toplevel_show_window_menu (toplevel: GDK_TOPLEVEL (ptr: surface), event))
265 do_popup_fallback (self, event);
266}
267
268static gboolean
269perform_titlebar_action_fallback (GtkWindowHandle *self,
270 GdkEvent *event,
271 GdkTitlebarGesture gesture)
272{
273 GtkSettings *settings;
274 char *action = NULL;
275 gboolean retval = TRUE;
276
277 settings = gtk_widget_get_settings (GTK_WIDGET (self));
278 switch (gesture)
279 {
280 case GDK_TITLEBAR_GESTURE_DOUBLE_CLICK:
281 g_object_get (object: settings, first_property_name: "gtk-titlebar-double-click", &action, NULL);
282 break;
283 case GDK_TITLEBAR_GESTURE_MIDDLE_CLICK:
284 g_object_get (object: settings, first_property_name: "gtk-titlebar-middle-click", &action, NULL);
285 break;
286 case GDK_TITLEBAR_GESTURE_RIGHT_CLICK:
287 g_object_get (object: settings, first_property_name: "gtk-titlebar-right-click", &action, NULL);
288 break;
289 default:
290 break;
291 }
292
293 if (action == NULL)
294 retval = FALSE;
295 else if (g_str_equal (v1: action, v2: "none"))
296 retval = FALSE;
297 /* treat all maximization variants the same */
298 else if (g_str_has_prefix (str: action, prefix: "toggle-maximize"))
299 gtk_widget_activate_action (GTK_WIDGET (self),
300 name: "window.toggle-maximized",
301 NULL);
302 else if (g_str_equal (v1: action, v2: "lower"))
303 lower_window (self);
304 else if (g_str_equal (v1: action, v2: "minimize"))
305 gtk_widget_activate_action (GTK_WIDGET (self),
306 name: "window.minimize",
307 NULL);
308 else if (g_str_equal (v1: action, v2: "menu"))
309 do_popup (self, event);
310 else
311 {
312 g_warning ("Unsupported titlebar action %s", action);
313 retval = FALSE;
314 }
315
316 g_free (mem: action);
317
318 return retval;
319}
320
321static gboolean
322perform_titlebar_action (GtkWindowHandle *self,
323 GdkEvent *event,
324 guint button,
325 int n_press)
326{
327 GdkSurface *surface =
328 gtk_native_get_surface (self: gtk_widget_get_native (GTK_WIDGET (self)));
329 GdkTitlebarGesture gesture;
330
331 switch (button)
332 {
333 case GDK_BUTTON_PRIMARY:
334 if (n_press == 2)
335 gesture = GDK_TITLEBAR_GESTURE_DOUBLE_CLICK;
336 else
337 return FALSE;
338 break;
339 case GDK_BUTTON_MIDDLE:
340 gesture = GDK_TITLEBAR_GESTURE_MIDDLE_CLICK;
341 break;
342 case GDK_BUTTON_SECONDARY:
343 gesture = GDK_TITLEBAR_GESTURE_RIGHT_CLICK;
344 break;
345 default:
346 return FALSE;
347 }
348
349 if (gdk_toplevel_titlebar_gesture (toplevel: GDK_TOPLEVEL (ptr: surface), gesture))
350 return TRUE;
351
352 return perform_titlebar_action_fallback (self, event, gesture);
353}
354
355static void
356click_gesture_pressed_cb (GtkGestureClick *gesture,
357 int n_press,
358 double x,
359 double y,
360 GtkWindowHandle *self)
361{
362 GtkWidget *widget;
363 GdkEventSequence *sequence;
364 GdkEvent *event;
365 guint button;
366
367 widget = GTK_WIDGET (self);
368 sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
369 button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
370 event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);
371
372 if (!event)
373 return;
374
375 if (n_press > 1)
376 gtk_gesture_set_state (gesture: self->drag_gesture, state: GTK_EVENT_SEQUENCE_DENIED);
377
378 if (gdk_display_device_is_grabbed (display: gtk_widget_get_display (widget),
379 device: gtk_gesture_get_device (GTK_GESTURE (gesture))))
380 {
381 gtk_gesture_set_state (gesture: self->drag_gesture, state: GTK_EVENT_SEQUENCE_DENIED);
382 return;
383 }
384
385 switch (button)
386 {
387 case GDK_BUTTON_PRIMARY:
388 if (n_press == 2)
389 {
390 perform_titlebar_action (self, event, button, n_press);
391 gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
392 sequence, state: GTK_EVENT_SEQUENCE_CLAIMED);
393 }
394 break;
395
396 case GDK_BUTTON_SECONDARY:
397 if (perform_titlebar_action (self, event, button, n_press))
398 gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
399 sequence, state: GTK_EVENT_SEQUENCE_CLAIMED);
400
401 gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
402 gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->drag_gesture));
403 break;
404
405 case GDK_BUTTON_MIDDLE:
406 if (perform_titlebar_action (self, event, button, n_press))
407 gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
408 sequence, state: GTK_EVENT_SEQUENCE_CLAIMED);
409 break;
410
411 default:
412 return;
413 }
414}
415
416static void
417drag_gesture_update_cb (GtkGestureDrag *gesture,
418 double offset_x,
419 double offset_y,
420 GtkWindowHandle *self)
421{
422 if (gtk_drag_check_threshold_double (GTK_WIDGET (self), start_x: 0, start_y: 0, current_x: offset_x, current_y: offset_y))
423 {
424 double start_x, start_y;
425 double native_x, native_y;
426 double window_x, window_y;
427 GtkNative *native;
428 GdkSurface *surface;
429
430 gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_CLAIMED);
431
432 gtk_gesture_drag_get_start_point (gesture, x: &start_x, y: &start_y);
433
434 native = gtk_widget_get_native (GTK_WIDGET (self));
435
436 gtk_widget_translate_coordinates (GTK_WIDGET (self),
437 GTK_WIDGET (native),
438 src_x: start_x, src_y: start_y,
439 dest_x: &window_x, dest_y: &window_y);
440
441 gtk_native_get_surface_transform (self: native, x: &native_x, y: &native_y);
442 window_x += native_x;
443 window_y += native_y;
444
445 surface = gtk_native_get_surface (self: native);
446 if (GDK_IS_TOPLEVEL (ptr: surface))
447 gdk_toplevel_begin_move (toplevel: GDK_TOPLEVEL (ptr: surface),
448 device: gtk_gesture_get_device (GTK_GESTURE (gesture)),
449 button: gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)),
450 x: window_x, y: window_y,
451 timestamp: gtk_event_controller_get_current_event_time (GTK_EVENT_CONTROLLER (gesture)));
452
453 gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
454 gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->click_gesture));
455 }
456}
457
458static void
459gtk_window_handle_unrealize (GtkWidget *widget)
460{
461 GtkWindowHandle *self = GTK_WINDOW_HANDLE (ptr: widget);
462
463 g_clear_pointer (&self->fallback_menu, gtk_widget_unparent);
464
465 GTK_WIDGET_CLASS (gtk_window_handle_parent_class)->unrealize (widget);
466}
467
468static void
469gtk_window_handle_dispose (GObject *object)
470{
471 GtkWindowHandle *self = GTK_WINDOW_HANDLE (ptr: object);
472
473 g_clear_pointer (&self->child, gtk_widget_unparent);
474
475 G_OBJECT_CLASS (gtk_window_handle_parent_class)->dispose (object);
476}
477
478static void
479gtk_window_handle_get_property (GObject *object,
480 guint prop_id,
481 GValue *value,
482 GParamSpec *pspec)
483{
484 GtkWindowHandle *self = GTK_WINDOW_HANDLE (ptr: object);
485
486 switch (prop_id)
487 {
488 case PROP_CHILD:
489 g_value_set_object (value, v_object: gtk_window_handle_get_child (self));
490 break;
491
492 default:
493 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
494 break;
495 }
496}
497
498static void
499gtk_window_handle_set_property (GObject *object,
500 guint prop_id,
501 const GValue *value,
502 GParamSpec *pspec)
503{
504 GtkWindowHandle *self = GTK_WINDOW_HANDLE (ptr: object);
505
506 switch (prop_id)
507 {
508 case PROP_CHILD:
509 gtk_window_handle_set_child (self, child: g_value_get_object (value));
510 break;
511
512 default:
513 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
514 break;
515 }
516}
517
518static void
519gtk_window_handle_class_init (GtkWindowHandleClass *klass)
520{
521 GObjectClass *object_class = G_OBJECT_CLASS (klass);
522 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
523
524 object_class->dispose = gtk_window_handle_dispose;
525 object_class->get_property = gtk_window_handle_get_property;
526 object_class->set_property = gtk_window_handle_set_property;
527
528 widget_class->unrealize = gtk_window_handle_unrealize;
529
530 /**
531 * GtkWindowHandle:child: (attributes org.gtk.Property.get=gtk_window_handle_get_child org.gtk.Property.set=gtk_window_handle_set_child)
532 *
533 * The child widget.
534 */
535 props[PROP_CHILD] =
536 g_param_spec_object (name: "child",
537 P_("Child"),
538 P_("The child widget"),
539 GTK_TYPE_WIDGET,
540 GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
541
542 g_object_class_install_properties (oclass: object_class, n_pspecs: LAST_PROP, pspecs: props);
543
544 gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
545 gtk_widget_class_set_css_name (widget_class, I_("windowhandle"));
546 gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_GROUP);
547}
548
549static void
550gtk_window_handle_init (GtkWindowHandle *self)
551{
552 self->click_gesture = gtk_gesture_click_new ();
553 gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->click_gesture), button: 0);
554 g_signal_connect (self->click_gesture, "pressed",
555 G_CALLBACK (click_gesture_pressed_cb), self);
556 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (self->click_gesture));
557
558 self->drag_gesture = gtk_gesture_drag_new ();
559 g_signal_connect (self->drag_gesture, "drag-update",
560 G_CALLBACK (drag_gesture_update_cb), self);
561 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (self->drag_gesture));
562}
563
564static GtkBuildableIface *parent_buildable_iface;
565
566static void
567gtk_window_handle_buildable_add_child (GtkBuildable *buildable,
568 GtkBuilder *builder,
569 GObject *child,
570 const char *type)
571{
572 if (GTK_IS_WIDGET (child))
573 gtk_window_handle_set_child (self: GTK_WINDOW_HANDLE (ptr: buildable), GTK_WIDGET (child));
574 else
575 parent_buildable_iface->add_child (buildable, builder, child, type);
576}
577
578static void
579gtk_window_handle_buildable_iface_init (GtkBuildableIface *iface)
580{
581 parent_buildable_iface = g_type_interface_peek_parent (g_iface: iface);
582
583 iface->add_child = gtk_window_handle_buildable_add_child;
584}
585
586/**
587 * gtk_window_handle_new:
588 *
589 * Creates a new `GtkWindowHandle`.
590 *
591 * Returns: a new `GtkWindowHandle`.
592 */
593GtkWidget *
594gtk_window_handle_new (void)
595{
596 return g_object_new (GTK_TYPE_WINDOW_HANDLE, NULL);
597}
598
599/**
600 * gtk_window_handle_get_child: (attributes org.gtk.Method.get_property=child)
601 * @self: a `GtkWindowHandle`
602 *
603 * Gets the child widget of @self.
604 *
605 * Returns: (nullable) (transfer none): the child widget of @self
606 */
607GtkWidget *
608gtk_window_handle_get_child (GtkWindowHandle *self)
609{
610 g_return_val_if_fail (GTK_IS_WINDOW_HANDLE (self), NULL);
611
612 return self->child;
613}
614
615/**
616 * gtk_window_handle_set_child: (attributes org.gtk.Method.set_property=child)
617 * @self: a `GtkWindowHandle`
618 * @child: (nullable): the child widget
619 *
620 * Sets the child widget of @self.
621 */
622void
623gtk_window_handle_set_child (GtkWindowHandle *self,
624 GtkWidget *child)
625{
626 g_return_if_fail (GTK_IS_WINDOW_HANDLE (self));
627 g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));
628
629 if (self->child == child)
630 return;
631
632 g_clear_pointer (&self->child, gtk_widget_unparent);
633
634 self->child = child;
635
636 if (child)
637 gtk_widget_set_parent (widget: child, GTK_WIDGET (self));
638
639 g_object_notify_by_pspec (G_OBJECT (self), pspec: props[PROP_CHILD]);
640}
641

source code of gtk/gtk/gtkwindowhandle.c