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 <glib.h>
25 : : #include <glib-object.h>
26 : : #include <gtk/gtk.h>
27 : :
28 : : #include "cc-bar-chart-group.h"
29 : : #include "cc-bar-chart-bar.h"
30 : :
31 : : /**
32 : : * CcBarChartGroup:
33 : : *
34 : : * #CcBarChartGroup is a grouping of bars in a #CcBarChart.
35 : : *
36 : : * It contains only #CcBarChartBar children. Currently, exactly one bar is
37 : : * supported per group, but this could be relaxed in future to support multiple
38 : : * grouped bars.
39 : : *
40 : : * #CcBarChartGroup forms the touch landing area for highlighting and selecting
41 : : * a #CcBarChartBar, regardless of its rendered height. The group as a whole
42 : : * may be selected, indicated by #CcBarChartGroup:is-selected.
43 : : *
44 : : * # CSS nodes
45 : : *
46 : : * |[<!-- language="plain" -->
47 : : * bar-group[:hover][:selected]
48 : : * ╰── bar[:hover][:selected]
49 : : * ]|
50 : : *
51 : : * #CcBarChartGroup uses a single CSS node named `bar-group`. Each bar is a
52 : : * sub-node named `bar`. Bars and groups may have `:hover` or `:selected`
53 : : * pseudo-selectors to indicate whether they are selected or being hovered over
54 : : * with the mouse.
55 : : *
56 : : * # Accessibility
57 : : *
58 : : * #CcBarChartGroup uses the %GTK_ACCESSIBLE_ROLE_GROUP role and #CcBarChartBar
59 : : * uses the %GTK_ACCESSIBLE_ROLE_LIST_ITEM role.
60 : : */
61 : : struct _CcBarChartGroup {
62 : : GtkWidget parent_instance;
63 : :
64 : : /* Configured state: */
65 : : gboolean selectable;
66 : : enum
67 : : {
68 : : SELECTION_STATE_NONE,
69 : : SELECTION_STATE_GROUP,
70 : : SELECTION_STATE_BAR,
71 : : }
72 : : selection_state;
73 : : unsigned int selected_bar_index; /* only defined if selection_state == SELECTION_STATE_BAR */
74 : :
75 : : double scale; /* number of pixels per data value */
76 : : GPtrArray *bars; /* (not nullable) (owned) (element-type CcBarChartBar) */
77 : : };
78 : :
79 [ # # # # : 0 : G_DEFINE_TYPE (CcBarChartGroup, cc_bar_chart_group, GTK_TYPE_WIDGET)
# # ]
80 : :
81 : : typedef enum {
82 : : PROP_SELECTABLE = 1,
83 : : PROP_IS_SELECTED,
84 : : PROP_SELECTED_INDEX,
85 : : PROP_SELECTED_INDEX_SET,
86 : : PROP_SCALE,
87 : : } CcBarChartGroupProperty;
88 : :
89 : : static GParamSpec *props[PROP_SCALE + 1];
90 : :
91 : : static void cc_bar_chart_group_get_property (GObject *object,
92 : : guint property_id,
93 : : GValue *value,
94 : : GParamSpec *pspec);
95 : : static void cc_bar_chart_group_set_property (GObject *object,
96 : : guint property_id,
97 : : const GValue *value,
98 : : GParamSpec *pspec);
99 : : static void cc_bar_chart_group_dispose (GObject *object);
100 : : static void cc_bar_chart_group_size_allocate (GtkWidget *widget,
101 : : int width,
102 : : int height,
103 : : int baseline);
104 : : static void cc_bar_chart_group_measure (GtkWidget *widget,
105 : : GtkOrientation orientation,
106 : : int for_size,
107 : : int *minimum,
108 : : int *natural,
109 : : int *minimum_baseline,
110 : : int *natural_baseline);
111 : : static gboolean cc_bar_chart_group_focus (GtkWidget *widget,
112 : : GtkDirectionType direction);
113 : : static void gesture_click_pressed_cb (GtkGestureClick *gesture,
114 : : guint n_press,
115 : : double x,
116 : : double y,
117 : : gpointer user_data);
118 : :
119 : : static gboolean find_index_for_bar (CcBarChartGroup *self,
120 : : CcBarChartBar *bar,
121 : : unsigned int *out_idx);
122 : : static gboolean bar_is_focusable (CcBarChartBar *bar);
123 : : static CcBarChartBar *get_adjacent_focusable_bar (CcBarChartGroup *self,
124 : : CcBarChartBar *bar,
125 : : int direction);
126 : : static CcBarChartBar *get_first_focusable_bar (CcBarChartGroup *self);
127 : : static CcBarChartBar *get_last_focusable_bar (CcBarChartGroup *self);
128 : :
129 : : static void
130 : 0 : cc_bar_chart_group_class_init (CcBarChartGroupClass *klass)
131 : : {
132 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
133 : 0 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
134 : :
135 : 0 : object_class->get_property = cc_bar_chart_group_get_property;
136 : 0 : object_class->set_property = cc_bar_chart_group_set_property;
137 : 0 : object_class->dispose = cc_bar_chart_group_dispose;
138 : :
139 : 0 : widget_class->size_allocate = cc_bar_chart_group_size_allocate;
140 : 0 : widget_class->measure = cc_bar_chart_group_measure;
141 : 0 : widget_class->focus = cc_bar_chart_group_focus;
142 : :
143 : : /**
144 : : * CcBarChartGroup:selectable:
145 : : *
146 : : * Whether the group itself can be selected.
147 : : *
148 : : * If `FALSE`, any attempt to select the group will select its first bar
149 : : * instead.
150 : : */
151 : 0 : props[PROP_SELECTABLE] =
152 : 0 : g_param_spec_boolean ("selectable",
153 : : NULL, NULL,
154 : : TRUE,
155 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
156 : :
157 : : /**
158 : : * CcBarChartGroup:is-selected:
159 : : *
160 : : * Whether the group itself is currently selected.
161 : : */
162 : 0 : props[PROP_IS_SELECTED] =
163 : 0 : g_param_spec_boolean ("is-selected",
164 : : NULL, NULL,
165 : : FALSE,
166 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
167 : :
168 : : /**
169 : : * CcBarChartGroup:selected-index:
170 : : *
171 : : * Index of the currently selected bar.
172 : : *
173 : : * If nothing is currently selected, the value of this property is undefined.
174 : : * See #CcBarChartGroup:selected-index-set to check this.
175 : : */
176 : 0 : props[PROP_SELECTED_INDEX] =
177 : 0 : g_param_spec_uint ("selected-index",
178 : : NULL, NULL,
179 : : 0, G_MAXUINT, 0,
180 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
181 : :
182 : : /**
183 : : * CcBarChartGroup:selected-index-set:
184 : : *
185 : : * Whether a bar is currently selected.
186 : : *
187 : : * If this property is `TRUE`, the value of #CcBarChartGroup:selected-index is
188 : : * defined.
189 : : */
190 : 0 : props[PROP_SELECTED_INDEX_SET] =
191 : 0 : g_param_spec_boolean ("selected-index-set",
192 : : NULL, NULL,
193 : : FALSE,
194 : : G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
195 : :
196 : : /**
197 : : * CcBarChartGroup:scale:
198 : : *
199 : : * Scale used to render the bars in the group, in pixels per data unit.
200 : : *
201 : : * It must be greater than `0.0`.
202 : : *
203 : : * This is used internally and does not need to be set by code outside
204 : : * #CcBarChart.
205 : : */
206 : 0 : props[PROP_SCALE] =
207 : 0 : g_param_spec_double ("scale",
208 : : NULL, NULL,
209 : : -G_MAXDOUBLE, G_MAXDOUBLE, 1.0,
210 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
211 : :
212 : 0 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
213 : :
214 : 0 : gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/cc-bar-chart-group.ui");
215 : :
216 : 0 : gtk_widget_class_set_css_name (widget_class, "bar-group");
217 : 0 : gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GROUP);
218 : 0 : }
219 : :
220 : : static void
221 : 0 : cc_bar_chart_group_init (CcBarChartGroup *self)
222 : : {
223 : 0 : g_autoptr(GtkGestureClick) gesture = NULL;
224 : :
225 : 0 : gtk_widget_init_template (GTK_WIDGET (self));
226 : :
227 : 0 : self->selectable = TRUE;
228 : 0 : self->bars = g_ptr_array_new_null_terminated (1, (GDestroyNotify) gtk_widget_unparent, TRUE);
229 : :
230 : : /* Handle clicks */
231 : 0 : gesture = GTK_GESTURE_CLICK (gtk_gesture_click_new ());
232 : 0 : g_signal_connect (gesture, "pressed", G_CALLBACK (gesture_click_pressed_cb), self);
233 : 0 : gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (g_steal_pointer (&gesture)));
234 : 0 : }
235 : :
236 : : static void
237 : 0 : cc_bar_chart_group_get_property (GObject *object,
238 : : guint property_id,
239 : : GValue *value,
240 : : GParamSpec *pspec)
241 : : {
242 : 0 : CcBarChartGroup *self = CC_BAR_CHART_GROUP (object);
243 : :
244 [ # # # # : 0 : switch ((CcBarChartGroupProperty) property_id)
# # ]
245 : : {
246 : 0 : case PROP_SELECTABLE:
247 : 0 : g_value_set_boolean (value, cc_bar_chart_group_get_selectable (self));
248 : 0 : break;
249 : 0 : case PROP_IS_SELECTED:
250 : 0 : g_value_set_boolean (value, cc_bar_chart_group_get_is_selected (self));
251 : 0 : break;
252 : 0 : case PROP_SELECTED_INDEX: {
253 : : size_t idx;
254 : 0 : gboolean valid = cc_bar_chart_group_get_selected_index (self, &idx);
255 [ # # ]: 0 : g_value_set_uint (value, valid ? idx : 0);
256 : 0 : break;
257 : : }
258 : 0 : case PROP_SELECTED_INDEX_SET:
259 : 0 : g_value_set_boolean (value, cc_bar_chart_group_get_selected_index (self, NULL));
260 : 0 : break;
261 : 0 : case PROP_SCALE:
262 : 0 : g_value_set_double (value, cc_bar_chart_group_get_scale (self));
263 : 0 : break;
264 : 0 : default:
265 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
266 : 0 : break;
267 : : }
268 : 0 : }
269 : :
270 : : static void
271 : 0 : cc_bar_chart_group_set_property (GObject *object,
272 : : guint property_id,
273 : : const GValue *value,
274 : : GParamSpec *pspec)
275 : : {
276 : 0 : CcBarChartGroup *self = CC_BAR_CHART_GROUP (object);
277 : :
278 [ # # # # : 0 : switch ((CcBarChartGroupProperty) property_id)
# # ]
279 : : {
280 : 0 : case PROP_SELECTABLE:
281 : 0 : cc_bar_chart_group_set_selectable (self, g_value_get_boolean (value));
282 : 0 : break;
283 : 0 : case PROP_IS_SELECTED:
284 : 0 : cc_bar_chart_group_set_is_selected (self, g_value_get_boolean (value));
285 : 0 : break;
286 : 0 : case PROP_SELECTED_INDEX:
287 : 0 : cc_bar_chart_group_set_selected_index (self, TRUE, g_value_get_uint (value));
288 : 0 : break;
289 : 0 : case PROP_SELECTED_INDEX_SET:
290 : : /* Read only */
291 : : g_assert_not_reached ();
292 : : break;
293 : 0 : case PROP_SCALE:
294 : 0 : cc_bar_chart_group_set_scale (self, g_value_get_double (value));
295 : 0 : break;
296 : 0 : default:
297 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
298 : : }
299 : 0 : }
300 : :
301 : : static void
302 : 0 : cc_bar_chart_group_dispose (GObject *object)
303 : : {
304 : 0 : CcBarChartGroup *self = CC_BAR_CHART_GROUP (object);
305 : :
306 [ # # ]: 0 : g_clear_pointer (&self->bars, g_ptr_array_unref);
307 : 0 : gtk_widget_dispose_template (GTK_WIDGET (object), CC_TYPE_BAR_CHART_GROUP);
308 : :
309 : 0 : G_OBJECT_CLASS (cc_bar_chart_group_parent_class)->dispose (object);
310 : 0 : }
311 : :
312 : : static void
313 : 0 : cc_bar_chart_group_size_allocate (GtkWidget *widget,
314 : : int width,
315 : : int height,
316 : : int baseline)
317 : : {
318 : 0 : CcBarChartGroup *self = CC_BAR_CHART_GROUP (widget);
319 : :
320 [ # # ]: 0 : for (unsigned int i = 0; i < self->bars->len; i++)
321 : : {
322 : 0 : CcBarChartBar *bar = self->bars->pdata[i];
323 : : int bar_top_y, bar_bottom_y, bar_left_x, bar_right_x;
324 : : GtkAllocation child_alloc;
325 : :
326 : : /* If drawing RTL, reverse the bar positions. */
327 [ # # ]: 0 : if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
328 : 0 : bar = self->bars->pdata[self->bars->len - i - 1];
329 : :
330 : 0 : bar_left_x = width * i / self->bars->len;
331 : 0 : bar_right_x = width * (i + 1) / self->bars->len;
332 : :
333 : 0 : bar_top_y = height - cc_bar_chart_bar_get_value (bar) * self->scale;
334 : 0 : bar_bottom_y = height;
335 : :
336 : 0 : child_alloc.x = bar_left_x;
337 : 0 : child_alloc.y = bar_top_y;
338 : 0 : child_alloc.width = bar_right_x - bar_left_x;
339 : 0 : child_alloc.height = bar_bottom_y - bar_top_y;
340 : :
341 : 0 : gtk_widget_size_allocate (GTK_WIDGET (bar), &child_alloc, -1);
342 : : }
343 : 0 : }
344 : :
345 : : static void
346 : 0 : cc_bar_chart_group_measure (GtkWidget *widget,
347 : : GtkOrientation orientation,
348 : : int for_size,
349 : : int *minimum,
350 : : int *natural,
351 : : int *minimum_baseline,
352 : : int *natural_baseline)
353 : : {
354 : 0 : CcBarChartGroup *self = CC_BAR_CHART_GROUP (widget);
355 : :
356 [ # # ]: 0 : if (orientation == GTK_ORIENTATION_HORIZONTAL)
357 : : {
358 : 0 : int total_minimum_width = 0, total_natural_width = 0;
359 : :
360 [ # # ]: 0 : for (unsigned int i = 0; i < self->bars->len; i++)
361 : : {
362 : 0 : CcBarChartBar *bar = self->bars->pdata[i];
363 : 0 : int bar_minimum_width = 0, bar_natural_width = 0;
364 : :
365 : 0 : gtk_widget_measure (GTK_WIDGET (bar), orientation, -1,
366 : : &bar_minimum_width, &bar_natural_width,
367 : : NULL, NULL);
368 : :
369 : 0 : total_minimum_width += bar_minimum_width;
370 : 0 : total_natural_width += bar_natural_width;
371 : : }
372 : :
373 : 0 : *minimum = total_minimum_width;
374 : 0 : *natural = total_natural_width;
375 : 0 : *minimum_baseline = -1;
376 : 0 : *natural_baseline = -1;
377 : : }
378 [ # # ]: 0 : else if (orientation == GTK_ORIENTATION_VERTICAL)
379 : : {
380 : 0 : int maximum_minimum_height = 0, maximum_natural_height = 0;
381 : :
382 [ # # ]: 0 : for (unsigned int i = 0; i < self->bars->len; i++)
383 : : {
384 : 0 : CcBarChartBar *bar = self->bars->pdata[i];
385 : 0 : int bar_minimum_height = 0, bar_natural_height = 0;
386 : :
387 : 0 : gtk_widget_measure (GTK_WIDGET (bar), orientation, -1,
388 : : &bar_minimum_height, &bar_natural_height,
389 : : NULL, NULL);
390 : :
391 : 0 : maximum_minimum_height = MAX (maximum_minimum_height, bar_minimum_height);
392 : 0 : maximum_natural_height = MAX (maximum_natural_height, bar_natural_height);
393 : : }
394 : :
395 : 0 : *minimum = maximum_minimum_height;
396 : 0 : *natural = maximum_natural_height;
397 : 0 : *minimum_baseline = -1;
398 : 0 : *natural_baseline = -1;
399 : : }
400 : : else
401 : : {
402 : : g_assert_not_reached ();
403 : : }
404 : 0 : }
405 : :
406 : : static gboolean
407 : 0 : cc_bar_chart_group_focus (GtkWidget *widget,
408 : : GtkDirectionType direction)
409 : : {
410 : 0 : CcBarChartGroup *self = CC_BAR_CHART_GROUP (widget);
411 : : GtkWidget *focus_child;
412 : 0 : CcBarChartBar *next_focus_bar = NULL;
413 : :
414 : : /* Reverse the direction if in RTL mode, as the chart presents things on a
415 : : * left–right axis. */
416 [ # # ]: 0 : if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
417 : : {
418 [ # # # # : 0 : switch (direction)
# ]
419 : : {
420 : 0 : case GTK_DIR_TAB_BACKWARD:
421 : 0 : direction = GTK_DIR_TAB_FORWARD;
422 : 0 : break;
423 : 0 : case GTK_DIR_TAB_FORWARD:
424 : 0 : direction = GTK_DIR_TAB_BACKWARD;
425 : 0 : break;
426 : 0 : case GTK_DIR_LEFT:
427 : 0 : direction = GTK_DIR_RIGHT;
428 : 0 : break;
429 : 0 : case GTK_DIR_RIGHT:
430 : 0 : direction = GTK_DIR_LEFT;
431 : 0 : break;
432 : 0 : case GTK_DIR_UP:
433 : : case GTK_DIR_DOWN:
434 : : default:
435 : : /* No change. */
436 : 0 : break;
437 : : }
438 : : }
439 : :
440 : 0 : focus_child = gtk_widget_get_focus_child (widget);
441 : :
442 [ # # ]: 0 : if (focus_child != NULL)
443 : : {
444 : : /* Can the focus move around inside the currently focused child widget? */
445 [ # # ]: 0 : if (gtk_widget_child_focus (focus_child, direction))
446 : 0 : return TRUE;
447 : :
448 [ # # # # ]: 0 : if (CC_IS_BAR_CHART_BAR (focus_child) &&
449 [ # # ]: 0 : (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD))
450 : 0 : next_focus_bar = get_adjacent_focusable_bar (self, CC_BAR_CHART_BAR (focus_child), -1);
451 [ # # # # ]: 0 : else if (CC_IS_BAR_CHART_BAR (focus_child) &&
452 [ # # ]: 0 : (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD))
453 : 0 : next_focus_bar = get_adjacent_focusable_bar (self, CC_BAR_CHART_BAR (focus_child), 1);
454 : : }
455 : : else
456 : : {
457 : : /* No current focus bar. If a bar is selected, focus on that. Otherwise,
458 : : * focus on the first/last focusable bar, depending on which direction
459 : : * we’re coming in from. */
460 [ # # ]: 0 : if (self->selection_state == SELECTION_STATE_BAR)
461 : 0 : next_focus_bar = self->bars->pdata[self->selected_bar_index];
462 : :
463 [ # # # # ]: 0 : if (next_focus_bar == NULL &&
464 [ # # # # ]: 0 : (direction == GTK_DIR_UP || direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD))
465 : 0 : next_focus_bar = get_last_focusable_bar (self);
466 [ # # # # ]: 0 : else if (next_focus_bar == NULL &&
467 [ # # # # ]: 0 : (direction == GTK_DIR_DOWN || direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD))
468 : 0 : next_focus_bar = get_first_focusable_bar (self);
469 : : }
470 : :
471 [ # # ]: 0 : if (next_focus_bar == NULL)
472 : 0 : return FALSE;
473 : :
474 : 0 : return gtk_widget_child_focus (GTK_WIDGET (next_focus_bar), direction);
475 : : }
476 : :
477 : : static void
478 : 0 : gesture_click_pressed_cb (GtkGestureClick *gesture,
479 : : guint n_press,
480 : : double x,
481 : : double y,
482 : : gpointer user_data)
483 : : {
484 : 0 : CcBarChartGroup *self = CC_BAR_CHART_GROUP (user_data);
485 : : GtkWidget *bar;
486 : 0 : unsigned int bar_idx = 0;
487 : :
488 [ # # # # ]: 0 : if (gtk_widget_get_focus_on_click (GTK_WIDGET (self)) && !gtk_widget_has_focus (GTK_WIDGET (self)))
489 : 0 : gtk_widget_grab_focus (GTK_WIDGET (self));
490 : :
491 : 0 : bar = gtk_widget_pick (GTK_WIDGET (self), x, y, GTK_PICK_DEFAULT);
492 : :
493 [ # # # # ]: 0 : if (!CC_IS_BAR_CHART_BAR (bar) ||
494 : 0 : !find_index_for_bar (self, CC_BAR_CHART_BAR (bar), &bar_idx))
495 : 0 : bar = NULL;
496 : :
497 : : /* Select and focus the bar or group. */
498 [ # # ]: 0 : if (bar != NULL)
499 : : {
500 [ # # ]: 0 : if (bar_is_focusable (CC_BAR_CHART_BAR (bar)))
501 : 0 : gtk_widget_set_focus_child (GTK_WIDGET (self), bar);
502 : 0 : cc_bar_chart_group_set_selected_index (self, TRUE, bar_idx);
503 : : }
504 : : else
505 : : {
506 : : /* already grabbed focus above */
507 : 0 : cc_bar_chart_group_set_is_selected (self, TRUE);
508 : : }
509 : 0 : }
510 : :
511 : : static gboolean
512 : 0 : find_index_for_bar (CcBarChartGroup *self,
513 : : CcBarChartBar *bar,
514 : : unsigned int *out_idx)
515 : : {
516 : 0 : g_assert (gtk_widget_is_ancestor (GTK_WIDGET (bar), GTK_WIDGET (self)));
517 : 0 : g_assert (self->bars != NULL);
518 : :
519 : 0 : return g_ptr_array_find (self->bars, bar, out_idx);
520 : : }
521 : :
522 : : static gboolean
523 : 0 : bar_is_focusable (CcBarChartBar *bar)
524 : : {
525 : 0 : GtkWidget *widget = GTK_WIDGET (bar);
526 : :
527 [ # # ]: 0 : return (gtk_widget_is_visible (widget) &&
528 [ # # ]: 0 : gtk_widget_is_sensitive (widget) &&
529 [ # # # # ]: 0 : gtk_widget_get_focusable (widget) &&
530 : 0 : gtk_widget_get_can_focus (widget));
531 : : }
532 : :
533 : : /* direction == -1 means get previous sensitive and visible bar;
534 : : * direction == 1 means get next one. */
535 : : static CcBarChartBar *
536 : 0 : get_adjacent_focusable_bar (CcBarChartGroup *self,
537 : : CcBarChartBar *bar,
538 : : int direction)
539 : : {
540 : : unsigned int bar_idx, i;
541 : :
542 : 0 : g_assert (gtk_widget_is_ancestor (GTK_WIDGET (bar), GTK_WIDGET (self)));
543 : 0 : g_assert (self->bars != NULL);
544 : 0 : g_assert (direction == -1 || direction == 1);
545 : :
546 [ # # ]: 0 : if (!find_index_for_bar (self, bar, &bar_idx))
547 : 0 : return NULL;
548 : :
549 : 0 : i = bar_idx;
550 : :
551 [ # # # # : 0 : while (!((direction == -1 && i == 0) ||
# # ]
552 [ # # ]: 0 : (direction == 1 && i >= self->bars->len - 1)))
553 : : {
554 : : CcBarChartBar *adjacent_bar;
555 : :
556 : 0 : i += direction;
557 : 0 : adjacent_bar = self->bars->pdata[i];
558 : :
559 [ # # ]: 0 : if (bar_is_focusable (adjacent_bar))
560 : 0 : return adjacent_bar;
561 : : }
562 : :
563 : 0 : return NULL;
564 : : }
565 : :
566 : : static CcBarChartBar *
567 : 0 : get_first_focusable_bar (CcBarChartGroup *self)
568 : : {
569 : 0 : g_assert (self->bars != NULL);
570 : :
571 [ # # ]: 0 : for (unsigned int i = 0; i < self->bars->len; i++)
572 : : {
573 : 0 : CcBarChartBar *bar = self->bars->pdata[i];
574 : :
575 [ # # ]: 0 : if (bar_is_focusable (bar))
576 : 0 : return bar;
577 : : }
578 : :
579 : 0 : return NULL;
580 : : }
581 : :
582 : : static CcBarChartBar *
583 : 0 : get_last_focusable_bar (CcBarChartGroup *self)
584 : : {
585 : 0 : g_assert (self->bars != NULL);
586 : :
587 [ # # ]: 0 : for (unsigned int i = 0; i < self->bars->len; i++)
588 : : {
589 : 0 : CcBarChartBar *bar = self->bars->pdata[self->bars->len - 1 - i];
590 : :
591 [ # # ]: 0 : if (bar_is_focusable (bar))
592 : 0 : return bar;
593 : : }
594 : :
595 : 0 : return NULL;
596 : : }
597 : :
598 : : /**
599 : : * cc_bar_chart_group_new:
600 : : *
601 : : * Create a new #CcBarChartGroup.
602 : : *
603 : : * Returns: (transfer full): the new #CcBarChartGroup
604 : : */
605 : : CcBarChartGroup *
606 : 0 : cc_bar_chart_group_new (void)
607 : : {
608 : 0 : return g_object_new (CC_TYPE_BAR_CHART_GROUP, NULL);
609 : : }
610 : :
611 : : /**
612 : : * cc_bar_chart_group_get_bars:
613 : : * @self: a #CcBarChartGroup
614 : : * @out_n_bars: (out) (optional): return location for the number of bars,
615 : : * or `NULL` to ignore
616 : : *
617 : : * Get the bars in the group.
618 : : *
619 : : * If there are currently no bars in the group, `NULL` is returned and
620 : : * @out_n_bars is set to `0`.
621 : : *
622 : : * Returns: (array length=out_n_bars zero-terminated=1) (nullable) (transfer none): array of
623 : : * bars in the group, or `NULL` if empty
624 : : */
625 : : CcBarChartBar * const *
626 : 0 : cc_bar_chart_group_get_bars (CcBarChartGroup *self,
627 : : size_t *out_n_bars)
628 : : {
629 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART_GROUP (self), NULL);
630 : :
631 [ # # ]: 0 : if (out_n_bars != NULL)
632 : 0 : *out_n_bars = self->bars->len;
633 : :
634 [ # # ]: 0 : return (self->bars->len != 0) ? (CcBarChartBar * const *) self->bars->pdata : NULL;
635 : : }
636 : :
637 : : /**
638 : : * cc_bar_chart_group_insert_bar:
639 : : * @self: a #CcBarChartGroup
640 : : * @idx: position to insert the bar at, or `-1` to append
641 : : * @bar: (transfer none): bar to insert; will be sunk if floating
642 : : *
643 : : * Insert @bar into the group at index @idx.
644 : : *
645 : : * Pass `-1` to @idx to append the bar.
646 : : *
647 : : * @bar will be unparented from its existing parent (if set) first, so this
648 : : * method can be used to rearrange bars within the group.
649 : : */
650 : : void
651 : 0 : cc_bar_chart_group_insert_bar (CcBarChartGroup *self,
652 : : int idx,
653 : : CcBarChartBar *bar)
654 : : {
655 : : CcBarChartBar *previous_sibling;
656 : :
657 : 0 : g_return_if_fail (CC_IS_BAR_CHART_GROUP (self));
658 : 0 : g_return_if_fail (CC_IS_BAR_CHART_BAR (bar));
659 : :
660 : 0 : g_object_ref_sink (bar);
661 : :
662 : 0 : gtk_widget_unparent (GTK_WIDGET (bar));
663 : 0 : gtk_widget_set_parent (GTK_WIDGET (bar), GTK_WIDGET (self));
664 : :
665 [ # # # # ]: 0 : if (idx < 0 && self->bars->len > 0)
666 : 0 : previous_sibling = self->bars->pdata[self->bars->len - 1];
667 [ # # # # ]: 0 : else if (self->bars->len == 0 || idx == 0)
668 : 0 : previous_sibling = NULL;
669 : : else
670 : 0 : previous_sibling = self->bars->pdata[idx - 1];
671 : :
672 : 0 : gtk_widget_insert_after (GTK_WIDGET (bar), GTK_WIDGET (self), GTK_WIDGET (previous_sibling));
673 : :
674 : 0 : g_ptr_array_insert (self->bars, idx, bar);
675 : :
676 : 0 : g_object_unref (bar);
677 : :
678 : 0 : gtk_widget_queue_resize (GTK_WIDGET (self));
679 : : }
680 : :
681 : : /**
682 : : * cc_bar_chart_group_remove_bar:
683 : : * @self: a #CcBarChartGroup
684 : : * @bar: (transfer none): bar to remove
685 : : *
686 : : * Remove @bar from the group.
687 : : *
688 : : * It is an error to call this on a @bar which is not currently in the group.
689 : : */
690 : : void
691 : 0 : cc_bar_chart_group_remove_bar (CcBarChartGroup *self,
692 : : CcBarChartBar *bar)
693 : : {
694 : : gboolean was_removed;
695 : :
696 : 0 : g_return_if_fail (CC_IS_BAR_CHART_GROUP (self));
697 : 0 : g_return_if_fail (CC_IS_BAR_CHART_BAR (bar));
698 : :
699 : 0 : was_removed = g_ptr_array_remove (self->bars, bar);
700 : 0 : g_assert (was_removed);
701 : :
702 : 0 : gtk_widget_unparent (GTK_WIDGET (bar));
703 : :
704 : 0 : gtk_widget_queue_resize (GTK_WIDGET (self));
705 : : }
706 : :
707 : : /**
708 : : * cc_bar_chart_group_get_selectable:
709 : : * @self: a #CcBarChartGroup
710 : : *
711 : : * Get the value of #CcBarChartGroup:selectable.
712 : : *
713 : : * Returns: `TRUE` if the group itself is selectable, `FALSE` otherwise
714 : : */
715 : : gboolean
716 : 0 : cc_bar_chart_group_get_selectable (CcBarChartGroup *self)
717 : : {
718 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART_GROUP (self), FALSE);
719 : :
720 : 0 : return self->selectable;
721 : : }
722 : :
723 : : /**
724 : : * cc_bar_chart_group_set_selectable:
725 : : * @self: a #CcBarChartGroup
726 : : * @selectable: `TRUE` if the group itself is selectable, `FALSE` otherwise
727 : : *
728 : : * Set the value of #CcBarChartGroup:selectable.
729 : : */
730 : : void
731 : 0 : cc_bar_chart_group_set_selectable (CcBarChartGroup *self,
732 : : gboolean selectable)
733 : : {
734 : 0 : g_return_if_fail (CC_IS_BAR_CHART_GROUP (self));
735 : :
736 [ # # ]: 0 : if (self->selectable == selectable)
737 : 0 : return;
738 : :
739 : 0 : self->selectable = selectable;
740 : :
741 : 0 : g_object_freeze_notify (G_OBJECT (self));
742 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTABLE]);
743 : :
744 : : /* If the group is currently selected but shouldn’t be any more, run through
745 : : * the default fall-through selection logic again. */
746 [ # # # # ]: 0 : if (!self->selectable && self->selection_state == SELECTION_STATE_GROUP)
747 : 0 : cc_bar_chart_group_set_is_selected (self, TRUE);
748 : :
749 : 0 : g_object_thaw_notify (G_OBJECT (self));
750 : : }
751 : :
752 : : static void
753 : 0 : set_or_unset_selection_state_flags (CcBarChartGroup *self,
754 : : gboolean set)
755 : : {
756 : : GtkWidget *selected_widget;
757 : :
758 [ # # # ]: 0 : switch (self->selection_state)
759 : : {
760 : 0 : case SELECTION_STATE_BAR:
761 : 0 : selected_widget = GTK_WIDGET (self->bars->pdata[self->selected_bar_index]);
762 : 0 : break;
763 : 0 : case SELECTION_STATE_GROUP:
764 : 0 : selected_widget = GTK_WIDGET (self);
765 : 0 : break;
766 : 0 : case SELECTION_STATE_NONE:
767 : : default:
768 : 0 : selected_widget = NULL;
769 : 0 : break;
770 : : }
771 : :
772 [ # # ]: 0 : if (selected_widget != NULL)
773 : : {
774 [ # # ]: 0 : if (set)
775 : 0 : gtk_widget_set_state_flags (selected_widget, GTK_STATE_FLAG_SELECTED, FALSE);
776 : : else
777 : 0 : gtk_widget_unset_state_flags (selected_widget, GTK_STATE_FLAG_SELECTED);
778 : :
779 : 0 : gtk_accessible_update_state (GTK_ACCESSIBLE (selected_widget),
780 : : GTK_ACCESSIBLE_STATE_SELECTED, set,
781 : : -1);
782 : : }
783 : 0 : }
784 : :
785 : : /**
786 : : * cc_bar_chart_group_get_is_selected:
787 : : * @self: a #CcBarChartGroup
788 : : *
789 : : * Get the value of #CcBarChartGroup:is-selected.
790 : : *
791 : : * Returns: `TRUE` if the group is selected, `FALSE` otherwise
792 : : */
793 : : gboolean
794 : 0 : cc_bar_chart_group_get_is_selected (CcBarChartGroup *self)
795 : : {
796 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART_GROUP (self), FALSE);
797 : :
798 : 0 : return (self->selection_state == SELECTION_STATE_GROUP);
799 : : }
800 : :
801 : : /**
802 : : * cc_bar_chart_group_set_is_selected:
803 : : * @self: a #CcBarChartGroup
804 : : * @is_selected: `TRUE` if the group is selected, `FALSE` otherwise
805 : : *
806 : : * Set the value of #CcBarChartGroup:is-selected.
807 : : */
808 : : void
809 : 0 : cc_bar_chart_group_set_is_selected (CcBarChartGroup *self,
810 : : gboolean is_selected)
811 : : {
812 : 0 : g_return_if_fail (CC_IS_BAR_CHART_GROUP (self));
813 : :
814 : : /* If the group is not selectable, pass this through to the first bar. */
815 [ # # ]: 0 : if (!self->selectable)
816 : : {
817 [ # # ]: 0 : if (self->bars->len > 0)
818 : 0 : cc_bar_chart_group_set_selected_index (self, is_selected, 0);
819 : 0 : return;
820 : : }
821 : :
822 [ # # ]: 0 : if ((self->selection_state == SELECTION_STATE_GROUP) == is_selected)
823 : 0 : return;
824 : :
825 : : /* Update state and flags. */
826 : 0 : set_or_unset_selection_state_flags (self, FALSE);
827 : 0 : self->selection_state = is_selected ? SELECTION_STATE_GROUP : SELECTION_STATE_NONE;
828 : 0 : set_or_unset_selection_state_flags (self, TRUE);
829 : :
830 : : /* Re-render */
831 : 0 : gtk_widget_queue_draw (GTK_WIDGET (self));
832 : :
833 : 0 : g_object_freeze_notify (G_OBJECT (self));
834 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_SELECTED]);
835 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
836 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX_SET]);
837 : 0 : g_object_thaw_notify (G_OBJECT (self));
838 : : }
839 : :
840 : : /**
841 : : * cc_bar_chart_group_get_selected_index:
842 : : * @self: a #CcBarChartGroup
843 : : * @out_index: (out) (optional): return location for the selected index, or
844 : : * `NULL` to ignore
845 : : *
846 : : * Get the currently selected bar index.
847 : : *
848 : : * If no bar is currently selected, or if the group as a whole is selected
849 : : * (see cc_bar_chart_group_get_is_selected()), @out_index will be set to `0` and
850 : : * `FALSE` will be returned.
851 : : *
852 : : * Returns: `TRUE` if a bar is currently selected, `FALSE` otherwise
853 : : */
854 : : gboolean
855 : 0 : cc_bar_chart_group_get_selected_index (CcBarChartGroup *self,
856 : : size_t *out_index)
857 : : {
858 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART_GROUP (self), FALSE);
859 : :
860 [ # # ]: 0 : if (out_index != NULL)
861 [ # # ]: 0 : *out_index = (self->selection_state == SELECTION_STATE_BAR) ? self->selected_bar_index : 0;
862 : :
863 : 0 : return (self->selection_state == SELECTION_STATE_BAR);
864 : : }
865 : :
866 : : /**
867 : : * cc_bar_chart_group_set_selected_index:
868 : : * @self: a #CcBarChartGroup
869 : : * @is_selected: `TRUE` if a bar should be selected, `FALSE` if everything
870 : : * should be unselected
871 : : * @idx: index of the data to select, ignored if @is_selected is `FALSE`
872 : : *
873 : : * Set the currently selected bar index, or unselect everything.
874 : : *
875 : : * If @is_selected is `TRUE`, the bar at @idx will be selected. If @is_selected
876 : : * is `FALSE`, @idx will be ignored and all bars (and the group itself) will be
877 : : * unselected.
878 : : */
879 : : void
880 : 0 : cc_bar_chart_group_set_selected_index (CcBarChartGroup *self,
881 : : gboolean is_selected,
882 : : size_t idx)
883 : : {
884 : 0 : g_return_if_fail (CC_IS_BAR_CHART_GROUP (self));
885 : 0 : g_return_if_fail (!is_selected || idx < self->bars->len);
886 : :
887 [ # # # # : 0 : if ((is_selected && self->selection_state == SELECTION_STATE_BAR && self->selected_bar_index == idx) ||
# # # # ]
888 [ # # ]: 0 : (!is_selected && self->selection_state == SELECTION_STATE_NONE))
889 : 0 : return;
890 : :
891 : : /* Update state and flags. */
892 : 0 : set_or_unset_selection_state_flags (self, FALSE);
893 : :
894 [ # # ]: 0 : self->selection_state = is_selected ? SELECTION_STATE_BAR : SELECTION_STATE_NONE;
895 [ # # ]: 0 : self->selected_bar_index = is_selected ? idx : 0;
896 : :
897 : 0 : set_or_unset_selection_state_flags (self, TRUE);
898 : :
899 : : /* Re-render */
900 : 0 : gtk_widget_queue_draw (GTK_WIDGET (self));
901 : :
902 : 0 : g_object_freeze_notify (G_OBJECT (self));
903 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_SELECTED]);
904 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
905 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX_SET]);
906 : 0 : g_object_thaw_notify (G_OBJECT (self));
907 : : }
908 : :
909 : : /**
910 : : * cc_bar_chart_group_get_scale:
911 : : * @self: a #CcBarChartGroup
912 : : *
913 : : * Get the value of #CcBarChartGroup:scale.
914 : : *
915 : : * Returns: pixels per data unit to render the bars with
916 : : */
917 : : double
918 : 0 : cc_bar_chart_group_get_scale (CcBarChartGroup *self)
919 : : {
920 : 0 : g_return_val_if_fail (CC_IS_BAR_CHART_GROUP (self), NAN);
921 : :
922 : 0 : return self->scale;
923 : : }
924 : :
925 : : /**
926 : : * cc_bar_chart_group_set_scale:
927 : : * @self: a #CcBarChartGroup
928 : : * @scale: pixels per data unit to render the bars with
929 : : *
930 : : * Set the value of #CcBarChartGroup:scale.
931 : : */
932 : : void
933 : 0 : cc_bar_chart_group_set_scale (CcBarChartGroup *self,
934 : : double scale)
935 : : {
936 : 0 : g_return_if_fail (CC_IS_BAR_CHART_GROUP (self));
937 : 0 : g_return_if_fail (scale > 0.0);
938 : :
939 [ # # ]: 0 : if (scale == self->scale)
940 : 0 : return;
941 : :
942 : 0 : self->scale = scale;
943 : :
944 : : /* Re-render */
945 : 0 : gtk_widget_queue_allocate (GTK_WIDGET (self));
946 : :
947 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SCALE]);
948 : : }
|