Branch data Line data Source code
1 : : /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
2 : : /* cc-timelike-entry.c
3 : : *
4 : : * Copyright 2020 Purism SPC
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 : : * Mohammed Sadiq <sadiq@sadiqpk.org>
21 : : *
22 : : * SPDX-License-Identifier: GPL-3.0-or-later
23 : : */
24 : :
25 : : #undef G_LOG_DOMAIN
26 : : #define G_LOG_DOMAIN "cc-timelike-entry"
27 : :
28 : : #ifdef HAVE_CONFIG_H
29 : : # include "config.h"
30 : : #endif
31 : :
32 : : #include <gtk/gtk.h>
33 : : #include <glib/gi18n.h>
34 : :
35 : : #include "cc-timelike-entry.h"
36 : :
37 : : #define SEPARATOR_INDEX 2
38 : : #define END_INDEX 4
39 : : #define EMIT_CHANGED_TIMEOUT 100
40 : :
41 : :
42 : : struct _CcTimelikeEntry
43 : : {
44 : : GtkWidget parent_instance;
45 : :
46 : : GtkWidget *text;
47 : :
48 : : guint insert_text_id;
49 : : guint time_changed_id;
50 : : int hour; /* Range: 0-23 in 24H and 1-12 in 12H with is_am set/unset */
51 : : int minute;
52 : : gboolean is_am_pm;
53 : : gboolean is_am; /* AM if TRUE. PM if FALSE. valid iff is_am_pm set */
54 : : guint minute_increment;
55 : : };
56 : :
57 : :
58 : : static void editable_insert_text_cb (GtkText *text,
59 : : char *new_text,
60 : : gint new_text_length,
61 : : gint *position,
62 : : CcTimelikeEntry *self);
63 : :
64 : : static void gtk_editable_interface_init (GtkEditableInterface *iface);
65 : :
66 [ # # # # : 0 : G_DEFINE_TYPE_WITH_CODE (CcTimelikeEntry, cc_timelike_entry, GTK_TYPE_WIDGET,
# # ]
67 : : G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, gtk_editable_interface_init));
68 : :
69 : : typedef enum {
70 : : PROP_MINUTE_INCREMENT = 1,
71 : : } CcTimelikeEntryProperty;
72 : :
73 : : static GParamSpec *props[PROP_MINUTE_INCREMENT + 1];
74 : :
75 : : enum {
76 : : CHANGE_VALUE,
77 : : TIME_CHANGED,
78 : : N_SIGNALS
79 : : };
80 : :
81 : : static guint signals[N_SIGNALS];
82 : :
83 : : static gboolean
84 : 0 : emit_time_changed (CcTimelikeEntry *self)
85 : : {
86 : 0 : self->time_changed_id = 0;
87 : :
88 : 0 : g_signal_emit (self, signals[TIME_CHANGED], 0);
89 : :
90 : 0 : return G_SOURCE_REMOVE;
91 : : }
92 : :
93 : : static void
94 : 0 : timelike_entry_fill_time (CcTimelikeEntry *self)
95 : : {
96 : 0 : g_autofree gchar *str = NULL;
97 : :
98 : 0 : g_assert (CC_IS_TIMELIKE_ENTRY (self));
99 : :
100 : 0 : str = g_strdup_printf ("%02d∶%02d", self->hour, self->minute);
101 : :
102 : 0 : g_signal_handlers_block_by_func (self->text, editable_insert_text_cb, self);
103 : 0 : gtk_editable_set_text (GTK_EDITABLE (self->text), str);
104 : 0 : g_signal_handlers_unblock_by_func (self->text, editable_insert_text_cb, self);
105 : 0 : }
106 : :
107 : : static void
108 : 0 : cursor_position_changed_cb (CcTimelikeEntry *self)
109 : : {
110 : : int current_pos;
111 : :
112 : 0 : g_assert (CC_IS_TIMELIKE_ENTRY (self));
113 : :
114 : 0 : current_pos = gtk_editable_get_position (GTK_EDITABLE (self));
115 : :
116 : 0 : g_signal_handlers_block_by_func (self->text, cursor_position_changed_cb, self);
117 : :
118 : : /* If cursor is on ‘:’ move to the next field */
119 [ # # ]: 0 : if (current_pos == SEPARATOR_INDEX)
120 : 0 : gtk_editable_set_position (GTK_EDITABLE (self->text), current_pos + 1);
121 : :
122 : : /* If cursor is after the last digit and without selection, move to last digit */
123 [ # # # # ]: 0 : if (current_pos > END_INDEX &&
124 : 0 : !gtk_editable_get_selection_bounds (GTK_EDITABLE (self->text), NULL, NULL))
125 : 0 : gtk_editable_set_position (GTK_EDITABLE (self->text), END_INDEX);
126 : :
127 : 0 : g_signal_handlers_unblock_by_func (self->text, cursor_position_changed_cb, self);
128 : 0 : }
129 : :
130 : : static void
131 : 0 : entry_selection_changed_cb (CcTimelikeEntry *self)
132 : : {
133 : : GtkEditable *editable;
134 : :
135 : 0 : g_assert (CC_IS_TIMELIKE_ENTRY (self));
136 : :
137 : 0 : editable = GTK_EDITABLE (self->text);
138 : :
139 : 0 : g_signal_handlers_block_by_func (self->text, cursor_position_changed_cb, self);
140 : :
141 : : /* If cursor is after the last digit and without selection, move to last digit */
142 [ # # # # ]: 0 : if (gtk_editable_get_position (editable) > END_INDEX &&
143 : 0 : !gtk_editable_get_selection_bounds (editable, NULL, NULL))
144 : 0 : gtk_editable_set_position (editable, END_INDEX);
145 : :
146 : 0 : g_signal_handlers_unblock_by_func (self->text, cursor_position_changed_cb, self);
147 : 0 : }
148 : :
149 : : static void
150 : 0 : editable_insert_text_cb (GtkText *text,
151 : : char *new_text,
152 : : gint new_text_length,
153 : : gint *position,
154 : : CcTimelikeEntry *self)
155 : : {
156 : 0 : g_assert (CC_IS_TIMELIKE_ENTRY (self));
157 : :
158 [ # # ]: 0 : if (new_text_length == -1)
159 : 0 : new_text_length = strlen (new_text);
160 : :
161 [ # # ]: 0 : if (new_text_length == 5)
162 : : {
163 : 0 : const gchar *text_str = gtk_editable_get_text (GTK_EDITABLE (self));
164 : : guint16 text_length;
165 : :
166 : 0 : text_length = g_utf8_strlen (text_str, -1);
167 : :
168 : : /* Return if the text matches XX:XX template (where X is a number) */
169 [ # # ]: 0 : if (text_length == 0 &&
170 [ # # ]: 0 : strstr (new_text, "0123456789:") == new_text + new_text_length &&
171 [ # # ]: 0 : strchr (new_text, ':') == strrchr (new_text, ':'))
172 : 0 : return;
173 : : }
174 : :
175 : : /* Insert text if single digit number */
176 [ # # ]: 0 : if (new_text_length == 1 &&
177 [ # # ]: 0 : strspn (new_text, "0123456789"))
178 : : {
179 : : int pos, number;
180 : :
181 : 0 : pos = *position;
182 : 0 : number = *new_text - '0';
183 : :
184 [ # # ]: 0 : if (pos == 0)
185 : 0 : self->hour = self->hour % 10 + number * 10;
186 [ # # ]: 0 : else if (pos == 1)
187 : 0 : self->hour = self->hour / 10 * 10 + number;
188 [ # # ]: 0 : else if (pos == 3)
189 : 0 : self->minute = self->minute % 10 + number * 10;
190 [ # # ]: 0 : else if (pos == 4)
191 : 0 : self->minute = self->minute / 10 * 10 + number;
192 : :
193 [ # # ]: 0 : if (self->is_am_pm)
194 : 0 : self->hour = CLAMP (self->hour, 1, 12);
195 : : else
196 : 0 : self->hour = CLAMP (self->hour, 0, 23);
197 : :
198 : 0 : self->minute = CLAMP (self->minute, 0, 59);
199 : :
200 : 0 : g_signal_stop_emission_by_name (text, "insert-text");
201 : 0 : timelike_entry_fill_time (self);
202 : 0 : *position = pos + 1;
203 : :
204 [ # # ]: 0 : g_clear_handle_id (&self->time_changed_id, g_source_remove);
205 : 0 : self->time_changed_id = g_timeout_add (EMIT_CHANGED_TIMEOUT,
206 : : (GSourceFunc)emit_time_changed, self);
207 : 0 : return;
208 : : }
209 : :
210 : : /* Warn otherwise */
211 : 0 : g_signal_stop_emission_by_name (text, "insert-text");
212 : 0 : gtk_widget_error_bell (GTK_WIDGET (self));
213 : : }
214 : :
215 : :
216 : : static gboolean
217 : 0 : change_value_cb (GtkWidget *widget,
218 : : GVariant *arguments,
219 : : gpointer user_data)
220 : : {
221 : 0 : CcTimelikeEntry *self = CC_TIMELIKE_ENTRY (widget);
222 : : GtkScrollType type;
223 : : int position;
224 : :
225 : 0 : g_assert (CC_IS_TIMELIKE_ENTRY (self));
226 : :
227 : 0 : type = g_variant_get_int32 (arguments);
228 : 0 : position = gtk_editable_get_position (GTK_EDITABLE (self));
229 : :
230 [ # # ]: 0 : if (position > SEPARATOR_INDEX)
231 : : {
232 [ # # ]: 0 : if (type == GTK_SCROLL_STEP_UP)
233 : 0 : self->minute += self->minute_increment;
234 : : else
235 : 0 : self->minute -= self->minute_increment;
236 : :
237 [ # # ]: 0 : if (self->minute >= 60)
238 : 0 : self->minute = 0;
239 [ # # ]: 0 : else if (self->minute <= -1)
240 : 0 : self->minute = 60 - self->minute_increment;
241 : : }
242 : : else
243 : : {
244 [ # # ]: 0 : if (type == GTK_SCROLL_STEP_UP)
245 : 0 : self->hour++;
246 : : else
247 : 0 : self->hour--;
248 : :
249 [ # # ]: 0 : if (self->is_am_pm)
250 : : {
251 [ # # ]: 0 : if (self->hour > 12)
252 : 0 : self->hour = 1;
253 [ # # ]: 0 : else if (self->hour < 1)
254 : 0 : self->hour = 12;
255 : : }
256 : : else
257 : : {
258 [ # # ]: 0 : if (self->hour >= 24)
259 : 0 : self->hour = 0;
260 [ # # ]: 0 : else if (self->hour <= -1)
261 : 0 : self->hour = 23;
262 : : }
263 : : }
264 : :
265 : 0 : timelike_entry_fill_time (self);
266 : 0 : gtk_editable_set_position (GTK_EDITABLE (self), position);
267 : :
268 [ # # ]: 0 : g_clear_handle_id (&self->time_changed_id, g_source_remove);
269 : 0 : self->time_changed_id = g_timeout_add (EMIT_CHANGED_TIMEOUT,
270 : : (GSourceFunc)emit_time_changed, self);
271 : :
272 : 0 : return GDK_EVENT_STOP;
273 : : }
274 : :
275 : : static void
276 : 0 : value_changed_cb (CcTimelikeEntry *self,
277 : : GtkScrollType type)
278 : : {
279 : 0 : g_autoptr(GVariant) value;
280 : :
281 : 0 : g_assert (CC_IS_TIMELIKE_ENTRY (self));
282 : :
283 : 0 : value = g_variant_new_int32 (type);
284 : :
285 : 0 : change_value_cb (GTK_WIDGET (self), value, NULL);
286 : 0 : }
287 : :
288 : : static void
289 : 0 : on_text_cut_clipboard_cb (GtkText *text,
290 : : CcTimelikeEntry *self)
291 : : {
292 : 0 : gtk_widget_error_bell (GTK_WIDGET (self));
293 : 0 : g_signal_stop_emission_by_name (text, "cut-clipboard");
294 : 0 : }
295 : :
296 : : static void
297 : 0 : on_text_delete_from_cursor_cb (GtkText *text,
298 : : GtkDeleteType *type,
299 : : gint count,
300 : : CcTimelikeEntry *self)
301 : : {
302 : 0 : gtk_widget_error_bell (GTK_WIDGET (self));
303 : 0 : g_signal_stop_emission_by_name (text, "delete-from-cursor");
304 : 0 : }
305 : :
306 : : static void
307 : 0 : on_text_move_cursor_cb (GtkText *text,
308 : : GtkMovementStep step,
309 : : gint count,
310 : : gboolean extend,
311 : : CcTimelikeEntry *self)
312 : : {
313 : : int current_pos;
314 : :
315 : 0 : current_pos = gtk_editable_get_position (GTK_EDITABLE (self));
316 : :
317 : : /* If cursor is on ‘:’ move backward/forward depending on the current movement */
318 [ # # # # ]: 0 : if ((step == GTK_MOVEMENT_LOGICAL_POSITIONS ||
319 : 0 : step == GTK_MOVEMENT_VISUAL_POSITIONS) &&
320 [ # # ]: 0 : current_pos + count == SEPARATOR_INDEX)
321 [ # # ]: 0 : count > 0 ? count++ : count--;
322 : :
323 : 0 : g_signal_handlers_block_by_func (text, on_text_move_cursor_cb, self);
324 : 0 : gtk_editable_set_position (GTK_EDITABLE (text), current_pos + count);
325 : 0 : g_signal_handlers_unblock_by_func (text, on_text_move_cursor_cb, self);
326 : :
327 : 0 : g_signal_stop_emission_by_name (text, "move-cursor");
328 : 0 : }
329 : :
330 : : static void
331 : 0 : on_text_paste_clipboard_cb (GtkText *text,
332 : : CcTimelikeEntry *self)
333 : : {
334 : 0 : gtk_widget_error_bell (GTK_WIDGET (self));
335 : 0 : g_signal_stop_emission_by_name (text, "paste-clipboard");
336 : 0 : }
337 : :
338 : : static void
339 : 0 : on_text_toggle_overwrite_cb (GtkText *text,
340 : : CcTimelikeEntry *self)
341 : : {
342 : 0 : gtk_widget_error_bell (GTK_WIDGET (self));
343 : 0 : g_signal_stop_emission_by_name (text, "toggle-overwrite");
344 : 0 : }
345 : :
346 : : static gboolean
347 : 0 : on_key_pressed_cb (CcTimelikeEntry *self,
348 : : guint keyval,
349 : : guint keycode,
350 : : GdkModifierType state)
351 : : {
352 [ # # ]: 0 : if (keyval == GDK_KEY_Escape)
353 : 0 : return GDK_EVENT_PROPAGATE;
354 : :
355 : : /* Allow entering numbers */
356 [ # # # # ]: 0 : if (!(state & GDK_SHIFT_MASK) &&
357 [ # # # # ]: 0 : ((keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9) ||
358 [ # # ]: 0 : (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)))
359 : 0 : return GDK_EVENT_PROPAGATE;
360 : :
361 : : /* Allow navigation keys */
362 [ # # # # : 0 : if ((keyval >= GDK_KEY_Left && keyval <= GDK_KEY_Down) ||
# # ]
363 [ # # # # ]: 0 : (keyval >= GDK_KEY_KP_Left && keyval <= GDK_KEY_KP_Down) ||
364 [ # # ]: 0 : keyval == GDK_KEY_Home ||
365 [ # # ]: 0 : keyval == GDK_KEY_End ||
366 : : keyval == GDK_KEY_Menu)
367 : 0 : return GDK_EVENT_PROPAGATE;
368 : :
369 [ # # ]: 0 : if (state & (GDK_CONTROL_MASK | GDK_ALT_MASK))
370 : 0 : return GDK_EVENT_PROPAGATE;
371 : :
372 [ # # ]: 0 : if (keyval == GDK_KEY_Tab)
373 : : {
374 : : /* If focus is on Hour field skip to minute field */
375 [ # # ]: 0 : if (gtk_editable_get_position (GTK_EDITABLE (self)) <= 1)
376 : : {
377 : 0 : gtk_editable_set_position (GTK_EDITABLE (self), SEPARATOR_INDEX + 1);
378 : :
379 : 0 : return GDK_EVENT_STOP;
380 : : }
381 : :
382 : 0 : return GDK_EVENT_PROPAGATE;
383 : : }
384 : :
385 : : /* Shift-Tab */
386 [ # # ]: 0 : if (keyval == GDK_KEY_ISO_Left_Tab)
387 : : {
388 : : /* If focus is on Minute field skip back to Hour field */
389 [ # # ]: 0 : if (gtk_editable_get_position (GTK_EDITABLE (self)) >= 2)
390 : : {
391 : 0 : gtk_editable_set_position (GTK_EDITABLE (self), 0);
392 : :
393 : 0 : return GDK_EVENT_STOP;
394 : : }
395 : :
396 : 0 : return GDK_EVENT_PROPAGATE;
397 : : }
398 : :
399 : 0 : return GDK_EVENT_STOP;
400 : : }
401 : :
402 : : static GtkEditable *
403 : 0 : cc_timelike_entry_get_delegate (GtkEditable *editable)
404 : : {
405 : 0 : CcTimelikeEntry *self = CC_TIMELIKE_ENTRY (editable);
406 : 0 : return GTK_EDITABLE (self->text);
407 : : }
408 : :
409 : : static void
410 : 0 : gtk_editable_interface_init (GtkEditableInterface *iface)
411 : : {
412 : 0 : iface->get_delegate = cc_timelike_entry_get_delegate;
413 : 0 : }
414 : :
415 : : static void
416 : 0 : cc_timelike_entry_constructed (GObject *object)
417 : : {
418 : 0 : CcTimelikeEntry *self = CC_TIMELIKE_ENTRY (object);
419 : : PangoAttrList *list;
420 : : PangoAttribute *attribute;
421 : :
422 : 0 : G_OBJECT_CLASS (cc_timelike_entry_parent_class)->constructed (object);
423 : :
424 : 0 : gtk_widget_set_direction (GTK_WIDGET (self->text), GTK_TEXT_DIR_LTR);
425 : 0 : timelike_entry_fill_time (CC_TIMELIKE_ENTRY (object));
426 : :
427 : 0 : list = pango_attr_list_new ();
428 : :
429 : 0 : attribute = pango_attr_size_new (PANGO_SCALE * 32);
430 : 0 : pango_attr_list_insert (list, attribute);
431 : :
432 : 0 : attribute = pango_attr_weight_new (PANGO_WEIGHT_LIGHT);
433 : 0 : pango_attr_list_insert (list, attribute);
434 : :
435 : : /* Use tabular(monospace) letters */
436 : 0 : attribute = pango_attr_font_features_new ("tnum");
437 : 0 : pango_attr_list_insert (list, attribute);
438 : :
439 : 0 : gtk_text_set_attributes (GTK_TEXT (self->text), list);
440 : :
441 : 0 : pango_attr_list_unref (list);
442 : 0 : }
443 : :
444 : : static void
445 : 0 : cc_timelike_entry_dispose (GObject *object)
446 : : {
447 : 0 : CcTimelikeEntry *self = CC_TIMELIKE_ENTRY (object);
448 : :
449 : 0 : gtk_editable_finish_delegate (GTK_EDITABLE (self));
450 [ # # ]: 0 : g_clear_pointer (&self->text, gtk_widget_unparent);
451 : :
452 : 0 : G_OBJECT_CLASS (cc_timelike_entry_parent_class)->dispose (object);
453 : 0 : }
454 : :
455 : : static void
456 : 0 : cc_timelike_entry_get_property (GObject *object,
457 : : guint property_id,
458 : : GValue *value,
459 : : GParamSpec *pspec)
460 : : {
461 : 0 : CcTimelikeEntry *self = CC_TIMELIKE_ENTRY (object);
462 : :
463 [ # # ]: 0 : switch ((CcTimelikeEntryProperty) property_id)
464 : : {
465 : 0 : case PROP_MINUTE_INCREMENT:
466 : 0 : g_value_set_uint (value, cc_timelike_entry_get_minute_increment (self));
467 : 0 : break;
468 : 0 : default:
469 [ # # ]: 0 : if (!gtk_editable_delegate_get_property (object, property_id, value, pspec))
470 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
471 : : }
472 : 0 : }
473 : :
474 : : static void
475 : 0 : cc_timelike_entry_set_property (GObject *object,
476 : : guint property_id,
477 : : const GValue *value,
478 : : GParamSpec *pspec)
479 : : {
480 : 0 : CcTimelikeEntry *self = CC_TIMELIKE_ENTRY (object);
481 : :
482 [ # # ]: 0 : switch ((CcTimelikeEntryProperty) property_id)
483 : : {
484 : 0 : case PROP_MINUTE_INCREMENT:
485 : 0 : cc_timelike_entry_set_minute_increment (self, g_value_get_uint (value));
486 : 0 : break;
487 : 0 : default:
488 [ # # ]: 0 : if (!gtk_editable_delegate_set_property (object, property_id, value, pspec))
489 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
490 : : }
491 : 0 : }
492 : :
493 : : static void
494 : 0 : cc_timelike_entry_class_init (CcTimelikeEntryClass *klass)
495 : : {
496 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
497 : 0 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
498 : :
499 : 0 : object_class->constructed = cc_timelike_entry_constructed;
500 : 0 : object_class->dispose = cc_timelike_entry_dispose;
501 : 0 : object_class->get_property = cc_timelike_entry_get_property;
502 : 0 : object_class->set_property = cc_timelike_entry_set_property;
503 : :
504 : : /**
505 : : * CcTimelikeEntry:minute-increment:
506 : : *
507 : : * Number of minutes the up/down keys change the time by, which will
508 : : * always be in the range [1, 59].
509 : : */
510 : 0 : props[PROP_MINUTE_INCREMENT] =
511 : 0 : g_param_spec_uint ("minute-increment",
512 : : NULL, NULL,
513 : : 1, 59, 1,
514 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
515 : :
516 : 0 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
517 : :
518 : 0 : signals[CHANGE_VALUE] =
519 : 0 : g_signal_new ("change-value",
520 : : G_TYPE_FROM_CLASS (klass),
521 : : G_SIGNAL_ACTION,
522 : : 0, NULL, NULL,
523 : : NULL,
524 : : G_TYPE_NONE, 1,
525 : : GTK_TYPE_SCROLL_TYPE);
526 : :
527 : 0 : signals[TIME_CHANGED] =
528 : 0 : g_signal_new ("time-changed",
529 : : G_TYPE_FROM_CLASS (klass),
530 : : G_SIGNAL_RUN_FIRST,
531 : : 0, NULL, NULL,
532 : : NULL,
533 : : G_TYPE_NONE, 0);
534 : :
535 : 0 : gtk_editable_install_properties (object_class, 1);
536 : :
537 : 0 : gtk_widget_class_set_css_name (widget_class, "entry");
538 : 0 : gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
539 : 0 : gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_TEXT_BOX);
540 : :
541 : 0 : gtk_widget_class_add_binding (widget_class, GDK_KEY_Up, 0,
542 : : change_value_cb, "i", GTK_SCROLL_STEP_UP);
543 : 0 : gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Up, 0,
544 : : change_value_cb, "i", GTK_SCROLL_STEP_UP);
545 : 0 : gtk_widget_class_add_binding (widget_class, GDK_KEY_Down, 0,
546 : : change_value_cb, "i", GTK_SCROLL_STEP_DOWN);
547 : 0 : gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Down, 0,
548 : : change_value_cb, "i", GTK_SCROLL_STEP_DOWN);
549 : 0 : }
550 : :
551 : : static void
552 : 0 : cc_timelike_entry_init (CcTimelikeEntry *self)
553 : : {
554 : : GtkEventController *key_controller;
555 : :
556 : : /* Default value */
557 : 0 : self->minute_increment = 1;
558 : :
559 : 0 : key_controller = gtk_event_controller_key_new ();
560 : 0 : gtk_event_controller_set_propagation_phase (key_controller, GTK_PHASE_CAPTURE);
561 : 0 : g_signal_connect_swapped (key_controller, "key-pressed", G_CALLBACK (on_key_pressed_cb), self);
562 : 0 : gtk_widget_add_controller (GTK_WIDGET (self), key_controller);
563 : :
564 : 0 : self->text = g_object_new (GTK_TYPE_TEXT,
565 : : "input-purpose", GTK_INPUT_PURPOSE_DIGITS,
566 : : "input-hints", GTK_INPUT_HINT_NO_EMOJI,
567 : : "overwrite-mode", TRUE,
568 : : "xalign", 0.5,
569 : : "max-length", 5,
570 : : NULL);
571 : 0 : gtk_widget_set_parent (self->text, GTK_WIDGET (self));
572 : 0 : gtk_editable_init_delegate (GTK_EDITABLE (self));
573 : 0 : g_object_connect (self->text,
574 : : "signal::cut-clipboard", on_text_cut_clipboard_cb, self,
575 : : "signal::delete-from-cursor", on_text_delete_from_cursor_cb, self,
576 : : "signal::insert-text", editable_insert_text_cb, self,
577 : : "signal::move-cursor", on_text_move_cursor_cb, self,
578 : : "swapped-signal::notify::cursor-position", cursor_position_changed_cb, self,
579 : : "swapped-signal::notify::selection-bound", entry_selection_changed_cb, self,
580 : : "signal::paste-clipboard", on_text_paste_clipboard_cb, self,
581 : : "signal::toggle-overwrite", on_text_toggle_overwrite_cb, self,
582 : : NULL);
583 : 0 : g_signal_connect (self, "change-value",
584 : : G_CALLBACK (value_changed_cb), self);
585 : 0 : }
586 : :
587 : : GtkWidget *
588 : 0 : cc_timelike_entry_new (void)
589 : : {
590 : 0 : return g_object_new (CC_TYPE_TIMELIKE_ENTRY, NULL);
591 : : }
592 : :
593 : : void
594 : 0 : cc_timelike_entry_set_time (CcTimelikeEntry *self,
595 : : guint hour,
596 : : guint minute)
597 : : {
598 : : gboolean is_am_pm;
599 : :
600 : 0 : g_return_if_fail (CC_IS_TIMELIKE_ENTRY (self));
601 : :
602 [ # # # # ]: 0 : if (cc_timelike_entry_get_hour (self) == hour &&
603 : 0 : cc_timelike_entry_get_minute (self) == minute)
604 : 0 : return;
605 : :
606 : 0 : is_am_pm = cc_timelike_entry_get_am_pm (self);
607 : 0 : cc_timelike_entry_set_am_pm (self, FALSE);
608 : :
609 : 0 : self->hour = MIN (hour, 23);
610 : 0 : self->minute = MIN (minute, 59);
611 : :
612 : 0 : cc_timelike_entry_set_am_pm (self, is_am_pm);
613 : :
614 : 0 : g_signal_emit (self, signals[TIME_CHANGED], 0);
615 : 0 : timelike_entry_fill_time (self);
616 : : }
617 : :
618 : : guint
619 : 0 : cc_timelike_entry_get_hour (CcTimelikeEntry *self)
620 : : {
621 : 0 : g_return_val_if_fail (CC_IS_TIMELIKE_ENTRY (self), 0);
622 : :
623 [ # # ]: 0 : if (!self->is_am_pm)
624 : 0 : return self->hour;
625 : :
626 [ # # # # ]: 0 : if (self->is_am && self->hour == 12)
627 : 0 : return 0;
628 [ # # # # ]: 0 : else if (self->is_am || self->hour == 12)
629 : 0 : return self->hour;
630 : : else
631 : 0 : return self->hour + 12;
632 : : }
633 : :
634 : : guint
635 : 0 : cc_timelike_entry_get_minute (CcTimelikeEntry *self)
636 : : {
637 : 0 : g_return_val_if_fail (CC_IS_TIMELIKE_ENTRY (self), 0);
638 : :
639 : 0 : return self->minute;
640 : : }
641 : :
642 : : gboolean
643 : 0 : cc_timelike_entry_get_is_am (CcTimelikeEntry *self)
644 : : {
645 : 0 : g_return_val_if_fail (CC_IS_TIMELIKE_ENTRY (self), FALSE);
646 : :
647 [ # # ]: 0 : if (self->is_am_pm)
648 : 0 : return self->is_am;
649 : :
650 : 0 : return self->hour < 12;
651 : : }
652 : :
653 : : void
654 : 0 : cc_timelike_entry_set_is_am (CcTimelikeEntry *self,
655 : : gboolean is_am)
656 : : {
657 : 0 : g_return_if_fail (CC_IS_TIMELIKE_ENTRY (self));
658 : :
659 : 0 : self->is_am = !!is_am;
660 : 0 : g_signal_emit (self, signals[TIME_CHANGED], 0);
661 : : }
662 : :
663 : : gboolean
664 : 0 : cc_timelike_entry_get_am_pm (CcTimelikeEntry *self)
665 : : {
666 : 0 : g_return_val_if_fail (CC_IS_TIMELIKE_ENTRY (self), FALSE);
667 : :
668 : 0 : return self->is_am_pm;
669 : : }
670 : :
671 : : void
672 : 0 : cc_timelike_entry_set_am_pm (CcTimelikeEntry *self,
673 : : gboolean is_am_pm)
674 : : {
675 : 0 : g_return_if_fail (CC_IS_TIMELIKE_ENTRY (self));
676 : :
677 [ # # ]: 0 : if (self->is_am_pm == !!is_am_pm)
678 : 0 : return;
679 : :
680 [ # # ]: 0 : if (self->hour < 12)
681 : 0 : self->is_am = TRUE;
682 : : else
683 : 0 : self->is_am = FALSE;
684 : :
685 [ # # ]: 0 : if (is_am_pm)
686 : : {
687 [ # # ]: 0 : if (self->hour == 0)
688 : 0 : self->hour = 12;
689 [ # # ]: 0 : else if (self->hour > 12)
690 : 0 : self->hour = self->hour - 12;
691 : : }
692 : : else
693 : : {
694 [ # # # # ]: 0 : if (self->hour == 12 && self->is_am)
695 : 0 : self->hour = 0;
696 [ # # ]: 0 : else if (!self->is_am)
697 : 0 : self->hour = self->hour + 12;
698 : : }
699 : :
700 : 0 : self->is_am_pm = !!is_am_pm;
701 : 0 : timelike_entry_fill_time (self);
702 : : }
703 : :
704 : : /**
705 : : * cc_timelike_entry_get_minute_increment:
706 : : * @self: a #CcTimelikeEntry
707 : : *
708 : : * Get the value of #CcTimelikeEntry:minute-increment.
709 : : *
710 : : * Returns: number of minutes the up/down keys change the time by, which will
711 : : * always be in the range [1, 59]
712 : : */
713 : : guint
714 : 0 : cc_timelike_entry_get_minute_increment (CcTimelikeEntry *self)
715 : : {
716 : 0 : g_return_val_if_fail (CC_IS_TIMELIKE_ENTRY (self), 1);
717 : :
718 : 0 : return self->minute_increment;
719 : : }
720 : :
721 : : /**
722 : : * cc_timelike_entry_set_minute_increment:
723 : : * @self: a #CcTimelikeEntry
724 : : * @minutes: number of minutes the up/down keys change the time by; must be
725 : : * in the range [1, 59]
726 : : *
727 : : * Set the value of #CcTimelikeEntry:minute-increment.
728 : : */
729 : : void
730 : 0 : cc_timelike_entry_set_minute_increment (CcTimelikeEntry *self,
731 : : guint minutes)
732 : : {
733 : 0 : g_return_if_fail (CC_IS_TIMELIKE_ENTRY (self));
734 : 0 : g_return_if_fail (minutes > 0 && minutes < 60);
735 : :
736 [ # # ]: 0 : if (self->minute_increment == minutes)
737 : 0 : return;
738 : :
739 : 0 : self->minute_increment = minutes;
740 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MINUTE_INCREMENT]);
741 : : }
742 : :
743 : : /**
744 : : * cc_timelike_entry_get_hours_and_minutes_midpoints:
745 : : * @self: a #CcTimelikeEntry
746 : : * @out_hours_midpoint_x: (out) (optional): return location for the X coordinate
747 : : * of the midpoint of the hours digits
748 : : * @out_minutes_midpoint_x: (out) (optional): return location for the X
749 : : * coordinate of the midpoint of the minutes digits
750 : : *
751 : : * Get the X coordinates of the midpoints of the hours and minutes parts of the
752 : : * entry, in the coordinate space of @self.
753 : : *
754 : : * These can be used to align surrounding widgets with the hours and minutes
755 : : * displays. Remember to convert to the coordinate space of the relevant parent
756 : : * widget to take account of intermediate margins, etc.
757 : : */
758 : : void
759 : 0 : cc_timelike_entry_get_hours_and_minutes_midpoints (CcTimelikeEntry *self,
760 : : float *out_hours_midpoint_x,
761 : : float *out_minutes_midpoint_x)
762 : : {
763 : : gboolean success;
764 : : graphene_rect_t hours_cursor, minutes_cursor;
765 : : graphene_point_t hours_midpoint_self, minutes_midpoint_self;
766 : :
767 : 0 : g_return_if_fail (CC_IS_TIMELIKE_ENTRY (self));
768 : :
769 : : /* The layout offsets in GtkText are only correctly calculated once the widget
770 : : * has been realised (gtk_text_adjust_scroll() bails out if unrealised, and
771 : : * priv->scroll_offset is used in gtk_text_compute_cursor_extents()), so
772 : : * realize it before proceeding. */
773 : 0 : gtk_widget_realize (GTK_WIDGET (self->text));
774 : :
775 : : /* Calculate the midpoints of the hours and minutes, so that surrounding
776 : : * widgets (such as increment and decrement buttons) can be lined up with them. */
777 : 0 : gtk_text_compute_cursor_extents (GTK_TEXT (self->text), 1 /* half-way through hours */,
778 : : &hours_cursor, NULL);
779 : 0 : gtk_text_compute_cursor_extents (GTK_TEXT (self->text), 4 /* half-way through minutes */,
780 : : &minutes_cursor, NULL);
781 : :
782 : 0 : success = gtk_widget_compute_point (GTK_WIDGET (self->text), GTK_WIDGET (self),
783 : : &hours_cursor.origin, &hours_midpoint_self);
784 : 0 : g_assert (success);
785 : :
786 : 0 : success = gtk_widget_compute_point (GTK_WIDGET (self->text), GTK_WIDGET (self),
787 : : &minutes_cursor.origin, &minutes_midpoint_self);
788 : 0 : g_assert (success);
789 : :
790 [ # # ]: 0 : if (out_hours_midpoint_x != NULL)
791 : 0 : *out_hours_midpoint_x = hours_midpoint_self.x;
792 [ # # ]: 0 : if (out_minutes_midpoint_x != NULL)
793 : 0 : *out_minutes_midpoint_x = minutes_midpoint_self.x;
794 : : }
|