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