[evolution] Calendar: Implement event drag&drop for the Year view



commit 3e9d38d1faf6c324e6637af3b42fcbcfb485ef36
Author: Milan Crha <mcrha redhat com>
Date:   Thu Jun 9 14:01:53 2022 +0200

    Calendar: Implement event drag&drop for the Year view
    
    To be able to copy/move an event to a different day.

 src/calendar/calendar.error.xml  |   7 ++
 src/calendar/gui/comp-util.c     | 112 +++++++++++++++++++++
 src/calendar/gui/comp-util.h     |   9 +-
 src/calendar/gui/e-cal-dialogs.c |  32 ++++++
 src/calendar/gui/e-cal-dialogs.h |   2 +
 src/calendar/gui/e-year-view.c   | 210 +++++++++++++++++++++++++++++++++++++++
 6 files changed, 371 insertions(+), 1 deletion(-)
---
diff --git a/src/calendar/calendar.error.xml b/src/calendar/calendar.error.xml
index 03f71fa705..9a1b2cf6ef 100644
--- a/src/calendar/calendar.error.xml
+++ b/src/calendar/calendar.error.xml
@@ -582,4 +582,11 @@
     <_secondary>A WebKitWebProcess crashed when displaying the task. You can try again by moving to another 
task and back. If the issue persists, please file a bug report in GNOME Gitlab.</_secondary>
   </error>
 
+  <error id="prompt-detach-copy-event" type="question" default="GTK_RESPONSE_NO">
+    <_primary>Do you want to detach event from the series?</_primary>
+    <_secondary>The event “{0}” will be detached from its series and saved as a new standalone 
event.</_secondary>
+    <button _label="_Cancel" response="GTK_RESPONSE_NO"/>
+    <button _label="_Detach and Create Copy" response="GTK_RESPONSE_YES"/>
+  </error>
+
 </error-list>
diff --git a/src/calendar/gui/comp-util.c b/src/calendar/gui/comp-util.c
index d0630d744e..0708e96dbd 100644
--- a/src/calendar/gui/comp-util.c
+++ b/src/calendar/gui/comp-util.c
@@ -31,6 +31,8 @@
 #include "calendar-config.h"
 #include "comp-util.h"
 #include "e-calendar-view.h"
+#include "e-cal-dialogs.h"
+#include "e-cal-ops.h"
 #include "itip-utils.h"
 
 #include "shell/e-shell-window.h"
@@ -2532,3 +2534,113 @@ cal_comp_util_get_attendee_email (const ECalComponentAttendee *attendee)
                e_cal_component_attendee_get_value (attendee),
                e_cal_component_attendee_get_parameter_bag (attendee));
 }
