[california/wip/725786-edit-recurring] Basically working now! Some clean up and error checking remains



commit 44ce56620d93a7570237e18ba773c4baf96efdbb
Author: Jim Nelson <jim yorba org>
Date:   Wed Jul 16 13:45:16 2014 -0700

    Basically working now!  Some clean up and error checking remains

 src/component/component-event.vala         |   11 ++
 src/component/component-instance.vala      |   17 ++++
 src/host/host-create-update-event.vala     |   90 +++++++++---------
 src/host/host-create-update-recurring.vala |  137 ++++++++++++++--------------
 src/host/host-main-window.vala             |    4 +
 src/host/host-quick-create-event.vala      |   28 +++---
 src/host/host-show-event.vala              |   12 ++-
 7 files changed, 173 insertions(+), 126 deletions(-)
---
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index 27e13cc..b09aca1 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -229,6 +229,17 @@ public class Event : Instance, Gee.Comparable<Event> {
     }
     
     /**
+     * @inheritDoc
+     */
+    public override Component.Instance clone() throws Error {
+        Component.Event cloned_event = new Component.Event(calendar_source, ical_component);
+        if (master != null)
+            cloned_event.master = new Component.Event(master.calendar_source, master.ical_component);
+        
+        return cloned_event;
+    }
+    
+    /**
      * Returns a { link Calendar.DateSpan} for the { link Event}.
      *
      * This will return a DateSpan whether the Event is a DATE or DATE-TIME VEVENT.
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index a57c6d0..002dd08 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -19,6 +19,10 @@ namespace California.Component {
  * The second is to update the mutable properties themselves, which will then update the underlying
  * iCal component.
  *
+ * Instances produced by { link Backing.CalendarSourceSubscription}s will be updated by the
+ * subscription if the Instance is updated or removed locally or remotely.  Cloned Instances,
+ * however, are not automatically updated.  See { link clone}.
+ *
  * Alarms will be contained within Instance components.  Timezones are handled separately.
  *
  * Instance also offers a number of methods to convert iCal structures into internal objects.
@@ -350,6 +354,19 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     }
     
     /**
+     * Make a detached copy of the { link Instance}.
+     *
+     * This produces an exact copy of the Instance at the time of the call.  Unlike Instances
+     * produced by { link Backing.CalendarSourceSubscription}s, cloned Instances are not
+     * automatically updated as local and/or remote changes are made.  This makes them good for
+     * editing (where a number of changes are made and stored in the Instance, only being submitted
+     * when the user gives the okay).
+     *
+     * Cloning will also clone the { link master}, if present.
+     */
+    public abstract Component.Instance clone() throws Error;
+    
+    /**
      * Add a { link RecurrenceRule} to the { link Instance}.
      *
      * Pass null to make the Instance non-recurring.
diff --git a/src/host/host-create-update-event.vala b/src/host/host-create-update-event.vala
index 68f92ae..9de97b8 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -8,6 +8,9 @@ namespace California.Host {
 
 /**
  * A blank "form" of widgets for the user to enter or update event details.
+ *
+ * Message IN: If creating a new event, send Component.Event.blank() (pre-filled with any known
+ * details).  If updating an existing event, send Component.Event.clone().
  */
 
 [GtkTemplate (ui = "/org/yorba/california/rc/create-update-event.ui")]
@@ -66,7 +69,6 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
     public bool is_update { get; set; default = false; }
     
     private new Component.Event event = new Component.Event.blank();
-    private Component.RecurrenceRule? rrule = null;
     private Gee.HashMap<string, Calendar.WallTime> time_map = new Gee.HashMap<string, Calendar.WallTime>();
     private Backing.CalendarSource? original_calendar_source;
     private Toolkit.ComboBoxTextModel<Backing.CalendarSource> calendar_model;
@@ -142,24 +144,13 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
     }
     
     public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
-        bool update = false;
-        
-        if (message != null) {
-            if (message.type() == typeof (Component.Event)) {
-                event = (Component.Event) message;
-                update = true;
-            } else if (message.type() == typeof (Component.RecurrenceRule)) {
-                rrule = (Component.RecurrenceRule) message;
-            }
-        } else if (event == null) {
-            event = new Component.Event.blank();
-            update = true;
-        }
+        // if no message, leave everything as it is
+        if (message == null)
+            return;
         
