[evolution] I#1645 - Tasks: Support ESTIMATED-DURATION



commit 8aceb0c28d0aefced19b4e0cd3501383bc405ed1
Author: Milan Crha <mcrha redhat com>
Date:   Thu Dec 2 18:44:41 2021 +0100

    I#1645 - Tasks: Support ESTIMATED-DURATION
    
    Closes https://gitlab.gnome.org/GNOME/evolution/-/issues/1645

 po/POTFILES.in                                     |   1 +
 src/calendar/gui/CMakeLists.txt                    |   2 +
 src/calendar/gui/e-cal-component-preview.c         |  21 +
 src/calendar/gui/e-comp-editor-property-parts.c    | 137 ++++++
 src/calendar/gui/e-comp-editor-property-parts.h    |   3 +
 src/calendar/gui/e-comp-editor-task.c              |  15 +-
 src/calendar/gui/e-estimated-duration-entry.c      | 522 +++++++++++++++++++++
 src/calendar/gui/e-estimated-duration-entry.h      |  57 +++
 src/calendar/gui/print.c                           |  31 ++
 .../itip-formatter/itip-view-elements-defines.h    |   1 +
 src/modules/itip-formatter/itip-view.c             |  28 ++
 11 files changed, 815 insertions(+), 3 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index af0c350485..6faa645e63 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -96,6 +96,7 @@ src/calendar/gui/e-comp-editor.c
 src/calendar/gui/e-day-view.c
 src/calendar/gui/e-day-view-time-item.c
 src/calendar/gui/e-day-view-top-item.c
+src/calendar/gui/e-estimated-duration-entry.c
 src/calendar/gui/e-meeting-list-view.c
 src/calendar/gui/e-meeting-store.c
 src/calendar/gui/e-meeting-time-sel.c
