/* gcal-quick-add-popover.c * * Copyright (C) 2016-2020 Georges Basile Stavracas Neto * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define G_LOG_DOMAIN "GcalQuickAddPopover" #include #include "gcal-quick-add-popover.h" #include "gcal-calendar-row.h" #include "gcal-utils.h" struct _GcalQuickAddPopover { GtkPopover parent; GtkWidget *add_button; GtkWidget *calendars_list_box; GtkWidget *color_image; GtkWidget *edit_button; GtkWidget *summary_entry; GtkWidget *title_label; /* Internal data */ GcalRange *range; GListModel *read_write_calendars_model; }; G_DEFINE_TYPE (GcalQuickAddPopover, gcal_quick_add_popover, GTK_TYPE_POPOVER) enum { PROP_0, PROP_RANGE, N_PROPS }; enum { EDIT_EVENT, NUM_SIGNALS }; static guint signals[NUM_SIGNALS] = { 0, }; /* * Utility functions */ static void row_selected_cb (GtkListBox *list_box, GtkListBoxRow *row, GcalQuickAddPopover *self) { GtkWidget *check_button; if (!row) return; check_button = gcal_calendar_row_get_first_suffix_child (GCAL_CALENDAR_ROW (row)); gtk_check_button_set_active (GTK_CHECK_BUTTON (check_button), TRUE); } static void emit_changed (GcalQuickAddPopover *self) { GtkFilter *filter; filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->read_write_calendars_model)); gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); } static GtkWidget* create_row_func (gpointer data, gpointer user_data) { GcalCalendar *default_calendar; GcalCalendar *calendar = data; GcalQuickAddPopover *self = user_data; GcalContext *context; GcalManager *manager; g_autoptr (GdkPaintable) paintable = NULL; g_autofree gchar *parent_name = NULL; g_autofree gchar *tooltip = NULL; GtkWidget *first_row, *row, *check_button; context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION); manager = gcal_context_get_manager (context); default_calendar = gcal_manager_get_default_calendar (manager); /* The check button */ check_button = gtk_check_button_new (); gtk_widget_set_valign (check_button, GTK_ALIGN_CENTER); gtk_widget_set_can_focus (check_button, FALSE); gtk_widget_set_can_target (check_button, FALSE); /* Turn these check buttons into radio buttons */ first_row = gtk_widget_get_first_child (self->calendars_list_box); if (first_row) { GtkWidget *first_check_button; first_check_button = gcal_calendar_row_get_first_suffix_child (GCAL_CALENDAR_ROW (first_row)); gtk_check_button_set_group (GTK_CHECK_BUTTON (check_button), GTK_CHECK_BUTTON (first_check_button)); } row = gcal_calendar_row_new (calendar); gcal_calendar_row_add_header_class (GCAL_CALENDAR_ROW (row)); gcal_calendar_row_add_suffix (GCAL_CALENDAR_ROW (row), check_button); if (calendar == default_calendar) gtk_list_box_select_row (GTK_LIST_BOX (self->calendars_list_box), GTK_LIST_BOX_ROW (row)); g_signal_connect_object (calendar, "notify::visible", G_CALLBACK (emit_changed), self, G_CONNECT_SWAPPED); g_signal_connect_object (calendar, "notify::read-only", G_CALLBACK (emit_changed), self, G_CONNECT_SWAPPED); return row; } static void bind_model (GcalQuickAddPopover *self) { gtk_list_box_bind_model (GTK_LIST_BOX (self->calendars_list_box), self->read_write_calendars_model, create_row_func, self, NULL); } static gint get_number_of_days_from_today (GDateTime *day) { g_autoptr (GDateTime) now = NULL; GDate today, event_day; gint n_days; now = g_date_time_new_now_local (); g_date_set_dmy (&today, g_date_time_get_day_of_month (now), g_date_time_get_month (now), g_date_time_get_year (now)); g_date_set_dmy (&event_day, g_date_time_get_day_of_month (day), g_date_time_get_month (day), g_date_time_get_year (day)); n_days = g_date_days_between (&event_day, &today); return n_days; } static gchar* get_date_string_for_multiday (GDateTime *start, GDateTime *end) { g_autofree gchar *start_date_str = NULL; g_autofree gchar *end_date_str = NULL; gchar *date_string; gint n_days; const gchar *start_date_weekdays_strings[] = { N_("from next Monday"), N_("from next Tuesday"), N_("from next Wednesday"), N_("from next Thursday"), N_("from next Friday"), N_("from next Saturday"), N_("from next Sunday"), NULL }; const gchar *end_date_weekdays_strings[] = { N_("to next Monday"), N_("to next Tuesday"), N_("to next Wednesday"), N_("to next Thursday"), N_("to next Friday"), N_("to next Saturday"), N_("to next Sunday"), NULL }; const gchar *month_names[] = { N_("January"), N_("February"), N_("March"), N_("April"), N_("May"), N_("June"), N_("July"), N_("August"), N_("September"), N_("October"), N_("November"), N_("December"), NULL }; n_days = get_number_of_days_from_today (start); if (n_days == 0) { start_date_str = g_strdup_printf (_("from Today")); } else if (n_days == -1) { start_date_str = g_strdup_printf (_("from Tomorrow")); } else if (n_days == 1) { start_date_str = g_strdup_printf (_("from Yesterday")); } else if (n_days < -1 && n_days > -8) { const gchar *start_weekday_str; start_weekday_str = gettext (start_date_weekdays_strings[g_date_time_get_day_of_week (start) - 1]); start_date_str = g_strdup_printf ("%s", start_weekday_str); } else { g_autofree gchar *day_number_str = NULL; day_number_str = g_strdup_printf ("%u", g_date_time_get_day_of_month (start)); /* Translators: * this is the format string for representing a date consisting of a month * name and a date of month. */ start_date_str = g_strdup_printf (_("from %1$s %2$s"), gettext (month_names[g_date_time_get_month (start) - 1]), day_number_str); } n_days = get_number_of_days_from_today (end); if (n_days == 0) { end_date_str = g_strdup_printf (_("to Today")); } else if (n_days == -1) { end_date_str = g_strdup_printf (_("to Tomorrow")); } else if (n_days == 1) { end_date_str = g_strdup_printf (_("to Yesterday")); } else if (n_days < -1 && n_days > -8) { const gchar *end_weekday_str; end_weekday_str = gettext (end_date_weekdays_strings[g_date_time_get_day_of_week (end) - 1]); end_date_str = g_strdup_printf ("%s", end_weekday_str); } else { g_autofree gchar *day_number_str = NULL; day_number_str = g_strdup_printf ("%u", g_date_time_get_day_of_month (end)); /* Translators: * this is the format string for representing a date consisting of a month * name and a date of month. */ end_date_str = g_strdup_printf (_("to %1$s %2$s"), gettext (month_names[g_date_time_get_month (end) - 1]), day_number_str); } /* Translators: %1$s is the start date (e.g. "from Today") and %2$s is the end date (e.g. "to Tomorrow") */ date_string = g_strdup_printf (_("New Event %1$s %2$s"), start_date_str, end_date_str); return date_string; } static gchar* get_date_string_for_day (GDateTime *day) { gchar *string_for_date; gint n_days; string_for_date = NULL; n_days = get_number_of_days_from_today (day); if (n_days == 0) { string_for_date = g_strdup_printf (_("New Event Today")); } else if (n_days == -1) { string_for_date = g_strdup_printf (_("New Event Tomorrow")); } else if (n_days == 1) { string_for_date = g_strdup_printf (_("New Event Yesterday")); } else if (n_days < -1 && n_days > -8) { const gchar *event_weekday; const gchar *event_weekday_names[] = { N_("New Event next Monday"), N_("New Event next Tuesday"), N_("New Event next Wednesday"), N_("New Event next Thursday"), N_("New Event next Friday"), N_("New Event next Saturday"), N_("New Event next Sunday"), NULL }; event_weekday = gettext (event_weekday_names [g_date_time_get_day_of_week (day) - 1]); string_for_date = g_strdup_printf ("%s", event_weekday); } else { const gchar *event_month; const gchar *event_month_names[] = { /* Translators: %d is the numeric day of month */ N_("New Event on January %d"), N_("New Event on February %d"), N_("New Event on March %d"), N_("New Event on April %d"), N_("New Event on May %d"), N_("New Event on June %d"), N_("New Event on July %d"), N_("New Event on August %d"), N_("New Event on September %d"), N_("New Event on October %d"), N_("New Event on November %d"), N_("New Event on December %d"), NULL }; event_month = gettext (event_month_names [g_date_time_get_month (day) - 1]); string_for_date = g_strdup_printf (event_month, g_date_time_get_day_of_month (day)); } return string_for_date; } static void update_header (GcalQuickAddPopover *self) { g_autoptr (GDateTime) range_start = NULL; g_autoptr (GDateTime) range_end = NULL; g_autofree gchar *title_date = NULL; gboolean multiday_or_timed; GcalContext *context; if (!self->range) return; range_start = gcal_range_get_start (self->range); range_end = gcal_range_get_end (self->range); multiday_or_timed = gcal_range_get_range_type (self->range) == GCAL_RANGE_DEFAULT || gcal_date_time_compare_date (range_start, range_end) != 0; if (multiday_or_timed) { gboolean all_day; all_day = gcal_range_get_range_type (self->range) == GCAL_RANGE_DATE_ONLY; if (all_day && (g_date_time_difference (range_end, range_start) / G_TIME_SPAN_DAY > 1 && g_date_time_difference (range_end, range_start) / G_TIME_SPAN_MINUTE > 1)) { title_date = get_date_string_for_multiday (range_start, g_date_time_add_days (range_end, -1)); } else { g_autofree gchar *event_date_name = NULL; g_autofree gchar *start_hour = NULL; g_autofree gchar *end_hour = NULL; GcalTimeFormat time_format; const gchar *hour_format; context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION); time_format = gcal_context_get_time_format (context); if (time_format == GCAL_TIME_FORMAT_24H) hour_format = "%R"; else hour_format = "%I:%M %P"; start_hour = g_date_time_format (range_start, hour_format); end_hour = g_date_time_format (range_end, hour_format); event_date_name = get_date_string_for_day (range_start); if (all_day) { title_date = g_strdup (event_date_name); } else { /* Translators: %1$s is the event name, %2$s is the start hour, and %3$s is the end hour. * To avoid spurious line wrapping, use non-breaking space characters around the en dash. */ title_date = g_strdup_printf (_("%1$s, %2$s – %3$s"), event_date_name, start_hour, end_hour); } } } else { g_autofree gchar *event_date_name = NULL; event_date_name = get_date_string_for_day (range_start); title_date = g_strdup_printf ("%s", event_date_name); } gtk_label_set_label (GTK_LABEL (self->title_label), title_date); } /* * Callbacks */ static void edit_or_create_event (GcalQuickAddPopover *self, GtkWidget *button) { g_autoptr (GDateTime) range_start = NULL; g_autoptr (GDateTime) range_end = NULL; ECalComponent *component; GcalCalendar *calendar; GtkListBoxRow *selected_row; GcalContext *context; GcalManager *manager; GDateTime *date_start, *date_end; GTimeZone *tz; GcalEvent *event; const gchar *summary; gboolean all_day, single_day; range_start = gcal_range_get_start (self->range); range_end = gcal_range_get_end (self->range); context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION); manager = gcal_context_get_manager (context); selected_row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self->calendars_list_box)); calendar = gcal_calendar_row_get_calendar (GCAL_CALENDAR_ROW (selected_row)); single_day = gcal_date_time_compare_date (range_end, range_start) == 0; all_day = gcal_date_time_compare_date (range_end, range_start) > 1 || g_date_time_compare (range_end, range_start) == 0; tz = all_day ? g_time_zone_new_utc () : g_time_zone_new_local (); /* Gather start date */ date_start = g_date_time_new (tz, g_date_time_get_year (range_start), g_date_time_get_month (range_start), g_date_time_get_day_of_month (range_start), g_date_time_get_hour (range_start), g_date_time_get_minute (range_start), 0); /* Gather date end */ if (range_end) { date_end = g_date_time_new (tz, g_date_time_get_year (range_end), g_date_time_get_month (range_end), g_date_time_get_day_of_month (range_end) + (single_day && all_day ? 1 : 0), g_date_time_get_hour (range_end), g_date_time_get_minute (range_end), 0); } else { date_end = all_day ? g_date_time_add_days (date_start, 1) : g_date_time_add_hours (date_start, 1); } /* Gather the summary */ summary = gtk_editable_get_text (GTK_EDITABLE (self->summary_entry)); /* Create an ECalComponent from the data above */ component = build_component_from_details (summary, date_start, date_end); event = gcal_event_new (calendar, component, NULL); gcal_event_set_all_day (event, all_day); /* If we clicked edit button, send a signal; otherwise, create the event */ if (button == self->add_button) gcal_manager_create_event (manager, event); else g_signal_emit (self, signals[EDIT_EVENT], 0, event); gtk_widget_set_visible (GTK_WIDGET (self), FALSE); g_clear_pointer (&date_start, g_date_time_unref); g_clear_pointer (&date_end, g_date_time_unref); g_clear_pointer (&tz, g_time_zone_unref); g_clear_object (&event); } static void summary_entry_text_changed (AdwEntryRow *entry, GParamSpec *pspec, GcalQuickAddPopover *self) { gboolean is_valid_event_name; is_valid_event_name = gcal_is_valid_event_name (gtk_editable_get_text (GTK_EDITABLE (entry))); if (is_valid_event_name) gtk_widget_remove_css_class (GTK_WIDGET (entry), "error"); else gtk_widget_add_css_class (GTK_WIDGET (entry), "error"); gtk_widget_set_sensitive (self->add_button, is_valid_event_name); } static void summary_entry_activated (AdwEntryRow *entry, GcalQuickAddPopover *self) { if (gcal_is_valid_event_name (gtk_editable_get_text (GTK_EDITABLE (entry)))) edit_or_create_event (self, self->add_button); else edit_or_create_event (self, self->edit_button); } /* * Overrides */ static void gcal_quick_add_popover_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GcalQuickAddPopover *self = GCAL_QUICK_ADD_POPOVER (object); switch (prop_id) { case PROP_RANGE: g_value_set_object (value, self->range); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gcal_quick_add_popover_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GcalQuickAddPopover *self = GCAL_QUICK_ADD_POPOVER (object); switch (prop_id) { case PROP_RANGE: gcal_quick_add_popover_set_range (self, g_value_get_boxed (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gcal_quick_add_popover_closed (GtkPopover *popover) { GcalQuickAddPopover *self; self = GCAL_QUICK_ADD_POPOVER (popover); /* Clear text */ gtk_editable_set_text (GTK_EDITABLE (self->summary_entry), ""); /* Remove "error" class */ gtk_widget_remove_css_class (self->summary_entry, "error"); g_clear_pointer (&self->range, gcal_range_unref); bind_model (self); } static void gcal_quick_add_popover_class_init (GcalQuickAddPopoverClass *klass) { GtkPopoverClass *popover_class = GTK_POPOVER_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); popover_class->closed = gcal_quick_add_popover_closed; object_class->get_property = gcal_quick_add_popover_get_property; object_class->set_property = gcal_quick_add_popover_set_property; /** * GcalQuickAddPopover::edit-event: * @self: a #GcalQuickAddPopover * @event: the new #GcalEvent * * Emitted when the user clicks 'Edit event' button. */ signals[EDIT_EVENT] = g_signal_new ("edit-event", GCAL_TYPE_QUICK_ADD_POPOVER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, GCAL_TYPE_EVENT); /** * GcalQuickAddPopover::range: * * The range of the new event. */ g_object_class_install_property (object_class, PROP_RANGE, g_param_spec_boxed ("range", "Range of the new event", "The range of the new event", G_TYPE_DATE_TIME, G_PARAM_READWRITE)); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/gui/gcal-quick-add-popover.ui"); gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, add_button); gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, calendars_list_box); gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, edit_button); gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, summary_entry); gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, title_label); gtk_widget_class_bind_template_callback (widget_class, edit_or_create_event); gtk_widget_class_bind_template_callback (widget_class, row_selected_cb); gtk_widget_class_bind_template_callback (widget_class, summary_entry_activated); gtk_widget_class_bind_template_callback (widget_class, summary_entry_text_changed); } static void gcal_quick_add_popover_init (GcalQuickAddPopover *self) { GcalContext *context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION); GcalManager *manager = gcal_context_get_manager (context); gtk_widget_init_template (GTK_WIDGET (self)); self->read_write_calendars_model = gcal_create_writable_calendars_model (manager); bind_model (self); /* Connect to the manager signals and keep the list updates */ g_signal_connect_object (manager, "notify::default-calendar", G_CALLBACK (bind_model), self, G_CONNECT_SWAPPED); g_signal_connect_object (context, "notify::time-format", G_CALLBACK (update_header), self, G_CONNECT_SWAPPED); } GtkWidget* gcal_quick_add_popover_new (void) { return g_object_new (GCAL_TYPE_QUICK_ADD_POPOVER, NULL); } /** * gcal_quick_add_popover_get_range: * @self: a #GcalQuickAddPopover * * Retrieves the range of @self. * * Returns: (transfer none): a #GcalRange */ GcalRange* gcal_quick_add_popover_get_range (GcalQuickAddPopover *self) { g_return_val_if_fail (GCAL_IS_QUICK_ADD_POPOVER (self), NULL); return self->range; } /** * gcal_quick_add_popover_set_range: * @self: a #GcalQuickAddPopover * @range: a #GcalRange * * Sets the range of the to-be-created event. */ void gcal_quick_add_popover_set_range (GcalQuickAddPopover *self, GcalRange *range) { g_return_if_fail (GCAL_IS_QUICK_ADD_POPOVER (self)); if (self->range != range && (!self->range || !range || (self->range && range && gcal_range_compare (self->range, range) != 0))) { g_clear_pointer (&self->range, gcal_range_unref); self->range = gcal_range_ref (range); update_header (self); g_object_notify (G_OBJECT (self), "range"); } }