1 /* gcal-utils.c
2 *
3 * Copyright (C) 2012 - Erick Pérez Castellanos
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program 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
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "config.h"
20
21 #define G_LOG_DOMAIN "Utils"
22
23 /* langinfo.h in glibc 2.27 defines ALTMON_* only if _GNU_SOURCE is defined. */
24 #define _GNU_SOURCE
25
26 #include "gcal-application.h"
27 #include "gcal-context.h"
28 #include "gcal-enums.h"
29 #include "gcal-utils.h"
30 #include "gcal-event-widget.h"
31 #include "gcal-view.h"
32
33 #include <libecal/libecal.h>
34 #include <libedataserver/libedataserver.h>
35
36 #include <glib/gi18n.h>
37
38 #include <langinfo.h>
39 #include <locale.h>
40
41 #include <string.h>
42 #include <math.h>
43 #include <stdlib.h>
44
45 /**
46 * SECTION:gcal-utils
47 * @short_description: Utility functions
48 * @title:Utility functions
49 */
50
51 static const gint
52 ab_day[7] =
53 {
54 ABDAY_1,
55 ABDAY_2,
56 ABDAY_3,
57 ABDAY_4,
58 ABDAY_5,
59 ABDAY_6,
60 ABDAY_7,
61 };
62
63 static const gint
64 month_item[12] =
65 {
66 /* ALTMON_* constants have been introduced in glibc 2.27 (Feb 1, 2018), also
67 * have been supported in *BSD family (but not in OS X) since 1990s.
68 * If they exist they are the correct way to obtain the month names in
69 * nominative case, standalone, without the day number, as used in the
70 * calendar header. This is obligatory in some languages (Slavic, Baltic,
71 * Greek, etc.) but also recommended to use in all languages because for
72 * other languages there is no difference between ALTMON_* and MON_*.
73 * If ALTMON_* is not supported then we must use MON_*.
74 */
75 #ifdef HAVE_ALTMON
76 ALTMON_1,
77 ALTMON_2,
78 ALTMON_3,
79 ALTMON_4,
80 ALTMON_5,
81 ALTMON_6,
82 ALTMON_7,
83 ALTMON_8,
84 ALTMON_9,
85 ALTMON_10,
86 ALTMON_11,
87 ALTMON_12
88 #else
89 MON_1,
90 MON_2,
91 MON_3,
92 MON_4,
93 MON_5,
94 MON_6,
95 MON_7,
96 MON_8,
97 MON_9,
98 MON_10,
99 MON_11,
100 MON_12
101 #endif
102 };
103
104 #define SCROLL_HARDNESS 10.0
105
106 /**
107 * gcal_get_weekday:
108 * @i: the weekday index
109 *
110 * Retrieves the weekday name.
111 *
112 * Returns: (transfer full): the weekday name
113 */
114 gchar*
gcal_get_weekday(gint i)115 gcal_get_weekday (gint i)
116 {
117 return nl_langinfo (ab_day[i]);
118 }
119
120 /**
121 * gcal_get_month_name:
122 * @i: the month index
123 *
124 * Retrieves the month name.
125 *
126 * Returns: (transfer full): the month name
127 */
128 gchar*
gcal_get_month_name(gint i)129 gcal_get_month_name (gint i)
130 {
131 return nl_langinfo (month_item[i]);
132 }
133
134 /**
135 * gcal_get_surface_from_color:
136 * @color: a #GdkRGBA
137 * @size: the size of the surface
138 *
139 * Creates a squared surface filled with @color. The
140 * surface is always @size x @size.
141 *
142 * Returns: (transfer full): a #cairo_surface_t
143 */
144 cairo_surface_t*
gcal_get_surface_from_color(const GdkRGBA * color,gint size)145 gcal_get_surface_from_color (const GdkRGBA *color,
146 gint size)
147 {
148 cairo_surface_t *surface;
149 cairo_t *cr;
150
151 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size);
152 cr = cairo_create (surface);
153
154 cairo_set_source_rgba (cr,
155 color->red,
156 color->green,
157 color->blue,
158 color->alpha);
159 cairo_rectangle (cr, 0, 0, size, size);
160 cairo_fill (cr);
161 cairo_destroy (cr);
162
163 return surface;
164 }
165
166 /**
167 * get_circle_surface_from_color:
168 * @color: a #GdkRGBA
169 * @size: the size of the surface
170 *
171 * Creates a circular surface filled with @color. The
172 * surface is always @size x @size.
173 *
174 * Returns: (transfer full): a #cairo_surface_t
175 */
176 cairo_surface_t*
get_circle_surface_from_color(const GdkRGBA * color,gint size)177 get_circle_surface_from_color (const GdkRGBA *color,
178 gint size)
179 {
180 cairo_surface_t *surface;
181 cairo_t *cr;
182
183 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size);
184 cr = cairo_create (surface);
185
186 cairo_set_source_rgba (cr,
187 color->red,
188 color->green,
189 color->blue,
190 color->alpha);
191 cairo_arc (cr, size / 2.0, size / 2.0, size / 2.0, 0., 2 * M_PI);
192 cairo_fill (cr);
193 cairo_destroy (cr);
194
195 return surface;
196 }
197
198 /**
199 * get_color_name_from_source:
200 * @source: an #ESource
201 * @out_color: return value for the color
202 *
203 * Utility function to retrieve the color from @source.
204 */
205 void
get_color_name_from_source(ESource * source,GdkRGBA * out_color)206 get_color_name_from_source (ESource *source,
207 GdkRGBA *out_color)
208 {
209 ESourceSelectable *extension = E_SOURCE_SELECTABLE (e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR));
210
211 /* FIXME: We should handle calendars colours better */
212 if (!gdk_rgba_parse (out_color, e_source_selectable_get_color (extension)))
213 gdk_rgba_parse (out_color, "#becedd"); /* calendar default colour */
214 }
215
216 /**
217 * get_desc_from_component:
218 * @component: an #ECalComponent
219 * @joint_char: the character to use when merging event comments
220 *
221 * Utility method to handle the extraction of the description from an
222 * #ECalComponent. This cycle through the list of #ECalComponentText
223 * and concatenate each string into one.
224 *
225 * Returns: (nullable)(transfer full) a new allocated string with the
226 * description
227 **/
228 gchar*
get_desc_from_component(ECalComponent * component,const gchar * joint_char)229 get_desc_from_component (ECalComponent *component,
230 const gchar *joint_char)
231 {
232 GSList *text_list;
233 GSList *l;
234
235 gchar *desc = NULL;
236 text_list = e_cal_component_get_descriptions (component);
237
238 for (l = text_list; l != NULL; l = l->next)
239 {
240 if (l->data != NULL)
241 {
242 ECalComponentText *text;
243 gchar *carrier;
244 text = l->data;
245
246 if (desc != NULL)
247 {
248 carrier = g_strconcat (desc, joint_char, e_cal_component_text_get_value (text), NULL);
249 g_free (desc);
250 desc = carrier;
251 }
252 else
253 {
254 desc = g_strdup (e_cal_component_text_get_value (text));
255 }
256 }
257 }
258
259 g_slist_free_full (text_list, e_cal_component_text_free);
260 return desc != NULL ? g_strstrip (desc) : NULL;
261 }
262
263 /**
264 * get_uuid_from_component:
265 * @source: an {@link ESource}
266 * @component: an {@link ECalComponent}
267 *
268 * Obtains the uuid from a component in the form
269 * "source_uid:event_uid:event_rid" or "source:uid:event_uid" if the
270 * component doesn't hold a recurrence event
271 *
272 * Returns: (Transfer full) a new allocated string with the description
273 **/
274 gchar*
get_uuid_from_component(ESource * source,ECalComponent * component)275 get_uuid_from_component (ESource *source,
276 ECalComponent *component)
277 {
278 gchar *uuid;
279 ECalComponentId *id;
280
281 id = e_cal_component_get_id (component);
282 if (e_cal_component_id_get_rid (id) != NULL)
283 {
284 uuid = g_strdup_printf ("%s:%s:%s",
285 e_source_get_uid (source),
286 e_cal_component_id_get_uid (id),
287 e_cal_component_id_get_rid (id));
288 }
289 else
290 {
291 uuid = g_strdup_printf ("%s:%s",
292 e_source_get_uid (source),
293 e_cal_component_id_get_uid (id));
294 }
295 e_cal_component_id_free (id);
296
297 return uuid;
298 }
299
300 /**
301 * get_first_weekday:
302 *
303 * Copied from Clocks, which by itself is
304 * copied from GtkCalendar.
305 *
306 * Returns: the first weekday, from 0 to 6
307 */
308 gint
get_first_weekday(void)309 get_first_weekday (void)
310 {
311 int week_start;
312
313 #ifdef HAVE__NL_TIME_FIRST_WEEKDAY
314
315 union { unsigned int word; char *string; } langinfo;
316 gint week_1stday = 0;
317 gint first_weekday = 1;
318 guint week_origin;
319
320 langinfo.string = nl_langinfo (_NL_TIME_FIRST_WEEKDAY);
321 first_weekday = langinfo.string[0];
322 langinfo.string = nl_langinfo (_NL_TIME_WEEK_1STDAY);
323 week_origin = langinfo.word;
324 if (week_origin == 19971130) /* Sunday */
325 week_1stday = 0;
326 else if (week_origin == 19971201) /* Monday */
327 week_1stday = 1;
328 else
329 g_warning ("Unknown value of _NL_TIME_WEEK_1STDAY.\n");
330
331 week_start = (week_1stday + first_weekday - 1) % 7;
332
333 #else
334
335 gchar *gtk_week_start;
336
337
338 /* Use a define to hide the string from xgettext */
339 # define GTK_WEEK_START "calendar:week_start:0"
340 gtk_week_start = dgettext ("gtk30", GTK_WEEK_START);
341
342 if (strncmp (gtk_week_start, "calendar:week_start:", 20) == 0)
343 week_start = *(gtk_week_start + 20) - '0';
344 else
345 week_start = -1;
346
347 if (week_start < 0 || week_start > 6)
348 {
349 g_warning ("Whoever translated calendar:week_start:0 for GTK+ "
350 "did so wrongly.\n");
351 week_start = 0;
352 }
353
354 #endif
355
356 return week_start;
357 }
358
359 /**
360 * build_component_from_details:
361 * @summary:
362 * @initial_date:
363 * @final_date:
364 *
365 * Create a component with the provided details
366 *
367 * Returns: (transfer full): an {@link ECalComponent} object
368 **/
369 ECalComponent*
build_component_from_details(const gchar * summary,GDateTime * initial_date,GDateTime * final_date)370 build_component_from_details (const gchar *summary,
371 GDateTime *initial_date,
372 GDateTime *final_date)
373 {
374 GcalApplication *application;
375 GcalContext *context;
376 ECalComponent *event;
377 ECalComponentDateTime *dt;
378 ECalComponentText *summ;
379 ICalTimezone *tz;
380 ICalTime *itt;
381 gboolean all_day;
382
383 application = GCAL_APPLICATION (g_application_get_default ());
384 context = gcal_application_get_context (application);
385 event = e_cal_component_new ();
386 e_cal_component_set_new_vtype (event, E_CAL_COMPONENT_EVENT);
387
388 /*
389 * Check if the event is all day. Notice that it can be all day even
390 * without the final date.
391 */
392 all_day = gcal_date_time_is_date (initial_date) && (final_date ? gcal_date_time_is_date (final_date) : TRUE);
393
394 /*
395 * When the event is all day, we consider UTC timezone by default. Otherwise,
396 * we always use the system timezone to create new events
397 */
398 if (all_day)
399 {
400 tz = i_cal_timezone_get_utc_timezone ();
401 }
402 else
403 {
404 GTimeZone *zone;
405
406 zone = gcal_context_get_timezone (context);
407 tz = gcal_timezone_to_icaltimezone (zone);
408 }
409
410 /* Start date */
411 itt = gcal_date_time_to_icaltime (initial_date);
412 i_cal_time_set_timezone (itt, tz);
413 i_cal_time_set_is_date (itt, all_day);
414 dt = e_cal_component_datetime_new_take (itt, tz ? g_strdup (i_cal_timezone_get_tzid (tz)) : NULL);
415 e_cal_component_set_dtstart (event, dt);
416
417 e_cal_component_datetime_free (dt);
418
419 /* End date */
420 if (!final_date)
421 final_date = g_date_time_add_days (initial_date, 1);
422
423 itt = gcal_date_time_to_icaltime (final_date);
424 i_cal_time_set_timezone (itt, tz);
425 i_cal_time_set_is_date (itt, all_day);
426 dt = e_cal_component_datetime_new_take (itt, tz ? g_strdup (i_cal_timezone_get_tzid (tz)) : NULL);
427 e_cal_component_set_dtend (event, dt);
428
429 e_cal_component_datetime_free (dt);
430
431 /* Summary */
432 summ = e_cal_component_text_new (summary, NULL);
433 e_cal_component_set_summary (event, summ);
434 e_cal_component_text_free (summ);
435
436 e_cal_component_commit_sequence (event);
437
438 return event;
439 }
440
441 /**
442 * icaltime_compare_date:
443 * @date1: an #ICalTime
444 * @date2: an #ICalTime
445 *
446 * Compare date parts of #ICalTime objects. Returns negative value,
447 * 0 or positive value accordingly if @date1 is before, same day or
448 * after date2.
449 *
450 * As a bonus it returns the amount of days passed between two days on the
451 * same year.
452 *
453 * Returns: negative, 0 or positive
454 **/
455 gint
icaltime_compare_date(const ICalTime * date1,const ICalTime * date2)456 icaltime_compare_date (const ICalTime *date1,
457 const ICalTime *date2)
458 {
459 if (date2 == NULL)
460 return 0;
461
462 if (i_cal_time_get_year (date1) < i_cal_time_get_year (date2))
463 return -1;
464 else if (i_cal_time_get_year (date1) > i_cal_time_get_year (date2))
465 return 1;
466 else
467 return time_day_of_year (i_cal_time_get_day (date1), i_cal_time_get_month (date1) - 1, i_cal_time_get_year (date1)) -
468 time_day_of_year (i_cal_time_get_day (date2), i_cal_time_get_month (date2) - 1, i_cal_time_get_year (date2));
469 }
470
471 /**
472 * icaltime_compare_with_current:
473 * @date1: an #ICalTime
474 * @date2: an #ICalTime
475 * @current_time_t: the current time
476 *
477 * Compares @date1 and @date2 against the current time. Dates
478 * closer to the current date are sorted before.
479 *
480 * Returns: negative if @date1 comes after @date2, 0 if they're
481 * equal, positive otherwise
482 */
483 gint
icaltime_compare_with_current(const ICalTime * date1,const ICalTime * date2,time_t * current_time_t)484 icaltime_compare_with_current (const ICalTime *date1,
485 const ICalTime *date2,
486 time_t *current_time_t)
487 {
488 GcalApplication *application;
489 GcalContext *context;
490 GTimeZone *zone;
491 ICalTimezone *zone1, *zone2;
492 gint result = 0;
493 time_t start1, start2, diff1, diff2;
494
495 application = GCAL_APPLICATION (g_application_get_default ());
496 context = gcal_application_get_context (application);
497 zone = gcal_context_get_timezone (context);
498
499 zone1 = i_cal_time_get_timezone (date1);
500 if (!zone1)
501 zone1 = gcal_timezone_to_icaltimezone (zone);
502
503 zone2 = i_cal_time_get_timezone (date2);
504 if (!zone2)
505 zone2 = gcal_timezone_to_icaltimezone (zone);
506
507 start1 = i_cal_time_as_timet_with_zone (date1, zone1);
508 start2 = i_cal_time_as_timet_with_zone (date2, zone2);
509 diff1 = start1 - *current_time_t;
510 diff2 = start2 - *current_time_t;
511
512 if (diff1 != diff2)
513 {
514 if (diff1 == 0)
515 result = -1;
516 else if (diff2 == 0)
517 result = 1;
518
519 if (diff1 > 0 && diff2 < 0)
520 result = -1;
521 else if (diff2 > 0 && diff1 < 0)
522 result = 1;
523 else if (diff1 < 0 && diff2 < 0)
524 result = ABS (diff1) - ABS (diff2);
525 else if (diff1 > 0 && diff2 > 0)
526 result = diff1 - diff2;
527 }
528
529 return result;
530 }
531
532 /**
533 * is_clock_format_24h:
534 *
535 * Retrieves whether the current clock format is
536 * 12h or 24h based.
537 *
538 * Returns: %TRUE if the clock format is 24h, %FALSE
539 * otherwise.
540 */
541 gboolean
is_clock_format_24h(void)542 is_clock_format_24h (void)
543 {
544 static GSettings *settings = NULL;
545 g_autofree gchar *clock_format = NULL;
546
547 if (!settings)
548 settings = g_settings_new ("org.gnome.desktop.interface");
549
550 clock_format = g_settings_get_string (settings, "clock-format");
551
552 return g_strcmp0 (clock_format, "24h") == 0;
553 }
554
555 /**
556 * e_strftime_fix_am_pm:
557 *
558 * Function to do a last minute fixup of the AM/PM stuff if the locale
559 * and gettext haven't done it right. Most English speaking countries
560 * except the USA use the 24 hour clock (UK, Australia etc). However
561 * since they are English nobody bothers to write a language
562 * translation (gettext) file. So the locale turns off the AM/PM, but
563 * gettext does not turn on the 24 hour clock. Leaving a mess.
564 *
565 * This routine checks if AM/PM are defined in the locale, if not it
566 * forces the use of the 24 hour clock.
567 *
568 * The function itself is a front end on strftime and takes exactly
569 * the same arguments.
570 *
571 * TODO: Actually remove the '%p' from the fixed up string so that
572 * there isn't a stray space.
573 */
574 gsize
e_strftime_fix_am_pm(gchar * str,gsize max,const gchar * fmt,const struct tm * tm)575 e_strftime_fix_am_pm (gchar *str,
576 gsize max,
577 const gchar *fmt,
578 const struct tm *tm)
579 {
580 gchar buf[10];
581 gchar *sp;
582 gchar *ffmt;
583 gsize ret;
584
585 if (strstr(fmt, "%p")==NULL && strstr(fmt, "%P")==NULL) {
586 /* No AM/PM involved - can use the fmt string directly */
587 ret = e_strftime (str, max, fmt, tm);
588 } else {
589 /* Get the AM/PM symbol from the locale */
590 e_strftime (buf, 10, "%p", tm);
591
592 if (buf[0]) {
593 /* AM/PM have been defined in the locale
594 * so we can use the fmt string directly. */
595 ret = e_strftime (str, max, fmt, tm);
596 } else {
597 /* No AM/PM defined by locale
598 * must change to 24 hour clock. */
599 ffmt = g_strdup (fmt);
600 for (sp=ffmt; (sp=strstr(sp, "%l")); sp++) {
601 /* Maybe this should be 'k', but I have never
602 * seen a 24 clock actually use that format. */
603 sp[1]='H';
604 }
605 for (sp=ffmt; (sp=strstr(sp, "%I")); sp++) {
606 sp[1]='H';
607 }
608 ret = e_strftime (str, max, ffmt, tm);
609 g_free (ffmt);
610 }
611 }
612
613 return (ret);
614 }
615
616 /**
617 * e_utf8_strftime_fix_am_pm:
618 *
619 * Stolen from Evolution codebase. Selects the
620 * correct time format.
621 *
622 * Returns: the size of the string
623 */
624 gsize
e_utf8_strftime_fix_am_pm(gchar * str,gsize max,const gchar * fmt,const struct tm * tm)625 e_utf8_strftime_fix_am_pm (gchar *str,
626 gsize max,
627 const gchar *fmt,
628 const struct tm *tm)
629 {
630 gsize sz, ret;
631 gchar *locale_fmt, *buf;
632
633 locale_fmt = g_locale_from_utf8 (fmt, -1, NULL, &sz, NULL);
634 if (!locale_fmt)
635 return 0;
636
637 ret = e_strftime_fix_am_pm (str, max, locale_fmt, tm);
638 if (!ret) {
639 g_free (locale_fmt);
640 return 0;
641 }
642
643 buf = g_locale_to_utf8 (str, ret, NULL, &sz, NULL);
644 if (!buf) {
645 g_free (locale_fmt);
646 return 0;
647 }
648
649 if (sz >= max) {
650 gchar *tmp = buf + max - 1;
651 tmp = g_utf8_find_prev_char (buf, tmp);
652 if (tmp)
653 sz = tmp - buf;
654 else
655 sz = 0;
656 }
657 memcpy (str, buf, sz);
658 str[sz] = '\0';
659 g_free (locale_fmt);
660 g_free (buf);
661 return sz;
662 }
663
664
665 /**
666 * fix_popover_menu_icons:
667 * @window: a #GtkPopover
668 *
669 * Hackish code that inspects the popover's children,
670 * retrieve the hidden GtkImage buried under lots of
671 * widgets, and make it visible again.
672 *
673 * Hopefully, we'll find a better way to do this in
674 * the long run.
675 *
676 */
677 void
fix_popover_menu_icons(GtkPopover * popover)678 fix_popover_menu_icons (GtkPopover *popover)
679 {
680 GtkWidget *popover_stack;
681 GtkWidget *menu_section;
682 GtkWidget *menu_section_box;
683 GList *stack_children;
684 GList *menu_section_children;
685 GList *menu_section_box_children, *aux;
686
687 popover_stack = gtk_bin_get_child (GTK_BIN (popover));
688 stack_children = gtk_container_get_children (GTK_CONTAINER (popover_stack));
689
690 /**
691 * At the moment, the popover stack surely contains only
692 * one child of type GtkMenuSectionBox, which contains
693 * a single GtkBox.
694 */
695 menu_section = stack_children->data;
696 menu_section_children = gtk_container_get_children (GTK_CONTAINER (menu_section));
697
698 /**
699 * Get the unique box's children.
700 */
701 menu_section_box = menu_section_children->data;
702 menu_section_box_children = gtk_container_get_children (GTK_CONTAINER (menu_section_box));
703
704 gtk_style_context_add_class (gtk_widget_get_style_context (menu_section_box), "calendars-list");
705
706 /**
707 * Iterate through the GtkModelButtons inside the menu section box.
708 */
709 for (aux = menu_section_box_children; aux != NULL; aux = aux->next)
710 {
711 GtkWidget *button_box;
712 GList *button_box_children, *aux2;
713
714 button_box = gtk_bin_get_child (GTK_BIN (aux->data));
715 button_box_children = gtk_container_get_children (GTK_CONTAINER (button_box));
716
717 /**
718 * Since there is no guarantee that the first child is
719 * the GtkImage we're looking for, we have to iterate
720 * through the children and check if the types match.
721 */
722 for (aux2 = button_box_children; aux2 != NULL; aux2 = aux2->next)
723 {
724 GtkWidget *button_box_child;
725 button_box_child = aux2->data;
726
727 if (g_type_is_a (G_OBJECT_TYPE (button_box_child), GTK_TYPE_IMAGE))
728 {
729 gtk_style_context_add_class (gtk_widget_get_style_context (button_box_child), "calendar-color-image");
730 gtk_widget_show (button_box_child);
731 break;
732 }
733 }
734
735 g_list_free (button_box_children);
736 }
737
738 g_list_free (stack_children);
739 g_list_free (menu_section_children);
740 g_list_free (menu_section_box_children);
741 }
742
743 /**
744 * get_source_parent_name_color:
745 * @manager: a #GcalManager
746 * @source: an #ESource
747 * @name: (nullable): return location for the name
748 * @color: (nullable): return location for the color
749 *
750 * Retrieves the name and the color of the #ESource that is
751 * parent of @source.
752 */
753 void
get_source_parent_name_color(GcalManager * manager,ESource * source,gchar ** name,gchar ** color)754 get_source_parent_name_color (GcalManager *manager,
755 ESource *source,
756 gchar **name,
757 gchar **color)
758 {
759 ESource *parent_source;
760
761 g_assert (source && E_IS_SOURCE (source));
762
763 parent_source = gcal_manager_get_source (manager, e_source_get_parent (source));
764
765 if (name != NULL)
766 *name = e_source_dup_display_name (parent_source);
767
768 if (color != NULL)
769 {
770 GdkRGBA c;
771
772 get_color_name_from_source (parent_source, &c);
773
774 *color = gdk_rgba_to_string (&c);
775 }
776 }
777
778 /**
779 * format_utc_offset:
780 * @offset: an UTC offset
781 *
782 * Formats the UTC offset to a string that GTimeZone can
783 * parse. E.g. "-0300" or "+0530".
784 *
785 * Returns: (transfer full): a string representing the
786 * offset
787 */
788 gchar*
format_utc_offset(gint64 offset)789 format_utc_offset (gint64 offset)
790 {
791 const char *sign = "+";
792 gint hours, minutes, seconds;
793
794 if (offset < 0) {
795 offset = -offset;
796 sign = "-";
797 }
798
799 /* offset can be seconds or microseconds */
800 if (offset >= 1000000)
801 offset = offset / 1000000;
802
803 hours = offset / 3600;
804 minutes = (offset % 3600) / 60;
805 seconds = offset % 60;
806
807 if (seconds > 0)
808 return g_strdup_printf ("%s%02i%02i%02i", sign, hours, minutes, seconds);
809 else
810 return g_strdup_printf ("%s%02i%02i", sign, hours, minutes);
811 }
812
813 /**
814 * get_alarm_trigger_minutes:
815 * @event: a #GcalEvent
816 * @alarm: a #ECalComponentAlarm
817 *
818 * Calculates the number of minutes before @event's
819 * start time that the alarm should be triggered.
820 *
821 * Returns: the number of minutes before the event
822 * start that @alarm will be triggered.
823 */
824 gint
get_alarm_trigger_minutes(GcalEvent * event,ECalComponentAlarm * alarm)825 get_alarm_trigger_minutes (GcalEvent *event,
826 ECalComponentAlarm *alarm)
827 {
828 ECalComponentAlarmTrigger *trigger;
829 ICalDuration *duration;
830 GDateTime *alarm_dt;
831 gint diff;
832
833 trigger = e_cal_component_alarm_get_trigger (alarm);
834
835 /*
836 * We only support alarms relative to the start date, and solely
837 * ignore whetever different it may be.
838 */
839 if (!trigger || e_cal_component_alarm_trigger_get_kind (trigger) != E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START)
840 return -1;
841
842 duration = e_cal_component_alarm_trigger_get_duration (trigger);
843 alarm_dt = g_date_time_add_full (gcal_event_get_date_start (event),
844 0,
845 0,
846 - (i_cal_duration_get_days (duration) + i_cal_duration_get_weeks (duration) * 7),
847 - i_cal_duration_get_hours (duration),
848 - i_cal_duration_get_minutes (duration),
849 - i_cal_duration_get_seconds (duration));
850
851 diff = g_date_time_difference (gcal_event_get_date_start (event), alarm_dt) / G_TIME_SPAN_MINUTE;
852
853 g_clear_pointer (&alarm_dt, g_date_time_unref);
854
855 return diff;
856 }
857
858 /**
859 * should_change_date_for_scroll:
860 * @scroll_value: the current scroll value
861 * @scroll_event: the #GdkEventScroll that is being parsed
862 *
863 * Utility function to check if the date should change based
864 * on the scroll. The date is changed when the user scrolls
865 * too much on touchpad, or performs a rotation of the scroll
866 * button in a mouse.
867 *
868 * Returns: %TRUE if the date should change, %FALSE otherwise.
869 */
870 gboolean
should_change_date_for_scroll(gdouble * scroll_value,GdkEventScroll * scroll_event)871 should_change_date_for_scroll (gdouble *scroll_value,
872 GdkEventScroll *scroll_event)
873 {
874 gdouble delta_y;
875
876 switch (scroll_event->direction)
877 {
878 case GDK_SCROLL_DOWN:
879 *scroll_value = SCROLL_HARDNESS;
880 break;
881
882 case GDK_SCROLL_UP:
883 *scroll_value = -SCROLL_HARDNESS;
884 break;
885
886 case GDK_SCROLL_SMOOTH:
887 gdk_event_get_scroll_deltas ((GdkEvent*) scroll_event, NULL, &delta_y);
888 *scroll_value += delta_y;
889 break;
890
891 /* Ignore horizontal scrolling for now */
892 case GDK_SCROLL_LEFT:
893 case GDK_SCROLL_RIGHT:
894 default:
895 break;
896 }
897
898 if (*scroll_value <= -SCROLL_HARDNESS || *scroll_value >= SCROLL_HARDNESS)
899 return TRUE;
900
901 return FALSE;
902 }
903
904 /**
905 * is_source_enabled:
906 * @source: an #ESource
907 *
908 * Retrieves whether the @source is enabled or not.
909 * Disabled sources don't show their events.
910 *
911 * Returns: %TRUE if @source is enabled, %FALSE otherwise.
912 */
913 gboolean
is_source_enabled(ESource * source)914 is_source_enabled (ESource *source)
915 {
916 ESourceSelectable *selectable;
917
918 g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
919
920 selectable = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR);
921
922 return e_source_selectable_get_selected (selectable);
923 }
924
925 /**
926 * ask_recurrence_modification_type:
927 * @parent: a #GtkWidget
928 * @modtype: an #ECalObjModType
929 * @source: an #ESource
930 *
931 * Assigns the appropriate modtype while modifying an event
932 * based on user's choice in the GtkMessageDialog that pops up.
933 * The modtype helps the user choose the part of recurrent events
934 * to modify. Such as Only This Event, Subsequent events
935 * or All events.
936 *
937 * Returns: %TRUE if user chooses appropriate option and
938 * @modtype is assigned, %FALSE otherwise.
939 */
940 gboolean
ask_recurrence_modification_type(GtkWidget * parent,GcalRecurrenceModType * modtype,GcalCalendar * calendar)941 ask_recurrence_modification_type (GtkWidget *parent,
942 GcalRecurrenceModType *modtype,
943 GcalCalendar *calendar)
944 {
945 GtkDialogFlags flags;
946 ECalClient *client;
947 GtkWidget *dialog;
948 gboolean is_set;
949 gint result;
950
951 flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT;
952 *modtype = GCAL_RECURRENCE_MOD_THIS_ONLY;
953
954 dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (parent)),
955 flags,
956 GTK_MESSAGE_QUESTION,
957 GTK_BUTTONS_NONE,
958 _("The event you are trying to modify is recurring. The changes you have selected should be applied to:"));
959
960 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
961 _("_Cancel"),
962 GTK_RESPONSE_CANCEL,
963 _("_Only This Event"),
964 GTK_RESPONSE_ACCEPT,
965 NULL);
966
967 client = gcal_calendar_get_client (calendar);
968
969 if (!e_client_check_capability (E_CLIENT (client), E_CAL_STATIC_CAPABILITY_NO_THISANDFUTURE))
970 gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Subsequent events"), GTK_RESPONSE_OK);
971
972 gtk_dialog_add_button (GTK_DIALOG (dialog), _("_All events"), GTK_RESPONSE_YES);
973
974 gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (gtk_widget_get_toplevel (parent)));
975
976 result = gtk_dialog_run (GTK_DIALOG (dialog));
977
978 switch (result)
979 {
980 case GTK_RESPONSE_CANCEL:
981 is_set = FALSE;
982 break;
983 case GTK_RESPONSE_ACCEPT:
984 *modtype = GCAL_RECURRENCE_MOD_THIS_ONLY;
985 is_set = TRUE;
986 break;
987 case GTK_RESPONSE_OK:
988 *modtype = GCAL_RECURRENCE_MOD_THIS_AND_FUTURE;
989 is_set = TRUE;
990 break;
991 case GTK_RESPONSE_YES:
992 *modtype = GCAL_RECURRENCE_MOD_ALL;
993 is_set = TRUE;
994 break;
995 default:
996 is_set = FALSE;
997 break;
998 }
999
1000 gtk_widget_destroy (GTK_WIDGET (dialog));
1001
1002 return is_set;
1003 }
1004
1005 struct
1006 {
1007 const gchar *territory;
1008 GcalWeekDay no_work_days;
1009 } no_work_day_per_locale[] = {
1010 { "AE", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* United Arab Emirates */,
1011 { "AF", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Afghanistan */,
1012 { "BD", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Bangladesh */,
1013 { "BH", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Bahrain */,
1014 { "BN", GCAL_WEEK_DAY_SUNDAY | GCAL_WEEK_DAY_FRIDAY } /* Brunei Darussalam */,
1015 { "CR", GCAL_WEEK_DAY_SATURDAY } /* Costa Rica */,
1016 { "DJ", GCAL_WEEK_DAY_FRIDAY } /* Djibouti */,
1017 { "DZ", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Algeria */,
1018 { "EG", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Egypt */,
1019 { "GN", GCAL_WEEK_DAY_SATURDAY } /* Equatorial Guinea */,
1020 { "HK", GCAL_WEEK_DAY_SATURDAY } /* Hong Kong */,
1021 { "IL", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Israel */,
1022 { "IQ", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Iraq */,
1023 { "IR", GCAL_WEEK_DAY_THURSDAY | GCAL_WEEK_DAY_FRIDAY } /* Iran */,
1024 { "KW", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Kuwait */,
1025 { "KZ", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Kazakhstan */,
1026 { "LY", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Libya */,
1027 { "MX", GCAL_WEEK_DAY_SATURDAY } /* Mexico */,
1028 { "MY", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Malaysia */,
1029 { "NP", GCAL_WEEK_DAY_SATURDAY } /* Nepal */,
1030 { "OM", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Oman */,
1031 { "QA", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Qatar */,
1032 { "SA", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Saudi Arabia */,
1033 { "SU", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Sudan */,
1034 { "SY", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Syria */,
1035 { "UG", GCAL_WEEK_DAY_SUNDAY } /* Uganda */,
1036 { "YE", GCAL_WEEK_DAY_FRIDAY | GCAL_WEEK_DAY_SATURDAY } /* Yemen */,
1037 };
1038
1039
1040 /**
1041 * is_workday:
1042 * @day: a guint representing the day of a week (0…Sunday, 6…Saturday)
1043 *
1044 * Checks whether @day is workday or not based on the Territory part of Locale.
1045 *
1046 * Returns: %TRUE if @day is a workday, %FALSE otherwise.
1047 */
1048 gboolean
is_workday(guint day)1049 is_workday (guint day)
1050 {
1051 GcalWeekDay no_work_days;
1052 gchar *locale;
1053 gchar territory[3] = { 0, };
1054 guint i;
1055
1056 if (day > 6)
1057 return FALSE;
1058
1059 no_work_days = GCAL_WEEK_DAY_SATURDAY | GCAL_WEEK_DAY_SUNDAY;
1060
1061 locale = setlocale (LC_TIME, NULL);
1062
1063 if (!locale || g_utf8_strlen (locale, -1) < 5)
1064 {
1065 g_warning ("Locale is unset or lacks territory code, assuming Saturday and Sunday as non workdays");
1066 return !(no_work_days & 1 << day);
1067 }
1068
1069 territory[0] = locale[3];
1070 territory[1] = locale[4];
1071
1072 for (i = 0; i < G_N_ELEMENTS (no_work_day_per_locale); i++)
1073 {
1074 if (g_strcmp0 (territory, no_work_day_per_locale[i].territory) == 0)
1075 {
1076 no_work_days = no_work_day_per_locale[i].no_work_days;
1077 break;
1078 }
1079 }
1080
1081 return !(no_work_days & 1 << day);
1082 }
1083
1084 GList*
filter_event_list_by_uid_and_modtype(GList * widgets,GcalRecurrenceModType mod,const gchar * uid)1085 filter_event_list_by_uid_and_modtype (GList *widgets,
1086 GcalRecurrenceModType mod,
1087 const gchar *uid)
1088 {
1089 GcalEvent *event;
1090 GList *result;
1091 GList *l;
1092
1093 event = NULL;
1094 result = NULL;
1095
1096 /* First pass: find the GcalEvent */
1097 for (l = widgets; l != NULL; l = l->next)
1098 {
1099 GcalEventWidget *event_widget;
1100 GcalEvent *ev;
1101
1102 event_widget = l->data;
1103
1104 /* Safeguard against stray widgets */
1105 if (!GCAL_IS_EVENT_WIDGET (event_widget))
1106 continue;
1107
1108 ev = gcal_event_widget_get_event (event_widget);
1109
1110 /*
1111 * We can assume only one event will have the exact uuid. Even among
1112 * recurrencies.
1113 */
1114 if (g_str_equal (uid, gcal_event_get_uid (ev)))
1115 {
1116 result = g_list_prepend (result, event_widget);
1117 event = ev;
1118 }
1119 }
1120
1121 /* Second pass: find the other related events */
1122 if (event && mod != GCAL_RECURRENCE_MOD_THIS_ONLY)
1123 {
1124 g_autofree gchar *id_prefix = NULL;
1125 ECalComponentId *id;
1126 ECalComponent *component;
1127 GcalCalendar *calendar;
1128
1129 component = gcal_event_get_component (event);
1130 calendar = gcal_event_get_calendar (event);
1131 id = e_cal_component_get_id (component);
1132 id_prefix = g_strdup_printf ("%s:%s", gcal_calendar_get_id (calendar), e_cal_component_id_get_uid (id));
1133
1134 for (l = widgets; l != NULL; l = l->next)
1135 {
1136 GcalEventWidget *event_widget;
1137 GcalEvent *ev;
1138
1139 event_widget = l->data;
1140
1141 /* Safeguard against stray widgets */
1142 if (!GCAL_IS_EVENT_WIDGET (event_widget))
1143 continue;
1144
1145 ev = gcal_event_widget_get_event (event_widget);
1146
1147 if (g_str_equal (gcal_event_get_uid (ev), uid))
1148 continue;
1149
1150 if (!g_str_has_prefix (gcal_event_get_uid (ev), id_prefix))
1151 continue;
1152
1153 if (mod == GCAL_RECURRENCE_MOD_ALL)
1154 {
1155 result = g_list_prepend (result, event_widget);
1156 }
1157 else if (mod == GCAL_RECURRENCE_MOD_THIS_AND_FUTURE)
1158 {
1159 if (g_date_time_compare (gcal_event_get_date_start (event), gcal_event_get_date_start (ev)) < 0)
1160 result = g_list_prepend (result, event_widget);
1161 }
1162
1163 }
1164
1165 e_cal_component_id_free (id);
1166 }
1167
1168 return result;
1169 }
1170
1171 gboolean
gcal_translate_child_window_position(GtkWidget * target,GdkWindow * child_window,gdouble src_x,gdouble src_y,gdouble * real_x,gdouble * real_y)1172 gcal_translate_child_window_position (GtkWidget *target,
1173 GdkWindow *child_window,
1174 gdouble src_x,
1175 gdouble src_y,
1176 gdouble *real_x,
1177 gdouble *real_y)
1178 {
1179 GdkWindow *window;
1180 gdouble x, y;
1181
1182 x = src_x;
1183 y = src_y;
1184
1185 /* Find the (x, y) values relative to the workbench */
1186 window = child_window;
1187 while (window && window != gtk_widget_get_window (target))
1188 {
1189 gdk_window_coords_to_parent (window, x, y, &x, &y);
1190 window = gdk_window_get_parent (window);
1191 }
1192
1193 if (!window)
1194 return FALSE;
1195
1196 if (real_x)
1197 *real_x = x;
1198
1199 if (real_y)
1200 *real_y = y;
1201
1202 return TRUE;
1203 }
1204
1205 void
gcal_utils_launch_online_accounts_panel(GDBusConnection * connection,const gchar * action,const gchar * arg)1206 gcal_utils_launch_online_accounts_panel (GDBusConnection *connection,
1207 const gchar *action,
1208 const gchar *arg)
1209 {
1210 g_autoptr (GDBusProxy) proxy = NULL;
1211 GVariantBuilder builder;
1212 GVariant *params[3];
1213 GVariant *array[1];
1214
1215 g_variant_builder_init (&builder, G_VARIANT_TYPE ("av"));
1216
1217 if (!action && !arg)
1218 {
1219 g_variant_builder_add (&builder, "v", g_variant_new_string (""));
1220 }
1221 else
1222 {
1223 if (action)
1224 g_variant_builder_add (&builder, "v", g_variant_new_string (action));
1225
1226 if (arg)
1227 g_variant_builder_add (&builder, "v", g_variant_new_string (arg));
1228 }
1229
1230 array[0] = g_variant_new ("v", g_variant_new ("(sav)", "online-accounts", &builder));
1231
1232 params[0] = g_variant_new_string ("launch-panel");
1233 params[1] = g_variant_new_array (G_VARIANT_TYPE ("v"), array, 1);
1234 params[2] = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0);
1235
1236 proxy = g_dbus_proxy_new_sync (connection,
1237 G_DBUS_PROXY_FLAGS_NONE,
1238 NULL,
1239 "org.gnome.ControlCenter",
1240 "/org/gnome/ControlCenter",
1241 "org.gtk.Actions",
1242 NULL,
1243 NULL);
1244
1245 if (!proxy)
1246 {
1247 g_warning ("Couldn't open Online Accounts panel");
1248 return;
1249 }
1250
1251 g_dbus_proxy_call_sync (proxy,
1252 "Activate",
1253 g_variant_new_tuple (params, 3),
1254 G_DBUS_CALL_FLAGS_NONE,
1255 -1,
1256 NULL,
1257 NULL);
1258 }
1259
1260 gchar*
gcal_utils_format_filename_for_display(const gchar * filename)1261 gcal_utils_format_filename_for_display (const gchar *filename)
1262 {
1263 /*
1264 * Foo_bar-something-cool.ics
1265 */
1266 g_autofree gchar *display_name = NULL;
1267 gchar *file_extension;
1268
1269 display_name = g_strdup (filename);
1270
1271 /* Strip out the file extension */
1272 file_extension = g_strrstr (display_name, ".");
1273 if (file_extension)
1274 *file_extension = '\0';
1275
1276 /* Replace underscores with spaces */
1277 display_name = g_strdelimit (display_name, "_", ' ');
1278 display_name = g_strstrip (display_name);
1279
1280 return g_steal_pointer (&display_name);
1281 }
1282
1283 void
gcal_utils_extract_google_section(const gchar * description,gchar ** out_description,gchar ** out_meeting_url)1284 gcal_utils_extract_google_section (const gchar *description,
1285 gchar **out_description,
1286 gchar **out_meeting_url)
1287 {
1288 g_autofree gchar *actual_description = NULL;
1289 g_autofree gchar *meeting_url = NULL;
1290 gssize description_len;
1291 gsize delimiter_len;
1292 gchar *first_delimiter;
1293 gchar *last_delimiter;
1294
1295 if (!description)
1296 goto out;
1297
1298 #define GOOGLE_DELIMITER "-::~:~::~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~::~:~::-"
1299
1300 description_len = strlen (description);
1301 first_delimiter = g_strstr_len (description, description_len, GOOGLE_DELIMITER);
1302 if (!first_delimiter)
1303 goto out;
1304
1305 delimiter_len = strlen (GOOGLE_DELIMITER);
1306 last_delimiter = g_strstr_len (first_delimiter + delimiter_len,
1307 description_len,
1308 GOOGLE_DELIMITER);
1309 if (!last_delimiter)
1310 goto out;
1311
1312 if (out_description)
1313 actual_description = g_utf8_substring (description, 0, first_delimiter - description);
1314
1315 if (out_meeting_url)
1316 {
1317 gchar *google_section_start;
1318 gchar *meet_url_start;
1319
1320 google_section_start = first_delimiter + delimiter_len;
1321 meet_url_start = g_strstr_len (google_section_start,
1322 first_delimiter - description - delimiter_len,
1323 "https://meet.google.com");
1324 if (meet_url_start)
1325 meeting_url = g_utf8_substring (meet_url_start, 0, strlen ("https://meet.google.com/xxx-xxxx-xxx"));
1326 }
1327
1328 out:
1329 if (out_description)
1330 *out_description = actual_description ? g_steal_pointer (&actual_description) : g_strdup (description);
1331
1332 if (out_meeting_url)
1333 *out_meeting_url = g_steal_pointer (&meeting_url);
1334 }
1335