/* dzl-application-window.c
*
* Copyright (C) 2017 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define G_LOG_DOMAIN "dzl-application-window"
#include "config.h"
#include "app/dzl-application-window.h"
#include "shortcuts/dzl-shortcut-manager.h"
#include "util/dzl-gtk.h"
#define DEFAULT_DISMISSAL_SECONDS 3
#define SHOW_HEADER_WITHIN_DISTANCE 5
/**
* SECTION:dzl-application-window
* @title: DzlApplicationWindow
* @short_description: An base application window for applications
*
* The #DzlApplicationWindow class provides a #GtkApplicationWindow subclass
* that integrates well with #DzlApplication. It provides features such as:
*
* - Integration with the #DzlShortcutManager for capture/bubble keyboard
* input events.
* - Native support for fullscreen state by re-parenting the #GtkHeaderBar as
* necessary. #DzlApplicationWindow does expect you to use GtkHeaderBar.
*
* Since: 3.26
*/
typedef struct
{
GtkStack *titlebar_container;
GtkRevealer *titlebar_revealer;
GtkEventBox *event_box;
GtkOverlay *overlay;
gulong motion_notify_handler;
guint fullscreen_source;
guint fullscreen_reveal_source;
guint fullscreen : 1;
guint in_key_press : 1;
} DzlApplicationWindowPrivate;
enum {
PROP_0,
PROP_FULLSCREEN,
N_PROPS
};
static void buildable_iface_init (GtkBuildableIface *iface);
G_DEFINE_TYPE_WITH_CODE (DzlApplicationWindow, dzl_application_window, GTK_TYPE_APPLICATION_WINDOW,
G_ADD_PRIVATE (DzlApplicationWindow)
G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
static GParamSpec *properties [N_PROPS];
static GtkBuildableIface *parent_buildable;
static gboolean
dzl_application_window_dismissal (DzlApplicationWindow *self)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_assert (DZL_IS_APPLICATION_WINDOW (self));
priv->fullscreen_reveal_source = 0;
if (dzl_application_window_get_fullscreen (self))
gtk_revealer_set_reveal_child (priv->titlebar_revealer, FALSE);
return G_SOURCE_REMOVE;
}
static void
dzl_application_window_queue_dismissal (DzlApplicationWindow *self)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_assert (DZL_IS_APPLICATION_WINDOW (self));
if (priv->fullscreen_reveal_source != 0)
g_source_remove (priv->fullscreen_reveal_source);
priv->fullscreen_reveal_source =
gdk_threads_add_timeout_seconds_full (G_PRIORITY_LOW,
DEFAULT_DISMISSAL_SECONDS,
(GSourceFunc) dzl_application_window_dismissal,
self, NULL);
}
static gboolean
dzl_application_window_real_get_fullscreen (DzlApplicationWindow *self)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_assert (DZL_IS_APPLICATION_WINDOW (self));
return priv->fullscreen;
}
static void
revealer_set_reveal_child_now (GtkRevealer *revealer,
gboolean reveal_child)
{
g_assert (GTK_IS_REVEALER (revealer));
gtk_revealer_set_transition_type (revealer, GTK_REVEALER_TRANSITION_TYPE_NONE);
gtk_revealer_set_reveal_child (revealer, reveal_child);
gtk_revealer_set_transition_type (revealer, GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
}
static gboolean
dzl_application_window_event_box_motion (DzlApplicationWindow *self,
GdkEventMotion *motion,
GtkEventBox *event_box)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
GtkWidget *widget = NULL;
GtkWidget *focus;
gint x = 0;
gint y = 0;
g_assert (DZL_IS_APPLICATION_WINDOW (self));
g_assert (motion != NULL);
g_assert (GTK_IS_EVENT_BOX (event_box));
/*
* If we are focused in the revealer, ignore this. We will have already
* killed the GSource in set_focus().
*/
focus = gtk_window_get_focus (GTK_WINDOW (self));
if (focus != NULL &&
dzl_gtk_widget_is_ancestor_or_relative (focus, GTK_WIDGET (priv->titlebar_revealer)))
return GDK_EVENT_PROPAGATE;
/* The widget is stored in GdkWindow user_data */
gdk_window_get_user_data (motion->window, (gpointer *)&widget);
if (widget == NULL)
return GDK_EVENT_PROPAGATE;
/* If the headerbar is underneath the pointer or we are within a
* small distance of the edge of the window (and monitor), ensure
* that the titlebar is displayed and queue our next dismissal.
*/
if (dzl_gtk_widget_is_ancestor_or_relative (widget, GTK_WIDGET (priv->titlebar_revealer)) ||
gtk_widget_translate_coordinates (widget, GTK_WIDGET (self), motion->x, motion->y, &x, &y))
{
if (y < SHOW_HEADER_WITHIN_DISTANCE)
{
gtk_revealer_set_reveal_child (priv->titlebar_revealer, TRUE);
dzl_application_window_queue_dismissal (self);
}
}
return GDK_EVENT_PROPAGATE;
}
static gboolean
dzl_application_window_complete_fullscreen (DzlApplicationWindow *self)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
GtkWidget *titlebar;
g_assert (DZL_IS_APPLICATION_WINDOW (self));
priv->fullscreen_source = 0;
titlebar = dzl_application_window_get_titlebar (self);
if (titlebar == NULL)
{
g_warning ("Attempt to alter fullscreen state without a titlebar set!");
return G_SOURCE_REMOVE;
}
/*
* This is where we apply the fullscreen widget transitions.
*
* If we were toggled really fast, we could have skipped oure previous
* operation. So make sure that the revealer is in the state we expect
* it before performing further (destructive) work.
*/
g_object_ref (titlebar);
if (priv->fullscreen)
{
/* Only listen for motion notify events while in fullscreen mode. */
g_signal_handler_unblock (priv->event_box, priv->motion_notify_handler);
if (titlebar && gtk_widget_is_ancestor (titlebar, GTK_WIDGET (priv->titlebar_container)))
{
revealer_set_reveal_child_now (priv->titlebar_revealer, FALSE);
gtk_container_remove (GTK_CONTAINER (priv->titlebar_container), titlebar);
gtk_container_add (GTK_CONTAINER (priv->titlebar_revealer), titlebar);
gtk_revealer_set_reveal_child (priv->titlebar_revealer, TRUE);
dzl_application_window_queue_dismissal (self);
}
}
else
{
/* Motion events are no longer needed */
g_signal_handler_block (priv->event_box, priv->motion_notify_handler);
if (gtk_widget_is_ancestor (titlebar, GTK_WIDGET (priv->titlebar_revealer)))
{
gtk_container_remove (GTK_CONTAINER (priv->titlebar_revealer), titlebar);
gtk_container_add (GTK_CONTAINER (priv->titlebar_container), titlebar);
revealer_set_reveal_child_now (priv->titlebar_revealer, FALSE);
}
}
g_object_unref (titlebar);
return G_SOURCE_REMOVE;
}
static void
dzl_application_window_real_set_fullscreen (DzlApplicationWindow *self,
gboolean fullscreen)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_assert (DZL_IS_APPLICATION_WINDOW (self));
g_assert (priv->fullscreen != fullscreen);
priv->fullscreen = fullscreen;
if (priv->fullscreen_source != 0)
{
g_source_remove (priv->fullscreen_source);
priv->fullscreen_source = 0;
}
if (priv->fullscreen)
{
/*
* Once we go fullscreen, the headerbar will no longer be visible.
* So we will delay for a short bit until we've likely entered the
* fullscreen state, and then remove the titlebar and place it into
* the hover state.
*/
priv->fullscreen_source =
gdk_threads_add_timeout_full (G_PRIORITY_LOW,
300,
(GSourceFunc) dzl_application_window_complete_fullscreen,
self, NULL);
gtk_window_fullscreen (GTK_WINDOW (self));
}
else
{
/*
* We must apply our unfullscreen state transition immediately
* so that we have a titlebar as soon as the window changes.
*/
dzl_application_window_complete_fullscreen (self);
gtk_window_unfullscreen (GTK_WINDOW (self));
}
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FULLSCREEN]);
}
static void
dzl_application_window_set_focus (GtkWindow *window,
GtkWidget *widget)
{
DzlApplicationWindow *self = (DzlApplicationWindow *)window;
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
GtkWidget *old_focus;
gboolean titlebar_had_focus = FALSE;
g_assert (DZL_IS_APPLICATION_WINDOW (self));
g_assert (!widget || GTK_IS_WIDGET (widget));
/* Check if we have titlebar focus already */
old_focus = gtk_window_get_focus (window);
if (old_focus != NULL &&
dzl_gtk_widget_is_ancestor_or_relative (old_focus, GTK_WIDGET (priv->titlebar_revealer)))
titlebar_had_focus = TRUE;
/* Chain-up first */
GTK_WINDOW_CLASS (dzl_application_window_parent_class)->set_focus (window, widget);
/* Now see what is selected */
widget = gtk_window_get_focus (window);
if (widget != NULL)
{
if (dzl_gtk_widget_is_ancestor_or_relative (widget, GTK_WIDGET (priv->titlebar_revealer)))
{
/* Disable transition while the revealer is focused */
if (priv->fullscreen_reveal_source != 0)
{
g_source_remove (priv->fullscreen_reveal_source);
priv->fullscreen_reveal_source = 0;
}
/* If this was just focused, we might need to make it visible */
gtk_revealer_set_reveal_child (priv->titlebar_revealer, TRUE);
}
else if (titlebar_had_focus)
{
/* We are going from titlebar to non-titlebar focus. Dismiss
* the titlebar immediately to get out of the users way.
*/
gtk_revealer_set_reveal_child (priv->titlebar_revealer, FALSE);
if (priv->fullscreen_reveal_source != 0)
{
g_source_remove (priv->fullscreen_reveal_source);
priv->fullscreen_reveal_source = 0;
}
}
}
}
static void
dzl_application_window_add (GtkContainer *container,
GtkWidget *widget)
{
DzlApplicationWindow *self = (DzlApplicationWindow *)container;
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_assert (DZL_IS_APPLICATION_WINDOW (self));
g_assert (GTK_IS_WIDGET (widget));
gtk_container_add (GTK_CONTAINER (priv->event_box), widget);
}
static gboolean
dzl_application_window_key_press_event (GtkWidget *widget,
GdkEventKey *event)
{
DzlApplicationWindow *self = (DzlApplicationWindow *)widget;
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
gboolean ret;
g_assert (DZL_IS_APPLICATION_WINDOW (self));
g_assert (event != NULL);
/* Be re-entrant safe from the shortcut manager */
if (priv->in_key_press)
return GTK_WIDGET_CLASS (dzl_application_window_parent_class)->key_press_event (widget, event);
priv->in_key_press = TRUE;
ret = dzl_shortcut_manager_handle_event (NULL, event, widget);
priv->in_key_press = FALSE;
return ret;
}
static void
dzl_application_window_destroy (GtkWidget *widget)
{
DzlApplicationWindow *self = (DzlApplicationWindow *)widget;
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_assert (DZL_IS_APPLICATION_WINDOW (self));
if (priv->event_box != NULL)
{
g_signal_handler_disconnect (priv->event_box, priv->motion_notify_handler);
priv->motion_notify_handler = 0;
}
g_clear_pointer (&priv->titlebar_container, gtk_widget_destroy);
g_clear_pointer (&priv->titlebar_revealer, gtk_widget_destroy);
g_clear_pointer (&priv->event_box, gtk_widget_destroy);
g_clear_pointer (&priv->overlay, gtk_widget_destroy);
if (priv->fullscreen_source)
{
g_source_remove (priv->fullscreen_source);
priv->fullscreen_source = 0;
}
if (priv->fullscreen_reveal_source)
{
g_source_remove (priv->fullscreen_reveal_source);
priv->fullscreen_reveal_source = 0;
}
GTK_WIDGET_CLASS (dzl_application_window_parent_class)->destroy (widget);
}
static void
dzl_application_window_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
DzlApplicationWindow *self = DZL_APPLICATION_WINDOW (object);
switch (prop_id)
{
case PROP_FULLSCREEN:
g_value_set_boolean (value, dzl_application_window_get_fullscreen (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
dzl_application_window_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
DzlApplicationWindow *self = DZL_APPLICATION_WINDOW (object);
switch (prop_id)
{
case PROP_FULLSCREEN:
dzl_application_window_set_fullscreen (self, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
dzl_application_window_class_init (DzlApplicationWindowClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass);
object_class->get_property = dzl_application_window_get_property;
object_class->set_property = dzl_application_window_set_property;
widget_class->destroy = dzl_application_window_destroy;
widget_class->key_press_event = dzl_application_window_key_press_event;
container_class->add = dzl_application_window_add;
window_class->set_focus = dzl_application_window_set_focus;
klass->get_fullscreen = dzl_application_window_real_get_fullscreen;
klass->set_fullscreen = dzl_application_window_real_set_fullscreen;
/**
* DzlApplicationWindow:fullscreen:
*
* The "fullscreen" property denotes if the window is in the fullscreen
* state. The titlebar of the #DzlApplicationWindow contains a revealer
* which will be repurposed into a floating bar while the window is in
* the fullscreen mode.
*
* Set this property to %FALSE to unfullscreen.
*/
properties [PROP_FULLSCREEN] =
g_param_spec_boolean ("fullscreen",
"Fullscreen",
"If the window is fullscreen",
FALSE,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, N_PROPS, properties);
}
static void
dzl_application_window_init (DzlApplicationWindow *self)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_autoptr(GPropertyAction) fullscreen = NULL;
priv->titlebar_container = g_object_new (GTK_TYPE_STACK,
"name", "titlebar_container",
"visible", TRUE,
NULL);
g_signal_connect (priv->titlebar_container,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
&priv->titlebar_container);
gtk_window_set_titlebar (GTK_WINDOW (self), GTK_WIDGET (priv->titlebar_container));
priv->overlay = g_object_new (GTK_TYPE_OVERLAY,
"visible", TRUE,
NULL);
g_signal_connect (priv->overlay,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
&priv->overlay);
GTK_CONTAINER_CLASS (dzl_application_window_parent_class)->add (GTK_CONTAINER (self),
GTK_WIDGET (priv->overlay));
priv->event_box = g_object_new (GTK_TYPE_EVENT_BOX,
"above-child", FALSE,
"visible", TRUE,
"visible-window", TRUE,
NULL);
gtk_widget_set_events (GTK_WIDGET (priv->event_box), GDK_POINTER_MOTION_MASK);
g_signal_connect (priv->event_box,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
&priv->event_box);
priv->motion_notify_handler =
g_signal_connect_swapped (priv->event_box,
"motion-notify-event",
G_CALLBACK (dzl_application_window_event_box_motion),
self);
g_signal_handler_block (priv->event_box, priv->motion_notify_handler);
gtk_container_add (GTK_CONTAINER (priv->overlay), GTK_WIDGET (priv->event_box));
priv->titlebar_revealer = g_object_new (GTK_TYPE_REVEALER,
"valign", GTK_ALIGN_START,
"hexpand", TRUE,
"transition-duration", 500,
"transition-type", GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN,
"reveal-child", TRUE,
"visible", TRUE,
NULL);
g_signal_connect (priv->titlebar_revealer,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
&priv->titlebar_revealer);
gtk_overlay_add_overlay (priv->overlay, GTK_WIDGET (priv->titlebar_revealer));
fullscreen = g_property_action_new ("fullscreen", self, "fullscreen");
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (fullscreen));
}
/**
* dzl_application_window_get_fullscreen:
* @self: a #DzlApplicationWindow
*
* Gets if the window is in the fullscreen state.
*
* Returns: %TRUE if @self is fullscreen, otherwise %FALSE.
*
* Since: 3.26
*/
gboolean
dzl_application_window_get_fullscreen (DzlApplicationWindow *self)
{
g_return_val_if_fail (DZL_IS_APPLICATION_WINDOW (self), FALSE);
return DZL_APPLICATION_WINDOW_GET_CLASS (self)->get_fullscreen (self);
}
/**
* dzl_application_window_set_fullscreen:
* @self: a #DzlApplicationWindow
* @fullscreen: if the window should be in the fullscreen state
*
* Sets the #DzlApplicationWindow into either the fullscreen or unfullscreen
* state based on @fullscreen.
*
* The titlebar for the window is contained within a #GtkRevealer which is
* repurposed as a floating bar when the application is in fullscreen mode.
*
* See dzl_application_window_get_fullscreen() to get the current fullscreen
* state.
*
* Since: 3.26
*/
void
dzl_application_window_set_fullscreen (DzlApplicationWindow *self,
gboolean fullscreen)
{
g_return_if_fail (DZL_IS_APPLICATION_WINDOW (self));
fullscreen = !!fullscreen;
if (fullscreen != dzl_application_window_get_fullscreen (self))
DZL_APPLICATION_WINDOW_GET_CLASS (self)->set_fullscreen (self, fullscreen);
}
static void
dzl_application_window_add_child (GtkBuildable *buildable,
GtkBuilder *builder,
GObject *child,
const gchar *type)
{
DzlApplicationWindow *self = (DzlApplicationWindow *)buildable;
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_assert (DZL_IS_APPLICATION_WINDOW (self));
g_assert (GTK_IS_BUILDER (builder));
g_assert (G_IS_OBJECT (child));
if (g_strcmp0 (type, "titlebar") == 0)
gtk_container_add (GTK_CONTAINER (priv->titlebar_container), GTK_WIDGET (child));
else
parent_buildable->add_child (buildable, builder, child, type);
}
static void
buildable_iface_init (GtkBuildableIface *iface)
{
parent_buildable = g_type_interface_peek_parent (iface);
iface->add_child = dzl_application_window_add_child;
}
/**
* dzl_application_window_get_titlebar:
* @self: a #DzlApplicationWindow
*
* Gets the titlebar for the window, if there is one.
*
* Returns: (transfer none): A #GtkWidget or %NULL
*
* Since: 3.26
*/
GtkWidget *
dzl_application_window_get_titlebar (DzlApplicationWindow *self)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
GtkWidget *child;
g_return_val_if_fail (DZL_IS_APPLICATION_WINDOW (self), NULL);
child = gtk_stack_get_visible_child (priv->titlebar_container);
if (child == NULL)
child = gtk_bin_get_child (GTK_BIN (priv->titlebar_revealer));
return child;
}
/**
* dzl_application_window_set_titlebar:
* @self: a #DzlApplicationWindow
*
* Sets the titlebar for the window.
*
* Generally, you want to do this from your GTK ui template by setting
* the <child type="titlebar">
*
* Since: 3.26
*/
void
dzl_application_window_set_titlebar (DzlApplicationWindow *self,
GtkWidget *titlebar)
{
DzlApplicationWindowPrivate *priv = dzl_application_window_get_instance_private (self);
g_return_if_fail (DZL_IS_APPLICATION_WINDOW (self));
g_return_if_fail (GTK_IS_WIDGET (titlebar));
if (titlebar != NULL)
gtk_container_add (GTK_CONTAINER (priv->titlebar_container), titlebar);
}