/* dzl-radio-box.c
*
* Copyright (C) 2016 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-radio-box"
#include "config.h"
#include "dzl-radio-box.h"
/*
* XXX: Ideally we would manage all the size requests ourselves. However,
* that takes some more work to do correctly (and support stuff like
* linked, etc).
*/
#define N_PER_ROW 4
typedef struct
{
gchar *id;
gchar *text;
GtkToggleButton *button;
} DzlRadioBoxItem;
typedef struct
{
GArray *items;
gchar *active_id;
GtkBox *vbox;
GtkBox *hbox;
GtkRevealer *revealer;
guint has_more : 1;
} DzlRadioBoxPrivate;
static void buildable_iface_init (GtkBuildableIface *iface);
G_DEFINE_TYPE_EXTENDED (DzlRadioBox, dzl_radio_box, GTK_TYPE_BIN, 0,
G_ADD_PRIVATE (DzlRadioBox)
G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
enum {
PROP_0,
PROP_ACTIVE_ID,
PROP_HAS_MORE,
PROP_SHOW_MORE,
N_PROPS
};
enum {
CHANGED,
N_SIGNALS
};
static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];
static gboolean
dzl_radio_box_get_has_more (DzlRadioBox *self)
{
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
g_return_val_if_fail (DZL_IS_RADIO_BOX (self), FALSE);
return priv->has_more;
}
static gboolean
dzl_radio_box_get_show_more (DzlRadioBox *self)
{
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
g_return_val_if_fail (DZL_IS_RADIO_BOX (self), FALSE);
return gtk_revealer_get_reveal_child (priv->revealer);
}
static void
dzl_radio_box_set_show_more (DzlRadioBox *self,
gboolean show_more)
{
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
g_return_if_fail (DZL_IS_RADIO_BOX (self));
gtk_revealer_set_reveal_child (priv->revealer, show_more);
}
static void
dzl_radio_box_item_clear (DzlRadioBoxItem *item)
{
g_free (item->id);
g_free (item->text);
}
static void
dzl_radio_box_finalize (GObject *object)
{
DzlRadioBox *self = (DzlRadioBox *)object;
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
g_clear_pointer (&priv->items, g_array_unref);
g_clear_pointer (&priv->active_id, g_free);
G_OBJECT_CLASS (dzl_radio_box_parent_class)->finalize (object);
}
static void
dzl_radio_box_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
DzlRadioBox *self = DZL_RADIO_BOX (object);
switch (prop_id)
{
case PROP_ACTIVE_ID:
g_value_set_string (value, dzl_radio_box_get_active_id (self));
break;
case PROP_HAS_MORE:
g_value_set_boolean (value, dzl_radio_box_get_has_more (self));
break;
case PROP_SHOW_MORE:
g_value_set_boolean (value, dzl_radio_box_get_show_more (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
dzl_radio_box_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
DzlRadioBox *self = DZL_RADIO_BOX (object);
switch (prop_id)
{
case PROP_ACTIVE_ID:
dzl_radio_box_set_active_id (self, g_value_get_string (value));
break;
case PROP_SHOW_MORE:
dzl_radio_box_set_show_more (self, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
dzl_radio_box_class_init (DzlRadioBoxClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->finalize = dzl_radio_box_finalize;
object_class->get_property = dzl_radio_box_get_property;
object_class->set_property = dzl_radio_box_set_property;
properties [PROP_ACTIVE_ID] =
g_param_spec_string ("active-id",
"Active Id",
"Active Id",
NULL,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
properties [PROP_HAS_MORE] =
g_param_spec_boolean ("has-more",
"Has More",
"Has more items to view",
FALSE,
(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
properties [PROP_SHOW_MORE] =
g_param_spec_boolean ("show-more",
"Show More",
"Show additional items",
FALSE,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, N_PROPS, properties);
signals [CHANGED] =
g_signal_new ("changed", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL, G_TYPE_NONE, 0);
gtk_widget_class_set_css_name (widget_class, "radiobox");
}
static void
dzl_radio_box_init (DzlRadioBox *self)
{
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
g_autoptr(GSimpleActionGroup) group = g_simple_action_group_new ();
g_autoptr(GPropertyAction) action = NULL;
GtkWidget *vbox;
/* GPropertyAction doesn't like NULL strings */
priv->active_id = g_strdup ("");
priv->items = g_array_new (FALSE, FALSE, sizeof (DzlRadioBoxItem));
g_array_set_clear_func (priv->items, (GDestroyNotify)dzl_radio_box_item_clear);
vbox = g_object_new (GTK_TYPE_BOX,
"orientation", GTK_ORIENTATION_VERTICAL,
"visible", TRUE,
NULL);
gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (vbox));
priv->hbox = g_object_new (GTK_TYPE_BOX,
"orientation", GTK_ORIENTATION_HORIZONTAL,
"visible", TRUE,
NULL);
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->hbox)), "linked");
gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (priv->hbox));
priv->revealer = g_object_new (GTK_TYPE_REVEALER,
"reveal-child", FALSE,
"visible", TRUE,
NULL);
gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (priv->revealer));
priv->vbox = g_object_new (GTK_TYPE_BOX,
"orientation", GTK_ORIENTATION_VERTICAL,
"margin-top", 12,
"spacing", 12,
"visible", TRUE,
NULL);
gtk_container_add (GTK_CONTAINER (priv->revealer), GTK_WIDGET (priv->vbox));
action = g_property_action_new ("active", self, "active-id");
g_action_map_add_action (G_ACTION_MAP (group), G_ACTION (action));
gtk_widget_insert_action_group (GTK_WIDGET (self), "radiobox", G_ACTION_GROUP (group));
}
void
dzl_radio_box_add_item (DzlRadioBox *self,
const gchar *id,
const gchar *text)
{
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
DzlRadioBoxItem item = { 0 };
guint precount;
g_return_if_fail (DZL_IS_RADIO_BOX (self));
g_return_if_fail (id != NULL);
g_return_if_fail (text != NULL);
precount = priv->items->len;
for (guint i = 0; i < precount; ++i)
{
/* Avoid duplicate items */
if (!g_strcmp0 (g_array_index (priv->items, DzlRadioBoxItem, i).id, id))
return;
}
item.id = g_strdup (id);
item.text = g_strdup (text);
item.button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
"active", (g_strcmp0 (id, priv->active_id) == 0),
"action-name", "radiobox.active",
"action-target", g_variant_new_string (id),
"label", text,
"visible", TRUE,
NULL);
g_array_append_val (priv->items, item);
if (precount > 0 && (precount % N_PER_ROW) == 0)
{
gboolean show_more = dzl_radio_box_get_show_more (self);
gboolean visible = !priv->has_more || show_more;
priv->has_more = priv->items->len > N_PER_ROW;
priv->hbox = g_object_new (GTK_TYPE_BOX,
"orientation", GTK_ORIENTATION_HORIZONTAL,
"visible", visible,
NULL);
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->hbox)), "linked");
gtk_container_add (GTK_CONTAINER (priv->vbox), GTK_WIDGET (priv->hbox));
}
gtk_container_add_with_properties (GTK_CONTAINER (priv->hbox), GTK_WIDGET (item.button),
"expand", TRUE,
NULL);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_MORE]);
/* If this is the first item and no active id has been set,
* then go ahead and set the active item to this one.
*/
if (priv->items->len == 1 && (!priv->active_id || !*priv->active_id))
dzl_radio_box_set_active_id (self, id);
}
void
dzl_radio_box_set_active_id (DzlRadioBox *self,
const gchar *id)
{
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
g_return_if_fail (DZL_IS_RADIO_BOX (self));
if (id == NULL)
id = "";
if (g_strcmp0 (id, priv->active_id) != 0)
{
g_free (priv->active_id);
priv->active_id = g_strdup (id);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE_ID]);
g_signal_emit (self, signals [CHANGED], 0);
}
}
const gchar *
dzl_radio_box_get_active_id (DzlRadioBox *self)
{
DzlRadioBoxPrivate *priv = dzl_radio_box_get_instance_private (self);
g_return_val_if_fail (DZL_IS_RADIO_BOX (self), NULL);
return priv->active_id;
}
GtkWidget *
dzl_radio_box_new (void)
{
return g_object_new (DZL_TYPE_RADIO_BOX, NULL);
}
typedef struct
{
DzlRadioBox *self;
GtkBuilder *builder;
gchar *id;
GString *text;
guint translatable : 1;
} ItemParserData;
static void
item_parser_start_element (GMarkupParseContext *context,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
gpointer user_data,
GError **error)
{
ItemParserData *parser_data = user_data;
g_assert (context != NULL);
g_assert (element_name != NULL);
g_assert (parser_data != NULL);
if (g_strcmp0 (element_name, "item") == 0)
{
const gchar *translatable = NULL;
g_clear_pointer (&parser_data->id, g_free);
g_string_truncate (parser_data->text, 0);
if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
G_MARKUP_COLLECT_STRDUP, "id", &parser_data->id,
G_MARKUP_COLLECT_STRING, "translatable", &translatable,
G_MARKUP_COLLECT_INVALID))
return;
parser_data->translatable = translatable != NULL && g_str_equal ("yes", translatable);
}
}
static void
item_parser_end_element (GMarkupParseContext *context,
const gchar *element_name,
gpointer user_data,
GError **error)
{
ItemParserData *parser_data = user_data;
g_assert (context != NULL);
g_assert (element_name != NULL);
g_assert (parser_data != NULL);
if (g_strcmp0 (element_name, "item") == 0)
{
if (parser_data->id && parser_data->text != NULL)
{
const gchar *str = parser_data->text->str;
if (parser_data->translatable && str != NULL)
{
const gchar *domain;
domain = gtk_builder_get_translation_domain (parser_data->builder);
str = g_dgettext (domain, str);
}
dzl_radio_box_add_item (parser_data->self, parser_data->id, str);
}
}
}
static void
item_parser_text (GMarkupParseContext *context,
const gchar *text,
gsize text_len,
gpointer user_data,
GError **error)
{
ItemParserData *parser_data = user_data;
g_assert (parser_data != NULL);
if (parser_data->text == NULL)
parser_data->text = g_string_new (NULL);
g_string_append_len (parser_data->text, text, text_len);
}
static GMarkupParser ItemParser = {
item_parser_start_element,
item_parser_end_element,
item_parser_text,
};
static gboolean
dzl_radio_box_custom_tag_start (GtkBuildable *buildable,
GtkBuilder *builder,
GObject *child,
const gchar *tagname,
GMarkupParser *parser,
gpointer *data)
{
DzlRadioBox *self = (DzlRadioBox *)buildable;
g_assert (DZL_IS_RADIO_BOX (self));
g_assert (GTK_IS_BUILDER (builder));
g_assert (tagname != NULL);
g_assert (parser != NULL);
g_assert (data != NULL);
if (g_strcmp0 (tagname, "items") == 0)
{
ItemParserData *parser_data;
parser_data = g_slice_new0 (ItemParserData);
parser_data->self = self;
parser_data->builder = builder;
*parser = ItemParser;
*data = parser_data;
return TRUE;
}
return FALSE;
}
static void
dzl_radio_box_custom_finished (GtkBuildable *buildable,
GtkBuilder *builder,
GObject *child,
const gchar *tagname,
gpointer user_data)
{
DzlRadioBox *self = (DzlRadioBox *)buildable;
g_assert (DZL_IS_RADIO_BOX (self));
g_assert (GTK_IS_BUILDER (builder));
g_assert (tagname != NULL);
if (g_strcmp0 (tagname, "items") == 0)
{
ItemParserData *parser_data = user_data;
g_free (parser_data->id);
g_string_free (parser_data->text, TRUE);
g_slice_free (ItemParserData, parser_data);
}
}
static void
buildable_iface_init (GtkBuildableIface *iface)
{
iface->custom_tag_start = dzl_radio_box_custom_tag_start;
iface->custom_finished = dzl_radio_box_custom_finished;
}