+
+/* moves the @comp by @days days, preserving its time and duration */
+gboolean
+cal_comp_util_move_component_by_days (GtkWindow *parent,
+                                     ECalModel *model,
+                                     ECalClient *client,
+                                     ECalComponent *in_comp,
+                                     gint days,
+                                     gboolean is_move)
+{
+       ECalComponentDateTime *datetime;
+       ECalComponent *comp_copy;
+       ESourceRegistry *registry;
+       ICalTime *itt;
+       GtkResponseType send = GTK_RESPONSE_NO;
+       gboolean only_new_attendees = FALSE;
+       gboolean strip_alarms = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_MODEL (model), FALSE);
+       g_return_val_if_fail (E_IS_CAL_CLIENT (client), FALSE);
+       g_return_val_if_fail (E_IS_CAL_COMPONENT (in_comp), FALSE);
+       g_return_val_if_fail (days != 0, FALSE);
+
+       registry = e_cal_model_get_registry (model);
+
+       if (e_cal_component_has_attendees (in_comp) &&
+           !itip_organizer_is_user (registry, in_comp, client)) {
+               /* Can continue with the next component */
+               return TRUE;
+       }
+
+       if (itip_has_any_attendees (in_comp) &&
+           (itip_organizer_is_user (registry, in_comp, client) ||
+            itip_sentby_is_user (registry, in_comp, client)))
+               send = e_cal_dialogs_send_dragged_or_resized_component (parent, client, in_comp, 
&strip_alarms, &only_new_attendees);
+
+       if (send == GTK_RESPONSE_CANCEL)
+               return FALSE;
+
+       comp_copy = e_cal_component_clone (in_comp);
+
+       datetime = e_cal_component_get_dtstart (comp_copy);
+       itt = e_cal_component_datetime_get_value (datetime);
+       i_cal_time_adjust (itt, days, 0, 0, 0);
+       cal_comp_set_dtstart_with_oldzone (client, comp_copy, datetime);
+       e_cal_component_datetime_free (datetime);
+
+       datetime = e_cal_component_get_dtend (comp_copy);
+       itt = e_cal_component_datetime_get_value (datetime);
+       i_cal_time_adjust (itt, days, 0, 0, 0);
+       cal_comp_set_dtend_with_oldzone (client, comp_copy, datetime);
+       e_cal_component_datetime_free (datetime);
+
+       e_cal_component_commit_sequence (comp_copy);
+
+       if (is_move) {
+               ECalObjModType mod = E_CAL_OBJ_MOD_ALL;
+
+               if (e_cal_component_has_recurrences (comp_copy)) {
+                       if (!e_cal_dialogs_recur_component (client, comp_copy, &mod, NULL, FALSE)) {
+                               g_clear_object (&comp_copy);
+                               return FALSE;
+                       }
+
+                       if (mod == E_CAL_OBJ_MOD_THIS) {
+                               e_cal_component_set_rdates (comp_copy, NULL);
+                               e_cal_component_set_rrules (comp_copy, NULL);
+                               e_cal_component_set_exdates (comp_copy, NULL);
+                               e_cal_component_set_exrules (comp_copy, NULL);
+                       }
+               } else if (e_cal_component_is_instance (comp_copy)) {
+                       mod = E_CAL_OBJ_MOD_THIS;
+               }
+
+               e_cal_component_commit_sequence (comp_copy);
+
+               e_cal_ops_modify_component (model, client, e_cal_component_get_icalcomponent (comp_copy), mod,
+                       (send == GTK_RESPONSE_YES ? E_CAL_OPS_SEND_FLAG_SEND : E_CAL_OPS_SEND_FLAG_DONT_SEND) 
|
+                       (strip_alarms ? E_CAL_OPS_SEND_FLAG_STRIP_ALARMS : 0) |
+                       (only_new_attendees ? E_CAL_OPS_SEND_FLAG_ONLY_NEW_ATTENDEES : 0));
+       } else {
+               gchar *new_uid;
+
+               if ((e_cal_component_has_recurrences (comp_copy) ||
+                   e_cal_component_is_instance (comp_copy)) &&
+                   !e_cal_dialogs_detach_and_copy (parent, e_cal_component_get_icalcomponent (comp_copy))) {
+                       g_clear_object (&comp_copy);
+                       return FALSE;
+               }
+
+               new_uid = e_util_generate_uid ();
+               e_cal_component_set_uid (comp_copy, new_uid);
+               g_free (new_uid);
+
+               /* Detach the instance from the series and make a new independent component for it */
+               e_cal_component_set_recurid (comp_copy, NULL);
+               e_cal_component_set_rdates (comp_copy, NULL);
+               e_cal_component_set_rrules (comp_copy, NULL);
+               e_cal_component_set_exdates (comp_copy, NULL);
+               e_cal_component_set_exrules (comp_copy, NULL);
+               e_cal_component_commit_sequence (comp_copy);
+
+               e_cal_ops_create_component (model, client, e_cal_component_get_icalcomponent (comp_copy),
+                       NULL, NULL, NULL);
+       }
+
+       g_clear_object (&comp_copy);
+
+       return TRUE;
+}
diff --git a/src/calendar/gui/comp-util.h b/src/calendar/gui/comp-util.h
index 322f2da6cd..3ec120a0a9 100644
--- a/src/calendar/gui/comp-util.h
+++ b/src/calendar/gui/comp-util.h
@@ -29,6 +29,7 @@
 
 #include <e-util/e-util.h>
 #include <calendar/gui/e-cal-data-model.h>
+#include <calendar/gui/e-cal-model.h>
 
 struct _EShell;
 
@@ -218,5 +219,11 @@ const gchar *      cal_comp_util_get_organizer_email
                                                (const ECalComponentOrganizer *organizer);
 const gchar *  cal_comp_util_get_attendee_email
                                                (const ECalComponentAttendee *attendee);
