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