diff --git a/src/calendar/gui/CMakeLists.txt b/src/calendar/gui/CMakeLists.txt
index 7514dc3de3..657f70aa69 100644
--- a/src/calendar/gui/CMakeLists.txt
+++ b/src/calendar/gui/CMakeLists.txt
@@ -61,6 +61,7 @@ set(SOURCES
        e-day-view-main-item.c
        e-day-view-time-item.c
        e-day-view-top-item.c
+       e-estimated-duration-entry.c
        e-meeting-attendee.c
        e-meeting-list-view.c
        e-meeting-store.c
@@ -130,6 +131,7 @@ set(HEADERS
        e-comp-editor-property-part.h
        e-comp-editor-property-parts.h
        e-comp-editor-task.h
+       e-estimated-duration-entry.h
        e-date-time-list.h
        e-day-view-layout.h
        e-day-view-main-item.h
diff --git a/src/calendar/gui/e-cal-component-preview.c b/src/calendar/gui/e-cal-component-preview.c
index a27abafed4..c50371e33a 100644
--- a/src/calendar/gui/e-cal-component-preview.c
+++ b/src/calendar/gui/e-cal-component-preview.c
@@ -318,6 +318,27 @@ cal_component_preview_write_html (ECalComponentPreview *preview,
 
        icomp = e_cal_component_get_icalcomponent (comp);
 
+       prop = i_cal_component_get_first_property (icomp, I_CAL_ESTIMATEDDURATION_PROPERTY);
+       if (prop) {
+               ICalDuration *duration;
+
+               duration = i_cal_property_get_estimatedduration (prop);
+
+               if (duration) {
+                       gint seconds;
+
+                       seconds = i_cal_duration_as_int (duration);
+                       if (seconds > 0) {
+                               str = e_cal_util_seconds_to_string (seconds);
+                               cal_component_preview_add_table_line (buffer, _("Estimated duration:"), str);
+                               g_free (str);
+                       }
+               }
+
+               g_clear_object (&duration);
+               g_object_unref (prop);
+       }
+
        if (e_cal_util_component_has_recurrences (icomp)) {
                str = e_cal_recur_describe_recurrence_ex (icomp,
                        calendar_config_get_week_start_day (),
diff --git a/src/calendar/gui/e-comp-editor-property-parts.c b/src/calendar/gui/e-comp-editor-property-parts.c
index 8278590f23..99e993dbb9 100644
--- a/src/calendar/gui/e-comp-editor-property-parts.c
+++ b/src/calendar/gui/e-comp-editor-property-parts.c
@@ -28,6 +28,7 @@
 #include "comp-util.h"
 #include "e-cal-model.h"
 #include "e-timezone-entry.h"
+#include "e-estimated-duration-entry.h"
 
 #include "e-comp-editor-property-part.h"
 #include "e-comp-editor-property-parts.h"
@@ -2424,3 +2425,139 @@ e_comp_editor_property_part_color_new (void)
 {
        return g_object_new (E_TYPE_COMP_EDITOR_PROPERTY_PART_COLOR, NULL);
 }
+
+/* ************************************************************************* */
+
+#define E_TYPE_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION \
+       (e_comp_editor_property_part_estimated_duration_get_type ())
+#define E_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION, 
ECompEditorPropertyParteEtimatedDuration))
+#define E_IS_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION))
+
+typedef struct _ECompEditorPropertyPartEstimatedDuration ECompEditorPropertyPartEstimatedDuration;
+typedef struct _ECompEditorPropertyPartEstimatedDurationClass ECompEditorPropertyPartEstimatedDurationClass;
+
+struct _ECompEditorPropertyPartEstimatedDuration {
+       ECompEditorPropertyPart parent;
+};
+
+struct _ECompEditorPropertyPartEstimatedDurationClass {
+       ECompEditorPropertyPartClass parent_class;
+};
+
+GType e_comp_editor_property_part_estimated_duration_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_TYPE (ECompEditorPropertyPartEstimatedDuration, e_comp_editor_property_part_estimated_duration, 
E_TYPE_COMP_EDITOR_PROPERTY_PART)
+
+static void
+ecepp_estimated_duration_create_widgets (ECompEditorPropertyPart *property_part,
+                                        GtkWidget **out_label_widget,
+                                        GtkWidget **out_edit_widget)
+{
+       g_return_if_fail (E_IS_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION (property_part));
+       g_return_if_fail (out_label_widget != NULL);
+       g_return_if_fail (out_edit_widget != NULL);
+
+       *out_label_widget = gtk_label_new_with_mnemonic (_("Esti_mated duration:"));
+
+       g_object_set (G_OBJECT (*out_label_widget),
+               "hexpand", FALSE,
+               "halign", GTK_ALIGN_END,
+               "vexpand", FALSE,
+               "valign", GTK_ALIGN_CENTER,
+               NULL);
+
+       gtk_widget_show (*out_label_widget);
+
+       *out_edit_widget = e_estimated_duration_entry_new ();
+       gtk_widget_show (*out_edit_widget);
+
+       gtk_label_set_mnemonic_widget (GTK_LABEL (*out_label_widget), *out_edit_widget);
+
+       g_signal_connect_swapped (*out_edit_widget, "changed",
+               G_CALLBACK (e_comp_editor_property_part_emit_changed), property_part);
+}
+
+static void
+ecepp_estimated_duration_fill_widget (ECompEditorPropertyPart *property_part,
+                                     ICalComponent *component)
+{
+       GtkWidget *edit_widget;
+       ICalProperty *prop;
+
+       g_return_if_fail (E_IS_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION (property_part));
+
+       edit_widget = e_comp_editor_property_part_get_edit_widget (property_part);
+       g_return_if_fail (E_IS_ESTIMATED_DURATION_ENTRY (edit_widget));
+
+       prop = i_cal_component_get_first_property (component, I_CAL_ESTIMATEDDURATION_PROPERTY);
+       if (prop) {
+               ICalDuration *duration = i_cal_property_get_estimatedduration (prop);
+
+               e_estimated_duration_entry_set_value (E_ESTIMATED_DURATION_ENTRY (edit_widget), duration);
+
+               g_clear_object (&duration);
+               g_clear_object (&prop);
+       } else {
+               e_estimated_duration_entry_set_value (E_ESTIMATED_DURATION_ENTRY (edit_widget), NULL);
+       }
+}
+
+static void
+ecepp_estimated_duration_fill_component (ECompEditorPropertyPart *property_part,
+                                        ICalComponent *component)
+{
+       GtkWidget *edit_widget;
+       ICalProperty *prop;
+       ICalDuration *duration;
+
+       g_return_if_fail (E_IS_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION (property_part));
+
+       edit_widget = e_comp_editor_property_part_get_edit_widget (property_part);
+       g_return_if_fail (E_IS_ESTIMATED_DURATION_ENTRY (edit_widget));
+
+       duration = e_estimated_duration_entry_get_value (E_ESTIMATED_DURATION_ENTRY (edit_widget));
+
+       prop = i_cal_component_get_first_property (component, I_CAL_ESTIMATEDDURATION_PROPERTY);
+
+       if (duration) {
+               if (prop) {
+                       i_cal_property_set_estimatedduration (prop, duration);
+               } else {
+                       prop = i_cal_property_new_estimatedduration (duration);
+                       i_cal_component_add_property (component, prop);
+               }
+       } else {
+               if (prop)
+                       i_cal_component_remove_property (component, prop);
+       }
+
+       g_clear_object (&prop);
+}
+
+static void
+e_comp_editor_property_part_estimated_duration_init (ECompEditorPropertyPartEstimatedDuration 
*part_estimated_duration)
+{
+}
+
+static void
+e_comp_editor_property_part_estimated_duration_class_init (ECompEditorPropertyPartEstimatedDurationClass 
*klass)
+{
+       ECompEditorPropertyPartClass *part_class;
+
+       part_class = E_COMP_EDITOR_PROPERTY_PART_CLASS (klass);
+       part_class->create_widgets = ecepp_estimated_duration_create_widgets;
+       part_class->fill_widget = ecepp_estimated_duration_fill_widget;
+       part_class->fill_component = ecepp_estimated_duration_fill_component;
+}
+
+ECompEditorPropertyPart *
+e_comp_editor_property_part_estimated_duration_new (void)
+{
+       return g_object_new (E_TYPE_COMP_EDITOR_PROPERTY_PART_ESTIMATED_DURATION, NULL);
+}
+
+/* ************************************************************************* */
diff --git a/src/calendar/gui/e-comp-editor-property-parts.h b/src/calendar/gui/e-comp-editor-property-parts.h
index 5a3954f337..e0cf5114e7 100644
--- a/src/calendar/gui/e-comp-editor-property-parts.h
+++ b/src/calendar/gui/e-comp-editor-property-parts.h
@@ -62,6 +62,9 @@ ECompEditorPropertyPart *
                e_comp_editor_property_part_transparency_new    (void);
 ECompEditorPropertyPart *
                e_comp_editor_property_part_color_new           (void);
+ECompEditorPropertyPart *
+               e_comp_editor_property_part_estimated_duration_new
+                                                               (void);
 
 G_END_DECLS
 
diff --git a/src/calendar/gui/e-comp-editor-task.c b/src/calendar/gui/e-comp-editor-task.c
index 81ee16cb0c..3fde93a7ac 100644
--- a/src/calendar/gui/e-comp-editor-task.c
+++ b/src/calendar/gui/e-comp-editor-task.c
@@ -45,6 +45,7 @@ struct _ECompEditorTaskPrivate {
        ECompEditorPropertyPart *completed_date;
        ECompEditorPropertyPart *percentcomplete;
        ECompEditorPropertyPart *status;
+       ECompEditorPropertyPart *estimated_duration;
        ECompEditorPropertyPart *timezone;
        ECompEditorPropertyPart *description;
 
@@ -221,6 +222,7 @@ ece_task_notify_target_client_cb (GObject *object,
        gboolean was_allday;
        gboolean can_recur;
        gboolean can_reminders;
+       gboolean can_estimated_duration;
 
        g_return_if_fail (E_IS_COMP_EDITOR_TASK (object));
 
@@ -256,6 +258,9 @@ ece_task_notify_target_client_cb (GObject *object,
 
        can_recur = !cal_client || e_client_check_capability (E_CLIENT (cal_client), 
E_CAL_STATIC_CAPABILITY_TASK_CAN_RECUR);
        gtk_widget_set_visible (GTK_WIDGET (task_editor->priv->recurrence_page), can_recur);
+
+       can_estimated_duration = !cal_client || e_client_check_capability (E_CLIENT (cal_client), 
E_CAL_STATIC_CAPABILITY_TASK_ESTIMATED_DURATION);
+       e_comp_editor_property_part_set_visible (task_editor->priv->estimated_duration, 
can_estimated_duration);
 }
 
 static void
@@ -964,16 +969,20 @@ e_comp_editor_task_constructed (GObject *object)
        part = e_comp_editor_property_part_classification_new ();
        e_comp_editor_page_add_property_part (page, part, 2, 7, 2, 1);
 
-       part = e_comp_editor_property_part_timezone_new ();
+       part = e_comp_editor_property_part_estimated_duration_new ();
        e_comp_editor_page_add_property_part (page, part, 0, 8, 4, 1);
+       task_editor->priv->estimated_duration = part;
+
+       part = e_comp_editor_property_part_timezone_new ();
+       e_comp_editor_page_add_property_part (page, part, 0, 9, 4, 1);
        task_editor->priv->timezone = part;
 
        part = e_comp_editor_property_part_categories_new (focus_tracker);
-       e_comp_editor_page_add_property_part (page, part, 0, 9, 4, 1);
+       e_comp_editor_page_add_property_part (page, part, 0, 10, 4, 1);
        task_editor->priv->categories = part;
 
        part = e_comp_editor_property_part_description_new (focus_tracker);
-       e_comp_editor_page_add_property_part (page, part, 0, 10, 4, 1);
+       e_comp_editor_page_add_property_part (page, part, 0, 11, 4, 1);
        task_editor->priv->description = part;
 
        e_comp_editor_add_page (comp_editor, C_("ECompEditorPage", "General"), page);
diff --git a/src/calendar/gui/e-estimated-duration-entry.c b/src/calendar/gui/e-estimated-duration-entry.c
new file mode 100644
index 0000000000..168831ec30
--- /dev/null
+++ b/src/calendar/gui/e-estimated-duration-entry.c
@@ -0,0 +1,522 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2021 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "e-estimated-duration-entry.h"
+
+struct _EEstimatedDurationEntryPrivate {
+       ICalDuration *value;
+
+       GtkWidget *popover;
+       GtkWidget *days_spin;
+       GtkWidget *hours_spin;
+       GtkWidget *minutes_spin;
+       GtkWidget *set_button;
+       GtkWidget *unset_button;
+       GtkSizeGroup *size_group;
+
+       GtkWidget *entry;
+       GtkWidget *button;
+};
+
+enum {
+       PROP_0,
+       PROP_VALUE
+};
+
+enum {
+       CHANGED,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_PRIVATE (EEstimatedDurationEntry, e_estimated_duration_entry, GTK_TYPE_BOX)
+
+static void
+estimated_duration_entry_emit_changed (EEstimatedDurationEntry *self)
+{
+       g_signal_emit (self, signals[CHANGED], 0);
+}
+
+static void
+estimated_duration_entry_update_entry (EEstimatedDurationEntry *self)
+{
+       gchar *tmp = NULL;
+       ICalDuration *value;
+
+       value = e_estimated_duration_entry_get_value (self);
+
+       if (value != NULL) {
+               gint64 seconds = i_cal_duration_as_int (value);
+
+               if (seconds > 0)
+                       tmp = e_cal_util_seconds_to_string (seconds);
+       }
+
+       gtk_entry_set_text (GTK_ENTRY (self->priv->entry), tmp ? tmp : C_("estimated-duration", "None"));
+
+       g_free (tmp);
+}
+
+static void
+estimated_duration_entry_add_relation (EEstimatedDurationEntry *self)
+{
+       AtkObject *a11y_estimated_duration_entry;
+       AtkObject *a11y_widget;
+       AtkRelationSet *set;
+       AtkRelation *relation;
+       GtkWidget *widget;
+       GPtrArray *target;
+       gpointer target_object;
+
+       /* add a labelled_by relation for widget for accessibility */
+
+       widget = GTK_WIDGET (self);
+       a11y_estimated_duration_entry = gtk_widget_get_accessible (widget);
+
+       widget = self->priv->entry;
+       a11y_widget = gtk_widget_get_accessible (widget);
+
+       set = atk_object_ref_relation_set (a11y_widget);
+       if (set != NULL) {
+               relation = atk_relation_set_get_relation_by_type (set, ATK_RELATION_LABELLED_BY);
+               /* check whether has a labelled_by relation already */
+               if (relation != NULL) {
+                       g_object_unref (set);
+                       return;
+               }
+       }
+
+       g_clear_object (&set);
+
+       set = atk_object_ref_relation_set (a11y_estimated_duration_entry);
+       if (!set)
+               return;
+
+       relation = atk_relation_set_get_relation_by_type (set, ATK_RELATION_LABELLED_BY);
+       if (relation != NULL) {
+               target = atk_relation_get_target (relation);
+               target_object = g_ptr_array_index (target, 0);
+               if (ATK_IS_OBJECT (target_object)) {
+                       atk_object_add_relationship (
+                               a11y_widget,
+                               ATK_RELATION_LABELLED_BY,
+                               ATK_OBJECT (target_object));
+               }
+       }
+
+       g_clear_object (&set);
+}
+
+static void
+estimated_duration_set_button_clicked_cb (GtkButton *button,
+                                         gpointer user_data)
+{
+       EEstimatedDurationEntry *self = user_data;
+       ICalDuration *duration;
+       gint new_minutes;
+
+       g_return_if_fail (E_IS_ESTIMATED_DURATION_ENTRY (self));
+
+       new_minutes =
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->priv->minutes_spin)) +
+               (60 * gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->priv->hours_spin))) +
+               (24 * 60 * gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->priv->days_spin)));
+       g_return_if_fail (new_minutes > 0);
+
+       gtk_widget_hide (self->priv->popover);
+
+       duration = i_cal_duration_new_from_int (60 * new_minutes);
+       e_estimated_duration_entry_set_value (self, duration);
+       g_clear_object (&duration);
+}
+
+static void
+estimated_duration_unset_button_clicked_cb (GtkButton *button,
+                                           gpointer user_data)
+{
+       EEstimatedDurationEntry *self = user_data;
+
+       g_return_if_fail (E_IS_ESTIMATED_DURATION_ENTRY (self));
+
+       gtk_widget_hide (self->priv->popover);
+
+       e_estimated_duration_entry_set_value (self, NULL);
+}
+
+static void
+estimated_duration_update_sensitize_cb (GtkSpinButton *spin,
+                                       gpointer user_data)
+{
+       EEstimatedDurationEntry *self = user_data;
+
+       g_return_if_fail (E_IS_ESTIMATED_DURATION_ENTRY (self));
+
+       gtk_widget_set_sensitive (self->priv->set_button,
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->priv->minutes_spin)) +
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->priv->hours_spin)) +
+               gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->priv->days_spin)) > 0);
+}
+
+static void
+estimated_duration_entry_button_clicked_cb (EEstimatedDurationEntry *self)
+{
+       gint value;
+
+       if (!self->priv->popover) {
+               GtkWidget *widget;
+               GtkBox *vbox, *box;
+
+               self->priv->days_spin = gtk_spin_button_new_with_range (0.0, 366.0, 1.0);
+               self->priv->hours_spin = gtk_spin_button_new_with_range (0.0, 23.0, 1.0);
+               self->priv->minutes_spin = gtk_spin_button_new_with_range (0.0, 59.0, 1.0);
+
+               g_object_set (G_OBJECT (self->priv->days_spin),
+                       "digits", 0,
+                       "numeric", TRUE,
+                       "snap-to-ticks", TRUE,
+                       NULL);
+
+               g_object_set (G_OBJECT (self->priv->hours_spin),
+                       "digits", 0,
+                       "numeric", TRUE,
+                       "snap-to-ticks", TRUE,
+                       NULL);
+
+               g_object_set (G_OBJECT (self->priv->minutes_spin),
+                       "digits", 0,
+                       "numeric", TRUE,
+                       "snap-to-ticks", TRUE,
+                       NULL);
+
+               vbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 2));
+
+               widget = gtk_label_new (_("Set an estimated duration for"));
+               gtk_box_pack_start (vbox, widget, FALSE, FALSE, 0);
+
+               box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+               g_object_set (G_OBJECT (box),
+                       "halign", GTK_ALIGN_START,
+                       "hexpand", FALSE,
+                       "valign", GTK_ALIGN_CENTER,
+                       "vexpand", FALSE,
+                       NULL);
+
+               gtk_box_pack_start (box, self->priv->days_spin, FALSE, FALSE, 4);
+               /* Translators: this is part of: "Set an estimated duration for [nnn] days [nnn] hours [nnn] 
minutes", where the text in "[]" means a separate widget */
+               widget = gtk_label_new_with_mnemonic (C_("estimated-duration", "_days"));
+               gtk_label_set_mnemonic_widget (GTK_LABEL (widget), self->priv->days_spin);
+               gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+               gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+               box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+               g_object_set (G_OBJECT (box),
+                       "halign", GTK_ALIGN_START,
+                       "hexpand", FALSE,
+                       "valign", GTK_ALIGN_CENTER,
+                       "vexpand", FALSE,
+                       NULL);
+
+               gtk_box_pack_start (box, self->priv->hours_spin, FALSE, FALSE, 4);
+               /* Translators: this is part of: "Set an estimated duration for [nnn] days [nnn] hours [nnn] 
minutes", where the text in "[]" means a separate widget */
+               widget = gtk_label_new_with_mnemonic (C_("estimated-duration", "_hours"));
+               gtk_label_set_mnemonic_widget (GTK_LABEL (widget), self->priv->hours_spin);
+               gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+               gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+               box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+               g_object_set (G_OBJECT (box),
+                       "halign", GTK_ALIGN_START,
+                       "hexpand", FALSE,
+                       "valign", GTK_ALIGN_CENTER,
+                       "vexpand", FALSE,
+                       NULL);
+
+               gtk_box_pack_start (box, self->priv->minutes_spin, FALSE, FALSE, 4);
+               /* Translators: this is part of: "Set an estimated duration for [nnn] days [nnn] hours [nnn] 
minutes", where the text in "[]" means a separate widget */
+               widget = gtk_label_new_with_mnemonic (C_("estimated-duration", "_minutes"));
+               gtk_label_set_mnemonic_widget (GTK_LABEL (widget), self->priv->minutes_spin);
+               gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+               gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+               box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+               g_object_set (G_OBJECT (box),
+                       "halign", GTK_ALIGN_CENTER,
+                       "hexpand", TRUE,
+                       "valign", GTK_ALIGN_CENTER,
+                       "vexpand", FALSE,
+                       NULL);
+
+               self->priv->unset_button = gtk_button_new_with_mnemonic (_("_Unset"));
+               g_object_set (G_OBJECT (self->priv->unset_button),
+                       "halign", GTK_ALIGN_CENTER,
+                       NULL);
+
+               gtk_box_pack_start (box, self->priv->unset_button, FALSE, FALSE, 1);
+
+               self->priv->set_button = gtk_button_new_with_mnemonic (_("_Set"));
+               g_object_set (G_OBJECT (self->priv->set_button),
+                       "halign", GTK_ALIGN_CENTER,
+                       NULL);
+
+               gtk_box_pack_start (box, self->priv->set_button, FALSE, FALSE, 1);
+
+               gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+               self->priv->size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+               gtk_size_group_add_widget (self->priv->size_group, self->priv->unset_button);
+               gtk_size_group_add_widget (self->priv->size_group, self->priv->set_button);
+
+               gtk_widget_show_all (GTK_WIDGET (vbox));
+
+               self->priv->popover = gtk_popover_new (GTK_WIDGET (self));
+               gtk_popover_set_position (GTK_POPOVER (self->priv->popover), GTK_POS_BOTTOM);
+               gtk_container_add (GTK_CONTAINER (self->priv->popover), GTK_WIDGET (vbox));
+               gtk_container_set_border_width (GTK_CONTAINER (vbox), 6);
+
+               g_signal_connect (self->priv->set_button, "clicked",
+                       G_CALLBACK (estimated_duration_set_button_clicked_cb), self);
+
+               g_signal_connect (self->priv->unset_button, "clicked",
+                       G_CALLBACK (estimated_duration_unset_button_clicked_cb), self);
+
+               g_signal_connect (self->priv->days_spin, "value-changed",
+                       G_CALLBACK (estimated_duration_update_sensitize_cb), self);
+
+               g_signal_connect (self->priv->hours_spin, "value-changed",
+                       G_CALLBACK (estimated_duration_update_sensitize_cb), self);
+
+               g_signal_connect (self->priv->minutes_spin, "value-changed",
+                       G_CALLBACK (estimated_duration_update_sensitize_cb), self);
+       }
+
+       value = self->priv->value ? i_cal_duration_as_int (self->priv->value) : 0;
+
+       /* seconds are ignored */
+       value = value / 60;
+
+       gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->priv->minutes_spin), value % 60);
+
+       value = value / 60;
+       gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->priv->hours_spin), value % 24);
+
+       value = value / 24;
+       gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->priv->days_spin), value);
+
+       gtk_widget_hide (self->priv->popover);
+       gtk_popover_set_relative_to (GTK_POPOVER (self->priv->popover), self->priv->entry);
+       gtk_widget_show (self->priv->popover);
+
+       gtk_widget_grab_focus (self->priv->days_spin);
+
+       estimated_duration_update_sensitize_cb (NULL, self);
+}
+
+static void
+estimated_duration_entry_set_property (GObject *object,
+                                      guint property_id,
+                                      const GValue *value,
+                                      GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_VALUE:
+                       e_estimated_duration_entry_set_value (
+                               E_ESTIMATED_DURATION_ENTRY (object),
+                               g_value_get_object (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+estimated_duration_entry_get_property (GObject *object,
+                                      guint property_id,
+                                      GValue *value,
+                                      GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_VALUE:
+                       g_value_set_object (
+                               value, e_estimated_duration_entry_get_value (
+                               E_ESTIMATED_DURATION_ENTRY (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+estimated_duration_entry_get_finalize (GObject *object)
+{
+       EEstimatedDurationEntry *self = E_ESTIMATED_DURATION_ENTRY (object);
+
+       g_clear_object (&self->priv->value);
+       g_clear_object (&self->priv->size_group);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_estimated_duration_entry_parent_class)->finalize (object);
+}
+
+static gboolean
+estimated_duration_entry_mnemonic_activate (GtkWidget *widget,
+                                           gboolean group_cycling)
+{
+       EEstimatedDurationEntry *self = E_ESTIMATED_DURATION_ENTRY (widget);
+
+       if (gtk_widget_get_can_focus (widget)) {
+               if (self->priv->button != NULL)
+                       gtk_widget_grab_focus (self->priv->button);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+estimated_duration_entry_focus (GtkWidget *widget,
+                               GtkDirectionType direction)
+{
+       EEstimatedDurationEntry *self = E_ESTIMATED_DURATION_ENTRY (widget);
+
+       if (direction == GTK_DIR_TAB_FORWARD) {
+               if (gtk_widget_has_focus (self->priv->entry))
+                       gtk_widget_grab_focus (self->priv->button);
+               else if (gtk_widget_has_focus (self->priv->button))
+                       return FALSE;
+               else if (gtk_widget_get_visible (self->priv->entry))
+                       gtk_widget_grab_focus (self->priv->entry);
+               else
+                       gtk_widget_grab_focus (self->priv->button);
+
+       } else if (direction == GTK_DIR_TAB_BACKWARD) {
+               if (gtk_widget_has_focus (self->priv->entry))
+                       return FALSE;
+               else if (gtk_widget_has_focus (self->priv->button)) {
+                       if (gtk_widget_get_visible (self->priv->entry))
+                               gtk_widget_grab_focus (self->priv->entry);
+                       else
+                               return FALSE;
+               } else
+                       gtk_widget_grab_focus (self->priv->button);
+       } else
+               return FALSE;
+
+       return TRUE;
+}
+
+static void
+e_estimated_duration_entry_class_init (EEstimatedDurationEntryClass *klass)
+{
+       GObjectClass *object_class;
+       GtkWidgetClass *widget_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->set_property = estimated_duration_entry_set_property;
+       object_class->get_property = estimated_duration_entry_get_property;
+       object_class->finalize = estimated_duration_entry_get_finalize;
+
+       widget_class = GTK_WIDGET_CLASS (klass);
+       widget_class->mnemonic_activate = estimated_duration_entry_mnemonic_activate;
+       widget_class->focus = estimated_duration_entry_focus;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_VALUE,
+               g_param_spec_object (
+                       "value",
+                       "Value",
+                       NULL,
+                       I_CAL_TYPE_DURATION,
+                       G_PARAM_READWRITE));
+
+       signals[CHANGED] = g_signal_new (
+               "changed",
+               G_TYPE_FROM_CLASS (object_class),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (EEstimatedDurationEntryClass, changed),
+               NULL, NULL,
+               g_cclosure_marshal_VOID__VOID,
+               G_TYPE_NONE, 0);
+}
+
+static void
+e_estimated_duration_entry_init (EEstimatedDurationEntry *self)
+{
+       GtkWidget *widget;
+
+       self->priv = e_estimated_duration_entry_get_instance_private (self);
+
+       gtk_widget_set_can_focus (GTK_WIDGET (self), TRUE);
+       gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL);
+
+       widget = gtk_entry_new ();
+       gtk_editable_set_editable (GTK_EDITABLE (widget), FALSE);
+       gtk_box_pack_start (GTK_BOX (self), widget, TRUE, TRUE, 0);
+       self->priv->entry = widget;
+       gtk_widget_show (widget);
+
+       g_signal_connect_swapped (
+               widget, "changed",
+               G_CALLBACK (estimated_duration_entry_emit_changed), self);
+
+       widget = gtk_button_new_with_mnemonic (_("Chan_ge…"));
+       gtk_box_pack_start (GTK_BOX (self), widget, FALSE, FALSE, 6);
+       self->priv->button = widget;
+       gtk_widget_show (widget);
+
+       g_signal_connect_swapped (
+               widget, "clicked",
+               G_CALLBACK (estimated_duration_entry_button_clicked_cb), self);
+
+       estimated_duration_entry_update_entry (self);
+}
+
+GtkWidget *
+e_estimated_duration_entry_new (void)
+{
+       return g_object_new (E_TYPE_ESTIMATED_DURATION_ENTRY, NULL);
+}
+
+ICalDuration *
+e_estimated_duration_entry_get_value (EEstimatedDurationEntry *self)
+{
+       g_return_val_if_fail (E_IS_ESTIMATED_DURATION_ENTRY (self), NULL);
+
+       return self->priv->value;
+}
+
+void
+e_estimated_duration_entry_set_value (EEstimatedDurationEntry *self,
+                                     const ICalDuration *value)
+{
+       g_return_if_fail (E_IS_ESTIMATED_DURATION_ENTRY (self));
+
+       if (value && !i_cal_duration_as_int ((ICalDuration *) value))
+               value = NULL;
+
+       if (self->priv->value == value)
+               return;
+
+       if (self->priv->value && value && i_cal_duration_as_int (self->priv->value) == i_cal_duration_as_int 
((ICalDuration *) value))
+               return;
+
+       g_clear_object (&self->priv->value);
+       if (value)
+               self->priv->value = i_cal_duration_new_from_int (i_cal_duration_as_int ((ICalDuration *) 
value));
+
+       estimated_duration_entry_update_entry (self);
+       estimated_duration_entry_add_relation (self);
+
+       g_object_notify (G_OBJECT (self), "value");
+}
diff --git a/src/calendar/gui/e-estimated-duration-entry.h b/src/calendar/gui/e-estimated-duration-entry.h
new file mode 100644
index 0000000000..a78e655b2a
--- /dev/null
+++ b/src/calendar/gui/e-estimated-duration-entry.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2021 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_ESTIMATED_DURATION_ENTRY_H
+#define E_ESTIMATED_DURATION_ENTRY_H
+
+#include <gtk/gtk.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_ESTIMATED_DURATION_ENTRY \
+       (e_estimated_duration_entry_get_type ())
+#define E_ESTIMATED_DURATION_ENTRY(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_ESTIMATED_DURATION_ENTRY, EEstimatedDurationEntry))
+#define E_ESTIMATED_DURATION_ENTRY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_ESTIMATED_DURATION_ENTRY, EEstimatedDurationEntryClass))
+#define E_IS_ESTIMATED_DURATION_ENTRY(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_ESTIMATED_DURATION_ENTRY))
+#define E_IS_ESTIMATED_DURATION_ENTRY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_ESTIMATED_DURATION_ENTRY))
+#define E_IS_ESTIMATED_DURATION_ENTRY_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_ESTIMATED_DURATION_ENTRY, EEstimatedDurationEntryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EEstimatedDurationEntry EEstimatedDurationEntry;
+typedef struct _EEstimatedDurationEntryClass EEstimatedDurationEntryClass;
+typedef struct _EEstimatedDurationEntryPrivate EEstimatedDurationEntryPrivate;
+
+struct _EEstimatedDurationEntry {
+       GtkBox parent;
+       EEstimatedDurationEntryPrivate *priv;
+};
+
+struct _EEstimatedDurationEntryClass {
+       GtkBoxClass parent_class;
+
+       void            (*changed)              (EEstimatedDurationEntry *estimated_duration_entry);
+};
+
+GType          e_estimated_duration_entry_get_type     (void);
+GtkWidget *    e_estimated_duration_entry_new          (void);
+ICalDuration * e_estimated_duration_entry_get_value    (EEstimatedDurationEntry *self);
+void           e_estimated_duration_entry_set_value    (EEstimatedDurationEntry *self,
+                                                        const ICalDuration *value);
+
+G_END_DECLS
+
+#endif /* E_ESTIMATED_DURATION_ENTRY_H */
diff --git a/src/calendar/gui/print.c b/src/calendar/gui/print.c
index 1c3bec5bc8..43ef9e2fbe 100644
--- a/src/calendar/gui/print.c
+++ b/src/calendar/gui/print.c
@@ -3660,12 +3660,43 @@ print_comp_draw_real (GtkPrintOperation *operation,
 
        /* For a VTODO we print the Status, Priority, % Complete and URL. */
        if (vtype == E_CAL_COMPONENT_TODO) {
+               ICalComponent *icomp;
+               ICalProperty *prop;
                ICalPropertyStatus status;
                const gchar *status_string = NULL;
                gint percent;
                gint priority;
                gchar *url;
 
+               icomp = e_cal_component_get_icalcomponent (comp);
+
+               /* Estimated duration */
+               prop = i_cal_component_get_first_property (icomp, I_CAL_ESTIMATEDDURATION_PROPERTY);
+               if (prop) {
+                       ICalDuration *duration;
+
+                       duration = i_cal_property_get_estimatedduration (prop);
+
+                       if (duration) {
+                               gint seconds;
+
+                               seconds = i_cal_duration_as_int (duration);
+                               if (seconds > 0) {
+                                       gchar *tmp = e_cal_util_seconds_to_string (seconds);
+                                       gchar *estimated_duration = g_strdup_printf (_("Estimated duration: 
%s"), tmp);
+                                       top = bound_text (
+                                               context, font, estimated_duration, -1,
+                                               0.0, top, width, height, FALSE, NULL, &page_start, &pages);
+                                       top += get_font_size (font) - 6;
+                                       g_free (estimated_duration);
+                                       g_free (tmp);
+                               }
+                       }
+
+                       g_clear_object (&duration);
+                       g_object_unref (prop);
+               }
+
                /* Status */
                status = e_cal_component_get_status (comp);
                if (status != I_CAL_STATUS_NONE) {
diff --git a/src/modules/itip-formatter/itip-view-elements-defines.h 
b/src/modules/itip-formatter/itip-view-elements-defines.h
index 23c4b1912a..c8ba032abc 100644
--- a/src/modules/itip-formatter/itip-view-elements-defines.h
+++ b/src/modules/itip-formatter/itip-view-elements-defines.h
@@ -26,6 +26,7 @@
 #define TABLE_ROW_START_DATE "table_row_start_time"
 #define TABLE_ROW_END_DATE "table_row_end_time"
 #define TABLE_ROW_DUE_DATE "table_row_due_date"
+#define TABLE_ROW_ESTIMATED_DURATION "table_row_estimated_duration"
 #define TABLE_ROW_STATUS "table_row_status"
 #define TABLE_ROW_COMMENT "table_row_comment"
 #define TABLE_ROW_CATEGORIES "table_row_categories"
diff --git a/src/modules/itip-formatter/itip-view.c b/src/modules/itip-formatter/itip-view.c
index 6c611716bc..e4da17c9c0 100644
--- a/src/modules/itip-formatter/itip-view.c
+++ b/src/modules/itip-formatter/itip-view.c
@@ -97,6 +97,7 @@ struct _ItipViewPrivate {
 
        gchar *categories;
        gchar *due_date_label;
+       gchar *estimated_duration;
 
        GSList *upper_info_items;
        GSList *lower_info_items;
@@ -854,6 +855,7 @@ update_start_end_times (ItipView *view)
        g_free (priv->end_label);
        g_free (priv->categories);
        g_free (priv->due_date_label);
+       g_free (priv->estimated_duration);
 
        #define is_same(_member) (priv->start_tm->_member == priv->end_tm->_member)
        if (priv->start_tm && priv->end_tm && priv->start_tm_is_date && priv->end_tm_is_date
@@ -1712,6 +1714,7 @@ itip_view_write (gpointer itip_part_ptr,
        append_text_table_row (buffer, TABLE_ROW_START_DATE, _("Start time:"), NULL);
        append_text_table_row (buffer, TABLE_ROW_END_DATE, _("End time:"), NULL);
        append_text_table_row (buffer, TABLE_ROW_DUE_DATE, _("Due date:"), NULL);
+       append_text_table_row (buffer, TABLE_ROW_ESTIMATED_DURATION, _("Estimated duration:"), NULL);
        append_text_table_row (buffer, TABLE_ROW_STATUS, _("Status:"), NULL);
        append_text_table_row (buffer, TABLE_ROW_COMMENT, _("Comment:"), NULL);
        append_text_table_row (buffer, TABLE_ROW_CATEGORIES, _("Categories:"), NULL);
@@ -1835,6 +1838,9 @@ itip_view_write_for_printing (ItipView *view,
        append_text_table_row_nonempty (
                buffer, TABLE_ROW_DUE_DATE,
                _("Due date:"), view->priv->due_date_label);
+       append_text_table_row_nonempty (
+               buffer, TABLE_ROW_ESTIMATED_DURATION,
+               _("Estimated duration:"), view->priv->estimated_duration);
        append_text_table_row_nonempty (
                buffer, TABLE_ROW_STATUS,
                _("Status:"), view->priv->status);
@@ -6816,6 +6822,28 @@ itip_view_init_view (ItipView *view)
                set_area_text (view, TABLE_ROW_CATEGORIES, view->priv->categories, FALSE);
        }
 
+       g_clear_pointer (&view->priv->estimated_duration, g_free);
+
+       prop = i_cal_component_get_first_property (icomp, I_CAL_ESTIMATEDDURATION_PROPERTY);
+       if (prop) {
+               ICalDuration *duration;
+
+               duration = i_cal_property_get_estimatedduration (prop);
+
+               if (duration) {
+                       gint seconds;
+
+                       seconds = i_cal_duration_as_int (duration);
+                       if (seconds > 0) {
+                               view->priv->estimated_duration = e_cal_util_seconds_to_string (seconds);
+                               set_area_text (view, TABLE_ROW_ESTIMATED_DURATION, 
view->priv->estimated_duration, FALSE);
+                       }
+               }
+
+               g_clear_object (&duration);
+               g_object_unref (prop);
+       }
+
         /* Recurrence info */
        itip_view_add_recurring_info (view);
 


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