/* GTK - The GIMP Toolkit
* Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see .
*
* SPDX-License-Identifier: LGPL-2.1+
*/
/*
* Modified by the GTK+ Team and others 1997-2000. See the AUTHORS
* file for a list of people on the GTK+ Team. See the ChangeLog
* files for a list of changes. These files are distributed with
* GTK+ at ftp://ftp.gtk.org/pub/gtk/.
*/
/* Most of the file is based on bits of code from GtkWindow */
#include "config.h"
#include "gtk-window-private.h"
#include "hdy-window-handle-controller-private.h"
#include
/**
* PRIVATE:hdy-window-handle-controller
* @short_description: An oblect that makes widgets behave like titlebars.
* @Title: HdyWindowHandleController
* @See_also: #HdyHeaderBar, #HdyWindowHandle
* @stability: Private
*
* When HdyWindowHandleController is added to the widget, dragging that widget
* will move the window, and right click, double click and middle click will be
* handled as if that widget was a titlebar. Currently it's used to implement
* these properties in #HdyWindowHandle and #HdyHeaderBar
*
* Since: 1.0
*/
struct _HdyWindowHandleController
{
GObject parent;
GtkWidget *widget;
GtkGesture *multipress_gesture;
GtkWidget *fallback_menu;
gboolean keep_above;
};
G_DEFINE_TYPE (HdyWindowHandleController, hdy_window_handle_controller, G_TYPE_OBJECT);
static GtkWindow *
get_window (HdyWindowHandleController *self)
{
GtkWidget *toplevel = gtk_widget_get_toplevel (self->widget);
if (GTK_IS_WINDOW (toplevel))
return GTK_WINDOW (toplevel);
return NULL;
}
static void
popup_menu_detach (GtkWidget *widget,
GtkMenu *menu)
{
HdyWindowHandleController *self;
self = g_object_steal_data (G_OBJECT (menu), "hdywindowhandlecontroller");
self->fallback_menu = NULL;
}
static void
restore_window_cb (GtkMenuItem *menuitem,
HdyWindowHandleController *self)
{
GtkWindow *window = get_window (self);
GdkWindowState state;
if (!window)
return;
if (gtk_window_is_maximized (window)) {
gtk_window_unmaximize (window);
return;
}
state = hdy_gtk_window_get_state (window);
if (state & GDK_WINDOW_STATE_ICONIFIED)
gtk_window_deiconify (window);
}
static void
move_window_cb (GtkMenuItem *menuitem,
HdyWindowHandleController *self)
{
GtkWindow *window = get_window (self);
if (!window)
return;
gtk_window_begin_move_drag (window,
0, /* 0 means "use keyboard" */
0, 0,
GDK_CURRENT_TIME);
}
static void
resize_window_cb (GtkMenuItem *menuitem,
HdyWindowHandleController *self)
{
GtkWindow *window = get_window (self);
if (!window)
return;
gtk_window_begin_resize_drag (window,
0,
0, /* 0 means "use keyboard" */
0, 0,
GDK_CURRENT_TIME);
}
static void
minimize_window_cb (GtkMenuItem *menuitem,
HdyWindowHandleController *self)
{
GtkWindow *window = get_window (self);
if (!window)
return;
/* Turns out, we can't iconify a maximized window */
if (gtk_window_is_maximized (window))
gtk_window_unmaximize (window);
gtk_window_iconify (window);
}
static void
maximize_window_cb (GtkMenuItem *menuitem,
HdyWindowHandleController *self)
{
GtkWindow *window = get_window (self);
GdkWindowState state;
if (!window)
return;
state = hdy_gtk_window_get_state (window);
if (state & GDK_WINDOW_STATE_ICONIFIED)
gtk_window_deiconify (window);
gtk_window_maximize (window);
}
static void
ontop_window_cb (GtkMenuItem *menuitem,
HdyWindowHandleController *self)
{
GtkWindow *window = get_window (self);
if (!window)
return;
/*
* FIXME: It will go out of sync if something else calls
* gtk_window_set_keep_above(), so we need to actually track it.
* For some reason this doesn't seem to be reflected in the
* window state.
*/
self->keep_above = !self->keep_above;
gtk_window_set_keep_above (window, self->keep_above);
}
static void
close_window_cb (GtkMenuItem *menuitem,
HdyWindowHandleController *self)
{
GtkWindow *window = get_window (self);
if (!window)
return;
gtk_window_close (window);
}
static void
do_popup (HdyWindowHandleController *self,
GdkEventButton *event)
{
GtkWindow *window = get_window (self);
GtkWidget *menuitem;
GdkWindowState state;
gboolean maximized, iconified, resizable;
GdkWindowTypeHint type_hint;
if (!window)
return;
if (gdk_window_show_window_menu (gtk_widget_get_window (GTK_WIDGET (window)),
(GdkEvent *) event))
return;
if (self->fallback_menu)
gtk_widget_destroy (self->fallback_menu);
state = hdy_gtk_window_get_state (window);
iconified = (state & GDK_WINDOW_STATE_ICONIFIED) == GDK_WINDOW_STATE_ICONIFIED;
maximized = gtk_window_is_maximized (window) && !iconified;
resizable = gtk_window_get_resizable (window);
type_hint = gtk_window_get_type_hint (window);
self->fallback_menu = gtk_menu_new ();
gtk_style_context_add_class (gtk_widget_get_style_context (self->fallback_menu),
GTK_STYLE_CLASS_CONTEXT_MENU);
/* We can't pass self to popup_menu_detach, so will have to use custom data */
g_object_set_data (G_OBJECT (self->fallback_menu),
"hdywindowhandlecontroller", self);
gtk_menu_attach_to_widget (GTK_MENU (self->fallback_menu),
self->widget,
popup_menu_detach);
menuitem = gtk_menu_item_new_with_label (_("Restore"));
gtk_widget_show (menuitem);
/* "Restore" means "Unmaximize" or "Unminimize"
* (yes, some WMs allow window menu to be shown for minimized windows).
* Not restorable:
* - visible windows that are not maximized or minimized
* - non-resizable windows that are not minimized
* - non-normal windows
*/
if ((gtk_widget_is_visible (GTK_WIDGET (window)) &&
!(maximized || iconified)) ||
(!iconified && !resizable) ||
type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
gtk_widget_set_sensitive (menuitem, FALSE);
g_signal_connect (G_OBJECT (menuitem), "activate",
G_CALLBACK (restore_window_cb), self);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_menu_item_new_with_label (_("Move"));
gtk_widget_show (menuitem);
if (maximized || iconified)
gtk_widget_set_sensitive (menuitem, FALSE);
g_signal_connect (G_OBJECT (menuitem), "activate",
G_CALLBACK (move_window_cb), self);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_menu_item_new_with_label (_("Resize"));
gtk_widget_show (menuitem);
if (!resizable || maximized || iconified)
gtk_widget_set_sensitive (menuitem, FALSE);
g_signal_connect (G_OBJECT (menuitem), "activate",
G_CALLBACK (resize_window_cb), self);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_menu_item_new_with_label (_("Minimize"));
gtk_widget_show (menuitem);
if (iconified ||
type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
gtk_widget_set_sensitive (menuitem, FALSE);
g_signal_connect (G_OBJECT (menuitem), "activate",
G_CALLBACK (minimize_window_cb), self);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_menu_item_new_with_label (_("Maximize"));
gtk_widget_show (menuitem);
if (maximized ||
!resizable ||
type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
gtk_widget_set_sensitive (menuitem, FALSE);
g_signal_connect (G_OBJECT (menuitem), "activate",
G_CALLBACK (maximize_window_cb), self);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_separator_menu_item_new ();
gtk_widget_show (menuitem);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_check_menu_item_new_with_label (_("Always on Top"));
gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menuitem), self->keep_above);
if (maximized)
gtk_widget_set_sensitive (menuitem, FALSE);
gtk_widget_show (menuitem);
g_signal_connect (G_OBJECT (menuitem), "activate",
G_CALLBACK (ontop_window_cb), self);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_separator_menu_item_new ();
gtk_widget_show (menuitem);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
menuitem = gtk_menu_item_new_with_label (_("Close"));
gtk_widget_show (menuitem);
if (!gtk_window_get_deletable (window))
gtk_widget_set_sensitive (menuitem, FALSE);
g_signal_connect (G_OBJECT (menuitem), "activate",
G_CALLBACK (close_window_cb), self);
gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
gtk_menu_popup_at_pointer (GTK_MENU (self->fallback_menu), (GdkEvent *) event);
}
static gboolean
titlebar_action (HdyWindowHandleController *self,
const GdkEvent *event,
guint button)
{
GtkSettings *settings;
g_autofree gchar *action = NULL;
GtkWindow *window = get_window (self);
if (!window)
return FALSE;
settings = gtk_widget_get_settings (GTK_WIDGET (window));
switch (button) {
case GDK_BUTTON_PRIMARY:
g_object_get (settings, "gtk-titlebar-double-click", &action, NULL);
break;
case GDK_BUTTON_MIDDLE:
g_object_get (settings, "gtk-titlebar-middle-click", &action, NULL);
break;
case GDK_BUTTON_SECONDARY:
g_object_get (settings, "gtk-titlebar-right-click", &action, NULL);
break;
default:
g_assert_not_reached ();
}
if (action == NULL)
return FALSE;
if (g_str_equal (action, "none"))
return FALSE;
if (g_str_has_prefix (action, "toggle-maximize")) {
/*
* gtk header bar won't show the maximize button if the following
* properties are not met, apply the same to title bar actions for
* consistency.
*/
if (gtk_window_get_resizable (window) &&
gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL)
hdy_gtk_window_toggle_maximized (window);
return TRUE;
}
if (g_str_equal (action, "lower")) {
gdk_window_lower (gtk_widget_get_window (GTK_WIDGET (window)));
return TRUE;
}
if (g_str_equal (action, "minimize")) {
gdk_window_iconify (gtk_widget_get_window (GTK_WIDGET (window)));
return TRUE;
}
if (g_str_equal (action, "menu")) {
do_popup (self, (GdkEventButton*) event);
return TRUE;
}
g_warning ("Unsupported titlebar action %s", action);
return FALSE;
}
static void
pressed_cb (GtkGestureMultiPress *gesture,
gint n_press,
gdouble x,
gdouble y,
HdyWindowHandleController *self)
{
GtkWidget *window = gtk_widget_get_toplevel (self->widget);
GdkEventSequence *sequence =
gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
const GdkEvent *event =
gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);
guint button =
gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
if (!event)
return;
if (gdk_display_device_is_grabbed (gtk_widget_get_display (window),
gtk_gesture_get_device (GTK_GESTURE (gesture))))
return;
switch (button) {
case GDK_BUTTON_PRIMARY:
gdk_window_raise (gtk_widget_get_window (window));
if (n_press == 2)
titlebar_action (self, event, button);
if (gtk_widget_has_grab (window))
gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
sequence, GTK_EVENT_SEQUENCE_CLAIMED);
break;
case GDK_BUTTON_SECONDARY:
if (titlebar_action (self, event, button))
gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
sequence, GTK_EVENT_SEQUENCE_CLAIMED);
gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
break;
case GDK_BUTTON_MIDDLE:
if (titlebar_action (self, event, button))
gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
sequence, GTK_EVENT_SEQUENCE_CLAIMED);
break;
default:
break;
}
}
static void
hdy_window_handle_controller_finalize (GObject *object)
{
HdyWindowHandleController *self = (HdyWindowHandleController *)object;
self->widget = NULL;
g_clear_object (&self->multipress_gesture);
g_clear_object (&self->fallback_menu);
G_OBJECT_CLASS (hdy_window_handle_controller_parent_class)->finalize (object);
}
static void
hdy_window_handle_controller_class_init (HdyWindowHandleControllerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = hdy_window_handle_controller_finalize;
}
static void
hdy_window_handle_controller_init (HdyWindowHandleController *self)
{
}
/**
* hdy_window_handle_controller_new:
* @widget: The widget to create a controller for
*
* Creates a new #HdyWindowHandleController for @widget.
*
* Returns: (transfer full): a newly created #HdyWindowHandleController
*
* Since: 1.0
*/
HdyWindowHandleController *
hdy_window_handle_controller_new (GtkWidget *widget)
{
HdyWindowHandleController *self;
g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
self = g_object_new (HDY_TYPE_WINDOW_HANDLE_CONTROLLER, NULL);
/* The object is intended to have the same life cycle as the widget,
* so we don't ref it. */
self->widget = widget;
self->multipress_gesture = g_object_new (GTK_TYPE_GESTURE_MULTI_PRESS,
"widget", widget,
"button", 0,
NULL);
g_signal_connect_object (self->multipress_gesture,
"pressed",
G_CALLBACK (pressed_cb),
self,
0);
gtk_widget_add_events (widget,
GDK_BUTTON_PRESS_MASK |
GDK_BUTTON_RELEASE_MASK |
GDK_BUTTON_MOTION_MASK |
GDK_TOUCH_MASK);
gtk_style_context_add_class (gtk_widget_get_style_context (widget),
"windowhandle");
return self;
}