diff --git a/configure.ac b/configure.ac index 663570a8..ef32d946 100644 --- a/configure.ac +++ b/configure.ac @@ -12,7 +12,7 @@ AC_CONFIG_AUX_DIR([build-aux]) AM_SILENT_RULES([yes]) # require automake 1.13 -AM_INIT_AUTOMAKE([1.13 dist-xz no-dist-gzip tar-ustar]) +AM_INIT_AUTOMAKE([1.13 dist-xz no-dist-gzip tar-ustar subdir-objects]) AM_MAINTAINER_MODE([enable]) dnl *************************************************************************** diff --git a/data/Makefile.am b/data/Makefile.am index 046d2404..888b3a1e 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -61,6 +61,8 @@ EXTRA_DIST= \ ui/search-view.ui \ ui/source-dialog.ui \ ui/time-selector.ui \ + ui/week-header.ui \ + ui/week-view.ui \ ui/window.ui \ ui/year-view.ui \ theme/gtk-styles.css \ diff --git a/data/calendar.gresource.xml b/data/calendar.gresource.xml index 7c64779b..6b5397e4 100644 --- a/data/calendar.gresource.xml +++ b/data/calendar.gresource.xml @@ -12,6 +12,8 @@ ui/search-view.ui ui/source-dialog.ui ui/time-selector.ui + ui/week-header.ui + ui/week-view.ui ui/window.ui ui/year-view.ui theme/gtk-styles.css diff --git a/data/theme/gtk-styles.css b/data/theme/gtk-styles.css index 89d8e9e3..45f23a3d 100644 --- a/data/theme/gtk-styles.css +++ b/data/theme/gtk-styles.css @@ -28,7 +28,8 @@ calendar-view.header { font-weight: bold; } -calendar-view.current { +calendar-view.current, +weekgrid.current { background-color: alpha(@theme_selected_bg_color, 0.3); } @@ -52,14 +53,14 @@ calendar-view.lines { color: alpha(@theme_fg_color, 0.4); } -calendar-view.first-view-header { +calendar-view .first-view-header { font-size: 16pt; font-weight: bold; color: @theme_selected_bg_color; padding: 12px; } -calendar-view.second-view-header { +calendar-view .second-view-header { font-size: 16pt; font-weight: bold; padding: 12px; @@ -358,3 +359,74 @@ datechooser day:selected:hover { background-color: @theme_selected_bg_color; font-weight: bold; } + +/* Week View CSS */ + +.week-header { + padding: 0px; + border-bottom: solid @borders 1px; +} + +.week-header .month-name { + font-size: 16pt; + font-weight: bold; + color: alpha(@theme_selected_bg_color, 0.75); + padding: 12px; +} + +.week-header .week-number { + font-size: 16pt; + font-weight: bold; + padding: 12px 12px 18px 12px; + color: alpha(@theme_fg_color, 0.25); +} + +.week-header.week-names { + font-size: 10pt; + font-weight: bold; + color: alpha(@theme_fg_color, 0.55); +} + +.week-header.week-dates { + font-size: 16pt; + font-weight: bold; + color: alpha(@theme_fg_color, 0.70); +} + +.week-header.lines { + color: alpha(@theme_fg_color, 0.4); +} + +.week-header .margin { + color: alpha(@theme_fg_color, 0.6); +} + +.week-header .today { + color: @theme_selected_bg_color; +} + +.week-view { + padding: 0px; +} + +.week-view .hours { + font-size: 11pt; + font-weight: normal; + color: alpha(@theme_fg_color, 0.25); + padding: 18px 12px; +} + +weekgrid.now-strip { + background-color: alpha(@theme_selected_bg_color, 0.8); + margin: 0 0 0 1px; + min-height: 2px; +} + +weekgrid:selected, +.week-header:selected { + background-color: alpha(@theme_selected_bg_color, 0.25); +} + +.week-header:selected { + border: solid 1px alpha(@theme_selected_bg_color, 0.8); +} diff --git a/data/ui/help-overlay.ui b/data/ui/help-overlay.ui index 2525fe30..1b92fbc9 100644 --- a/data/ui/help-overlay.ui +++ b/data/ui/help-overlay.ui @@ -93,6 +93,13 @@ True View + + + True + Week view + <Primary>1 + + True diff --git a/data/ui/week-header.ui b/data/ui/week-header.ui new file mode 100644 index 00000000..00f25aed --- /dev/null +++ b/data/ui/week-header.ui @@ -0,0 +1,205 @@ + + + + + horizontal + + + + + diff --git a/data/ui/week-view.ui b/data/ui/week-view.ui new file mode 100644 index 00000000..62d8754c --- /dev/null +++ b/data/ui/week-view.ui @@ -0,0 +1,51 @@ + + + + diff --git a/data/ui/window.ui b/data/ui/window.ui index 067cb95a..085c84d3 100644 --- a/data/ui/window.ui +++ b/data/ui/window.ui @@ -52,6 +52,18 @@ + + + True + + + + + + week + Week + + True @@ -62,6 +74,7 @@ + 1 month Month @@ -75,7 +88,7 @@ - 1 + 2 year Year diff --git a/po/POTFILES.in b/po/POTFILES.in index 7ec83b98..f5b240d0 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -14,6 +14,8 @@ data/org.gnome.calendar.gschema.xml.in [type: gettext/glade]data/ui/time-selector.ui [type: gettext/glade]data/ui/window.ui [type: gettext/glade]data/ui/year-view.ui +src/views/gcal-week-header.c +src/views/gcal-week-view.c src/gcal-application.c src/gcal-date-selector.c src/gcal-edit-dialog.c diff --git a/src/Makefile.am b/src/Makefile.am index ab79ae70..af0b73ca 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,4 +1,5 @@ AM_CPPFLAGS = \ + -I$(srcdir)/views \ -DPACKAGE_LOCALE_DIR=\""$(localedir)"\" \ -DPACKAGE_SRC_DIR=\""$(srcdir)"\" \ -DPACKAGE_DATA_DIR=\""$(pkgdatadir)"\" \ @@ -66,8 +67,14 @@ gnome_calendar_SOURCES = \ gcal-utils.h \ gcal-view.c \ gcal-view.h \ - gcal-week-view.c \ - gcal-week-view.h \ + views/gcal-range-tree.c \ + views/gcal-range-tree.h \ + views/gcal-week-grid.c \ + views/gcal-week-grid.h \ + views/gcal-week-header.c \ + views/gcal-week-header.h \ + views/gcal-week-view.c \ + views/gcal-week-view.h \ gcal-window.h \ gcal-window.c \ gcal-year-view.c \ diff --git a/src/gcal-event-widget.c b/src/gcal-event-widget.c index 6b889c61..e4d63093 100644 --- a/src/gcal-event-widget.c +++ b/src/gcal-event-widget.c @@ -17,10 +17,13 @@ * with this program. If not, see . */ +#include + #include "gcal-event-widget.h" #include "gcal-utils.h" #define INTENSITY(c) ((c->red) * 0.30 + (c->green) * 0.59 + (c->blue) * 0.11) +#define ICON_SIZE 16 struct _GcalEventWidget { @@ -37,8 +40,7 @@ struct _GcalEventWidget GcalEvent *event; - /* Drag n' Drop icon */ - GdkPixbuf *dnd_pixbuf; + GtkOrientation orientation; GdkWindow *event_window; gboolean button_pressed; @@ -50,6 +52,7 @@ enum PROP_DATE_END, PROP_DATE_START, PROP_EVENT, + PROP_ORIENTATION, NUM_PROPS }; @@ -61,7 +64,8 @@ enum static guint signals[NUM_SIGNALS] = { 0, }; -G_DEFINE_TYPE (GcalEventWidget, gcal_event_widget, GTK_TYPE_WIDGET) +G_DEFINE_TYPE_WITH_CODE (GcalEventWidget, gcal_event_widget, GTK_TYPE_WIDGET, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)); static void gcal_event_widget_update_style (GcalEventWidget *self) @@ -188,13 +192,10 @@ static void gcal_event_widget_init (GcalEventWidget *self) { GtkWidget *widget; - GSettings *settings; - gchar *clock_format; widget = GTK_WIDGET (self); - settings = g_settings_new ("org.gnome.desktop.interface"); - clock_format = g_settings_get_string (settings, "clock-format"); - self->clock_format_24h = g_strcmp0 (clock_format, "24h") == 0; + self->clock_format_24h = is_clock_format_24h (); + self->orientation = GTK_ORIENTATION_HORIZONTAL; gtk_widget_set_has_window (widget, FALSE); gtk_widget_set_can_focus (widget, TRUE); @@ -207,9 +208,6 @@ gcal_event_widget_init (GcalEventWidget *self) GDK_ACTION_COPY); gtk_drag_source_add_text_targets (widget); - - g_clear_object (&settings); - g_free (clock_format); } static void @@ -234,6 +232,12 @@ gcal_event_widget_set_property (GObject *object, gcal_event_widget_set_event_internal (self, g_value_get_object (value)); break; + case PROP_ORIENTATION: + self->orientation = g_value_get_enum (value); + gtk_widget_queue_draw (GTK_WIDGET (object)); + g_object_notify (object, "orientation"); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -261,6 +265,10 @@ gcal_event_widget_get_property (GObject *object, g_value_set_object (value, self->event); break; + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -279,7 +287,6 @@ gcal_event_widget_finalize (GObject *object) /* releasing properties */ g_clear_pointer (&self->css_class, g_free); - g_clear_object (&self->dnd_pixbuf); g_clear_object (&self->event); G_OBJECT_CLASS (gcal_event_widget_parent_class)->finalize (object); @@ -314,11 +321,14 @@ gcal_event_widget_get_preferred_height (GtkWidget *widget, gint *minimum, gint *natural) { + GcalEventWidget *self; GtkBorder border, padding; GtkStyleContext *context; PangoLayout *layout; gint layout_height; + gint factor; + self = GCAL_EVENT_WIDGET (widget); context = gtk_widget_get_style_context (widget); layout = gtk_widget_create_pango_layout (widget, NULL); pango_layout_get_pixel_size (layout, NULL, &layout_height); @@ -327,10 +337,12 @@ gcal_event_widget_get_preferred_height (GtkWidget *widget, gtk_style_context_get_border (context, gtk_style_context_get_state (context), &border); gtk_style_context_get_padding (context, gtk_style_context_get_state (context), &padding); + factor = self->orientation == GTK_ORIENTATION_HORIZONTAL ? 1 : 2; + if (minimum != NULL) - *minimum = layout_height + padding.top + padding.bottom + border.top + border.bottom; + *minimum = (factor * layout_height) + padding.top + padding.bottom + border.top + border.bottom; if (natural != NULL) - *natural = layout_height + padding.top + padding.bottom + border.top + border.bottom; + *natural = (factor * layout_height) + padding.top + padding.bottom + border.top + border.bottom; } static void @@ -494,10 +506,22 @@ gcal_event_widget_draw (GtkWidget *widget, else start_time = g_date_time_format (local_start_time, "%I:%M %P"); - if (is_ltr) - display_text = g_strdup_printf ("(%s) %s", start_time, gcal_event_get_summary (self->event)); + /* + * When in horizontal, draw the elements sequentially; otherwise, + * draw the time in the first line, and the title in subsequent + * lines. + */ + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (is_ltr) + display_text = g_strdup_printf ("(%s) %s", start_time, gcal_event_get_summary (self->event)); + else + display_text = g_strdup_printf ("%s (%s)", gcal_event_get_summary (self->event), start_time); + } else - display_text = g_strdup_printf ("%s (%s)", gcal_event_get_summary (self->event), start_time); + { + display_text = g_strdup_printf ("%s\n%s", start_time, gcal_event_get_summary (self->event)); + } g_clear_pointer (&local_start_time, g_date_time_unref); g_clear_pointer (&start_time, g_free); @@ -505,16 +529,13 @@ gcal_event_widget_draw (GtkWidget *widget, /* Retrieve the font description */ gtk_style_context_get (context, state, "font", &font_desc, NULL); - layout = gtk_widget_create_pango_layout (widget, display_text); + layout = gtk_widget_create_pango_layout (widget, ""); + pango_layout_set_markup (layout, display_text, -1); pango_layout_set_font_description (layout, font_desc); pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END); - pango_layout_set_width (layout, (width - (padding.left + padding.right) ) * PANGO_SCALE); - pango_cairo_update_layout (cr, layout); /* Icon size */ - pango_layout_get_pixel_size (layout, NULL, &layout_height); - icon_size = 4 * (layout_height / 4); - + icon_size = ICON_SIZE; end_gap = 0; if (gcal_event_has_alarms (self->event)) @@ -526,8 +547,36 @@ gcal_event_widget_draw (GtkWidget *widget, x = is_ltr ? padding.left : padding.left + end_gap; pango_layout_set_width (layout, (width - (padding.left + padding.right + end_gap)) * PANGO_SCALE); + pango_layout_get_pixel_size (layout, NULL, &layout_height); gtk_render_layout (context, cr, x, padding.top, layout); +#if 0 + /* TODO: find out a good way to handle this */ + /* + * On vertical event widgets, we assume we might have space to draw the + * event's detail (or at least part of it). + */ + if (self->orientation == GTK_ORIENTATION_VERTICAL && + 2 * padding.top + layout_height + padding.bottom < height && + strlen (gcal_event_get_description (self->event)) > 0) + { + g_autofree gchar *description; + + description = g_strdup_printf ("%s", gcal_event_get_description (self->event)); + pango_layout_set_markup (layout, description, -1); + pango_layout_set_width (layout, (width - (padding.left + padding.right) ) * PANGO_SCALE); + pango_layout_set_height (layout, (height - (2 * padding.top + layout_height + padding.bottom) ) * PANGO_SCALE); + pango_layout_set_font_description (layout, font_desc); + pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END); + + gtk_render_layout (context, + cr, + x, + 2 * padding.top + layout_height + padding.bottom, + layout); + } +#endif + x += is_ltr ? (width - end_gap - padding.left - padding.right) : (- padding.right - padding.left - icon_size); /* render reminder icon */ @@ -548,7 +597,11 @@ gcal_event_widget_draw (GtkWidget *widget, &was_symbolic, NULL); - gdk_cairo_set_source_pixbuf (cr, pixbuf, x + padding.left, padding.top); + gtk_render_icon (context, + cr, + pixbuf, + x + padding.left, + padding.top); x += is_ltr ? (padding.left + icon_size) : (- padding.right - icon_size); @@ -574,7 +627,12 @@ gcal_event_widget_draw (GtkWidget *widget, &was_symbolic, NULL); - gdk_cairo_set_source_pixbuf (cr, pixbuf, x + padding.left, padding.top); + gtk_render_icon (context, + cr, + pixbuf, + x + padding.left, + padding.top); + g_object_unref (pixbuf); cairo_paint (cr); } @@ -616,13 +674,30 @@ gcal_event_widget_button_release_event (GtkWidget *widget, return FALSE; } +static cairo_surface_t* +get_dnd_icon (GtkWidget *widget) +{ + cairo_surface_t *surface; + cairo_t *cr; + + /* Make it transparent */ + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + + cr = cairo_create (surface); + + gtk_widget_draw (widget, cr); + + return surface; +} + static void gcal_event_widget_drag_begin (GtkWidget *widget, GdkDragContext *context) { GcalEventWidget *self = GCAL_EVENT_WIDGET (widget); - - g_clear_object (&self->dnd_pixbuf); + cairo_surface_t *surface; if (self->read_only) { @@ -631,13 +706,11 @@ gcal_event_widget_drag_begin (GtkWidget *widget, } /* Setup the drag n' drop icon */ - self->dnd_pixbuf = gdk_pixbuf_get_from_window (self->event_window, - 0, - 0, - gtk_widget_get_allocated_width (widget), - gtk_widget_get_allocated_height (widget)); + surface = get_dnd_icon (widget); - gtk_drag_set_icon_pixbuf (context, self->dnd_pixbuf, 0, 0); + gtk_drag_set_icon_surface (context, surface); + + g_clear_pointer (&surface, cairo_surface_destroy); } static gboolean @@ -717,6 +790,13 @@ gcal_event_widget_class_init(GcalEventWidgetClass *klass) GCAL_TYPE_EVENT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + /** + * GcalEventWidget::orientation: + * + * The orientation of the event widget. + */ + g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation"); + signals[ACTIVATE] = g_signal_new ("activate", GCAL_TYPE_EVENT_WIDGET, G_SIGNAL_RUN_LAST, diff --git a/src/gcal-quick-add-popover.c b/src/gcal-quick-add-popover.c index f5a4af3f..672a0c5e 100644 --- a/src/gcal-quick-add-popover.c +++ b/src/gcal-quick-add-popover.c @@ -39,6 +39,8 @@ struct _GcalQuickAddPopover GtkWidget *selected_row; + gboolean clock_format_24h; + GcalManager *manager; }; @@ -213,6 +215,7 @@ update_header (GcalQuickAddPopover *self) { icaltimetype *dtstart; struct tm stm_date, etm_date; + gboolean multiday_or_timed; gboolean all_day; gchar start[64], end[64]; gchar *title_date; @@ -227,9 +230,17 @@ update_header (GcalQuickAddPopover *self) stm_date = icaltimetype_to_tm (dtstart); e_utf8_strftime_fix_am_pm (start, 64, C_("event date format", "%B %d"), &stm_date); - if (self->date_end && - (!all_day || - (all_day && g_date_time_difference (self->date_end, self->date_start) / G_TIME_SPAN_DAY > 1))) + if (self->date_end) + { + multiday_or_timed = g_date_time_difference (self->date_end, self->date_start) / G_TIME_SPAN_DAY > 1 || + g_date_time_difference (self->date_end, self->date_start) / G_TIME_SPAN_MINUTE > 1; + } + else + { + multiday_or_timed = FALSE; + } + + if (multiday_or_timed) { icaltimetype *dtend; @@ -238,14 +249,39 @@ update_header (GcalQuickAddPopover *self) if (dtend->is_date) icaltime_adjust (dtend, -1, 0, 0, 0); - /* Translators: - * this is the format string for representing a date consisting of a month name - * and a date of month. - */ - etm_date = icaltimetype_to_tm (dtend); - e_utf8_strftime_fix_am_pm (end, 64, C_("event date format", "%B %d"), &etm_date); + if (all_day && + (g_date_time_difference (self->date_end, self->date_start) / G_TIME_SPAN_DAY > 1 && + g_date_time_difference (self->date_end, self->date_start) / G_TIME_SPAN_MINUTE > 1)) + { + /* Translators: + * this is the format string for representing a date consisting of a month name + * and a date of month. + */ + etm_date = icaltimetype_to_tm (dtend); + e_utf8_strftime_fix_am_pm (end, 64, C_("event date format", "%B %d"), &etm_date); - title_date = g_strdup_printf (_("New Event from %s to %s"), start, end); + title_date = g_strdup_printf (_("New Event from %s to %s"), start, end); + } + else + { + g_autofree gchar *start_hour, *end_hour; + + if (self->clock_format_24h) + { + start_hour = g_date_time_format (self->date_start, "%R"); + end_hour = g_date_time_format (self->date_end, "%R"); + } + else + { + start_hour = g_date_time_format (self->date_start, "%I:%M %P"); + end_hour = g_date_time_format (self->date_end, "%I:%M %P"); + } + + title_date = g_strdup_printf (_("New Event on %s, %s - %s"), + start, + start_hour, + end_hour); + } g_free (dtend); } @@ -413,27 +449,21 @@ edit_or_create_event (GcalQuickAddPopover *self, GtkWidget *button) { ECalComponent *component; - GDateTime *date_start, *date_end, *now; + GDateTime *date_start, *date_end; GTimeZone *tz; GcalEvent *event; ESource *source; const gchar *summary; - gboolean all_day, single_day, is_today; + gboolean all_day, single_day; if (!self->selected_row) return; - now = g_date_time_new_now_local (); source = g_object_get_data (G_OBJECT (self->selected_row), "source"); - /* - * We consider all day events multiday and/or non-today events. - * Events on today starts now and lasts 1 hour. - */ single_day = datetime_compare_date (self->date_end, self->date_start) == 0; - is_today = single_day && datetime_compare_date (now, self->date_start) == 0; - all_day = datetime_compare_date (self->date_end, self->date_start) > 1 || !is_today; - + all_day = datetime_compare_date (self->date_end, self->date_start) > 1 || + g_date_time_compare (self->date_end, self->date_start) == 0; tz = all_day ? g_time_zone_new_utc () : g_time_zone_new_local (); /* Gather start date */ @@ -441,8 +471,8 @@ edit_or_create_event (GcalQuickAddPopover *self, g_date_time_get_year (self->date_start), g_date_time_get_month (self->date_start), g_date_time_get_day_of_month (self->date_start), - all_day ? 0 : g_date_time_get_hour (now), - all_day ? 0 : g_date_time_get_minute (now), + g_date_time_get_hour (self->date_start), + g_date_time_get_minute (self->date_start), 0); /* Gather date end */ @@ -452,8 +482,8 @@ edit_or_create_event (GcalQuickAddPopover *self, g_date_time_get_year (self->date_end), g_date_time_get_month (self->date_end), g_date_time_get_day_of_month (self->date_end) + (single_day && all_day ? 1 : 0), - all_day ? 0 : g_date_time_get_hour (now) + (is_today ? 1 : 0), - all_day ? 0 : g_date_time_get_minute (now), + g_date_time_get_hour (self->date_end), + g_date_time_get_minute (self->date_end), 0); } else @@ -681,6 +711,8 @@ gcal_quick_add_popover_init (GcalQuickAddPopover *self) gtk_widget_init_template (GTK_WIDGET (self)); gtk_list_box_set_sort_func (GTK_LIST_BOX (self->calendars_listbox), sort_func, NULL, NULL); + + self->clock_format_24h = is_clock_format_24h (); } GtkWidget* diff --git a/src/gcal-utils.c b/src/gcal-utils.c index ec91ee33..ed73f611 100644 --- a/src/gcal-utils.c +++ b/src/gcal-utils.c @@ -524,6 +524,57 @@ icaltime_compare_with_current (const icaltimetype *date1, return result; } +GDateTime* +get_start_of_week (icaltimetype *date) +{ + icaltimetype *new_date; + GDateTime *dt; + + new_date = g_new0 (icaltimetype, 1); + *new_date = icaltime_from_day_of_year (icaltime_start_doy_week (*date, get_first_weekday () + 1), + date->year); + new_date->is_date = 0; + new_date->hour = 0; + new_date->minute = 0; + new_date->second = 0; + + dt = g_date_time_new_local (new_date->year, + new_date->month, + new_date->day, + 0, 0, 0); + + g_clear_pointer (&new_date, g_free); + + return dt; +} + +GDateTime* +get_end_of_week (icaltimetype *date) +{ + GDateTime *week_start, *week_end; + + week_start = get_start_of_week (date); + week_end = g_date_time_add_days (week_start, 7); + + g_clear_pointer (&week_start, g_date_time_unref); + + return week_end; +} + +gboolean +is_clock_format_24h (void) +{ + static GSettings *settings = NULL; + g_autofree gchar *clock_format = NULL; + + if (!settings) + settings = g_settings_new ("org.gnome.desktop.interface"); + + clock_format = g_settings_get_string (settings, "clock-format"); + + return g_strcmp0 (clock_format, "24h") == 0; +} + /* Function to do a last minute fixup of the AM/PM stuff if the locale * and gettext haven't done it right. Most English speaking countries * except the USA use the 24 hour clock (UK, Australia etc). However diff --git a/src/gcal-utils.h b/src/gcal-utils.h index 90992a9f..8c3a249f 100644 --- a/src/gcal-utils.h +++ b/src/gcal-utils.h @@ -27,6 +27,12 @@ #define ICAL_TIME_TYPE (icaltime_get_type ()) +#define ALIGNED(x) (round (x) + 0.5) +#define MINUTES_PER_DAY 1440 +#define MAX_MINUTES (7 * MINUTES_PER_DAY) + +#define gcal_clear_datetime(dt) g_clear_pointer (dt, g_date_time_unref) + typedef enum { GCAL_WINDOW_VIEW_DAY, @@ -95,6 +101,12 @@ gint icaltime_compare_with_current (const icaltimet const icaltimetype *date2, time_t *current_time_t); +GDateTime* get_start_of_week (icaltimetype *date); + +GDateTime* get_end_of_week (icaltimetype *date); + +gboolean is_clock_format_24h (void); + /* code brought from evolution */ gsize e_strftime_fix_am_pm (gchar *str, gsize max, diff --git a/src/gcal-week-view.c b/src/gcal-week-view.c deleted file mode 100644 index 264ab891..00000000 --- a/src/gcal-week-view.c +++ /dev/null @@ -1,1422 +0,0 @@ -/* -*- mode: c; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* - * gcal-week-view.c - * - * Copyright (C) 2015 - Erick Pérez Castellanos - * - * 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 2 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 . - */ - -#include "gcal-week-view.h" -#include "gcal-utils.h" -#include "gcal-view.h" -#include "gcal-event-widget.h" - -#include - -#include - -#define ALL_DAY_CELLS_HEIGHT 40 - -static const double dashed [] = -{ - 1.0, - 1.0 -}; - -enum -{ - PROP_0, - PROP_DATE -}; - -struct _GcalWeekViewChild -{ - GtkWidget *widget; - gboolean hidden_by_me; - - /* vertical index */ - gint index; -}; - -typedef struct _GcalWeekViewChild GcalWeekViewChild; - -typedef struct -{ - /** - * This is where we keep the refs of the child widgets. - * Every child added to the list placed in the position - * of it corresponding cell number. - * The cell number is calculated in _add method. - * day[0] Represents the first_weekday according to locale - */ - GList *days [7]; - - GdkWindow *view_window; - GdkWindow *grid_window; - - GtkWidget *vscrollbar; - - /** - * first day of the week according to user locale, being - * 0 for Sunday, 1 for Monday and so on */ - gint first_weekday; - - /** - * clock format from GNOME desktop settings - */ - gboolean use_24h_format; - - /* property */ - icaltimetype *date; - GcalManager *manager; /* weak referenced */ - - GdkWindow *event_window; - - gint clicked_cell; -} GcalWeekViewPrivate; - -static gint get_start_grid_y (GtkWidget *widget); - -static void event_opened (GcalEventWidget *event_widget, - gpointer user_data); - -static void draw_header (GcalWeekView *view, - cairo_t *cr, - GtkAllocation *alloc, - GtkBorder *padding); - -static void draw_grid_window (GcalWeekView *view, - cairo_t *cr); - -static gint get_sidebar_width (GtkWidget *widget); - -static void gcal_week_view_constructed (GObject *object); - -static void gcal_week_view_finalize (GObject *object); - -static void gcal_week_view_set_property (GObject *object, - guint property_id, - const GValue *value, - GParamSpec *pspec); - -static void gcal_week_view_get_property (GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec); - -static void gcal_week_view_realize (GtkWidget *widget); - -static void gcal_week_view_unrealize (GtkWidget *widget); - -static void gcal_week_view_size_allocate (GtkWidget *widget, - GtkAllocation *allocation); - -static gboolean gcal_week_view_draw (GtkWidget *widget, - cairo_t *cr); - -static gboolean gcal_week_view_scroll_event (GtkWidget *widget, - GdkEventScroll *event); - -static gboolean gcal_week_view_button_press_event (GtkWidget *widget, - GdkEventButton *event); - -static gboolean gcal_week_view_button_release_event (GtkWidget *widget, - GdkEventButton *event); - -static void gcal_week_view_add (GtkContainer *constainer, - GtkWidget *widget); - -static void gcal_week_view_remove (GtkContainer *constainer, - GtkWidget *widget); - -static void gcal_week_view_forall (GtkContainer *container, - gboolean include_internals, - GtkCallback callback, - gpointer callback_data); - -static void gcal_week_view_scroll_value_changed (GtkAdjustment *adjusment, - gpointer user_data); - -static icaltimetype* gcal_week_view_get_initial_date (GcalView *view); - -static icaltimetype* gcal_week_view_get_final_date (GcalView *view); - -static GtkWidget* gcal_week_view_get_by_uuid (GcalSubscriberView *view, - const gchar *uuid); - -static void gcal_view_interface_init (GcalViewInterface *iface); - -G_DEFINE_TYPE_WITH_CODE (GcalWeekView, gcal_week_view, GCAL_TYPE_SUBSCRIBER_VIEW, - G_ADD_PRIVATE (GcalWeekView) - G_IMPLEMENT_INTERFACE (GCAL_TYPE_VIEW, gcal_view_interface_init)); - -/** - * get_start_grid_y: - * - * In GcalMonthView this method returns the height of the headers of the view - * and the grid. Here this points just the place where the grid_window hides - * behind the header - * Here this height includes: - * - The big header of the view - * - The grid header dislaying weekdays - * - The cell containing all-day events. - */ -static gint -get_start_grid_y (GtkWidget *widget) -{ - GtkStyleContext *context; - GtkStateFlags flags; - GtkBorder padding; - - PangoLayout *layout; - PangoFontDescription *font_desc; - gint font_height; - gdouble start_grid_y; - - context = gtk_widget_get_style_context (widget); - flags = gtk_widget_get_state_flags (widget); - - layout = gtk_widget_create_pango_layout (widget, NULL); - - /* init header values */ - gtk_style_context_get_padding (context, flags, &padding); - - gtk_style_context_get (context, flags, "font", &font_desc, NULL); - - pango_font_description_set_weight (font_desc, PANGO_WEIGHT_SEMIBOLD); - pango_layout_set_font_description (layout, font_desc); - pango_layout_get_pixel_size (layout, NULL, &font_height); - pango_font_description_free (font_desc); - - /* 6: is padding around the header */ - start_grid_y = font_height + padding.bottom; - - /* for including the all-day cells */ - start_grid_y += ALL_DAY_CELLS_HEIGHT; - - g_object_unref (layout); - return start_grid_y; -} - -static void -event_opened (GcalEventWidget *event_widget, - gpointer user_data) -{ - g_signal_emit_by_name (GCAL_VIEW (user_data), - "event-activated", - event_widget); -} - -static void -draw_header (GcalWeekView *view, - cairo_t *cr, - GtkAllocation *alloc, - GtkBorder *padding) -{ - GcalWeekViewPrivate *priv; - GtkWidget *widget; - GtkStyleContext *context; - GtkStateFlags state; - GdkRGBA color; - /* GdkRGBA current_color; */ - - PangoLayout *layout; - PangoFontDescription *bold_font; - - gint i; - gint pos_i; - gint start_grid_y; - gint font_height; - gdouble sidebar_width; - gdouble cell_width; - icaltimetype *start_of_week; - gint current_cell; - - cairo_pattern_t *pattern; - - priv = gcal_week_view_get_instance_private (view); - widget = GTK_WIDGET (view); - - cairo_save (cr); - start_grid_y = get_start_grid_y (widget); - context = gtk_widget_get_style_context (widget); - state = gtk_widget_get_state_flags (widget); - - /* adding shadow */ - pattern = cairo_pattern_create_linear(0, start_grid_y - 18, - 0, start_grid_y + 6); - - cairo_pattern_add_color_stop_rgba(pattern, 0.0, 0, 0, 0, 0.6); - cairo_pattern_add_color_stop_rgba(pattern, 1.0, 0, 0, 0, 0.0); - - cairo_set_source(cr, pattern); - cairo_pattern_destroy(pattern); - - cairo_rectangle(cr, 0, start_grid_y, alloc->width, 6); - cairo_fill(cr); - - gtk_style_context_get_color (context, state, &color); - gdk_cairo_set_source_rgba (cr, &color); - - /* grid header */ - layout = pango_cairo_create_layout (cr); - gtk_style_context_get (context, state, "font", &bold_font, NULL); - pango_font_description_set_weight (bold_font, PANGO_WEIGHT_SEMIBOLD); - pango_layout_set_font_description (layout, bold_font); - - start_of_week = gcal_week_view_get_initial_date (GCAL_VIEW (view)); - current_cell = icaltime_day_of_week (*(priv->date)) - 1; - current_cell = (7 + current_cell - priv->first_weekday) % 7; - - sidebar_width = get_sidebar_width (widget); - cell_width = (alloc->width - sidebar_width) / 7; - pango_layout_get_pixel_size (layout, NULL, &font_height); - - gtk_style_context_save (context); - gtk_style_context_add_class (context, "current"); - gtk_render_background (context, cr, - cell_width * current_cell + sidebar_width, - font_height + padding->bottom, - cell_width, - ALL_DAY_CELLS_HEIGHT); - gtk_style_context_remove_class (context, "current"); - gtk_style_context_restore (context); - - for (i = 0; i < 7; i++) - { - gchar *weekday_header; - gchar *weekday_abv; - gint n_day; - - n_day = start_of_week->day + i; - if (n_day > icaltime_days_in_month (start_of_week->month, - start_of_week->year)) - { - n_day = n_day - icaltime_days_in_month (start_of_week->month, - start_of_week->year); - } - - weekday_abv = gcal_get_weekday ((i + priv->first_weekday) % 7); - weekday_header = g_strdup_printf ("%s %d", weekday_abv, n_day); - - pango_layout_set_text (layout, weekday_header, -1); - cairo_move_to (cr, - padding->left + cell_width * i + sidebar_width, - 0.0); - pango_cairo_show_layout (cr, layout); - - cairo_save (cr); - cairo_move_to (cr, - cell_width * i + sidebar_width + 0.3, - font_height + padding->bottom); - cairo_rel_line_to (cr, 0.0, ALL_DAY_CELLS_HEIGHT); - cairo_stroke (cr); - cairo_restore (cr); - - g_free (weekday_header); - } - - /* horizontal line */ - gtk_style_context_get_color (context, - state | GTK_STATE_FLAG_INSENSITIVE, - &color); - gdk_cairo_set_source_rgba (cr, &color); - pos_i = font_height + padding->bottom; - cairo_move_to (cr, sidebar_width, pos_i + 0.3); - cairo_rel_line_to (cr, alloc->width - sidebar_width, 0); - - cairo_stroke (cr); - - cairo_restore (cr); - - g_free (start_of_week); - pango_font_description_free (bold_font); - g_object_unref (layout); -} - -static void -draw_grid_window (GcalWeekView *view, - cairo_t *cr) -{ - GcalWeekViewPrivate *priv; - GtkWidget *widget; - - GtkStyleContext *context; - GtkStateFlags state; - GtkBorder padding; - GdkRGBA color; - - gint i; - gint width; - gint height; - gdouble sidebar_width; - gint current_cell; - - PangoLayout *layout; - PangoFontDescription *font_desc; - - priv = gcal_week_view_get_instance_private (view); - widget = GTK_WIDGET (view); - - context = gtk_widget_get_style_context (widget); - state = gtk_widget_get_state_flags (widget); - - gtk_style_context_get_color (context, state, &color); - gtk_style_context_get_padding (context, state, &padding); - gtk_style_context_get (context, state, "font", &font_desc, NULL); - - layout = pango_cairo_create_layout (cr); - pango_layout_set_font_description (layout, font_desc); - gdk_cairo_set_source_rgba (cr, &color); - - sidebar_width = get_sidebar_width (widget); - width = gdk_window_get_width (priv->grid_window); - height = gdk_window_get_height (priv->grid_window); - - current_cell = icaltime_day_of_week (*(priv->date)) - 1; - current_cell = (7 + current_cell - priv->first_weekday) % 7; - - gtk_cairo_transform_to_window (cr, widget, priv->grid_window); - - gtk_style_context_save (context); - gtk_style_context_add_class (context, "current"); - gtk_render_background ( - context, cr, - ((width - sidebar_width)/ 7.0) * current_cell + sidebar_width, - 0, - ((width - sidebar_width)/ 7.0), - height); - gtk_style_context_remove_class (context, "current"); - gtk_style_context_restore (context); - - /* grid, sidebar hours */ - for (i = 0; i < 24; i++) - { - gchar *hours; - if (priv->use_24h_format) - { - hours = g_strdup_printf ("%02d:00", i); - pango_layout_set_text (layout, hours, -1); - } - else - { - hours = g_strdup_printf ("%02d:00 %s", - i % 12, - i < 12 ? _("AM") : _("PM")); - - if (i == 0) - pango_layout_set_text (layout, _("Midnight"), -1); - else if (i == 12) - pango_layout_set_text (layout, _("Noon"), -1); - else - pango_layout_set_text (layout, hours, -1); - } - - cairo_move_to (cr, padding.left, padding.top + (height / 24) * i); - pango_cairo_show_layout (cr, layout); - - g_free (hours); - } - - /* grid, vertical lines first */ - for (i = 0; i < 7; i++) - { - cairo_move_to (cr, - sidebar_width + ((width - sidebar_width) / 7) * i + 0.4, - 0); - cairo_rel_line_to (cr, 0, height); - } - - /* rest of the lines */ - for (i = 0; i < 24; i++) - { - /* hours lines */ - cairo_move_to (cr, 0, (height / 24) * i + 0.4); - cairo_rel_line_to (cr, width, 0); - } - - cairo_stroke (cr); - - cairo_set_dash (cr, dashed, 2, 0); - for (i = 0; i < 24; i++) - { - /* 30 minutes lines */ - cairo_move_to (cr, sidebar_width, (height / 24) * i + (height / 48) + 0.4); - cairo_rel_line_to (cr, width - sidebar_width, 0); - } - - cairo_stroke (cr); - - pango_font_description_free (font_desc); - g_object_unref (layout); -} - -static gint -get_sidebar_width (GtkWidget *widget) -{ - GtkStyleContext *context; - GtkBorder padding; - - PangoLayout *layout; - PangoFontDescription *font_desc; - gint mid_width; - gint noon_width; - gint hours_width; - gint sidebar_width; - - context = gtk_widget_get_style_context (widget); - - gtk_style_context_get_padding ( - gtk_widget_get_style_context (widget), - gtk_widget_get_state_flags (widget), - &padding); - - layout = pango_layout_new (gtk_widget_get_pango_context (widget)); - gtk_style_context_get (context, - gtk_widget_get_state_flags(widget), - "font", &font_desc, - NULL); - pango_layout_set_font_description (layout, font_desc); - - pango_layout_set_text (layout, _("Midnight"), -1); - pango_layout_get_pixel_size (layout, &mid_width, NULL); - - pango_layout_set_text (layout, _("Noon"), -1); - pango_layout_get_pixel_size (layout, &noon_width, NULL); - - pango_layout_set_text (layout, _("00:00 PM"), -1); - pango_layout_get_pixel_size (layout, &hours_width, NULL); - - sidebar_width = noon_width > mid_width ? noon_width : mid_width; - sidebar_width = sidebar_width > hours_width ? 0 : hours_width; - sidebar_width += padding.left + padding.right; - - pango_font_description_free (font_desc); - g_object_unref (layout); - - return sidebar_width; -} - -static void -gcal_week_view_class_init (GcalWeekViewClass *klass) -{ - GcalSubscriberViewClass *subscriber_view_class; - GtkContainerClass *container_class; - GtkWidgetClass *widget_class; - GObjectClass *object_class; - - subscriber_view_class = GCAL_SUBSCRIBER_VIEW_CLASS (klass); - subscriber_view_class->get_child_by_uuid = gcal_week_view_get_by_uuid; - - container_class = GTK_CONTAINER_CLASS (klass); - container_class->add = gcal_week_view_add; - container_class->remove = gcal_week_view_remove; - container_class->forall = gcal_week_view_forall; - gtk_container_class_handle_border_width (container_class); - - widget_class = GTK_WIDGET_CLASS (klass); - widget_class->realize = gcal_week_view_realize; - widget_class->unrealize = gcal_week_view_unrealize; - widget_class->size_allocate = gcal_week_view_size_allocate; - widget_class->draw = gcal_week_view_draw; - widget_class->scroll_event = gcal_week_view_scroll_event; - widget_class->button_press_event = gcal_week_view_button_press_event; - widget_class->button_release_event = gcal_week_view_button_release_event; - - object_class = G_OBJECT_CLASS (klass); - object_class->constructed = gcal_week_view_constructed; - object_class->finalize = gcal_week_view_finalize; - object_class->set_property = gcal_week_view_set_property; - object_class->get_property = gcal_week_view_get_property; - - g_object_class_override_property (object_class, - PROP_DATE, "active-date"); -} - -static void -gcal_week_view_init (GcalWeekView *self) -{ - gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); - gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)), "calendar-view"); -} - -static void -gcal_view_interface_init (GcalViewInterface *iface) -{ - iface->get_initial_date = gcal_week_view_get_initial_date; - iface->get_final_date = gcal_week_view_get_final_date; - - /* iface->clear_marks = gcal_week_view_clear_marks; */ -} - -static void -gcal_week_view_constructed (GObject *object) -{ - GcalWeekViewPrivate *priv; - - g_return_if_fail (GCAL_IS_WEEK_VIEW (object)); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (object)); - - if (G_OBJECT_CLASS (gcal_week_view_parent_class)->constructed != NULL) - G_OBJECT_CLASS (gcal_week_view_parent_class)->constructed (object); - - priv->vscrollbar = gtk_scrollbar_new (GTK_ORIENTATION_VERTICAL, NULL); - - gtk_widget_set_parent (priv->vscrollbar, GTK_WIDGET (object)); - gtk_widget_show (priv->vscrollbar); - - g_signal_connect (gtk_range_get_adjustment (GTK_RANGE (priv->vscrollbar)), - "value-changed", - G_CALLBACK (gcal_week_view_scroll_value_changed), - object); - - g_object_ref (priv->vscrollbar); -} - -static void -gcal_week_view_finalize (GObject *object) -{ - GcalWeekViewPrivate *priv; - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (object)); - - if (priv->date != NULL) - g_free (priv->date); - - if (priv->vscrollbar != NULL) - { - gtk_widget_unparent (priv->vscrollbar); - g_clear_object (&(priv->vscrollbar)); - } - - /* Chain up to parent's finalize() method. */ - G_OBJECT_CLASS (gcal_week_view_parent_class)->finalize (object); -} - -static void -gcal_week_view_set_property (GObject *object, - guint property_id, - const GValue *value, - GParamSpec *pspec) -{ - GcalWeekViewPrivate *priv; - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (object)); - - switch (property_id) - { - case PROP_DATE: - { - time_t range_start, range_end; - icaltimetype *date; - icaltimezone* default_zone; - - if (priv->date != NULL) - g_free (priv->date); - - priv->date = g_value_dup_boxed (value); - - default_zone = - gcal_manager_get_system_timezone (priv->manager); - date = gcal_view_get_initial_date (GCAL_VIEW (object)); - range_start = icaltime_as_timet_with_zone (*date, - default_zone); - g_free (date); - date = gcal_view_get_final_date (GCAL_VIEW (object)); - range_end = icaltime_as_timet_with_zone (*date, - default_zone); - g_free (date); - gcal_manager_set_subscriber (priv->manager, - E_CAL_DATA_MODEL_SUBSCRIBER (object), - range_start, - range_end); - gtk_widget_queue_draw (GTK_WIDGET (object)); - break; - } - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); - break; - } -} - -static void -gcal_week_view_get_property (GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec) -{ - GcalWeekViewPrivate *priv; - - g_return_if_fail (GCAL_IS_WEEK_VIEW (object)); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (object)); - - switch (property_id) - { - case PROP_DATE: - g_value_set_boxed (value, priv->date); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); - break; - } -} - -static void -gcal_week_view_realize (GtkWidget *widget) -{ - GcalWeekViewPrivate *priv; - GdkWindow *parent_window; - GdkWindowAttr attributes; - gint attributes_mask; - GtkAllocation allocation; - - gint i; - GList *l; - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (widget)); - gtk_widget_set_realized (widget, TRUE); - - gtk_widget_get_allocation (widget, &allocation); - - parent_window = gtk_widget_get_parent_window (widget); - gtk_widget_set_window (widget, parent_window); - g_object_ref (parent_window); - - attributes.window_type = GDK_WINDOW_CHILD; - attributes.wclass = GDK_INPUT_ONLY; - attributes.x = allocation.x; - attributes.y = allocation.y; - attributes.event_mask = gtk_widget_get_events (widget); - attributes.event_mask |= (GDK_BUTTON_PRESS_MASK | - GDK_BUTTON_RELEASE_MASK); - attributes_mask = GDK_WA_X | GDK_WA_Y; - - priv->event_window = gdk_window_new (parent_window, - &attributes, - attributes_mask); - gtk_widget_register_window (widget, priv->event_window); - - attributes.window_type = GDK_WINDOW_CHILD; - attributes.wclass = GDK_INPUT_OUTPUT; - attributes.x = allocation.x; - attributes.y = allocation.y; - attributes.event_mask = gtk_widget_get_events (widget); - attributes.event_mask |= (GDK_EXPOSURE_MASK | - GDK_BUTTON_PRESS_MASK | - GDK_BUTTON_RELEASE_MASK | - GDK_BUTTON_MOTION_MASK | - GDK_POINTER_MOTION_MASK | - GDK_SCROLL_MASK | - GDK_TOUCH_MASK | - GDK_SMOOTH_SCROLL_MASK); - attributes.visual = gtk_widget_get_visual (widget); - attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; - - priv->view_window = gdk_window_new (parent_window, - &attributes, - attributes_mask); - gtk_widget_register_window (widget, priv->view_window); - - attributes.event_mask = gtk_widget_get_events (widget); - attributes.event_mask |= GDK_EXPOSURE_MASK; - priv->grid_window = gdk_window_new (priv->view_window, - &attributes, - attributes_mask); - gtk_widget_register_window (widget, priv->grid_window); - - gdk_window_show (priv->event_window); - gdk_window_show (priv->grid_window); - gdk_window_show (priv->view_window); - - /* setting parent_window for every event child */ - for (i = 0; i < 7; i++) - { - for (l = priv->days[i]; l != NULL; l = l->next) - { - GcalWeekViewChild *child; - - child = (GcalWeekViewChild*) l->data; - gtk_widget_set_parent_window (child->widget, - priv->grid_window); - } - } -} - -static void -gcal_week_view_unrealize (GtkWidget *widget) -{ - GcalWeekViewPrivate *priv; - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (widget)); - if (priv->view_window != NULL) - { - gtk_widget_unregister_window (widget, priv->view_window); - gdk_window_destroy (priv->view_window); - priv->view_window = NULL; - } - - if (priv->grid_window != NULL) - { - gtk_widget_unregister_window (widget, priv->grid_window); - gdk_window_destroy (priv->grid_window); - priv->grid_window = NULL; - } - if (priv->event_window != NULL) - { - gtk_widget_unregister_window (widget, priv->event_window); - gdk_window_destroy (priv->event_window); - priv->event_window = NULL; - } - - GTK_WIDGET_CLASS (gcal_week_view_parent_class)->unrealize (widget); -} - -static void -gcal_week_view_size_allocate (GtkWidget *widget, - GtkAllocation *allocation) -{ - GcalWeekViewPrivate *priv; - - GtkBorder padding; - PangoLayout *layout; - PangoFontDescription *font_desc; - gint font_height; - - gint start_grid_y; - gint sidebar_width; - - GtkAllocation scroll_allocation; - gint min; - gint natural; - - gdouble horizontal_block; - gdouble vertical_block; - gdouble adj_value; - - gint i; - GList *l; - - gboolean scroll_needed; - gint grid_height; - gint view_height; - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (widget)); - - gtk_widget_set_allocation (widget, allocation); - - if (! gtk_widget_get_realized (widget)) - return; - - gtk_style_context_get_padding ( - gtk_widget_get_style_context (widget), - gtk_widget_get_state_flags (widget), - &padding); - - start_grid_y = get_start_grid_y (widget); - - gdk_window_move_resize (priv->event_window, - allocation->x, - allocation->y, - allocation->width, - start_grid_y); - - /* Estimating cell height */ - layout = pango_layout_new (gtk_widget_get_pango_context (widget)); - gtk_style_context_get (gtk_widget_get_style_context (widget), - gtk_widget_get_state_flags (widget), - "font", &font_desc, - NULL); - pango_layout_set_font_description (layout, font_desc); - pango_layout_get_pixel_size (layout, NULL, &font_height); - pango_font_description_free (font_desc); - - gtk_widget_get_preferred_width (priv->vscrollbar, &min, &natural); - - view_height = allocation->height - start_grid_y; - grid_height = 24 * (padding.top + padding.bottom + 3 * font_height); - scroll_needed = grid_height > allocation->height - start_grid_y ? TRUE : FALSE; - grid_height = scroll_needed ? grid_height : allocation->height - start_grid_y; - - if (scroll_needed) - { - adj_value = gtk_adjustment_get_value ( - gtk_range_get_adjustment (GTK_RANGE (priv->vscrollbar))); - } - else - { - adj_value = 0; - } - - gdk_window_move_resize (priv->view_window, - allocation->x, - allocation->y + start_grid_y, - allocation->width, - allocation->height - start_grid_y); - gdk_window_move_resize (priv->grid_window, - allocation->x, - - adj_value, - allocation->width, - grid_height); - - if (scroll_needed) - { - scroll_allocation.x = allocation->width - natural; - scroll_allocation.y = start_grid_y; - scroll_allocation.width = natural; - scroll_allocation.height = view_height; - gtk_widget_size_allocate (priv->vscrollbar, &scroll_allocation); - - gtk_adjustment_set_page_size ( - gtk_range_get_adjustment (GTK_RANGE (priv->vscrollbar)), - view_height * view_height / grid_height); - gtk_adjustment_set_upper ( - gtk_range_get_adjustment (GTK_RANGE (priv->vscrollbar)), - grid_height - view_height + (view_height * view_height / grid_height)); - } - else - { - gtk_widget_hide (priv->vscrollbar); - } - - sidebar_width = get_sidebar_width (widget); - horizontal_block = (allocation->width - sidebar_width) / 7.0; - vertical_block = gdk_window_get_height (priv->grid_window) / 24.0; - - for (i = 0; i < 7; i++) - { - gdouble *added_height; - added_height = g_malloc0 (sizeof (gdouble) * 25); - - for (l = priv->days[i]; l != NULL; l = l->next) - { - GcalWeekViewChild *child; - gint min_height; - gint natural_height; - gdouble vertical_span; - GtkAllocation child_allocation; - - child = (GcalWeekViewChild*) l->data; - - if ((! gtk_widget_get_visible (child->widget)) && - (! child->hidden_by_me)) - { - continue; - } - - gtk_widget_get_preferred_height (child->widget, - &min_height, - &natural_height); - - child_allocation.x = i * horizontal_block + sidebar_width; - if (child->index == -1) - { - vertical_span = 2 * font_height; - child_allocation.y = start_grid_y - vertical_span; - } - else - { - vertical_span = vertical_block; - child_allocation.y = child->index * vertical_block; - } - - child_allocation.width = horizontal_block; - child_allocation.height = MIN (natural_height, vertical_span); - if (added_height[child->index + 1] + child_allocation.height > vertical_span) - { - gtk_widget_hide (child->widget); - child->hidden_by_me = TRUE; - } - else - { - gtk_widget_show (child->widget); - child->hidden_by_me = FALSE; - child_allocation.y = child_allocation.y + added_height[child->index + 1]; - gtk_widget_size_allocate (child->widget, &child_allocation); - added_height[child->index + 1] += child_allocation.height; - } - } - g_free (added_height); - } -} - -static gboolean -gcal_week_view_draw (GtkWidget *widget, - cairo_t *cr) -{ - GcalWeekViewPrivate *priv; - GtkStyleContext *context; - GtkStateFlags state; - GtkBorder padding; - GtkAllocation alloc; - - g_return_val_if_fail (GCAL_IS_WEEK_VIEW (widget), FALSE); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (widget)); - - context = gtk_widget_get_style_context (widget); - state = gtk_widget_get_state_flags (widget); - - /* getting padding and allocation */ - gtk_style_context_get_padding (context, state, &padding); - gtk_widget_get_allocation (widget, &alloc); - - if (gtk_cairo_should_draw_window (cr, gtk_widget_get_window (widget))) - { - cairo_save (cr); - cairo_set_line_width (cr, 0.4); - draw_header (GCAL_WEEK_VIEW (widget), cr, &alloc, &padding); - cairo_restore (cr); - } - - if (priv->view_window != NULL && - gtk_cairo_should_draw_window (cr, priv->view_window)) - { - gint y; - - gdk_window_get_position (priv->view_window, NULL, &y); - cairo_rectangle (cr, - alloc.x, - y, - alloc.width, - gdk_window_get_height (priv->view_window)); - cairo_clip (cr); - } - - if (priv->grid_window != NULL && - gtk_cairo_should_draw_window (cr, priv->grid_window)) - { - cairo_save (cr); - cairo_set_line_width (cr, 0.3); - draw_grid_window (GCAL_WEEK_VIEW (widget), cr); - cairo_restore (cr); - } - - GTK_WIDGET_CLASS (gcal_week_view_parent_class)->draw (widget, cr); - - return FALSE; -} - -static gboolean -gcal_week_view_scroll_event (GtkWidget *widget, - GdkEventScroll *event) -{ - GcalWeekViewPrivate *priv; - - gdouble delta_y; - gdouble delta; - - g_return_val_if_fail (GCAL_IS_WEEK_VIEW (widget), FALSE); - g_return_val_if_fail (event != NULL, FALSE); - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (widget)); - - if (gdk_event_get_scroll_deltas ((GdkEvent *) event, NULL, &delta_y)) - { - if (delta_y != 0.0 && - priv->vscrollbar != NULL && - gtk_widget_get_visible (priv->vscrollbar)) - { - GtkAdjustment *adj; - gdouble new_value; - gdouble page_size; - gdouble scroll_unit; - - adj = gtk_range_get_adjustment (GTK_RANGE (priv->vscrollbar)); - page_size = gtk_adjustment_get_page_size (adj); - scroll_unit = pow (page_size, 2.0 / 3.0); - - new_value = CLAMP (gtk_adjustment_get_value (adj) + delta_y * scroll_unit, - gtk_adjustment_get_lower (adj), - gtk_adjustment_get_upper (adj) - - gtk_adjustment_get_page_size (adj)); - - gtk_adjustment_set_value (adj, new_value); - - return TRUE; - } - } - else - { - GtkWidget *range = NULL; - - if (event->direction == GDK_SCROLL_UP || - event->direction == GDK_SCROLL_DOWN) - { - range = priv->vscrollbar; - } - - if (range != NULL && gtk_widget_get_visible (range)) - { - GtkAdjustment *adj; - gdouble new_value; - gdouble page_size; - - adj = gtk_range_get_adjustment (GTK_RANGE (range)); - page_size = gtk_adjustment_get_page_size (adj); - delta = pow (page_size, 2.0 / 3.0); - - new_value = CLAMP (gtk_adjustment_get_value (adj) + delta, - gtk_adjustment_get_lower (adj), - gtk_adjustment_get_upper (adj) - - gtk_adjustment_get_page_size (adj)); - - gtk_adjustment_set_value (adj, new_value); - - return TRUE; - } - } - - return FALSE; -} - -static gboolean -gcal_week_view_button_press_event (GtkWidget *widget, - GdkEventButton *event) -{ - gdouble y, start_grid_y; - - y = event->y; - - start_grid_y = get_start_grid_y (widget); - - if (y - start_grid_y < 0) - { - /* FIXME: click on the header should create all day events */ - return FALSE; - } - - return FALSE; -} - -static gboolean -gcal_week_view_button_release_event (GtkWidget *widget, - GdkEventButton *event) -{ - GcalWeekViewPrivate *priv; - gdouble y, start_grid_y; - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (widget)); - - y = event->y; - - start_grid_y = get_start_grid_y (widget); - - if (y - start_grid_y < 0) - { - /* FIXME: click on the header should create all day events */ - priv->clicked_cell = -1; - return FALSE; - } - - priv->clicked_cell = -1; - return FALSE; -} - -static void -gcal_week_view_add (GtkContainer *container, - GtkWidget *widget) -{ - GcalWeekViewPrivate *priv; - GList *l; - - gint day; - - GcalWeekViewChild *new_child; - GDateTime *date; - GcalEvent *event; - - g_return_if_fail (GCAL_IS_WEEK_VIEW (container)); - g_return_if_fail (GCAL_IS_EVENT_WIDGET (widget)); - g_return_if_fail (gtk_widget_get_parent (widget) == NULL); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (container)); - - /* Check if it's already added for date */ - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - date = gcal_event_get_date_start (event); - day = (g_date_time_get_day_of_week (date) - priv->first_weekday + 6) % 7; - - for (l = priv->days[day]; l != NULL; l = l->next) - { - GtkWidget *event_widget; - - event_widget = GTK_WIDGET (((GcalWeekViewChild*) l->data)->widget); - if (gcal_event_widget_equal (GCAL_EVENT_WIDGET (widget), - GCAL_EVENT_WIDGET (event_widget))) - { - //TODO: remove once the main-dev phase its over - g_warning ("Trying to add an event with the same uuid to the view"); - g_free (date); - gtk_widget_destroy (widget); - return; - } - } - - new_child = g_new0 (GcalWeekViewChild, 1); - new_child->widget = widget; - new_child->hidden_by_me = FALSE; - - if (gcal_event_get_all_day (event)) - { - new_child->index = -1; - if (gtk_widget_get_window (widget) != NULL) - gtk_widget_set_parent_window (widget, - gtk_widget_get_window (widget)); - } - else - { - new_child->index = g_date_time_get_hour (date); - if (priv->grid_window != NULL) - gtk_widget_set_parent_window (widget, priv->grid_window); - } - - priv->days[day] = g_list_append (priv->days[day], new_child); - gtk_widget_set_parent (widget, GTK_WIDGET (container)); - - g_signal_connect (widget, - "activate", - G_CALLBACK (event_opened), - container); -} - -static void -gcal_week_view_remove (GtkContainer *container, - GtkWidget *widget) -{ - GcalWeekViewPrivate *priv; - GcalEvent *event; - GDateTime *date; - GList *l; - gint day; - gboolean was_visible; - - g_return_if_fail (GCAL_IS_WEEK_VIEW (container)); - g_return_if_fail (gtk_widget_get_parent (widget) == GTK_WIDGET (container)); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (container)); - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - date = gcal_event_get_date_start (event); - day = (g_date_time_get_day_of_week (date) - priv->first_weekday + 6) % 7; - - for (l = priv->days[day]; l != NULL; l = l->next) - { - GcalWeekViewChild *child; - - child = (GcalWeekViewChild*) l->data; - if (child->widget == widget) - { - priv->days[day] = g_list_remove (priv->days[day], child); - g_free (child); - break; - } - } - - - was_visible = gtk_widget_get_visible (widget); - gtk_widget_unparent (widget); - - if (was_visible) - gtk_widget_queue_resize (GTK_WIDGET (container)); - - g_free (date); -} - -static void -gcal_week_view_forall (GtkContainer *container, - gboolean include_internals, - GtkCallback callback, - gpointer callback_data) -{ - GcalWeekViewPrivate *priv; - gint i; - GList *l; - - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (container)); - - for (i = 0; i < 7; i++) - { - l = priv->days[i]; - - while (l) - { - GcalWeekViewChild *child; - - child = (GcalWeekViewChild*) l->data; - l = l->next; - - (* callback) (child->widget, callback_data); - } - } - - if (include_internals && priv->vscrollbar != NULL) - (* callback) (priv->vscrollbar, callback_data); -} - -static void -gcal_week_view_scroll_value_changed (GtkAdjustment *adjusment, - gpointer user_data) -{ - GcalWeekViewPrivate *priv; - - g_return_if_fail (GCAL_IS_WEEK_VIEW (user_data)); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW (user_data)); - - if (priv->grid_window != NULL) - { - gdk_window_move (priv->grid_window, - 0, - - gtk_adjustment_get_value (adjusment)); - } -} - -/* GcalView Interface API */ -/** - * gcal_week_view_get_initial_date: - * - * Since: 0.1 - * Return value: the first day of the week - * Returns: (transfer full): Release with g_free() - **/ -static icaltimetype* -gcal_week_view_get_initial_date (GcalView *view) -{ - GcalWeekViewPrivate *priv; - icaltimetype *new_date; - - g_return_val_if_fail (GCAL_IS_WEEK_VIEW (view), NULL); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW(view)); - new_date = g_new0 (icaltimetype, 1); - *new_date = icaltime_from_day_of_year ( - icaltime_start_doy_week (*(priv->date), priv->first_weekday + 1), - priv->date->year); - new_date->is_date = 0; - new_date->hour = 0; - new_date->minute = 0; - new_date->second = 0; - *new_date = icaltime_set_timezone (new_date, priv->date->zone); - return new_date; -} - -/** - * gcal_week_view_get_final_date: - * - * Since: 0.1 - * Return value: the last day of the week - * Returns: (transfer full): Release with g_free() - **/ -static icaltimetype* -gcal_week_view_get_final_date (GcalView *view) -{ - GcalWeekViewPrivate *priv; - icaltimetype *new_date; - - g_return_val_if_fail (GCAL_IS_WEEK_VIEW (view), NULL); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW(view)); - new_date = g_new0 (icaltimetype, 1); - *new_date = icaltime_from_day_of_year ( - icaltime_start_doy_week (*(priv->date), priv->first_weekday + 1) + 6, - priv->date->year); - new_date->is_date = 0; - new_date->hour = 23; - new_date->minute = 59; - new_date->second = 0; - *new_date = icaltime_set_timezone (new_date, priv->date->zone); - return new_date; -} - -static GtkWidget* -gcal_week_view_get_by_uuid (GcalSubscriberView *subscriber_view, - const gchar *uuid) -{ - GcalWeekViewPrivate *priv; - gint i; - GList *l; - - g_return_val_if_fail (GCAL_IS_WEEK_VIEW (subscriber_view), NULL); - priv = gcal_week_view_get_instance_private (GCAL_WEEK_VIEW(subscriber_view)); - - for (i = 0; i < 7; i++) - { - for (l = priv->days[i]; l != NULL; l = l->next) - { - GcalWeekViewChild *child; - GcalEvent *event; - const gchar* widget_uuid; - - child = (GcalWeekViewChild*) l->data; - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (child->widget)); - widget_uuid = gcal_event_get_uid (event); - - if (g_strcmp0 (uuid, widget_uuid) == 0) - return child->widget; - } - } - return NULL; -} - -/* Public API */ -/** - * gcal_week_view_new: - * - * Create a week-view widget - * - * Returns: (transfer full): - **/ -GtkWidget* -gcal_week_view_new (void) -{ - return g_object_new (GCAL_TYPE_WEEK_VIEW, NULL); -} - -void -gcal_week_view_set_manager (GcalWeekView *week_view, - GcalManager *manager) -{ - GcalWeekViewPrivate *priv = gcal_week_view_get_instance_private (week_view); - - priv->manager = manager; -} - -/** - * gcal_week_view_set_first_weekday: - * @view: - * @day_nr: - * - * Set the first day of the week according to the locale, being - * 0 for Sunday, 1 for Monday and so on. - **/ -void -gcal_week_view_set_first_weekday (GcalWeekView *view, - gint day_nr) -{ - GcalWeekViewPrivate *priv; - - priv = gcal_week_view_get_instance_private (view); - priv->first_weekday = day_nr; -} - -/** - * gcal_week_view_set_use_24h_format: - * @view: - * @use_24h: - * - * Whether the view will show time using 24h or 12h format - **/ -void -gcal_week_view_set_use_24h_format (GcalWeekView *view, - gboolean use_24h) -{ - GcalWeekViewPrivate *priv; - - priv = gcal_week_view_get_instance_private (view); - priv->use_24h_format = use_24h; -} diff --git a/src/gcal-window.c b/src/gcal-window.c index 5bc2bbbf..7e5aa0a0 100644 --- a/src/gcal-window.c +++ b/src/gcal-window.c @@ -22,8 +22,8 @@ #include "gcal-manager.h" #include "gcal-view.h" -#include "gcal-month-view.h" #include "gcal-week-view.h" +#include "gcal-month-view.h" #include "gcal-year-view.h" #include "gcal-search-view.h" #include "gcal-event-widget.h" @@ -66,6 +66,7 @@ struct _GcalWindow GtkWidget *search_bar; GtkWidget *views_overlay; GtkWidget *views_stack; + GtkWidget *week_view; GtkWidget *month_view; GtkWidget *year_view; GtkWidget *notification; @@ -336,7 +337,7 @@ on_view_action_activated (GSimpleAction *action, else if (view == -2) view = --(window->active_view); - window->active_view = CLAMP (view, GCAL_WINDOW_VIEW_MONTH, GCAL_WINDOW_VIEW_YEAR); + window->active_view = CLAMP (view, CLAMP(view, GCAL_WINDOW_VIEW_WEEK, GCAL_WINDOW_VIEW_MONTH), GCAL_WINDOW_VIEW_YEAR); gtk_stack_set_visible_child (GTK_STACK (window->views_stack), window->views[window->active_view]); g_object_notify (G_OBJECT (user_data), "active-view"); @@ -364,6 +365,7 @@ update_active_date (GcalWindow *window, GDateTime *date_start, *date_end; time_t range_start, range_end; icaltimetype *previous_date; + GDate old_week, new_week; previous_date = window->active_date; window->active_date = new_date; @@ -399,6 +401,32 @@ update_active_date (GcalWindow *window, g_clear_pointer (&date_end, g_date_time_unref); } + /* week_view */ + g_date_clear (&old_week, 1); + + if (previous_date->day > 0 && previous_date->month > 0 && previous_date->year) + g_date_set_dmy (&old_week, previous_date->day, previous_date->month, previous_date->year); + + g_date_clear (&new_week, 1); + g_date_set_dmy (&new_week, new_date->day, new_date->month, new_date->year); + + if (previous_date->year != new_date->year || + !g_date_valid (&old_week) || + g_date_get_iso8601_week_of_year (&old_week) != g_date_get_iso8601_week_of_year (&new_week)) + { + date_start = g_date_time_new_local (new_date->year, new_date->month, new_date->day, 0, 0, 0); + date_start = g_date_time_add_days (date_start, (get_first_weekday () - g_date_time_get_day_of_week (date_start) + 7) % 7 - 7); + range_start = g_date_time_to_unix (date_start); + + date_end = g_date_time_add_days (date_start, 7); + range_end = g_date_time_to_unix (date_end); + + gcal_manager_set_subscriber (window->manager, E_CAL_DATA_MODEL_SUBSCRIBER (window->week_view), range_start, range_end); + + g_clear_pointer (&date_start, g_date_time_unref); + g_clear_pointer (&date_end, g_date_time_unref); + } + update_today_button_sensitive (window); g_free (previous_date); @@ -415,6 +443,7 @@ update_current_date (GcalWindow *window) *(window->current_date) = icaltime_current_time_with_zone (gcal_manager_get_system_timezone (window->manager)); *(window->current_date) = icaltime_set_timezone (window->current_date, gcal_manager_get_system_timezone (window->manager)); + gcal_week_view_set_current_date (GCAL_WEEK_VIEW (window->week_view), window->current_date); gcal_month_view_set_current_date (GCAL_MONTH_VIEW (window->month_view), window->current_date); gcal_year_view_set_current_date (GCAL_YEAR_VIEW (window->year_view), window->current_date); @@ -437,8 +466,8 @@ update_today_button_sensitive (GcalWindow *window) break; case GCAL_WINDOW_VIEW_WEEK: - /* TODO: determine when the today button would be sensitive on week view */ - sensitive = TRUE; + sensitive = window->active_date->year != window->current_date->year || + icaltime_week_number (*window->active_date) != icaltime_week_number (*window->current_date); break; case GCAL_WINDOW_VIEW_MONTH: @@ -1311,6 +1340,7 @@ gcal_window_class_init(GcalWindowClass *klass) gtk_widget_class_bind_template_child (widget_class, GcalWindow, forward_button); gtk_widget_class_bind_template_child (widget_class, GcalWindow, views_overlay); gtk_widget_class_bind_template_child (widget_class, GcalWindow, views_stack); + gtk_widget_class_bind_template_child (widget_class, GcalWindow, week_view); gtk_widget_class_bind_template_child (widget_class, GcalWindow, month_view); gtk_widget_class_bind_template_child (widget_class, GcalWindow, year_view); gtk_widget_class_bind_template_child (widget_class, GcalWindow, views_switcher); @@ -1404,18 +1434,15 @@ gcal_window_constructed (GObject *object) g_object_unref (builder); - /* XXX: Week view disabled until after the release when we restart the work on it*/ - //priv->views[GCAL_WINDOW_VIEW_WEEK] = gcal_week_view_new (); - //gcal_week_view_set_manager (GCAL_WEEK_VIEW (priv->views[GCAL_WINDOW_VIEW_WEEK]), priv->manager); - //gcal_week_view_set_first_weekday (GCAL_WEEK_VIEW (priv->views[GCAL_WINDOW_VIEW_WEEK]), get_first_weekday ()); - //gcal_week_view_set_use_24h_format (GCAL_WEEK_VIEW (priv->views[GCAL_WINDOW_VIEW_WEEK]), use_24h_format); - //gtk_stack_add_titled (GTK_STACK (priv->views_stack), priv->views[GCAL_WINDOW_VIEW_WEEK], "week", _("Week")); - + window->views[GCAL_WINDOW_VIEW_WEEK] = window->week_view; window->views[GCAL_WINDOW_VIEW_MONTH] = window->month_view; window->views[GCAL_WINDOW_VIEW_YEAR] = window->year_view; gcal_edit_dialog_set_time_format (GCAL_EDIT_DIALOG (window->edit_dialog), use_24h_format); + gcal_week_view_set_first_weekday (GCAL_WEEK_VIEW (window->views[GCAL_WINDOW_VIEW_WEEK]), get_first_weekday ()); + gcal_week_view_set_use_24h_format (GCAL_WEEK_VIEW (window->views[GCAL_WINDOW_VIEW_WEEK]), use_24h_format); + gcal_month_view_set_first_weekday (GCAL_MONTH_VIEW (window->views[GCAL_WINDOW_VIEW_MONTH]), get_first_weekday ()); gcal_month_view_set_use_24h_format (GCAL_MONTH_VIEW (window->views[GCAL_WINDOW_VIEW_MONTH]), use_24h_format); @@ -1515,6 +1542,7 @@ gcal_window_set_property (GObject *object, g_signal_connect_swapped (self->manager, "source-changed", G_CALLBACK (source_changed), object); gcal_edit_dialog_set_manager (GCAL_EDIT_DIALOG (self->edit_dialog), self->manager); + gcal_week_view_set_manager (GCAL_WEEK_VIEW (self->week_view), self->manager); gcal_month_view_set_manager (GCAL_MONTH_VIEW (self->month_view), self->manager); gcal_year_view_set_manager (GCAL_YEAR_VIEW (self->year_view), self->manager); gcal_quick_add_popover_set_manager (GCAL_QUICK_ADD_POPOVER (self->quick_add_popover), self->manager); @@ -1628,6 +1656,7 @@ gcal_window_new_with_view_and_date (GcalApplication *app, gcal_window_add_accelerator (app, "win.change-view(-1)", "Page_Down"); gcal_window_add_accelerator (app, "win.change-view(-2)", "Page_Up"); + gcal_window_add_accelerator (app, "win.change-view(1)", "1") gcal_window_add_accelerator (app, "win.change-view(2)", "2"); gcal_window_add_accelerator (app, "win.change-view(3)", "3"); diff --git a/src/views/gcal-range-tree.c b/src/views/gcal-range-tree.c new file mode 100644 index 00000000..8b856490 --- /dev/null +++ b/src/views/gcal-range-tree.c @@ -0,0 +1,604 @@ +/* gcal-range-tree.c + * + * Copyright (C) 2016 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 . + */ + +#include "gcal-range-tree.h" + +typedef struct _Node +{ + struct _Node *left; + struct _Node *right; + guint16 start; + guint16 end; + guint16 max; + guint16 hits; + gint64 height; + GPtrArray *data_array; +} Node; + +struct _GcalRangeTree +{ + guint ref_count; + + Node *root; +}; + +G_DEFINE_BOXED_TYPE (GcalRangeTree, gcal_range_tree, gcal_range_tree_ref, gcal_range_tree_unref) + +/* Auxiliary methods */ +static void +destroy_tree (Node *n) +{ + if (!n) + return; + + destroy_tree (n->left); + destroy_tree (n->right); + + g_ptr_array_unref (n->data_array); + g_free (n); +} + +static inline gint32 +height (Node *n) +{ + return n ? n->height : 0; +} + +static inline void +update_height (Node *n) +{ + n->height = MAX (height (n->left), height (n->right)) + 1; +} + +static inline guint32 +balance (Node *n) +{ + return n ? height (n->left) - height (n->right) : 0; +} + +static inline gboolean +compare_intervals (guint16 a_start, + guint16 a_end, + guint16 b_start, + guint16 b_end) +{ + if (a_start != b_start) + return b_start - a_start; + + if (a_end != b_end) + return b_end - a_end; + + return 0; +} + +static inline gboolean +is_interval_lower (Node *n, + guint32 start, + guint32 end) +{ + return start < n->start || (start == n->start && end < n->end); +} + +static inline gboolean +is_interval_higher (Node *n, + guint32 start, + guint32 end) +{ + return start > n->start || (start == n->start && end > n->end); +} + +static Node* +node_new (guint32 start, + guint32 end, + gpointer data) +{ + Node *n; + + n = g_new0 (Node, 1); + n->start = start; + n->end = end; + n->max = end; + n->height = 1; + n->hits = 1; + + n->data_array = g_ptr_array_new (); + g_ptr_array_add (n->data_array, data); + + return n; +} + +/* AVL rotations */ +static Node* +rotate_left (Node *n) +{ + Node *tmp; + + tmp = n->right; + n->right = tmp->left; + tmp->left = n; + + /* Update heights */ + update_height (n); + update_height (tmp); + + return tmp; +} + +static Node* +rotate_right (Node *n) +{ + Node *tmp; + + tmp = n->left; + n->left = tmp->right; + tmp->right = n; + + /* Update heights */ + update_height (n); + update_height (tmp); + + return tmp; +} + +static inline Node* +hit_node (Node *n, + gpointer data) +{ + n->hits++; + g_ptr_array_add (n->data_array, data); + return n; +} + +static inline Node* +rebalance (Node *n) +{ + gint32 node_balance; + + update_height (n); + + /* Rotate the tree */ + node_balance = balance (n); + + if (node_balance > 1) + { + if (n->left && height (n->left->right) > height (n->left->left)) + n->left = rotate_left (n->left); + + return rotate_right (n); + } + else if (node_balance < -1) + { + if (n->right && height (n->right->right) < height (n->right->left)) + n->right = rotate_right (n->right); + + return rotate_left (n); + } + + return n; +} + +static Node* +insert (Node *n, + guint32 start, + guint32 end, + gpointer data) +{ + if (!n) + return node_new (start, end, data); + + if (compare_intervals (n->start, n->end, start, end) < 0) + n->left = insert (n->left, start, end, data); + else if (compare_intervals (n->start, n->end, start, end) > 0) + n->right = insert (n->right, start, end, data); + else + return hit_node (n, data); + + /* Update the current node's maximum subrange value */ + n->max = MAX (n->max, end); + + return rebalance (n); +} + +/* Remove */ +static Node* +find_minimum (Node *n) +{ + if (n->left) + return find_minimum (n->left); + + return n; +} + +static Node* +delete_minimum (Node *n) +{ + if (!n->left) + return n->right; + + n->left = delete_minimum (n->left); + + return rebalance (n); +} + +static inline Node* +delete_node (Node *n, + gpointer data) +{ + Node *left, *right, *min; + + n->hits--; + g_ptr_array_remove (n->data_array, data); + + /* Only remove the node when the hit count reaches zero */ + if (n->hits > 0) + return n; + + left = n->left; + right = n->right; + + g_ptr_array_unref (n->data_array); + g_free (n); + + if (!right) + return left; + + min = find_minimum (right); + min->right = delete_minimum (right); + min->left = left; + + return rebalance (min); +} + +static Node* +remove (Node *n, + guint32 start, + guint32 end, + gpointer data) +{ + if (!n) + return NULL; + + if (is_interval_lower (n, start, end)) + n->left = remove (n->left, start, end, data); + else if (is_interval_higher (n, start, end)) + n->right = remove (n->right, start, end, data); + else + return delete_node (n, data); + + return rebalance (n); +} +/* Traverse */ +static inline gboolean +run_traverse_func (Node *n, + GcalRangeTraverseFunc func, + gpointer user_data) +{ + guint i; + + for (i = 0; i < n->hits; i++) + { + if (func (n->start, n->end, g_ptr_array_index (n->data_array, i), user_data)) + return TRUE; + } + + return FALSE; +} + +static gboolean +traverse (Node *n, + GTraverseType type, + GcalRangeTraverseFunc func, + gpointer user_data) +{ + if (!n) + return FALSE; + + if (type == G_PRE_ORDER) + { + if (run_traverse_func (n, func, user_data)) + return TRUE; + } + + if (traverse (n->left, type, func, user_data)) + return TRUE; + + if (type == G_IN_ORDER) + { + if (run_traverse_func (n, func, user_data)) + return TRUE; + } + + if (traverse (n->right, type, func, user_data)) + return TRUE; + + if (type == G_POST_ORDER) + { + if (run_traverse_func (n, func, user_data)) + return TRUE; + } + + return FALSE; +} + +/* Internal traverse functions */ +static inline gboolean +gather_data_at_range (guint16 start, + guint16 end, + gpointer data, + gpointer user_data) +{ + struct { + guint16 start, end; + GPtrArray **array; + } *range = user_data; + + /* Since we're traversing in order, stop here */ + if (compare_intervals (range->start, range->end, start, end) > 0 && + start >= range->end) + { + return TRUE; + } + + /* + * If the current range doesn't overlap but we're before the desired range, keep + * traversing the tree + */ + if (range->start >= end) + return FALSE; + + if (!*range->array) + *range->array = g_ptr_array_new (); + + g_ptr_array_add (*range->array, data); + + return FALSE; +} + +static inline gboolean +count_entries_at_range (guint16 start, + guint16 end, + gpointer data, + gpointer user_data) +{ + struct { + guint16 start, end; + guint64 counter; + } *range = user_data; + + /* Since we're traversing in order, stop here */ + if (compare_intervals (range->start, range->end, start, end) > 0 && + start >= range->end) + { + return TRUE; + } + + /* + * If the current range doesn't overlap but we're before the desired range, keep + * traversing the tree + */ + if (range->start >= end) + return FALSE; + + range->counter++; + + return FALSE; +} +static void +gcal_range_tree_free (GcalRangeTree *self) +{ + g_assert (self); + g_assert_cmpint (self->ref_count, ==, 0); + + destroy_tree (self->root); + + g_slice_free (GcalRangeTree, self); +} + +/** + * gcal_range_tree_new: + * + * Creates a new range tree + * + * Returns: (transfer full): a newly created #GcalRangeTree. + * Free with gcal_range_tree_unref() when done. + */ +GcalRangeTree* +gcal_range_tree_new (void) +{ + GcalRangeTree *self; + + self = g_slice_new0 (GcalRangeTree); + self->ref_count = 1; + + return self; +} + +/** + * gcal_range_tree_copy: + * @self: a #GcalRangeTree + * + * Copies @self into a new range tree. + * + * Returns: (transfer full): a newly created #GcalRangeTree. + * Free with gcal_range_tree_unref() when done. + */ +GcalRangeTree* +gcal_range_tree_copy (GcalRangeTree *self) +{ + GcalRangeTree *copy; + + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (self->ref_count, NULL); + + copy = gcal_range_tree_new (); + + return copy; +} + +/** + * gcal_range_tree_ref: + * @self: a #GcalRangeTree + * + * Increases the reference count of @self. + * + * Returns: (transfer full): pointer to the just-referenced tree. + */ +GcalRangeTree* +gcal_range_tree_ref (GcalRangeTree *self) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (self->ref_count, NULL); + + g_atomic_int_inc (&self->ref_count); + + return self; +} + +/** + * gcal_range_tree_unref: + * @self: a #GcalRangeTree + * + * Decreases the reference count of @self, and frees it when + * if the reference count reaches zero. + */ +void +gcal_range_tree_unref (GcalRangeTree *self) +{ + g_return_if_fail (self); + g_return_if_fail (self->ref_count); + + if (g_atomic_int_dec_and_test (&self->ref_count)) + gcal_range_tree_free (self); +} + +/** + * gcal_range_tree_add_range: + * @self: a #GcalRangeTree + * @start: start of the interval + * @end: end of the interval + * @data: user data + * + * Adds the range into @self, and attach @data do it. It is + * possible to have multiple entries in the same interval, + * in which case the interval will have a reference counting like + * behavior. + */ +void +gcal_range_tree_add_range (GcalRangeTree *self, + guint16 start, + guint16 end, + gpointer data) +{ + g_return_if_fail (self); + g_return_if_fail (end >= start); + + self->root = insert (self->root, start, end, data); +} + +/** + * gcal_range_tree_remove_range: + * @self: a #GcalRangeTree + * @start: the start of the range + * @end: the end of the range + * @data: user data + * + * Removes (or decreases the reference count) of the given interval. + */ +void +gcal_range_tree_remove_range (GcalRangeTree *self, + guint16 start, + guint16 end, + gpointer data) +{ + g_return_if_fail (self); + g_return_if_fail (end >= start); + + self->root = remove (self->root, start, end, data); +} + +/** + * gcal_range_tree_traverse: + * @self: a #GcalRangeTree + * @type: the type of traverse to perform + * @func: the function to call on each traverse iteration + * @user_data: user data for @func + * + * Traverse @self calling @func according to the @type specified. + */ +void +gcal_range_tree_traverse (GcalRangeTree *self, + GTraverseType type, + GcalRangeTraverseFunc func, + gpointer user_data) +{ + g_return_if_fail (self); + + traverse (self->root, type, func, user_data); +} + + +/** + * gcal_range_tree_get_data_at_range: + * @self: a #GcalRangeTree + * @start: inclusive start of the range + * @end: exclusive end of the range + * + * Retrieves the data of every node between @start and @end. + * + * Returns: (transfer full): a #GPtrArray. Unref with g_ptr_array_unref() + * when finished. + */ +GPtrArray* +gcal_range_tree_get_data_at_range (GcalRangeTree *self, + guint16 start, + guint16 end) +{ + GPtrArray *data; + + struct { + guint16 start, end; + GPtrArray **array; + } range = { start, end, &data }; + + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (end >= start, NULL); + + data = NULL; + + gcal_range_tree_traverse (self, G_IN_ORDER, gather_data_at_range, &range); + + return data; +} + +guint64 +gcal_range_tree_count_entries_at_range (GcalRangeTree *self, + guint16 start, + guint16 end) +{ + struct { + guint16 start, end; + guint64 counter; + } range = { start, end, 0 }; + + g_return_val_if_fail (self, 0); + g_return_val_if_fail (end >= start, 0); + + gcal_range_tree_traverse (self, G_IN_ORDER, count_entries_at_range, &range); + + return range.counter; +} diff --git a/src/views/gcal-range-tree.h b/src/views/gcal-range-tree.h new file mode 100644 index 00000000..3f8b75d5 --- /dev/null +++ b/src/views/gcal-range-tree.h @@ -0,0 +1,72 @@ +/* gcal-range-tree.h + * + * Copyright (C) 2016 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 . + */ + +#ifndef GCAL_RANGE_TREE_H +#define GCAL_RANGE_TREE_H + +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_RANGE_TREE (gcal_range_tree_get_type()) + +typedef struct _GcalRangeTree GcalRangeTree; + +typedef gboolean (*GcalRangeTraverseFunc) (guint16 start, + guint16 end, + gpointer data, + gpointer user_data); + +GType gcal_range_tree_get_type (void) G_GNUC_CONST; + +GcalRangeTree* gcal_range_tree_new (void); + +GcalRangeTree* gcal_range_tree_copy (GcalRangeTree *self); + +GcalRangeTree* gcal_range_tree_ref (GcalRangeTree *self); + +void gcal_range_tree_unref (GcalRangeTree *self); + +void gcal_range_tree_add_range (GcalRangeTree *self, + guint16 start, + guint16 end, + gpointer data); + +void gcal_range_tree_remove_range (GcalRangeTree *self, + guint16 start, + guint16 end, + gpointer data); + +void gcal_range_tree_traverse (GcalRangeTree *self, + GTraverseType type, + GcalRangeTraverseFunc func, + gpointer user_data); + +GPtrArray* gcal_range_tree_get_data_at_range (GcalRangeTree *self, + guint16 start, + guint16 end); + +guint64 gcal_range_tree_count_entries_at_range (GcalRangeTree *self, + guint16 start, + guint16 end); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GcalRangeTree, gcal_range_tree_unref) + +G_END_DECLS + +#endif /* GCAL_RANGE_TREE_H */ diff --git a/src/views/gcal-week-grid.c b/src/views/gcal-week-grid.c new file mode 100644 index 00000000..4bafb87c --- /dev/null +++ b/src/views/gcal-week-grid.c @@ -0,0 +1,1279 @@ +/* gcal-week-grid.c + * + * Copyright (C) 2016 Georges Basile Stavracas Neto + * Vamsi Krishna Gollapudi + * + * 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 . + */ + +#include "gcal-week-grid.h" +#include "gcal-week-view.h" +#include "gcal-utils.h" +#include "gcal-view.h" +#include "gcal-event-widget.h" +#include "gcal-range-tree.h" + +#include +#include +#include + +static const double dashed [] = +{ + 5.0, + 6.0 +}; + +typedef struct +{ + GtkWidget *widget; + guint16 start; + guint16 end; +} ChildData; + +struct _GcalWeekGrid +{ + GtkContainer parent; + + GtkWidget *hours_sidebar; + + GdkWindow *event_window; + + gint first_weekday; + gboolean use_24h_format : 1; + + icaltimetype *active_date; + icaltimetype *current_date; + + GcalRangeTree *events; + + gboolean children_changed; + gint redraw_timeout_id; + + /* + * These fields are "cells" rather than minutes. Each cell + * correspond to 30 minutes. + */ + gint selection_start; + gint selection_end; + gint dnd_cell; + + GcalManager *manager; +}; + +G_DEFINE_TYPE (GcalWeekGrid, gcal_week_grid, GTK_TYPE_CONTAINER); + +enum +{ + PROP_0, + PROP_ACTIVE_DATE, + LAST_PROP +}; + +enum +{ + EVENT_ACTIVATED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; +static GParamSpec* properties[LAST_PROP] = { NULL, }; + +/* ChildData methods */ +static ChildData* +child_data_new (GtkWidget *widget, + guint16 start, + guint16 end) +{ + ChildData *data; + + data = g_new (ChildData, 1); + data->widget = widget; + data->start = start; + data->end = end; + + return data; +} + +/* Event activation methods */ +static void +on_event_widget_activated (GcalEventWidget *widget, + GcalWeekGrid *self) +{ + g_signal_emit (self, signals[EVENT_ACTIVATED], 0, widget); +} + + +static inline void +setup_event_widget (GcalWeekGrid *self, + GtkWidget *widget) +{ + g_signal_connect (widget, "activate", G_CALLBACK (on_event_widget_activated), self); +} + +static inline void +destroy_event_widget (GcalWeekGrid *self, + GtkWidget *widget) +{ + g_signal_handlers_disconnect_by_func (widget, on_event_widget_activated, self); + gtk_widget_destroy (widget); +} + +/* Auxiliary methods */ +static void +get_event_range (GcalWeekGrid *self, + GcalEvent *event, + guint16 *start, + guint16 *end) +{ + GDateTime *week_start; + GTimeSpan diff; + + if (!self->active_date) + return; + + week_start = get_start_of_week (self->active_date); + + if (start) + { + GDateTime *event_start; + + event_start = g_date_time_to_local (gcal_event_get_date_start (event)); + + diff = g_date_time_difference (event_start, week_start); + *start = CLAMP (diff / G_TIME_SPAN_MINUTE, 0, MAX_MINUTES); + + g_clear_pointer (&event_start, g_date_time_unref); + } + + if (end) + { + + GDateTime *event_end; + + event_end = g_date_time_to_local (gcal_event_get_date_end (event)); + + diff = g_date_time_difference (event_end, week_start); + *end = CLAMP (diff / G_TIME_SPAN_MINUTE, 0, MAX_MINUTES); + + g_clear_pointer (&event_end, g_date_time_unref); + } + + g_clear_pointer (&week_start, g_date_time_unref); +} + +static inline gint +int16_compare (gconstpointer a, + gconstpointer b) +{ + return GPOINTER_TO_INT (a) - GPOINTER_TO_INT (b); +} + +static inline guint +get_event_index (GcalRangeTree *tree, + guint16 start, + guint16 end) +{ + g_autoptr (GPtrArray) array; + gint idx, i; + + i = idx = 0; + array = gcal_range_tree_get_data_at_range (tree, start, end); + + if (!array) + return 0; + + g_ptr_array_sort (array, int16_compare); + + for (i = 0; array && i < array->len; i++) + { + if (idx == GPOINTER_TO_INT (g_ptr_array_index (array, i))) + idx++; + else + break; + } + + return idx; +} + +static guint +count_overlaps_at_range (GcalRangeTree *self, + guint16 start, + guint16 end) +{ + guint64 i, counter; + + g_return_val_if_fail (self, 0); + g_return_val_if_fail (end >= start, 0); + + counter = 0; + + for (i = start; i < end; i++) + { + guint n_events; + + n_events = gcal_range_tree_count_entries_at_range (self, i, i + 1); + + counter = MAX (counter, n_events); + } + + return counter; +} + +static void +gcal_week_grid_finalize (GObject *object) +{ + GcalWeekGrid *self = GCAL_WEEK_GRID (object); + + g_clear_pointer (&self->events, gcal_range_tree_unref); + g_clear_pointer (&self->active_date, g_free); + g_clear_pointer (&self->current_date, g_free); + + if (self->redraw_timeout_id > 0) + { + g_source_remove (self->redraw_timeout_id); + self->redraw_timeout_id = 0; + } + + G_OBJECT_CLASS (gcal_week_grid_parent_class)->finalize (object); +} + +static void +gcal_week_grid_add (GtkContainer *container, + GtkWidget *widget) +{ + if (!gtk_widget_get_parent (widget)) + gtk_widget_set_parent (widget, GTK_WIDGET (container)); +} + +static void +gcal_week_grid_remove (GtkContainer *container, + GtkWidget *widget) +{ +} + +static void +gcal_week_grid_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + GcalWeekGrid *self; + GPtrArray *widgets_data; + guint i; + + self = GCAL_WEEK_GRID (container); + widgets_data = gcal_range_tree_get_data_at_range (self->events, 0, MAX_MINUTES); + + for (i = 0; widgets_data && i < widgets_data->len; i++) + { + ChildData *data; + + data = g_ptr_array_index (widgets_data, i); + + callback (data->widget, callback_data); + } + + g_clear_pointer (&widgets_data, g_ptr_array_unref); +} + +static void +gcal_week_grid_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalWeekGrid *self = GCAL_WEEK_GRID (object); + + switch (prop_id) + { + case PROP_ACTIVE_DATE: + g_value_set_boxed (value, self->active_date); + return; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_week_grid_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalWeekGrid *self = GCAL_WEEK_GRID (object); + + switch (prop_id) + { + case PROP_ACTIVE_DATE: + g_clear_pointer (&self->active_date, g_free); + self->active_date = g_value_dup_boxed (value); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + gtk_widget_queue_draw (GTK_WIDGET (self)); + return; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_week_grid_realize (GtkWidget *widget) +{ + GcalWeekGrid *self; + GdkWindow *parent_window; + GdkWindowAttr attributes; + gint attributes_mask; + GtkAllocation allocation; + + self = GCAL_WEEK_GRID (widget); + parent_window = gtk_widget_get_parent_window (widget); + + gtk_widget_set_realized (widget, TRUE); + gtk_widget_set_window (widget, parent_window); + g_object_ref (parent_window); + + gtk_widget_get_allocation (widget, &allocation); + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_ONLY; + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.event_mask = gtk_widget_get_events (widget); + attributes.event_mask |= (GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_BUTTON1_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_POINTER_MOTION_MASK | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK | + GDK_SCROLL_MASK | + GDK_SMOOTH_SCROLL_MASK); + attributes_mask = GDK_WA_X | GDK_WA_Y; + + self->event_window = gdk_window_new (parent_window, + &attributes, + attributes_mask); + gtk_widget_register_window (widget, self->event_window); +} + +static void +gcal_week_grid_unrealize (GtkWidget *widget) +{ + GcalWeekGrid *self; + + self = GCAL_WEEK_GRID (widget); + + if (self->event_window) + { + gtk_widget_unregister_window (widget, self->event_window); + gdk_window_destroy (self->event_window); + self->event_window = NULL; + } + + GTK_WIDGET_CLASS (gcal_week_grid_parent_class)->unrealize (widget); +} + +static void +gcal_week_grid_map (GtkWidget *widget) +{ + GcalWeekGrid *self; + + self = GCAL_WEEK_GRID (widget); + + if (self->event_window) + gdk_window_show (self->event_window); + + GTK_WIDGET_CLASS (gcal_week_grid_parent_class)->map (widget); +} + +static void +gcal_week_grid_unmap (GtkWidget *widget) +{ + GcalWeekGrid *self; + + self = GCAL_WEEK_GRID (widget); + + if (self->event_window) + gdk_window_hide (self->event_window); + + GTK_WIDGET_CLASS (gcal_week_grid_parent_class)->unmap (widget); +} + +static inline gint +get_today_column (GcalWeekGrid *self) +{ + g_autoptr(GDateTime) today, week_start; + gint days_diff; + + today = g_date_time_new_now_local (); + week_start = get_start_of_week (self->active_date); + days_diff = g_date_time_difference (today, week_start) / G_TIME_SPAN_DAY; + + /* Today is out of range */ + if (g_date_time_compare (today, week_start) < 0 || days_diff > 7) + return -1; + + return days_diff; +} + +static gboolean +gcal_week_grid_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkStyleContext *context; + GtkStateFlags state; + GcalWeekGrid *self; + GtkBorder padding; + GdkRGBA color; + + gboolean ltr; + gdouble minutes_height; + gdouble x, column_width; + gint i, width, height, today_column; + + PangoLayout *layout; + PangoFontDescription *font_desc; + + self = GCAL_WEEK_GRID (widget); + context = gtk_widget_get_style_context (widget); + state = gtk_widget_get_state_flags (widget); + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + + gtk_style_context_add_class (context, "hours"); + gtk_style_context_get_color (context, state, &color); + gtk_style_context_get_padding (context, state, &padding); + gtk_style_context_get (context, state, "font", &font_desc, NULL); + + layout = pango_cairo_create_layout (cr); + pango_layout_set_font_description (layout, font_desc); + gdk_cairo_set_source_rgba (cr, &color); + + width = gtk_widget_get_allocated_width (widget); + height = gtk_widget_get_allocated_height (widget); + column_width = width / 7.0; + minutes_height = (gdouble) height / MINUTES_PER_DAY; + + cairo_set_line_width (cr, 0.65); + + /* First, draw the selection */ + if (self->selection_start != -1 && self->selection_end != -1) + { + gint selection_height; + gint column; + gint start; + gint end; + + start = self->selection_start; + end = self->selection_end; + + /* Swap cells if needed */ + if (start > end) + { + start = start + end; + end = start - end; + start = start - end; + } + + column = start * 30 / MINUTES_PER_DAY; + selection_height = (end - start + 1) * 30 * minutes_height; + + x = column * column_width; + + gtk_style_context_save (context); + gtk_style_context_set_state (context, state | GTK_STATE_FLAG_SELECTED); + + gtk_render_background (context, + cr, + ALIGNED (x), + round ((start * 30 % MINUTES_PER_DAY) * minutes_height), + column_width, + selection_height); + + gtk_style_context_restore (context); + } + + /* Drag and Drop highlight */ + if (self->dnd_cell != -1) + { + gdouble cell_height; + gint column, row; + + cell_height = minutes_height * 30; + column = self->dnd_cell / (MINUTES_PER_DAY / 30); + row = self->dnd_cell - column * 48; + + gtk_render_background (context, + cr, + column * column_width, + row * cell_height, + column_width, + cell_height); + } + + /* Today column */ + today_column = get_today_column (GCAL_WEEK_GRID (widget)); + + if (today_column != -1) + { + g_autoptr (GDateTime) now; + GtkBorder margin; + gdouble strip_width; + guint minutes_from_midnight; + gint min_stip_height; + + now = g_date_time_new_now_local (); + minutes_from_midnight = g_date_time_get_hour (now) * 60 + g_date_time_get_minute (now); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "now-strip"); + + gtk_style_context_get (context, state, "min-height", &min_stip_height, NULL); + gtk_style_context_get_margin (context, state, &margin); + + strip_width = column_width - margin.left - margin.right; + + if (ltr) + x = today_column * column_width + margin.left; + else + x = width - (today_column * column_width + margin.right) - strip_width; + + gtk_render_background (context, + cr, + x, + round (minutes_from_midnight * ((gdouble) height / MINUTES_PER_DAY) + margin.top), + strip_width, + MAX (1, min_stip_height - margin.top - margin.bottom)); + + gtk_style_context_restore (context); + } + + /* Vertical lines */ + for (i = 0; i < 7; i++) + { + if (ltr) + x = column_width * i; + else + x = width - column_width * i; + + cairo_move_to (cr, ALIGNED (x), 0); + cairo_rel_line_to (cr, 0, height); + } + + /* Horizontal lines */ + for (i = 1; i < 24; i++) + { + cairo_move_to (cr, 0, ALIGNED ((height / 24.0) * i)); + cairo_rel_line_to (cr, width, 0); + } + + cairo_stroke (cr); + + /* Dashed lines between the vertical lines */ + cairo_set_dash (cr, dashed, 2, 0); + + for (i = 0; i < 24; i++) + { + cairo_move_to (cr, 0, ALIGNED ((height / 24.0) * i + (height / 48.0))); + cairo_rel_line_to (cr, width, 0); + } + + cairo_stroke (cr); + + pango_font_description_free (font_desc); + g_object_unref (layout); + + GTK_WIDGET_CLASS (gcal_week_grid_parent_class)->draw (widget, cr); + + /* Fire the redraw timeout if needed */ + if (self->redraw_timeout_id == 0) + { + self->redraw_timeout_id = g_timeout_add_seconds (5, + (GSourceFunc) gtk_widget_queue_draw, + self); + } + + return FALSE; +} + +static void +gcal_week_grid_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GcalWeekGrid *self = GCAL_WEEK_GRID (widget); + GcalRangeTree *overlaps; + gboolean ltr; + gdouble minutes_height; + gdouble column_width; + guint i; + guint x; + + /* No need to relayout stuff if nothing changed */ + if (allocation->height == gtk_widget_get_allocated_height (widget) && + allocation->width == gtk_widget_get_allocated_width (widget) && + !self->children_changed) + { + return; + } + + /* Allocate the widget */ + gtk_widget_set_allocation (widget, allocation); + + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + + if (gtk_widget_get_realized (widget)) + { + gdk_window_move_resize (self->event_window, + allocation->x, + allocation->y, + allocation->width, + allocation->height); + } + + + /* Preliminary calculations */ + minutes_height = (gdouble) allocation->height / MINUTES_PER_DAY; + column_width = (gdouble) allocation->width / 7.0; + + /* Temporary range tree to hold positioned events' indexes */ + overlaps = gcal_range_tree_new (); + + /* + * Iterate through weekdays; we don't have to worry about events that + * jump between days because they're already handled by GcalWeekHeader. + */ + for (i = 0; i < 7; i++) + { + GPtrArray *widgets_data; + guint16 day_start; + guint16 day_end; + guint j; + + day_start = i * MINUTES_PER_DAY; + day_end = day_start + MINUTES_PER_DAY; + widgets_data = gcal_range_tree_get_data_at_range (self->events, day_start, day_end); + + for (j = 0; widgets_data && j < widgets_data->len; j++) + { + GtkStyleContext *context; + GtkAllocation child_allocation; + GtkWidget *event_widget; + ChildData *data; + GtkBorder margin; + guint64 events_at_range; + gint natural_height; + gint widget_index; + gint offset; + gint height; + gint width; + + data = g_ptr_array_index (widgets_data, j); + event_widget = data->widget; + context = gtk_widget_get_style_context (event_widget); + + /* The total number of events available in this range */ + events_at_range = count_overlaps_at_range (self->events, data->start, data->end); + + /* The real horizontal position of this event */ + widget_index = get_event_index (overlaps, data->start, data->end); + + /* Gtk complains about that */ + gtk_widget_get_preferred_height (event_widget, NULL, &natural_height); + + /* Consider the margins of the child */ + gtk_style_context_get_margin (context, + gtk_style_context_get_state (context), + &margin); + + width = column_width / events_at_range - margin.left - margin.right; + height = MAX ((data->end - data->start) * minutes_height - margin.top - margin.bottom, natural_height); + offset = (width + margin.left + margin.right) * widget_index; + + if (ltr) + x = column_width * i + offset + allocation->x + margin.left + 1; + else + x = allocation->width - width - (column_width * i + offset + allocation->x + margin.left + 1); + + /* Setup the child position and size */ + child_allocation.x = x; + child_allocation.y = (data->start % MINUTES_PER_DAY) * minutes_height + margin.top; + child_allocation.width = width; + child_allocation.height = height; + + gtk_widget_size_allocate (event_widget, &child_allocation); + + /* + * Add the current event to the temporary overlaps tree so we have a way to + * know how many events are already positioned in the current column. + */ + gcal_range_tree_add_range (overlaps, + data->start, + data->end, + GINT_TO_POINTER (widget_index)); + } + + g_clear_pointer (&widgets_data, g_ptr_array_unref); + } + + self->children_changed = FALSE; + + g_clear_pointer (&overlaps, gcal_range_tree_unref); +} + +static void +gcal_week_grid_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + GtkStyleContext *context; + GtkStateFlags state; + GtkBorder padding; + + PangoLayout *layout; + PangoFontDescription *font_desc; + + gint hours_12_height, hours_24_height, cell_height, height; + + context = gtk_widget_get_style_context (widget); + state = gtk_style_context_get_state (context); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "hours"); + + gtk_style_context_get (context, state, + "font", &font_desc, + NULL); + gtk_style_context_get_padding (context, state, &padding); + + layout = pango_layout_new (gtk_widget_get_pango_context (widget)); + pango_layout_set_font_description (layout, font_desc); + + pango_layout_set_text (layout, _("00 AM"), -1); + pango_layout_get_pixel_size (layout, NULL, &hours_12_height); + + pango_layout_set_text (layout, _("00:00"), -1); + pango_layout_get_pixel_size (layout, NULL, &hours_24_height); + + cell_height = MAX (hours_12_height, hours_24_height) + padding.top + padding.bottom; + height = cell_height * 48; + + gtk_style_context_restore (context); + + pango_font_description_free (font_desc); + g_object_unref (layout); + + /* Report the height */ + if (minimum_height) + *minimum_height = height; + + if (natural_height) + *natural_height = height; +} + +static gboolean +gcal_week_grid_button_press (GtkWidget *widget, + GdkEventButton *event_button) +{ + GcalWeekGrid *self; + GtkAllocation alloc; + gdouble minute_height; + gint column_width; + gint column; + gint minute; + + self = GCAL_WEEK_GRID (widget); + + gtk_widget_get_allocation (widget, &alloc); + minute_height = (gdouble) alloc.height / MINUTES_PER_DAY; + column_width = floor (alloc.width / 7); + column = (gint) event_button->x / column_width; + minute = event_button->y / minute_height; + minute = minute - (minute % 30); + + self->selection_start = (column * MINUTES_PER_DAY + minute) / 30; + self->selection_end = self->selection_start; + + gtk_widget_queue_draw (widget); + + return GDK_EVENT_STOP; +} + +static gboolean +gcal_week_grid_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + GcalWeekGrid *self; + GtkAllocation alloc; + gdouble minute_height; + gint column; + gint minute; + + if (!(event->state & GDK_BUTTON_PRESS_MASK)) + return GDK_EVENT_PROPAGATE; + + self = GCAL_WEEK_GRID (widget); + + gtk_widget_get_allocation (widget, &alloc); + minute_height = (gdouble) alloc.height / MINUTES_PER_DAY; + column = self->selection_start * 30 / MINUTES_PER_DAY; + minute = event->y / minute_height; + minute = minute - (minute % 30); + + self->selection_end = (column * MINUTES_PER_DAY + minute) / 30; + + gtk_widget_queue_draw (widget); + + return GDK_EVENT_STOP; +} + +static gboolean +gcal_week_grid_button_release (GtkWidget *widget, + GdkEventButton *event) +{ + GcalWeekGrid *self; + GtkAllocation alloc; + GDateTime *week_start; + GDateTime *start, *end; + GtkWidget *weekview; + gboolean ltr; + gdouble minute_height; + gdouble x, y; + gint column; + gint minute; + gint start_cell; + gint end_cell; + gint out_x; + gint out_y; + + self = GCAL_WEEK_GRID (widget); + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + + gtk_widget_get_allocation (widget, &alloc); + minute_height = (gdouble) alloc.height / MINUTES_PER_DAY; + column = self->selection_start * 30 / MINUTES_PER_DAY; + minute = event->y / minute_height; + minute = minute - (minute % 30); + + self->selection_end = (column * MINUTES_PER_DAY + minute) / 30; + + start_cell = self->selection_start; + end_cell = self->selection_end; + + if (start_cell > end_cell) + { + start_cell = start_cell + end_cell; + end_cell = start_cell - end_cell; + start_cell = start_cell - end_cell; + } + + gtk_widget_queue_draw (widget); + + /* Fake the week view's event so we can control the X and Y values */ + weekview = gtk_widget_get_ancestor (widget, GCAL_TYPE_WEEK_VIEW); + week_start = get_start_of_week (self->active_date); + + if (ltr) + { + start = g_date_time_add_minutes (week_start, start_cell * 30); + end = g_date_time_add_minutes (week_start, (end_cell + 1) * 30); + } + else + { + start = g_date_time_add_minutes (week_start, MAX_MINUTES - column * MINUTES_PER_DAY + minute); + end = g_date_time_add_minutes (week_start, MAX_MINUTES - column * MINUTES_PER_DAY + (end_cell + 1) * 30); + } + + x = round ((column + 0.5) * (alloc.width / 7.0)); + y = (minute + 15) * minute_height; + + gtk_widget_translate_coordinates (widget, + weekview, + x, + y, + &out_x, + &out_y); + + g_signal_emit_by_name (weekview, + "create-event", + start, + end, + (gdouble) out_x, + (gdouble) out_y); + + gcal_clear_datetime (&week_start); + gcal_clear_datetime (&start); + gcal_clear_datetime (&end); + + return GDK_EVENT_STOP; +} + +static gint +get_dnd_cell (GtkWidget *widget, + gint x, + gint y) +{ + GtkAllocation alloc; + gdouble column_width, cell_height; + gint column, row; + + gtk_widget_get_allocation (widget, &alloc); + + column_width = alloc.width / 7.0; + cell_height = alloc.height / 48.0; + column = floor (x / column_width); + row = y / cell_height; + + return column * 48 + row; +} + +static gboolean +gcal_week_grid_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + GcalWeekGrid *self; + + self = GCAL_WEEK_GRID (widget); + self->dnd_cell = get_dnd_cell (widget, x, y); + + /* Setup the drag highlight */ + if (self->dnd_cell != -1) + gtk_drag_highlight (widget); + else + gtk_drag_unhighlight (widget); + + /* + * Sets the status of the drag - if it fails, sets the action to 0 and + * aborts the drag with FALSE. + */ + gdk_drag_status (context, + self->dnd_cell == -1 ? 0 : GDK_ACTION_COPY, + time); + + gtk_widget_queue_draw (widget); + + return self->dnd_cell != -1; +} + +static gboolean +gcal_week_grid_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + GcalWeekGrid *self; + g_autoptr (GDateTime) week_start; + g_autoptr (GDateTime) dnd_date; + GtkWidget *event_widget; + GcalEvent *event; + GTimeSpan timespan = 0; + gboolean ltr; + gint drop_cell; + + self = GCAL_WEEK_GRID (widget); + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + drop_cell = get_dnd_cell (widget, x, y); + event_widget = gtk_drag_get_source_widget (context); + + week_start = dnd_date = NULL; + + if (!GCAL_IS_EVENT_WIDGET (event_widget)) + return FALSE; + + /* RTL languages swap the drop cell column */ + if (!ltr) + { + gint column, row; + + column = drop_cell / (MINUTES_PER_DAY / 30); + row = drop_cell - column * 48; + + drop_cell = (6 - column) * 48 + row; + } + + event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (event_widget)); + week_start = get_start_of_week (self->active_date); + dnd_date = g_date_time_add_minutes (week_start, drop_cell * 30); + + /* + * Calculate the diff between the dropped cell and the event's start date, + * so we can update the end date accordingly. + */ + if (gcal_event_get_date_end (event)) + timespan = g_date_time_difference (gcal_event_get_date_end (event), gcal_event_get_date_start (event)); + + /* + * Set the event's start and end dates. Since the event may have a + * NULL end date, so we have to check it here + */ + gcal_event_set_all_day (event, FALSE); + gcal_event_set_date_start (event, dnd_date); + + if (gcal_event_get_date_end (event)) + { + g_autoptr (GDateTime) new_end = g_date_time_add (dnd_date, timespan); + + gcal_event_set_date_end (event, new_end); + } + + /* Commit the changes */ + gcal_manager_update_event (self->manager, event); + + /* Cancel the DnD */ + self->dnd_cell = -1; + gtk_drag_unhighlight (widget); + + gtk_drag_finish (context, TRUE, FALSE, time); + + gtk_widget_queue_draw (widget); + + return TRUE; +} + +static void +gcal_week_grid_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time) +{ + GcalWeekGrid *self = GCAL_WEEK_GRID (widget); + + /* Cancel the drag */ + self->dnd_cell = -1; + gtk_drag_unhighlight (widget); + + gtk_widget_queue_draw (widget); +} + +static void +gcal_week_grid_class_init (GcalWeekGridClass *klass) +{ + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + container_class->add = gcal_week_grid_add; + container_class->remove = gcal_week_grid_remove; + container_class->forall = gcal_week_grid_forall; + + object_class->finalize = gcal_week_grid_finalize; + object_class->get_property = gcal_week_grid_get_property; + object_class->set_property = gcal_week_grid_set_property; + + widget_class->draw = gcal_week_grid_draw; + widget_class->size_allocate = gcal_week_grid_size_allocate; + widget_class->realize = gcal_week_grid_realize; + widget_class->unrealize = gcal_week_grid_unrealize; + widget_class->map = gcal_week_grid_map; + widget_class->unmap = gcal_week_grid_unmap; + widget_class->get_preferred_height = gcal_week_grid_get_preferred_height; + widget_class->button_press_event = gcal_week_grid_button_press; + widget_class->motion_notify_event = gcal_week_grid_motion_notify_event; + widget_class->button_release_event = gcal_week_grid_button_release; + widget_class->drag_motion = gcal_week_grid_drag_motion; + widget_class->drag_leave = gcal_week_grid_drag_leave; + widget_class->drag_drop = gcal_week_grid_drag_drop; + + properties[PROP_ACTIVE_DATE] = g_param_spec_boxed ("active-date", + "Date", + "The active selected date", + ICAL_TIME_TYPE, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE); + + + signals[EVENT_ACTIVATED] = g_signal_new ("event-activated", + GCAL_TYPE_WEEK_GRID, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GCAL_TYPE_EVENT_WIDGET); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); + + gtk_widget_class_set_css_name (widget_class, "weekgrid"); +} + +static void +gcal_week_grid_init (GcalWeekGrid *self) +{ + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + + self->selection_start = -1; + self->selection_end = -1; + self->dnd_cell = -1; + + self->events = gcal_range_tree_new (); + + /* Setup the week view as a drag n' drop destination */ + gtk_drag_dest_set (GTK_WIDGET (self), + 0, + NULL, + 0, + GDK_ACTION_MOVE); +} + +/* Public API */ +void +gcal_week_grid_set_manager (GcalWeekGrid *self, + GcalManager *manager) +{ + g_return_if_fail (GCAL_IS_WEEK_GRID (self)); + + self->manager = manager; +} + +void +gcal_week_grid_set_first_weekday (GcalWeekGrid *self, + gint nr_day) +{ + g_return_if_fail (GCAL_IS_WEEK_GRID (self)); + + self->first_weekday = nr_day; +} + +void +gcal_week_grid_set_use_24h_format (GcalWeekGrid *self, + gboolean use_24h_format) +{ + g_return_if_fail (GCAL_IS_WEEK_GRID (self)); + + self->use_24h_format = use_24h_format; +} + +void +gcal_week_grid_set_current_date (GcalWeekGrid *self, + icaltimetype *current_date) +{ + g_return_if_fail (GCAL_IS_WEEK_GRID (self)); + + g_clear_pointer (&self->current_date, g_free); + self->current_date = gcal_dup_icaltime (current_date); + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +void +gcal_week_grid_add_event (GcalWeekGrid *self, + GcalEvent *event) +{ + GtkWidget *widget; + guint16 start, end; + + g_return_if_fail (GCAL_IS_WEEK_GRID (self)); + + end = 0; + start = 0; + + widget = g_object_new (GCAL_TYPE_EVENT_WIDGET, + "event", event, + "orientation", GTK_ORIENTATION_VERTICAL, + NULL); + + self->children_changed = TRUE; + + get_event_range (self, event, &start, &end); + + gcal_range_tree_add_range (self->events, + start, + end, + child_data_new (widget, start, end)); + + setup_event_widget (self, widget); + gtk_widget_show (widget); + + gtk_container_add (GTK_CONTAINER (self), widget); +} + +void +gcal_week_grid_remove_event (GcalWeekGrid *self, + const gchar *uid) +{ + GPtrArray *widgets; + guint i; + + g_return_if_fail (GCAL_IS_WEEK_GRID (self)); + + widgets = gcal_range_tree_get_data_at_range (self->events, 0, MAX_MINUTES); + + for (i = 0; widgets && i < widgets->len; i++) + { + ChildData *data; + GcalEvent *event; + guint16 event_start; + guint16 event_end; + + data = g_ptr_array_index (widgets, i); + event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (data->widget)); + + if (g_strcmp0 (gcal_event_get_uid (event), uid) != 0) + continue; + + self->children_changed = TRUE; + + get_event_range (self, event, &event_start, &event_end); + + gcal_range_tree_remove_range (self->events, data->start, data->end, data); + destroy_event_widget (self, data->widget); + gtk_widget_queue_allocate (GTK_WIDGET (self)); + g_free (data); + } + + g_clear_pointer (&widgets, g_ptr_array_unref); +} + +GList* +gcal_week_grid_get_children_by_uuid (GcalWeekGrid *self, + const gchar *uid) +{ + GPtrArray *widgets; + GList *result; + guint i; + + result = NULL; + widgets = gcal_range_tree_get_data_at_range (self->events, 0, MAX_MINUTES); + + for (i = 0; widgets && i < widgets->len; i++) + { + ChildData *data; + GcalEvent *event; + + data = g_ptr_array_index (widgets, i); + event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (data->widget)); + + if (g_strcmp0 (gcal_event_get_uid (event), uid) == 0) + result = g_list_prepend (result, data->widget); + + } + + g_clear_pointer (&widgets, g_ptr_array_unref); + + return result; +} + +void +gcal_week_grid_clear_marks (GcalWeekGrid *self) +{ + g_return_if_fail (GCAL_IS_WEEK_GRID (self)); + + self->selection_start = -1; + self->selection_end = -1; + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} diff --git a/src/views/gcal-week-grid.h b/src/views/gcal-week-grid.h new file mode 100644 index 00000000..34d48812 --- /dev/null +++ b/src/views/gcal-week-grid.h @@ -0,0 +1,59 @@ +/* gcal-week-grid.h + * + * Copyright (C) 2016 Vamsi Krishna Gollapudi + * + * 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 . + */ + +#ifndef GCAL_WEEK_GRID_H +#define GCAL_WEEK_GRID_H + +#include "gcal-manager.h" +#include "gcal-event-widget.h" +#include "gcal-subscriber-view.h" + +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_WEEK_GRID (gcal_week_grid_get_type()) + +G_DECLARE_FINAL_TYPE (GcalWeekGrid, gcal_week_grid, GCAL, WEEK_GRID, GtkContainer) + +void gcal_week_grid_set_manager (GcalWeekGrid *week_grid, + GcalManager *manager); + +void gcal_week_grid_set_first_weekday (GcalWeekGrid *week_grid, + gint nr_day); + +void gcal_week_grid_set_use_24h_format (GcalWeekGrid *week_grid, + gboolean use_24h_format); + +void gcal_week_grid_set_current_date (GcalWeekGrid *week_grid, + icaltimetype *current_date); + +void gcal_week_grid_add_event (GcalWeekGrid *self, + GcalEvent *event); + +void gcal_week_grid_remove_event (GcalWeekGrid *self, + const gchar *uid); + +GList* gcal_week_grid_get_children_by_uuid (GcalWeekGrid *self, + const gchar *uid); + +void gcal_week_grid_clear_marks (GcalWeekGrid *self); + +G_END_DECLS + +#endif /* GCAL_WEEK_GRID_H */ diff --git a/src/views/gcal-week-header.c b/src/views/gcal-week-header.c new file mode 100644 index 00000000..32d16557 --- /dev/null +++ b/src/views/gcal-week-header.c @@ -0,0 +1,1743 @@ +/* gcal-week-header.c + * + * Copyright (C) 2016 Georges Basile Stavracas Neto + * Vamsi Krishna Gollapudi + * + * 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 . + */ + +#include "config.h" + +#include "gcal-week-header.h" +#include "gcal-week-view.h" +#include "gcal-utils.h" +#include "gcal-view.h" +#include "gcal-event-widget.h" + +#include +#include +#include + +#define COLUMN_PADDING 6 + +struct _GcalWeekHeader +{ + GtkGrid parent; + + GtkWidget *grid; + GtkWidget *month_label; + GtkWidget *week_label; + GtkWidget *year_label; + GtkWidget *scrolledwindow; + GtkWidget *expand_button; + GtkWidget *expand_button_box; + GtkWidget *expand_button_image; + GtkWidget *header_labels_box; + + GcalManager *manager; + + /* + * Stores the events as they come from the week-view + * The list will later be iterated after the active date is changed + * and the events will be placed + */ + GList *events[7]; + GtkWidget *overflow_label[7]; + + gint first_weekday; + + /* + * Used for checking if the header is in collapsed state or expand state + * false is collapse state true is expand state + */ + gboolean expanded; + + gboolean use_24h_format; + gint redraw_timeout_id; + + icaltimetype *active_date; + icaltimetype *current_date; + + gint selection_start; + gint selection_end; + gint dnd_cell; + + GtkSizeGroup *sizegroup; +}; + +static void header_collapse (GcalWeekHeader *self); + +static void header_expand (GcalWeekHeader *self); + +static void on_expand_action_activated (GcalWeekHeader *self, + gpointer user_data); + +static void gcal_week_header_finalize (GObject *object); + +static void gcal_week_header_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *psec); + +static void gcal_week_header_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); + +static void gcal_week_header_size_allocate (GtkWidget *widget, + GtkAllocation *alloc); + +typedef enum +{ + UP, + DOWN +} MoveDirection; + +enum +{ + PROP_0, + PROP_ACTIVE_DATE +}; + +enum +{ + EVENT_ACTIVATED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE (GcalWeekHeader, gcal_week_header, GTK_TYPE_GRID); + +/* Event activation methods */ +static void +on_event_widget_activated (GcalEventWidget *widget, + GcalWeekHeader *self) +{ + g_signal_emit (self, signals[EVENT_ACTIVATED], 0, widget); +} + + +static inline void +setup_event_widget (GcalWeekHeader *self, + GtkWidget *widget) +{ + gtk_widget_set_margin_end (widget, 6); + g_signal_connect (widget, "activate", G_CALLBACK (on_event_widget_activated), self); +} + +static inline void +destroy_event_widget (GcalWeekHeader *self, + GtkWidget *widget) +{ + g_signal_handlers_disconnect_by_func (widget, on_event_widget_activated, self); + gtk_widget_destroy (widget); +} + +/* Auxiliary methods */ +static gboolean +on_button_pressed (GcalWeekHeader *self, + GdkEventButton *event, + GtkWidget *widget) +{ + gboolean ltr; + gdouble column_width; + gint column; + gint width; + + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + width = gtk_widget_get_allocated_width (widget); + column_width = width / 7.0; + column = ltr ? (event->x / column_width) : (7 - event->x / column_width); + + self->selection_start = column; + self->selection_end = column; + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + return GDK_EVENT_STOP; +} + +static gboolean +on_motion_notify (GcalWeekHeader *self, + GdkEventMotion *event, + GtkWidget *widget) +{ + gboolean ltr; + gdouble column_width; + gint column; + gint width; + + if (!(event->state & GDK_BUTTON_PRESS_MASK)) + return GDK_EVENT_PROPAGATE; + + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + width = gtk_widget_get_allocated_width (widget); + column_width = width / 7.0; + column = ltr ? (event->x / column_width) : (7 - event->x / column_width); + + self->selection_end = column; + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + return GDK_EVENT_STOP; +} + +static gboolean +on_button_released (GcalWeekHeader *self, + GdkEventButton *event, + GtkWidget *widget) +{ + g_autoptr (GDateTime) week_start, selection_start, selection_end; + GtkWidget *weekview; + gboolean ltr; + gdouble column_width; + gint out_x, out_y; + gint column; + gint width; + gint start; + gint end; + + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + width = gtk_widget_get_allocated_width (widget); + column_width = width / 7.0; + column = ltr ? (event->x / column_width) : (7 - event->x / column_width); + + self->selection_end = column; + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + /* Fake the week view's event so we can control the X and Y values */ + weekview = gtk_widget_get_ancestor (widget, GCAL_TYPE_WEEK_VIEW); + + start = self->selection_start; + end = self->selection_end; + + if (start > end) + { + start = start + end; + end = start - end; + start = start - end; + } + + week_start = get_start_of_week (self->active_date); + selection_start = g_date_time_add_days (week_start, start); + selection_end = end == start ? g_date_time_ref (selection_start) : g_date_time_add_days (week_start, end + 1); + + out_x = ltr ? (column_width * (column + 0.5)) : (width - column_width * (column + 0.5)); + + /* Translate X... */ + gtk_widget_translate_coordinates (widget, weekview, out_x, 0, &out_x, NULL); + + /* And Y */ + gtk_widget_translate_coordinates (GTK_WIDGET (self), + weekview, + 0, + gtk_widget_get_allocated_height (GTK_WIDGET (self)), + NULL, + &out_y); + + g_signal_emit_by_name (weekview, + "create-event", + selection_start, + selection_end, + (gdouble) out_x, + (gdouble) out_y); + + return GDK_EVENT_STOP; +} + +static GcalEvent* +get_event_by_uuid (GcalWeekHeader *self, + const gchar *uuid) +{ + gint weekday; + + for (weekday = 0; weekday < 7; weekday++) + { + GList *l; + + for (l = self->events[weekday]; l != NULL; l = l->next) + { + if (g_strcmp0 (gcal_event_get_uid (l->data), uuid) == 0) + return l->data; + } + } + + return NULL; +} + +static inline gint +get_today_column (GcalWeekHeader *self) +{ + g_autoptr(GDateTime) today, week_start; + gint days_diff; + + today = g_date_time_new_now_local (); + week_start = get_start_of_week (self->active_date); + days_diff = g_date_time_difference (today, week_start) / G_TIME_SPAN_DAY; + + /* Today is out of range */ + if (g_date_time_compare (today, week_start) < 0 || days_diff > 7) + return -1; + + return days_diff; +} + +static gint +compare_events_by_length (GcalEvent *event1, + GcalEvent *event2) +{ + /* Multiday events should come before single day events */ + if (gcal_event_is_multiday (event1) != gcal_event_is_multiday (event2)) + return gcal_event_is_multiday (event2) - gcal_event_is_multiday (event1); + + /* Compare with respect to start day */ + return gcal_event_compare (event1, event2); +} + +static gint +add_event_to_weekday (GcalWeekHeader *self, + GcalEvent *event, + gint weekday) +{ + GList *l; + + l = self->events[weekday]; + l = g_list_insert_sorted (l, event, (GCompareFunc) compare_events_by_length); + + self->events[weekday] = l; + + return g_list_index (l, event); +} + +static gboolean +is_event_visible (GcalWeekHeader *self, + gint weekday, + gint position) +{ + gboolean show_label; + + if (self->expanded) + return TRUE; + + show_label = g_list_length (self->events[weekday]) > 3; + + return show_label ? position < 2 : position < 3; +} + +/* Grid management */ +static void +update_overflow (GcalWeekHeader *self) +{ + gboolean show_expand; + gint i; + + show_expand = FALSE; + + for (i = 0; i < 7; i++) + { + GtkWidget *label; + gboolean show_label; + gint n_events; + + n_events = g_list_length (self->events[i]); + show_label = n_events > 3; + label = self->overflow_label[i]; + + show_expand = show_expand || show_label; + + if (show_label && !self->expanded) + { + gchar *text; + + text = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "Other event", "Other %d events", n_events - 2), n_events - 2); + + /* Show the button if not visible yet */ + if (!gtk_widget_get_visible (self->expand_button)) + gtk_widget_show (self->expand_button); + + /* TODO: use a button and show an overflow popover */ + if (!label) + { + label = gtk_label_new (""); + gtk_grid_attach (GTK_GRID (self->grid), + label, + i, + 3, + 1, + 1); + + self->overflow_label[i] = label; + + gtk_widget_show (label); + } + + gtk_label_set_label (GTK_LABEL (label), text); + g_free (text); + } + else + { + g_clear_pointer (&self->overflow_label[i], gtk_widget_destroy); + } + } + + gtk_widget_set_visible (self->expand_button, show_expand); +} + +static void +merge_events (GcalWeekHeader *self, + GtkWidget *event, + GtkWidget *to_be_removed) +{ + GDateTime *end_date; + gint deleted_width, current_width; + + /* Setup the new end date of the merged event */ + end_date = gcal_event_widget_get_date_end (GCAL_EVENT_WIDGET (to_be_removed)); + gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (event), end_date); + + /* Retrieve the current sizes */ + gtk_container_child_get (GTK_CONTAINER (self->grid), + to_be_removed, + "width", &deleted_width, + NULL); + + gtk_container_child_get (GTK_CONTAINER (self->grid), + event, + "width", ¤t_width, + NULL); + + destroy_event_widget (self, to_be_removed); + + /* Update the event's size */ + gtk_container_child_set (GTK_CONTAINER (self->grid), + event, + "width", current_width + deleted_width, + NULL); +} + +static void +check_mergeable_events (GcalWeekHeader *self) +{ + GList *checked_events[7] = { NULL, }; + gint weekday; + + /* We don't need to check the last column */ + for (weekday = 0; weekday < 6; weekday++) + { + GList *l; + gint index; + + index = 0; + + for (l = self->events[weekday]; l != NULL; l = l->next) + { + GcalEvent *current_event; + gint events_to_merge, i; + + current_event = l->data; + events_to_merge = 0; + + if (g_list_find (checked_events[weekday], current_event)) + { + index++; + continue; + } + else + { + checked_events[weekday] = g_list_prepend (checked_events[weekday], current_event); + } + + /* + * Horizontally check if the next cells have the same event + * than the current cell. + */ + for (i = 1; i < 7 - weekday; i++) + { + GcalEvent *next_event; + + next_event = g_list_nth_data (self->events[weekday + i], index); + + if (next_event != current_event) + break; + + events_to_merge++; + + /* Add to the list of checked days so we don't check it more times than necessary */ + checked_events[weekday + i] = g_list_prepend (checked_events[weekday + i], current_event); + } + + /* We found events to merge. Lets merge them */ + i = 0; + + for (i = 0; i < events_to_merge; i++) + { + GtkWidget *current_widget, *to_be_removed; + + current_widget = gtk_grid_get_child_at (GTK_GRID (self->grid), weekday + i, index + 1); + to_be_removed = gtk_grid_get_child_at (GTK_GRID (self->grid), weekday + i + 1, index + 1); + + /* + * We don't want to merge: + * - Overflow labels + * - The same widget + * - Widgets with different visibilities (i.e. broken by overflow + */ + if (!GCAL_IS_EVENT_WIDGET (current_widget) || + !GCAL_IS_EVENT_WIDGET (to_be_removed) || + current_widget == to_be_removed || + gtk_widget_get_visible (current_widget) != gtk_widget_get_visible (to_be_removed)) + { + continue; + } + + merge_events (self, current_widget, to_be_removed); + + gtk_widget_set_visible (current_widget, is_event_visible (self, weekday + i, index)); + } + + index++; + } + + /* We can get rid of the checked list here */ + g_list_free (checked_events[weekday]); + } +} + +static void +split_event_widget_at_column (GcalWeekHeader *self, + GtkWidget *widget, + gint column) +{ + GDateTime *week_start, *column_date, *end_column_date; + gboolean create_before; + gboolean create_after; + gint left_attach; + gint top_attach; + gint new_width; + gint old_width; + + week_start = get_start_of_week (self->active_date); + column_date = g_date_time_add_days (week_start, column); + end_column_date = g_date_time_add_days (column_date, 1); + + gtk_container_child_get (GTK_CONTAINER (self->grid), + widget, + "top_attach", &top_attach, + "left_attach", &left_attach, + "width", &old_width, + NULL); + + create_before = column > 0 && left_attach < column; + create_after = column < 6 && old_width > 1 && left_attach + old_width > column + 1; + + if (create_before) + { + GtkWidget *widget_before; + + widget_before = gcal_event_widget_clone (GCAL_EVENT_WIDGET (widget)); + gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (widget_before), column_date); + + setup_event_widget (self, widget_before); + + gtk_grid_attach (GTK_GRID (self->grid), + widget_before, + left_attach, + top_attach, + column - left_attach, + 1); + + gtk_widget_set_visible (widget_before, is_event_visible (self, left_attach, top_attach - 1)); + + new_width = old_width - column + left_attach; + old_width = new_width; + left_attach = column; + + /* Update the current event position, size and start date */ + gtk_container_child_set (GTK_CONTAINER (self->grid), + widget, + "left_attach", left_attach, + "width", new_width, + NULL); + + gcal_event_widget_set_date_start (GCAL_EVENT_WIDGET (widget), column_date); + gtk_widget_set_visible (widget, is_event_visible (self, left_attach, top_attach - 1)); + } + + /* Create a new widget after the current widget */ + if (create_after) + { + g_autoptr (GDateTime) event_end; + GtkWidget *widget_after; + + event_end = g_date_time_to_local (gcal_event_widget_get_date_end (GCAL_EVENT_WIDGET (widget))); + + widget_after = gcal_event_widget_clone (GCAL_EVENT_WIDGET (widget)); + gcal_event_widget_set_date_start (GCAL_EVENT_WIDGET (widget_after), end_column_date); + gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (widget_after), event_end); + + setup_event_widget (self, widget_after); + + gtk_grid_attach (GTK_GRID (self->grid), + widget_after, + column + 1, + top_attach, + old_width - 1, + 1); + + new_width = column - left_attach + 1; + + /* Only update the current widget's width */ + gtk_container_child_set (GTK_CONTAINER (self->grid), + widget, + "width", new_width, + NULL); + + gtk_widget_set_visible (widget_after, is_event_visible (self, column + 1, top_attach - 1)); + } + + g_clear_pointer (&end_column_date, g_date_time_unref); + g_clear_pointer (&column_date, g_date_time_unref); + g_clear_pointer (&week_start, g_date_time_unref); +} + +static void +move_events_at_column (GcalWeekHeader *self, + MoveDirection direction, + gint column, + gint start_at) +{ + GList *children, *l; + + children = gtk_container_get_children (GTK_CONTAINER (self->grid)); + + /* First, lets find the widgets at this column */ + for (l = children; l != NULL; l = l->next) + { + gint top_attach, left_attach, width; + + /* Get the widget's current position... */ + gtk_container_child_get (GTK_CONTAINER (self->grid), + l->data, + "top_attach", &top_attach, + "left_attach", &left_attach, + "width", &width, + NULL); + + if (left_attach != column || start_at > top_attach - 1 || !GCAL_IS_EVENT_WIDGET (l->data)) + continue; + + /* If this is a multiday event, break it */ + if (width > 1) + split_event_widget_at_column (self, l->data, column); + + top_attach = top_attach + (direction == DOWN ? 1 : -1); + + /* And move it to position + 1 */ + gtk_container_child_set (GTK_CONTAINER (self->grid), + l->data, + "top_attach", top_attach, + NULL); + + gtk_widget_set_visible (l->data, is_event_visible (self, left_attach, top_attach - 1)); + } + + g_clear_pointer (&children, g_list_free); +} + +static void +apply_overflow_at_weekday (GcalWeekHeader *self, + guint weekday) +{ + GtkWidget *child; + + /* + * If we don't need overflow, or we already applied the overflow, + * we don't need to do anything els. + */ + if (self->expanded || self->overflow_label[weekday] || g_list_length (self->events[weekday]) < 4) + return; + + child = gtk_grid_get_child_at (GTK_GRID (self->grid), weekday, 3); + + split_event_widget_at_column (self, child, weekday); + gtk_widget_hide (child); +} + +static void +add_event_to_grid (GcalWeekHeader *self, + GcalEvent *event, + gint start, + gint end) +{ + g_autoptr (GDateTime) week_start = NULL; + g_autoptr (GDateTime) week_end = NULL; + GtkWidget *widget; + gboolean is_visible, was_visible; + gint position; + gint i; + + /* Add at least at the first weekday */ + position = add_event_to_weekday (self, event, start); + + move_events_at_column (self, DOWN, start, position); + + /* Add the event to the grid */ + widget = gcal_event_widget_new (event); + setup_event_widget (self, widget); + + gtk_grid_attach (GTK_GRID (self->grid), + widget, + start, + position + 1, + 1, + 1); + + /* Setup event visibility */ + is_visible = is_event_visible (self, start, position); + + gtk_widget_set_visible (widget, is_visible); + + /* Eventually apply the overflow rules */ + apply_overflow_at_weekday (self, start); + + /* Single-day events are the simplest case, and the code above is enough to deal with them */ + if (!gcal_event_is_multiday (event)) + return; + + week_start = get_start_of_week (self->active_date); + week_end = get_end_of_week (self->active_date); + + gcal_event_widget_set_date_start (GCAL_EVENT_WIDGET (widget), week_start); + gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (widget), week_end); + + /* + * In addition to moving the current column's events below, multiday + * events must also move the events from ~all~ the columns it spans + * below + */ + for (i = start + 1; i <= end; i++) + { + gint new_position; + + /* Add the event to that day */ + new_position = add_event_to_weekday (self, event, i); + + was_visible = is_visible; + is_visible = is_event_visible (self, i, new_position); + + move_events_at_column (self, DOWN, i, new_position); + + /* Add the event to the grid */ + if (new_position == position && was_visible == is_visible) + { + gtk_container_child_set (GTK_CONTAINER (self->grid), + widget, + "width", i - start + 1, + NULL); + } + else + { + GDateTime *cloned_widget_start_dt; + GtkWidget *cloned_widget; + + cloned_widget_start_dt = g_date_time_add_days (week_start, i); + cloned_widget = gcal_event_widget_clone (GCAL_EVENT_WIDGET (widget)); + setup_event_widget (self, cloned_widget); + + gtk_grid_attach (GTK_GRID (self->grid), + cloned_widget, + i, + new_position + 1, + 1, + 1); + + gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (widget), cloned_widget_start_dt); + gcal_event_widget_set_date_start (GCAL_EVENT_WIDGET (cloned_widget), cloned_widget_start_dt); + + /* From now on, let's modify this widget */ + widget = cloned_widget; + + gtk_widget_set_visible (widget, is_visible); + + g_clear_pointer (&cloned_widget_start_dt, g_date_time_unref); + } + + /* Eventually apply the overflow rules */ + apply_overflow_at_weekday (self, i); + + /* Update the widget's end date */ + gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (widget), week_end); + } +} + +static void +update_unchanged_events (GcalWeekHeader *self, + icaltimetype *old_icaldt, + icaltimetype *new_icaldt) +{ + GDateTime *old_week_start, *old_week_end; + GDateTime *new_week_start, *new_week_end; + GList *events_to_update, *l; + gint weekday; + + events_to_update = NULL; + + old_week_start = get_start_of_week (old_icaldt); + old_week_end = get_end_of_week (old_icaldt); + + new_week_start = get_start_of_week (new_icaldt); + new_week_end = get_end_of_week (new_icaldt); + + for (weekday = 0; weekday < 7; weekday++) + { + GList *events; + + events = self->events[weekday]; + + for (l = events; l != NULL; l = l->next) + { + g_autoptr (GDateTime) event_start, event_end; + + event_start = g_date_time_to_local (gcal_event_get_date_start (l->data)); + event_end = g_date_time_to_local (gcal_event_get_date_end (l->data)); + + /* + * Check if the event must be updated. If we're going to the future, updatable events + * are events that started somewhere in the past, and are still present. If we're + * going to the past, updatable events are events that started + */ + if (g_date_time_compare (event_start, new_week_end) <= 0 && + g_date_time_compare (event_end, new_week_start) > 0 && + !g_list_find (events_to_update, l->data)) + { + events_to_update = g_list_append (events_to_update, l->data); + } + } + } + + for (l = events_to_update; l != NULL; l = l->next) + { + gcal_week_header_remove_event (self, gcal_event_get_uid (l->data)); + gcal_week_header_add_event (self, l->data); + } + + g_clear_pointer (&events_to_update, g_list_free); + g_clear_pointer (&new_week_start, g_date_time_unref); + g_clear_pointer (&old_week_start, g_date_time_unref); + g_clear_pointer (&new_week_end, g_date_time_unref); + g_clear_pointer (&old_week_end, g_date_time_unref); +} + +/* Header */ +static void +update_title (GcalWeekHeader *self) +{ + GDateTime *week_start, *week_end; + gchar *year_label, *month_label, *week_label; + + if(!self->active_date) + return; + + week_start = get_start_of_week (self->active_date); + week_end = g_date_time_add_days (week_start, 6); + + if (g_date_time_get_month (week_start) == g_date_time_get_month (week_end)) + { + month_label = g_strdup_printf ("%s ", gcal_get_month_name (g_date_time_get_month (week_start) - 1)); + } + else + { + month_label = g_strdup_printf("%s - %s ", + gcal_get_month_name (g_date_time_get_month (week_start) -1), + gcal_get_month_name (g_date_time_get_month (week_end) - 1)); + } + + if (g_date_time_get_year (week_start) == g_date_time_get_year (week_end)) + { + week_label = g_strdup_printf ("week %d", g_date_time_get_week_of_year (week_start)); + year_label = g_strdup_printf ("%d", g_date_time_get_year (week_start)); + } + else + { + week_label = g_strdup_printf ("week %d/%d", + g_date_time_get_week_of_year (week_start), + g_date_time_get_week_of_year (week_end)); + year_label = g_strdup_printf ("%d-%d", + g_date_time_get_year (week_start), + g_date_time_get_year (week_end)); + } + + gtk_label_set_label (GTK_LABEL (self->month_label), month_label); + gtk_label_set_label (GTK_LABEL (self->week_label), week_label); + gtk_label_set_label (GTK_LABEL (self->year_label), year_label); + + g_clear_pointer (&week_start, g_date_time_unref); + g_clear_pointer (&week_end, g_date_time_unref); + g_clear_pointer (&month_label, g_free); + g_clear_pointer (&week_label, g_free); + g_clear_pointer (&year_label, g_free); +} + +static void +header_collapse (GcalWeekHeader *self) +{ + GList *children, *l; + + children = gtk_container_get_children (GTK_CONTAINER (self->grid)); + + self->expanded = FALSE; + + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (self->scrolledwindow), + GTK_POLICY_NEVER, + GTK_POLICY_NEVER); + gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (self->scrolledwindow), -1); + gtk_image_set_from_icon_name (GTK_IMAGE (self->expand_button_image), "go-down-symbolic", 4); + + for (l = children; l != NULL; l = l->next) + { + gint top_attach, left_attach; + + gtk_container_child_get (GTK_CONTAINER (self->grid), + l->data, + "top-attach", &top_attach, + "left_attach", &left_attach, + NULL); + + gtk_widget_set_visible (l->data, is_event_visible (self, left_attach, top_attach - 1)); + + apply_overflow_at_weekday (self, left_attach); + } + + update_overflow (self); + + g_clear_pointer (&children, g_list_free); +} + +static void +header_expand (GcalWeekHeader *self) +{ + GtkWidget *week_view; + GList *children, *l; + + children = gtk_container_get_children (GTK_CONTAINER (self->grid)); + week_view = gtk_widget_get_ancestor (GTK_WIDGET (self), GCAL_TYPE_WEEK_VIEW); + + self->expanded = TRUE; + + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (self->scrolledwindow), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + + /* TODO: animate this transition */ + gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (self->scrolledwindow), + gtk_widget_get_allocated_height (week_view) / 2); + + gtk_image_set_from_icon_name (GTK_IMAGE (self->expand_button_image), "go-up-symbolic", 4); + + for (l = children; l != NULL; l = l->next) + { + /* Remove any remaining labels */ + if (GTK_IS_LABEL (l->data)) + { + gint left_attach; + + gtk_container_child_get (GTK_CONTAINER (self->grid), + l->data, + "left_attach", &left_attach, + NULL); + + self->overflow_label[left_attach] = NULL; + gtk_widget_destroy (l->data); + } + else + { + gtk_widget_show (l->data); + } + } + + /* Merge events that were broken because of the overflow label */ + check_mergeable_events (self); + + g_clear_pointer (&children, g_list_free); +} + +static void +on_expand_action_activated (GcalWeekHeader *self, + gpointer user_data) +{ + if (self->expanded) + header_collapse (self); + else + header_expand (self); +} + +/* Drawing area content and size */ +static gdouble +get_weekday_names_height (GtkWidget *widget) +{ + PangoFontDescription *font_desc; + GtkStyleContext* context; + GtkStateFlags state_flags; + PangoLayout *layout; + GtkBorder padding; + gint final_height; + gint font_height; + + context = gtk_widget_get_style_context (widget); + state_flags = gtk_style_context_get_state (context); + + layout = gtk_widget_create_pango_layout (widget, "A"); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "week-dates"); + gtk_style_context_get (context, state_flags, "font", &font_desc, NULL); + gtk_style_context_get_padding (context, state_flags, &padding); + + pango_layout_set_font_description (layout, font_desc); + pango_layout_get_pixel_size (layout, NULL, &font_height); + + pango_font_description_free (font_desc); + gtk_style_context_restore (context); + + final_height = padding.top + font_height + padding.bottom; + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "week-names"); + gtk_style_context_get (context, state_flags, "font", &font_desc, NULL); + gtk_style_context_get_padding (context, state_flags, &padding); + + pango_layout_set_font_description (layout, font_desc); + pango_layout_get_pixel_size (layout, NULL, &font_height); + + pango_font_description_free (font_desc); + gtk_style_context_restore (context); + + final_height += padding.top + font_height + padding.bottom; + + return final_height; +} + +static void +gcal_week_header_finalize (GObject *object) +{ + GcalWeekHeader *self = GCAL_WEEK_HEADER (object); + gint i; + + g_clear_pointer (&self->active_date, g_free); + g_clear_pointer (&self->current_date, g_free); + + if (self->redraw_timeout_id > 0) + { + g_source_remove (self->redraw_timeout_id); + self->redraw_timeout_id = 0; + } + + for (i = 0; i < 7; i++) + g_list_free (self->events[i]); +} + +static void +gcal_week_header_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalWeekHeader *self = GCAL_WEEK_HEADER (object); + + switch (prop_id) + { + case PROP_ACTIVE_DATE: + g_value_set_boxed (value, self->active_date); + return; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_week_header_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalWeekHeader *self = GCAL_WEEK_HEADER (object); + icaltimetype *old_date, *new_date; + + switch (prop_id) + { + case PROP_ACTIVE_DATE: + old_date = self->active_date; + new_date = g_value_dup_boxed (value); + + /* + * If the active date changed, but we're still in the same week, + * there's no need to recalculate visible events. + */ + if (old_date && new_date && + old_date->year == new_date->year && + icaltime_week_number (*old_date) == icaltime_week_number (*new_date)) + { + g_free (new_date); + break; + } + + self->active_date = new_date; + + update_title (self); + gtk_widget_queue_draw (GTK_WIDGET (self)); + + if (old_date) + update_unchanged_events (self, old_date, self->active_date); + + g_clear_pointer (&old_date, g_free); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_week_header_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + GcalWeekHeader *self = GCAL_WEEK_HEADER (widget); + gint min_header_height; + + min_header_height = get_weekday_names_height (widget); + + gtk_widget_set_margin_top (self->scrolledwindow, min_header_height); + + GTK_WIDGET_CLASS (gcal_week_header_parent_class)->size_allocate (widget, alloc); +} + +static gboolean +gcal_week_header_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkStyleContext *context; + GcalWeekHeader *self; + GtkStateFlags state; + GdkRGBA color; + GtkAllocation alloc; + GtkBorder padding; + + GDateTime *week_start, *week_end; + + PangoLayout *layout; + PangoFontDescription *bold_font; + + gdouble cell_width; + gint i, font_height, current_cell, today_column; + gint start_x, start_y; + + gboolean ltr; + + cairo_save (cr); + + /* Fonts and colour selection */ + self = GCAL_WEEK_HEADER (widget); + context = gtk_widget_get_style_context (widget); + state = gtk_style_context_get_state (context); + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + + start_x = ltr ? gtk_widget_get_allocated_width (self->expand_button_box) : 0; + start_y = gtk_widget_get_allocated_height (self->header_labels_box); + + gtk_style_context_get_padding (context, state, &padding); + gtk_widget_get_allocation (widget, &alloc); + + if (!ltr) + alloc.width -= gtk_widget_get_allocated_width (self->expand_button_box); + + gtk_style_context_get_color (context, state, &color); + gdk_cairo_set_source_rgba (cr, &color); + + layout = pango_cairo_create_layout (cr); + gtk_style_context_get (context, state, "font", &bold_font, NULL); + pango_font_description_set_weight (bold_font, PANGO_WEIGHT_MEDIUM); + pango_layout_set_font_description (layout, bold_font); + + week_start = get_start_of_week (self->active_date); + week_end = g_date_time_add_days (week_start, 6); + current_cell = icaltime_day_of_week (*(self->active_date)) - 1; + current_cell = (7 + current_cell - self->first_weekday) % 7; + today_column = get_today_column (self); + + cell_width = (alloc.width - start_x) / 7.0; + + /* Drag and Drop highlight */ + if (self->dnd_cell != -1) + { + gtk_render_background (context, + cr, + start_x + self->dnd_cell * cell_width, + start_y, + cell_width, + alloc.height - start_y + 6); + } + + /* Draw the selection background */ + if (self->selection_start != -1 && self->selection_end != -1) + { + gint selection_width; + gint selection_x; + gint start, end; + + start = self->selection_start; + end = self->selection_end; + + if (start > end) + { + start = start + end; + end = start - end; + start = start - end; + } + + gtk_style_context_save (context); + gtk_style_context_set_state (context, state | GTK_STATE_FLAG_SELECTED); + + selection_width = (end - start + 1) * cell_width; + selection_x = ltr ? (start * cell_width) : (alloc.width - (start * cell_width + selection_width)); + + gtk_render_background (context, + cr, + ALIGNED (start_x + selection_x) + 0.33, + start_y - 6, + ALIGNED (selection_width + 1), + alloc.height - start_y + 6); + + gtk_render_frame (context, + cr, + ALIGNED (start_x + selection_x) + 0.33, + start_y - 6, + ALIGNED (selection_width + 1), + alloc.height - start_y + 6); + + gtk_style_context_restore (context); + } + + pango_layout_get_pixel_size (layout, NULL, &font_height); + + for (i = 0; i < 7; i++) + { + gchar *weekday_date, *weekday_abv; + gdouble x; + gint font_width; + gint n_day; + + n_day = g_date_time_get_day_of_month (week_start) + i; + + if (n_day > g_date_get_days_in_month (g_date_time_get_month (week_start), g_date_time_get_year (week_start))) + n_day = n_day - g_date_get_days_in_month (g_date_time_get_month (week_start), g_date_time_get_year (week_start)); + + /* Draws the date of days in the week */ + weekday_date = g_strdup_printf ("%d", n_day); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "week-dates"); + gtk_style_context_get (context, state, "font", &bold_font, NULL); + + if (i == today_column) + gtk_style_context_add_class (context, "today"); + + pango_layout_set_font_description (layout, bold_font); + pango_layout_set_text (layout, weekday_date, -1); + + pango_layout_get_pixel_size (layout, &font_width, NULL); + + if (ltr) + x = padding.left + cell_width * i + COLUMN_PADDING + start_x; + else + x = alloc.width - (cell_width * i + font_width + COLUMN_PADDING + start_x); + + gtk_render_layout (context, + cr, + x, + font_height + padding.bottom + start_y, + layout); + + gtk_style_context_restore (context); + + /* Draws the days name */ + weekday_abv = g_strdup_printf ("%s", g_utf8_strup (gcal_get_weekday ((i + self->first_weekday) % 7), -1)); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "week-names"); + gtk_style_context_get (context, state, "font", &bold_font, NULL); + + if (i == today_column) + gtk_style_context_add_class (context, "today"); + + pango_layout_set_font_description (layout, bold_font); + pango_layout_set_text (layout, weekday_abv, -1); + + pango_layout_get_pixel_size (layout, &font_width, NULL); + + if (ltr) + x = padding.left + cell_width * i + COLUMN_PADDING + start_x; + else + x = alloc.width - (cell_width * i + font_width + COLUMN_PADDING + start_x); + + gtk_render_layout (context, + cr, + x, + start_y, + layout); + + gtk_style_context_restore (context); + + /* Draws the lines after each day of the week */ + gtk_style_context_save (context); + gtk_style_context_add_class (context, "lines"); + + gtk_style_context_get_color (context, state, &color); + gdk_cairo_set_source_rgba (cr, &color); + + cairo_set_line_width (cr, 0.25); + + cairo_move_to (cr, + ALIGNED (ltr ? (cell_width * i + start_x) : (alloc.width - (cell_width * i + start_x))), + font_height + padding.bottom + start_y); + + cairo_rel_line_to (cr, + 0.0, + gtk_widget_get_allocated_height (widget) - font_height - start_y + padding.bottom); + cairo_stroke (cr); + + gtk_style_context_restore (context); + + g_free (weekday_date); + g_free (weekday_abv); + } + + cairo_restore (cr); + + pango_font_description_free (bold_font); + g_object_unref (layout); + + GTK_WIDGET_CLASS (gcal_week_header_parent_class)->draw (widget, cr); + + /* Fire the redraw timeout if needed */ + if (self->redraw_timeout_id == 0) + { + self->redraw_timeout_id = g_timeout_add_seconds (5, + (GSourceFunc) gtk_widget_queue_draw, + self); + } + + g_clear_pointer (&week_start, g_date_time_unref); + g_clear_pointer (&week_end, g_date_time_unref); + + return FALSE; +} + + +static gint +get_dnd_cell (GtkWidget *widget, + gint x, + gint y) +{ + gdouble column_width; + + column_width = gtk_widget_get_allocated_width (widget) / 7.0; + + return x / column_width; +} + +static gboolean +gcal_week_header_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + GcalWeekHeader *self; + + self = GCAL_WEEK_HEADER (widget); + self->dnd_cell = get_dnd_cell (widget, x, y); + + /* Setup the drag highlight */ + if (self->dnd_cell != -1) + gtk_drag_highlight (widget); + else + gtk_drag_unhighlight (widget); + + /* + * Sets the status of the drag - if it fails, sets the action to 0 and + * aborts the drag with FALSE. + */ + gdk_drag_status (context, + self->dnd_cell == -1 ? 0 : GDK_ACTION_COPY, + time); + + gtk_widget_queue_draw (widget); + + return self->dnd_cell != -1; +} + +static gboolean +gcal_week_header_drag_drop (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + GcalWeekHeader *self; + g_autoptr (GDateTime) week_start; + g_autoptr (GDateTime) dnd_date; + GtkWidget *event_widget; + GcalEvent *event; + gboolean ltr; + gint drop_cell; + + self = GCAL_WEEK_HEADER (widget); + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + drop_cell = get_dnd_cell (widget, x, y); + event_widget = gtk_drag_get_source_widget (context); + + week_start = dnd_date = NULL; + + if (!GCAL_IS_EVENT_WIDGET (event_widget)) + return FALSE; + + /* RTL languages swap the drop cell column */ + if (!ltr) + { + drop_cell = 6 - drop_cell; + } + + event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (event_widget)); + week_start = get_start_of_week (self->active_date); + dnd_date = g_date_time_add_days (week_start, drop_cell); + + /* + * Set the event's start and end dates. Since the event may have a + * NULL end date, so we have to check it here + */ + gcal_event_set_all_day (event, TRUE); + gcal_event_set_date_start (event, dnd_date); + + if (gcal_event_get_date_end (event)) + { + g_autoptr (GDateTime) new_end; + + new_end = g_date_time_add_days (dnd_date, 1); + + gcal_event_set_date_end (event, new_end); + } + + /* Commit the changes */ + gcal_manager_update_event (self->manager, event); + + /* Cancel the DnD */ + self->dnd_cell = -1; + gtk_drag_unhighlight (widget); + + gtk_drag_finish (context, TRUE, FALSE, time); + + gtk_widget_queue_draw (widget); + + return TRUE; +} + +static void +gcal_week_header_drag_leave (GtkWidget *widget, + GdkDragContext *context, + guint time) +{ + GcalWeekHeader *self = GCAL_WEEK_HEADER (widget); + + /* Cancel the drag */ + self->dnd_cell = -1; + gtk_drag_unhighlight (widget); + + gtk_widget_queue_draw (widget); +} + +static void +gcal_week_header_class_init (GcalWeekHeaderClass *kclass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (kclass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (kclass); + + object_class->finalize = gcal_week_header_finalize; + object_class->get_property = gcal_week_header_get_property; + object_class->set_property = gcal_week_header_set_property; + + widget_class->draw = gcal_week_header_draw; + widget_class->size_allocate = gcal_week_header_size_allocate; + widget_class->drag_motion = gcal_week_header_drag_motion; + widget_class->drag_leave = gcal_week_header_drag_leave; + widget_class->drag_drop = gcal_week_header_drag_drop; + + g_object_class_install_property (object_class, + PROP_ACTIVE_DATE, + g_param_spec_boxed ("active-date", + "Date", + "The active selected date", + ICAL_TIME_TYPE, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE)); + + signals[EVENT_ACTIVATED] = g_signal_new ("event-activated", + GCAL_TYPE_WEEK_HEADER, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GCAL_TYPE_EVENT_WIDGET); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/week-header.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, expand_button); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, expand_button_box); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, expand_button_image); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, grid); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, header_labels_box); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, month_label); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, scrolledwindow); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, sizegroup); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, week_label); + gtk_widget_class_bind_template_child (widget_class, GcalWeekHeader, year_label); + + gtk_widget_class_bind_template_callback (widget_class, on_button_pressed); + gtk_widget_class_bind_template_callback (widget_class, on_button_released); + gtk_widget_class_bind_template_callback (widget_class, on_expand_action_activated); + gtk_widget_class_bind_template_callback (widget_class, on_motion_notify); + + gtk_widget_class_set_css_name (widget_class, "calendar-view"); +} + +static void +gcal_week_header_init (GcalWeekHeader *self) +{ + self->expanded = FALSE; + self->selection_start = -1; + self->selection_end = -1; + self->dnd_cell = -1; + + gtk_widget_init_template (GTK_WIDGET (self)); + + /* Setup the week header as a drag n' drop destination */ + gtk_drag_dest_set (GTK_WIDGET (self), + 0, + NULL, + 0, + GDK_ACTION_MOVE); +} + +/* Public API */ +void +gcal_week_header_set_manager (GcalWeekHeader *self, + GcalManager *manager) +{ + g_return_if_fail (GCAL_IS_WEEK_HEADER (self)); + + self->manager = manager; +} + +void +gcal_week_header_set_first_weekday (GcalWeekHeader *self, + gint nr_day) +{ + g_return_if_fail (GCAL_IS_WEEK_HEADER (self)); + + self->first_weekday = nr_day; +} + +void +gcal_week_header_set_use_24h_format (GcalWeekHeader *self, + gboolean use_24h_format) +{ + g_return_if_fail (GCAL_IS_WEEK_HEADER (self)); + + self->use_24h_format = use_24h_format; +} + +void +gcal_week_header_add_event (GcalWeekHeader *self, + GcalEvent *event) +{ + g_autoptr (GDateTime) start_date = NULL; + g_autoptr (GDateTime) end_date = NULL; + g_autoptr (GDateTime) week_start = NULL; + g_autoptr (GDateTime) week_end = NULL; + gboolean all_day; + gint start, end; + + g_return_if_fail (GCAL_IS_WEEK_HEADER (self)); + + all_day = gcal_event_get_all_day (event); + week_start = get_start_of_week (self->active_date); + week_end = get_end_of_week (self->active_date); + + /* Retrieve the real start and end dates */ + if (all_day) + { + start_date = g_date_time_ref (gcal_event_get_date_start (event)); + end_date = g_date_time_ref (gcal_event_get_date_end (event)); + } + else + { + start_date = g_date_time_to_local (gcal_event_get_date_start (event)); + end_date = g_date_time_to_local (gcal_event_get_date_end (event)); + } + + /* Start position */ + if (datetime_compare_date (start_date, week_start) >= 0) + start = (g_date_time_get_day_of_week (start_date) - get_first_weekday ()) % 7; + else + start = 0; + + /* End position */ + if (g_date_time_compare (end_date, week_end) <= 0) + end = (g_date_time_get_day_of_week (end_date) - get_first_weekday () - all_day) % 7; + else + end = 6; + + /* Add the event widget to the grid */ + add_event_to_grid (self, event, start, end); + + /* Check if we eventually can merge events */ + check_mergeable_events (self); + + /* And also update the overflow labels */ + update_overflow (self); +} + +void +gcal_week_header_remove_event (GcalWeekHeader *self, + const gchar *uuid) +{ + GcalEvent *removed_event; + GList *children, *l; + gint weekday; + + g_return_if_fail (GCAL_IS_WEEK_HEADER (self)); + + removed_event = get_event_by_uuid (self, uuid); + + if (!removed_event) + return; + + children = gtk_container_get_children (GTK_CONTAINER (self->grid)); + + for (l = children; l != NULL; l = l->next) + { + GcalEventWidget *child_widget; + GcalEvent *event; + + if (!GCAL_IS_EVENT_WIDGET (l->data)) + continue; + + child_widget = GCAL_EVENT_WIDGET (l->data); + event = gcal_event_widget_get_event (child_widget); + + if (g_strcmp0 (uuid, gcal_event_get_uid (event)) == 0) + destroy_event_widget (self, l->data); + } + + /* Remove from the weekday's GList */ + for (weekday = 0; weekday < 7; weekday++) + { + gint event_position; + + l = self->events[weekday]; + event_position = g_list_index (self->events[weekday], removed_event); + + if (event_position == -1) + continue; + + /* Move remaining events up */ + move_events_at_column (self, UP, weekday, event_position); + + /* Remove from the current weekday */ + l = g_list_remove (l, removed_event); + self->events[weekday] = l; + + } + + /* Check if we eventually can merge events */ + check_mergeable_events (self); + + /* And also update the overflow labels */ + update_overflow (self); + + g_clear_pointer (&children, g_list_free); +} + +GList* +gcal_week_header_get_children_by_uuid (GcalWeekHeader *self, + const gchar *uuid) +{ + GList *children, *l, *result; + + result = NULL; + children = gtk_container_get_children (GTK_CONTAINER (self->grid)); + + for (l = children; l != NULL; l = l->next) + { + GcalEventWidget *child_widget; + GcalEvent *event; + + if (!GCAL_IS_EVENT_WIDGET (l->data)) + continue; + + child_widget = GCAL_EVENT_WIDGET (l->data); + event = gcal_event_widget_get_event (child_widget); + + if (g_strcmp0 (uuid, gcal_event_get_uid (event)) == 0) + result = g_list_prepend (result, l->data); + } + + g_list_free (children); + + return result; +} + +void +gcal_week_header_set_current_date (GcalWeekHeader *self, + icaltimetype *current_date) +{ + g_return_if_fail (GCAL_IS_WEEK_HEADER (self)); + + g_clear_pointer (&self->current_date, g_free); + self->current_date = gcal_dup_icaltime (current_date); + + update_title (self); + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +GtkSizeGroup* +gcal_week_header_get_sidebar_size_group (GcalWeekHeader *self) +{ + g_return_val_if_fail (GCAL_IS_WEEK_HEADER (self), NULL); + + return self->sizegroup; +} + +void +gcal_week_header_clear_marks (GcalWeekHeader *self) +{ + g_return_if_fail (GCAL_IS_WEEK_HEADER (self)); + + self->selection_start = -1; + self->selection_end = -1; + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} diff --git a/src/views/gcal-week-header.h b/src/views/gcal-week-header.h new file mode 100644 index 00000000..f916c4e5 --- /dev/null +++ b/src/views/gcal-week-header.h @@ -0,0 +1,61 @@ +/* gcal-week-header.h + * + * Copyright (C) 2016 Vamsi Krishna Gollapudi + * + * 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 . + */ + +#ifndef GCAL_WEEK_HEADER_H +#define GCAL_WEEK_HEADER_H + +#include "gcal-manager.h" +#include "gcal-event-widget.h" +#include "gcal-subscriber-view.h" + +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_WEEK_HEADER (gcal_week_header_get_type()) + +G_DECLARE_FINAL_TYPE (GcalWeekHeader, gcal_week_header, GCAL, WEEK_HEADER, GtkGrid) + +void gcal_week_header_set_manager (GcalWeekHeader *self, + GcalManager *manager); + +void gcal_week_header_set_first_weekday (GcalWeekHeader *self, + gint nr_day); + +void gcal_week_header_set_use_24h_format (GcalWeekHeader *self, + gboolean use_24h_format); + +void gcal_week_header_set_current_date (GcalWeekHeader *self, + icaltimetype *current_date); + +void gcal_week_header_add_event (GcalWeekHeader *self, + GcalEvent *event); + +void gcal_week_header_remove_event (GcalWeekHeader *self, + const gchar *uuid); + +GList* gcal_week_header_get_children_by_uuid (GcalWeekHeader *self, + const gchar *uuid); + +GtkSizeGroup* gcal_week_header_get_sidebar_size_group (GcalWeekHeader *self); + +void gcal_week_header_clear_marks (GcalWeekHeader *self); + +G_END_DECLS + +#endif /* GCAL_WEEK_HEADER_H */ diff --git a/src/views/gcal-week-view.c b/src/views/gcal-week-view.c new file mode 100644 index 00000000..f5e84e10 --- /dev/null +++ b/src/views/gcal-week-view.c @@ -0,0 +1,684 @@ +/* -*- mode: c; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * gcal-week-view.c + * + * Copyright (C) 2015 - Erick Pérez Castellanos + * + * 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 2 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 . + */ + +#include "gcal-week-view.h" +#include "gcal-utils.h" +#include "gcal-view.h" +#include "gcal-event-widget.h" +#include "gcal-week-header.h" +#include "gcal-week-grid.h" + +#include + +#include + +#define ALL_DAY_CELLS_HEIGHT 40 + +static const double dashed [] = +{ + 5.0, + 6.0 +}; + +enum +{ + PROP_0, + PROP_DATE +}; + +struct _GcalWeekView +{ + GtkBox parent; + + GtkWidget *header; + GtkWidget *hours_bar; + GtkWidget *scrolled_window; + GtkWidget *week_grid; + + /* + * first day of the week according to user locale, being + * 0 for Sunday, 1 for Monday and so on + */ + gint first_weekday; + + /* + * clock format from GNOME desktop settings + */ + gboolean use_24h_format; + + /* property */ + icaltimetype *date; + icaltimetype *current_date; + GcalManager *manager; /* weak referenced */ + + guint scroll_grid_timeout_id; + + gint clicked_cell; +}; + +static void gcal_week_view_component_added (ECalDataModelSubscriber *subscriber, + ECalClient *client, + ECalComponent *comp); + +static void gcal_week_view_component_modified (ECalDataModelSubscriber *subscriber, + ECalClient *client, + ECalComponent *comp); + +static void gcal_week_view_component_removed (ECalDataModelSubscriber *subscriber, + ECalClient *client, + const gchar *uid, + const gchar *rid); + +static void gcal_week_view_freeze (ECalDataModelSubscriber *subscriber); + +static void gcal_week_view_thaw (ECalDataModelSubscriber *subscriber); + +static gboolean gcal_week_view_draw_hours (GcalWeekView *self, + cairo_t *cr, + GtkWidget *widget); + +static void gcal_view_interface_init (GcalViewInterface *iface); + +static void gcal_data_model_subscriber_interface_init (ECalDataModelSubscriberInterface *iface); + +static void gcal_week_view_finalize (GObject *object); + +static void gcal_week_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); + +static void gcal_week_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); + +static icaltimetype* gcal_week_view_get_final_date (GcalView *view); + +enum +{ + EVENT_ACTIVATED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE_WITH_CODE (GcalWeekView, gcal_week_view, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GCAL_TYPE_VIEW, gcal_view_interface_init) + G_IMPLEMENT_INTERFACE (E_TYPE_CAL_DATA_MODEL_SUBSCRIBER, + gcal_data_model_subscriber_interface_init)); + +/* Callbacks */ +static void +on_event_activated (GcalWeekView *self, + GtkWidget *widget) +{ + g_signal_emit (self, signals[EVENT_ACTIVATED], 0, widget); +} + +/* Auxiliary methods */ +static gboolean +update_grid_scroll_position (GcalWeekView *self) +{ + g_autoptr(GDateTime) week_start = NULL; + g_autoptr(GDateTime) week_end = NULL; + g_autoptr(GDateTime) now = NULL; + GtkAdjustment *vadjustment; + gdouble minutes, real_value; + gdouble max, page, page_increment, value; + + /* While the scrolled window is not mapped, we keep waiting */ + if (!gtk_widget_get_mapped (self->scrolled_window)) + return G_SOURCE_CONTINUE; + + now = g_date_time_new_now_local (); + week_start = get_start_of_week (self->date); + week_end = get_end_of_week (self->date); + + /* Don't animate when not today */ + if (datetime_compare_date (now, week_start) < 0 || datetime_compare_date (now, week_end) >= 0) + goto out; + + vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window)); + minutes = g_date_time_get_hour (now) * 60 + g_date_time_get_minute (now); + page = gtk_adjustment_get_page_size (vadjustment); + max = gtk_adjustment_get_upper (vadjustment); + + real_value = max / MINUTES_PER_DAY * minutes - (page / 2.0); + page_increment = gtk_adjustment_get_page_increment (vadjustment); + value = gtk_adjustment_get_value (vadjustment); + + gtk_adjustment_set_page_increment (vadjustment, real_value - value); + + g_signal_emit_by_name (self->scrolled_window, + "scroll-child", + GTK_SCROLL_PAGE_FORWARD, + FALSE); + + gtk_adjustment_set_page_increment (vadjustment, page_increment); + +out: + self->scroll_grid_timeout_id = 0; + return G_SOURCE_REMOVE; +} + +static void +schedule_position_scroll (GcalWeekView *self) +{ + /* Nothing is scheduled, we should do it now */ + if (self->scroll_grid_timeout_id > 0) + g_source_remove (self->scroll_grid_timeout_id); + + self->scroll_grid_timeout_id = g_timeout_add (200, + (GSourceFunc) update_grid_scroll_position, + self); +} + +/** + * gcal_week_view_get_initial_date: + * + * Since: 0.1 + * Return value: the first day of the week + * Returns: (transfer full): Release with g_free() + **/ +static icaltimetype* +gcal_week_view_get_initial_date (GcalView *view) +{ + GcalWeekView *self; + icaltimetype *new_date; + + g_return_val_if_fail (GCAL_IS_WEEK_VIEW (view), NULL); + self = GCAL_WEEK_VIEW(view); + new_date = g_new0 (icaltimetype, 1); + *new_date = icaltime_from_day_of_year (icaltime_start_doy_week (*(self->date), + self->first_weekday + 1), + self->date->year); + new_date->is_date = 0; + new_date->hour = 0; + new_date->minute = 0; + new_date->second = 0; + *new_date = icaltime_set_timezone (new_date, self->date->zone); + return new_date; +} + +/** + * gcal_week_view_get_final_date: + * + * Since: 0.1 + * Return value: the last day of the week + * Returns: (transfer full): Release with g_free() + **/ +static icaltimetype* +gcal_week_view_get_final_date (GcalView *view) +{ + GcalWeekView *self; + icaltimetype *new_date; + + g_return_val_if_fail (GCAL_IS_WEEK_VIEW (view), NULL); + self = GCAL_WEEK_VIEW(view); + new_date = g_new0 (icaltimetype, 1); + *new_date = icaltime_from_day_of_year (icaltime_start_doy_week (*(self->date), + self->first_weekday + 1) + 6, + self->date->year); + new_date->is_date = 0; + new_date->hour = 23; + new_date->minute = 59; + new_date->second = 0; + *new_date = icaltime_set_timezone (new_date, self->date->zone); + return new_date; +} + +static GList* +gcal_week_view_get_children_by_uuid (GcalView *view, + const gchar *uuid) +{ + GcalWeekView *self; + GList *grid, *header; + + self = GCAL_WEEK_VIEW (view); + grid = gcal_week_grid_get_children_by_uuid (GCAL_WEEK_GRID (self->week_grid), uuid); + header = gcal_week_header_get_children_by_uuid (GCAL_WEEK_HEADER (self->header), uuid); + + return g_list_concat (grid, header); +} + +static void +gcal_week_view_clear_marks (GcalView *view) +{ + GcalWeekView *self = GCAL_WEEK_VIEW (view); + + gcal_week_header_clear_marks (GCAL_WEEK_HEADER (self->header)); + gcal_week_grid_clear_marks (GCAL_WEEK_GRID (self->week_grid)); +} + +static void +update_hours_sidebar_size (GcalWeekView *self) +{ + GtkStyleContext *context; + GtkStateFlags state; + GtkSizeGroup *sidebar_sizegroup; + GtkWidget *widget; + GtkBorder padding; + + PangoLayout *layout; + PangoFontDescription *font_desc; + + gint hours_12_width, hours_24_width, sidebar_width; + gint hours_12_height, hours_24_height, cell_height; + + widget = GTK_WIDGET (self); + context = gtk_widget_get_style_context (widget); + state = gtk_style_context_get_state (context); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "hours"); + + gtk_style_context_get (context, state, + "font", &font_desc, + NULL); + gtk_style_context_get_padding (context, state, &padding); + + layout = pango_layout_new (gtk_widget_get_pango_context (widget)); + pango_layout_set_font_description (layout, font_desc); + + pango_layout_set_text (layout, _("00 AM"), -1); + pango_layout_get_pixel_size (layout, &hours_12_width, &hours_12_height); + + pango_layout_set_text (layout, _("00:00"), -1); + pango_layout_get_pixel_size (layout, &hours_24_width, &hours_24_height); + + sidebar_width = MAX (hours_12_width, hours_24_width) + padding.left + padding.right; + cell_height = MAX (hours_12_height, hours_24_height) + padding.top + padding.bottom + 1; + + gtk_style_context_restore (context); + + /* Update the size requests */ + gtk_widget_set_size_request (self->hours_bar, + sidebar_width, + 48 * cell_height); + + /* Sync with the week header sidebar */ + sidebar_sizegroup = gcal_week_header_get_sidebar_size_group (GCAL_WEEK_HEADER (self->header)); + gtk_size_group_add_widget (sidebar_sizegroup, self->hours_bar); + + pango_font_description_free (font_desc); + g_object_unref (layout); +} + +static void +gcal_week_view_component_added (ECalDataModelSubscriber *subscriber, + ECalClient *client, + ECalComponent *comp) +{ + GcalWeekView *self = GCAL_WEEK_VIEW (subscriber); + + GcalEvent *event; + + event = gcal_event_new (e_client_get_source (E_CLIENT (client)), comp, NULL); + + if (gcal_event_is_multiday (event) || gcal_event_get_all_day (event)) + gcal_week_header_add_event (GCAL_WEEK_HEADER (self->header), event); + else + gcal_week_grid_add_event (GCAL_WEEK_GRID (self->week_grid), event); +} + +static void +gcal_week_view_component_modified (ECalDataModelSubscriber *subscriber, + ECalClient *client, + ECalComponent *comp) +{ + GcalWeekView *self = GCAL_WEEK_VIEW (subscriber); + GcalWeekHeader *header; + gchar *uuid; + + header = GCAL_WEEK_HEADER (self->header); + + uuid = get_uuid_from_component (e_client_get_source (E_CLIENT (client)), comp); + + gcal_week_header_remove_event (header, uuid); + gcal_week_grid_remove_event (GCAL_WEEK_GRID (self->week_grid), uuid); + + gcal_week_view_component_added (subscriber, client, comp); + + g_free (uuid); +} + +static void +gcal_week_view_component_removed (ECalDataModelSubscriber *subscriber, + ECalClient *client, + const gchar *uid, + const gchar *rid) +{ + GcalWeekView *self = GCAL_WEEK_VIEW (subscriber); + ESource *source; + gchar *uuid; + + source = e_client_get_source (E_CLIENT (client)); + + if (rid != NULL) + uuid = g_strdup_printf ("%s:%s:%s", e_source_get_uid (source), uid, rid); + else + uuid = g_strdup_printf ("%s:%s", e_source_get_uid (source), uid); + + gcal_week_header_remove_event (GCAL_WEEK_HEADER (self->header), uuid); + gcal_week_grid_remove_event (GCAL_WEEK_GRID (self->week_grid), uuid); + + g_free (uuid); +} + +static void +gcal_week_view_freeze (ECalDataModelSubscriber *subscriber) +{ +} + +static void +gcal_week_view_thaw (ECalDataModelSubscriber *subscriber) +{ +} + +static gboolean +gcal_week_view_draw_hours (GcalWeekView *self, + cairo_t *cr, + GtkWidget *widget) +{ + GtkStyleContext *context; + GtkStateFlags state; + GtkBorder padding; + GdkRGBA color; + + gboolean ltr; + gint i, width, height; + gint font_width; + + PangoLayout *layout; + PangoFontDescription *font_desc; + + context = gtk_widget_get_style_context (widget); + state = gtk_widget_get_state_flags (widget); + ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + + gtk_style_context_add_class (context, "hours"); + gtk_style_context_get_color (context, state, &color); + gtk_style_context_get_padding (context, state, &padding); + gtk_style_context_get (context, state, "font", &font_desc, NULL); + + layout = pango_cairo_create_layout (cr); + pango_layout_set_font_description (layout, font_desc); + gdk_cairo_set_source_rgba (cr, &color); + + /* Gets the size of the widget */ + width = gtk_widget_get_allocated_width (widget); + height = gtk_widget_get_allocated_height (widget); + + /* Draws the hours in the sidebar */ + for (i = 0; i < 24; i++) + { + gchar *hours; + + if (self->use_24h_format) + { + hours = g_strdup_printf ("%02d:00", i); + } + else + { + hours = g_strdup_printf ("%d %s", + i % 12 == 0 ? 12 : i % 12, + i > 12 ? _("PM") : _("AM")); + } + + pango_layout_set_text (layout, hours, -1); + pango_layout_get_pixel_size (layout, &font_width, NULL); + + cairo_move_to (cr, + ltr ? padding.left : width - font_width - padding.right, + (height / 24) * i + padding.top); + + pango_cairo_show_layout (cr, layout); + + g_free (hours); + } + + cairo_set_line_width (cr, 0.65); + + if (!ltr) + { + cairo_move_to (cr, 0.5, 0); + cairo_rel_line_to (cr, 0, height); + } + + /* Draws the horizontal complete lines */ + for (i = 1; i < 24; i++) + { + cairo_move_to (cr, 0, (height / 24) * i + 0.4); + cairo_rel_line_to (cr, width, 0); + } + + cairo_stroke (cr); + + cairo_set_dash (cr, dashed, 2, 0); + + /* Draws the horizontal dashed lines */ + for (i = 0; i < 24; i++) + { + cairo_move_to (cr, 0, (height / 24) * i + (height / 48) + 0.4); + cairo_rel_line_to (cr, width, 0); + } + + cairo_stroke (cr); + + pango_font_description_free (font_desc); + g_object_unref (layout); + + return FALSE; +} + +static void +gcal_week_view_class_init (GcalWeekViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gcal_week_view_finalize; + object_class->set_property = gcal_week_view_set_property; + object_class->get_property = gcal_week_view_get_property; + + g_object_class_override_property (object_class, PROP_DATE, "active-date"); + + signals[EVENT_ACTIVATED] = g_signal_new ("event-activated", + GCAL_TYPE_WEEK_VIEW, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GCAL_TYPE_EVENT_WIDGET); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/week-view.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalWeekView, header); + gtk_widget_class_bind_template_child (widget_class, GcalWeekView, hours_bar); + gtk_widget_class_bind_template_child (widget_class, GcalWeekView, scrolled_window); + gtk_widget_class_bind_template_child (widget_class, GcalWeekView, week_grid); + + gtk_widget_class_bind_template_callback (widget_class, gcal_week_view_draw_hours); + gtk_widget_class_bind_template_callback (widget_class, on_event_activated); + + gtk_widget_class_set_css_name (widget_class, "calendar-view"); +} + +static void +gcal_week_view_init (GcalWeekView *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + update_hours_sidebar_size (self); +} + +static void +gcal_view_interface_init (GcalViewInterface *iface) +{ + iface->get_initial_date = gcal_week_view_get_initial_date; + iface->get_final_date = gcal_week_view_get_final_date; + iface->get_children_by_uuid = gcal_week_view_get_children_by_uuid; + iface->clear_marks = gcal_week_view_clear_marks; +} + +static void +gcal_data_model_subscriber_interface_init (ECalDataModelSubscriberInterface *iface) +{ + iface->component_added = gcal_week_view_component_added; + iface->component_modified = gcal_week_view_component_modified; + iface->component_removed = gcal_week_view_component_removed; + iface->freeze = gcal_week_view_freeze; + iface->thaw = gcal_week_view_thaw; +} + +static void +gcal_week_view_finalize (GObject *object) +{ + GcalWeekView *self; + + self = GCAL_WEEK_VIEW (object); + + g_clear_pointer (&self->date, g_free); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (gcal_week_view_parent_class)->finalize (object); +} + +static void +gcal_week_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalWeekView *self; + + self = GCAL_WEEK_VIEW (object); + + switch (property_id) + { + case PROP_DATE: + g_clear_pointer (&self->date, g_free); + self->date = g_value_dup_boxed (value); + + schedule_position_scroll (self); + + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gcal_week_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GcalWeekView *self; + + g_return_if_fail (GCAL_IS_WEEK_VIEW (object)); + self = GCAL_WEEK_VIEW (object); + + switch (property_id) + { + case PROP_DATE: + g_value_set_boxed (value, self->date); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +/* Public API */ +/** + * gcal_week_view_new: + * + * Create a week-view widget + * + * Returns: (transfer full): + **/ +GtkWidget* +gcal_week_view_new (void) +{ + return g_object_new (GCAL_TYPE_WEEK_VIEW, NULL); +} + +void +gcal_week_view_set_manager (GcalWeekView *self, + GcalManager *manager) +{ + g_return_if_fail (GCAL_IS_WEEK_VIEW (self)); + + self->manager = manager; + + gcal_week_grid_set_manager (GCAL_WEEK_GRID (self->week_grid), manager); + gcal_week_header_set_manager (GCAL_WEEK_HEADER (self->header), manager); +} + +/** + * gcal_week_view_set_first_weekday: + * @view: + * @day_nr: + * + * Set the first day of the week according to the locale, being + * 0 for Sunday, 1 for Monday and so on. + **/ +void +gcal_week_view_set_first_weekday (GcalWeekView *self, + gint day_nr) +{ + g_return_if_fail (GCAL_IS_WEEK_VIEW (self)); + + self->first_weekday = day_nr; +} + +/** + * gcal_week_view_set_use_24h_format: + * @view: + * @use_24h: + * + * Whether the view will show time using 24h or 12h format + **/ +void +gcal_week_view_set_use_24h_format (GcalWeekView *self, + gboolean use_24h) +{ + g_return_if_fail (GCAL_IS_WEEK_VIEW (self)); + + self->use_24h_format = use_24h; +} + +void +gcal_week_view_set_current_date (GcalWeekView *self, + icaltimetype *current_date) +{ + g_return_if_fail (GCAL_IS_WEEK_VIEW (self)); + + g_clear_pointer (&self->current_date, g_free); + self->current_date = current_date; + + gcal_week_header_set_current_date (GCAL_WEEK_HEADER (self->header), current_date); + gcal_week_grid_set_current_date (GCAL_WEEK_GRID (self->week_grid), current_date); +} diff --git a/src/gcal-week-view.h b/src/views/gcal-week-view.h similarity index 57% rename from src/gcal-week-view.h rename to src/views/gcal-week-view.h index 120abb4a..0e9cc571 100644 --- a/src/gcal-week-view.h +++ b/src/views/gcal-week-view.h @@ -21,32 +21,14 @@ #define __GCAL_WEEK_VIEW_H__ #include "gcal-manager.h" - -#include "gcal-subscriber-view.h" +#include "gcal-view.h" G_BEGIN_DECLS -#define GCAL_TYPE_WEEK_VIEW (gcal_week_view_get_type ()) -#define GCAL_WEEK_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GCAL_TYPE_WEEK_VIEW, GcalWeekView)) -#define GCAL_WEEK_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GCAL_TYPE_WEEK_VIEW, GcalWeekViewClass)) -#define GCAL_IS_WEEK_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GCAL_TYPE_WEEK_VIEW)) -#define GCAL_IS_WEEK_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GCAL_TYPE_WEEK_VIEW)) -#define GCAL_WEEK_VIEW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GCAL_TYPE_WEEK_VIEW, GcalWeekViewClass)) +#define GCAL_TYPE_WEEK_VIEW (gcal_week_view_get_type ()) -typedef struct _GcalWeekView GcalWeekView; -typedef struct _GcalWeekViewClass GcalWeekViewClass; +G_DECLARE_FINAL_TYPE (GcalWeekView, gcal_week_view, GCAL, WEEK_VIEW, GtkBox) -struct _GcalWeekView -{ - GcalSubscriberView parent; -}; - -struct _GcalWeekViewClass -{ - GcalSubscriberViewClass parent_class; -}; - -GType gcal_week_view_get_type (void); GtkWidget* gcal_week_view_new (void); void gcal_week_view_set_manager (GcalWeekView *view, GcalManager *manager); @@ -54,6 +36,8 @@ void gcal_week_view_set_first_weekday (GcalWeekView *view, gint day_nr); void gcal_week_view_set_use_24h_format (GcalWeekView *view, gboolean use_24h); +void gcal_week_view_set_current_date (GcalWeekView *week_header, + icaltimetype *current_date); G_END_DECLS