Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-date-label"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <glib/gi18n.h>
9 : : #include <gtk/gtk.h>
10 : : #include <valent.h>
11 : :
12 : : #include "valent-date-label.h"
13 : :
14 [ + - + - ]: 4 : G_DEFINE_ENUM_TYPE (ValentDateFormat, valent_date_format,
15 : : G_DEFINE_ENUM_VALUE (VALENT_DATE_FORMAT_ADAPTIVE, "adaptive"),
16 : : G_DEFINE_ENUM_VALUE (VALENT_DATE_FORMAT_ADAPTIVE_SHORT, "adaptive-short"),
17 : : G_DEFINE_ENUM_VALUE (VALENT_DATE_FORMAT_TIME, "time"))
18 : :
19 : : struct _ValentDateLabel
20 : : {
21 : : GtkWidget parent_instance;
22 : :
23 : : GtkWidget *label;
24 : : int64_t date;
25 : : ValentDateFormat mode;
26 : : double xalign;
27 : : };
28 : :
29 [ + + + - ]: 43 : G_DEFINE_FINAL_TYPE (ValentDateLabel, valent_date_label, GTK_TYPE_WIDGET)
30 : :
31 : : typedef enum {
32 : : PROP_DATE = 1,
33 : : PROP_MODE,
34 : : PROP_XALIGN,
35 : : } ValentDateLabelProperty;
36 : :
37 : : static GParamSpec *properties[PROP_XALIGN + 1] = { NULL, };
38 : :
39 : : static GPtrArray *label_cache = NULL;
40 : : static unsigned int label_source = 0;
41 : :
42 : :
43 : : /*< private >
44 : : * valent_date_label_string_adaptive:
45 : : * @timestamp: a UNIX epoch timestamp (ms)
46 : : * @abbreviated: use abbreviations for brevity
47 : : *
48 : : * Create a user friendly date-time string for @timestamp, in a relative format.
49 : : *
50 : : * Examples:
51 : : * - "Just now"
52 : : * - "15 minutes"
53 : : * - "11:45 PM"
54 : : * - "Yesterday · 11:45 PM"
55 : : * - "Tuesday"
56 : : * - "February 29"
57 : : *
58 : : * Abbreviated Examples:
59 : : * - "Just now"
60 : : * - "15 mins"
61 : : * - "11:45 PM"
62 : : * - "Tue"
63 : : * - "Feb 29"
64 : : *
65 : : * Returns: (transfer full): a new string
66 : : */
67 : : static char *
68 : 13 : valent_date_label_string_adaptive (int64_t timestamp,
69 : : gboolean abbreviated)
70 : : {
71 : 26 : g_autoptr (GDateTime) dt = NULL;
72 [ + - ]: 13 : g_autoptr (GDateTime) now = NULL;
73 [ + - ]: 13 : g_autofree char *date_str = NULL;
74 : 13 : g_autofree char *time_str = NULL;
75 : 13 : GTimeSpan diff;
76 : :
77 : 13 : dt = g_date_time_new_from_unix_local (timestamp / 1000);
78 : 13 : now = g_date_time_new_now_local ();
79 : 13 : diff = g_date_time_difference (now, dt);
80 : :
81 : : /* TRANSLATORS: Less than a minute ago
82 : : */
83 [ + + ]: 13 : if (diff < G_TIME_SPAN_MINUTE)
84 [ - + ]: 10 : return g_strdup (_("Just now"));
85 : :
86 [ - + ]: 8 : if (diff < G_TIME_SPAN_HOUR)
87 : : {
88 : 0 : unsigned int n_minutes;
89 : :
90 : 0 : n_minutes = (diff / G_TIME_SPAN_MINUTE);
91 : :
92 [ # # ]: 0 : if (abbreviated)
93 : : {
94 : : /* TRANSLATORS: Time duration in minutes, abbreviated (eg. 15 mins)
95 : : */
96 : 0 : return g_strdup_printf (ngettext ("%d min", "%d mins", n_minutes),
97 : : n_minutes);
98 : : }
99 : : else
100 : : {
101 : : /* TRANSLATORS: Time duration in minutes (eg. 15 minutes)
102 : : */
103 : 0 : return g_strdup_printf (ngettext("%d minute", "%d minutes", n_minutes),
104 : : n_minutes);
105 : : }
106 : : }
107 : :
108 : 8 : time_str = g_date_time_format (dt, "%-l:%M %p");
109 [ - + ]: 8 : if (diff < G_TIME_SPAN_DAY)
110 : : {
111 : 0 : int today = g_date_time_get_day_of_month (now);
112 : 0 : int day = g_date_time_get_day_of_month (dt);
113 : :
114 [ # # ]: 0 : if (abbreviated || today == day)
115 : : return g_steal_pointer (&time_str);
116 : :
117 : : /* TRANSLATORS: Yesterday, but less than 24 hours (eg. Yesterday · 11:45 PM)
118 : : */
119 : 0 : return g_strdup_printf (_("Yesterday · %s"), time_str);
120 : : }
121 : :
122 : : /* Less than a week ago (eg. Tuesday/Tue)
123 : : */
124 [ - + ]: 8 : if (diff < G_TIME_SPAN_DAY * 7)
125 : : {
126 [ # # ]: 0 : if (abbreviated)
127 : 0 : return g_date_time_format (dt, "%a");
128 : :
129 : : /* TRANSLATORS: Date and time (eg. Tuesday · 23:45:00 PM)
130 : : */
131 : 0 : date_str = g_date_time_format (dt, "%A");
132 : 0 : return g_strdup_printf (_("%s · %s"), date_str, time_str);
133 : : }
134 : :
135 : : /* More than a week ago (eg. Feb 29)
136 : : */
137 [ + + ]: 8 : if (abbreviated)
138 : 4 : return g_date_time_format (dt, "%b %-e");
139 : :
140 : : /* TRANSLATORS: Date and time (eg. February 29 · 23:45:00 PM)
141 : : */
142 : 4 : date_str = g_date_time_format(dt, "%B %-e");
143 : 4 : return g_strdup_printf (_("%s · %s"), date_str, time_str);
144 : : }
145 : :
146 : : /*< private >
147 : : * valent_date_label_string_absolute:
148 : : * @timestamp: a UNIX epoch timestamp (ms)
149 : : *
150 : : * Create a user friendly time string for @timestamp, in an absolute
151 : : * format.
152 : : *
153 : : * Examples:
154 : : * - "11:45 PM"
155 : : *
156 : : * Returns: (transfer full): a new string
157 : : */
158 : : static char *
159 : 3 : valent_date_label_string_time (int64_t timestamp)
160 : : {
161 : 6 : g_autoptr (GDateTime) dt = NULL;
162 : :
163 : 3 : dt = g_date_time_new_from_unix_local (timestamp / 1000);
164 [ + - ]: 3 : return g_date_time_format (dt, "%-l:%M %p");
165 : : }
166 : :
167 : : static void
168 : 11 : valent_date_label_sync (ValentDateLabel *label)
169 : : {
170 : 11 : g_autofree char *text = NULL;
171 : 11 : g_autofree char *tooltip_text = NULL;
172 : :
173 [ + - ]: 11 : g_return_if_fail (VALENT_IS_DATE_LABEL (label));
174 : :
175 [ + + + - ]: 11 : switch (label->mode)
176 : : {
177 : 3 : case VALENT_DATE_FORMAT_ADAPTIVE:
178 : 3 : text = valent_date_label_string_adaptive (label->date, FALSE);
179 : 3 : break;
180 : :
181 : 5 : case VALENT_DATE_FORMAT_ADAPTIVE_SHORT:
182 : 5 : text = valent_date_label_string_adaptive (label->date, TRUE);
183 : 5 : tooltip_text = valent_date_label_string_adaptive (label->date, FALSE);
184 : 5 : break;
185 : :
186 : 3 : case VALENT_DATE_FORMAT_TIME:
187 : 3 : text = valent_date_label_string_time (label->date);
188 : 3 : break;
189 : :
190 : : default:
191 : : break;
192 : : }
193 : :
194 : 11 : gtk_label_set_label (GTK_LABEL (label->label), text);
195 : 11 : gtk_widget_set_tooltip_text (GTK_WIDGET (label->label), tooltip_text);
196 [ + + ]: 11 : if (tooltip_text != NULL)
197 : : {
198 : 5 : gtk_accessible_update_property (GTK_ACCESSIBLE (label->label),
199 : : GTK_ACCESSIBLE_PROPERTY_LABEL, tooltip_text,
200 : : -1);
201 : : }
202 : : }
203 : :
204 : : static gboolean
205 : 0 : valent_date_label_sync_func (gpointer user_data)
206 : : {
207 [ # # ]: 0 : for (unsigned int i = 0; i < label_cache->len; i++)
208 : 0 : valent_date_label_sync (g_ptr_array_index (label_cache, i));
209 : :
210 : 0 : return G_SOURCE_CONTINUE;
211 : : }
212 : :
213 : : /*
214 : : * GObject
215 : : */
216 : : static void
217 : 6 : valent_date_label_finalize (GObject *object)
218 : : {
219 : 6 : ValentDateLabel *self = VALENT_DATE_LABEL (object);
220 : :
221 : : /* Remove from update list */
222 : 6 : g_ptr_array_remove (label_cache, self);
223 : :
224 [ + - ]: 6 : if (label_cache->len == 0)
225 : : {
226 [ + - ]: 6 : g_clear_handle_id (&label_source, g_source_remove);
227 [ + - ]: 6 : g_clear_pointer (&label_cache, g_ptr_array_unref);
228 : : }
229 : :
230 [ + - ]: 6 : g_clear_pointer (&self->label, gtk_widget_unparent);
231 : :
232 : 6 : G_OBJECT_CLASS (valent_date_label_parent_class)->finalize (object);
233 : 6 : }
234 : :
235 : : static void
236 : 19 : valent_date_label_get_property (GObject *object,
237 : : guint prop_id,
238 : : GValue *value,
239 : : GParamSpec *pspec)
240 : : {
241 : 19 : ValentDateLabel *self = VALENT_DATE_LABEL (object);
242 : :
243 [ + + + - ]: 19 : switch ((ValentDateLabelProperty)prop_id)
244 : : {
245 : 3 : case PROP_DATE:
246 : 3 : g_value_set_int64 (value, valent_date_label_get_date (self));
247 : 3 : break;
248 : :
249 : 3 : case PROP_MODE:
250 : 3 : g_value_set_enum (value, valent_date_label_get_mode (self));
251 : 3 : break;
252 : :
253 : 13 : case PROP_XALIGN:
254 : 13 : g_value_set_double (value, self->xalign);
255 : 13 : break;
256 : :
257 : 0 : default:
258 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
259 : : }
260 : 19 : }
261 : :
262 : : static void
263 : 19 : valent_date_label_set_property (GObject *object,
264 : : guint prop_id,
265 : : const GValue *value,
266 : : GParamSpec *pspec)
267 : : {
268 : 19 : ValentDateLabel *self = VALENT_DATE_LABEL (object);
269 : :
270 [ + + + - ]: 19 : switch ((ValentDateLabelProperty)prop_id)
271 : : {
272 : 6 : case PROP_DATE:
273 : 6 : valent_date_label_set_date (self, g_value_get_int64 (value));
274 : 6 : break;
275 : :
276 : 6 : case PROP_MODE:
277 : 6 : valent_date_label_set_mode (self, g_value_get_enum (value));
278 : 6 : break;
279 : :
280 : 7 : case PROP_XALIGN:
281 : 7 : self->xalign = g_value_get_double (value);
282 : 7 : break;
283 : :
284 : 0 : default:
285 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
286 : : }
287 : 19 : }
288 : :
289 : : static void
290 : 4 : valent_date_label_class_init (ValentDateLabelClass *klass)
291 : : {
292 : 4 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
293 : 4 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
294 : :
295 : 4 : object_class->finalize = valent_date_label_finalize;
296 : 4 : object_class->get_property = valent_date_label_get_property;
297 : 4 : object_class->set_property = valent_date_label_set_property;
298 : :
299 : 4 : gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
300 : 4 : gtk_widget_class_set_css_name (widget_class, "date-label");
301 : :
302 : : /**
303 : : * ValentDateLabel:date
304 : : *
305 : : * The timestamp this label represents.
306 : : */
307 : 8 : properties [PROP_DATE] =
308 : 4 : g_param_spec_int64 ("date", NULL, NULL,
309 : : 0, G_MAXINT64,
310 : : 0,
311 : : (G_PARAM_READWRITE |
312 : : G_PARAM_EXPLICIT_NOTIFY |
313 : : G_PARAM_STATIC_STRINGS));
314 : :
315 : : /**
316 : : * ValentDateLabel:mode
317 : : *
318 : : * The brevity of the label.
319 : : */
320 : 8 : properties [PROP_MODE] =
321 : 4 : g_param_spec_enum ("mode", NULL, NULL,
322 : : VALENT_TYPE_DATE_FORMAT,
323 : : VALENT_DATE_FORMAT_ADAPTIVE,
324 : : (G_PARAM_READWRITE |
325 : : G_PARAM_EXPLICIT_NOTIFY |
326 : : G_PARAM_STATIC_STRINGS));
327 : :
328 : : /**
329 : : * ValentDateLabel:xalign
330 : : *
331 : : * The X alignment of the label.
332 : : */
333 : 8 : properties [PROP_XALIGN] =
334 : 4 : g_param_spec_double ("xalign", NULL, NULL,
335 : : 0.0, 1.0,
336 : : 0.5,
337 : : (G_PARAM_READWRITE |
338 : : G_PARAM_CONSTRUCT |
339 : : G_PARAM_STATIC_STRINGS));
340 : :
341 : 4 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
342 : 4 : }
343 : :
344 : : static void
345 : 6 : valent_date_label_init (ValentDateLabel *self)
346 : : {
347 : 6 : self->label = gtk_label_new (NULL);
348 : 6 : gtk_widget_insert_after (self->label, GTK_WIDGET (self), NULL);
349 : :
350 : 6 : g_object_bind_property (self, "xalign",
351 : 6 : self->label, "xalign",
352 : : G_BINDING_BIDIRECTIONAL);
353 : :
354 [ + - ]: 6 : if (label_cache == NULL)
355 : : {
356 : 6 : label_cache = g_ptr_array_new ();
357 : 6 : label_source = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT_IDLE,
358 : : 60,
359 : : valent_date_label_sync_func,
360 : : NULL,
361 : : NULL);
362 : : }
363 : :
364 : 6 : g_ptr_array_add (label_cache, self);
365 : 6 : }
366 : :
367 : : /**
368 : : * valent_date_label_get_date:
369 : : * @label: a `ValentDateLabel`
370 : : *
371 : : * Get the UNIX epoch timestamp (ms) for @label.
372 : : *
373 : : * Returns: the timestamp
374 : : */
375 : : int64_t
376 : 3 : valent_date_label_get_date (ValentDateLabel *label)
377 : : {
378 [ + - ]: 3 : g_return_val_if_fail (VALENT_IS_DATE_LABEL (label), 0);
379 : :
380 : 3 : return label->date;
381 : : }
382 : :
383 : : /**
384 : : * valent_date_label_set_date:
385 : : * @label: a `ValentDateLabel`
386 : : * @date: a UNIX epoch timestamp
387 : : *
388 : : * Set the timestamp for @label to @date.
389 : : */
390 : : void
391 : 6 : valent_date_label_set_date (ValentDateLabel *label,
392 : : int64_t date)
393 : : {
394 [ + - ]: 6 : g_return_if_fail (VALENT_IS_DATE_LABEL (label));
395 : :
396 [ + - ]: 6 : if (label->date == date)
397 : : return;
398 : :
399 : 6 : label->date = date;
400 : 6 : valent_date_label_sync (label);
401 : 6 : g_object_notify_by_pspec (G_OBJECT (label), properties [PROP_DATE]);
402 : : }
403 : :
404 : : /**
405 : : * valent_date_label_get_mode:
406 : : * @label: a `ValentDateLabel`
407 : : *
408 : : * Get the display mode @label.
409 : : *
410 : : * Returns: the display mode
411 : : */
412 : : ValentDateFormat
413 : 3 : valent_date_label_get_mode (ValentDateLabel *label)
414 : : {
415 [ + - ]: 3 : g_return_val_if_fail (VALENT_IS_DATE_LABEL (label), 0);
416 : :
417 : 3 : return label->mode;
418 : : }
419 : :
420 : : /**
421 : : * valent_date_label_set_mode:
422 : : * @label: a `ValentDateLabel`
423 : : * @mode: a mode
424 : : *
425 : : * Set the mode of @label to @mode. Currently the options are `0` and `1`.
426 : : */
427 : : void
428 : 6 : valent_date_label_set_mode (ValentDateLabel *label,
429 : : ValentDateFormat mode)
430 : : {
431 [ + - ]: 6 : g_return_if_fail (VALENT_IS_DATE_LABEL (label));
432 : :
433 [ + + ]: 6 : if (label->mode == mode)
434 : : return;
435 : :
436 : 5 : label->mode = mode;
437 : 5 : valent_date_label_sync (label);
438 : 5 : g_object_notify_by_pspec (G_OBJECT (label), properties [PROP_MODE]);
439 : : }
440 : :
|