Branch data Line data Source code
1 : : /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
2 : : /* cc-timelike-editor-layout.c
3 : : *
4 : : * Copyright 2025 GNOME Foundation, Inc.
5 : : *
6 : : * This program is free software: you can redistribute it and/or modify
7 : : * it under the terms of the GNU General Public License as published by
8 : : * the Free Software Foundation, either version 2 of the License, or
9 : : * (at your option) any later version.
10 : : *
11 : : * This program is distributed in the hope that it will be useful,
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : : * GNU General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU General Public License
17 : : * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Author(s):
20 : : * Philip Withnall <pwithnall@gnome.org>
21 : : *
22 : : * SPDX-License-Identifier: GPL-3.0-or-later
23 : : */
24 : :
25 : : #include <gtk/gtk.h>
26 : :
27 : : #include "cc-timelike-editor-layout.h"
28 : : #include "cc-timelike-entry.h"
29 : :
30 : : /**
31 : : * CcTimelikeEditorLayout:
32 : : *
33 : : * A #GtkLayoutManager for the child widgets inside #CcTimelikeEditor.
34 : : *
35 : : * Although #CcTimelikeEditor’s child widgets are seemingly layed out on a grid
36 : : * pattern, a custom layout manager is needed to align the up/down buttons with
37 : : * the relevant parts of the #CcTimelikeEntry’s text.
38 : : *
39 : : * This is done by calling cc_timelike_entry_get_hours_and_minutes_midpoints()
40 : : * to get the X coordinates of the midpoints of the hours and minutes parts of
41 : : * the entry’s text, and aligning the midpoint of the up/down buttons with
42 : : * those.
43 : : *
44 : : * The rest of the widget layout is fairly basic, and allocates any additional
45 : : * space to the #CcTimelikeEntry child widget.
46 : : *
47 : : * As this layout manager is specific to #CcTimelikeEditor, it is not very
48 : : * generalised, and will probably need to be modified if #CcTimelikeEditor (or
49 : : * its default CSS) is modified.
50 : : *
51 : : * In particular, the layout manager doesn’t support RTL (because
52 : : * #CcTimelikeEditor doesn’t support that, because times are always LTR); and
53 : : * the layout manager hard-codes the order of the child widgets. It expects 5
54 : : * child buttons, in the order:
55 : : * - 0: hours up
56 : : * - 1: minutes up
57 : : * - 2: hours down
58 : : * - 3: minutes down
59 : : * - 4: AM/PM button (may be hidden)
60 : : *
61 : : * A more generalised implementation of the layout manager would add a type
62 : : * derived from #GtkLayoutChild which allowed specifying which button is which.
63 : : */
64 : :
65 : : #define ROW_SPACING_DEFAULT 6
66 : : #define COLUMN_SPACING_DEFAULT 6
67 : :
68 : : struct _CcTimelikeEditorLayout {
69 : : GtkLayoutManager parent_instance;
70 : :
71 : : unsigned int row_spacing;
72 : : unsigned int column_spacing;
73 : : };
74 : :
75 [ # # # # : 0 : G_DEFINE_TYPE (CcTimelikeEditorLayout, cc_timelike_editor_layout, GTK_TYPE_LAYOUT_MANAGER)
# # ]
76 : :
77 : : typedef enum {
78 : : PROP_ROW_SPACING = 1,
79 : : PROP_COLUMN_SPACING,
80 : : } CcTimelikeEditorLayoutProperty;
81 : :
82 : : static GParamSpec *props[PROP_COLUMN_SPACING + 1];
83 : :
84 : : static void
85 : 0 : cc_timelike_editor_layout_get_property (GObject *object,
86 : : guint property_id,
87 : : GValue *value,
88 : : GParamSpec *pspec)
89 : : {
90 : 0 : CcTimelikeEditorLayout *self = CC_TIMELIKE_EDITOR_LAYOUT (object);
91 : :
92 [ # # # ]: 0 : switch ((CcTimelikeEditorLayoutProperty) property_id)
93 : : {
94 : 0 : case PROP_ROW_SPACING:
95 : 0 : g_value_set_uint (value, cc_timelike_editor_layout_get_row_spacing (self));
96 : 0 : break;
97 : 0 : case PROP_COLUMN_SPACING:
98 : 0 : g_value_set_uint (value, cc_timelike_editor_layout_get_column_spacing (self));
99 : 0 : break;
100 : 0 : default:
101 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
102 : 0 : break;
103 : : }
104 : 0 : }
105 : :
106 : : static void
107 : 0 : cc_timelike_editor_layout_set_property (GObject *object,
108 : : guint property_id,
109 : : const GValue *value,
110 : : GParamSpec *pspec)
111 : : {
112 : 0 : CcTimelikeEditorLayout *self = CC_TIMELIKE_EDITOR_LAYOUT (object);
113 : :
114 [ # # # ]: 0 : switch ((CcTimelikeEditorLayoutProperty) property_id)
115 : : {
116 : 0 : case PROP_ROW_SPACING:
117 : 0 : cc_timelike_editor_layout_set_row_spacing (self, g_value_get_uint (value));
118 : 0 : break;
119 : 0 : case PROP_COLUMN_SPACING:
120 : 0 : cc_timelike_editor_layout_set_column_spacing (self, g_value_get_uint (value));
121 : 0 : break;
122 : 0 : default:
123 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
124 : 0 : break;
125 : : }
126 : 0 : }
127 : :
128 : : static void
129 : 0 : cc_timelike_editor_layout_measure (GtkLayoutManager *layout_manager,
130 : : GtkWidget *widget,
131 : : GtkOrientation orientation,
132 : : int for_size,
133 : : int *out_minimum,
134 : : int *out_natural,
135 : : int *out_minimum_baseline,
136 : : int *out_natural_baseline)
137 : : {
138 : 0 : CcTimelikeEditorLayout *self = CC_TIMELIKE_EDITOR_LAYOUT (layout_manager);
139 : : GtkWidget *child;
140 : 0 : int max_up_down_button_minimum = 0, max_up_down_button_natural = 0;
141 : 0 : int am_pm_button_minimum = 0, am_pm_button_natural = 0;
142 : 0 : int entry_minimum = 0, entry_natural = 0;
143 : : unsigned int button_pos;
144 : :
145 : 0 : for (child = gtk_widget_get_first_child (widget), button_pos = 0;
146 [ # # ]: 0 : child != NULL;
147 : 0 : child = gtk_widget_get_next_sibling (child))
148 : : {
149 : 0 : int child_minimum = 0, child_natural = 0;
150 : :
151 [ # # ]: 0 : if (!gtk_widget_should_layout (child))
152 : 0 : continue;
153 : :
154 : 0 : gtk_widget_measure (child, orientation, for_size,
155 : : &child_minimum, &child_natural,
156 : : NULL, NULL);
157 : :
158 [ # # # # : 0 : if (GTK_IS_BUTTON (child) && button_pos < 4)
# # # # #
# ]
159 : : {
160 : : /* Up/Down buttons */
161 : 0 : max_up_down_button_minimum = MAX (max_up_down_button_minimum, child_minimum);
162 : 0 : max_up_down_button_natural = MAX (max_up_down_button_natural, child_natural);
163 : 0 : button_pos++;
164 : : }
165 [ # # # # : 0 : else if (GTK_IS_BUTTON (child) && button_pos == 4)
# # # # #
# ]
166 : : {
167 : : /* AM/PM button; may not be visible, in which case these will default to 0. */
168 : 0 : am_pm_button_minimum = child_minimum;
169 : 0 : am_pm_button_natural = child_natural;
170 : 0 : button_pos++;
171 : : }
172 [ # # ]: 0 : else if (CC_IS_TIMELIKE_ENTRY (child))
173 : : {
174 : 0 : entry_minimum = child_minimum;
175 : 0 : entry_natural = child_natural;
176 : : }
177 : : else
178 : : {
179 : : g_assert_not_reached ();
180 : : }
181 : : }
182 : :
183 [ # # ]: 0 : if (orientation == GTK_ORIENTATION_HORIZONTAL)
184 : : {
185 [ # # ]: 0 : *out_minimum = MAX (2 * max_up_down_button_minimum, entry_minimum) + ((am_pm_button_minimum > 0) ? am_pm_button_minimum + self->column_spacing : 0);
186 [ # # ]: 0 : *out_natural = MAX (2 * max_up_down_button_natural, entry_natural) + ((am_pm_button_natural > 0) ? am_pm_button_natural + self->column_spacing : 0);
187 : : }
188 [ # # ]: 0 : else if (orientation == GTK_ORIENTATION_VERTICAL)
189 : : {
190 : 0 : *out_minimum = 2 * max_up_down_button_minimum + 2 * self->row_spacing + MAX (entry_minimum, am_pm_button_minimum);
191 : 0 : *out_natural = 2 * max_up_down_button_natural + 2 * self->row_spacing + MAX (entry_natural, am_pm_button_natural);
192 : : }
193 : : else
194 : : {
195 : : g_assert_not_reached ();
196 : : }
197 : :
198 : 0 : *out_minimum_baseline = -1;
199 : 0 : *out_natural_baseline = -1;
200 : 0 : }
201 : :
202 : : static void
203 : 0 : cc_timelike_editor_layout_allocate (GtkLayoutManager *layout_manager,
204 : : GtkWidget *widget,
205 : : int width,
206 : : int height,
207 : : int baseline)
208 : : {
209 : 0 : CcTimelikeEditorLayout *self = CC_TIMELIKE_EDITOR_LAYOUT (layout_manager);
210 : : GtkWidget *child;
211 : 0 : CcTimelikeEntry *entry = NULL;
212 : 0 : int max_up_down_button_minimum = 0, max_up_down_button_natural = 0;
213 : 0 : int entry_width_minimum = 0, entry_width_natural = 0;
214 : 0 : int am_pm_button_width_minimum = 0, am_pm_button_width_natural = 0;
215 : 0 : graphene_point_t hours_midpoint = { 0, 0 }, minutes_midpoint = { 0, 0 };
216 : : graphene_point_t hours_midpoint_self, minutes_midpoint_self;
217 : : int entry_extra_width, entry_height;
218 : : int up_down_button_size;
219 : : int am_pm_button_width;
220 : : unsigned int button_pos;
221 : :
222 : : /* Get the up/down button sizes. Take the largest from both dimensions of all
223 : : * buttons, to make them equal and square. */
224 : 0 : for (child = gtk_widget_get_first_child (widget), button_pos = 0;
225 [ # # ]: 0 : child != NULL;
226 : 0 : child = gtk_widget_get_next_sibling (child))
227 : : {
228 : : GtkOrientation orientation;
229 : 0 : int child_minimum = 0, child_natural = 0;
230 : :
231 [ # # ]: 0 : if (!gtk_widget_should_layout (child) ||
232 [ # # # # : 0 : !GTK_IS_BUTTON (child))
# # # # ]
233 : 0 : continue;
234 : :
235 [ # # ]: 0 : if (button_pos < 4)
236 : : {
237 : 0 : for (orientation = GTK_ORIENTATION_HORIZONTAL;
238 [ # # ]: 0 : orientation <= GTK_ORIENTATION_VERTICAL;
239 : 0 : orientation++)
240 : : {
241 : 0 : gtk_widget_measure (child, orientation, -1,
242 : : &child_minimum, &child_natural,
243 : : NULL, NULL);
244 : :
245 : 0 : max_up_down_button_minimum = MAX (max_up_down_button_minimum, child_minimum);
246 : 0 : max_up_down_button_natural = MAX (max_up_down_button_natural, child_natural);
247 : : }
248 : : }
249 [ # # ]: 0 : else if (button_pos == 4)
250 : : {
251 : 0 : gtk_widget_measure (child, GTK_ORIENTATION_HORIZONTAL, -1,
252 : : &am_pm_button_width_minimum, &am_pm_button_width_natural,
253 : : NULL, NULL);
254 : : }
255 : : else
256 : : {
257 : : g_assert_not_reached ();
258 : : }
259 : :
260 : 0 : button_pos++;
261 : : }
262 : :
263 : : /* We don’t support these buttons growing; only the entry and AM/PM button can grow. */
264 : 0 : g_assert (max_up_down_button_minimum == max_up_down_button_natural);
265 : 0 : up_down_button_size = max_up_down_button_minimum;
266 : 0 : g_assert (up_down_button_size > 0);
267 : :
268 : 0 : g_assert (am_pm_button_width_minimum == am_pm_button_width_natural);
269 : 0 : am_pm_button_width = am_pm_button_width_minimum;
270 : :
271 : : /* Allocate the entry first, so we can get the offsets from it for the buttons. */
272 : 0 : for (child = gtk_widget_get_first_child (widget);
273 [ # # ]: 0 : child != NULL;
274 : 0 : child = gtk_widget_get_next_sibling (child))
275 : : {
276 [ # # ]: 0 : if (!gtk_widget_should_layout (child))
277 : 0 : continue;
278 : :
279 [ # # ]: 0 : if (CC_IS_TIMELIKE_ENTRY (child))
280 : : {
281 : : GtkAllocation child_allocation;
282 : 0 : int child_minimum = 0, child_natural = 0;
283 : : gboolean success;
284 : :
285 : 0 : entry = CC_TIMELIKE_ENTRY (child);
286 : :
287 : 0 : gtk_widget_measure (child, GTK_ORIENTATION_HORIZONTAL, -1,
288 : : &entry_width_minimum, &entry_width_natural,
289 : : NULL, NULL);
290 : 0 : gtk_widget_measure (child, GTK_ORIENTATION_VERTICAL, width,
291 : : &child_minimum, &child_natural,
292 : : NULL, NULL);
293 : :
294 [ # # ]: 0 : child_allocation.width = width - ((am_pm_button_width > 0) ? self->column_spacing + am_pm_button_width : 0);
295 [ # # ]: 0 : child_allocation.height = CLAMP (height - 2 * (int) self->row_spacing - 2 * up_down_button_size, child_minimum, child_natural);
296 : 0 : child_allocation.x = 0;
297 : 0 : child_allocation.y = up_down_button_size + self->row_spacing;
298 : :
299 : 0 : gtk_widget_size_allocate (child, &child_allocation, -1);
300 : :
301 : 0 : cc_timelike_entry_get_hours_and_minutes_midpoints (entry,
302 : : &hours_midpoint.x,
303 : : &minutes_midpoint.x);
304 : :
305 : 0 : success = gtk_widget_compute_point (child, widget,
306 : : &hours_midpoint, &hours_midpoint_self);
307 : 0 : g_assert (success);
308 : :
309 : 0 : success = gtk_widget_compute_point (child, widget,
310 : : &minutes_midpoint, &minutes_midpoint_self);
311 : 0 : g_assert (success);
312 : :
313 [ # # ]: 0 : entry_extra_width = (child_allocation.width >= entry_width_natural) ? child_allocation.width - entry_width_natural : 0;
314 : 0 : entry_height = child_allocation.height;
315 : :
316 : 0 : break;
317 : : }
318 : : }
319 : :
320 : 0 : g_assert (entry != NULL);
321 : :
322 : : /* Now allocate the up/down buttons. button_pos counts the button position:
323 : : * - 0: hours up
324 : : * - 1: minutes up
325 : : * - 2: hours down
326 : : * - 3: minutes down
327 : : * - 4: AM/PM button (may be hidden)
328 : : */
329 : 0 : for (child = gtk_widget_get_first_child (widget), button_pos = 0;
330 [ # # ]: 0 : child != NULL;
331 : 0 : child = gtk_widget_get_next_sibling (child))
332 : : {
333 [ # # ]: 0 : if (!gtk_widget_should_layout (child) ||
334 [ # # # # : 0 : !GTK_IS_BUTTON (child))
# # # # ]
335 : 0 : continue;
336 : :
337 [ # # ]: 0 : if (button_pos < 4)
338 : : {
339 : : GtkAllocation child_allocation;
340 : 0 : int child_minimum = 0, child_natural = 0;
341 : 0 : gboolean is_hours = (button_pos % 2 == 0);
342 : 0 : gboolean is_up = (button_pos < 2);
343 : :
344 : 0 : gtk_widget_measure (child, GTK_ORIENTATION_VERTICAL, width,
345 : : &child_minimum, &child_natural,
346 : : NULL, NULL);
347 : :
348 : 0 : child_allocation.width = up_down_button_size;
349 : 0 : child_allocation.height = up_down_button_size;
350 [ # # ]: 0 : child_allocation.x = entry_extra_width / 2 + (is_hours ? hours_midpoint_self.x : minutes_midpoint_self.x) - up_down_button_size / 2;
351 [ # # ]: 0 : child_allocation.y = is_up ? 0 : up_down_button_size + 2 * self->row_spacing + entry_height;
352 : :
353 : 0 : gtk_widget_size_allocate (child, &child_allocation, -1);
354 : : }
355 [ # # ]: 0 : else if (button_pos == 4)
356 : : {
357 : : GtkAllocation child_allocation;
358 : :
359 : 0 : child_allocation.width = am_pm_button_width;
360 : 0 : child_allocation.height = entry_height;
361 : 0 : child_allocation.x = width - am_pm_button_width;
362 : 0 : child_allocation.y = up_down_button_size + self->row_spacing;
363 : :
364 : 0 : gtk_widget_size_allocate (child, &child_allocation, -1);
365 : : }
366 : : else
367 : : {
368 : : g_assert_not_reached ();
369 : : }
370 : :
371 : 0 : button_pos++;
372 : : }
373 : 0 : }
374 : :
375 : : static void
376 : 0 : cc_timelike_editor_layout_class_init (CcTimelikeEditorLayoutClass *klass)
377 : : {
378 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
379 : 0 : GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass);
380 : :
381 : 0 : object_class->get_property = cc_timelike_editor_layout_get_property;
382 : 0 : object_class->set_property = cc_timelike_editor_layout_set_property;
383 : :
384 : 0 : layout_manager_class->measure = cc_timelike_editor_layout_measure;
385 : 0 : layout_manager_class->allocate = cc_timelike_editor_layout_allocate;
386 : :
387 : : /**
388 : : * CcTimelikeEditorLayout:row-spacing:
389 : : *
390 : : * Spacing between the rows, in logical pixels.
391 : : *
392 : : * This is the spacing between the row of ‘up’ buttons and the timelike entry,
393 : : * and the timelike entry and the row of ‘down’ buttons.
394 : : */
395 : 0 : props[PROP_ROW_SPACING] =
396 : 0 : g_param_spec_uint ("row-spacing",
397 : : NULL, NULL,
398 : : 0, G_MAXUINT, 6,
399 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
400 : :
401 : : /**
402 : : * CcTimelikeEditorLayout:column-spacing:
403 : : *
404 : : * Spacing between the columns, in logical pixels.
405 : : *
406 : : * This is the spacing between the timelike entry, and the AM/PM button (if
407 : : * the latter is visible).
408 : : */
409 : 0 : props[PROP_COLUMN_SPACING] =
410 : 0 : g_param_spec_uint ("column-spacing",
411 : : NULL, NULL,
412 : : 0, G_MAXUINT, COLUMN_SPACING_DEFAULT,
413 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
414 : :
415 : 0 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
416 : 0 : }
417 : :
418 : : static void
419 : 0 : cc_timelike_editor_layout_init (CcTimelikeEditorLayout *self)
420 : : {
421 : : /* Default spacing. */
422 : 0 : self->row_spacing = ROW_SPACING_DEFAULT;
423 : 0 : self->column_spacing = COLUMN_SPACING_DEFAULT;
424 : 0 : }
425 : :
426 : : /**
427 : : * cc_timelike_editor_layout_new:
428 : : *
429 : : * Create a new #CcTimelikeEditorLayout.
430 : : *
431 : : * Returns: (transfer full): a new #CcTimelikeEditorLayout
432 : : */
433 : : CcTimelikeEditorLayout *
434 : 0 : cc_timelike_editor_layout_new (void)
435 : : {
436 : 0 : return g_object_new (CC_TYPE_TIMELIKE_EDITOR_LAYOUT, NULL);
437 : : }
438 : :
439 : : /**
440 : : * cc_timelike_editor_layout_get_row_spacing:
441 : : * @self: a #CcTimelikeEditorLayout
442 : : *
443 : : * Get the value of #CcTimelikeEditorLayout:row-spacing.
444 : : *
445 : : * Returns: row spacing, in logical pixels
446 : : */
447 : : unsigned int
448 : 0 : cc_timelike_editor_layout_get_row_spacing (CcTimelikeEditorLayout *self)
449 : : {
450 : 0 : g_return_val_if_fail (CC_IS_TIMELIKE_EDITOR_LAYOUT (self), 0);
451 : :
452 : 0 : return self->row_spacing;
453 : : }
454 : :
455 : : /**
456 : : * cc_timelike_editor_layout_set_row_spacing:
457 : : * @self: a #CcTimelikeEditorLayout
458 : : * @row_spacing: row spacing, in logical pixels
459 : : *
460 : : * Set the value of #CcTimelikeEditorLayout:row-spacing.
461 : : */
462 : : void
463 : 0 : cc_timelike_editor_layout_set_row_spacing (CcTimelikeEditorLayout *self,
464 : : unsigned int row_spacing)
465 : : {
466 : 0 : g_return_if_fail (CC_IS_TIMELIKE_EDITOR_LAYOUT (self));
467 : :
468 [ # # ]: 0 : if (self->row_spacing == row_spacing)
469 : 0 : return;
470 : :
471 : 0 : self->row_spacing = row_spacing;
472 : 0 : gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
473 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ROW_SPACING]);
474 : : }
475 : :
476 : : /**
477 : : * cc_timelike_editor_layout_get_column_spacing:
478 : : * @self: a #CcTimelikeEditorLayout
479 : : *
480 : : * Get the value of #CcTimelikeEditorLayout:column-spacing.
481 : : *
482 : : * Returns: column spacing, in logical pixels
483 : : */
484 : : unsigned int
485 : 0 : cc_timelike_editor_layout_get_column_spacing (CcTimelikeEditorLayout *self)
486 : : {
487 : 0 : g_return_val_if_fail (CC_IS_TIMELIKE_EDITOR_LAYOUT (self), 0);
488 : :
489 : 0 : return self->column_spacing;
490 : : }
491 : :
492 : : /**
493 : : * cc_timelike_editor_layout_set_column_spacing:
494 : : * @self: a #CcTimelikeEditorLayout
495 : : * @column_spacing: column spacing, in logical pixels
496 : : *
497 : : * Set the value of #CcTimelikeEditorLayout:column-spacing.
498 : : */
499 : : void
500 : 0 : cc_timelike_editor_layout_set_column_spacing (CcTimelikeEditorLayout *self,
501 : : unsigned int column_spacing)
502 : : {
503 : 0 : g_return_if_fail (CC_IS_TIMELIKE_EDITOR_LAYOUT (self));
504 : :
505 [ # # ]: 0 : if (self->column_spacing == column_spacing)
506 : 0 : return;
507 : :
508 : 0 : self->column_spacing = column_spacing;
509 : 0 : gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
510 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COLUMN_SPACING]);
511 : : }
|