gtk2/gtk/gtkmodelbutton.c
Matthias Clasen 6429b33f44 GtkModelButton: Add an iconic mode
In iconic mode, model buttons will be styled like regular icon
buttons, preferring to show only the icon if one is set, falling
back to showing the label.

https://bugzilla.gnome.org/show_bug.cgi?id=727477
2014-04-28 14:20:15 -04:00

754 lines
24 KiB
C

/*
* Copyright © 2014 Red Hat, Inc.
*
* 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 licence, 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 <http://www.gnu.org/licenses/>.
*
* Author: Matthias Clasen
*/
#include "config.h"
#include "gtkmodelbutton.h"
#include "gtkbutton.h"
#include "gtkbuttonprivate.h"
#include "gtkwidgetprivate.h"
#include "gtkmenutrackeritem.h"
#include "gtkimage.h"
#include "gtklabel.h"
#include "gtkbox.h"
#include "gtkstylecontext.h"
#include "gtktypebuiltins.h"
struct _GtkModelButton
{
GtkButton parent_instance;
GtkWidget *box;
GtkWidget *image;
GtkWidget *label;
gboolean toggled;
gboolean has_submenu;
gboolean centered;
gboolean inverted;
gboolean iconic;
GtkMenuTrackerItemRole role;
};
typedef GtkButtonClass GtkModelButtonClass;
G_DEFINE_TYPE (GtkModelButton, gtk_model_button, GTK_TYPE_BUTTON)
enum
{
PROP_0,
PROP_ACTION_ROLE,
PROP_ICON,
PROP_TEXT,
PROP_TOGGLED,
PROP_ACCEL,
PROP_HAS_SUBMENU,
PROP_INVERTED,
PROP_CENTERED,
PROP_ICONIC
};
static void
gtk_model_button_set_action_role (GtkModelButton *button,
GtkMenuTrackerItemRole role)
{
AtkObject *accessible;
AtkRole a11y_role;
if (role == button->role)
return;
button->role = role;
gtk_widget_queue_draw (GTK_WIDGET (button));
accessible = gtk_widget_get_accessible (GTK_WIDGET (button));
switch (role)
{
case GTK_MENU_TRACKER_ITEM_ROLE_NORMAL:
a11y_role = ATK_ROLE_PUSH_BUTTON;
break;
case GTK_MENU_TRACKER_ITEM_ROLE_CHECK:
a11y_role = ATK_ROLE_CHECK_BOX;
break;
case GTK_MENU_TRACKER_ITEM_ROLE_RADIO:
a11y_role = ATK_ROLE_RADIO_BUTTON;
break;
default:
g_assert_not_reached ();
}
atk_object_set_role (accessible, a11y_role);
}
static void
update_visibility (GtkModelButton *button)
{
gboolean has_icon;
gboolean has_text;
has_icon = gtk_image_get_storage_type (GTK_IMAGE (button->image)) != GTK_IMAGE_EMPTY;
has_text = gtk_label_get_text (GTK_LABEL (button->label))[0] != '\0';
gtk_widget_set_visible (button->image, has_icon);
gtk_widget_set_visible (button->label, has_text && (!button->iconic || !has_icon));
}
static void
gtk_model_button_set_icon (GtkModelButton *button,
GIcon *icon)
{
gtk_image_set_from_gicon (GTK_IMAGE (button->image), icon, GTK_ICON_SIZE_MENU);
update_visibility (button);
}
static void
gtk_model_button_set_text (GtkModelButton *button,
const gchar *text)
{
gtk_label_set_text_with_mnemonic (GTK_LABEL (button->label), text);
update_visibility (button);
}
static void
gtk_model_button_set_accel (GtkModelButton *button,
const gchar *accel)
{
/* ignore */
}
static void
gtk_model_button_set_toggled (GtkModelButton *button,
gboolean toggled)
{
button->toggled = toggled;
gtk_widget_queue_draw (GTK_WIDGET (button));
}
static void
gtk_model_button_set_has_submenu (GtkModelButton *button,
gboolean has_submenu)
{
button->has_submenu = has_submenu;
gtk_widget_queue_resize (GTK_WIDGET (button));
}
static void
gtk_model_button_set_inverted (GtkModelButton *button,
gboolean inverted)
{
button->inverted = inverted;
gtk_widget_queue_resize (GTK_WIDGET (button));
}
static void
gtk_model_button_set_centered (GtkModelButton *button,
gboolean centered)
{
button->centered = centered;
gtk_widget_set_halign (button->box, button->centered ? GTK_ALIGN_CENTER : GTK_ALIGN_FILL);
gtk_widget_queue_draw (GTK_WIDGET (button));
}
static void
gtk_model_button_set_iconic (GtkModelButton *button,
gboolean iconic)
{
button->iconic = iconic;
if (iconic)
{
gtk_style_context_remove_class (gtk_widget_get_style_context (GTK_WIDGET (button)),
GTK_STYLE_CLASS_MENUITEM);
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NORMAL);
}
else
{
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (button)),
GTK_STYLE_CLASS_MENUITEM);
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
}
update_visibility (button);
gtk_widget_queue_resize (GTK_WIDGET (button));
}
static void
gtk_model_button_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkModelButton *button = GTK_MODEL_BUTTON (object);
switch (prop_id)
{
case PROP_ACTION_ROLE:
gtk_model_button_set_action_role (button, g_value_get_enum (value));
break;
case PROP_ICON:
gtk_model_button_set_icon (button, g_value_get_object (value));
break;
case PROP_TEXT:
gtk_model_button_set_text (button, g_value_get_string (value));
break;
case PROP_TOGGLED:
gtk_model_button_set_toggled (button, g_value_get_boolean (value));
break;
case PROP_ACCEL:
gtk_model_button_set_accel (button, g_value_get_string (value));
break;
case PROP_HAS_SUBMENU:
gtk_model_button_set_has_submenu (button, g_value_get_boolean (value));
break;
case PROP_INVERTED:
gtk_model_button_set_inverted (button, g_value_get_boolean (value));
break;
case PROP_CENTERED:
gtk_model_button_set_centered (button, g_value_get_boolean (value));
break;
case PROP_ICONIC:
gtk_model_button_set_iconic (button, g_value_get_boolean (value));
break;
default:
g_assert_not_reached ();
}
}
static void
gtk_model_button_get_full_border (GtkModelButton *button,
GtkBorder *border,
gint *indicator)
{
gint focus_width, focus_pad;
gint indicator_size, indicator_spacing;
gint border_width;
border_width = gtk_container_get_border_width (GTK_CONTAINER (button));
gtk_widget_style_get (GTK_WIDGET (button),
"focus-line-width", &focus_width,
"focus-padding", &focus_pad,
NULL);
gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &indicator_size, NULL);
indicator_spacing = indicator_size / 8;
border->left = border_width + focus_width + focus_pad;
border->right = border_width + focus_width + focus_pad;
border->top = border_width + focus_width + focus_pad;
border->bottom = border_width + focus_width + focus_pad;
if (button->iconic)
*indicator = 0;
else
*indicator = indicator_size + 2 * indicator_spacing;
}
static gboolean
has_sibling_with_indicator (GtkWidget *button)
{
GtkWidget *parent;
gboolean has_indicator;
GList *children, *l;
GtkModelButton *sibling;
has_indicator = FALSE;
parent = gtk_widget_get_parent (button);
children = gtk_container_get_children (GTK_CONTAINER (parent));
for (l = children; l; l = l->next)
{
sibling = l->data;
if (!GTK_IS_MODEL_BUTTON (sibling))
continue;
if (!gtk_widget_is_visible (GTK_WIDGET (sibling)))
continue;
if (!sibling->centered &&
(sibling->has_submenu || sibling->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL))
{
has_indicator = TRUE;
break;
}
}
g_list_free (children);
return has_indicator;
}
static gboolean
needs_indicator (GtkModelButton *button)
{
if (button->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
return TRUE;
return has_sibling_with_indicator (GTK_WIDGET (button));
}
static void
gtk_model_button_get_preferred_width_for_height (GtkWidget *widget,
gint height,
gint *minimum,
gint *natural)
{
GtkModelButton *button = GTK_MODEL_BUTTON (widget);
GtkWidget *child;
GtkBorder border;
gint indicator;
gtk_model_button_get_full_border (button, &border, &indicator);
child = gtk_bin_get_child (GTK_BIN (widget));
if (child && gtk_widget_get_visible (child))
{
if (height > -1)
height -= border.top + border.bottom;
_gtk_widget_get_preferred_size_for_size (child,
GTK_ORIENTATION_HORIZONTAL,
height,
minimum, natural,
NULL, NULL);
}
else
{
*minimum = 0;
*natural = 0;
}
*minimum += border.left + border.right;
*natural += border.left + border.right;
if (button->centered)
{
*minimum += 2 * indicator;
*natural += 2 * indicator;
}
else if (needs_indicator (button))
{
*minimum += indicator;
*natural += indicator;
}
}
static void
gtk_model_button_get_preferred_width (GtkWidget *widget,
gint *minimum,
gint *natural)
{
gtk_model_button_get_preferred_width_for_height (widget, -1, minimum, natural);
}
static void
gtk_model_button_get_preferred_height_and_baseline_for_width (GtkWidget *widget,
gint width,
gint *minimum,
gint *natural,
gint *minimum_baseline,
gint *natural_baseline)
{
GtkModelButton *button = GTK_MODEL_BUTTON (widget);
GtkWidget *child;
GtkBorder border;
gint indicator;
gtk_model_button_get_full_border (button, &border, &indicator);
child = gtk_bin_get_child (GTK_BIN (widget));
if (child && gtk_widget_get_visible (child))
{
gint child_min, child_nat;
gint child_min_baseline = -1, child_nat_baseline = -1;
if (width > -1)
{
width -= border.left + border.right;
if (button->centered)
width -= 2 * indicator;
else if (needs_indicator (button))
width -= indicator;
}
gtk_widget_get_preferred_height_and_baseline_for_width (child, width,
&child_min, &child_nat,
&child_min_baseline, &child_nat_baseline);
if (button->centered)
{
*minimum = MAX (2 * indicator, child_min);
*natural = MAX (2 * indicator, child_nat);
}
else if (needs_indicator (button))
{
*minimum = MAX (indicator, child_min);
*natural = MAX (indicator, child_nat);
}
else
{
*minimum = child_min;
*natural = child_nat;
}
if (minimum_baseline && child_min_baseline >= 0)
*minimum_baseline = child_min_baseline + border.top + (*minimum - child_min) / 2;
if (natural_baseline && child_nat_baseline >= 0)
*natural_baseline = child_nat_baseline + border.top + (*natural - child_nat) / 2;
}
else
{
if (button->centered)
{
*minimum = 2 * indicator;
*natural = 2 * indicator;
}
else if (needs_indicator (button))
{
*minimum = indicator;
*natural = indicator;
}
else
{
*minimum = 0;
*natural = 0;
}
}
*minimum += border.top + border.bottom;
*natural += border.top + border.bottom;
}
static void
gtk_model_button_get_preferred_height_for_width (GtkWidget *widget,
gint width,
gint *minimum,
gint *natural)
{
gtk_model_button_get_preferred_height_and_baseline_for_width (widget, width,
minimum, natural,
NULL, NULL);
}
static void
gtk_model_button_get_preferred_height (GtkWidget *widget,
gint *minimum,
gint *natural)
{
gtk_model_button_get_preferred_height_and_baseline_for_width (widget, -1,
minimum, natural,
NULL, NULL);
}
static gboolean
indicator_is_left (GtkWidget *widget)
{
GtkModelButton *button = GTK_MODEL_BUTTON (widget);
return ((gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL && !button->inverted) ||
(gtk_widget_get_direction (widget) == GTK_TEXT_DIR_LTR && button->inverted));
}
static void
gtk_model_button_size_allocate (GtkWidget *widget,
GtkAllocation *allocation)
{
GtkModelButton *button = GTK_MODEL_BUTTON (widget);
PangoContext *pango_context;
PangoFontMetrics *metrics;
GtkAllocation child_allocation;
gint baseline;
GtkWidget *child;
gtk_widget_set_allocation (widget, allocation);
if (gtk_widget_get_realized (widget))
gdk_window_move_resize (gtk_button_get_event_window (GTK_BUTTON (widget)),
allocation->x, allocation->y,
allocation->width, allocation->height);
child = gtk_bin_get_child (GTK_BIN (widget));
if (child && gtk_widget_get_visible (child))
{
GtkBorder border;
gint indicator;
gtk_model_button_get_full_border (button, &border, &indicator);
if (button->centered)
{
border.left += indicator;
border.right += indicator;
}
else if (needs_indicator (button))
{
if (indicator_is_left (widget))
border.left += indicator;
else
border.right += indicator;
}
child_allocation.x = allocation->x + border.left;
child_allocation.y = allocation->y + border.top;
child_allocation.width = allocation->width - border.left - border.right;
child_allocation.height = allocation->height - border.top - border.bottom;
baseline = gtk_widget_get_allocated_baseline (widget);
if (baseline != -1)
baseline -= border.top;
gtk_widget_size_allocate_with_baseline (child, &child_allocation, baseline);
}
pango_context = gtk_widget_get_pango_context (widget);
metrics = pango_context_get_metrics (pango_context,
pango_context_get_font_description (pango_context),
pango_context_get_language (pango_context));
GTK_BUTTON (button)->priv->baseline_align =
(double)pango_font_metrics_get_ascent (metrics) /
(pango_font_metrics_get_ascent (metrics) + pango_font_metrics_get_descent (metrics));
pango_font_metrics_unref (metrics);
}
static GtkStateFlags
get_button_state (GtkModelButton *model_button)
{
GtkButton *button = GTK_BUTTON (model_button);
GtkStateFlags state;
state = gtk_widget_get_state_flags (GTK_WIDGET (model_button));
state &= ~(GTK_STATE_FLAG_INCONSISTENT |
GTK_STATE_FLAG_ACTIVE |
GTK_STATE_FLAG_SELECTED |
GTK_STATE_FLAG_PRELIGHT);
if (model_button->toggled ||
(button->priv->button_down && button->priv->in_button))
state |= GTK_STATE_FLAG_ACTIVE;
if (button->priv->activate_timeout ||
(button->priv->button_down && button->priv->in_button))
state |= GTK_STATE_FLAG_SELECTED;
if (button->priv->in_button)
state |= GTK_STATE_FLAG_PRELIGHT;
if (model_button->has_submenu)
state &= ~GTK_STATE_FLAG_ACTIVE;
return state;
}
static gint
gtk_model_button_draw (GtkWidget *widget,
cairo_t *cr)
{
GtkModelButton *model_button = GTK_MODEL_BUTTON (widget);
GtkButton *button = GTK_BUTTON (widget);
GtkWidget *child;
GtkStyleContext *context;
GtkStateFlags state;
gint border_width;
gint x, y;
gint width, height;
gint indicator_size, indicator_spacing;
gint focus_width, focus_pad;
gint baseline;
state = get_button_state (model_button);
context = gtk_widget_get_style_context (widget);
gtk_style_context_save (context);
gtk_style_context_set_state (context, state);
if (model_button->iconic)
{
GTK_WIDGET_CLASS (gtk_model_button_parent_class)->draw (widget, cr);
goto out;
}
width = gtk_widget_get_allocated_width (widget);
height = gtk_widget_get_allocated_height (widget);
border_width = gtk_container_get_border_width (GTK_CONTAINER (widget));
baseline = gtk_widget_get_allocated_baseline (widget);
gtk_widget_style_get (widget,
"focus-line-width", &focus_width,
"focus-padding", &focus_pad,
NULL);
gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &indicator_size, NULL);
indicator_spacing = indicator_size / 8;
x = width - border_width - focus_width - focus_pad - indicator_spacing - indicator_size;
if (indicator_is_left (widget))
x = width - (indicator_size + x);
if (baseline == -1)
y = (height - indicator_size) / 2;
else
y = CLAMP (baseline - indicator_size * button->priv->baseline_align,
0, height - indicator_size);
gtk_render_background (context, cr,
border_width, border_width,
width - 2 * border_width,
height - 2 * border_width);
gtk_render_frame (context, cr,
border_width, border_width,
width - 2 * border_width,
height - 2 * border_width);
if (model_button->has_submenu)
{
state = state & ~(GTK_STATE_FLAG_DIR_LTR|GTK_STATE_FLAG_DIR_RTL);
if (indicator_is_left (widget))
state = state | GTK_STATE_FLAG_DIR_RTL;
else
state = state | GTK_STATE_FLAG_DIR_LTR;
gtk_style_context_set_state (context, state);
gtk_style_context_add_class (context, GTK_STYLE_CLASS_EXPANDER);
gtk_render_expander (context, cr, x, y, indicator_size, indicator_size);
}
else if (model_button->role == GTK_MENU_TRACKER_ITEM_ROLE_CHECK)
{
gtk_style_context_add_class (context, GTK_STYLE_CLASS_CHECK);
gtk_render_check (context, cr, x, y, indicator_size, indicator_size);
}
else if (model_button->role == GTK_MENU_TRACKER_ITEM_ROLE_RADIO)
{
gtk_style_context_add_class (context, GTK_STYLE_CLASS_RADIO);
gtk_render_option (context, cr, x, y, indicator_size, indicator_size);
}
if (gtk_widget_has_visible_focus (widget))
{
GtkBorder border;
gtk_style_context_get_border (context, state, &border);
gtk_render_focus (context, cr,
border_width + border.left + focus_pad,
border_width + border.top + focus_pad,
width - 2 * (border_width + focus_pad) - border.left - border.right,
height - 2 * (border_width + focus_pad) - border.top - border.bottom);
}
child = gtk_bin_get_child (GTK_BIN (widget));
if (child)
gtk_container_propagate_draw (GTK_CONTAINER (widget), child, cr);
out:
gtk_style_context_restore (context);
return FALSE;
}
static void
gtk_model_button_class_init (GtkModelButtonClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
object_class->set_property = gtk_model_button_set_property;
widget_class->get_preferred_width = gtk_model_button_get_preferred_width;
widget_class->get_preferred_width_for_height = gtk_model_button_get_preferred_width_for_height;
widget_class->get_preferred_height = gtk_model_button_get_preferred_height;
widget_class->get_preferred_height_for_width = gtk_model_button_get_preferred_height_for_width;
widget_class->get_preferred_height_and_baseline_for_width = gtk_model_button_get_preferred_height_and_baseline_for_width;
widget_class->size_allocate = gtk_model_button_size_allocate;
widget_class->draw = gtk_model_button_draw;
g_object_class_install_property (object_class, PROP_ACTION_ROLE,
g_param_spec_enum ("action-role", "", "",
GTK_TYPE_MENU_TRACKER_ITEM_ROLE,
GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_ICON,
g_param_spec_object ("icon", "", "", G_TYPE_ICON,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_TEXT,
g_param_spec_string ("text", "", "", NULL,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_TOGGLED,
g_param_spec_boolean ("toggled", "", "", FALSE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_ACCEL,
g_param_spec_string ("accel", "", "", NULL,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_HAS_SUBMENU,
g_param_spec_boolean ("has-submenu", "", "", FALSE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_INVERTED,
g_param_spec_boolean ("inverted", "", "", FALSE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_CENTERED,
g_param_spec_boolean ("centered", "", "", FALSE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_ICONIC,
g_param_spec_boolean ("iconic", "", "", TRUE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
gtk_widget_class_set_accessible_role (GTK_WIDGET_CLASS (class), ATK_ROLE_PUSH_BUTTON);
}
static void
gtk_model_button_init (GtkModelButton *button)
{
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
button->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
g_object_set (button->box,
"margin-start", 12,
"margin-end", 12,
"margin-top", 3,
"margin-bottom", 3,
NULL);
gtk_widget_set_halign (button->box, GTK_ALIGN_FILL);
gtk_widget_show (button->box);
button->image = gtk_image_new ();
button->label = gtk_label_new ("");
gtk_container_add (GTK_CONTAINER (button->box), button->image);
gtk_container_add (GTK_CONTAINER (button->box), button->label);
gtk_container_add (GTK_CONTAINER (button), button->box);
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (button)),
GTK_STYLE_CLASS_MENUITEM);
}
GtkWidget *
gtk_model_button_new (void)
{
return g_object_new (GTK_TYPE_MODEL_BUTTON, NULL);
}