Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 : : /*
3 : : * Copyright 2024 GNOME Foundation, Inc.
4 : : *
5 : : * This library is free software; you can redistribute it and/or
6 : : * modify it under the terms of the GNU Lesser General Public
7 : : * License as published by the Free Software Foundation; either
8 : : * version 2 of the License, or (at your option) any later version.
9 : : *
10 : : * This library is distributed in the hope that it will be useful,
11 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 : : * Lesser General Public License for more details.
14 : : *
15 : : * You should have received a copy of the GNU Lesser General
16 : : * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
17 : : *
18 : : * Authors:
19 : : * - Philip Withnall <pwithnall@gnome.org>
20 : : *
21 : : * SPDX-License-Identifier: GPL-3.0-or-later
22 : : */
23 : :
24 : : #include <adwaita.h>
25 : : #include <glib.h>
26 : : #include <glib-object.h>
27 : : #include <gsk/gsk.h>
28 : : #include <gtk/gtk.h>
29 : : #include <math.h>
30 : :
31 : : #include "cc-bar-chart.h"
32 : : #include "cc-bar-chart-bar.h"
33 : : #include "cc-bar-chart-group.h"
34 : :
35 : : /**
36 : : * CcBarChart:
37 : : *
38 : : * #CcBarChart is a widget for displaying a
39 : : * [bar chart](https://en.wikipedia.org/wiki/Bar_chart).
40 : : *
41 : : * It currently supports vertical bar charts, with a horizontal discrete axis
42 : : * and a vertical continuous axis. It supports single (non-grouped) bars, and a
43 : : * single colour scheme. It does not support negative data values.
44 : : * These limitations may change in future.
45 : : *
46 : : * The labels on the discrete axis can be set using
47 : : * cc_bar_chart_set_discrete_axis_labels(). You must localise these before
48 : : * setting them. The number of discrete axis labels must match the number of
49 : : * data elements set using cc_bar_chart_set_data().
50 : : *
51 : : * The widget’s appearance is undefined until data is provided to it via
52 : : * cc_bar_chart_set_data(). If you need to present a placeholder if no data is
53 : : * available, it’s recommended to place the #CcBarChart in a #GtkStack with the
54 : : * placeholder widgets in another page of the stack. Placeholders could be an
55 : : * image, text or spinner.
56 : : *
57 : : * The labels on the continuous axis are constructed dynamically to match the
58 : : * grid lines. Provide a grid line generator callback using
59 : : * cc_bar_chart_set_continuous_axis_grid_line_callback(), and provide a
60 : : * labelling callback using cc_bar_chart_set_continuous_axis_label_callback().
61 : : *
62 : : * An overlay line may be rendered to indicate a target or average. Set its
63 : : * value using cc_bar_chart_set_overlay_line_value();
64 : : *
65 : : * Bars in the chart may be selected, and the currently selected bar is
66 : : * available as #CcBarChart:selected-index. Bars may also be activated,
67 : : * resulting in #CcBarChart:bar-activated being emitted. By default, activating
68 : : * a bar will also focus and select it.
69 : : *
70 : : * # Shortcuts and Gestures
71 : : *
72 : : * The following signals have default keybindings:
73 : : *
74 : : * - #CcBarChart::move-cursor
75 : : *
76 : : * # CSS nodes
77 : : *
78 : : * |[<!-- language="plain" -->
79 : : * bar-chart
80 : : * ├── label.discrete-axis-label
81 : : * ├── label.continuous-axis-label
82 : : * ╰── bar-group[:hover][:selected]
83 : : * ╰── bar[:hover][:selected]
84 : : * ]|
85 : : *
86 : : * #CcBarChart uses a single CSS node named `bar-chart`. Each bar group is a
87 : : * sub-node named `bar-group`, with `bar` sub-nodes beneath it. Bars and groups
88 : : * may have `:hover` or `:selected` pseudo-selectors to indicate whether they
89 : : * are selected or being hovered over with the mouse. Axis labels are `label`
90 : : * sub-nodes, with either a `.discrete-axis-label` or `.continuous-axis-label`
91 : : * class.
92 : : *
93 : : * # Accessibility
94 : : *
95 : : * #CcBarChart uses the %GTK_ACCESSIBLE_ROLE_LIST role, with its groups using
96 : : * the %GTK_ACCESSIBLE_ROLE_GROUP role and bars using the
97 : : * %GTK_ACCESSIBLE_ROLE_LIST_ITEM role.
98 : : *
99 : : * This allows access technologies to interpret the chart as if it were a table
100 : : * of data. You should, however, specify a description for the chart as a whole,
101 : : * which also includes the semantics and value of
102 : : * #CcBarChart:overlay-line-value (if set).
103 : : *
104 : : * For example:
105 : : * |[
106 : : * gtk_accessible_update_property (GTK_ACCESSIBLE (bar_chart),
107 : : * GTK_ACCESSIBLE_PROPERTY_DESCRIPTION,
108 : : * _("Bar chart of screen time usage over "
109 : : * "the week starting 2nd February 2024. A "
110 : : * "line is overlayed at the 10 hour mark "
111 : : * "to indicate the configured screen time "
112 : : * "limit."),
113 : : * -1);
114 : : * ]|
115 : : *
116 : : * # Text direction
117 : : *
118 : : * If the widget text direction is changed (see gtk_widget_set_direction()),
119 : : * the discrete axis will be reversed and the continuous axis will be drawn on
120 : : * the opposite side of the graph from normal.
121 : : *
122 : : * For discrete axes which represent the passage of time, this is correct. For
123 : : * discrete axes which represent unordered categories, reversing the axis is
124 : : * not necessary and can be disabled by explicitly setting the text direction
125 : : * of the widget using gtk_widget_set_direction().
126 : : *
127 : : * See the [Material design guidelines](https://m2.material.io/design/usability/bidirectionality.html#mirroring-elements)
128 : : * or the [GNOME HIG](https://gitlab.gnome.org/Teams/Websites/developer.gnome.org-hig/-/issues/61)
129 : : * for guidance about when charts should be mirrored.
130 : : */
131 : : struct _CcBarChart {
132 : : GtkWidget parent_instance;
133 : :
134 : : /* Configured state: */
135 : : double *data; /* (nullable) (owned) */
136 : : size_t n_data;
137 : :
138 : : char **discrete_axis_labels; /* (nullable) (array zero-terminated=1) */
139 : : size_t n_discrete_axis_labels; /* cached result of g_strv_length(discrete_axis_labels) */
140 : :
141 : : CcBarChartLabelCallback continuous_axis_label_callback;
142 : : void *continuous_axis_label_user_data;
143 : : GDestroyNotify continuous_axis_label_destroy_notify;
144 : :
145 : : CcBarChartGridLineCallback continuous_axis_grid_line_callback;
146 : : void *continuous_axis_grid_line_user_data;
147 : : GDestroyNotify continuous_axis_grid_line_destroy_notify;
148 : :
149 : : gboolean selected_index_set;
150 : : unsigned int selected_index; /* undefined if !selected_index_set */
151 : :
152 : : double overlay_line_value;
153 : :
154 : : /* Layout and rendering cache. See cc-bar-chart-diagram.svg for a rough sketch
155 : : * of how these child widgets and lengths fit into the overall widget. */
156 : : GPtrArray *cached_discrete_axis_labels; /* (owned) (nullable) (element-type GtkLabel), always indexed the same as data */
157 : : GPtrArray *cached_continuous_axis_labels; /* (owned) (nullable) (element-type GtkLabel), always indexed the same as cached_continuous_axis_grid_line_values */
158 : : GArray *cached_continuous_axis_grid_line_values; /* (owned) (nullable) (element-type double), always indexed the same as cached_continuous_axis_labels */
159 : : GPtrArray *cached_groups; /* (owned) (nullable) (element-type CcBarChartGroup), always indexed the same as data */
160 : : int cached_continuous_axis_area_width;
161 : : int cached_continuous_axis_label_height;
162 : : int cached_discrete_axis_area_height;
163 : : int cached_discrete_axis_baseline; /* may be -1 if baseline is undefined */
164 : : double cached_pixels_per_data; /* > 0.0 */
165 : : int cached_minimum_group_width;
166 : : unsigned int cached_continuous_axis_label_collision_modulus;
167 : : };
168 : :
169 [ # # # # : 0 : G_DEFINE_TYPE (CcBarChart, cc_bar_chart, GTK_TYPE_WIDGET)
# # ]
170 : :
171 : : typedef enum {
172 : : PROP_SELECTED_INDEX = 1,
173 : : PROP_SELECTED_INDEX_SET,
174 : : PROP_OVERLAY_LINE_VALUE,
175 : : PROP_DISCRETE_AXIS_LABELS,
176 : : } CcBarChartProperty;
177 : :
178 : : static GParamSpec *props[PROP_DISCRETE_AXIS_LABELS + 1];
179 : :
180 : : typedef enum {
181 : : SIGNAL_DATA_CHANGED,
182 : : SIGNAL_BAR_ACTIVATED,
183 : : SIGNAL_ACTIVATE_CURSOR_BAR,
184 : : SIGNAL_MOVE_CURSOR,
185 : : } CcBarChartSignal;
186 : :
187 : : static guint signals[SIGNAL_MOVE_CURSOR + 1];
188 : :
189 : : static void cc_bar_chart_get_property (GObject *object,
190 : : guint property_id,
191 : : GValue *value,
192 : : GParamSpec *pspec);
193 : : static void cc_bar_chart_set_property (GObject *object,
194 : : guint property_id,
195 : : const GValue *value,
196 : : GParamSpec *pspec);
197 : : static void cc_bar_chart_dispose (GObject *object);
198 : : static void cc_bar_chart_finalize (GObject *object);
199 : : static void cc_bar_chart_size_allocate (GtkWidget *widget,
200 : : int width,
201 : : int height,
202 : : int baseline);
203 : : static void cc_bar_chart_measure (GtkWidget *widget,
204 : : GtkOrientation orientation,
205 : : int for_size,
206 : : int *minimum,
207 : : int *natural,
208 : : int *minimum_baseline,
209 : : int *natural_baseline);
210 : : static void cc_bar_chart_snapshot (GtkWidget *widget,
211 : : GtkSnapshot *snapshot);
212 : : static gboolean cc_bar_chart_focus (GtkWidget *widget,
213 : : GtkDirectionType direction);
214 : :
215 : : static void activate_cursor_bar_cb (CcBarChart *self,
216 : : gpointer user_data);
217 : : static void move_cursor_cb (CcBarChart *self,
218 : : GtkMovementStep step,
219 : : int count);
220 : :
221 : : static gboolean find_index_for_group (CcBarChart *self,
222 : : CcBarChartGroup *group,
223 : : unsigned int *out_idx);
224 : : static gboolean find_index_for_bar (CcBarChart *self,
225 : : CcBarChartBar *bar,
226 : : unsigned int *out_idx);
227 : : static CcBarChartGroup *get_adjacent_focusable_group (CcBarChart *self,
228 : : CcBarChartGroup *group,
229 : : int direction);
230 : : static CcBarChartGroup *get_first_focusable_group (CcBarChart *self);
231 : : static CcBarChartGroup *get_last_focusable_group (CcBarChart *self);
232 : : static void ensure_cached_grid_lines_and_labels (CcBarChart *self);
233 : : static inline void calculate_axis_area_widths (CcBarChart *self,
234 : : int *out_left_axis_area_width,
235 : : int *out_right_axis_area_width);
236 : : static inline void calculate_group_x_bounds (CcBarChart *self,
237 : : unsigned int idx,
238 : : int *out_spacing_start_x,
239 : : int *out_bar_start_x,
240 : : int *out_bar_finish_x,
241 : : int *out_spacing_finish_x);
242 : : static int value_to_widget_y (CcBarChart *self,
243 : : double value);
244 : : static GtkLabel *create_discrete_axis_label (const char *text);
245 : : static GtkLabel *create_continuous_axis_label (const char *text);
246 : : static char *format_continuous_axis_label (CcBarChart *self,
247 : : double value);
248 : : static double get_maximum_data_value (CcBarChart *self,
249 : : gboolean include_overlay_line);
250 : :
251 : : static void
252 : 0 : cc_bar_chart_class_init (CcBarChartClass *klass)
253 : : {
254 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
255 : 0 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
256 : :
257 : 0 : object_class->get_property = cc_bar_chart_get_property;
258 : 0 : object_class->set_property = cc_bar_chart_set_property;
259 : 0 : object_class->dispose = cc_bar_chart_dispose;
260 : 0 : object_class->finalize = cc_bar_chart_finalize;
261 : :
262 : 0 : widget_class->size_allocate = cc_bar_chart_size_allocate;
263 : 0 : widget_class->measure = cc_bar_chart_measure;
264 : 0 : widget_class->snapshot = cc_bar_chart_snapshot;
265 : 0 : widget_class->focus = cc_bar_chart_focus;
266 : :
267 : : /**
268 : : * CcBarChart:selected-index:
269 : : *
270 : : * Index of the currently selected data.
271 : : *
272 : : * If nothing is currently selected, the value of this property is undefined.
273 : : * See #CcBarChart:selected-index-set to check this.
274 : : */
275 : 0 : props[PROP_SELECTED_INDEX] =
276 : 0 : g_param_spec_uint ("selected-index",
277 : : NULL, NULL,
278 : : 0, G_MAXUINT, 0,
279 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
280 : :
281 : : /**
282 : : * CcBarChart:selected-index-set:
283 : : *
284 : : * Whether a data item is currently selected.
285 : : *
286 : : * If this property is `TRUE`, the value of #CcBarChart:selected-index is
287 : : * defined.
288 : : */
289 : 0 : props[PROP_SELECTED_INDEX_SET] =
290 : 0 : g_param_spec_boolean ("selected-index-set",
291 : : NULL, NULL,
292 : : FALSE,
293 : : G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
294 : :
295 : : /**
296 : : * CcBarChart:overlay-line-value:
297 : : *
298 : : * Value (in the same domain as the chart data) to render an overlay line at,
299 : : * or `NAN` to not render one.
300 : : *
301 : : * An overlay line could represent an average value or target value for the
302 : : * bars, for example.
303 : : */
304 : 0 : props[PROP_OVERLAY_LINE_VALUE] =
305 : 0 : g_param_spec_double ("overlay-line-value",
306 : : NULL, NULL,
307 : : -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
308 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
309 : :
310 : : /**
311 : : * CcBarChart:discrete-axis-labels: (nullable)
312 : : *
313 : : * Labels for the discrete axis of the chart.
314 : : *
315 : : * The number of labels must match the number of bars set in the chart data,
316 : : * one label per bar.
317 : : *
318 : : * This will be `NULL` if no labels have been set yet.
319 : : */
320 : 0 : props[PROP_DISCRETE_AXIS_LABELS] =
321 : 0 : g_param_spec_boxed ("discrete-axis-labels",
322 : : NULL, NULL,
323 : : G_TYPE_STRV,
324 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
325 : :
326 : 0 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
327 : :
328 : : /**
329 : : * CcBarChart::data-changed:
330 : : *
331 : : * Emitted when the data in the widget is updated.
332 : : */
333 : 0 : signals[SIGNAL_DATA_CHANGED] =
334 : 0 : g_signal_new ("data-changed",
335 : : G_TYPE_FROM_CLASS (klass),
336 : : G_SIGNAL_RUN_FIRST,
337 : : 0, NULL, NULL,
338 : : NULL,
339 : : G_TYPE_NONE, 0);
340 : :
341 : : /**
342 : : * CcBarChart::bar-activated:
343 : : * @idx: index of the activated bar’s data entry
344 : : *
345 : : * Emitted when one of the bars in the chart is activated.
346 : : */
347 : 0 : signals[SIGNAL_BAR_ACTIVATED] =
348 : 0 : g_signal_new ("bar-activated",
349 : : G_TYPE_FROM_CLASS (klass),
350 : : G_SIGNAL_RUN_FIRST,
351 : : 0, NULL, NULL,
352 : : NULL,
353 : : G_TYPE_NONE, 1, G_TYPE_UINT);
354 : :
355 : : /**
356 : : * CcBarChart::activate-cursor-bar:
357 : : *
358 : : * Emitted when the bar under the cursor (the focused bar) is activated.
359 : : */
360 : 0 : signals[SIGNAL_ACTIVATE_CURSOR_BAR] =
361 : 0 : g_signal_new ("activate-cursor-bar",
362 : : G_TYPE_FROM_CLASS (klass),
363 : : G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
364 : : 0, NULL, NULL,
365 : : NULL,
366 : : G_TYPE_NONE, 0);
367 : :
368 : : /**
369 : : * CcBarChart::move-cursor:
370 : : * @chart: the chart on which the signal is emitted
371 : : * @step: the granularity of the move, as a #GtkMovementStep
372 : : * @count: the number of @step units to move
373 : : *
374 : : * Emitted when the user initiates a cursor movement.
375 : : *
376 : : * The default bindings for this signal are:
377 : : *
378 : : * - ←, →, ↑, ↓: move by individual bars
379 : : * - Home, End: move to the ends of the chart
380 : : */
381 : 0 : signals[SIGNAL_MOVE_CURSOR] =
382 : 0 : g_signal_new ("move-cursor",
383 : : G_TYPE_FROM_CLASS (klass),
384 : : G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
385 : : 0, NULL, NULL,
386 : : NULL,
387 : : G_TYPE_NONE, 2,
388 : : GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT);
389 : :
390 : 0 : gtk_widget_class_set_activate_signal (widget_class, signals[SIGNAL_ACTIVATE_CURSOR_BAR]);
391 : :
392 : 0 : gtk_widget_class_add_binding_signal (widget_class,
393 : : GDK_KEY_Home, 0,
394 : : "move-cursor",
395 : : "(ii)", GTK_MOVEMENT_BUFFER_ENDS, -1);
396 : 0 : gtk_widget_class_add_binding_signal (widget_class,
397 : : GDK_KEY_KP_Home, 0,
398 : : "move-cursor",
399 : : "(ii)", GTK_MOVEMENT_BUFFER_ENDS, -1);
400 : 0 : gtk_widget_class_add_binding_signal (widget_class,
401 : : GDK_KEY_End, 0,
402 : : "move-cursor",
403 : : "(ii)", GTK_MOVEMENT_BUFFER_ENDS, 1);
404 : 0 : gtk_widget_class_add_binding_signal (widget_class,
405 : : GDK_KEY_KP_End, 0,
406 : : "move-cursor",
407 : : "(ii)", GTK_MOVEMENT_BUFFER_ENDS, 1);
408 : 0 : gtk_widget_class_add_binding_signal (widget_class,
409 : : GDK_KEY_Up, 0,
410 : : "move-cursor",
411 : : "(ii)", GTK_MOVEMENT_LOGICAL_POSITIONS, -1);
412 : 0 : gtk_widget_class_add_binding_signal (widget_class,
413 : : GDK_KEY_KP_Up, 0,
414 : : "move-cursor",
415 : : "(ii)", GTK_MOVEMENT_LOGICAL_POSITIONS, -1);
416 : 0 : gtk_widget_class_add_binding_signal (widget_class,
417 : : GDK_KEY_Down, 0,
418 : : "move-cursor",
419 : : "(ii)", GTK_MOVEMENT_LOGICAL_POSITIONS, 1);
420 : 0 : gtk_widget_class_add_binding_signal (widget_class,
421 : : GDK_KEY_KP_Down, 0,
422 : : "move-cursor",
423 : : "(ii)", GTK_MOVEMENT_LOGICAL_POSITIONS, 1);
424 : 0 : gtk_widget_class_add_binding_signal (widget_class,
425 : : GDK_KEY_Left, 0,
426 : : "move-cursor",
427 : : "(ii)", GTK_MOVEMENT_VISUAL_POSITIONS, -1);
428 : 0 : gtk_widget_class_add_binding_signal (widget_class,
429 : : GDK_KEY_KP_Left, 0,
430 : : "move-cursor",
431 : : "(ii)", GTK_MOVEMENT_VISUAL_POSITIONS, -1);
432 : 0 : gtk_widget_class_add_binding_signal (widget_class,
433 : : GDK_KEY_Right, 0,
434 : : "move-cursor",
435 : : "(ii)", GTK_MOVEMENT_VISUAL_POSITIONS, 1);
436 : 0 : gtk_widget_class_add_binding_signal (widget_class,
437 : : GDK_KEY_KP_Right, 0,
438 : : "move-cursor",
439 : : "(ii)", GTK_MOVEMENT_VISUAL_POSITIONS, 1);
440 : :
441 : 0 : gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/cc-bar-chart.ui");
442 : :
443 : 0 : gtk_widget_class_bind_template_callback (widget_class, activate_cursor_bar_cb);
444 : 0 : gtk_widget_class_bind_template_callback (widget_class, move_cursor_cb);
445 : :
446 : 0 : gtk_widget_class_set_css_name (widget_class, "bar-chart");
447 : 0 : gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_LIST);
448 : 0 : }
449 : :
450 : : static void
451 : 0 : cc_bar_chart_init (CcBarChart *self)
452 : : {
453 : 0 : gtk_widget_init_template (GTK_WIDGET (self));
454 : :
455 : : /* Some default values */
456 : 0 : self->cached_pixels_per_data = 1.0;
457 : 0 : self->overlay_line_value = NAN;
458 : 0 : }
459 : :
460 : : static void
461 : 0 : cc_bar_chart_get_property (GObject *object,
462 : : guint property_id,
463 : : GValue *value,
464 : : GParamSpec *pspec)
465 : : {
466 : 0 : CcBarChart *self = CC_BAR_CHART (object);
467 : :
468 [ # # # # : 0 : switch ((CcBarChartProperty) property_id)
# ]
469 : : {
470 : 0 : case PROP_SELECTED_INDEX: {
471 : : size_t idx;
472 : 0 : gboolean valid = cc_bar_chart_get_selected_index (self, &idx);
473 [ # # ]: 0 : g_value_set_uint (value, valid ? idx : 0);
474 : 0 : break;
475 : : }
476 : 0 : case PROP_SELECTED_INDEX_SET:
477 : 0 : g_value_set_boolean (value, cc_bar_chart_get_selected_index (self, NULL));
478 : 0 : break;
479 : 0 : case PROP_OVERLAY_LINE_VALUE:
480 : 0 : g_value_set_double (value, cc_bar_chart_get_overlay_line_value (self));
481 : 0 : break;
482 : 0 : case PROP_DISCRETE_AXIS_LABELS:
483 : 0 : g_value_set_boxed (value, cc_bar_chart_get_discrete_axis_labels (self, NULL));
484 : 0 : break;
485 : 0 : default:
486 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
487 : 0 : break;
488 : : }
489 : 0 : }
490 : :
491 : : static void
492 : 0 : cc_bar_chart_set_property (GObject *object,
493 : : guint property_id,
494 : : const GValue *value,
495 : : GParamSpec *pspec)
496 : : {
497 : 0 : CcBarChart *self = CC_BAR_CHART (object);
498 : :
499 [ # # # # : 0 : switch ((CcBarChartProperty) property_id)
# ]
500 : : {
501 : 0 : case PROP_SELECTED_INDEX:
502 : 0 : cc_bar_chart_set_selected_index (self, TRUE, g_value_get_uint (value));
503 : 0 : break;
504 : 0 : case PROP_SELECTED_INDEX_SET:
505 : : /* Read only */
506 : : g_assert_not_reached ();
507 : : break;
508 : 0 : case PROP_OVERLAY_LINE_VALUE:
509 : 0 : cc_bar_chart_set_overlay_line_value (self, g_value_get_double (value));
510 : 0 : break;
511 : 0 : case PROP_DISCRETE_AXIS_LABELS:
512 : 0 : cc_bar_chart_set_discrete_axis_labels (self, g_value_get_boxed (value));
513 : 0 : break;
514 : 0 : default:
515 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
516 : : }
517 : 0 : }
518 : :
519 : : static void
520 : 0 : cc_bar_chart_dispose (GObject *object)
521 : : {
522 : 0 : CcBarChart *self = CC_BAR_CHART (object);
523 : :
524 : 0 : gtk_widget_dispose_template (GTK_WIDGET (object), CC_TYPE_BAR_CHART);
525 : :
526 [ # # ]: 0 : g_clear_pointer (&self->cached_discrete_axis_labels, g_ptr_array_unref);
527 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_labels, g_ptr_array_unref);
528 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_grid_line_values, g_array_unref);
529 [ # # ]: 0 : g_clear_pointer (&self->cached_groups, g_ptr_array_unref);
530 : :
531 : 0 : G_OBJECT_CLASS (cc_bar_chart_parent_class)->dispose (object);
532 : 0 : }
533 : :
534 : : static void
535 : 0 : cc_bar_chart_finalize (GObject *object)
536 : : {
537 : 0 : CcBarChart *self = CC_BAR_CHART (object);
538 : :
539 : 0 : g_strfreev (self->discrete_axis_labels);
540 : :
541 [ # # ]: 0 : if (self->continuous_axis_label_destroy_notify != NULL)
542 : 0 : self->continuous_axis_label_destroy_notify (self->continuous_axis_label_user_data);
543 : :
544 [ # # ]: 0 : if (self->continuous_axis_grid_line_destroy_notify != NULL)
545 : 0 : self->continuous_axis_grid_line_destroy_notify (self->continuous_axis_grid_line_user_data);
546 : :
547 : 0 : g_free (self->data);
548 : :
549 : 0 : G_OBJECT_CLASS (cc_bar_chart_parent_class)->finalize (object);
550 : 0 : }
551 : :
552 : : /* Various constants defining the widget appearance. These can’t be moved into
553 : : * CSS yet as GTK doesn’t expose enough of the CSS parsing machinery. */
554 : : static const unsigned int GRID_LINE_WIDTH = 1;
555 : : static const GdkRGBA GRID_LINE_COLOR = { .red = 0, .green = 0, .blue = 0, .alpha = 0.15 };
556 : : static const GdkRGBA GRID_LINE_COLOR_DARK = { .red = 1, .green = 1, .blue = 1, .alpha = 0.15 };
557 : : static const GdkRGBA GRID_LINE_COLOR_HC = { .red = 0, .green = 0, .blue = 0, .alpha = 0.5 };
558 : : static const GdkRGBA GRID_LINE_COLOR_HC_DARK = { .red = 1, .green = 1, .blue = 1, .alpha = 0.5 };
559 : : static const unsigned int MINIMUM_CHART_HEIGHT = 120;
560 : : static const unsigned int NATURAL_CHART_HEIGHT = 300;
561 : : static const unsigned int OVERLAY_LINE_WIDTH = 2;
562 : : static const float OVERLAY_LINE_DASH[] = { 8, 6 };
563 : : static const GdkRGBA OVERLAY_LINE_COLOR = { .red = 0, .green = 0, .blue = 0, .alpha = 0.5 };
564 : : static const GdkRGBA OVERLAY_LINE_COLOR_DARK = { .red = 1, .green = 1, .blue = 1, .alpha = 0.5 };
565 : : static const GdkRGBA OVERLAY_LINE_COLOR_HC = { .red = 0, .green = 0, .blue = 0, .alpha = 0.8 };
566 : : static const GdkRGBA OVERLAY_LINE_COLOR_HC_DARK = { .red = 1, .green = 1, .blue = 1, .alpha = 0.8 };
567 : : static const double GROUP_TO_SPACE_WIDTH_FILL_RATIO = 0.8; /* proportion of additional width which gets allocated to bar groups, rather than the space between them */
568 : :
569 : : static void
570 : 0 : cc_bar_chart_size_allocate (GtkWidget *widget,
571 : : int width,
572 : : int height,
573 : : int baseline)
574 : : {
575 : 0 : CcBarChart *self = CC_BAR_CHART (widget);
576 : : double latest_grid_line_value;
577 : 0 : gboolean collision_detected = FALSE;
578 : :
579 : : /* Empty state. */
580 [ # # ]: 0 : if (self->n_data == 0)
581 : 0 : return;
582 : :
583 : 0 : const double max_value = get_maximum_data_value (self, TRUE);
584 : :
585 : : /* Position the labels for the discrete axis in the correct places. */
586 [ # # # # : 0 : for (unsigned int i = 0; self->n_data > 0 && self->cached_discrete_axis_labels != NULL && i < self->cached_discrete_axis_labels->len; i++)
# # ]
587 : : {
588 : : GtkAllocation child_alloc;
589 : : int spacing_start_x, spacing_finish_x;
590 : :
591 : : /* The label is allocated the full possible space, and its xalign and
592 : : * yalign are used to position the text correctly within that. */
593 : 0 : calculate_group_x_bounds (self, i,
594 : : &spacing_start_x,
595 : : NULL, NULL,
596 : : &spacing_finish_x);
597 : :
598 : 0 : child_alloc.x = spacing_start_x;
599 : 0 : child_alloc.y = height - self->cached_discrete_axis_area_height;
600 : 0 : child_alloc.width = spacing_finish_x - spacing_start_x;
601 : 0 : child_alloc.height = self->cached_discrete_axis_area_height;
602 : :
603 : 0 : gtk_widget_size_allocate (self->cached_discrete_axis_labels->pdata[i], &child_alloc, self->cached_discrete_axis_baseline);
604 : : }
605 : :
606 : : /* Calculate our continuous axis grid lines and labels */
607 : 0 : ensure_cached_grid_lines_and_labels (self);
608 : :
609 [ # # ]: 0 : if (self->cached_continuous_axis_grid_line_values != NULL)
610 : 0 : latest_grid_line_value = g_array_index (self->cached_continuous_axis_grid_line_values, double, self->cached_continuous_axis_grid_line_values->len - 1);
611 : : else
612 : 0 : latest_grid_line_value = max_value;
613 : :
614 : : /* Calculate the scale of data on the chart, given the available space to the
615 : : * topmost continuous axis grid line (factoring in half the height of the
616 : : * label extending beyond that). Space beyond our natural request is allocated
617 : : * to the chart area rather than the axis areas. */
618 : 0 : g_assert (height > self->cached_discrete_axis_area_height);
619 : 0 : self->cached_pixels_per_data = (height - self->cached_discrete_axis_area_height - self->cached_continuous_axis_label_height / 2) / (latest_grid_line_value + 1.0);
620 : 0 : g_assert (self->cached_pixels_per_data > 0.0);
621 : :
622 : : /* Position the continuous axis labels in the correct places. In a subsequent
623 : : * step we work out collisions and hide labels based on index modulus to avoid
624 : : * drawing text on top of other text. See below. */
625 [ # # ]: 0 : if (self->cached_continuous_axis_labels != NULL &&
626 [ # # ]: 0 : self->cached_continuous_axis_grid_line_values != NULL)
627 : : {
628 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_continuous_axis_labels->len; i++)
629 : : {
630 : 0 : const double grid_line_value = g_array_index (self->cached_continuous_axis_grid_line_values, double, i);
631 : : GtkAllocation child_alloc;
632 : : int label_natural_height, label_natural_baseline;
633 : :
634 : 0 : gtk_widget_measure (GTK_WIDGET (self->cached_continuous_axis_labels->pdata[i]),
635 : : GTK_ORIENTATION_VERTICAL, self->cached_continuous_axis_area_width,
636 : : NULL, &label_natural_height,
637 : : NULL, &label_natural_baseline);
638 : :
639 [ # # ]: 0 : if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
640 : 0 : child_alloc.x = 0;
641 : : else
642 : 0 : child_alloc.x = width - self->cached_continuous_axis_area_width;
643 : : /* centre the label vertically on the grid line position, and let the valign code
644 : : * in GtkLabel align the text baseline correctly with that */
645 : 0 : child_alloc.y = value_to_widget_y (self, grid_line_value) - label_natural_height / 2;
646 : 0 : child_alloc.width = self->cached_continuous_axis_area_width;
647 : 0 : child_alloc.height = label_natural_height;
648 : :
649 : 0 : gtk_widget_size_allocate (self->cached_continuous_axis_labels->pdata[i], &child_alloc, label_natural_baseline);
650 : : }
651 : :
652 : : /* Check for collisions. Compare pairs of continuous axis labels which are
653 : : * self->cached_continuous_axis_label_collision_modulus positions apart. If
654 : : * any of those pairs collide, increment the collision modulus and restart the
655 : : * check. */
656 : 0 : self->cached_continuous_axis_label_collision_modulus = 1;
657 : :
658 : : do
659 : : {
660 : 0 : collision_detected = FALSE;
661 : :
662 : 0 : for (unsigned int i = self->cached_continuous_axis_label_collision_modulus;
663 [ # # ]: 0 : i < self->cached_continuous_axis_labels->len;
664 : 0 : i++)
665 : : {
666 : : graphene_rect_t child_allocs[2];
667 : : gboolean success;
668 : :
669 : 0 : success = gtk_widget_compute_bounds (self->cached_continuous_axis_labels->pdata[i - self->cached_continuous_axis_label_collision_modulus],
670 : : widget,
671 : : &child_allocs[0]);
672 : 0 : g_assert (success);
673 : 0 : success = gtk_widget_compute_bounds (self->cached_continuous_axis_labels->pdata[i],
674 : : widget,
675 : : &child_allocs[1]);
676 : 0 : g_assert (success);
677 : :
678 [ # # ]: 0 : if (graphene_rect_intersection (&child_allocs[0], &child_allocs[1], NULL))
679 : : {
680 : 0 : collision_detected = TRUE;
681 : 0 : g_assert (self->cached_continuous_axis_label_collision_modulus < G_MAXUINT);
682 : 0 : self->cached_continuous_axis_label_collision_modulus++;
683 : 0 : break;
684 : : }
685 : : }
686 : : }
687 [ # # ]: 0 : while (collision_detected);
688 : :
689 : : /* Hide continuous axis labels according to the collision modulus. */
690 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_continuous_axis_labels->len; i++)
691 : : {
692 : 0 : gtk_widget_set_child_visible (self->cached_continuous_axis_labels->pdata[i],
693 : 0 : (i % self->cached_continuous_axis_label_collision_modulus) == 0);
694 : : }
695 : : }
696 : :
697 : : /* Chart bar groups */
698 [ # # # # ]: 0 : for (unsigned int i = 0; self->cached_groups != NULL && i < self->cached_groups->len; i++)
699 : : {
700 : 0 : CcBarChartGroup *group = CC_BAR_CHART_GROUP (self->cached_groups->pdata[i]);
701 : : int group_bottom_y, group_left_x, group_right_x;
702 : : GtkAllocation child_alloc;
703 : :
704 : 0 : calculate_group_x_bounds (self, i,
705 : : NULL,
706 : : &group_left_x,
707 : : &group_right_x,
708 : : NULL);
709 : :
710 : : /* Position the bottom of the bar just above the axis grid line, to avoid
711 : : * them overlapping. */
712 : 0 : group_bottom_y = value_to_widget_y (self, 0.0) - (GRID_LINE_WIDTH + (2 - 1)) / 2;
713 : :
714 : 0 : child_alloc.x = group_left_x;
715 : 0 : child_alloc.y = 0;
716 : 0 : child_alloc.width = group_right_x - group_left_x;
717 : 0 : child_alloc.height = group_bottom_y;
718 : :
719 : 0 : cc_bar_chart_group_set_scale (group, self->cached_pixels_per_data);
720 : 0 : gtk_widget_size_allocate (GTK_WIDGET (group), &child_alloc, -1);
721 : : }
722 : : }
723 : :
724 : : static void
725 : 0 : cc_bar_chart_measure (GtkWidget *widget,
726 : : GtkOrientation orientation,
727 : : int for_size,
728 : : int *minimum,
729 : : int *natural,
730 : : int *minimum_baseline,
731 : : int *natural_baseline)
732 : : {
733 : 0 : CcBarChart *self = CC_BAR_CHART (widget);
734 : :
735 : : /* Empty state. */
736 [ # # ]: 0 : if (self->n_data == 0)
737 : : {
738 : 0 : *minimum = 0;
739 : 0 : *natural = 0;
740 : 0 : *minimum_baseline = -1;
741 : 0 : *natural_baseline = -1;
742 : 0 : return;
743 : : }
744 : :
745 : : /* Calculate our continuous axis labels for measuring. Even though some of
746 : : * them won’t be visible (according to
747 : : * self->cached_continuous_axis_label_collision_modulus), measure them all
748 : : * so that the width of the chart doesn’t jump about as the modulus changes. */
749 [ # # ]: 0 : if (self->continuous_axis_label_callback != NULL)
750 : 0 : ensure_cached_grid_lines_and_labels (self);
751 : :
752 [ # # ]: 0 : if (orientation == GTK_ORIENTATION_HORIZONTAL)
753 : : {
754 : 0 : int maximum_continuous_label_natural_width = 0;
755 : 0 : int maximum_discrete_label_minimum_width = 0, maximum_discrete_label_natural_width = 0;
756 : 0 : int maximum_bar_minimum_width = 0, maximum_bar_natural_width = 0;
757 : :
758 : : /* Measure the first and last continuous axis labels and work out their
759 : : * minimum widths. */
760 [ # # # # ]: 0 : for (unsigned int i = 0; self->cached_continuous_axis_labels != NULL && i < self->cached_continuous_axis_labels->len; i++)
761 : : {
762 : 0 : int label_natural_width = -1;
763 : :
764 : 0 : gtk_widget_measure (GTK_WIDGET (self->cached_continuous_axis_labels->pdata[i]),
765 : : GTK_ORIENTATION_HORIZONTAL, -1,
766 : : NULL, &label_natural_width,
767 : : NULL, NULL);
768 : :
769 : 0 : maximum_continuous_label_natural_width = MAX (maximum_continuous_label_natural_width, label_natural_width);
770 : : }
771 : :
772 : : /* Measure the bar groups to get their minimum and natural widths too. */
773 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_groups->len; i++)
774 : : {
775 : 0 : int bar_natural_width = -1, bar_minimum_width = -1;
776 : :
777 : 0 : gtk_widget_measure (GTK_WIDGET (self->cached_groups->pdata[i]),
778 : : GTK_ORIENTATION_HORIZONTAL, -1,
779 : : &bar_minimum_width, &bar_natural_width,
780 : : NULL, NULL);
781 : :
782 : 0 : maximum_bar_natural_width = MAX (maximum_bar_natural_width, bar_natural_width);
783 : 0 : maximum_bar_minimum_width = MAX (maximum_bar_minimum_width, bar_minimum_width);
784 : : }
785 : :
786 : : /* Also measure the discrete axis labels to see if any of them are wider
787 : : * than the groups. */
788 [ # # # # ]: 0 : for (unsigned int i = 0; self->cached_discrete_axis_labels != NULL && i < self->cached_discrete_axis_labels->len; i++)
789 : : {
790 : 0 : int label_natural_width = -1, label_minimum_width = -1;
791 : :
792 : 0 : gtk_widget_measure (GTK_WIDGET (self->cached_discrete_axis_labels->pdata[i]),
793 : : GTK_ORIENTATION_HORIZONTAL, -1,
794 : : &label_minimum_width, &label_natural_width,
795 : : NULL, NULL);
796 : :
797 : 0 : maximum_discrete_label_natural_width = MAX (maximum_discrete_label_natural_width, label_natural_width);
798 : 0 : maximum_discrete_label_minimum_width = MAX (maximum_discrete_label_minimum_width, label_minimum_width);
799 : : }
800 : :
801 : 0 : self->cached_minimum_group_width = MAX (maximum_bar_minimum_width, maximum_discrete_label_minimum_width);
802 : :
803 : : /* Don’t complicate things by allowing the continuous axis labels to get
804 : : * less than their natural width as an allocation. */
805 : 0 : self->cached_continuous_axis_area_width = maximum_continuous_label_natural_width;
806 : :
807 : 0 : *minimum = MAX (maximum_bar_minimum_width, maximum_discrete_label_minimum_width) * self->n_data + maximum_continuous_label_natural_width;
808 : 0 : *natural = MAX (maximum_bar_natural_width, maximum_discrete_label_natural_width) * self->n_data + maximum_continuous_label_natural_width;
809 : 0 : *minimum_baseline = -1;
810 : 0 : *natural_baseline = -1;
811 : : }
812 [ # # ]: 0 : else if (orientation == GTK_ORIENTATION_VERTICAL)
813 : : {
814 : 0 : int maximum_discrete_label_natural_height = 0, maximum_discrete_label_natural_baseline = -1;
815 : 0 : int continuous_label_minimum_height = 0, continuous_label_natural_height = 0;
816 : :
817 : : /* Measure all the discrete labels. */
818 [ # # # # ]: 0 : for (unsigned int i = 0; self->cached_discrete_axis_labels != NULL && i < self->cached_discrete_axis_labels->len; i++)
819 : : {
820 : 0 : int label_natural_height = -1, label_natural_baseline = -1;
821 : :
822 : 0 : gtk_widget_measure (GTK_WIDGET (self->cached_discrete_axis_labels->pdata[i]),
823 : : GTK_ORIENTATION_VERTICAL, -1,
824 : : NULL, &label_natural_height,
825 : : NULL, &label_natural_baseline);
826 : :
827 : 0 : maximum_discrete_label_natural_height = MAX (maximum_discrete_label_natural_height, label_natural_height);
828 : 0 : maximum_discrete_label_natural_baseline = MAX (maximum_discrete_label_natural_baseline, label_natural_baseline);
829 : : }
830 : :
831 : : /* Measure the last continuous axis label and work out its minimum height,
832 : : * because it will be vertically centred on the top-most grid line, which
833 : : * might be near enough the top of the chart to push the top of the text
834 : : * off the default allocation.
835 : : *
836 : : * There’s no need to do the same for the first continuous axis label,
837 : : * because it will never drop below the discrete axis labels unless the
838 : : * font sizes are ludicrously different. */
839 [ # # ]: 0 : if (self->cached_continuous_axis_labels != NULL)
840 : : {
841 : 0 : gtk_widget_measure (GTK_WIDGET (self->cached_continuous_axis_labels->pdata[self->cached_continuous_axis_labels->len - 1]),
842 : : GTK_ORIENTATION_VERTICAL, -1,
843 : : &continuous_label_minimum_height, &continuous_label_natural_height,
844 : : NULL, NULL);
845 : : }
846 : :
847 : 0 : self->cached_continuous_axis_label_height = continuous_label_natural_height;
848 : :
849 : : /* Don’t complicate things by allowing the discrete axis labels to get
850 : : * less than their natural height as an allocation. */
851 : 0 : self->cached_discrete_axis_area_height = maximum_discrete_label_natural_height;
852 : 0 : self->cached_discrete_axis_baseline = maximum_discrete_label_natural_baseline;
853 : :
854 : 0 : *minimum = MINIMUM_CHART_HEIGHT + maximum_discrete_label_natural_height + continuous_label_minimum_height / 2;
855 : 0 : *natural = NATURAL_CHART_HEIGHT + maximum_discrete_label_natural_height + continuous_label_natural_height / 2;
856 : 0 : *minimum_baseline = -1;
857 : 0 : *natural_baseline = -1;
858 : : }
859 : : else
860 : : {
861 : : g_assert_not_reached ();
862 : : }
863 : : }
864 : :
865 : : static void
866 : 0 : cc_bar_chart_snapshot (GtkWidget *widget,
867 : : GtkSnapshot *snapshot)
868 : : {
869 : 0 : CcBarChart *self = CC_BAR_CHART (widget);
870 : 0 : const int width = gtk_widget_get_width (widget);
871 : : int left_axis_area_width, right_axis_area_width;
872 : : AdwStyleManager *style_manager;
873 : :
874 : : /* Empty state. */
875 [ # # ]: 0 : if (self->n_data == 0)
876 : 0 : return;
877 : :
878 : 0 : style_manager = adw_style_manager_get_for_display (gtk_widget_get_display (widget));
879 : :
880 : 0 : calculate_axis_area_widths (self, &left_axis_area_width, &right_axis_area_width);
881 : :
882 : : /* Continuous axis grid lines, should have been cached in size_allocate, but
883 : : * may not have been set yet. */
884 [ # # ]: 0 : if (self->cached_continuous_axis_grid_line_values != NULL)
885 : : {
886 : 0 : g_autoptr(GskPathBuilder) grid_line_builder = gsk_path_builder_new ();
887 : 0 : g_autoptr(GskPath) grid_line_path = NULL;
888 : 0 : GskStroke *grid_line_stroke = NULL;
889 : : const GdkRGBA *grid_line_color;
890 : :
891 : 0 : grid_line_stroke = gsk_stroke_new (GRID_LINE_WIDTH);
892 : :
893 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_continuous_axis_grid_line_values->len; i++)
894 : : {
895 : 0 : const double value = g_array_index (self->cached_continuous_axis_grid_line_values, double, i);
896 : 0 : int y = value_to_widget_y (self, value);
897 : :
898 : 0 : gsk_path_builder_move_to (grid_line_builder, left_axis_area_width, y);
899 : 0 : gsk_path_builder_line_to (grid_line_builder,
900 : 0 : width - right_axis_area_width,
901 : : y);
902 : : }
903 : :
904 : 0 : grid_line_path = gsk_path_builder_free_to_path (g_steal_pointer (&grid_line_builder));
905 : :
906 [ # # # # ]: 0 : if (adw_style_manager_get_dark (style_manager) && adw_style_manager_get_high_contrast (style_manager))
907 : 0 : grid_line_color = &GRID_LINE_COLOR_HC_DARK;
908 [ # # ]: 0 : else if (adw_style_manager_get_dark (style_manager))
909 : 0 : grid_line_color = &GRID_LINE_COLOR_DARK;
910 [ # # ]: 0 : else if (adw_style_manager_get_high_contrast (style_manager))
911 : 0 : grid_line_color = &GRID_LINE_COLOR_HC;
912 : : else
913 : 0 : grid_line_color = &GRID_LINE_COLOR;
914 : :
915 : 0 : gtk_snapshot_append_stroke (snapshot, grid_line_path, grid_line_stroke, grid_line_color);
916 : :
917 : 0 : gsk_stroke_free (g_steal_pointer (&grid_line_stroke));
918 : : }
919 : :
920 : : /* Continuous axis labels, should have been cached in size_allocate, but may
921 : : * not have been set yet. */
922 [ # # # # ]: 0 : for (unsigned int i = 0; self->cached_continuous_axis_labels != NULL && i < self->cached_continuous_axis_labels->len; i++)
923 : 0 : gtk_widget_snapshot_child (widget, self->cached_continuous_axis_labels->pdata[i], snapshot);
924 : :
925 : : /* Discrete axis labels, should have been cached in size_allocate, but may not
926 : : * have been set yet */
927 [ # # # # ]: 0 : for (unsigned int i = 0; self->cached_discrete_axis_labels != NULL && i < self->cached_discrete_axis_labels->len; i++)
928 : 0 : gtk_widget_snapshot_child (widget, self->cached_discrete_axis_labels->pdata[i], snapshot);
929 : :
930 : : /* Bar groups, should have been cached in size_allocate, but may not have been set yet */
931 [ # # # # ]: 0 : for (unsigned int i = 0; self->cached_groups != NULL && i < self->cached_groups->len; i++)
932 : 0 : gtk_widget_snapshot_child (widget, self->cached_groups->pdata[i], snapshot);
933 : :
934 : : /* Overlay line */
935 [ # # ]: 0 : if (!isnan (self->overlay_line_value))
936 : : {
937 : 0 : g_autoptr(GskPathBuilder) overlay_builder = gsk_path_builder_new ();
938 : 0 : g_autoptr(GskPath) overlay_path = NULL;
939 : 0 : GskStroke *overlay_stroke = NULL;
940 : : int overlay_y;
941 : : const GdkRGBA *overlay_line_color;
942 : :
943 : 0 : overlay_stroke = gsk_stroke_new (OVERLAY_LINE_WIDTH);
944 : 0 : gsk_stroke_set_line_cap (overlay_stroke, GSK_LINE_CAP_SQUARE);
945 : 0 : gsk_stroke_set_dash (overlay_stroke, OVERLAY_LINE_DASH, G_N_ELEMENTS (OVERLAY_LINE_DASH));
946 : :
947 : 0 : overlay_y = value_to_widget_y (self, self->overlay_line_value);
948 : 0 : gsk_path_builder_move_to (overlay_builder, left_axis_area_width, overlay_y);
949 : 0 : gsk_path_builder_line_to (overlay_builder,
950 : 0 : width - right_axis_area_width,
951 : : overlay_y);
952 : :
953 : 0 : overlay_path = gsk_path_builder_free_to_path (g_steal_pointer (&overlay_builder));
954 : :
955 [ # # # # ]: 0 : if (adw_style_manager_get_dark (style_manager) && adw_style_manager_get_high_contrast (style_manager))
956 : 0 : overlay_line_color = &OVERLAY_LINE_COLOR_HC_DARK;
957 [ # # ]: 0 : else if (adw_style_manager_get_dark (style_manager))
958 : 0 : overlay_line_color = &OVERLAY_LINE_COLOR_DARK;
959 [ # # ]: 0 : else if (adw_style_manager_get_high_contrast (style_manager))
960 : 0 : overlay_line_color = &OVERLAY_LINE_COLOR_HC;
961 : : else
962 : 0 : overlay_line_color = &OVERLAY_LINE_COLOR;
963 : :
964 : 0 : gtk_snapshot_append_stroke (snapshot, overlay_path, overlay_stroke, overlay_line_color);
965 : :
966 : 0 : gsk_stroke_free (g_steal_pointer (&overlay_stroke));
967 : : }
968 : : }
969 : :
970 : : static gboolean
971 : 0 : cc_bar_chart_focus (GtkWidget *widget,
972 : : GtkDirectionType direction)
973 : : {
974 : 0 : CcBarChart *self = CC_BAR_CHART (widget);
975 : : GtkWidget *focus_child;
976 : 0 : CcBarChartGroup *next_focus_group = NULL;
977 : :
978 : : /* Reverse the direction if in RTL mode, as the chart presents things on a
979 : : * left–right axis. */
980 [ # # ]: 0 : if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
981 : : {
982 [ # # # # : 0 : switch (direction)
# ]
983 : : {
984 : 0 : case GTK_DIR_TAB_BACKWARD:
985 : 0 : direction = GTK_DIR_TAB_FORWARD;
986 : 0 : break;
987 : 0 : case GTK_DIR_TAB_FORWARD:
988 : 0 : direction = GTK_DIR_TAB_BACKWARD;
989 : 0 : break;
990 : 0 : case GTK_DIR_LEFT:
991 : 0 : direction = GTK_DIR_RIGHT;
992 : 0 : break;
993 : 0 : case GTK_DIR_RIGHT:
994 : 0 : direction = GTK_DIR_LEFT;
995 : 0 : break;
996 : 0 : case GTK_DIR_UP:
997 : : case GTK_DIR_DOWN:
998 : : default:
999 : : /* No change. */
1000 : 0 : break;
1001 : : }
1002 : : }
1003 : :
1004 : 0 : focus_child = gtk_widget_get_focus_child (widget);
1005 : :
1006 [ # # ]: 0 : if (focus_child != NULL)
1007 : : {
1008 : : /* Can the focus move around inside the currently focused child widget? */
1009 [ # # ]: 0 : if (gtk_widget_child_focus (focus_child, direction))
1010 : 0 : return TRUE;
1011 : :
1012 [ # # # # ]: 0 : if (CC_IS_BAR_CHART_GROUP (focus_child) &&
1013 [ # # ]: 0 : (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD))
1014 : 0 : next_focus_group = get_adjacent_focusable_group (self, CC_BAR_CHART_GROUP (focus_child), -1);
1015 [ # # # # ]: 0 : else if (CC_IS_BAR_CHART_GROUP (focus_child) &&
1016 [ # # ]: 0 : (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD))
1017 : 0 : next_focus_group = get_adjacent_focusable_group (self, CC_BAR_CHART_GROUP (focus_child), 1);
1018 : : }
1019 : : else
1020 : : {
1021 : : /* No current focus group. If a group is selected, focus on that. Otherwise,
1022 : : * focus on the first/last focusable group, depending on which direction
1023 : : * we’re coming in from. */
1024 [ # # ]: 0 : if (self->selected_index_set)
1025 : 0 : next_focus_group = self->cached_groups->pdata[self->selected_index];
1026 : :
1027 [ # # # # ]: 0 : if (next_focus_group == NULL &&
1028 [ # # # # ]: 0 : (direction == GTK_DIR_UP || direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD))
1029 : 0 : next_focus_group = get_last_focusable_group (self);
1030 [ # # # # ]: 0 : else if (next_focus_group == NULL &&
1031 [ # # # # ]: 0 : (direction == GTK_DIR_DOWN || direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD))
1032 : 0 : next_focus_group = get_first_focusable_group (self);
1033 : : }
1034 : :
1035 [ # # ]: 0 : if (next_focus_group == NULL)
1036 : : {
1037 [ # # # # ]: 0 : if (direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT)
1038 : : {
1039 [ # # ]: 0 : if (gtk_widget_keynav_failed (widget, direction))
1040 : 0 : return TRUE;
1041 : : }
1042 : :
1043 : 0 : return FALSE;
1044 : : }
1045 : :
1046 : 0 : return gtk_widget_child_focus (GTK_WIDGET (next_focus_group), direction);
1047 : : }
1048 : :
1049 : : static void
1050 : 0 : group_notify_selected_index_cb (GObject *object,
1051 : : GParamSpec *pspec,
1052 : : gpointer user_data)
1053 : : {
1054 : 0 : CcBarChart *self = CC_BAR_CHART (user_data);
1055 : 0 : CcBarChartGroup *group = CC_BAR_CHART_GROUP (object);
1056 : : gboolean success;
1057 : 0 : unsigned int group_idx = 0;
1058 : 0 : size_t selected_idx = 0;
1059 : :
1060 : : /* Which group is this? */
1061 : 0 : success = find_index_for_group (self, group, &group_idx);
1062 : 0 : g_assert (success);
1063 : :
1064 [ # # # # ]: 0 : if (cc_bar_chart_group_get_is_selected (group) ||
1065 : 0 : cc_bar_chart_group_get_selected_index (group, NULL))
1066 : : {
1067 : : /* If the group is now selected, update our selection. */
1068 : 0 : cc_bar_chart_set_selected_index (self, TRUE, group_idx);
1069 : : }
1070 [ # # ]: 0 : else if (cc_bar_chart_get_selected_index (self, &selected_idx) &&
1071 [ # # ]: 0 : selected_idx == group_idx)
1072 : : {
1073 : : /* Otherwise, if the group is no longer selected, but was the selected
1074 : : * group in our selection, clear that. */
1075 : 0 : cc_bar_chart_set_selected_index (self, FALSE, 0);
1076 : : }
1077 : 0 : }
1078 : :
1079 : : static void
1080 : 0 : bar_activate_cb (CcBarChartBar *bar,
1081 : : gpointer user_data)
1082 : : {
1083 : 0 : CcBarChart *self = CC_BAR_CHART (user_data);
1084 : 0 : unsigned int idx = 0;
1085 : :
1086 : : /* Select and activate the bar */
1087 [ # # ]: 0 : if (!find_index_for_bar (self, bar, &idx))
1088 : 0 : return;
1089 : :
1090 : 0 : gtk_widget_grab_focus (GTK_WIDGET (bar));
1091 : 0 : cc_bar_chart_set_selected_index (self, TRUE, idx);
1092 : :
1093 : 0 : g_signal_emit (self, signals[SIGNAL_BAR_ACTIVATED], 0, bar);
1094 : : }
1095 : :
1096 : : static void
1097 : 0 : activate_cursor_bar_cb (CcBarChart *self,
1098 : : gpointer user_data)
1099 : : {
1100 [ # # ]: 0 : if (self->selected_index_set)
1101 : 0 : gtk_widget_activate (GTK_WIDGET (self->cached_groups->pdata[self->selected_index]));
1102 : 0 : }
1103 : :
1104 : : static void
1105 : 0 : move_cursor_cb (CcBarChart *self,
1106 : : GtkMovementStep step,
1107 : : int count)
1108 : : {
1109 : 0 : CcBarChartGroup *group = NULL, *selected_group = NULL;
1110 : 0 : unsigned int idx = 0;
1111 : : int visual_count;
1112 : :
1113 : 0 : g_assert (self->cached_groups != NULL);
1114 : :
1115 [ # # ]: 0 : if (self->selected_index_set)
1116 : 0 : selected_group = self->cached_groups->pdata[self->selected_index];
1117 : :
1118 [ # # ]: 0 : if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
1119 : 0 : visual_count = -count;
1120 : : else
1121 : 0 : visual_count = count;
1122 : :
1123 [ # # # # ]: 0 : switch (step)
1124 : : {
1125 : 0 : case GTK_MOVEMENT_BUFFER_ENDS:
1126 [ # # ]: 0 : if (count < 0)
1127 : 0 : group = get_first_focusable_group (self);
1128 : : else
1129 : 0 : group = get_last_focusable_group (self);
1130 : 0 : break;
1131 : 0 : case GTK_MOVEMENT_LOGICAL_POSITIONS:
1132 [ # # ]: 0 : if (selected_group != NULL)
1133 [ # # ]: 0 : group = get_adjacent_focusable_group (self, selected_group, (count < 0) ? -1 : 1);
1134 : 0 : break;
1135 : 0 : case GTK_MOVEMENT_VISUAL_POSITIONS:
1136 [ # # ]: 0 : if (selected_group != NULL)
1137 [ # # ]: 0 : group = get_adjacent_focusable_group (self, selected_group, (visual_count < 0) ? -1 : 1);
1138 : 0 : break;
1139 : 0 : case GTK_MOVEMENT_WORDS:
1140 : : case GTK_MOVEMENT_DISPLAY_LINES:
1141 : : case GTK_MOVEMENT_DISPLAY_LINE_ENDS:
1142 : : case GTK_MOVEMENT_PARAGRAPHS:
1143 : : case GTK_MOVEMENT_PARAGRAPH_ENDS:
1144 : : case GTK_MOVEMENT_PAGES:
1145 : : case GTK_MOVEMENT_HORIZONTAL_PAGES:
1146 : : default:
1147 : : /* Not currently supported */
1148 : 0 : return;
1149 : : }
1150 : :
1151 : : /* Did we fail to move anywhere? */
1152 [ # # # # ]: 0 : if (group == NULL || group == selected_group)
1153 : : {
1154 : 0 : GtkDirectionType direction = (count < 0) ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
1155 : :
1156 [ # # ]: 0 : if (!gtk_widget_keynav_failed (GTK_WIDGET (self), direction))
1157 : : {
1158 : 0 : GtkWidget *toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
1159 : :
1160 [ # # ]: 0 : if (toplevel != NULL)
1161 : 0 : gtk_widget_child_focus (toplevel, direction);
1162 : : }
1163 : :
1164 : 0 : return;
1165 : : }
1166 : :
1167 [ # # ]: 0 : if (find_index_for_group (self, group, &idx))
1168 : : {
1169 : 0 : gtk_widget_grab_focus (GTK_WIDGET (group));
1170 : 0 : cc_bar_chart_set_selected_index (self, TRUE, idx);
1171 : : }
1172 : : }
1173 : :
1174 : : static gboolean
1175 : 0 : find_index_for_group (CcBarChart *self,
1176 : : CcBarChartGroup *group,
1177 : : unsigned int *out_idx)
1178 : : {
1179 : 0 : g_assert (gtk_widget_is_ancestor (GTK_WIDGET (group), GTK_WIDGET (self)));
1180 : 0 : g_assert (self->cached_groups != NULL);
1181 : :
1182 : 0 : return g_ptr_array_find (self->cached_groups, group, out_idx);
1183 : : }
1184 : :
1185 : : static gboolean
1186 : 0 : find_index_for_bar (CcBarChart *self,
1187 : : CcBarChartBar *bar,
1188 : : unsigned int *out_idx)
1189 : : {
1190 : 0 : unsigned int bar_idx = 0;
1191 : :
1192 : 0 : g_assert (gtk_widget_is_ancestor (GTK_WIDGET (bar), GTK_WIDGET (self)));
1193 : 0 : g_assert (self->cached_groups != NULL);
1194 : :
1195 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_groups->len; i++)
1196 : : {
1197 : 0 : CcBarChartGroup *group = self->cached_groups->pdata[i];
1198 : 0 : size_t n_bars = 0;
1199 : 0 : CcBarChartBar * const *bars = cc_bar_chart_group_get_bars (group, &n_bars);
1200 : :
1201 [ # # ]: 0 : for (size_t j = 0; j < n_bars; j++)
1202 : : {
1203 [ # # ]: 0 : if (bars[j] == bar)
1204 : : {
1205 [ # # ]: 0 : if (out_idx != NULL)
1206 : 0 : *out_idx = bar_idx;
1207 : 0 : return TRUE;
1208 : : }
1209 : : else
1210 : : {
1211 : 0 : bar_idx++;
1212 : : }
1213 : : }
1214 : : }
1215 : :
1216 [ # # ]: 0 : if (out_idx != NULL)
1217 : 0 : *out_idx = 0;
1218 : :
1219 : 0 : return FALSE;
1220 : : }
1221 : :
1222 : : static gboolean
1223 : 0 : group_is_focusable (CcBarChartGroup *group)
1224 : : {
1225 : 0 : GtkWidget *widget = GTK_WIDGET (group);
1226 : :
1227 [ # # ]: 0 : return (gtk_widget_is_visible (widget) &&
1228 [ # # ]: 0 : gtk_widget_is_sensitive (widget) &&
1229 [ # # # # ]: 0 : gtk_widget_get_focusable (widget) &&
1230 : 0 : gtk_widget_get_can_focus (widget));
1231 : : }
1232 : :
1233 : : /* direction == -1 means get previous sensitive and visible group;
1234 : : * direction == 1 means get next one. */
1235 : : static CcBarChartGroup *
1236 : 0 : get_adjacent_focusable_group (CcBarChart *self,
1237 : : CcBarChartGroup *group,
1238 : : int direction)
1239 : : {
1240 : : unsigned int group_idx, i;
1241 : :
1242 : 0 : g_assert (gtk_widget_is_ancestor (GTK_WIDGET (group), GTK_WIDGET (self)));
1243 : 0 : g_assert (self->cached_groups != NULL);
1244 : 0 : g_assert (direction == -1 || direction == 1);
1245 : :
1246 [ # # ]: 0 : if (!find_index_for_group (self, group, &group_idx))
1247 : 0 : return NULL;
1248 : :
1249 : 0 : i = group_idx;
1250 : :
1251 [ # # # # : 0 : while (!((direction == -1 && i == 0) ||
# # ]
1252 [ # # ]: 0 : (direction == 1 && i >= self->cached_groups->len - 1)))
1253 : : {
1254 : : CcBarChartGroup *adjacent_group;
1255 : :
1256 : 0 : i += direction;
1257 : 0 : adjacent_group = self->cached_groups->pdata[i];
1258 : :
1259 [ # # ]: 0 : if (group_is_focusable (adjacent_group))
1260 : 0 : return adjacent_group;
1261 : : }
1262 : :
1263 : 0 : return NULL;
1264 : : }
1265 : :
1266 : : static CcBarChartGroup *
1267 : 0 : get_first_focusable_group (CcBarChart *self)
1268 : : {
1269 : 0 : g_assert (self->cached_groups != NULL);
1270 : :
1271 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_groups->len; i++)
1272 : : {
1273 : 0 : CcBarChartGroup *group = self->cached_groups->pdata[i];
1274 : :
1275 [ # # ]: 0 : if (group_is_focusable (group))
1276 : 0 : return group;
1277 : : }
1278 : :
1279 : 0 : return NULL;
1280 : : }
1281 : :
1282 : : static CcBarChartGroup *
1283 : 0 : get_last_focusable_group (CcBarChart *self)
1284 : : {
1285 : 0 : g_assert (self->cached_groups != NULL);
1286 : :
1287 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_groups->len; i++)
1288 : : {
1289 : 0 : CcBarChartGroup *group = self->cached_groups->pdata[self->cached_groups->len - 1 - i];
1290 : :
1291 [ # # ]: 0 : if (group_is_focusable (group))
1292 : 0 : return group;
1293 : : }
1294 : :
1295 : 0 : return NULL;
1296 : : }
1297 : :
1298 : : static void
1299 : 0 : ensure_cached_grid_lines_and_labels (CcBarChart *self)
1300 : : {
1301 : 0 : const double max_value = get_maximum_data_value (self, TRUE);
1302 : : double latest_grid_line_value;
1303 : :
1304 : : /* Calculate our continuous axis grid lines. Use the user’s provided callback
1305 : : * to lay them out, until we’ve got enough to cover the maximum data value.
1306 : : * We always need at least two grid lines to define the top and bottom of the
1307 : : * plot. */
1308 [ # # ]: 0 : if (self->cached_continuous_axis_grid_line_values == NULL &&
1309 [ # # ]: 0 : self->continuous_axis_grid_line_callback != NULL)
1310 : : {
1311 : 0 : self->cached_continuous_axis_grid_line_values = g_array_new (FALSE, FALSE, sizeof (double));
1312 : :
1313 : : do
1314 : : {
1315 : 0 : latest_grid_line_value = self->continuous_axis_grid_line_callback (self,
1316 : 0 : self->cached_continuous_axis_grid_line_values->len,
1317 : : self->continuous_axis_grid_line_user_data);
1318 : 0 : g_assert (latest_grid_line_value >= 0.0);
1319 : 0 : g_array_append_val (self->cached_continuous_axis_grid_line_values, latest_grid_line_value);
1320 : : }
1321 [ # # ]: 0 : while (latest_grid_line_value <= max_value ||
1322 [ # # ]: 0 : self->cached_continuous_axis_grid_line_values->len < 2);
1323 : : }
1324 : :
1325 : : /* Create one continuous axis label for each grid line. In a subsequent step
1326 : : * in cc_bar_chart_size_allocate() we position them all and we work out
1327 : : * collisions and hide labels based on index modulus to avoid drawing text on
1328 : : * top of other text. See cc_bar_chart_size_allocate(). */
1329 [ # # ]: 0 : if (self->cached_continuous_axis_labels == NULL &&
1330 [ # # ]: 0 : self->continuous_axis_label_callback != NULL &&
1331 [ # # ]: 0 : self->cached_continuous_axis_grid_line_values != NULL)
1332 : : {
1333 : 0 : self->cached_continuous_axis_labels = g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_widget_unparent);
1334 : :
1335 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_continuous_axis_grid_line_values->len; i++)
1336 : : {
1337 : 0 : const double grid_line_value = g_array_index (self->cached_continuous_axis_grid_line_values, double, i);
1338 : 0 : g_autofree char *label_text = format_continuous_axis_label (self, grid_line_value);
1339 : 0 : GtkLabel *label = create_continuous_axis_label (label_text);
1340 : 0 : gtk_widget_set_parent (GTK_WIDGET (label), GTK_WIDGET (self));
1341 : : /* don’t insert the label into the widget child order using
1342 : : * gtk_widget_insert_after(), as it shouldn’t be focusable */
1343 : 0 : g_ptr_array_add (self->cached_continuous_axis_labels, label);
1344 : : }
1345 : : }
1346 : 0 : }
1347 : :
1348 : : static inline void
1349 : 0 : calculate_axis_area_widths (CcBarChart *self,
1350 : : int *out_left_axis_area_width,
1351 : : int *out_right_axis_area_width)
1352 : : {
1353 : : int left_axis_area_width, right_axis_area_width;
1354 : :
1355 : : /* The continuous axis is on the right of the plot area in LTR directions,
1356 : : * and on the left in RTL. */
1357 [ # # ]: 0 : if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
1358 : : {
1359 : 0 : left_axis_area_width = self->cached_continuous_axis_area_width;
1360 : 0 : right_axis_area_width = 0;
1361 : : }
1362 : : else
1363 : : {
1364 : 0 : left_axis_area_width = 0;
1365 : 0 : right_axis_area_width = self->cached_continuous_axis_area_width;
1366 : : }
1367 : :
1368 [ # # ]: 0 : if (out_left_axis_area_width != NULL)
1369 : 0 : *out_left_axis_area_width = left_axis_area_width;
1370 [ # # ]: 0 : if (out_right_axis_area_width != NULL)
1371 : 0 : *out_right_axis_area_width = right_axis_area_width;
1372 : 0 : }
1373 : :
1374 : : /* Calculate the x-coordinate bounds of a bar group and its spacing. This is
1375 : : * done individually for each group, rather than caching a single width for
1376 : : * groups and multiplying it, so that rounding errors don’t accumulate across
1377 : : * the width of the plot area. */
1378 : : static inline void
1379 : 0 : calculate_group_x_bounds (CcBarChart *self,
1380 : : unsigned int idx,
1381 : : int *out_spacing_start_x,
1382 : : int *out_group_start_x,
1383 : : int *out_group_finish_x,
1384 : : int *out_spacing_finish_x)
1385 : : {
1386 : : int widget_width, plot_width, extra_plot_width;
1387 : : int left_axis_area_width, right_axis_area_width;
1388 : : int group_width, group_spacing;
1389 : : int spacing_start_x, group_start_x, group_finish_x, spacing_finish_x;
1390 : :
1391 : 0 : g_assert (self->n_data > 0);
1392 : :
1393 : 0 : calculate_axis_area_widths (self, &left_axis_area_width, &right_axis_area_width);
1394 : :
1395 : : /* If drawing RTL, reverse the bar positions. */
1396 [ # # ]: 0 : if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
1397 : 0 : idx = self->n_data - idx - 1;
1398 : :
1399 : 0 : widget_width = gtk_widget_get_width (GTK_WIDGET (self));
1400 : 0 : plot_width = widget_width - left_axis_area_width - right_axis_area_width;
1401 : 0 : extra_plot_width = plot_width - self->cached_minimum_group_width * self->n_data;
1402 : :
1403 : 0 : group_width = self->cached_minimum_group_width + (extra_plot_width / self->n_data) * GROUP_TO_SPACE_WIDTH_FILL_RATIO;
1404 : 0 : group_spacing = (extra_plot_width / self->n_data) * (1.0 - GROUP_TO_SPACE_WIDTH_FILL_RATIO);
1405 : :
1406 : 0 : spacing_start_x = left_axis_area_width + plot_width * idx / self->n_data;
1407 : 0 : group_start_x = spacing_start_x + group_spacing / 2;
1408 : 0 : group_finish_x = group_start_x + group_width;
1409 : 0 : spacing_finish_x = left_axis_area_width + plot_width * (idx + 1) / self->n_data;
1410 : :
1411 : 0 : g_assert (spacing_start_x <= group_start_x &&
1412 : : group_start_x <= group_finish_x &&
1413 : : group_finish_x <= spacing_finish_x);
1414 : :
1415 [ # # ]: 0 : if (out_spacing_start_x != NULL)
1416 : 0 : *out_spacing_start_x = spacing_start_x;
1417 [ # # ]: 0 : if (out_group_start_x != NULL)
1418 : 0 : *out_group_start_x = group_start_x;
1419 [ # # ]: 0 : if (out_group_finish_x != NULL)
1420 : 0 : *out_group_finish_x = group_finish_x;
1421 [ # # ]: 0 : if (out_spacing_finish_x != NULL)
1422 : 0 : *out_spacing_finish_x = spacing_finish_x;
1423 : 0 : }
1424 : :
1425 : : /* Convert a value from the domain of self->data to widget coordinates. */
1426 : : static int
1427 : 0 : value_to_widget_y (CcBarChart *self,
1428 : : double value)
1429 : : {
1430 : 0 : int height = gtk_widget_get_height (GTK_WIDGET (self));
1431 : :
1432 : : /* Negative values are not currently supported. */
1433 : 0 : g_assert (value >= 0.0);
1434 : :
1435 : : /* The widget should be sized to accommodate all values in the data. */
1436 : 0 : g_assert (self->cached_pixels_per_data * value <= height - self->cached_discrete_axis_area_height);
1437 : :
1438 : 0 : return height - self->cached_discrete_axis_area_height - self->cached_pixels_per_data * value;
1439 : : }
1440 : :
1441 : : /* returns floating reference */
1442 : : static GtkLabel *
1443 : 0 : create_discrete_axis_label (const char *text)
1444 : : {
1445 : 0 : GtkLabel *label = GTK_LABEL (gtk_label_new (text));
1446 : 0 : gtk_label_set_xalign (label, 0.5);
1447 : 0 : gtk_widget_add_css_class (GTK_WIDGET (label), "discrete-axis-label");
1448 : :
1449 : 0 : return g_steal_pointer (&label);
1450 : : }
1451 : :
1452 : : /* returns floating reference */
1453 : : static GtkLabel *
1454 : 0 : create_continuous_axis_label (const char *text)
1455 : : {
1456 : 0 : GtkLabel *label = GTK_LABEL (gtk_label_new (text));
1457 : 0 : gtk_label_set_xalign (label, 0.0);
1458 : 0 : gtk_label_set_yalign (label, 0.0);
1459 : 0 : gtk_widget_set_valign (GTK_WIDGET (label), GTK_ALIGN_BASELINE_CENTER);
1460 : 0 : gtk_widget_add_css_class (GTK_WIDGET (label), "continuous-axis-label");
1461 : :
1462 : 0 : return g_steal_pointer (&label);
1463 : : }
1464 : :
1465 : : static char *
1466 : 0 : format_continuous_axis_label (CcBarChart *self,
1467 : : double value)
1468 : : {
1469 : 0 : g_autofree char *out = NULL;
1470 : :
1471 : 0 : g_assert (self->continuous_axis_label_callback != NULL);
1472 : :
1473 : 0 : out = self->continuous_axis_label_callback (self, value, self->continuous_axis_label_user_data);
1474 : 0 : g_assert (out != NULL);
1475 : :
1476 : 0 : return g_steal_pointer (&out);
1477 : : }
1478 : :
1479 : : static double
1480 : 0 : get_maximum_data_value (CcBarChart *self,
1481 : : gboolean include_overlay_line)
1482 : : {
1483 : 0 : double value = 0.0;
1484 : :
1485 : 0 : g_assert (self->data != NULL);
1486 : :
1487 [ # # ]: 0 : for (size_t i = 0; i < self->n_data; i++)
1488 : : {
1489 [ # # ]: 0 : if (!isnan (self->data[i]))
1490 [ # # ]: 0 : value = MAX (value, self->data[i]);
1491 : : }
1492 : :
1493 [ # # # # ]: 0 : if (include_overlay_line && !isnan (self->overlay_line_value))
1494 [ # # ]: 0 : value = MAX (value, self->overlay_line_value);
1495 : :
1496 : 0 : return value;
1497 : : }
1498 : :
1499 : : static void
1500 : 0 : update_group_accessible_relations (CcBarChart *self)
1501 : : {
1502 : 0 : gtk_accessible_update_relation (GTK_ACCESSIBLE (self),
1503 : : GTK_ACCESSIBLE_RELATION_ROW_COUNT, self->n_data,
1504 : : -1);
1505 : :
1506 [ # # ]: 0 : if (self->cached_groups != NULL)
1507 : : {
1508 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_groups->len; i++)
1509 : : {
1510 : 0 : CcBarChartGroup *group = self->cached_groups->pdata[i];
1511 : :
1512 : 0 : gtk_accessible_update_relation (GTK_ACCESSIBLE (group),
1513 : : GTK_ACCESSIBLE_RELATION_ROW_INDEX, i,
1514 : : -1);
1515 : : }
1516 : : }
1517 : :
1518 [ # # ]: 0 : if (self->cached_groups != NULL &&
1519 [ # # ]: 0 : self->cached_discrete_axis_labels != NULL)
1520 : : {
1521 : 0 : g_assert (self->cached_groups->len == self->cached_discrete_axis_labels->len);
1522 : :
1523 [ # # ]: 0 : for (unsigned int i = 0; i < self->cached_groups->len; i++)
1524 : : {
1525 : 0 : CcBarChartGroup *group = self->cached_groups->pdata[i];
1526 : 0 : GtkLabel *label = self->cached_discrete_axis_labels->pdata[i];
1527 : 0 : CcBarChartBar * const *bars = NULL;
1528 : 0 : size_t n_bars = 0;
1529 : :
1530 : 0 : gtk_accessible_update_relation (GTK_ACCESSIBLE (group),
1531 : : GTK_ACCESSIBLE_RELATION_LABELLED_BY, label, NULL,
1532 : : -1);
1533 : :
1534 : 0 : bars = cc_bar_chart_group_get_bars (group, &n_bars);
1535 [ # # ]: 0 : for (unsigned int j = 0; j < n_bars; j++)
1536 : : {
1537 : 0 : gtk_accessible_update_relation (GTK_ACCESSIBLE (bars[j]),
1538 : : GTK_ACCESSIBLE_RELATION_LABELLED_BY, label, NULL,
1539 : : -1);
1540 : : }
1541 : : }
1542 : : }
1543 : 0 : }
1544 : :
1545 : : /**
1546 : : * cc_bar_chart_new:
1547 : : *
1548 : : * Create a new #CcBarChart.
1549 : : *
1550 : : * Returns: (transfer full): the new #CcBarChart
1551 : : */
1552 : : CcBarChart *
1553 : 0 : cc_bar_chart_new (void)
1554 : : {
1555 : 0 : return g_object_new (CC_TYPE_BAR_CHART, NULL);
1556 : : }
1557 : :
1558 : : /**
1559 : : * cc_bar_chart_get_discrete_axis_labels:
1560 : : * @self: a #CcBarChart
1561 : : * @out_n_discrete_axis_labels: (out) (optional): return location for the number
1562 : : * of labels, or `NULL` to ignore
1563 : : *
1564 : : * Get the discrete axis labels for the chart.
1565 : : *
1566 : : * This will be `NULL` if no labels have been set yet, in which case `0` will be
1567 : : * returned in @out_n_discrete_axis_labels.
1568 : : *
1569 : : * See #CcBarChart:discrete-axis-labels.
1570 : : *
1571 : : * Returns: (nullable) (array zero-terminated=1 length=out_n_discrete_axis_labels) (transfer none): array
1572 : : * of discrete axis labels
1573 : : */
1574 : : const char * const *
1575 : 0 : cc_bar_chart_get_discrete_axis_labels (CcBarChart *self,
1576 : : size_t *out_n_discrete_axis_labels)
1577 : : {
1578 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART (self), NULL);
1579 : :
1580 [ # # ]: 0 : if (out_n_discrete_axis_labels != NULL)
1581 : 0 : *out_n_discrete_axis_labels = self->n_discrete_axis_labels;
1582 : :
1583 : 0 : return (const char * const *) self->discrete_axis_labels;
1584 : : }
1585 : :
1586 : : /**
1587 : : * cc_bar_chart_set_discrete_axis_labels:
1588 : : * @self: a #CcBarChart
1589 : : * @labels: (array zero-terminated=1) (nullable) (transfer none): new set of
1590 : : * discrete axis labels, or `NULL` to unset
1591 : : *
1592 : : * Set the discrete axis labels for the chart.
1593 : : *
1594 : : * This can be `NULL` if the labels are currently unknown.
1595 : : *
1596 : : * See #CcBarChart:discrete-axis-labels.
1597 : : */
1598 : : void
1599 : 0 : cc_bar_chart_set_discrete_axis_labels (CcBarChart *self,
1600 : : const char * const *labels)
1601 : : {
1602 : 0 : g_return_if_fail (CC_IS_BAR_CHART (self));
1603 : :
1604 [ # # # # ]: 0 : if ((self->discrete_axis_labels == NULL && labels == NULL) ||
1605 [ # # # # : 0 : (self->discrete_axis_labels != NULL && labels != NULL &&
# # ]
1606 : 0 : g_strv_equal ((const char * const *) self->discrete_axis_labels, labels)))
1607 : 0 : return;
1608 : :
1609 : 0 : g_strfreev (self->discrete_axis_labels);
1610 : 0 : self->discrete_axis_labels = g_strdupv ((char **) labels);
1611 [ # # ]: 0 : self->n_discrete_axis_labels = (labels != NULL) ? g_strv_length ((char **) labels) : 0;
1612 : :
1613 : : /* Rebuild the cache */
1614 [ # # ]: 0 : g_clear_pointer (&self->cached_discrete_axis_labels, g_ptr_array_unref);
1615 [ # # ]: 0 : if (self->n_discrete_axis_labels > 0)
1616 : 0 : self->cached_discrete_axis_labels = g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_widget_unparent);
1617 : :
1618 [ # # ]: 0 : for (size_t i = 0; i < self->n_discrete_axis_labels; i++)
1619 : : {
1620 : 0 : GtkLabel *label = create_discrete_axis_label (self->discrete_axis_labels[i]);
1621 : 0 : gtk_widget_set_parent (GTK_WIDGET (label), GTK_WIDGET (self));
1622 : : /* don’t insert the label into the widget child order using
1623 : : * gtk_widget_insert_after(), as it shouldn’t be focusable */
1624 : 0 : g_ptr_array_add (self->cached_discrete_axis_labels, label);
1625 : : }
1626 : :
1627 : 0 : update_group_accessible_relations (self);
1628 : :
1629 : : /* Re-render */
1630 : 0 : gtk_widget_queue_resize (GTK_WIDGET (self));
1631 : :
1632 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DISCRETE_AXIS_LABELS]);
1633 : : }
1634 : :
1635 : : /**
1636 : : * cc_bar_chart_set_continuous_axis_label_callback:
1637 : : * @self: a #CcBarChart
1638 : : * @callback: (nullable): callback to generate continuous axis labels, or
1639 : : * `NULL` to disable them
1640 : : * @user_data: (nullable) (closure callback): user data for @callback
1641 : : * @destroy_notify: (nullable) (destroy callback): destroy function for @user_data
1642 : : *
1643 : : * Set the callback to generate labels for the continuous axis.
1644 : : *
1645 : : * This is called multiple times when sizing and rendering the chart, to
1646 : : * generate the labels for the continuous axis. Grid lines and marks are
1647 : : * generated, then some of them are converted to textual labels using @callback.
1648 : : */
1649 : : void
1650 : 0 : cc_bar_chart_set_continuous_axis_label_callback (CcBarChart *self,
1651 : : CcBarChartLabelCallback callback,
1652 : : void *user_data,
1653 : : GDestroyNotify destroy_notify)
1654 : : {
1655 : 0 : g_return_if_fail (CC_IS_BAR_CHART (self));
1656 : :
1657 [ # # ]: 0 : if (self->continuous_axis_label_callback == callback &&
1658 [ # # ]: 0 : self->continuous_axis_label_user_data == user_data &&
1659 [ # # ]: 0 : self->continuous_axis_label_destroy_notify == destroy_notify)
1660 : 0 : return;
1661 : :
1662 [ # # ]: 0 : if (self->continuous_axis_label_destroy_notify != NULL)
1663 : 0 : self->continuous_axis_label_destroy_notify (self->continuous_axis_label_user_data);
1664 : :
1665 : 0 : self->continuous_axis_label_callback = callback;
1666 : 0 : self->continuous_axis_label_user_data = user_data;
1667 : 0 : self->continuous_axis_label_destroy_notify = destroy_notify;
1668 : :
1669 : : /* Clear the old cache */
1670 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_labels, g_ptr_array_unref);
1671 : :
1672 : : /* Re-render */
1673 : 0 : gtk_widget_queue_resize (GTK_WIDGET (self));
1674 : : }
1675 : :
1676 : : /**
1677 : : * cc_bar_chart_set_continuous_axis_grid_line_callback:
1678 : : * @self: a #CcBarChart
1679 : : * @callback: (nullable): callback to generate continuous axis grid lines, or
1680 : : * `NULL` to disable them
1681 : : * @user_data: (nullable) (closure callback): user data for @callback
1682 : : * @destroy_notify: (nullable) (destroy callback): destroy function for @user_data
1683 : : *
1684 : : * Set the callback to generate grid lines for the continuous axis.
1685 : : *
1686 : : * This is called multiple times when sizing and rendering the chart, to
1687 : : * generate the grid lines for the continuous axis. See the documentation for
1688 : : * #CcBarChartGridLineCallback for further details.
1689 : : */
1690 : : void
1691 : 0 : cc_bar_chart_set_continuous_axis_grid_line_callback (CcBarChart *self,
1692 : : CcBarChartGridLineCallback callback,
1693 : : void *user_data,
1694 : : GDestroyNotify destroy_notify)
1695 : : {
1696 : 0 : g_return_if_fail (CC_IS_BAR_CHART (self));
1697 : :
1698 [ # # ]: 0 : if (self->continuous_axis_grid_line_callback == callback &&
1699 [ # # ]: 0 : self->continuous_axis_grid_line_user_data == user_data &&
1700 [ # # ]: 0 : self->continuous_axis_grid_line_destroy_notify == destroy_notify)
1701 : 0 : return;
1702 : :
1703 [ # # ]: 0 : if (self->continuous_axis_grid_line_destroy_notify != NULL)
1704 : 0 : self->continuous_axis_grid_line_destroy_notify (self->continuous_axis_grid_line_user_data);
1705 : :
1706 : 0 : self->continuous_axis_grid_line_callback = callback;
1707 : 0 : self->continuous_axis_grid_line_user_data = user_data;
1708 : 0 : self->continuous_axis_grid_line_destroy_notify = destroy_notify;
1709 : :
1710 : : /* Clear the old cache, including the labels */
1711 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_grid_line_values, g_array_unref);
1712 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_labels, g_ptr_array_unref);
1713 : :
1714 : : /* Re-render */
1715 : 0 : gtk_widget_queue_resize (GTK_WIDGET (self));
1716 : : }
1717 : :
1718 : : /**
1719 : : * cc_bar_chart_get_data:
1720 : : * @self: a #CcBarChart
1721 : : * @out_n_data: (out) (not optional): return location for the number of data
1722 : : *
1723 : : * Get the data for the bar chart.
1724 : : *
1725 : : * If no data is currently set, `NULL` will be returned and @out_n_data will be
1726 : : * set to `0`.
1727 : : *
1728 : : * Returns: (array length=out_n_data) (nullable) (transfer none): data for the
1729 : : * chart, or `NULL` if it’s not currently set
1730 : : */
1731 : : const double *
1732 : 0 : cc_bar_chart_get_data (CcBarChart *self,
1733 : : size_t *out_n_data)
1734 : : {
1735 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART (self), NULL);
1736 : 0 : g_return_val_if_fail (out_n_data != NULL, NULL);
1737 : :
1738 : 0 : *out_n_data = self->n_data;
1739 : :
1740 : : /* Normalise to `NULL` */
1741 [ # # ]: 0 : return (self->n_data != 0) ? self->data : NULL;
1742 : : }
1743 : :
1744 : : /**
1745 : : * cc_bar_chart_set_data:
1746 : : * @self: a #CcBarChart
1747 : : * @data: (array length=n_data) (nullable) (transfer none): data for the bar
1748 : : * chart, or `NULL` to unset
1749 : : * @n_data: number of data
1750 : : *
1751 : : * Set the data for the bar chart.
1752 : : *
1753 : : * To clear the data for the chart, pass `NULL` for @data and `0` for @n_data.
1754 : : */
1755 : : void
1756 : 0 : cc_bar_chart_set_data (CcBarChart *self,
1757 : : const double *data,
1758 : : size_t n_data)
1759 : : {
1760 : 0 : g_return_if_fail (CC_IS_BAR_CHART (self));
1761 : 0 : g_return_if_fail (n_data == 0 || data != NULL);
1762 : 0 : g_return_if_fail (n_data <= G_MAXSIZE / sizeof (*data));
1763 : :
1764 : : /* Normalise input. */
1765 [ # # ]: 0 : if (n_data == 0)
1766 : 0 : data = NULL;
1767 : :
1768 [ # # # # ]: 0 : if (self->data == NULL && data == NULL)
1769 : 0 : return;
1770 : :
1771 [ # # # # : 0 : if (self->data != NULL && data != NULL && self->n_data == n_data &&
# # ]
1772 [ # # ]: 0 : memcmp (self->data, data, n_data * sizeof (*data)) == 0)
1773 : 0 : return;
1774 : :
1775 [ # # ]: 0 : g_clear_pointer (&self->data, g_free);
1776 : 0 : self->data = g_memdup2 (data, n_data * sizeof (*data));
1777 : 0 : self->n_data = n_data;
1778 : :
1779 : : /* Clear the cached bars, and also the grid lines and labels which are calculated based on the data. */
1780 [ # # ]: 0 : g_clear_pointer (&self->cached_groups, g_ptr_array_unref);
1781 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_grid_line_values, g_array_unref);
1782 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_labels, g_ptr_array_unref);
1783 : :
1784 : : /* Also clear the selection index. */
1785 : 0 : self->selected_index_set = FALSE;
1786 : 0 : self->selected_index = 0;
1787 : :
1788 : : /* Rebuild the cache. Currently we support exactly at most one bar per group.
1789 : : * There will be zero bars in groups where the data is NAN (i.e. not provided). */
1790 [ # # ]: 0 : if (self->n_data > 0)
1791 : 0 : self->cached_groups = g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_widget_unparent);
1792 : :
1793 [ # # ]: 0 : for (size_t i = 0; i < self->n_data; i++)
1794 : : {
1795 : 0 : CcBarChartGroup *group = cc_bar_chart_group_new ();
1796 [ # # ]: 0 : CcBarChartGroup *previous_group = (i > 0) ? self->cached_groups->pdata[i - 1] : NULL;
1797 : 0 : g_autofree char *accessible_label = format_continuous_axis_label (self, self->data[i]);
1798 : :
1799 : 0 : g_signal_connect (group, "notify::selected-index", G_CALLBACK (group_notify_selected_index_cb), self);
1800 : 0 : g_signal_connect (group, "notify::is-selected", G_CALLBACK (group_notify_selected_index_cb), self);
1801 : 0 : cc_bar_chart_group_set_selectable (group, isnan (self->data[i]));
1802 : 0 : cc_bar_chart_group_set_scale (group, self->cached_pixels_per_data);
1803 : :
1804 [ # # ]: 0 : if (!isnan (self->data[i]))
1805 : : {
1806 : 0 : CcBarChartBar *bar = cc_bar_chart_bar_new (self->data[i], accessible_label);
1807 : 0 : cc_bar_chart_group_insert_bar (group, -1, bar);
1808 : 0 : g_signal_connect (bar, "activate", G_CALLBACK (bar_activate_cb), self);
1809 : : }
1810 : :
1811 : 0 : gtk_widget_set_parent (GTK_WIDGET (group), GTK_WIDGET (self));
1812 : 0 : gtk_widget_insert_after (GTK_WIDGET (group), GTK_WIDGET (self), GTK_WIDGET (previous_group));
1813 : 0 : g_ptr_array_add (self->cached_groups, group);
1814 : : }
1815 : :
1816 : 0 : update_group_accessible_relations (self);
1817 : :
1818 : : /* Re-render */
1819 : 0 : gtk_widget_queue_resize (GTK_WIDGET (self));
1820 : :
1821 : 0 : g_signal_emit (self, signals[SIGNAL_DATA_CHANGED], 0);
1822 : : }
1823 : :
1824 : : /**
1825 : : * cc_bar_chart_get_overlay_line_value:
1826 : : * @self: a #CcBarChart
1827 : : *
1828 : : * Get the value of #CcBarChart:overlay-line-value.
1829 : : *
1830 : : * Returns: value to render an overlay line at, or `NAN` if unset
1831 : : */
1832 : : double
1833 : 0 : cc_bar_chart_get_overlay_line_value (CcBarChart *self)
1834 : : {
1835 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART (self), NAN);
1836 : :
1837 : 0 : return self->overlay_line_value;
1838 : : }
1839 : :
1840 : : /**
1841 : : * cc_bar_chart_set_overlay_line_value:
1842 : : * @self: a #CcBarChart
1843 : : * @value: value to render an overlay line at, or `NAN` to not render one
1844 : : *
1845 : : * Set the value of #CcBarChart:overlay-line-value.
1846 : : */
1847 : : void
1848 : 0 : cc_bar_chart_set_overlay_line_value (CcBarChart *self,
1849 : : double value)
1850 : : {
1851 : 0 : g_return_if_fail (CC_IS_BAR_CHART (self));
1852 : :
1853 [ # # # # ]: 0 : if ((isnan (self->overlay_line_value) && isnan (value)) ||
1854 [ # # ]: 0 : self->overlay_line_value == value)
1855 : 0 : return;
1856 : :
1857 : 0 : self->overlay_line_value = value;
1858 : :
1859 : : /* Clear the cached grid lines and labels as the overlay line might have been
1860 : : * the highest data value. */
1861 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_grid_line_values, g_array_unref);
1862 [ # # ]: 0 : g_clear_pointer (&self->cached_continuous_axis_labels, g_ptr_array_unref);
1863 : :
1864 : : /* Re-render */
1865 : 0 : gtk_widget_queue_resize (GTK_WIDGET (self));
1866 : :
1867 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_OVERLAY_LINE_VALUE]);
1868 : : }
1869 : :
1870 : : /**
1871 : : * cc_bar_chart_get_selected_index:
1872 : : * @self: a #CcBarChart
1873 : : * @out_index: (out) (optional): return location for the selected index, or
1874 : : * `NULL` to ignore
1875 : : *
1876 : : * Get the currently selected data index.
1877 : : *
1878 : : * If nothing is currently selected, @out_index will be set to `0` and `FALSE`
1879 : : * will be returned.
1880 : : *
1881 : : * Returns: `TRUE` if something is currently selected, `FALSE` otherwise
1882 : : */
1883 : : gboolean
1884 : 0 : cc_bar_chart_get_selected_index (CcBarChart *self,
1885 : : size_t *out_index)
1886 : : {
1887 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART (self), FALSE);
1888 : :
1889 [ # # ]: 0 : if (out_index != NULL)
1890 [ # # ]: 0 : *out_index = self->selected_index_set ? self->selected_index : 0;
1891 : :
1892 : 0 : return self->selected_index_set;
1893 : : }
1894 : :
1895 : : /**
1896 : : * cc_bar_chart_set_selected_index:
1897 : : * @self: a #CcBarChart
1898 : : * @is_selected: `TRUE` if something should be selected, `FALSE` if everything
1899 : : * should be unselected
1900 : : * @idx: index of the data to select, ignored if @is_selected is `FALSE`
1901 : : *
1902 : : * Set the currently selected data index, or unselect everything.
1903 : : *
1904 : : * If @is_selected is `TRUE`, the data at @idx will be selected. If @is_selected
1905 : : * is `FALSE`, @idx will be ignored and all data will be unselected.
1906 : : */
1907 : : void
1908 : 0 : cc_bar_chart_set_selected_index (CcBarChart *self,
1909 : : gboolean is_selected,
1910 : : size_t idx)
1911 : : {
1912 : 0 : g_return_if_fail (CC_IS_BAR_CHART (self));
1913 : 0 : g_return_if_fail (!is_selected || idx < self->n_data);
1914 : :
1915 [ # # ]: 0 : if (self->selected_index_set == is_selected &&
1916 [ # # # # ]: 0 : (!self->selected_index_set || self->selected_index == idx))
1917 : 0 : return;
1918 : :
1919 : : /* Clear the old selection. */
1920 [ # # ]: 0 : if (self->selected_index_set)
1921 : : {
1922 : 0 : g_assert (self->cached_groups != NULL);
1923 : 0 : g_signal_handlers_block_by_func (self->cached_groups->pdata[self->selected_index],
1924 : : group_notify_selected_index_cb, self);
1925 : 0 : cc_bar_chart_group_set_is_selected (self->cached_groups->pdata[self->selected_index], FALSE);
1926 : 0 : g_signal_handlers_unblock_by_func (self->cached_groups->pdata[self->selected_index],
1927 : : group_notify_selected_index_cb, self);
1928 : : }
1929 : :
1930 : 0 : self->selected_index_set = is_selected;
1931 [ # # ]: 0 : self->selected_index = is_selected ? idx : 0;
1932 : :
1933 : : /* Set the new selection. */
1934 [ # # ]: 0 : if (is_selected)
1935 : : {
1936 : 0 : size_t n_bars = 0;
1937 : :
1938 : 0 : g_assert (self->cached_groups != NULL);
1939 : 0 : g_signal_handlers_block_by_func (self->cached_groups->pdata[idx],
1940 : : group_notify_selected_index_cb, self);
1941 : 0 : cc_bar_chart_group_get_bars (self->cached_groups->pdata[idx], &n_bars);
1942 [ # # ]: 0 : if (n_bars > 0)
1943 : 0 : cc_bar_chart_group_set_selected_index (self->cached_groups->pdata[idx], TRUE, 0);
1944 : : else
1945 : 0 : cc_bar_chart_group_set_is_selected (self->cached_groups->pdata[idx], TRUE);
1946 : 0 : g_signal_handlers_unblock_by_func (self->cached_groups->pdata[idx],
1947 : : group_notify_selected_index_cb, self);
1948 : : }
1949 : :
1950 : : /* Re-render */
1951 : 0 : gtk_widget_queue_draw (GTK_WIDGET (self));
1952 : :
1953 : 0 : g_object_freeze_notify (G_OBJECT (self));
1954 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
1955 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX_SET]);
1956 : 0 : g_object_thaw_notify (G_OBJECT (self));
1957 : : }
|