/* GTK - The GIMP Toolkit * Copyright © 2012 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 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 . * * Author: Cosimo Cecchi * */ /** * SECTION:gtklevelbar * @Title: GtkLevelBar * @Short_description: A bar that can used as a level indicator * * The #GtkLevelBar is a bar widget that can be used * as a level indicator. Typical use cases are displaying the strength * of a password, or showing the charge level of a battery. * * Use gtk_level_bar_set_value() to set the current value, and * gtk_level_bar_add_offset_value() to set the value offsets at which * the bar will be considered in a different state. GTK will add two offsets * by default on the level bar: #GTK_LEVEL_BAR_OFFSET_LOW and * #GTK_LEVEL_BAR_OFFSET_HIGH, with values 0.25 and 0.75 respectively. * * ## Adding a custom offset on the bar * * |[ * * static GtkWidget * * create_level_bar (void) * { * GtkWidget *widget; * GtkLevelBar *bar; * * widget = gtk_level_bar_new (); * bar = GTK_LEVEL_BAR (widget); * * // This changes the value of the default low offset * * gtk_level_bar_add_offset_value (bar, * GTK_LEVEL_BAR_OFFSET_LOW, * 0.10); * * // This adds a new offset to the bar; the application will * // be able to change its color CSS like this: * // * // levelbar block.my-offset { * // background-color: magenta; * // border-style: solid; * // border-color: black; * // border-style: 1px; * // } * * gtk_level_bar_add_offset_value (bar, "my-offset", 0.60); * * return widget; * } * ]| * * The default interval of values is between zero and one, but it’s possible to * modify the interval using gtk_level_bar_set_min_value() and * gtk_level_bar_set_max_value(). The value will be always drawn in proportion to * the admissible interval, i.e. a value of 15 with a specified interval between * 10 and 20 is equivalent to a value of 0.5 with an interval between 0 and 1. * When #GTK_LEVEL_BAR_MODE_DISCRETE is used, the bar level is rendered * as a finite number of separated blocks instead of a single one. The number * of blocks that will be rendered is equal to the number of units specified by * the admissible interval. * * For instance, to build a bar rendered with five blocks, it’s sufficient to * set the minimum value to 0 and the maximum value to 5 after changing the indicator * mode to discrete. * * Since: 3.6 * * # CSS nodes * * |[ * levelbar[.discrete] * ╰── trough * ├── block.filled. * ┊ * ├── block.empty * ┊ * ]| * * GtkLevelBar has a main CSS node with name levelbar and one of the style * classes .discrete or .continuous and a subnode with name trough. Below the * trough node are a number of nodes with name block and style class .filled * or .empty. In continuous mode, there is exactly one node of each, in discrete * mode, the number of filled and unfilled nodes corresponds to blocks that are * drawn. The block.filled nodes also get a style class . corresponding * to the level for the current value. */ #include "config.h" #include "gtkbuildable.h" #include "gtkbuilderprivate.h" #include "gtkintl.h" #include "gtkorientableprivate.h" #include "gtklevelbar.h" #include "gtkmarshalers.h" #include "gtkstylecontext.h" #include "gtktypebuiltins.h" #include "gtkwidget.h" #include "gtkwidgetprivate.h" #include "gtkstylecontextprivate.h" #include "gtkcssstylepropertyprivate.h" #include "gtkcssnodeprivate.h" #include #include #include "a11y/gtklevelbaraccessible.h" #include "fallback-c89.c" #define DEFAULT_BLOCK_SIZE 3 enum { PROP_VALUE = 1, PROP_MIN_VALUE, PROP_MAX_VALUE, PROP_MODE, PROP_INVERTED, LAST_PROPERTY, PROP_ORIENTATION /* overridden */ }; enum { SIGNAL_OFFSET_CHANGED, NUM_SIGNALS }; static GParamSpec *properties[LAST_PROPERTY] = { NULL, }; static guint signals[NUM_SIGNALS] = { 0, }; typedef struct { gchar *name; gdouble value; } GtkLevelBarOffset; struct _GtkLevelBarPrivate { GtkOrientation orientation; GtkLevelBarMode bar_mode; gdouble min_value; gdouble max_value; gdouble cur_value; GList *offsets; GtkCssNode *trough_node; GtkCssNode **block_node; guint n_blocks; guint inverted : 1; }; static void gtk_level_bar_set_value_internal (GtkLevelBar *self, gdouble value); static void gtk_level_bar_buildable_init (GtkBuildableIface *iface); G_DEFINE_TYPE_WITH_CODE (GtkLevelBar, gtk_level_bar, GTK_TYPE_WIDGET, G_ADD_PRIVATE (GtkLevelBar) G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL) G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_level_bar_buildable_init)) static GtkLevelBarOffset * gtk_level_bar_offset_new (const gchar *name, gdouble value) { GtkLevelBarOffset *offset = g_slice_new0 (GtkLevelBarOffset); offset->name = g_strdup (name); offset->value = value; return offset; } static void gtk_level_bar_offset_free (GtkLevelBarOffset *offset) { g_free (offset->name); g_slice_free (GtkLevelBarOffset, offset); } static gint offset_find_func (gconstpointer data, gconstpointer user_data) { const GtkLevelBarOffset *offset = data; const gchar *name = user_data; return g_strcmp0 (name, offset->name); } static gint offset_sort_func (gconstpointer a, gconstpointer b) { const GtkLevelBarOffset *offset_a = a; const GtkLevelBarOffset *offset_b = b; return (offset_a->value > offset_b->value); } static gboolean gtk_level_bar_ensure_offset (GtkLevelBar *self, const gchar *name, gdouble value) { GList *existing; GtkLevelBarOffset *offset = NULL; existing = g_list_find_custom (self->priv->offsets, name, offset_find_func); if (existing) offset = existing->data; if (offset && (offset->value == value)) return FALSE; if (offset) { gtk_level_bar_offset_free (offset); self->priv->offsets = g_list_delete_link (self->priv->offsets, existing); } offset = gtk_level_bar_offset_new (name, value); self->priv->offsets = g_list_insert_sorted (self->priv->offsets, offset, offset_sort_func); return TRUE; } static gboolean gtk_level_bar_value_in_interval (GtkLevelBar *self, gdouble value) { return ((value >= self->priv->min_value) && (value <= self->priv->max_value)); } static void gtk_level_bar_get_min_block_size (GtkLevelBar *self, gint *block_width, gint *block_height) { GtkWidget *widget = GTK_WIDGET (self); GtkStyleContext *context; GtkStateFlags state; GtkBorder border, tmp, tmp2; gint min_width, min_height; context = gtk_widget_get_style_context (widget); gtk_style_context_save_to_node (context, self->priv->block_node[0]); state = gtk_style_context_get_state (context); gtk_style_context_get_border (context, state, &border); gtk_style_context_get_padding (context, state, &tmp); gtk_style_context_get_margin (context, state, &tmp2); gtk_style_context_restore (context); gtk_style_context_get_style (context, "min-block-width", &min_width, "min-block-height", &min_height, NULL); border.top += tmp.top; border.right += tmp.right; border.bottom += tmp.bottom; border.left += tmp.left; border.top += tmp2.top; border.right += tmp2.right; border.bottom += tmp2.bottom; border.left += tmp2.left; if (block_width) *block_width = MAX (border.left + border.right, min_width); if (block_height) *block_height = MAX (border.top + border.bottom, min_height); } static gint gtk_level_bar_get_num_blocks (GtkLevelBar *self) { if (self->priv->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS) return 1; else if (self->priv->bar_mode == GTK_LEVEL_BAR_MODE_DISCRETE) return MAX (1, (gint) (round (self->priv->max_value) - round (self->priv->min_value))); return 0; } static void gtk_level_bar_get_borders (GtkLevelBar *self, GtkBorder *borders_out) { GtkWidget *widget = GTK_WIDGET (self); GtkStyleContext *context; GtkStateFlags state; GtkBorder border, tmp; context = gtk_widget_get_style_context (widget); gtk_style_context_save_to_node (context, self->priv->trough_node); state = gtk_style_context_get_state (context); gtk_style_context_get_border (context, state, &border); gtk_style_context_get_padding (context, state, &tmp); gtk_style_context_restore (context); border.top += tmp.top; border.right += tmp.right; border.bottom += tmp.bottom; border.left += tmp.left; if (borders_out) *borders_out = border; } static void gtk_level_bar_draw_fill_continuous (GtkLevelBar *self, cairo_t *cr, cairo_rectangle_int_t *fill_area) { GtkWidget *widget = GTK_WIDGET (self); GtkStyleContext *context; cairo_rectangle_int_t base_area, block_area; GtkBorder block_margin; gdouble fill_percentage; gboolean inverted; inverted = self->priv->inverted; if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL) { if (self->priv->orientation == GTK_ORIENTATION_HORIZONTAL) inverted = !inverted; } context = gtk_widget_get_style_context (widget); /* render the empty (unfilled) part */ gtk_style_context_save_to_node (context, self->priv->block_node[inverted ? 0 : 1]); gtk_style_context_get_margin (context, gtk_style_context_get_state (context), &block_margin); base_area = *fill_area; base_area.x += block_margin.left; base_area.y += block_margin.top; base_area.width -= block_margin.left + block_margin.right; base_area.height -= block_margin.top + block_margin.bottom; gtk_render_background (context, cr, base_area.x, base_area.y, base_area.width, base_area.height); gtk_render_frame (context, cr, base_area.x, base_area.y, base_area.width, base_area.height); gtk_style_context_restore (context); /* now render the filled part on top of it */ block_area = base_area; fill_percentage = (self->priv->cur_value - self->priv->min_value) / (self->priv->max_value - self->priv->min_value); if (self->priv->orientation == GTK_ORIENTATION_HORIZONTAL) { block_area.width = (gint) floor (block_area.width * fill_percentage); if (inverted) block_area.x += base_area.width - block_area.width; } else { block_area.height = (gint) floor (block_area.height * fill_percentage); if (inverted) block_area.y += base_area.height - block_area.height; } gtk_style_context_save_to_node (context, self->priv->block_node[inverted ? 1 : 0]); gtk_render_background (context, cr, block_area.x, block_area.y, block_area.width, block_area.height); gtk_render_frame (context, cr, block_area.x, block_area.y, block_area.width, block_area.height); gtk_style_context_restore (context); } static void gtk_level_bar_draw_fill_discrete (GtkLevelBar *self, cairo_t *cr, cairo_rectangle_int_t *fill_area) { GtkWidget *widget = GTK_WIDGET (self); GtkStyleContext *context; gint num_blocks, i; gint block_width, block_height; gint block_draw_width, block_draw_height; GtkBorder block_margin; cairo_rectangle_int_t block_area; context = gtk_widget_get_style_context (widget); gtk_level_bar_get_min_block_size (self, &block_width, &block_height); block_area = *fill_area; num_blocks = (gint) round (self->priv->max_value) - (gint) round (self->priv->min_value); if (self->priv->orientation == GTK_ORIENTATION_HORIZONTAL) block_width = MAX (block_width, (gint) floor (block_area.width / num_blocks)); else block_height = MAX (block_height, (gint) floor (block_area.height / num_blocks)); gtk_style_context_save_to_node (context, self->priv->block_node[0]); gtk_style_context_get_margin (context, gtk_style_context_get_state (context), &block_margin); gtk_style_context_restore (context); block_draw_width = block_width - block_margin.left - block_margin.right; block_draw_height = block_height - block_margin.top - block_margin.bottom; if (self->priv->orientation == GTK_ORIENTATION_HORIZONTAL) { block_draw_height = MAX (block_draw_height, block_area.height - block_margin.top - block_margin.bottom); block_area.y += block_margin.top; } else { block_draw_width = MAX (block_draw_width, block_area.width - block_margin.left - block_margin.right); block_area.x += block_margin.left; } for (i = 0; i < num_blocks; i++) { if (self->priv->orientation == GTK_ORIENTATION_HORIZONTAL) block_area.x += block_margin.left; else block_area.y += block_margin.top; gtk_style_context_save_to_node (context, self->priv->block_node[i]); gtk_render_background (context, cr, block_area.x, block_area.y, block_draw_width, block_draw_height); gtk_render_frame (context, cr, block_area.x, block_area.y, block_draw_width, block_draw_height); gtk_style_context_restore (context); if (self->priv->orientation == GTK_ORIENTATION_HORIZONTAL) block_area.x += block_draw_width + block_margin.right; else block_area.y += block_draw_height + block_margin.bottom; } } static void gtk_level_bar_draw_fill (GtkLevelBar *self, cairo_t *cr) { GtkWidget *widget = GTK_WIDGET (self); GtkBorder trough_borders; cairo_rectangle_int_t fill_area; gtk_level_bar_get_borders (self, &trough_borders); fill_area.x = trough_borders.left; fill_area.y = trough_borders.top; fill_area.width = gtk_widget_get_allocated_width (widget) - trough_borders.left - trough_borders.right; fill_area.height = gtk_widget_get_allocated_height (widget) - trough_borders.top - trough_borders.bottom; if (self->priv->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS) gtk_level_bar_draw_fill_continuous (self, cr, &fill_area); else gtk_level_bar_draw_fill_discrete (self, cr, &fill_area); } static gboolean gtk_level_bar_draw (GtkWidget *widget, cairo_t *cr) { GtkLevelBar *self = GTK_LEVEL_BAR (widget); GtkStyleContext *context; gint width, height; width = gtk_widget_get_allocated_width (widget); height = gtk_widget_get_allocated_height (widget); context = gtk_widget_get_style_context (widget); gtk_style_context_save_to_node (context, self->priv->trough_node); gtk_render_background (context, cr, 0, 0, width, height); gtk_render_frame (context, cr, 0, 0, width, height); gtk_style_context_restore (context); gtk_level_bar_draw_fill (self, cr); return FALSE; } static void gtk_level_bar_get_preferred_width (GtkWidget *widget, gint *minimum, gint *natural) { GtkLevelBar *self = GTK_LEVEL_BAR (widget); GtkBorder borders; gint num_blocks; gint width, block_width; num_blocks = gtk_level_bar_get_num_blocks (self); gtk_level_bar_get_min_block_size (self, &block_width, NULL); gtk_level_bar_get_borders (self, &borders); width = borders.left + borders.right; if (self->priv->orientation == GTK_ORIENTATION_HORIZONTAL) width += num_blocks * block_width; else width += block_width; *minimum = width; *natural = width; } static void gtk_level_bar_get_preferred_height (GtkWidget *widget, gint *minimum, gint *natural) { GtkLevelBar *self = GTK_LEVEL_BAR (widget); GtkBorder borders; gint num_blocks; gint height, block_height; num_blocks = gtk_level_bar_get_num_blocks (self); gtk_level_bar_get_min_block_size (self, NULL, &block_height); gtk_level_bar_get_borders (self, &borders); height = borders.top + borders.bottom; if (self->priv->orientation == GTK_ORIENTATION_VERTICAL) height += num_blocks * block_height; else height += block_height; *minimum = height; *natural = height; } static void gtk_level_bar_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GTK_WIDGET_CLASS (gtk_level_bar_parent_class)->size_allocate (widget, allocation); _gtk_widget_set_simple_clip (widget, NULL); } static void node_style_changed_cb (GtkCssNode *node, GtkCssStyle *old_style, GtkCssStyle *new_style, GtkWidget *widget) { GtkBitmask *changes; static GtkBitmask *affects_size = NULL; if (G_UNLIKELY (affects_size == NULL)) affects_size = _gtk_css_style_property_get_mask_affecting (GTK_CSS_AFFECTS_SIZE | GTK_CSS_AFFECTS_CLIP); changes = _gtk_bitmask_new (); changes = gtk_css_style_add_difference (changes, old_style, new_style); if (_gtk_bitmask_intersects (changes, affects_size)) gtk_widget_queue_resize (widget); else gtk_widget_queue_draw (widget); _gtk_bitmask_free (changes); } static void update_block_nodes (GtkLevelBar *self) { GtkLevelBarPrivate *priv = self->priv; guint n_blocks; guint i; if (priv->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS) n_blocks = 2; else n_blocks = MAX (1, (gint) (round (priv->max_value) - round (priv->min_value))); if (priv->n_blocks == n_blocks) return; else if (n_blocks < priv->n_blocks) { for (i = n_blocks; i < priv->n_blocks; i++) gtk_css_node_set_parent (priv->block_node[i], NULL); priv->block_node = g_renew (GtkCssNode*, priv->block_node, n_blocks); priv->n_blocks = n_blocks; } else { priv->block_node = g_renew (GtkCssNode*, priv->block_node, n_blocks); for (i = priv->n_blocks; i < n_blocks; i++) { priv->block_node[i] = gtk_css_node_new (); gtk_css_node_set_name (priv->block_node[i], I_("block")); gtk_css_node_set_parent (priv->block_node[i], priv->trough_node); gtk_css_node_set_state (priv->block_node[i], gtk_css_node_get_state (priv->trough_node)); g_signal_connect_object (priv->block_node[i], "style-changed", G_CALLBACK (node_style_changed_cb), self, 0); g_object_unref (priv->block_node[i]); } priv->n_blocks = n_blocks; } } static void update_mode_style_classes (GtkLevelBar *self) { GtkLevelBarPrivate *priv = self->priv; GtkCssNode *widget_node; widget_node = gtk_widget_get_css_node (GTK_WIDGET (self)); if (priv->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS) { gtk_css_node_remove_class (widget_node, g_quark_from_static_string ("discrete")); gtk_css_node_add_class (widget_node, g_quark_from_static_string ("continuous")); } else if (priv->bar_mode == GTK_LEVEL_BAR_MODE_DISCRETE) { gtk_css_node_add_class (widget_node, g_quark_from_static_string ("discrete")); gtk_css_node_remove_class (widget_node, g_quark_from_static_string ("continuous")); } } static void gtk_level_bar_state_flags_changed (GtkWidget *widget, GtkStateFlags previous_state) { GtkLevelBar *self = GTK_LEVEL_BAR (widget); GtkLevelBarPrivate *priv = self->priv; GtkStateFlags state; gint i; state = gtk_widget_get_state_flags (widget); gtk_css_node_set_state (priv->trough_node, state); for (i = 0; i < priv->n_blocks; i++) gtk_css_node_set_state (priv->block_node[i], state); GTK_WIDGET_CLASS (gtk_level_bar_parent_class)->state_flags_changed (widget, previous_state); } static void update_level_style_classes (GtkLevelBar *self) { GtkLevelBarPrivate *priv = self->priv; gdouble value; const gchar *value_class = NULL; GtkLevelBarOffset *offset, *prev_offset; GList *l; value = gtk_level_bar_get_value (self); for (l = priv->offsets; l != NULL; l = l->next) { offset = l->data; /* find the right offset for our style class */ if ((l->prev == NULL && value <= offset->value) || (l->next == NULL && value >= offset->value)) { value_class = offset->name; } else if (l->prev != NULL) { prev_offset = l->prev->data; if (prev_offset->value <= value && value < offset->value) value_class = offset->name; } if (value_class) break; } if (value_class) { const gchar *classes[3] = { NULL, NULL, NULL }; gint num_filled, num_blocks, i; gboolean inverted; inverted = priv->inverted; if (priv->orientation == GTK_ORIENTATION_HORIZONTAL && gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL) inverted = !inverted; if (priv->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS) { num_filled = 1; num_blocks = 2; } else { num_filled = (gint) round (priv->cur_value) - (gint) round (priv->min_value); num_blocks = (gint) round (priv->max_value) - (gint) round (priv->min_value); } classes[0] = "filled"; classes[1] = value_class; for (i = 0; i < num_filled; i++) gtk_css_node_set_classes (priv->block_node[inverted ? num_blocks - 1 - i : i], classes); classes[0] = "empty"; classes[1] = NULL; for (; i < num_blocks; i++) gtk_css_node_set_classes (priv->block_node[inverted ? num_blocks - 1 - i : i], classes); } } static void gtk_level_bar_direction_changed (GtkWidget *widget, GtkTextDirection previous_dir) { GtkLevelBar *self = GTK_LEVEL_BAR (widget); update_level_style_classes (self); GTK_WIDGET_CLASS (gtk_level_bar_parent_class)->direction_changed (widget, previous_dir); } static void gtk_level_bar_ensure_offsets_in_range (GtkLevelBar *self) { GtkLevelBarOffset *offset; GList *l = self->priv->offsets; while (l != NULL) { offset = l->data; l = l->next; if (offset->value < self->priv->min_value) gtk_level_bar_ensure_offset (self, offset->name, self->priv->min_value); else if (offset->value > self->priv->max_value) gtk_level_bar_ensure_offset (self, offset->name, self->priv->max_value); } } typedef struct { GtkLevelBar *self; GtkBuilder *builder; GList *offsets; } OffsetsParserData; static void offset_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **names, const gchar **values, gpointer user_data, GError **error) { OffsetsParserData *data = user_data; if (strcmp (element_name, "offsets") == 0) { if (!_gtk_builder_check_parent (data->builder, context, "object", error)) return; if (!g_markup_collect_attributes (element_name, names, values, error, G_MARKUP_COLLECT_INVALID, NULL, NULL, G_MARKUP_COLLECT_INVALID)) _gtk_builder_prefix_error (data->builder, context, error); } else if (strcmp (element_name, "offset") == 0) { const gchar *name; const gchar *value; GValue gvalue = G_VALUE_INIT; GtkLevelBarOffset *offset; if (!_gtk_builder_check_parent (data->builder, context, "offsets", error)) return; if (!g_markup_collect_attributes (element_name, names, values, error, G_MARKUP_COLLECT_STRING, "name", &name, G_MARKUP_COLLECT_STRING, "value", &value, G_MARKUP_COLLECT_INVALID)) { _gtk_builder_prefix_error (data->builder, context, error); return; } if (!gtk_builder_value_from_string_type (data->builder, G_TYPE_DOUBLE, value, &gvalue, error)) { _gtk_builder_prefix_error (data->builder, context, error); return; } offset = gtk_level_bar_offset_new (name, g_value_get_double (&gvalue)); data->offsets = g_list_prepend (data->offsets, offset); } else { _gtk_builder_error_unhandled_tag (data->builder, context, "GtkLevelBar", element_name, error); } } static const GMarkupParser offset_parser = { offset_start_element }; static gboolean gtk_level_bar_buildable_custom_tag_start (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const gchar *tagname, GMarkupParser *parser, gpointer *parser_data) { OffsetsParserData *data; if (child) return FALSE; if (strcmp (tagname, "offsets") != 0) return FALSE; data = g_slice_new0 (OffsetsParserData); data->self = GTK_LEVEL_BAR (buildable); data->builder = builder; data->offsets = NULL; *parser = offset_parser; *parser_data = data; return TRUE; } static void gtk_level_bar_buildable_custom_finished (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const gchar *tagname, gpointer user_data) { OffsetsParserData *data = user_data; GtkLevelBar *self; GtkLevelBarOffset *offset; GList *l; self = data->self; if (strcmp (tagname, "offsets") != 0) goto out; for (l = data->offsets; l != NULL; l = l->next) { offset = l->data; gtk_level_bar_add_offset_value (self, offset->name, offset->value); } out: g_list_free_full (data->offsets, (GDestroyNotify) gtk_level_bar_offset_free); g_slice_free (OffsetsParserData, data); } static void gtk_level_bar_buildable_init (GtkBuildableIface *iface) { iface->custom_tag_start = gtk_level_bar_buildable_custom_tag_start; iface->custom_finished = gtk_level_bar_buildable_custom_finished; } static void gtk_level_bar_set_orientation (GtkLevelBar *self, GtkOrientation orientation) { if (self->priv->orientation != orientation) { self->priv->orientation = orientation; _gtk_orientable_set_style_classes (GTK_ORIENTABLE (self)); gtk_widget_queue_resize (GTK_WIDGET (self)); g_object_notify (G_OBJECT (self), "orientation"); } } static void gtk_level_bar_get_property (GObject *obj, guint property_id, GValue *value, GParamSpec *pspec) { GtkLevelBar *self = GTK_LEVEL_BAR (obj); switch (property_id) { case PROP_VALUE: g_value_set_double (value, gtk_level_bar_get_value (self)); break; case PROP_MIN_VALUE: g_value_set_double (value, gtk_level_bar_get_min_value (self)); break; case PROP_MAX_VALUE: g_value_set_double (value, gtk_level_bar_get_max_value (self)); break; case PROP_MODE: g_value_set_enum (value, gtk_level_bar_get_mode (self)); break; case PROP_INVERTED: g_value_set_boolean (value, gtk_level_bar_get_inverted (self)); break; case PROP_ORIENTATION: g_value_set_enum (value, self->priv->orientation); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); break; } } static void gtk_level_bar_set_property (GObject *obj, guint property_id, const GValue *value, GParamSpec *pspec) { GtkLevelBar *self = GTK_LEVEL_BAR (obj); switch (property_id) { case PROP_VALUE: gtk_level_bar_set_value (self, g_value_get_double (value)); break; case PROP_MIN_VALUE: gtk_level_bar_set_min_value (self, g_value_get_double (value)); break; case PROP_MAX_VALUE: gtk_level_bar_set_max_value (self, g_value_get_double (value)); break; case PROP_MODE: gtk_level_bar_set_mode (self, g_value_get_enum (value)); break; case PROP_INVERTED: gtk_level_bar_set_inverted (self, g_value_get_boolean (value)); break; case PROP_ORIENTATION: gtk_level_bar_set_orientation (self, g_value_get_enum (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); break; } } static void gtk_level_bar_finalize (GObject *obj) { GtkLevelBar *self = GTK_LEVEL_BAR (obj); GtkLevelBarPrivate *priv = self->priv; g_list_free_full (priv->offsets, (GDestroyNotify) gtk_level_bar_offset_free); g_free (priv->block_node); G_OBJECT_CLASS (gtk_level_bar_parent_class)->finalize (obj); } static void gtk_level_bar_class_init (GtkLevelBarClass *klass) { GObjectClass *oclass = G_OBJECT_CLASS (klass); GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass); oclass->get_property = gtk_level_bar_get_property; oclass->set_property = gtk_level_bar_set_property; oclass->finalize = gtk_level_bar_finalize; wclass->draw = gtk_level_bar_draw; wclass->size_allocate = gtk_level_bar_size_allocate; wclass->get_preferred_width = gtk_level_bar_get_preferred_width; wclass->get_preferred_height = gtk_level_bar_get_preferred_height; wclass->state_flags_changed = gtk_level_bar_state_flags_changed; wclass->direction_changed = gtk_level_bar_direction_changed; g_object_class_override_property (oclass, PROP_ORIENTATION, "orientation"); /** * GtkLevelBar::offset-changed: * @self: a #GtkLevelBar * @name: the name of the offset that changed value * * Emitted when an offset specified on the bar changes value as an * effect to gtk_level_bar_add_offset_value() being called. * * The signal supports detailed connections; you can connect to the * detailed signal "changed::x" in order to only receive callbacks when * the value of offset "x" changes. * * Since: 3.6 */ signals[SIGNAL_OFFSET_CHANGED] = g_signal_new (I_("offset-changed"), GTK_TYPE_LEVEL_BAR, G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED, G_STRUCT_OFFSET (GtkLevelBarClass, offset_changed), NULL, NULL, _gtk_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); /** * GtkLevelBar:value: * * The #GtkLevelBar:value property determines the currently * filled value of the level bar. * * Since: 3.6 */ properties[PROP_VALUE] = g_param_spec_double ("value", P_("Currently filled value level"), P_("Currently filled value level of the level bar"), 0.0, G_MAXDOUBLE, 0.0, G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); /** * GtkLevelBar:min-value: * * The #GtkLevelBar:min-value property determines the minimum value of * the interval that can be displayed by the bar. * * Since: 3.6 */ properties[PROP_MIN_VALUE] = g_param_spec_double ("min-value", P_("Minimum value level for the bar"), P_("Minimum value level that can be displayed by the bar"), 0.0, G_MAXDOUBLE, 0.0, G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); /** * GtkLevelBar:max-value: * * The #GtkLevelBar:max-value property determaxes the maximum value of * the interval that can be displayed by the bar. * * Since: 3.6 */ properties[PROP_MAX_VALUE] = g_param_spec_double ("max-value", P_("Maximum value level for the bar"), P_("Maximum value level that can be displayed by the bar"), 0.0, G_MAXDOUBLE, 1.0, G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); /** * GtkLevelBar:mode: * * The #GtkLevelBar:mode property determines the way #GtkLevelBar * interprets the value properties to draw the level fill area. * Specifically, when the value is #GTK_LEVEL_BAR_MODE_CONTINUOUS, * #GtkLevelBar will draw a single block representing the current value in * that area; when the value is #GTK_LEVEL_BAR_MODE_DISCRETE, * the widget will draw a succession of separate blocks filling the * draw area, with the number of blocks being equal to the units separating * the integral roundings of #GtkLevelBar:min-value and #GtkLevelBar:max-value. * * Since: 3.6 */ properties[PROP_MODE] = g_param_spec_enum ("mode", P_("The mode of the value indicator"), P_("The mode of the value indicator displayed by the bar"), GTK_TYPE_LEVEL_BAR_MODE, GTK_LEVEL_BAR_MODE_CONTINUOUS, G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); /** * GtkLevelBar:inverted: * * Level bars normally grow from top to bottom or left to right. * Inverted level bars grow in the opposite direction. * * Since: 3.8 */ properties[PROP_INVERTED] = g_param_spec_boolean ("inverted", P_("Inverted"), P_("Invert the direction in which the level bar grows"), FALSE, G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); /** * GtkLevelBar:min-block-height: * * The min-block-height style property determines the minimum * height for blocks filling the #GtkLevelBar widget. * * Since: 3.6 */ gtk_widget_class_install_style_property (wclass, g_param_spec_int ("min-block-height", P_("Minimum height for filling blocks"), P_("Minimum height for blocks that fill the bar"), 1, G_MAXINT, DEFAULT_BLOCK_SIZE, G_PARAM_READWRITE)); /** * GtkLevelBar:min-block-width: * * The min-block-width style property determines the minimum * width for blocks filling the #GtkLevelBar widget. * * Since: 3.6 */ gtk_widget_class_install_style_property (wclass, g_param_spec_int ("min-block-width", P_("Minimum width for filling blocks"), P_("Minimum width for blocks that fill the bar"), 1, G_MAXINT, DEFAULT_BLOCK_SIZE, G_PARAM_READWRITE)); g_object_class_install_properties (oclass, LAST_PROPERTY, properties); gtk_widget_class_set_accessible_type (wclass, GTK_TYPE_LEVEL_BAR_ACCESSIBLE); gtk_widget_class_set_css_name (wclass, "levelbar"); } static void gtk_level_bar_init (GtkLevelBar *self) { GtkLevelBarPrivate *priv; GtkCssNode *widget_node; priv = self->priv = gtk_level_bar_get_instance_private (self); priv->cur_value = 0.0; priv->min_value = 0.0; priv->max_value = 1.0; /* set initial orientation and style classes */ priv->orientation = GTK_ORIENTATION_HORIZONTAL; _gtk_orientable_set_style_classes (GTK_ORIENTABLE (self)); priv->inverted = FALSE; gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); widget_node = gtk_widget_get_css_node (GTK_WIDGET (self)); priv->trough_node = gtk_css_node_new (); gtk_css_node_set_name (priv->trough_node, I_("trough")); gtk_css_node_set_parent (priv->trough_node, widget_node); gtk_css_node_set_state (priv->trough_node, gtk_css_node_get_state (widget_node)); g_signal_connect_object (priv->trough_node, "style-changed", G_CALLBACK (node_style_changed_cb), self, 0); g_object_unref (priv->trough_node); gtk_level_bar_ensure_offset (self, GTK_LEVEL_BAR_OFFSET_LOW, 0.25); gtk_level_bar_ensure_offset (self, GTK_LEVEL_BAR_OFFSET_HIGH, 0.75); gtk_level_bar_ensure_offset (self, "full", 1.0); priv->block_node = NULL; priv->n_blocks = 0; priv->bar_mode = GTK_LEVEL_BAR_MODE_CONTINUOUS; update_mode_style_classes (self); update_block_nodes (self); update_level_style_classes (self); } /** * gtk_level_bar_new: * * Creates a new #GtkLevelBar. * * Returns: a #GtkLevelBar. * * Since: 3.6 */ GtkWidget * gtk_level_bar_new (void) { return g_object_new (GTK_TYPE_LEVEL_BAR, NULL); } /** * gtk_level_bar_new_for_interval: * @min_value: a positive value * @max_value: a positive value * * Utility constructor that creates a new #GtkLevelBar for the specified * interval. * * Returns: a #GtkLevelBar * * Since: 3.6 */ GtkWidget * gtk_level_bar_new_for_interval (gdouble min_value, gdouble max_value) { return g_object_new (GTK_TYPE_LEVEL_BAR, "min-value", min_value, "max-value", max_value, NULL); } /** * gtk_level_bar_get_min_value: * @self: a #GtkLevelBar * * Returns the value of the #GtkLevelBar:min-value property. * * Returns: a positive value * * Since: 3.6 */ gdouble gtk_level_bar_get_min_value (GtkLevelBar *self) { g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0.0); return self->priv->min_value; } /** * gtk_level_bar_get_max_value: * @self: a #GtkLevelBar * * Returns the value of the #GtkLevelBar:max-value property. * * Returns: a positive value * * Since: 3.6 */ gdouble gtk_level_bar_get_max_value (GtkLevelBar *self) { g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0.0); return self->priv->max_value; } /** * gtk_level_bar_get_value: * @self: a #GtkLevelBar * * Returns the value of the #GtkLevelBar:value property. * * Returns: a value in the interval between * #GtkLevelBar:min-value and #GtkLevelBar:max-value * * Since: 3.6 */ gdouble gtk_level_bar_get_value (GtkLevelBar *self) { g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0.0); return self->priv->cur_value; } static void gtk_level_bar_set_value_internal (GtkLevelBar *self, gdouble value) { self->priv->cur_value = value; g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VALUE]); gtk_widget_queue_draw (GTK_WIDGET (self)); } /** * gtk_level_bar_set_min_value: * @self: a #GtkLevelBar * @value: a positive value * * Sets the value of the #GtkLevelBar:min-value property. * * Since: 3.6 */ void gtk_level_bar_set_min_value (GtkLevelBar *self, gdouble value) { GtkLevelBarPrivate *priv = self->priv; g_return_if_fail (GTK_IS_LEVEL_BAR (self)); g_return_if_fail (value >= 0.0); if (value == priv->min_value) return; priv->min_value = value; if (priv->min_value > priv->cur_value) gtk_level_bar_set_value_internal (self, priv->min_value); update_block_nodes (self); update_level_style_classes (self); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MIN_VALUE]); } /** * gtk_level_bar_set_max_value: * @self: a #GtkLevelBar * @value: a positive value * * Sets the value of the #GtkLevelBar:max-value property. * * Since: 3.6 */ void gtk_level_bar_set_max_value (GtkLevelBar *self, gdouble value) { GtkLevelBarPrivate *priv = self->priv; g_return_if_fail (GTK_IS_LEVEL_BAR (self)); g_return_if_fail (value >= 0.0); if (value == priv->max_value) return; priv->max_value = value; if (priv->max_value < priv->cur_value) gtk_level_bar_set_value_internal (self, priv->max_value); gtk_level_bar_ensure_offsets_in_range (self); update_block_nodes (self); update_level_style_classes (self); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_VALUE]); } /** * gtk_level_bar_set_value: * @self: a #GtkLevelBar * @value: a value in the interval between * #GtkLevelBar:min-value and #GtkLevelBar:max-value * * Sets the value of the #GtkLevelBar:value property. * * Since: 3.6 */ void gtk_level_bar_set_value (GtkLevelBar *self, gdouble value) { g_return_if_fail (GTK_IS_LEVEL_BAR (self)); if (value == self->priv->cur_value) return; gtk_level_bar_set_value_internal (self, value); update_level_style_classes (self); } /** * gtk_level_bar_get_mode: * @self: a #GtkLevelBar * * Returns the value of the #GtkLevelBar:mode property. * * Returns: a #GtkLevelBarMode * * Since: 3.6 */ GtkLevelBarMode gtk_level_bar_get_mode (GtkLevelBar *self) { g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0); return self->priv->bar_mode; } /** * gtk_level_bar_set_mode: * @self: a #GtkLevelBar * @mode: a #GtkLevelBarMode * * Sets the value of the #GtkLevelBar:mode property. * * Since: 3.6 */ void gtk_level_bar_set_mode (GtkLevelBar *self, GtkLevelBarMode mode) { GtkLevelBarPrivate *priv = self->priv; g_return_if_fail (GTK_IS_LEVEL_BAR (self)); if (priv->bar_mode == mode) return; priv->bar_mode = mode; update_mode_style_classes (self); update_block_nodes (self); update_level_style_classes (self); gtk_widget_queue_resize (GTK_WIDGET (self)); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODE]); } /** * gtk_level_bar_get_inverted: * @self: a #GtkLevelBar * * Return the value of the #GtkLevelBar:inverted property. * * Returns: %TRUE if the level bar is inverted * * Since: 3.8 */ gboolean gtk_level_bar_get_inverted (GtkLevelBar *self) { g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), FALSE); return self->priv->inverted; } /** * gtk_level_bar_set_inverted: * @self: a #GtkLevelBar * @inverted: %TRUE to invert the level bar * * Sets the value of the #GtkLevelBar:inverted property. * * Since: 3.8 */ void gtk_level_bar_set_inverted (GtkLevelBar *self, gboolean inverted) { GtkLevelBarPrivate *priv = self->priv; g_return_if_fail (GTK_IS_LEVEL_BAR (self)); if (priv->inverted == inverted) return; priv->inverted = inverted; gtk_widget_queue_resize (GTK_WIDGET (self)); update_level_style_classes (self); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_INVERTED]); } /** * gtk_level_bar_remove_offset_value: * @self: a #GtkLevelBar * @name: (allow-none): the name of an offset in the bar * * Removes an offset marker previously added with * gtk_level_bar_add_offset_value(). * * Since: 3.6 */ void gtk_level_bar_remove_offset_value (GtkLevelBar *self, const gchar *name) { GtkLevelBarPrivate *priv = self->priv; GList *existing; g_return_if_fail (GTK_IS_LEVEL_BAR (self)); existing = g_list_find_custom (priv->offsets, name, offset_find_func); if (existing) { gtk_level_bar_offset_free (existing->data); priv->offsets = g_list_delete_link (priv->offsets, existing); update_level_style_classes (self); } } /** * gtk_level_bar_add_offset_value: * @self: a #GtkLevelBar * @name: the name of the new offset * @value: the value for the new offset * * Adds a new offset marker on @self at the position specified by @value. * When the bar value is in the interval topped by @value (or between @value * and #GtkLevelBar:max-value in case the offset is the last one on the bar) * a style class named `level-`@name will be applied * when rendering the level bar fill. * If another offset marker named @name exists, its value will be * replaced by @value. * * Since: 3.6 */ void gtk_level_bar_add_offset_value (GtkLevelBar *self, const gchar *name, gdouble value) { GQuark name_quark; g_return_if_fail (GTK_IS_LEVEL_BAR (self)); g_return_if_fail (gtk_level_bar_value_in_interval (self, value)); if (!gtk_level_bar_ensure_offset (self, name, value)) return; update_level_style_classes (self); name_quark = g_quark_from_string (name); g_signal_emit (self, signals[SIGNAL_OFFSET_CHANGED], name_quark, name); } /** * gtk_level_bar_get_offset_value: * @self: a #GtkLevelBar * @name: (allow-none): the name of an offset in the bar * @value: (out): location where to store the value * * Fetches the value specified for the offset marker @name in @self, * returning %TRUE in case an offset named @name was found. * * Returns: %TRUE if the specified offset is found * * Since: 3.6 */ gboolean gtk_level_bar_get_offset_value (GtkLevelBar *self, const gchar *name, gdouble *value) { GList *existing; GtkLevelBarOffset *offset = NULL; g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0.0); existing = g_list_find_custom (self->priv->offsets, name, offset_find_func); if (existing) offset = existing->data; if (!offset) return FALSE; if (value) *value = offset->value; return TRUE; }