/* * gcal-month-view-row.c * * Copyright 2023 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "GcalMonthViewRow" #include "config.h" #include "gcal-date-time-utils.h" #include "gcal-debug.h" #include "gcal-event-widget.h" #include "gcal-month-cell.h" #include "gcal-month-view-row.h" #include "gcal-range-tree.h" #include "gcal-utils.h" #define SEPARATOR_OFFSET -1 #define EVENT_VERTICAL_GAP 2 typedef struct { GtkWidget *event_widget; guint8 length; guint8 cell; /* These are updated during allocation */ gboolean visible; gint height; } GcalEventBlock; struct _GcalMonthViewRow { GtkWidget parent; GtkWidget *day_cells[GCAL_N_WEEKDAYS]; GcalRange *range; gboolean ceiled_height; GListModel *events; GcalEventWidgetPool *event_widget_pool; GHashTable *layout_blocks; gboolean layout_blocks_valid; }; static void on_event_widget_activated_cb (GcalEventWidget *widget, GcalMonthViewRow *self); static void on_cell_activated_cb (GcalMonthCell *cell, GcalMonthViewRow *self); static gboolean widget_tick_cb (GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data); G_DEFINE_FINAL_TYPE (GcalMonthViewRow, gcal_month_view_row, GTK_TYPE_WIDGET) enum { PROP_0, PROP_RANGE, PROP_CEILED_HEIGHT, N_PROPS }; enum { EVENT_ACTIVATED, CELL_ACTIVATED, SHOW_OVERFLOW, N_SIGNALS, }; static guint signals[N_SIGNALS] = { 0, }; static GParamSpec *properties [N_PROPS] = { NULL, }; /* * FocusEventData */ typedef struct { GtkWidget *widget; graphene_rect_t rect; gint normalized_x; } FocusEventData; static void focus_event_data_free (gpointer data) { FocusEventData *focus_event_data = data; g_free (focus_event_data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (FocusEventData, focus_event_data_free) static FocusEventData * focus_event_data_new (GtkWidget *widget, const graphene_rect_t *rect, gint normalized_x) { g_autoptr (FocusEventData) focus_event_data = NULL; focus_event_data = g_new0 (FocusEventData, 1); focus_event_data->widget = widget; focus_event_data->normalized_x = normalized_x; graphene_rect_init_from_rect (&focus_event_data->rect, rect); return g_steal_pointer (&focus_event_data); } static inline void swap_focus_event_data (FocusEventData **dest, FocusEventData *src) { g_assert (dest != NULL); g_clear_pointer (dest, focus_event_data_free); *dest = g_steal_pointer (&src); } /* * Auxiliary methods */ static FocusEventData * create_focus_event_data (GcalMonthViewRow *self, GtkWidget *child) { gint focal_x, normalized_x; graphene_rect_t rect; gboolean is_rtl; g_assert (GTK_IS_WIDGET (child)); if (!gtk_widget_compute_bounds (child, GTK_WIDGET (self), &rect)) g_assert_not_reached (); is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL; focal_x = rect.origin.x + (is_rtl ? rect.size.width : 0); normalized_x = is_rtl ? -focal_x : focal_x; return focus_event_data_new (child, &rect, normalized_x); } static FocusEventData * create_focus_event_data_candidate (GcalMonthViewRow *self, GtkWidget *child) { g_autoptr (FocusEventData) candidate = NULL; g_assert (GTK_IS_WIDGET (child)); if (GCAL_IS_EVENT_WIDGET (child)) candidate = create_focus_event_data (self, child); else if (GCAL_IS_MONTH_CELL (child) && gcal_month_cell_get_overflow (GCAL_MONTH_CELL (child))) candidate = create_focus_event_data (self, gcal_month_cell_get_overflow_button (GCAL_MONTH_CELL (child))); else return FALSE; if (!gtk_widget_get_child_visible (candidate->widget)) return NULL; return g_steal_pointer (&candidate); } static GtkWidget * find_first_event_widget (GcalMonthViewRow *self) { g_autoptr (FocusEventData) candidate = NULL; g_autoptr (FocusEventData) nearest = NULL; GtkWidget *child; for (child = gtk_widget_get_first_child (GTK_WIDGET (self)); child != NULL; child = gtk_widget_get_next_sibling (child)) { g_autoptr (FocusEventData) aux = NULL; if (!(aux = create_focus_event_data_candidate (self, child))) continue; swap_focus_event_data (&candidate, g_steal_pointer (&aux)); g_assert (candidate != NULL); if (nearest == NULL) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if (candidate->normalized_x < nearest->normalized_x) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if (candidate->normalized_x == nearest->normalized_x && candidate->rect.origin.y < nearest->rect.origin.y) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); } g_assert (nearest == NULL || GTK_IS_WIDGET (nearest->widget)); GCAL_RETURN (nearest ? g_steal_pointer (&nearest->widget) : NULL); } static GtkWidget * find_last_event_widget (GcalMonthViewRow *self) { g_autoptr (FocusEventData) candidate = NULL; g_autoptr (FocusEventData) nearest = NULL; GtkWidget *child; for (child = gtk_widget_get_first_child (GTK_WIDGET (self)); child != NULL; child = gtk_widget_get_next_sibling (child)) { g_autoptr (FocusEventData) aux = NULL; if (!(aux = create_focus_event_data_candidate (self, child))) continue; swap_focus_event_data (&candidate, g_steal_pointer (&aux)); g_assert (candidate != NULL); if (nearest == NULL) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if (candidate->normalized_x > nearest->normalized_x) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if (candidate->normalized_x == nearest->normalized_x && candidate->rect.origin.y > nearest->rect.origin.y) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); } g_assert (nearest == NULL || GTK_IS_WIDGET (nearest->widget)); GCAL_RETURN (nearest ? g_steal_pointer (&nearest->widget) : NULL); } static GtkWidget * find_nearest_vertical_event_widget (GcalMonthViewRow *self, FocusEventData *focused, GtkDirectionType direction) { g_autoptr (FocusEventData) candidate = NULL; g_autoptr (FocusEventData) nearest = NULL; GtkWidget *child; gboolean downward, upward, forward_or_backward; GCAL_ENTRY; downward = direction == GTK_DIR_DOWN || direction == GTK_DIR_TAB_FORWARD; upward = !downward; forward_or_backward = direction == GTK_DIR_TAB_FORWARD || direction == GTK_DIR_TAB_BACKWARD; for (child = gtk_widget_get_first_child (GTK_WIDGET (self)); child != NULL; child = gtk_widget_get_next_sibling (child)) { g_autoptr (FocusEventData) aux = NULL; if (!(aux = create_focus_event_data_candidate (self, child))) continue; swap_focus_event_data (&candidate, g_steal_pointer (&aux)); g_assert (candidate != NULL); if (candidate->widget == focused->widget) continue; if (!graphene_rect_intersection (&GRAPHENE_RECT_INIT (focused->rect.origin.x, 0, focused->rect.size.width, 1), &GRAPHENE_RECT_INIT (candidate->rect.origin.x, 0, candidate->rect.size.width, 1), NULL)) continue; if (forward_or_backward && focused->normalized_x != candidate->normalized_x) continue; if ((downward && candidate->rect.origin.y < focused->rect.origin.y) || (upward && candidate->rect.origin.y > focused->rect.origin.y)) continue; if (nearest == NULL) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if ((downward && candidate->rect.origin.y < nearest->rect.origin.y) || (upward && candidate->rect.origin.y > nearest->rect.origin.y)) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if (downward && candidate->rect.origin.y == nearest->rect.origin.y && candidate->normalized_x < nearest->normalized_x) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); } g_assert (nearest == NULL || nearest->widget != focused->widget); GCAL_RETURN (nearest ? g_steal_pointer (&nearest->widget) : NULL); } static GtkWidget * find_nearest_horizontal_event_widget (GcalMonthViewRow *self, FocusEventData *focused, GtkDirectionType direction) { g_autoptr (FocusEventData) candidate = NULL; g_autoptr (FocusEventData) nearest = NULL; GtkWidget *child; gboolean forward_or_backward; GtkDirectionType lateral; gboolean is_rtl; GCAL_ENTRY; g_assert (focused != NULL); is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL; forward_or_backward = direction == GTK_DIR_TAB_FORWARD || direction == GTK_DIR_TAB_BACKWARD; switch (direction) { case GTK_DIR_TAB_FORWARD: lateral = is_rtl ? GTK_DIR_LEFT : GTK_DIR_RIGHT; break; case GTK_DIR_TAB_BACKWARD: lateral = is_rtl ? GTK_DIR_RIGHT : GTK_DIR_LEFT; break; default: g_assert (direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT); lateral = direction; break; } for (child = gtk_widget_get_first_child (GTK_WIDGET (self)); child != NULL; child = gtk_widget_get_next_sibling (child)) { g_autoptr (FocusEventData) aux = NULL; gboolean candidate_intersects_vertically; if (!(aux = create_focus_event_data_candidate (self, child))) continue; swap_focus_event_data (&candidate, g_steal_pointer (&aux)); g_assert (candidate != NULL); if (candidate->widget == focused->widget) continue; if ((direction == GTK_DIR_TAB_FORWARD && candidate->normalized_x <= focused->normalized_x) || (direction == GTK_DIR_TAB_BACKWARD && candidate->normalized_x >= focused->normalized_x)) continue; if ((direction == GTK_DIR_LEFT && ABS (candidate->normalized_x) > ABS (focused->normalized_x)) || (direction == GTK_DIR_RIGHT && ABS (candidate->normalized_x) < ABS (focused->normalized_x))) continue; candidate_intersects_vertically = graphene_rect_intersection (&GRAPHENE_RECT_INIT (0, focused->rect.origin.y, 1, focused->rect.size.height), &GRAPHENE_RECT_INIT (0, candidate->rect.origin.y, 1, candidate->rect.size.height), NULL); if (!forward_or_backward && !candidate_intersects_vertically) continue; if (nearest == NULL) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if ((lateral == GTK_DIR_LEFT && ABS (candidate->normalized_x) > ABS (nearest->normalized_x)) || (lateral == GTK_DIR_RIGHT && ABS (candidate->normalized_x) < ABS (nearest->normalized_x))) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); else if (ABS (candidate->normalized_x) == ABS (nearest->normalized_x) && ((direction == GTK_DIR_TAB_FORWARD && candidate->rect.origin.y < nearest->rect.origin.y) || (direction == GTK_DIR_TAB_BACKWARD && candidate->rect.origin.y > nearest->rect.origin.y))) swap_focus_event_data (&nearest, g_steal_pointer (&candidate)); } g_assert (nearest == NULL || nearest->widget != focused->widget); GCAL_RETURN (nearest ? g_steal_pointer (&nearest->widget) : NULL); } static inline gboolean is_overflow_focused (GtkWidget *focused) { GtkWidget *cell, *overflow; if ((cell = gtk_widget_get_ancestor (focused, GCAL_TYPE_MONTH_CELL))) overflow = gcal_month_cell_get_overflow_button (GCAL_MONTH_CELL (cell)); else return FALSE; return gtk_widget_is_focus (overflow); } static inline gboolean focus_vertical_cell (GcalMonthViewRow *self, GtkWidget *focused_cell) { graphene_point_t point; GtkWidget *row, *cell; gdouble cell_width; if (gtk_widget_is_ancestor (focused_cell, GTK_WIDGET (self))) return FALSE; cell_width = (gdouble) gtk_widget_get_width (GTK_WIDGET (self)) / (float) GCAL_N_WEEKDAYS; row = gtk_widget_get_ancestor (focused_cell, GCAL_TYPE_MONTH_VIEW_ROW); g_assert (GTK_WIDGET (self) != row); if (!gtk_widget_compute_point (focused_cell, row, &GRAPHENE_POINT_INIT (cell_width / 2, 0), &point)) g_assert_not_reached (); cell = gcal_month_view_row_get_cell_at_x (self, point.x); return gtk_widget_grab_focus (cell); } static gboolean focus_horizontal_cell (GcalMonthViewRow *self, GtkWidget *focused_cell, GtkDirectionType direction) { gint width; GtkWidget *new_cell; graphene_point_t point; width = gtk_widget_get_width (focused_cell); if (!gtk_widget_compute_point (focused_cell, GTK_WIDGET (self), &GRAPHENE_POINT_INIT (width / 2, 0), &point)) g_assert_not_reached (); if (direction == GTK_DIR_LEFT) point.x -= width; else point.x += width; new_cell = gcal_month_view_row_get_cell_at_x (self, point.x); if (new_cell) return gtk_widget_grab_focus (new_cell); else return FALSE; } 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 set_up_child_widget (GcalMonthViewRow *self, GtkWidget *widget) { gtk_widget_insert_after (widget, GTK_WIDGET (self), self->day_cells[GCAL_N_WEEKDAYS - 1]); g_signal_connect_object (widget, "activate", G_CALLBACK (on_event_widget_activated_cb), self, 0); } static void calculate_event_cells (GcalMonthViewRow *self, GcalEvent *event, gint *out_first_cell, gint *out_last_cell) { g_autoptr (GDateTime) range_start = NULL; g_autoptr (GDateTime) start_date = NULL; gboolean all_day; gint first_cell; g_assert (out_first_cell != NULL || out_last_cell != NULL); all_day = gcal_event_get_all_day (event); 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); first_cell = MAX (gcal_date_time_compare_date (start_date, range_start), 0); /* Start date */ if (out_first_cell) *out_first_cell = first_cell; /* * The logic for the end date is the same, except that we have to check * if the event is all day or not. */ if (out_last_cell) { g_autoptr (GDateTime) range_start = NULL; g_autoptr (GDateTime) end_date = NULL; gint last_cell = GCAL_N_WEEKDAYS - 1; range_start = gcal_range_get_start (self->range); end_date = gcal_event_get_date_end (event); if (all_day) { end_date = g_date_time_ref (end_date); } else { g_autoptr (GDateTime) local_end_date = g_date_time_to_local (end_date); end_date = g_date_time_add_seconds (local_end_date, -1); } last_cell = gcal_date_time_compare_date (end_date, range_start); if (all_day) last_cell--; *out_last_cell = CLAMP (last_cell, first_cell, GCAL_N_WEEKDAYS - 1); } } static void prepare_layout_blocks (GcalMonthViewRow *self, guint overflows[GCAL_N_WEEKDAYS]) { GPtrArray *blocks_per_day[GCAL_N_WEEKDAYS]; gboolean cell_will_overflow[GCAL_N_WEEKDAYS] = { FALSE, }; guint combined_height[GCAL_N_WEEKDAYS] = { 0, }; guint content_height[GCAL_N_WEEKDAYS] = { 0, }; guint weekday_heights[GCAL_N_WEEKDAYS] = { 0, }; guint n_events; g_assert (self->layout_blocks_valid); n_events = g_list_model_get_n_items (self->events); for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) { gint overflow_height; gint content_space; content_space = gcal_month_cell_get_content_space (GCAL_MONTH_CELL (self->day_cells[i])); if (self->ceiled_height) content_space--; overflow_height = gcal_month_cell_get_overflow_height (GCAL_MONTH_CELL (self->day_cells[i])); combined_height[i] = content_space + overflow_height; content_height[i] = content_space; 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 (self->events, i); if (!gcal_event_overlaps (event, self->range)) continue; 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; int spacing; g_ptr_array_add (blocks_per_day[cell], block); spacing = (blocks_per_day[cell]->len - 1) * EVENT_VERTICAL_GAP; weekday_heights[cell] += block->height; cell_will_overflow[cell] |= weekday_heights[cell] + spacing > combined_height[cell]; } } } /* Figure out which blocks will be visible */ for (guint cell = 0; cell < GCAL_N_WEEKDAYS; cell++) { for (guint block_index = 0; block_index < blocks_per_day[cell]->len; block_index++) { GcalEventBlock *block = g_ptr_array_index (blocks_per_day[cell], block_index); if (block->cell != cell) continue; /* Will this block be visible in all cells it spans? */ for (guint block_cell = cell; block_cell < cell + block->length; block_cell++) { gint available_height; if (cell_will_overflow[block_cell]) available_height = content_height[block_cell] - block_cell * EVENT_VERTICAL_GAP; else available_height = combined_height[block_cell]; block->visible &= available_height > block->height; } for (guint block_cell = cell; block_cell < cell + block->length; block_cell++) { if (block->visible) { content_height[block_cell] -= block->height; combined_height[block_cell] -= block->height; } else { overflows[block_cell]++; } } } } for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) g_clear_pointer (&blocks_per_day[i], g_ptr_array_unref); } static GHashTable * extract_current_event_widgets (GcalMonthViewRow *self) { g_autoptr (GHashTable) event_widgets = NULL; GHashTableIter iter; GPtrArray *blocks; GcalEvent *event; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); event_widgets = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_ptr_array_unref); g_hash_table_iter_init (&iter, self->layout_blocks); while (g_hash_table_iter_next (&iter, (gpointer *) &event, (gpointer *) &blocks)) { g_autoptr (GPtrArray) widgets = NULL; g_assert (GCAL_IS_EVENT (event)); g_assert (blocks != NULL); widgets = g_ptr_array_new_full (blocks->len, (GDestroyNotify) gtk_widget_unparent); for (unsigned int i = 0; i < blocks->len; i++) { GcalEventBlock *block = g_ptr_array_index (blocks, i); g_ptr_array_add (widgets, g_steal_pointer (&block->event_widget)); } g_hash_table_insert (event_widgets, event, g_steal_pointer (&widgets)); } return g_steal_pointer (&event_widgets); } static void return_remaining_event_widgets_to_pool (GcalMonthViewRow *self, GHashTable *event_widgets) { GHashTableIter iter; GPtrArray *widgets; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); g_assert (event_widgets != NULL); g_hash_table_iter_init (&iter, event_widgets); while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &widgets)) { g_assert (widgets != NULL); while (widgets->len > 0) { GtkWidget *widget = g_ptr_array_steal_index_fast (widgets, 0); g_signal_handlers_disconnect_by_func (widget, on_event_widget_activated_cb, self); gtk_widget_unparent (widget); gcal_event_widget_pool_reclaim (self->event_widget_pool, g_steal_pointer (&widget)); } g_hash_table_iter_remove (&iter); } g_assert (g_hash_table_size (event_widgets) == 0); } static GtkWidget * pick_existing_event_widget (GHashTable *event_widgets, GcalEvent *event) { GPtrArray *widgets; g_assert (event_widgets != NULL); g_assert (GCAL_IS_EVENT (event)); widgets = g_hash_table_lookup (event_widgets, event); if (!widgets || widgets->len == 0) return NULL; return g_ptr_array_steal_index_fast (widgets, 0); } static void recalculate_layout_blocks (GcalMonthViewRow *self) { g_autoptr (GHashTable) event_widgets = NULL; g_autoptr (GDateTime) range_start = NULL; guint events_at_weekday[GCAL_N_WEEKDAYS] = { 0, }; guint n_events; GCAL_ENTRY; g_assert (!self->layout_blocks_valid); range_start = gcal_range_get_start (self->range); n_events = g_list_model_get_n_items (self->events); event_widgets = extract_current_event_widgets (self); g_hash_table_remove_all (self->layout_blocks); for (guint i = 0; i < n_events; i++) { g_autoptr (GPtrArray) blocks = NULL; g_autoptr (GcalEvent) event = NULL; GcalEventBlock *block = NULL; gint first_cell; gint last_cell; event = g_list_model_get_item (self->events, i); if (!gcal_event_overlaps (event, self->range)) continue; calculate_event_cells (self, event, &first_cell, &last_cell); g_assert (last_cell >= first_cell); blocks = g_ptr_array_new_full (last_cell - first_cell + 1, layout_block_free); for (gint cell = first_cell; cell <= last_cell; cell++) { gboolean event_will_break; events_at_weekday[cell]++; event_will_break = cell > first_cell && events_at_weekday[cell] != events_at_weekday[cell - 1]; if (!block || event_will_break) { GtkWidget *event_widget = pick_existing_event_widget (event_widgets, event); if (!event_widget) { event_widget = gcal_event_widget_pool_take_or_create (self->event_widget_pool, event); g_assert (GCAL_IS_EVENT_WIDGET (event_widget)); set_up_child_widget (self, event_widget); } 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); block = g_new (GcalEventBlock, 1); block->event_widget = g_steal_pointer (&event_widget); block->length = 1; block->cell = cell; g_ptr_array_add (blocks, block); } else { block->length++; } } /* Adjust slanted edges of multiday events */ if (gcal_event_is_multiday (event)) { g_autoptr (GDateTime) adjusted_range_start = NULL; adjusted_range_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), 0, 0, 0); for (guint j = 0; j < blocks->len; j++) { g_autoptr (GDateTime) block_start = NULL; g_autoptr (GDateTime) block_end = NULL; block = g_ptr_array_index (blocks, j); block_start = g_date_time_add_days (adjusted_range_start, block->cell); block_end = g_date_time_add_days (block_start, block->length); /* FIXME: use end date's timezone here */ gcal_event_widget_set_date_start (GCAL_EVENT_WIDGET (block->event_widget), block_start); gcal_event_widget_set_date_end (GCAL_EVENT_WIDGET (block->event_widget), block_end); } } g_hash_table_insert (self->layout_blocks, event, g_steal_pointer (&blocks)); } return_remaining_event_widgets_to_pool (self, event_widgets); self->layout_blocks_valid = TRUE; GCAL_EXIT; } static void invalidate_layout_blocks (GcalMonthViewRow *self) { GCAL_ENTRY; if (!self->layout_blocks_valid) GCAL_RETURN (); if (gtk_widget_get_mapped (GTK_WIDGET (self))) { gtk_widget_add_tick_callback (GTK_WIDGET (self), widget_tick_cb, self, NULL); gtk_widget_queue_allocate (GTK_WIDGET (self)); } self->layout_blocks_valid = FALSE; GCAL_EXIT; } /* * Callbacks */ static gboolean filter_by_range_cb (gpointer item, gpointer user_data) { GcalMonthViewRow *self = user_data; GcalEvent *event = item; g_assert (GCAL_IS_EVENT (event)); g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); if (!self->range) return FALSE; return gcal_event_overlaps (event, self->range); } static void events_changed_cb (GListModel *model, guint position, guint removed, guint added, GcalMonthViewRow *self) { GCAL_ENTRY; invalidate_layout_blocks (self); GCAL_EXIT; } static void on_event_widget_activated_cb (GcalEventWidget *widget, GcalMonthViewRow *self) { g_signal_emit (self, signals[EVENT_ACTIVATED], 0, widget); } static void on_cell_activated_cb (GcalMonthCell *cell, GcalMonthViewRow *self) { g_signal_emit (self, signals[CELL_ACTIVATED], 0, cell); } static void on_month_cell_show_overflow_cb (GcalMonthCell *cell, GtkWidget *button, GcalMonthViewRow *self) { g_signal_emit (self, signals[SHOW_OVERFLOW], 0, cell); } static gboolean widget_tick_cb (GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data) { GcalMonthViewRow *self = (GcalMonthViewRow *) widget; GCAL_ENTRY; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); if (!self->layout_blocks_valid) recalculate_layout_blocks (self); GCAL_RETURN (G_SOURCE_REMOVE); } /* * GtkWidget overrides */ static void gcal_month_view_row_map (GtkWidget *widget) { GcalMonthViewRow *self = (GcalMonthViewRow *) widget; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); GTK_WIDGET_CLASS (gcal_month_view_row_parent_class)->map (widget); if (!self->layout_blocks_valid) recalculate_layout_blocks (self); } static gboolean gcal_month_view_row_focus (GtkWidget *widget, GtkDirectionType direction) { GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (widget); g_autoptr (FocusEventData) data = NULL; GtkWidget *focused, *new_focus, *cell; gboolean overflow_focused; GtkRoot *root; root = gtk_widget_get_root (widget); focused = gtk_root_get_focus (root); if (!focused) return GTK_WIDGET_CLASS (gcal_month_view_row_parent_class)->focus (widget, direction); if (!gtk_widget_is_ancestor (focused, widget)) { switch (direction) { case GTK_DIR_TAB_FORWARD: if ((new_focus = find_first_event_widget (self))) return gtk_widget_grab_focus (new_focus); else return FALSE; case GTK_DIR_TAB_BACKWARD: if ((new_focus = find_last_event_widget (self))) return gtk_widget_grab_focus (new_focus); else return FALSE; default: break; } } overflow_focused = is_overflow_focused (focused); cell = gtk_widget_get_ancestor (focused, GCAL_TYPE_MONTH_CELL); if (cell && !overflow_focused) { if (direction == GTK_DIR_UP || direction == GTK_DIR_DOWN) return focus_vertical_cell (self, cell); else if (direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT) return focus_horizontal_cell (self, cell, direction); } if (GCAL_IS_EVENT_WIDGET (focused) || overflow_focused) { g_autoptr (FocusEventData) focus_event_data = create_focus_event_data (self, focused); switch (direction) { case GTK_DIR_TAB_FORWARD: case GTK_DIR_TAB_BACKWARD: if ((new_focus = find_nearest_vertical_event_widget (self, focus_event_data, direction))) return gtk_widget_grab_focus (new_focus); if ((new_focus = find_nearest_horizontal_event_widget (self, focus_event_data, direction))) return gtk_widget_grab_focus (new_focus); return FALSE; case GTK_DIR_UP: case GTK_DIR_DOWN: if ((new_focus = find_nearest_vertical_event_widget (self, focus_event_data, direction))) return gtk_widget_grab_focus (new_focus); else return FALSE; case GTK_DIR_LEFT: case GTK_DIR_RIGHT: if ((new_focus = find_nearest_horizontal_event_widget (self, focus_event_data, direction))) return gtk_widget_grab_focus (new_focus); else return FALSE; } } return GTK_WIDGET_CLASS (gcal_month_view_row_parent_class)->focus (widget, direction); } static void gcal_month_view_row_measure (GtkWidget *widget, GtkOrientation orientation, gint for_size, gint *out_minimum, gint *out_natural, gint *out_minimum_baseline, gint *out_natural_baseline) { GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (widget); gint minimum = 0; gint natural = 0; for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) { gint child_minimum; gint child_natural; gtk_widget_measure (self->day_cells[i], orientation, for_size, &child_minimum, &child_natural, NULL, NULL); if (orientation == GTK_ORIENTATION_HORIZONTAL) { minimum += child_minimum; natural += child_natural; } else { minimum = MAX(minimum, child_minimum); natural = MAX(natural, child_natural); } } if (out_minimum) *out_minimum = minimum; if (out_natural) *out_natural = natural; if (out_minimum_baseline) *out_minimum_baseline = -1; if (out_natural_baseline) *out_natural_baseline = -1; } static void gcal_month_view_row_size_allocate (GtkWidget *widget, gint width, gint height, gint baseline) { GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (widget); gboolean is_ltr; gdouble cell_width; is_ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; cell_width = width / (float) GCAL_N_WEEKDAYS; /* Month cells */ for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) { GtkAllocation allocation; GtkWidget *cell; allocation.x = round (cell_width * i); allocation.y = 0; allocation.width = round (cell_width * (i + 1)) - allocation.x; allocation.height = height; cell = is_ltr ? self->day_cells[i] : self->day_cells[GCAL_N_WEEKDAYS - i - 1]; gtk_widget_size_allocate (cell, &allocation, baseline); } /* Event widgets */ if (self->layout_blocks_valid) { gdouble cell_y[GCAL_N_WEEKDAYS] = { 0, }; guint overflows[GCAL_N_WEEKDAYS] = { 0, }; guint n_events; prepare_layout_blocks (self, overflows); n_events = g_list_model_get_n_items (self->events); for (guint i = 0; i < n_events; i++) { g_autoptr (GcalEvent) event = NULL; GPtrArray *blocks; event = g_list_model_get_item (self->events, i); if (!gcal_event_overlaps (event, self->range)) continue; 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; gint header_height; gint start_cell; gint end_cell; gint spacing; block = g_ptr_array_index (blocks, block_index); start_cell = is_ltr ? block->cell : GCAL_N_WEEKDAYS - block->cell - block->length; end_cell = start_cell + block->length; header_height = gcal_month_cell_get_header_height (GCAL_MONTH_CELL (self->day_cells[block->cell])); spacing = (cell_y[block->cell] / block->height - 1) * EVENT_VERTICAL_GAP; allocation.x = round (start_cell * cell_width - (is_ltr ? 0 : SEPARATOR_OFFSET)); allocation.y = cell_y[block->cell] + header_height + spacing; allocation.width = round (end_cell * cell_width) - allocation.x + (is_ltr ? SEPARATOR_OFFSET : 0); 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; } } for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) gcal_month_cell_set_overflow (GCAL_MONTH_CELL (self->day_cells[i]), overflows[i]); } } /* * GObject overrides */ static void gcal_month_view_row_dispose (GObject *object) { GcalMonthViewRow *self = (GcalMonthViewRow *)object; for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) g_clear_pointer (&self->day_cells[i], gtk_widget_unparent); 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); } static void gcal_month_view_row_finalize (GObject *object) { GcalMonthViewRow *self = (GcalMonthViewRow *)object; g_clear_object (&self->event_widget_pool); g_clear_pointer (&self->range, gcal_range_unref); G_OBJECT_CLASS (gcal_month_view_row_parent_class)->finalize (object); } static void gcal_month_view_row_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (object); switch (prop_id) { case PROP_CEILED_HEIGHT: g_value_set_boolean (value, gcal_month_view_row_get_ceiled_height (self)); break; case PROP_RANGE: g_value_set_boxed (value, self->range); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gcal_month_view_row_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (object); switch (prop_id) { case PROP_CEILED_HEIGHT: gcal_month_view_row_set_ceiled_height (self, g_value_get_boolean (value)); break; case PROP_RANGE: gcal_month_view_row_set_range (self, g_value_get_boxed (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gcal_month_view_row_class_init (GcalMonthViewRowClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->dispose = gcal_month_view_row_dispose; object_class->finalize = gcal_month_view_row_finalize; object_class->get_property = gcal_month_view_row_get_property; object_class->set_property = gcal_month_view_row_set_property; widget_class->map = gcal_month_view_row_map; widget_class->focus = gcal_month_view_row_focus; widget_class->measure = gcal_month_view_row_measure; widget_class->size_allocate = gcal_month_view_row_size_allocate; /** * GcalMonthViewRow:ceiled-height: * * Whether the row should negate the compensated pixel when allocating overflow. * * This property should only be set to %TRUE if the height of [class@MonthView] * is not divisible by the amount of visible rows, and this row's height is being * allocated an additional pixel. */ properties[PROP_CEILED_HEIGHT] = g_param_spec_boolean ("ceiled-height", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); properties[PROP_RANGE] = g_param_spec_boxed ("range", NULL, NULL, GCAL_TYPE_RANGE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, N_PROPS, properties); signals[EVENT_ACTIVATED] = g_signal_new ("event-activated", GCAL_TYPE_MONTH_VIEW_ROW, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, GCAL_TYPE_EVENT_WIDGET); signals[CELL_ACTIVATED] = g_signal_new ("cell-activated", GCAL_TYPE_MONTH_VIEW_ROW, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, GCAL_TYPE_MONTH_CELL); signals[SHOW_OVERFLOW] = g_signal_new ("show-overflow", GCAL_TYPE_MONTH_VIEW_ROW, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, GCAL_TYPE_MONTH_CELL); gtk_widget_class_set_css_name (widget_class, "monthviewrow"); } static void gcal_month_view_row_init (GcalMonthViewRow *self) { self->events = G_LIST_MODEL (gtk_filter_list_model_new (NULL, GTK_FILTER (gtk_custom_filter_new (filter_by_range_cb, self, NULL)))); g_signal_connect (self->events, "items-changed", G_CALLBACK (events_changed_cb), self); self->layout_blocks = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_ptr_array_unref); self->layout_blocks_valid = TRUE; for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) { self->day_cells[i] = gcal_month_cell_new (); gcal_month_cell_set_overflow (GCAL_MONTH_CELL (self->day_cells[i]), 0); gtk_widget_set_parent (self->day_cells[i], GTK_WIDGET (self)); g_signal_connect (self->day_cells[i], "show-overflow", G_CALLBACK (on_month_cell_show_overflow_cb), self); g_signal_connect (self->day_cells[i], "activate", G_CALLBACK (on_cell_activated_cb), self); gtk_accessible_update_relation (GTK_ACCESSIBLE (self->day_cells[i]), GTK_ACCESSIBLE_RELATION_COL_INDEX, i + 1, -1); } } GtkWidget * gcal_month_view_row_new (GcalEventWidgetPool *event_widget_pool) { g_autoptr (GcalMonthViewRow) self = NULL; g_assert (GCAL_IS_EVENT_WIDGET_POOL (event_widget_pool)); self = g_object_new (GCAL_TYPE_MONTH_VIEW_ROW, NULL); self->event_widget_pool = g_object_ref (event_widget_pool); return GTK_WIDGET (g_steal_pointer (&self)); } GcalRange * gcal_month_view_row_get_range (GcalMonthViewRow *self) { g_return_val_if_fail (GCAL_IS_MONTH_VIEW_ROW (self), NULL); return self->range ? gcal_range_ref (self->range) : NULL; } void gcal_month_view_row_set_range (GcalMonthViewRow *self, GcalRange *range) { g_autoptr (GDateTime) start = NULL; GtkFilter *filter; g_return_if_fail (GCAL_IS_MONTH_VIEW_ROW (self)); g_return_if_fail (range != NULL); GCAL_ENTRY; if (self->range && gcal_range_compare (self->range, range) == 0) GCAL_RETURN (); g_clear_pointer (&self->range, gcal_range_unref); self->range = gcal_range_ref (range); start = gcal_range_get_start (range); for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) { g_autoptr (GDateTime) day = g_date_time_add_days (start, i); gcal_month_cell_set_date (GCAL_MONTH_CELL (self->day_cells[i]), day); } filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->events)); g_assert (GTK_IS_CUSTOM_FILTER (filter)); gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_RANGE]); GCAL_EXIT; } void gcal_month_view_row_set_model (GcalMonthViewRow *self, GListModel *model) { g_return_if_fail (GCAL_IS_MONTH_VIEW_ROW (self)); gtk_filter_list_model_set_model (GTK_FILTER_LIST_MODEL (self->events), model); invalidate_layout_blocks (self); } GList* gcal_month_view_row_get_children_by_uuid (GcalMonthViewRow *self, GcalRecurrenceModType mod, const gchar *uuid) { g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); return gcal_util_filter_children_by_uid_and_modtype (GTK_WIDGET (self), mod, uuid); } GtkWidget* gcal_month_view_row_get_cell_at_x (GcalMonthViewRow *self, gdouble x) { gint column; gint width; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); width = gtk_widget_get_width (GTK_WIDGET (self)); if (x < 0.0 || x > width) return NULL; column = floor ((float) GCAL_N_WEEKDAYS * x / (gdouble) width); if (gtk_widget_get_direction (GTK_WIDGET (self)) != GTK_TEXT_DIR_RTL) return self->day_cells[column]; else return self->day_cells[GCAL_N_WEEKDAYS - column - 1]; } void gcal_month_view_row_update_selection (GcalMonthViewRow *self, GcalRange *selection_range) { g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); for (guint i = 0; i < GCAL_N_WEEKDAYS; i++) { GcalMonthCell *month_cell; GDateTime *cell_date; gboolean selected; month_cell = GCAL_MONTH_CELL (self->day_cells[i]); cell_date = gcal_month_cell_get_date (month_cell); selected = selection_range && gcal_range_contains_datetime (selection_range, cell_date); gcal_month_cell_set_selected (month_cell, selected); } } gboolean gcal_month_view_row_focus_underlying_cell (GcalMonthViewRow *self, GtkWidget *widget) { GtkWidget *cell; g_autoptr (FocusEventData) data = NULL; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); g_assert (gtk_widget_is_ancestor (widget, GTK_WIDGET (self))); data = create_focus_event_data (self, widget); cell = gcal_month_view_row_get_cell_at_x (self, ABS (data->normalized_x)); return gtk_widget_grab_focus (cell); } /** * gcal_month_view_row_focus_overlaying_event: * @self: a #GcalMonthViewRow * @widget: the month cell * * Focuses the overlaying event widget. * * Returns: %TRUE if the event widget grabbed focus */ gboolean gcal_month_view_row_focus_overlaying_event (GcalMonthViewRow *self, GtkWidget *widget) { GtkWidget *picked_widget, *event_widget; graphene_rect_t rect; gdouble x, y; g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); g_assert (GCAL_IS_MONTH_CELL (widget)); g_assert (gtk_widget_is_ancestor (widget, GTK_WIDGET (self))); if (!gtk_widget_compute_bounds (widget, GTK_WIDGET (self), &rect)) g_assert_not_reached (); x = rect.origin.x + rect.size.width / 2; y = rect.origin.y + gcal_month_cell_get_header_height (GCAL_MONTH_CELL (widget)); picked_widget = gtk_widget_pick (GTK_WIDGET (self), x, y, GTK_PICK_DEFAULT); g_assert (GTK_IS_WIDGET (picked_widget)); event_widget = gtk_widget_get_ancestor (picked_widget, GCAL_TYPE_EVENT_WIDGET); g_assert (event_widget == NULL || GCAL_IS_EVENT_WIDGET (event_widget)); if (event_widget) return gtk_widget_grab_focus (event_widget); else return FALSE; } /** * gcal_month_view_row_get_ceiled_height: * @self: a #GcalMonthViewRow * * Gets whether the row is negating the compensated pixel. * * Returns: %TRUE if the row is negating the compensated pixel. */ gboolean gcal_month_view_row_get_ceiled_height (GcalMonthViewRow *self) { g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); return self->ceiled_height; } /** * gcal_month_view_row_set_ceiled_height: * @self: a #GcalMonthViewRow * @ceiled_height: %TRUE if the row should negate the compensated pixel. * * Sets whether the row should negate the compensated pixel. * * See [property@MonthViewRow:ceiled-height] for more details. */ void gcal_month_view_row_set_ceiled_height (GcalMonthViewRow *self, gboolean ceiled_height) { g_assert (GCAL_IS_MONTH_VIEW_ROW (self)); if (self->ceiled_height == ceiled_height) return; self->ceiled_height = ceiled_height; g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CEILED_HEIGHT]); }