From f9f9ccbb8ce689881508366fb06d68d7102acfb8 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Fri, 4 Aug 2023 13:28:37 -0300 Subject: [PATCH] month-view-row: Rewrite layout algorithm Calculate event blocks upfront, in a way that is agnostic to pixel sizes, and keep them alive as long the event is around. Calculate which event blocks are visible at layout time, and only set the child visibility of the event widgets, instead of truly showing or hiding them. This makes size_allocate not queue relayouts. There are many opportunities for optimizations that we can eventually land, but this seems to work decently so far. RTL support is still missing. --- src/gui/views/gcal-dazzling-month-view.c | 2 - src/gui/views/gcal-month-view-row.c | 945 ++++++----------------- 2 files changed, 240 insertions(+), 707 deletions(-) diff --git a/src/gui/views/gcal-dazzling-month-view.c b/src/gui/views/gcal-dazzling-month-view.c index 7ea38d99..8b1fac96 100644 --- a/src/gui/views/gcal-dazzling-month-view.c +++ b/src/gui/views/gcal-dazzling-month-view.c @@ -597,7 +597,6 @@ gcal_dazzling_month_view_add_event (GcalTimelineSubscriber *subscriber, overlap = gcal_range_calculate_overlap (row_range, gcal_event_get_range (event), &position); if (overlap != GCAL_RANGE_NO_OVERLAP) { - g_message ("Adding event %s to row %u", gcal_event_get_uid (event), i); gcal_month_view_row_add_event (row, event); } else @@ -651,7 +650,6 @@ gcal_dazzling_month_view_remove_event (GcalTimelineSubscriber *subscriber, overlap = gcal_range_calculate_overlap (row_range, gcal_event_get_range (event), &position); if (overlap != GCAL_RANGE_NO_OVERLAP) { - g_message ("Remove event %s to row %u", gcal_event_get_uid (event), i); gcal_month_view_row_remove_event (row, event); } else diff --git a/src/gui/views/gcal-month-view-row.c b/src/gui/views/gcal-month-view-row.c index a3242152..5ff13f4f 100644 --- a/src/gui/views/gcal-month-view-row.c +++ b/src/gui/views/gcal-month-view-row.c @@ -27,13 +27,17 @@ #include "gcal-event-widget.h" #include "gcal-month-cell.h" #include "gcal-month-view-row.h" +#include "gcal-range-tree.h" typedef struct { GtkWidget *event_widget; - gboolean visible; guint8 length; guint8 cell; + + /* These are updated during allocation */ + gboolean visible; + gint height; } GcalEventBlock; struct _GcalMonthViewRow @@ -44,43 +48,19 @@ struct _GcalMonthViewRow GcalRange *range; - /* - * Hash to keep children widgets (all of them, parent widgets and its parts if there's any), - * uuid as key and a list of all the instances of the event as value. Here, the first widget - * on the list is the master, and the rest are the parts. Note: the master is a part itself. - */ - GHashTable *children; - - /* - * Hash containing single-cell events, day of the month, on month-view, month of the year on - * year-view as key anda list of the events that belongs to this cell - */ - GHashTable *single_cell_children; - - /* - * A sorted list containing multiday events. This one contains only parents events, to find out - * its parts @children will be used. - */ - GList *multi_cell_children; - - /* Hash containing cells that who has overflow per list of hidden widgets */ - GHashTable *overflow_cells; - - gboolean needs_reallocation; - gint last_width; - gint last_height; + GListStore *events; + GHashTable *layout_blocks; GcalContext *context; }; +static gint compare_events_cb (gconstpointer a, + gconstpointer b, + gpointer user_data); + static void on_event_widget_activated_cb (GcalEventWidget *widget, GcalMonthViewRow *self); - -static void on_event_widget_visibility_changed_cb (GtkWidget *event_widget, - GParamSpec *pspec, - GcalMonthViewRow *self); - G_DEFINE_FINAL_TYPE (GcalMonthViewRow, gcal_month_view_row, GTK_TYPE_WIDGET) enum @@ -100,6 +80,21 @@ static guint signals[N_SIGNALS] = { 0, }; static GParamSpec *properties [N_PROPS]; +/* + * Auxiliary methods + */ + +static void +layout_block_free (gpointer data) +{ + GcalEventBlock *block = data; + + if (!block) + return; + + g_clear_pointer (&block->event_widget, gtk_widget_unparent); + g_free (block); +} static void setup_child_widget (GcalMonthViewRow *self, @@ -108,179 +103,6 @@ setup_child_widget (GcalMonthViewRow *self, gtk_widget_insert_after (widget, GTK_WIDGET (self), self->day_cells[6]); g_signal_connect_object (widget, "activate", G_CALLBACK (on_event_widget_activated_cb), self, 0); - g_signal_connect_object (widget, "notify::visible", G_CALLBACK (on_event_widget_visibility_changed_cb), self, 0); -} - -static inline guint -get_child_weekday (GcalMonthViewRow *self, - GcalEventWidget *child) -{ - GcalEvent *event; - GDateTime *dt; - gint weekday; - - event = gcal_event_widget_get_event (child); - dt = gcal_event_get_date_start (event); - - /* Don't adjust the date when the event is all day */ - if (gcal_event_get_all_day (event)) - { - weekday = g_date_time_get_day_of_week (dt) % 7; - } - else - { - dt = g_date_time_to_local (dt); - weekday = g_date_time_get_day_of_week (dt) % 7; - - g_clear_pointer (&dt, g_date_time_unref); - } - - return weekday; -} - -static void -add_event_widget (GcalMonthViewRow *self, - GtkWidget *widget) -{ - GcalEvent *event; - const gchar *uuid; - GList *l = NULL; - - g_return_if_fail (GCAL_IS_EVENT_WIDGET (widget)); - g_return_if_fail (gtk_widget_get_parent (widget) == NULL); - - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - uuid = gcal_event_get_uid (event); - - /* inserting in all children hash */ - if (g_hash_table_contains (self->children, uuid)) - { - g_warning ("Event with uuid: %s already added", uuid); - g_object_unref (widget); - return; - } - - l = g_list_append (l, widget); - g_hash_table_insert (self->children, g_strdup (uuid), l); - - if (gcal_event_is_multiday (event)) - { - self->multi_cell_children = g_list_insert_sorted (self->multi_cell_children, - widget, - (GCompareFunc) gcal_event_widget_sort_events); - } - else - { - guint weekday; - - weekday = get_child_weekday (self, GCAL_EVENT_WIDGET (widget)); - - l = g_hash_table_lookup (self->single_cell_children, GINT_TO_POINTER (weekday)); - l = g_list_insert_sorted (l, widget, (GCompareFunc) gcal_event_widget_compare_by_start_date); - - if (g_list_length (l) != 1) - g_hash_table_steal (self->single_cell_children, GINT_TO_POINTER (weekday)); - - g_hash_table_insert (self->single_cell_children, GINT_TO_POINTER (weekday), l); - } - - setup_child_widget (self, widget); -} - -static void -remove_event_widget (GcalMonthViewRow *self, - GtkWidget *widget) -{ - GtkWidget *master_widget; - GcalEvent *event; - GList *l, *aux; - const gchar *uuid; - - g_assert (gtk_widget_get_parent (widget) == GTK_WIDGET (self)); - - if (!GCAL_IS_EVENT_WIDGET (widget)) - return; - - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - uuid = gcal_event_get_uid (event); - - l = g_hash_table_lookup (self->children, uuid); - - if (l) - { - gtk_widget_unparent (widget); - - master_widget = (GtkWidget*) l->data; - if (widget == master_widget) - { - if (g_list_find (self->multi_cell_children, widget) != NULL) - { - self->multi_cell_children = g_list_remove (self->multi_cell_children, widget); - - aux = g_list_next (l); - if (aux != NULL) - { - l->next = NULL; - aux->prev = NULL; - g_list_foreach (aux, (GFunc) gtk_widget_unparent, NULL); - g_list_free (aux); - } - } - else - { - GHashTableIter iter; - gpointer key, value; - - /* - * When an event is changed, we can't rely on it's old date - * to remove the corresponding widget. Because of that, we have - * to iter through all the widgets to see which one matches - */ - g_hash_table_iter_init (&iter, self->single_cell_children); - - while (g_hash_table_iter_next (&iter, &key, &value)) - { - gboolean should_break; - - should_break = FALSE; - - for (aux = value; aux != NULL; aux = aux->next) - { - if (aux->data == widget) - { - aux = g_list_remove (g_list_copy (value), widget); - - /* - * If we removed the event and there's no event left for - * the day, remove the key from the table. If there are - * events for that day, replace the list. - */ - if (!aux) - g_hash_table_remove (self->single_cell_children, key); - else - g_hash_table_replace (self->single_cell_children, key, aux); - - should_break = TRUE; - - break; - } - } - - if (should_break) - break; - } - } - } - - l = g_list_remove (g_list_copy (l), widget); - - if (!l) - g_hash_table_remove (self->children, uuid); - else - g_hash_table_replace (self->children, g_strdup (uuid), l); - } - - gtk_widget_queue_resize (GTK_WIDGET (self)); } static void @@ -296,16 +118,14 @@ calculate_event_cells (GcalMonthViewRow *self, /* Start date */ if (out_first_cell) { + g_autoptr (GDateTime) range_start = NULL; g_autoptr (GDateTime) start_date = NULL; - gint first_cell = 0; + range_start = gcal_range_get_start (self->range); start_date = gcal_event_get_date_start (event); start_date = all_day ? g_date_time_ref (start_date) : g_date_time_to_local (start_date); - if (gcal_range_contains_datetime (self->range, start_date)) - first_cell = g_date_time_get_day_of_week (start_date) % 7; - - *out_first_cell = first_cell; + *out_first_cell = MAX (gcal_date_time_compare_date (start_date, range_start), 0); } /* @@ -314,455 +134,195 @@ calculate_event_cells (GcalMonthViewRow *self, */ if (out_last_cell) { + g_autoptr (GDateTime) range_start = NULL; g_autoptr (GDateTime) end_date = NULL; gint last_cell = 6; + range_start = gcal_range_get_start (self->range); end_date = gcal_event_get_date_end (event); end_date = all_day ? g_date_time_ref (end_date) : g_date_time_to_local (end_date); - if (gcal_range_contains_datetime (self->range, end_date)) - { - last_cell = g_date_time_get_day_of_week (end_date) % 7; + last_cell = gcal_date_time_compare_date (end_date, range_start); - /* If the event is all day, we have to subtract 1 to find the the real date */ - if (all_day) - last_cell--; + if (all_day) + last_cell--; + + *out_last_cell = MIN (last_cell, 6); + } +} + +static void +prepare_layout_blocks (GcalMonthViewRow *self, + guint overflows[7]) +{ + GPtrArray *blocks_per_day[7]; + gboolean cell_will_overflow[7] = { FALSE, }; + guint available_height_without_overflow[7] = { 0, }; + guint available_height_with_overflow[7] = { 0, }; + guint weekday_heights[7] = { 0, }; + guint n_events; + + n_events = g_list_model_get_n_items (G_LIST_MODEL (self->events)); + + for (guint i = 0; i < 7; i++) + { + gint overflow_height; + gint content_space; + + content_space = gcal_month_cell_get_content_space (GCAL_MONTH_CELL (self->day_cells[i])); + overflow_height = gcal_month_cell_get_overflow_height (GCAL_MONTH_CELL (self->day_cells[i])); + + available_height_without_overflow[i] = content_space; + available_height_with_overflow[i] = content_space - overflow_height; + blocks_per_day[i] = g_ptr_array_sized_new (n_events); + } + + /* Build a matrix with weekdays x blocks */ + for (guint i = 0; i < n_events; i++) + { + g_autoptr (GcalEvent) event = NULL; + GPtrArray *blocks; + + event = g_list_model_get_item (G_LIST_MODEL (self->events), i); + + blocks = g_hash_table_lookup (self->layout_blocks, event); + g_assert (blocks != NULL); + + for (guint block_index = 0; block_index < blocks->len; block_index++) + { + GcalEventBlock *block; + + block = g_ptr_array_index (blocks, block_index); + block->visible = TRUE; + + gtk_widget_measure (GTK_WIDGET (block->event_widget), + GTK_ORIENTATION_VERTICAL, + -1, + &block->height, + NULL, NULL, NULL); + + for (guint j = 0; j < block->length; j++) + { + guint cell = block->cell + j; + + g_ptr_array_add (blocks_per_day[cell], block); + weekday_heights[cell] += block->height; + cell_will_overflow[cell] |= weekday_heights[cell] > available_height_without_overflow[cell]; + } } - - *out_last_cell = last_cell; - } -} - -static void -cleanup_overflow_information (GcalMonthViewRow *self) -{ - g_autoptr (GList) widget_lists = NULL; - GList *l = NULL; - - /* Remove every widget' parts, but the master widget */ - widget_lists = g_hash_table_get_values (self->children); - - for (l = widget_lists; l; l = l->next) - { - g_autoptr (GList) widgets = g_list_copy (l->data); - GList *l2; - - for (l2 = widgets->next; l2; l2 = l2->next) - remove_event_widget (self, l2->data); } - /* Clean overflow information */ - g_hash_table_remove_all (self->overflow_cells); -} - -static void -count_events_per_day (GcalMonthViewRow *self, - gint events_per_day[7]) -{ - GHashTableIter iter; - gpointer key; - GList *children; - - /* Multiday events */ - for (GList *l = self->multi_cell_children; l; l = l->next) + /* Figure out which blocks will be visible */ + for (guint cell = 0; cell < 7; cell++) { - GcalEvent *event; - gint first_cell; - gint last_cell; - gint i; - - if (!gtk_widget_get_visible (l->data)) - continue; - - event = gcal_event_widget_get_event (l->data); - - calculate_event_cells (self, event, &first_cell, &last_cell); - - for (i = first_cell; i <= last_cell; i++) - events_per_day[i]++; - } - - /* Single day events */ - g_hash_table_iter_init (&iter, self->single_cell_children); - while (g_hash_table_iter_next (&iter, &key, (gpointer*) &children)) - { - gint cell; - - cell = GPOINTER_TO_INT (key); - - for (GList *l = children; l; l = l->next) + for (guint block_index = 0; + block_index < blocks_per_day[cell]->len; + block_index++) { - if (!gtk_widget_get_visible (l->data)) + GcalEventBlock *block = g_ptr_array_index (blocks_per_day[cell], block_index); + + if (block->cell != cell) continue; - events_per_day[cell]++; - } - } -} - -static GPtrArray* -calculate_multiday_event_blocks (GcalMonthViewRow *self, - GtkWidget *event_widget, - gdouble *vertical_cell_space, - gdouble *size_left, - gint *events_at_day, - gint *allocated_events_at_day) -{ - GcalEventBlock *block; - GcalEvent *event; - GPtrArray *blocks; - gboolean was_visible; - gdouble old_y; - gdouble y; - gint first_cell; - gint last_cell; - gint i; - - GCAL_ENTRY; - - old_y = -1.0; - was_visible = FALSE; - - /* Get the event cells */ - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (event_widget)); - calculate_event_cells (self, event, &first_cell, &last_cell); - - blocks = g_ptr_array_new_full (last_cell - first_cell + 1, g_free); - block = NULL; - - GCAL_TRACE_MSG ("Positioning '%s' (multiday) from %d to %d", gcal_event_get_summary (event), first_cell, last_cell); - - for (i = first_cell; i <= last_cell; i++) - { - gboolean visible_at_range; - gboolean will_overflow; - gdouble real_height; - gint remaining_events; - gint minimum_height; - - real_height = size_left[i]; - - /* Calculate the minimum event height */ - gtk_widget_measure (GTK_WIDGET (event_widget), - GTK_ORIENTATION_VERTICAL, - -1, - &minimum_height, - NULL, NULL, NULL); - - /* Count this event at this cell */ - remaining_events = events_at_day[i] - allocated_events_at_day[i]; - will_overflow = remaining_events * minimum_height > real_height; - - if (will_overflow) - real_height -= gcal_month_cell_get_overflow_height (GCAL_MONTH_CELL (self->day_cells[i])); - - visible_at_range = real_height >= minimum_height; - - if (visible_at_range) - allocated_events_at_day[i]++; - - y = vertical_cell_space[i] - size_left[i]; - - /* At the first iteration, make was_visible and visible_at_range equal */ - if (i == first_cell) - was_visible = visible_at_range; - - if (!block || y != old_y || was_visible != visible_at_range) - { - GCAL_TRACE_MSG ("Breaking event at cell %d", i); - - /* Only create a new event widget after the first one is consumed */ - if (block) + /* Will this block be visible in all cells it spans? */ + for (guint block_cell = cell; block_cell < cell + block->length; block_cell++) { - GcalEvent *event; - GList *aux; + gint available_height; - GCAL_TRACE_MSG ("Cloning event widget"); + if (cell_will_overflow[block_cell]) + available_height = available_height_with_overflow[block_cell]; + else + available_height = available_height_without_overflow[block_cell]; - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (event_widget)); - - event_widget = gcal_event_widget_clone (GCAL_EVENT_WIDGET (event_widget)); - setup_child_widget (self, event_widget); - - aux = g_hash_table_lookup (self->children, gcal_event_get_uid (event)); - aux = g_list_append (aux, event_widget); + block->visible &= available_height > block->height; } - old_y = y; - - /* Add a new block */ - block = g_new (GcalEventBlock, 1); - block->event_widget = event_widget; - block->visible = visible_at_range; - block->length = 1; - block->cell = i; - - g_ptr_array_add (blocks, block); + for (guint block_cell = cell; block_cell < cell + block->length; block_cell++) + { + if (block->visible) + { + available_height_with_overflow[block_cell] -= block->height; + available_height_without_overflow[block_cell] -= block->height; + } + else + { + overflows[block_cell]++; + } + } } - else - { - block->length++; - } - - was_visible = visible_at_range; } - GCAL_RETURN (blocks); + for (guint i = 0; i < 7; i++) + g_clear_pointer (&blocks_per_day[i], g_ptr_array_unref); } static void -allocate_multiday_events (GcalMonthViewRow *self, - gdouble *vertical_cell_space, - gdouble *size_left, - gint *events_at_day, - gint *allocated_events_at_day, - gint baseline) +recalculate_layout_blocks (GcalMonthViewRow *self) { - GtkAllocation event_allocation; - GList *l; - gboolean is_rtl; + guint events_at_weekday[7] = { 0, }; + guint n_events; - is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL; + n_events = g_list_model_get_n_items (G_LIST_MODEL (self->events)); - for (l = self->multi_cell_children; l; l = g_list_next (l)) + g_hash_table_remove_all (self->layout_blocks); + + for (guint i = 0; i < n_events; i++) { g_autoptr (GPtrArray) blocks = NULL; - GtkWidget *child_widget; - GcalEvent *event; - gint block_idx; + g_autoptr (GcalEvent) event = NULL; + GcalEventBlock *block = NULL; + gint first_cell; + gint last_cell; - child_widget = (GtkWidget*) l->data; + event = g_list_model_get_item (G_LIST_MODEL (self->events), i); - if (!gtk_widget_should_layout (child_widget)) - continue; + calculate_event_cells (self, event, &first_cell, &last_cell); + g_assert (last_cell >= first_cell); - event = gcal_event_widget_get_event (l->data); + blocks = g_ptr_array_new_full (last_cell - first_cell + 1, layout_block_free); - if (gcal_range_calculate_overlap (self->range, gcal_event_get_range (event), NULL) == GCAL_RANGE_NO_OVERLAP) - continue; - - /* - * Multiday events can "break" following these rules: - * - * - Break when line changed - * - Break when the vertical position in the cells changed - * - Break when the previous part's visibility is different than the current - */ - blocks = calculate_multiday_event_blocks (self, - child_widget, - vertical_cell_space, - size_left, - events_at_day, - allocated_events_at_day); - - for (block_idx = 0; block_idx < blocks->len; block_idx++) + for (gint cell = first_cell; cell <= last_cell; cell++) { - g_autoptr (GDateTime) range_start = NULL; - g_autoptr (GDateTime) dt_start = NULL; - g_autoptr (GDateTime) dt_end = NULL; - GcalEventBlock *block; - GtkAllocation first_cell_allocation; - GtkAllocation last_cell_allocation; - GtkWidget *last_month_cell; - GtkWidget *month_cell; - GList *aux; - gdouble pos_x; - gdouble pos_y; - gdouble width; - gint last_block_cell; - gint minimum_height; - gint length; - gint cell; - gint day; - gint j; + events_at_weekday[cell]++; - block = g_ptr_array_index (blocks, block_idx); - length = block->length; - cell = block->cell; - last_block_cell = cell + length - 1; - day = cell; - - child_widget = block->event_widget; - - /* No space left, add to the overflow and continue */ - if (!block->visible) + if (!block) { - gint idx; + GtkWidget *event_widget; - gtk_widget_set_child_visible (child_widget, FALSE); + event_widget = gcal_event_widget_new (self->context, event); + // TODO: gcal_event_widget_set_read_only (GCAL_EVENT_WIDGET (event_widget), gcal_calendar_is_read_only (calendar)); + if (!gcal_event_get_all_day (event) && !gcal_event_is_multiday (event)) + gcal_event_widget_set_timestamp_policy (GCAL_EVENT_WIDGET (event_widget), GCAL_TIMESTAMP_POLICY_START); + setup_child_widget (self, event_widget); - for (idx = cell; idx < cell + length; idx++) - { - aux = g_hash_table_lookup (self->overflow_cells, GINT_TO_POINTER (idx)); - aux = g_list_append (aux, child_widget); + block = g_new (GcalEventBlock, 1); + block->event_widget = g_steal_pointer (&event_widget); + block->length = 1; + block->cell = cell; - if (g_list_length (aux) == 1) - g_hash_table_insert (self->overflow_cells, GINT_TO_POINTER (idx), aux); - else - g_hash_table_replace (self->overflow_cells, GINT_TO_POINTER (idx), g_list_copy (aux)); - } - - continue; + g_ptr_array_add (blocks, block); } - - - /* Retrieve the cell widget. On RTL languages, swap the first and last cells */ - month_cell = self->day_cells[cell]; - last_month_cell = self->day_cells[last_block_cell]; - - if (is_rtl) + else if (cell > first_cell && events_at_weekday[cell] != events_at_weekday[cell - 1]) { - GtkWidget *aux = month_cell; - month_cell = last_month_cell; - last_month_cell = aux; + GtkWidget *new_event_widget; + + new_event_widget = gcal_event_widget_clone (GCAL_EVENT_WIDGET (block->event_widget)); + setup_child_widget (self, new_event_widget); + + block = g_new (GcalEventBlock, 1); + block->event_widget = g_steal_pointer (&new_event_widget); + block->length = 1; + block->cell = cell; + + g_ptr_array_add (blocks, block); + } + else + { + block->length++; } - - gtk_widget_get_allocation (month_cell, &first_cell_allocation); - gtk_widget_get_allocation (last_month_cell, &last_cell_allocation); - - gtk_widget_set_child_visible (child_widget, TRUE); - - /* - * Setup the widget's start date as the first day of the row, - * and the widget's end date as the last day of the row. We don't - * have to worry about the dates, since GcalEventWidget performs - * some checks and only applies the dates when it's valid. - */ - range_start = gcal_range_get_start (self->range); - dt_start = g_date_time_new (g_date_time_get_timezone (gcal_event_get_date_start (event)), - g_date_time_get_year (range_start), - g_date_time_get_month (range_start), - g_date_time_get_day_of_month (range_start) + day, - 0, 0, 0); - - /* FIXME: use end date's timezone here */ - dt_end = g_date_time_add_days (dt_start, length); - - gcal_event_widget_set_date_start (GCAL_EVENT_WIDGET (child_widget), dt_start); - gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (child_widget), dt_end); - - pos_x = first_cell_allocation.x; - pos_y = first_cell_allocation.y + gcal_month_cell_get_header_height (GCAL_MONTH_CELL (month_cell)); - width = last_cell_allocation.x + last_cell_allocation.width - first_cell_allocation.x; - - /* - * We can only get the minimum height after making all these calculations, - * otherwise GTK complains about allocating without calling get_preferred_height. - */ - gtk_widget_measure (GTK_WIDGET (child_widget), - GTK_ORIENTATION_VERTICAL, - -1, - &minimum_height, - NULL, NULL, NULL); - - event_allocation.x = pos_x; - event_allocation.y = pos_y + vertical_cell_space[cell] - size_left[cell]; - event_allocation.width = width; - event_allocation.height = minimum_height; - - gtk_widget_size_allocate (child_widget, &event_allocation, baseline); - - /* update size_left */ - for (j = 0; j < length; j++) - size_left[cell + j] -= minimum_height; } - } -} -static void -allocate_single_day_events (GcalMonthViewRow *self, - gdouble *vertical_cell_space, - gdouble *size_left, - gint *events_at_day, - gint *allocated_events_at_day, - gint baseline) -{ - GHashTableIter iter; - GtkAllocation event_allocation; - GtkAllocation cell_allocation; - gpointer key, value; - - g_hash_table_iter_init (&iter, self->single_cell_children); - - while (g_hash_table_iter_next (&iter, &key, &value)) - { - GtkWidget *month_cell; - gboolean will_overflow; - gint day; - - day = GPOINTER_TO_INT (key); - month_cell = self->day_cells[day]; - - gtk_widget_get_allocation (month_cell, &cell_allocation); - - for (GList *l = (GList *)value; l; l = l->next) - { - GcalEvent *event; - GtkWidget *child_widget; - gdouble real_height; - gdouble pos_y; - gdouble pos_x; - gdouble width; - gint remaining_events; - gint minimum_height; - - child_widget = l->data; - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (child_widget)); - - if (!gtk_widget_should_layout (child_widget)) - continue; - - if (gcal_range_calculate_overlap (self->range, gcal_event_get_range (event), NULL) == GCAL_RANGE_NO_OVERLAP) - continue; - - gtk_widget_measure (GTK_WIDGET (child_widget), - GTK_ORIENTATION_VERTICAL, - -1, - &minimum_height, - NULL, NULL, NULL); - - /* Check for overflow */ - remaining_events = events_at_day[day] - allocated_events_at_day[day]; - will_overflow = remaining_events * minimum_height >= size_left[day]; - real_height = size_left[day]; - - if (will_overflow) - real_height -= gcal_month_cell_get_overflow_height (GCAL_MONTH_CELL (month_cell)); - - /* No space left, add to the overflow and continue */ - if (real_height < minimum_height) - { - gtk_widget_set_child_visible (child_widget, FALSE); - - l = g_hash_table_lookup (self->overflow_cells, GINT_TO_POINTER (day)); - l = g_list_append (l, child_widget); - - if (g_list_length (l) == 1) - g_hash_table_insert (self->overflow_cells, GINT_TO_POINTER (day), l); - else - g_hash_table_replace (self->overflow_cells, GINT_TO_POINTER (day), g_list_copy (l)); - - continue; - } - - allocated_events_at_day[day]++; - - gtk_widget_set_child_visible (child_widget, TRUE); - - pos_x = cell_allocation.x; - pos_y = cell_allocation.y + gcal_month_cell_get_header_height (GCAL_MONTH_CELL (month_cell)); - width = cell_allocation.width; - - event_allocation.x = pos_x; - event_allocation.y = pos_y + vertical_cell_space[day] - size_left[day]; - event_allocation.width = width; - event_allocation.height = minimum_height; - - gtk_widget_set_child_visible (child_widget, TRUE); - gtk_widget_size_allocate (child_widget, &event_allocation, baseline); - - size_left[day] -= minimum_height; - } + g_hash_table_insert (self->layout_blocks, event, g_steal_pointer (&blocks)); } } @@ -771,6 +331,20 @@ allocate_single_day_events (GcalMonthViewRow *self, * Callbacks */ +static gint +compare_events_cb (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GcalEvent *event_a = (GcalEvent *)a; + GcalEvent *event_b = (GcalEvent *)b; + + if (gcal_event_is_multiday (event_a) != gcal_event_is_multiday (event_b)) + return gcal_event_is_multiday (event_b) - gcal_event_is_multiday (event_a); + + return gcal_event_compare (event_a, event_b); +} + static void on_event_widget_activated_cb (GcalEventWidget *widget, GcalMonthViewRow *self) @@ -778,14 +352,6 @@ on_event_widget_activated_cb (GcalEventWidget *widget, g_signal_emit (self, signals[EVENT_ACTIVATED], 0, widget); } -static void -on_event_widget_visibility_changed_cb (GtkWidget *event_widget, - GParamSpec *pspec, - GcalMonthViewRow *self) -{ - self->needs_reallocation = TRUE; -} - /* * GtkWidget overrides @@ -864,51 +430,52 @@ gcal_month_view_row_size_allocate (GtkWidget *widget, } /* Event widgets */ - if (self->needs_reallocation || self->last_width != width || self->last_height != height) { - gdouble vertical_cell_space [7]; - gdouble size_left [7]; - gint allocated_events_at_day [7] = { 0, }; - gint events_at_day [7] = { 0, }; + gdouble cell_y[7]; + guint overflows[7] = { 0, }; + guint n_events; - /* Remove every widget' parts, but the main widget */ - cleanup_overflow_information (self); - - /* Event widgets */ - count_events_per_day (self, events_at_day); + prepare_layout_blocks (self, overflows); for (guint i = 0; i < 7; i++) - { - gint content_space = gcal_month_cell_get_content_space (GCAL_MONTH_CELL (self->day_cells[i])); + cell_y[i] = gcal_month_cell_get_header_height (GCAL_MONTH_CELL (self->day_cells[i])); - vertical_cell_space[i] = content_space; - size_left[i] = content_space; + n_events = g_list_model_get_n_items (G_LIST_MODEL (self->events)); + + for (guint i = 0; i < n_events; i++) + { + g_autoptr (GcalEvent) event = NULL; + GPtrArray *blocks; + + event = g_list_model_get_item (G_LIST_MODEL (self->events), i); + + blocks = g_hash_table_lookup (self->layout_blocks, event); + g_assert (blocks != NULL); + + for (guint block_index = 0; block_index < blocks->len; block_index++) + { + GcalEventBlock *block; + GtkAllocation allocation; + + block = g_ptr_array_index (blocks, block_index); + + /* TODO: RTL */ + allocation.x = block->cell * cell_width; + allocation.y = cell_y[block->cell]; + allocation.width = block->length * cell_width; + allocation.height = block->height; + + gtk_widget_set_child_visible (block->event_widget, block->visible); + gtk_widget_size_allocate (block->event_widget, &allocation, baseline); + + for (guint j = 0; j < block->length; j++) + cell_y[block->cell + j] += block->height; + } } - allocate_multiday_events (self, vertical_cell_space, size_left, events_at_day, allocated_events_at_day, baseline); - allocate_single_day_events (self, vertical_cell_space, size_left, events_at_day, allocated_events_at_day, baseline); + for (guint i = 0; i < 7; i++) + gcal_month_cell_set_overflow (GCAL_MONTH_CELL (self->day_cells[i]), overflows[i]); } - else - { - GtkWidget *child; - - for (child = gtk_widget_get_first_child (widget); - child; - child = gtk_widget_get_next_sibling (child)) - { - GtkAllocation allocation; - - if (!GCAL_IS_EVENT_WIDGET (child)) - continue; - - gtk_widget_get_allocation (child, &allocation); - gtk_widget_size_allocate (child, &allocation, baseline); - } - } - - self->last_width = width; - self->last_height = height; - self->needs_reallocation = FALSE; } @@ -920,18 +487,12 @@ static void gcal_month_view_row_dispose (GObject *object) { GcalMonthViewRow *self = (GcalMonthViewRow *)object; - GtkWidget *child; for (guint i = 0; i < 7; i++) g_clear_pointer (&self->day_cells[i], gtk_widget_unparent); - while ((child = gtk_widget_get_first_child (GTK_WIDGET (self))) != NULL) - remove_event_widget (self, child); - - g_clear_pointer (&self->children, g_hash_table_destroy); - g_clear_pointer (&self->single_cell_children, g_hash_table_destroy); - g_clear_pointer (&self->overflow_cells, g_hash_table_destroy); - g_clear_pointer (&self->multi_cell_children, g_list_free); + g_clear_pointer (&self->layout_blocks, g_hash_table_destroy); + g_clear_object (&self->events); G_OBJECT_CLASS (gcal_month_view_row_parent_class)->dispose (object); } @@ -1020,11 +581,8 @@ gcal_month_view_row_class_init (GcalMonthViewRowClass *klass) static void gcal_month_view_row_init (GcalMonthViewRow *self) { - self->last_width = -1; - self->last_height = -1; - self->children = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_list_free); - self->single_cell_children = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_list_free); - self->overflow_cells = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_list_free); + self->events = g_list_store_new (GCAL_TYPE_EVENT); + self->layout_blocks = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_ptr_array_unref); for (guint i = 0; i < 7; i++) { @@ -1076,7 +634,6 @@ gcal_month_view_row_set_range (GcalMonthViewRow *self, GcalRange *range) { g_autoptr (GDateTime) start = NULL; - GtkWidget *child; g_return_if_fail (GCAL_IS_MONTH_VIEW_ROW (self)); g_return_if_fail (range != NULL); @@ -1090,16 +647,8 @@ gcal_month_view_row_set_range (GcalMonthViewRow *self, self->range = gcal_range_ref (range); /* Preemptively remove all event widgets */ - child = gtk_widget_get_first_child (GTK_WIDGET (self)); - while (child) - { - GtkWidget *next = gtk_widget_get_next_sibling (child); - - if (GCAL_IS_EVENT_WIDGET (child)) - remove_event_widget (self, child); - - child = next; - } + g_list_store_remove_all (self->events); + g_hash_table_remove_all (self->layout_blocks); start = gcal_range_get_start (range); for (guint i = 0; i < 7; i++) @@ -1113,49 +662,35 @@ void gcal_month_view_row_add_event (GcalMonthViewRow *self, GcalEvent *event) { - GcalCalendar *calendar; - GtkWidget *event_widget; - g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); g_assert (GCAL_IS_EVENT (event)); - calendar = gcal_event_get_calendar (event); - - event_widget = gcal_event_widget_new (self->context, event); - gcal_event_widget_set_read_only (GCAL_EVENT_WIDGET (event_widget), gcal_calendar_is_read_only (calendar)); - if (!gcal_event_get_all_day (event) && !gcal_event_is_multiday (event)) - gcal_event_widget_set_timestamp_policy (GCAL_EVENT_WIDGET (event_widget), GCAL_TIMESTAMP_POLICY_START); - - add_event_widget (self, event_widget); - - self->needs_reallocation = TRUE; + g_list_store_insert_sorted (self->events, event, compare_events_cb, self); + recalculate_layout_blocks (self); } void gcal_month_view_row_remove_event (GcalMonthViewRow *self, GcalEvent *event) { - const gchar *uid; - GList *l; + guint position; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); g_assert (GCAL_IS_EVENT (event)); GCAL_ENTRY; - uid = gcal_event_get_uid (event); - l = g_hash_table_lookup (self->children, uid); - - if (!l) + if (!g_list_store_find (self->events, event, &position)) { g_warning ("%s: Widget with uuid: %s not found in view: %s", - G_STRFUNC,uid, G_OBJECT_TYPE_NAME (self)); + G_STRFUNC, + gcal_event_get_uid (event), + G_OBJECT_TYPE_NAME (self)); GCAL_RETURN (); } - remove_event_widget (self, l->data); - - self->needs_reallocation = TRUE; + g_list_store_remove (self->events, position); + recalculate_layout_blocks (self); GCAL_EXIT; }