views/month-view: Implement drag and drop handling on month view

Replicate the week grid implementation of drag-and-drop to
allow events to be dropped on top of other events in the month view.

Closes: #1092

Co-authored-by: Ryan Oliveira <ryan.oliveiraf@usp.br>
Part-of: <https://gitlab.gnome.org/GNOME/gnome-calendar/-/merge_requests/806>
This commit is contained in:
Kaiky Cintra
2026-08-14 22:24:01 +00:00
committed by Marge Bot
co-authored by Ryan Oliveira
parent 3aed975bf5
commit 3a6d120863
+225
View File
@@ -24,6 +24,7 @@
#include "config.h"
#include "gcal-debug.h"
#include "gcal-event-widget-pool.h"
#include "gcal-gui-utils.h"
#include "gcal-month-cell.h"
#include "gcal-month-popover.h"
#include "gcal-month-view.h"
@@ -82,6 +83,9 @@ struct _GcalMonthView
GtkWidget *last_focused_widget;
GdkModifierType state;
GtkWidget *dnd_cell;
GtkWidget *dnd_widget;
};
static void gcal_view_interface_init (GcalViewInterface *iface);
@@ -790,6 +794,65 @@ focus_month_cell (GcalMonthView *self,
return gtk_widget_grab_focus (new_cell);
}
static GtkWidget *
get_month_cell_at_position (GcalMonthView *self,
double x,
double y)
{
gint height = gtk_widget_get_height (GTK_WIDGET (self));
gint header_height = gtk_widget_get_height (self->header);
gdouble grid_y = MAX (0, y - header_height);
gdouble grid_height = height - header_height;
gdouble row_height = grid_height / N_ROWS_PER_PAGE;
gint row_idx = floor (grid_y / row_height);
row_idx = CLAMP (row_idx, 0, N_ROWS_PER_PAGE - 1);
GtkWidget *row_widget = g_ptr_array_index (self->week_rows, FIRST_VISIBLE_ROW_INDEX + row_idx);
GtkWidget *hovered_cell = gcal_month_view_row_get_cell_at_x (GCAL_MONTH_VIEW_ROW (row_widget), x);
return hovered_cell;
}
static void
move_event_to_date (GcalMonthView *self,
GcalEvent *event,
GDateTime *target_date,
GcalRecurrenceModType mod_type)
{
GcalContext *context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
g_autoptr (GDateTime) new_start = NULL;
g_autoptr (GcalEvent) changed_event = NULL;
GDateTime *start_dt;
GDateTime *end_dt;
GTimeSpan timespan = 0;
changed_event = gcal_event_new_from_event (event);
start_dt = gcal_event_get_date_start (changed_event);
end_dt = gcal_event_get_date_end (changed_event);
if (end_dt && start_dt)
timespan = g_date_time_difference (end_dt, start_dt);
new_start = g_date_time_new (g_date_time_get_timezone (start_dt),
g_date_time_get_year (target_date),
g_date_time_get_month (target_date),
g_date_time_get_day_of_month (target_date),
g_date_time_get_hour (start_dt),
g_date_time_get_minute (start_dt),
g_date_time_get_seconds (start_dt));
gcal_event_set_date_start (changed_event, new_start);
if (end_dt)
{
g_autoptr (GDateTime) new_end = g_date_time_add (new_start, timespan);
gcal_event_set_date_end (changed_event, new_end);
}
gcal_manager_update_event (gcal_context_get_manager (context), changed_event, mod_type);
}
/*
@@ -877,6 +940,129 @@ on_click_gesture_released_cb (GtkGestureClick *click_gesture,
GCAL_EXIT;
}
static GdkDragAction
on_drop_target_motion_cb (GtkDropTarget *drop_target,
gdouble x,
gdouble y,
GcalMonthView *self)
{
GtkWidget *hovered_cell;
GCAL_ENTRY;
hovered_cell = get_month_cell_at_position (self, x, y);
if (self->dnd_cell != hovered_cell)
{
self->dnd_cell = hovered_cell;
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
GCAL_RETURN (GDK_ACTION_COPY);
}
static GdkDragAction
on_drop_target_enter_cb (GtkDropTarget *drop_target,
double x,
double y,
GcalMonthView *self)
{
GCAL_ENTRY;
gtk_widget_set_visible (self->dnd_widget, TRUE);
on_drop_target_motion_cb (drop_target, x, y, self);
GCAL_RETURN (GDK_ACTION_COPY);
}
static void
on_drop_target_leave_cb (GtkDropTarget *drop_target,
GcalMonthView *self)
{
GCAL_ENTRY;
self->dnd_cell = NULL;
gtk_widget_set_visible (self->dnd_widget, FALSE);
gtk_widget_queue_allocate (GTK_WIDGET (self));
GCAL_EXIT;
}
typedef struct
{
GcalMonthView *self;
GcalEvent *event;
GDateTime *target_date;
} MonthDropData;
static void
on_recurrence_response_cb (GcalEvent *event,
GcalRecurrenceModType mod_type,
gpointer user_data)
{
MonthDropData *data = user_data;
if (mod_type != GCAL_RECURRENCE_MOD_NONE)
move_event_to_date (data->self, data->event, data->target_date, mod_type);
g_clear_object (&data->event);
gcal_clear_date_time (&data->target_date);
g_free (data);
}
static gboolean
on_drop_target_drop_cb (GtkDropTarget *drop_target,
const GValue *value,
double x,
double y,
GcalMonthView *self)
{
GcalEventWidget *event_widget;
GDateTime *target_date = NULL;
GtkWidget *hovered_cell;
GcalEvent *event;
GCAL_ENTRY;
if (!G_VALUE_HOLDS (value, GCAL_TYPE_EVENT_WIDGET))
return FALSE;
event_widget = g_value_get_object (value);
event = gcal_event_widget_get_event (event_widget);
hovered_cell = get_month_cell_at_position (self, x, y);
g_assert (GCAL_IS_MONTH_CELL (hovered_cell));
target_date = gcal_month_cell_get_date (GCAL_MONTH_CELL (hovered_cell));
if (gcal_event_has_recurrence (event))
{
MonthDropData *data;
data = g_new0 (MonthDropData, 1);
data->self = self;
data->event = g_object_ref (event);
data->target_date = g_date_time_ref (target_date);
gcal_utils_ask_recurrence_modification_type (GTK_WIDGET (self),
event,
FALSE,
on_recurrence_response_cb,
data);
}
else
{
move_event_to_date (self, event, target_date, GCAL_RECURRENCE_MOD_THIS_ONLY);
}
self->dnd_cell = NULL;
gtk_widget_set_visible (self->dnd_widget, FALSE);
gtk_widget_queue_allocate (GTK_WIDGET (self));
GCAL_RETURN (TRUE);
}
static void
on_motion_controller_motion_cb (GtkEventControllerMotion *motion_controller,
gdouble x,
@@ -1688,6 +1874,25 @@ gcal_month_view_size_allocate (GtkWidget *widget,
/* Overflow popover */
if (gtk_widget_should_layout (self->overflow.popover))
allocate_overflow_popover (self, width, height, baseline);
if (gtk_widget_should_layout (self->dnd_widget))
{
graphene_point_t p;
GtkAllocation alloc = { 0, 0, 0, 0 };
if (self->dnd_cell && gtk_widget_compute_point (self->dnd_cell,
GTK_WIDGET (self),
&GRAPHENE_POINT_INIT (0, 0),
&p))
{
alloc.x = p.x;
alloc.y = p.y;
alloc.width = gtk_widget_get_width (self->dnd_cell);
alloc.height = gtk_widget_get_height (self->dnd_cell);
}
gtk_widget_size_allocate (self->dnd_widget, &alloc, baseline);
}
}
static void
@@ -1718,6 +1923,10 @@ gcal_month_view_snapshot (GtkWidget *widget,
/* Overflow popover */
gtk_widget_snapshot_child (widget, self->overflow.popover, snapshot);
/* Render the DND highlight on top of the cells and events */
if (self->dnd_widget && gtk_widget_is_visible (self->dnd_widget))
gtk_widget_snapshot_child (widget, self->dnd_widget, snapshot);
}
@@ -1740,6 +1949,7 @@ gcal_month_view_dispose (GObject *object)
g_clear_pointer (&self->week_rows, g_ptr_array_unref);
g_clear_weak_pointer (&self->last_focused_widget);
g_clear_pointer (&self->dnd_widget, gtk_widget_unparent);
G_OBJECT_CLASS (gcal_month_view_parent_class)->dispose (object);
@@ -1849,6 +2059,7 @@ gcal_month_view_class_init (GcalMonthViewClass *klass)
static void
gcal_month_view_init (GcalMonthView *self)
{
GtkDropTarget *drop_target;
g_autoptr (GDateTime) now = NULL;
self->event_widget_pool = gcal_event_widget_pool_new ();
@@ -1885,4 +2096,18 @@ gcal_month_view_init (GcalMonthView *self)
gcal_view_set_date (GCAL_VIEW (self), now);
update_visible_rows (self);
drop_target = gtk_drop_target_new (GCAL_TYPE_EVENT_WIDGET, GDK_ACTION_COPY);
gtk_drop_target_set_preload (drop_target, TRUE);
g_signal_connect (drop_target, "drop", G_CALLBACK (on_drop_target_drop_cb), self);
g_signal_connect (drop_target, "enter", G_CALLBACK (on_drop_target_enter_cb), self);
g_signal_connect (drop_target, "motion", G_CALLBACK (on_drop_target_motion_cb), self);
g_signal_connect (drop_target, "leave", G_CALLBACK (on_drop_target_leave_cb), self);
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drop_target));
self->dnd_widget = gcal_create_drop_target_widget ();
gtk_widget_set_visible (self->dnd_widget, FALSE);
gtk_widget_set_parent (self->dnd_widget, GTK_WIDGET (self));
}