-
+gboolean       cal_comp_util_move_component_by_days
+                                               (GtkWindow *parent,
+                                                ECalModel *model,
+                                                ECalClient *client,
+                                                ECalComponent *in_comp,
+                                                gint days,
+                                                gboolean is_move);
 #endif
diff --git a/src/calendar/gui/e-cal-dialogs.c b/src/calendar/gui/e-cal-dialogs.c
index 08a5f11f40..b176be537e 100644
--- a/src/calendar/gui/e-cal-dialogs.c
+++ b/src/calendar/gui/e-cal-dialogs.c
@@ -1304,3 +1304,35 @@ e_cal_dialogs_send_component_prompt_subject (GtkWindow *parent,
        else
                return FALSE;
 }
+
+gboolean
+e_cal_dialogs_detach_and_copy (GtkWindow *parent,
+                              ICalComponent *component)
+{
+       ICalComponentKind kind;
+       gchar *summary;
+       const gchar *id;
+       gboolean res;
+
+       kind = i_cal_component_isa (component);
+
+       switch (kind) {
+       case I_CAL_VEVENT_COMPONENT:
+               id = "calendar:prompt-detach-copy-event";
+               break;
+
+       case I_CAL_VTODO_COMPONENT:
+       case I_CAL_VJOURNAL_COMPONENT:
+               return TRUE;
+
+       default:
+               g_message ("%s: Cannot handle object of type %d", G_STRFUNC, kind);
+               return FALSE;
+       }
+
+       summary = e_calendar_view_dup_component_summary (component);
+       res = e_alert_run_dialog_for_args (parent, id, summary, NULL) == GTK_RESPONSE_YES;
+       g_free (summary);
+
+       return res;
+}
diff --git a/src/calendar/gui/e-cal-dialogs.h b/src/calendar/gui/e-cal-dialogs.h
index 348d1e6a45..2575d6545e 100644
--- a/src/calendar/gui/e-cal-dialogs.h
+++ b/src/calendar/gui/e-cal-dialogs.h
@@ -72,5 +72,7 @@ GtkResponseType       e_cal_dialogs_send_dragged_or_resized_component
 gboolean       e_cal_dialogs_send_component_prompt_subject
                                                (GtkWindow *parent,
                                                 ICalComponent *icomp);
+gboolean       e_cal_dialogs_detach_and_copy   (GtkWindow *parent,
+                                                ICalComponent *icomp);
 
 #endif /* E_CAL_DIALOGS_H */
diff --git a/src/calendar/gui/e-year-view.c b/src/calendar/gui/e-year-view.c
index 38b3faa43d..27b099f01e 100644
--- a/src/calendar/gui/e-year-view.c
+++ b/src/calendar/gui/e-year-view.c
@@ -18,6 +18,10 @@
 
 /* #define WITH_PREV_NEXT_BUTTONS 1 */
 