-        assert(event != null);
+        event = (Component.Event) message;
         
-        if (update)
-            update_controls();
+        update_controls();
     }
     
     private void update_controls() {
@@ -304,34 +295,39 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
             return;
         }
         
-        event.calendar_source = calendar_model.active;
-        event.summary = summary_entry.text;
-        event.location = location_entry.text;
-        event.description = description_textview.buffer.text;
+        // create/update this instance of the event
+        create_update_event(event, true);
+    }
+    
+    private void create_update_event(Component.Event target, bool update_dtstart) {
+        target.calendar_source = calendar_model.active;
+        target.summary = summary_entry.text;
+        target.location = location_entry.text;
+        target.description = description_textview.buffer.text;
         
-        if (all_day_toggle.active) {
-            event.set_event_date_span(selected_date_span);
-        } else {
-            // use existing timezone unless not specified in original event
-            Calendar.Timezone tz = (event.exact_time_span != null)
-                ? event.exact_time_span.start_exact_time.tz
-                : Calendar.Timezone.local;
-            event.set_event_exact_time_span(
-                new Calendar.ExactTimeSpan(
-                    new Calendar.ExactTime(tz, selected_date_span.start_date,
-                        time_map.get(dtstart_time_combo.get_active_text())),
-                    new Calendar.ExactTime(tz, selected_date_span.end_date,
-                        time_map.get(dtend_time_combo.get_active_text()))
-                )
-            );
+        if (update_dtstart) {
+            if (all_day_toggle.active) {
+                target.set_event_date_span(selected_date_span);
+            } else {
+                // use existing timezone unless not specified in original event
+                Calendar.Timezone tz = (target.exact_time_span != null)
+                    ? target.exact_time_span.start_exact_time.tz
+                    : Calendar.Timezone.local;
+                target.set_event_exact_time_span(
+                    new Calendar.ExactTimeSpan(
+                        new Calendar.ExactTime(tz, selected_date_span.start_date,
+                            time_map.get(dtstart_time_combo.get_active_text())),
+                        new Calendar.ExactTime(tz, selected_date_span.end_date,
+                            time_map.get(dtend_time_combo.get_active_text()))
+                    )
+                );
+            }
         }
         
-        event.make_recurring(rrule);
-        
         if (is_update)
-            update_event_async.begin(null);
+            update_event_async.begin(target, null);
         else
-            create_event_async.begin(null);
+            create_event_async.begin(target, null);
     }
     
     private void on_cancel_button_clicked() {
@@ -339,17 +335,19 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
     }
     
     private void on_update_all_button_clicked() {
+        create_update_event(event.is_master_instance ? event : (Component.Event) event.master, false);
     }
     
     private void on_update_this_button_clicked() {
+        create_update_event(event, true);
     }
     
     private void on_cancel_recurring_button_clicked() {
         rotating_button_box.family = FAMILY_NORMAL;
     }
     
-    private async void create_event_async(Cancellable? cancellable) {
-        if (event.calendar_source == null) {
+    private async void create_event_async(Component.Event target, Cancellable? cancellable) {
+        if (target.calendar_source == null) {
             notify_failure(_("Unable to create event: calendar must be specified"));
             
             return;
@@ -359,7 +357,7 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
         
         Error? create_err = null;
         try {
-            yield event.calendar_source.create_component_async(event, cancellable);
+            yield event.calendar_source.create_component_async(target, cancellable);
         } catch (Error err) {
             create_err = err;
         }
@@ -373,8 +371,8 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
     }
     
     // TODO: Delete from original source if not the same as the new source
-    private async void update_event_async(Cancellable? cancellable) {
-        if (event.calendar_source == null) {
+    private async void update_event_async(Component.Event target, Cancellable? cancellable) {
+        if (target.calendar_source == null) {
             notify_failure(_("Unable to update event: calendar must be specified"));
             
             return;
@@ -384,7 +382,7 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
         
         Error? update_err = null;
         try {
-            yield event.calendar_source.update_component_async(event, cancellable);
+            yield event.calendar_source.update_component_async(target, cancellable);
         } catch (Error err) {
             update_err = err;
         }
diff --git a/src/host/host-create-update-recurring.vala b/src/host/host-create-update-recurring.vala
index 1ff0f3b..5e0589e 100644
--- a/src/host/host-create-update-recurring.vala
+++ b/src/host/host-create-update-recurring.vala
@@ -23,37 +23,6 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         YEARLY = 4
     }
     
-    /**
-     * The message that must be passed to this Card when jumping to it.
-     *
-     * The proper master instance will be extracted.  If the RRULE is provided, that will be
-     * used by the card, otherwise the master's RRULE (if any) will be used.
-     */
-    public class MessageIn : Object {
-        public new Component.Event event;
-        public Component.Event master;
-        public Component.RecurrenceRule? rrule;
-        
-        public Message(Component.Event event, Component.RecurrenceRule? rrule) {
-            this.event = event;
-            master = event.is_master_instance ? event : (Component.Event) event.master;
-            rrule = rrule ?? master.rrule;
-        }
-    }
-    
-    /**
-     * The message this card will pass to the next Card when jumping to it.
-     */
-    public class MessageOut : Object {
-        public Component.RecurrenceRule rrule;
-        public Component.Date start_date;
-        
-        public MessageOut(Component.RecurrenceRule rrule, Component.Date start_date) {
-            this.rrule = rrule;
-            this.start_date = start_date;
-        }
-    }
-    
     public string card_id { get { return ID; } }
     
     public string? title { get { return null; } }
@@ -131,6 +100,8 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     [GtkChild]
     private Gtk.Button ok_button;
     
+    private new Component.Event? event = null;
+    private Component.Event? master = null;
     private Gee.HashMap<Calendar.DayOfWeek, Gtk.CheckButton> on_day_checkbuttons = new Gee.HashMap<
         Calendar.DayOfWeek, Gtk.CheckButton>();
     private bool blocking_insert_text_numbers_only_signal = false;
@@ -166,6 +137,11 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         on_day_checkbuttons[Calendar.DayOfWeek.THU] = thursday_checkbutton;
         on_day_checkbuttons[Calendar.DayOfWeek.FRI] = friday_checkbutton;
         on_day_checkbuttons[Calendar.DayOfWeek.SAT] = saturday_checkbutton;
+        
+        /*
+        foreach (Gtk.CheckButton check_button in on_day_checkbuttons.keys)
+            check_button.notify["active"].connect(on_checkbox_active_changed);
+        */
     }
     
     private bool transform_repeats_active_to_on_days_visible(Binding binding, Value source_value,
@@ -182,28 +158,28 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         return true;
     }
     
-    public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? msg) {
-        Message message = (Message) msg;
+    public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
+        assert(message != null);
+        
+        event = (Component.Event) message;
+        master = event.is_master_instance ? event : (Component.Event) event.master;
         
         // need to use the master component in order to update the master RRULE
-        if (!can_update_recurring(message.event)) {
+        if (!can_update_recurring(event)) {
             jump_back();
             
             return;
         }
         
-        update_controls(message);
+        update_controls();
     }
     
     public static bool can_update_recurring(Component.Event event) {
         return event.is_master_instance || (event.master is Component.Event);
     }
     
-    private void update_controls(Message message) {
-        Component.Event master = message.master;
-        Component.RecurrenceRule? rrule = message.rrule;
-        
-        make_recurring_checkbutton.active = (rrule != null);
+    private void update_controls() {
+        make_recurring_checkbutton.active = (master.rrule != null);
         
         // some defaults that may not be set even if an RRULE is present
         
@@ -220,7 +196,7 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             checkbutton.active = false;
         
         // set remaining defaults if not a recurring event
-        if (rrule == null) {
+        if (master.rrule == null) {
             repeats_combobox.active = Repeats.DAILY;
             every_entry.text = "1";
             never_radiobutton.active = true;
@@ -229,7 +205,7 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         }
         
         // "Repeats" combobox
-        switch (rrule.freq) {
+        switch (master.rrule.freq) {
             case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
                 repeats_combobox.active = Repeats.DAILY;
             break;
@@ -245,8 +221,8 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             // TODO: BYDAY should be the exact month-day of week for the DTSTART, MONTH_DAY should
             // be the month-day of the month for the DTSTART
             case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
-                bool by_day = rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
-                bool by_monthday = rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size > 0;
+                bool by_day = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
+                bool by_monthday = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size 
0;
                 
                 if (by_day && !by_monthday)
                     repeats_combobox.active = Repeats.DAY_OF_THE_WEEK;
@@ -266,12 +242,12 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         }
         
         // "Every" entry
-        every_entry.text = rrule.interval.to_string();
+        every_entry.text = master.rrule.interval.to_string();
         
         // "On days" week day checkboxes are only visible if a WEEKLY event
         if (master.rrule.is_weekly) {
             Gee.Map<Calendar.DayOfWeek?, int> by_days =
-                Component.RecurrenceRule.decode_days(rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY));
+                
Component.RecurrenceRule.decode_days(master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY));
             
             // the presence of a "null" day means every or all days
             if (by_days.has_key(null)) {
@@ -284,16 +260,16 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         }
         
         // "Ends" choices
-        if (!rrule.has_duration) {
+        if (!master.rrule.has_duration) {
             never_radiobutton.active = true;
-        } else if (rrule.count > 0) {
+        } else if (master.rrule.count > 0) {
             after_radiobutton.active = true;
             after_entry.text = master.rrule.count.to_string();
         } else {
-            assert(rrule.until_date != null || rrule.until_exact_time != null);
+            assert(master.rrule.until_date != null || master.rrule.until_exact_time != null);
             
             ends_on_radiobutton.active = true;
-            end_date = rrule.get_recurrence_end_date();
+            end_date = master.rrule.get_recurrence_end_date();
         }
     }
     
@@ -397,12 +373,16 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     
     [GtkCallback]
     private void on_ok_button_clicked() {
-        jump_to_card_by_name(CreateUpdateEvent.ID, make_rrule());
+        update_master();
+        jump_to_card_by_name(CreateUpdateEvent.ID, event);
     }
     
-    private MessageOut? make_message_out() {
-        if (!make_recurring_checkbutton.active)
-            return null;
+    private void update_master() {
+        if (!make_recurring_checkbutton.active) {
+            master.make_recurring(null);
+            
+            return;
+        }
         
         iCal.icalrecurrencetype_frequency freq;
         switch (repeats_combobox.active) {
@@ -430,18 +410,6 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         Component.RecurrenceRule rrule = new Component.RecurrenceRule(freq);
         rrule.interval = Numeric.floor_int(int.parse(every_entry.text), 1);
         
-        // set start and end dates (which may actually be date-times)
-        if (never_radiobutton.active) {
-            // no duration
-            rrule.set_recurrence_end_date(null);
-        } else if (ends_on_radiobutton.active) {
-            rrule.set_recurrence_end_date(end_date);
-        } else {
-            assert(after_radiobutton.active);
-            
-            rrule.set_recurrence_count(Numeric.floor_int(int.parse(after_entry.text), 1));
-        }
-        
         if (rrule.is_weekly) {
             Gee.HashMap<Calendar.DayOfWeek?, int> by_day = new Gee.HashMap<Calendar.DayOfWeek?, int>();
             foreach (Calendar.DayOfWeek dow in on_day_checkbuttons.keys) {
@@ -449,8 +417,43 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
                     by_day[dow] = 0;
             }
             
+            // although control sensitivity should prevent this from happening, be double-sure to
+            // prevent infinite loops below
+            if (by_day.size == 0)
+                by_day[start_date.day_of_week] = 0;
+            
             rrule.set_by_rule(Component.RecurrenceRule.ByRule.DAY,
                 Component.RecurrenceRule.encode_days(by_day));
+            
+            // need to also update the start date to fall on one of the selected days of the week
+            // start by looking backward
+            Calendar.Date new_start_date = start_date.prior(true, (date) => {
+                return date.day_of_week in by_day.keys;
+            });
+            
+            // if start date is prior to today's day, move forward
+            if (new_start_date.compare_to(Calendar.System.today) < 0) {
+                new_start_date = start_date.upcoming(true, (date) => {
+                    return date.day_of_week in by_day.keys;
+                });
+            }
+            
+            start_date = new_start_date;
+        }
+        
+        // set start and end dates (which may actually be date-times, so use adjust)
+        if (never_radiobutton.active) {
+            // no duration
+            master.adjust_event_date_span(start_date.to_date_span());
+            rrule.set_recurrence_end_date(null);
+        } else if (ends_on_radiobutton.active) {
+            master.adjust_event_date_span(new Calendar.DateSpan(start_date, end_date));
+            rrule.set_recurrence_end_date(end_date);
+        } else {
+            assert(after_radiobutton.active);
+            
+            master.adjust_event_date_span(start_date.to_date_span());
+            rrule.set_recurrence_count(Numeric.floor_int(int.parse(after_entry.text), 1));
         }
         
         if (rrule.is_monthly) {
@@ -466,7 +469,7 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             }
         }
         
-        return new MessageOut(rrule, start_date);
+        master.make_recurring(rrule);
     }
 }
 
diff --git a/src/host/host-main-window.vala b/src/host/host-main-window.vala
index e2ebb2f..a753a18 100644
--- a/src/host/host-main-window.vala
+++ b/src/host/host-main-window.vala
@@ -351,6 +351,10 @@ public class MainWindow : Gtk.ApplicationWindow {
             Toolkit.spin_event_loop();
         });
         
+        deck_window.deck.failure.connect((msg) => {
+            Application.instance.error_message(msg);
+        });
+        
         deck_window.show_all();
         deck_window.run();
         deck_window.destroy();
diff --git a/src/host/host-quick-create-event.vala b/src/host/host-quick-create-event.vala
index 2bf45b8..4ce6d9b 100644
--- a/src/host/host-quick-create-event.vala
+++ b/src/host/host-quick-create-event.vala
@@ -97,11 +97,7 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
         string details = details_entry.text.strip();
         
         if (String.is_empty(details)) {
-            // jump to Create/Update dialog and remove this Card from the Deck ... this ensures
-            // that if the user presses Cancel in the Create/Update dialog the Deck exits rather
-            // than returns here (via jump_home_or_user_closed())
-            jump_to_card_by_name(CreateUpdateEvent.ID, event);
-            deck.remove_cards(iterate<Toolkit.Card>(this).to_array_list());
+            create_empty_event();
             
             return;
         }
@@ -110,14 +106,22 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
             event);
         event = parser.event;
         
-        if (event.is_valid(true)) {
+        if (event.is_valid(true))
             create_event_async.begin(null);
-        } else {
-            // see note above about why the Deck jumps to Create/Update and then this Card is
-            // removed
-            jump_to_card_by_name(CreateUpdateEvent.ID, event);
-            deck.remove_cards(iterate<Toolkit.Card>(this).to_array_list());
-        }
+        else
+            create_empty_event();
+    }
+    
+    private void create_empty_event() {
+        // Must pass some kind of event to create/update, so use blank if required
+        if (event == null)
+            event = new Component.Event.blank();
+        
+        // jump to Create/Update dialog and remove this Card from the Deck ... this ensures
+        // that if the user presses Cancel in the Create/Update dialog the Deck exits rather
+        // than returns here (via jump_home_or_user_closed())
+        jump_to_card_by_name(CreateUpdateEvent.ID, event);
+        deck.remove_cards(iterate<Toolkit.Card>(this).to_array_list());
     }
     
     private async void create_event_async(Cancellable? cancellable) {
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 90e65a2..bcf151a 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -6,6 +6,10 @@
 
 namespace California.Host {
 
+/**
+ * MESSAGE IN: Send the Component.Event to be displayed.
+ */
+
 [GtkTemplate (ui = "/org/yorba/california/rc/show-event.ui")]
 public class ShowEvent : Gtk.Grid, Toolkit.Card {
     public const string ID = "ShowEvent";
@@ -64,6 +68,7 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     }
     
     public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
+        // no message, don't update display
         if (message == null)
             return;
         
@@ -176,7 +181,12 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     
     [GtkCallback]
     private void on_update_button_clicked() {
-        jump_to_card_by_name(CreateUpdateEvent.ID, event);
+        // pass a clone of the existing event for editing
+        try {
+            jump_to_card_by_name(CreateUpdateEvent.ID, event.clone() as Component.Event);
+        } catch (Error err) {
+            notify_failure(_("Unable to update event: %s").printf(err.message));
+        }
     }
     
     [GtkCallback]


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