gtk2/gtk/gtkstackswitcher.c
Matthias Clasen d9a146f54a Change icon-size properties
We no longer support registering custom icon sizes, so
we can make these properties just enums. This also lets
us specify them by nick in ui files. Nice!
2017-11-15 14:22:17 -05:00

714 lines
20 KiB
C

/*
* Copyright (c) 2013 Red Hat, Inc.
*
* This program 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 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "config.h"
#include "gtkstackswitcher.h"
#include "gtkradiobutton.h"
#include "gtklabel.h"
#include "gtkdnd.h"
#include "gtkdragdest.h"
#include "gtkorientable.h"
#include "gtkprivate.h"
#include "gtkintl.h"
#include "gtkwidgetprivate.h"
#include "gtktypebuiltins.h"
/**
* SECTION:gtkstackswitcher
* @Short_description: A controller for GtkStack
* @Title: GtkStackSwitcher
* @See_also: #GtkStack
*
* The GtkStackSwitcher widget acts as a controller for a
* #GtkStack; it shows a row of buttons to switch between
* the various pages of the associated stack widget.
*
* All the content for the buttons comes from the child properties
* of the #GtkStack; the button visibility in a #GtkStackSwitcher
* widget is controlled by the visibility of the child in the
* #GtkStack.
*
* It is possible to associate multiple #GtkStackSwitcher widgets
* with the same #GtkStack widget.
*
* The GtkStackSwitcher widget was added in 3.10.
*
* # CSS nodes
*
* GtkStackSwitcher has a single CSS node named stackswitcher and
* style class .stack-switcher.
*
* When circumstances require it, GtkStackSwitcher adds the
* .needs-attention style class to the widgets representing the
* stack pages.
*/
#define TIMEOUT_EXPAND 500
typedef struct _GtkStackSwitcherPrivate GtkStackSwitcherPrivate;
struct _GtkStackSwitcherPrivate
{
GtkStack *stack;
GHashTable *buttons;
GtkIconSize icon_size;
gboolean in_child_changed;
GtkWidget *switch_button;
guint switch_timer;
};
enum {
PROP_0,
PROP_ICON_SIZE,
PROP_STACK
};
G_DEFINE_TYPE_WITH_PRIVATE (GtkStackSwitcher, gtk_stack_switcher, GTK_TYPE_BOX)
static void
gtk_stack_switcher_init (GtkStackSwitcher *switcher)
{
GtkStyleContext *context;
GtkStackSwitcherPrivate *priv;
gtk_widget_set_has_window (GTK_WIDGET (switcher), FALSE);
priv = gtk_stack_switcher_get_instance_private (switcher);
priv->icon_size = GTK_ICON_SIZE_INHERIT;
priv->stack = NULL;
priv->buttons = g_hash_table_new (g_direct_hash, g_direct_equal);
context = gtk_widget_get_style_context (GTK_WIDGET (switcher));
gtk_style_context_add_class (context, "stack-switcher");
gtk_style_context_add_class (context, GTK_STYLE_CLASS_LINKED);
gtk_orientable_set_orientation (GTK_ORIENTABLE (switcher), GTK_ORIENTATION_HORIZONTAL);
gtk_drag_dest_set (GTK_WIDGET (switcher), 0, NULL, 0);
gtk_drag_dest_set_track_motion (GTK_WIDGET (switcher), TRUE);
}
static void
on_button_clicked (GtkWidget *widget,
GtkStackSwitcher *self)
{
GtkWidget *child;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
if (!priv->in_child_changed)
{
child = g_object_get_data (G_OBJECT (widget), "stack-child");
gtk_stack_set_visible_child (priv->stack, child);
}
}
static void
rebuild_child (GtkWidget *self,
const gchar *icon_name,
const gchar *title,
gint icon_size)
{
GtkStyleContext *context;
GtkWidget *button_child;
button_child = gtk_bin_get_child (GTK_BIN (self));
if (button_child != NULL)
gtk_widget_destroy (button_child);
button_child = NULL;
context = gtk_widget_get_style_context (GTK_WIDGET (self));
if (icon_name != NULL)
{
button_child = gtk_image_new_from_icon_name (icon_name);
if (title != NULL)
gtk_widget_set_tooltip_text (GTK_WIDGET (self), title);
gtk_style_context_remove_class (context, "text-button");
gtk_style_context_add_class (context, "image-button");
}
else if (title != NULL)
{
button_child = gtk_label_new (title);
gtk_widget_set_tooltip_text (GTK_WIDGET (self), NULL);
gtk_style_context_remove_class (context, "image-button");
gtk_style_context_add_class (context, "text-button");
}
if (button_child)
{
gtk_widget_set_halign (GTK_WIDGET (button_child), GTK_ALIGN_CENTER);
gtk_container_add (GTK_CONTAINER (self), button_child);
}
}
static void
update_needs_attention (GtkWidget *widget, GtkWidget *button, gpointer data)
{
GtkContainer *container;
gboolean needs_attention;
GtkStyleContext *context;
container = GTK_CONTAINER (data);
gtk_container_child_get (container, widget,
"needs-attention", &needs_attention,
NULL);
context = gtk_widget_get_style_context (button);
if (needs_attention)
gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
else
gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
}
static void
update_button (GtkStackSwitcher *self,
GtkWidget *widget,
GtkWidget *button)
{
gchar *title;
gchar *icon_name;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
"title", &title,
"icon-name", &icon_name,
NULL);
rebuild_child (button, icon_name, title, priv->icon_size);
gtk_widget_set_visible (button, gtk_widget_get_visible (widget) && (title != NULL || icon_name != NULL));
g_free (title);
g_free (icon_name);
update_needs_attention (widget, button, priv->stack);
}
static void
on_title_icon_visible_updated (GtkWidget *widget,
GParamSpec *pspec,
GtkStackSwitcher *self)
{
GtkWidget *button;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
button = g_hash_table_lookup (priv->buttons, widget);
update_button (self, widget, button);
}
static void
on_position_updated (GtkWidget *widget,
GParamSpec *pspec,
GtkStackSwitcher *self)
{
GtkWidget *button;
gint position;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
button = g_hash_table_lookup (priv->buttons, widget);
gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
"position", &position,
NULL);
gtk_box_reorder_child (GTK_BOX (self), button, position);
}
static void
on_needs_attention_updated (GtkWidget *widget,
GParamSpec *pspec,
GtkStackSwitcher *self)
{
GtkWidget *button;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
button = g_hash_table_lookup (priv->buttons, widget);
update_button (self, widget, button);
}
static void
remove_switch_timer (GtkStackSwitcher *self)
{
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
if (priv->switch_timer)
{
g_source_remove (priv->switch_timer);
priv->switch_timer = 0;
}
}
static gboolean
gtk_stack_switcher_switch_timeout (gpointer data)
{
GtkStackSwitcher *self = data;
GtkStackSwitcherPrivate *priv;
GtkWidget *button;
priv = gtk_stack_switcher_get_instance_private (self);
priv->switch_timer = 0;
button = priv->switch_button;
priv->switch_button = NULL;
if (button)
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
return G_SOURCE_REMOVE;
}
static gboolean
gtk_stack_switcher_drag_motion (GtkWidget *widget,
GdkDragContext *context,
gint x,
gint y,
guint time)
{
GtkStackSwitcher *self = GTK_STACK_SWITCHER (widget);
GtkStackSwitcherPrivate *priv;
GtkWidget *button;
GHashTableIter iter;
gpointer value;
gboolean retval = FALSE;
priv = gtk_stack_switcher_get_instance_private (self);
button = NULL;
g_hash_table_iter_init (&iter, priv->buttons);
while (g_hash_table_iter_next (&iter, NULL, &value))
{
GdkRectangle allocation;
gtk_widget_get_outer_allocation (GTK_WIDGET (value), &allocation);
if (gdk_rectangle_contains_point (&allocation, (int)x, (int)y))
{
button = GTK_WIDGET (value);
retval = TRUE;
break;
}
}
if (button != priv->switch_button)
remove_switch_timer (self);
priv->switch_button = button;
if (button && !priv->switch_timer)
{
priv->switch_timer = gdk_threads_add_timeout (TIMEOUT_EXPAND,
gtk_stack_switcher_switch_timeout,
self);
g_source_set_name_by_id (priv->switch_timer, "[gtk+] gtk_stack_switcher_switch_timeout");
}
return retval;
}
static void
gtk_stack_switcher_drag_leave (GtkWidget *widget,
GdkDragContext *context,
guint time)
{
GtkStackSwitcher *self = GTK_STACK_SWITCHER (widget);
remove_switch_timer (self);
}
static void
add_child (GtkWidget *widget,
GtkStackSwitcher *self)
{
GtkWidget *button;
GList *group;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
button = gtk_radio_button_new (NULL);
gtk_widget_set_focus_on_click (button, FALSE);
gtk_check_button_set_draw_indicator (GTK_CHECK_BUTTON (button), FALSE);
update_button (self, widget, button);
group = gtk_container_get_children (GTK_CONTAINER (self));
if (group != NULL)
{
gtk_radio_button_join_group (GTK_RADIO_BUTTON (button), GTK_RADIO_BUTTON (group->data));
g_list_free (group);
}
gtk_container_add (GTK_CONTAINER (self), button);
g_object_set_data (G_OBJECT (button), "stack-child", widget);
g_signal_connect (button, "clicked", G_CALLBACK (on_button_clicked), self);
g_signal_connect (widget, "notify::visible", G_CALLBACK (on_title_icon_visible_updated), self);
g_signal_connect (widget, "child-notify::title", G_CALLBACK (on_title_icon_visible_updated), self);
g_signal_connect (widget, "child-notify::icon-name", G_CALLBACK (on_title_icon_visible_updated), self);
g_signal_connect (widget, "child-notify::position", G_CALLBACK (on_position_updated), self);
g_signal_connect (widget, "child-notify::needs-attention", G_CALLBACK (on_needs_attention_updated), self);
g_hash_table_insert (priv->buttons, widget, button);
}
static void
remove_child (GtkWidget *widget,
GtkStackSwitcher *self)
{
GtkWidget *button;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
g_signal_handlers_disconnect_by_func (widget, on_title_icon_visible_updated, self);
g_signal_handlers_disconnect_by_func (widget, on_position_updated, self);
g_signal_handlers_disconnect_by_func (widget, on_needs_attention_updated, self);
button = g_hash_table_lookup (priv->buttons, widget);
gtk_container_remove (GTK_CONTAINER (self), button);
g_hash_table_remove (priv->buttons, widget);
}
static void
populate_switcher (GtkStackSwitcher *self)
{
GtkStackSwitcherPrivate *priv;
GtkWidget *widget, *button;
priv = gtk_stack_switcher_get_instance_private (self);
gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)add_child, self);
widget = gtk_stack_get_visible_child (priv->stack);
if (widget)
{
button = g_hash_table_lookup (priv->buttons, widget);
priv->in_child_changed = TRUE;
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
priv->in_child_changed = FALSE;
}
}
static void
clear_switcher (GtkStackSwitcher *self)
{
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)remove_child, self);
}
static void
on_child_changed (GtkWidget *widget,
GParamSpec *pspec,
GtkStackSwitcher *self)
{
GtkWidget *child;
GtkWidget *button;
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (self);
child = gtk_stack_get_visible_child (GTK_STACK (widget));
button = g_hash_table_lookup (priv->buttons, child);
if (button != NULL)
{
priv->in_child_changed = TRUE;
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
priv->in_child_changed = FALSE;
}
}
static void
on_stack_child_added (GtkContainer *container,
GtkWidget *widget,
GtkStackSwitcher *self)
{
add_child (widget, self);
}
static void
on_stack_child_removed (GtkContainer *container,
GtkWidget *widget,
GtkStackSwitcher *self)
{
remove_child (widget, self);
}
static void
disconnect_stack_signals (GtkStackSwitcher *switcher)
{
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (switcher);
g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_added, switcher);
g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_removed, switcher);
g_signal_handlers_disconnect_by_func (priv->stack, on_child_changed, switcher);
g_signal_handlers_disconnect_by_func (priv->stack, disconnect_stack_signals, switcher);
}
static void
connect_stack_signals (GtkStackSwitcher *switcher)
{
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (switcher);
g_signal_connect_after (priv->stack, "add",
G_CALLBACK (on_stack_child_added), switcher);
g_signal_connect_after (priv->stack, "remove",
G_CALLBACK (on_stack_child_removed), switcher);
g_signal_connect (priv->stack, "notify::visible-child",
G_CALLBACK (on_child_changed), switcher);
g_signal_connect_swapped (priv->stack, "destroy",
G_CALLBACK (disconnect_stack_signals), switcher);
}
/**
* gtk_stack_switcher_set_stack:
* @switcher: a #GtkStackSwitcher
* @stack: (allow-none): a #GtkStack
*
* Sets the stack to control.
*
* Since: 3.10
*/
void
gtk_stack_switcher_set_stack (GtkStackSwitcher *switcher,
GtkStack *stack)
{
GtkStackSwitcherPrivate *priv;
g_return_if_fail (GTK_IS_STACK_SWITCHER (switcher));
g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL);
priv = gtk_stack_switcher_get_instance_private (switcher);
if (priv->stack == stack)
return;
if (priv->stack)
{
disconnect_stack_signals (switcher);
clear_switcher (switcher);
g_clear_object (&priv->stack);
}
if (stack)
{
priv->stack = g_object_ref (stack);
populate_switcher (switcher);
connect_stack_signals (switcher);
}
gtk_widget_queue_resize (GTK_WIDGET (switcher));
g_object_notify (G_OBJECT (switcher), "stack");
}
/**
* gtk_stack_switcher_get_stack:
* @switcher: a #GtkStackSwitcher
*
* Retrieves the stack.
* See gtk_stack_switcher_set_stack().
*
* Returns: (nullable) (transfer none): the stack, or %NULL if
* none has been set explicitly.
*
* Since: 3.10
*/
GtkStack *
gtk_stack_switcher_get_stack (GtkStackSwitcher *switcher)
{
GtkStackSwitcherPrivate *priv;
g_return_val_if_fail (GTK_IS_STACK_SWITCHER (switcher), NULL);
priv = gtk_stack_switcher_get_instance_private (switcher);
return priv->stack;
}
static void
gtk_stack_switcher_set_icon_size (GtkStackSwitcher *switcher,
GtkIconSize icon_size)
{
GtkStackSwitcherPrivate *priv;
g_return_if_fail (GTK_IS_STACK_SWITCHER (switcher));
priv = gtk_stack_switcher_get_instance_private (switcher);
if (icon_size != priv->icon_size)
{
priv->icon_size = icon_size;
if (priv->stack != NULL)
{
clear_switcher (switcher);
populate_switcher (switcher);
}
g_object_notify (G_OBJECT (switcher), "icon-size");
}
}
static void
gtk_stack_switcher_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object);
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (switcher);
switch (prop_id)
{
case PROP_ICON_SIZE:
g_value_set_enum (value, priv->icon_size);
break;
case PROP_STACK:
g_value_set_object (value, priv->stack);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gtk_stack_switcher_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object);
switch (prop_id)
{
case PROP_ICON_SIZE:
gtk_stack_switcher_set_icon_size (switcher, g_value_get_enum (value));
break;
case PROP_STACK:
gtk_stack_switcher_set_stack (switcher, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gtk_stack_switcher_dispose (GObject *object)
{
GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object);
remove_switch_timer (switcher);
gtk_stack_switcher_set_stack (switcher, NULL);
G_OBJECT_CLASS (gtk_stack_switcher_parent_class)->dispose (object);
}
static void
gtk_stack_switcher_finalize (GObject *object)
{
GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object);
GtkStackSwitcherPrivate *priv;
priv = gtk_stack_switcher_get_instance_private (switcher);
g_hash_table_destroy (priv->buttons);
G_OBJECT_CLASS (gtk_stack_switcher_parent_class)->finalize (object);
}
static void
gtk_stack_switcher_class_init (GtkStackSwitcherClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
object_class->get_property = gtk_stack_switcher_get_property;
object_class->set_property = gtk_stack_switcher_set_property;
object_class->dispose = gtk_stack_switcher_dispose;
object_class->finalize = gtk_stack_switcher_finalize;
widget_class->drag_motion = gtk_stack_switcher_drag_motion;
widget_class->drag_leave = gtk_stack_switcher_drag_leave;
/**
* GtkStackSwitcher:icon-size:
*
* Use the "icon-size" property to change the size of the image displayed
* when a #GtkStackSwitcher is displaying icons.
*
* Since: 3.20
*/
g_object_class_install_property (object_class,
PROP_ICON_SIZE,
g_param_spec_enum ("icon-size",
P_("Icon Size"),
P_("Symbolic size to use for named icon"),
GTK_TYPE_ICON_SIZE,
GTK_ICON_SIZE_INHERIT,
G_PARAM_EXPLICIT_NOTIFY | GTK_PARAM_READWRITE));
g_object_class_install_property (object_class,
PROP_STACK,
g_param_spec_object ("stack",
P_("Stack"),
P_("Stack"),
GTK_TYPE_STACK,
GTK_PARAM_READWRITE |
G_PARAM_CONSTRUCT));
gtk_widget_class_set_css_name (widget_class, "stackswitcher");
}
/**
* gtk_stack_switcher_new:
*
* Create a new #GtkStackSwitcher.
*
* Returns: a new #GtkStackSwitcher.
*
* Since: 3.10
*/
GtkWidget *
gtk_stack_switcher_new (void)
{
return g_object_new (GTK_TYPE_STACK_SWITCHER, NULL);
}