+static GtkTargetEntry target_table[] = {
+       { (gchar *) "application/x-e-calendar-event", 0, 0 }
+};
+
 typedef struct _ComponentData {
        ECalClient *client;
        ECalComponent *comp;
@@ -37,6 +41,11 @@ typedef struct _DayData {
        GSList *comps_data; /* ComponentData * */
 } DayData;
 
+typedef struct _DragData {
+       ECalClient *client;
+       ECalComponent *comp;
+} DragData;
+
 struct _EYearViewPrivate {
        ESourceRegistry *registry;
        GHashTable *client_colors; /* ESource * ~> GdkRGBA * */
@@ -62,6 +71,11 @@ struct _EYearViewPrivate {
        guint current_month;
        guint current_year;
 
+       GSList *drag_data; /* DragData * */
+       guint drag_day;
+       guint drag_month;
+       guint drag_year;
+
        /* Track today */
        gboolean highlight_today;
        gboolean today_fix_timeout;
@@ -159,6 +173,31 @@ component_data_equal (gconstpointer ptr1,
                g_strcmp0 (cd1->rid, cd2->rid) == 0;
 }
 
+static DragData *
+drag_data_new (ECalClient *client,
+              ECalComponent *comp)
+{
+       DragData *dd;
+
+       dd = g_slice_new (DragData);
+       dd->client = g_object_ref (client);
+       dd->comp = g_object_ref (comp);
+
+       return dd;
+}
+
+static void
+drag_data_free (gpointer ptr)
+{
+       DragData *dd = ptr;
+
+       if (dd) {
+               g_clear_object (&dd->client);
+               g_clear_object (&dd->comp);
+               g_slice_free (DragData, dd);
+       }
+}
+
 static void
 year_view_calc_component_data (EYearView *self,
                               ComponentData *cd,
@@ -1317,6 +1356,155 @@ year_view_tree_view_row_activated_cb (GtkTreeView *tree_view,
        }
 }
 
+static void
+year_view_tree_view_drag_begin_cb (GtkWidget *tree_view,
+                                  GdkDragContext *context,
+                                  gpointer user_data)
+{
+       EYearView *self = user_data;
+       GtkTreeSelection *selection;
+       cairo_surface_t *surface = NULL;
+       GList *selected, *link;
+       GtkTreeModel *model = NULL;
+       GtkTreeIter iter;
+
+       g_slist_free_full (self->priv->drag_data, drag_data_free);
+       self->priv->drag_data = NULL;
+
+       selection = gtk_tree_view_get_selection (self->priv->tree_view);
+       selected = gtk_tree_selection_get_selected_rows (selection, &model);
+
+       for (link = selected; link; link = g_list_next (link)) {
+               if (gtk_tree_model_get_iter (model, &iter, link->data)) {
+                       ComponentData *cd = NULL;
+
+                       gtk_tree_model_get (model, &iter,
+                               COLUMN_COMPONENT_DATA, &cd,
+                               -1);
+
+                       self->priv->drag_data = g_slist_prepend (self->priv->drag_data,
+                               drag_data_new (cd->client, cd->comp));
+
+                       if (!surface)
+                               surface = gtk_tree_view_create_row_drag_icon (self->priv->tree_view, 
link->data);
+               }
+       }
+
+       g_list_free_full (selected, (GDestroyNotify) gtk_tree_path_free);
+
+       self->priv->drag_data = g_slist_reverse (self->priv->drag_data);
+       self->priv->drag_day = self->priv->current_day;
+       self->priv->drag_month = self->priv->current_month;
+       self->priv->drag_year = self->priv->current_year;
+
+       if (surface) {
+               gtk_drag_set_icon_surface (context, surface);
+               cairo_surface_destroy (surface);
+       }
+}
+
+static void
+year_view_tree_view_drag_end_cb (GtkWidget *widget,
+                                GdkDragContext *context,
+                                gpointer user_data)
+{
+       EYearView *self = user_data;
+
+       g_slist_free_full (self->priv->drag_data, drag_data_free);
+       self->priv->drag_data = NULL;
+       self->priv->drag_day = 0;
+       self->priv->drag_month = 0;
+       self->priv->drag_year = 0;
+}
+
+static gboolean
+year_view_month_drag_motion_cb (GtkWidget *widget,
+                               GdkDragContext *context,
+                               gint x,
+                               gint y,
+                               guint time,
+                               gpointer user_data)
+{
+       EYearView *self = user_data;
+       guint day, year = 0;
+       GDateMonth month = 0;
+       gboolean can_drop;
+
+       day = e_month_widget_get_day_at_position (E_MONTH_WIDGET (widget), x, y);
+       e_month_widget_get_month (E_MONTH_WIDGET (widget), &month, &year);
+
+       can_drop = day != 0 && self->priv->drag_data && (
+               day != self->priv->drag_day ||
+               month != self->priv->drag_month ||
+               year != self->priv->drag_year);
+
+       gdk_drag_status (context,
+               can_drop ? gdk_drag_context_get_selected_action (context) : 0, time);
+
+       return TRUE;
+}
+
+static gboolean
+year_view_month_drag_drop_cb (GtkWidget *widget,
+                             GdkDragContext *context,
+                             gint x,
+                             gint y,
+                             guint time,
+                             gpointer user_data)
+{
+       EYearView *self = user_data;
+       guint day, year = 0;
+       GDateMonth month = 0;
+       gboolean can_drop;
+
+       day = e_month_widget_get_day_at_position (E_MONTH_WIDGET (widget), x, y);
+       e_month_widget_get_month (E_MONTH_WIDGET (widget), &month, &year);
+
+       can_drop = day != 0 && self->priv->drag_data && (
+               day != self->priv->drag_day ||
+               month != self->priv->drag_month ||
+               year != self->priv->drag_year);
+
+       if (can_drop) {
+               GDate *from, *to;
+               gint diff_days;
+
+               from = g_date_new_dmy (self->priv->drag_day, self->priv->drag_month, self->priv->drag_year);
+               to = g_date_new_dmy (day, month, year);
+
+               diff_days = g_date_days_between (from, to);
+
+               if (diff_days != 0) {
+                       ECalModel *model = e_calendar_view_get_model (E_CALENDAR_VIEW (self));
+                       GtkWidget *toplevel;
+                       GtkWindow *parent;
+                       GSList *drag_data, *link;
+                       gboolean is_move;
+
+                       drag_data = g_steal_pointer (&self->priv->drag_data);
+                       toplevel = gtk_widget_get_toplevel (widget);
+                       parent = GTK_IS_WINDOW (toplevel) ? GTK_WINDOW (toplevel) : NULL;
+                       is_move = gdk_drag_context_get_selected_action (context) == GDK_ACTION_MOVE;
+
+                       for (link = drag_data; link; link = g_slist_next (link)) {
+                               DragData *dd = link->data;
+                               if (!cal_comp_util_move_component_by_days (parent, model,
+                                       dd->client, dd->comp, diff_days, is_move))
+                                       break;
+                       }
+
+                       g_slist_free_full (drag_data, drag_data_free);
+               }
+
+               g_date_free (from);
+               g_date_free (to);
+       }
+
+       gdk_drag_status (context, 0, time);
+
+       return FALSE;
+}
+
 static void
 year_view_timezone_changed_cb (GObject *object,
                               GParamSpec *param,
@@ -1569,6 +1757,17 @@ year_view_construct_year_widget (EYearView *self)
 
                e_month_widget_set_month (E_MONTH_WIDGET (widget), ii + 1, self->priv->current_year);
 
+               gtk_drag_dest_set (
+                       widget, GTK_DEST_DEFAULT_ALL,
+                       target_table, G_N_ELEMENTS (target_table),
+                       GDK_ACTION_COPY | GDK_ACTION_MOVE);
+
+               g_signal_connect_object (widget, "drag-motion",
+                       G_CALLBACK (year_view_month_drag_motion_cb), self, 0);
+
+               g_signal_connect_object (widget, "drag-drop",
+                       G_CALLBACK (year_view_month_drag_drop_cb), self, 0);
+
                gtk_container_add (GTK_CONTAINER (container), vbox);
 
                child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (container), ii);
@@ -1819,6 +2018,10 @@ year_view_constructed (GObject *object)
 
        gtk_tree_view_append_column (self->priv->tree_view, column);
 
+       gtk_drag_source_set (GTK_WIDGET (self->priv->tree_view), GDK_BUTTON1_MASK,
+               target_table, G_N_ELEMENTS (target_table),
+               GDK_ACTION_COPY | GDK_ACTION_MOVE);
+
        selection = gtk_tree_view_get_selection (self->priv->tree_view);
 
        g_signal_connect_object (selection, "changed",
@@ -1833,6 +2036,12 @@ year_view_constructed (GObject *object)
        g_signal_connect_object (self->priv->tree_view, "row-activated",
                G_CALLBACK (year_view_tree_view_row_activated_cb), self, 0);
 
+       g_signal_connect_object (self->priv->tree_view, "drag-begin",
+               G_CALLBACK (year_view_tree_view_drag_begin_cb), self, 0);
+
+       g_signal_connect_object (self->priv->tree_view, "drag-end",
+               G_CALLBACK (year_view_tree_view_drag_end_cb), self, 0);
+
        g_signal_connect_object (self->priv->data_model, "notify::timezone",
                G_CALLBACK (year_view_timezone_changed_cb), self, 0);
 
@@ -1902,6 +2111,7 @@ year_view_finalize (GObject *object)
 
        year_view_clear_comps (self);
 
+       g_slist_free_full (self->priv->drag_data, drag_data_free);
        g_hash_table_destroy (self->priv->client_colors);
        g_hash_table_destroy (self->priv->comps);
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]