[california/wip/725786-edit-recurring] Getting close, can create and update recurring events, still some work to be done to clean it up and



commit 7a8fcf06be51e423c8027ec130cc24c2568507d8
Author: Jim Nelson <jim yorba org>
Date:   Tue Jul 15 19:31:39 2014 -0700

    Getting close, can create and update recurring events, still some
    work to be done to clean it up and bullet-proof it.

 src/Makefile.am                              |    1 +
 src/component/component-date-time.vala       |    4 +-
 src/component/component-event.vala           |   39 +++++++
 src/component/component-instance.vala        |    2 +
 src/component/component-recurrence-rule.vala |   15 ++-
 src/host/host-create-update-event.vala       |   33 +++++--
 src/host/host-create-update-recurring.vala   |  148 +++++++++++++++++++++-----
 src/host/host-quick-create-event.vala        |    2 +-
 src/host/host-show-event.vala                |    4 +
 src/rc/create-update-recurring.ui            |   37 -------
 src/util/util-numeric.vala                   |   17 +++
 11 files changed, 224 insertions(+), 78 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index c404850..14a1332 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -134,6 +134,7 @@ california_VALASOURCES = \
        util/util-gfx.vala \
        util/util-markup.vala \
        util/util-memory.vala \
+       util/util-numeric.vala \
        util/util-scheduled.vala \
        util/util-string.vala \
        util/util-uri.vala \
diff --git a/src/component/component-date-time.vala b/src/component/component-date-time.vala
index aba1c9f..85825c5 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -133,10 +133,10 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
     public DateTime.rrule_until(iCal.icalrecurrencetype rrule, DateTime dtstart, bool strict)
         throws ComponentError {
         if (iCal.icaltime_is_null_time(rrule.until) != 0)
-            throw new ComponentError.INVALID("DATE-TIME for RRULE UNTIL is null time");
+            throw new ComponentError.UNAVAILABLE("DATE-TIME for RRULE UNTIL is null time");
         
         if (iCal.icaltime_is_valid_time(rrule.until) == 0)
-            throw new ComponentError.INVALID("DATE-TIME for RRULE UNTIL is invalid");
+            throw new ComponentError.UNAVAILABLE("DATE-TIME for RRULE UNTIL is invalid");
         
         bool until_is_date = (iCal.icaltime_is_date(rrule.until) != 0);
         bool until_is_utc = (iCal.icaltime_is_utc(rrule.until) != 0);
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index e5a492f..27e13cc 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -249,8 +249,12 @@ public class Event : Instance, Gee.Comparable<Event> {
      * @see set_event_exact_time_span
      */
     public void set_event_date_span(Calendar.DateSpan date_span) {
+        freeze_notify();
+        
         this.date_span = date_span;
         exact_time_span = null;
+        
+        thaw_notify();
     }
     
     /**
@@ -261,8 +265,43 @@ public class Event : Instance, Gee.Comparable<Event> {
      * @see set_event_date_span
      */
     public void set_event_exact_time_span(Calendar.ExactTimeSpan exact_time_span) {
+        freeze_notify();
+        
         this.exact_time_span = exact_time_span;
         date_span = null;
+        
+        thaw_notify();
+    }
+    
+    /**
+     * Adjusts the dates of an { link Event} while preserving { link WallTime}, if present.
+     *
+     * This will preserve the DATE/DATE-TIME aspect of an Event while adjusting the start and
+     * end { link Calendar.Date}s.  If a DATE Event, then this is functionally equivalent to
+     * { link set_event_date_span}.  If a DATE-TIME event, then this is like
+     * { link set_event_exact_time_span} but without the hassle of preserving start and end times
+     * while changing the dates.
+     */
+    public void adjust_event_date_span(Calendar.DateSpan date_span) {
+        if (is_all_day) {
+            set_event_date_span(date_span);
+            
+            return;
+        }
+        
+        Calendar.ExactTime new_start_time = new Calendar.ExactTime(
+            exact_time_span.start_exact_time.tz,
+            date_span.start_date,
+            exact_time_span.start_exact_time.to_wall_time()
+        );
+        
+        Calendar.ExactTime new_end_time = new Calendar.ExactTime(
+            exact_time_span.end_exact_time.tz,
+            date_span.end_date,
+            exact_time_span.end_exact_time.to_wall_time()
+        );
+        
+        set_event_exact_time_span(new Calendar.ExactTimeSpan(new_start_time, new_end_time));
     }
     
     /**
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 4154669..a57c6d0 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -304,6 +304,8 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
             make_recurring(new RecurrenceRule.from_ical(ical_component, false));
         } catch (ComponentError comperr) {
             // ignored; generally means no RRULE in component
+            if (!(comperr is ComponentError.UNAVAILABLE))
+                debug("Unable to parse RRULE for %s: %s", to_string(), comperr.message);
         }
         
         // save own copy of component; no ownership transferrance w/ current bindings
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index ab3d624..845e94e 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -155,11 +155,16 @@ public class RecurrenceRule : BaseObject {
         if (rrule.count > 0) {
             set_recurrence_count(rrule.count);
         } else {
-            Component.DateTime date_time = new DateTime.rrule_until(rrule, dtstart, strict);
-            if (date_time.is_date)
-                set_recurrence_end_date(date_time.to_date());
-            else
-                set_recurrence_end_exact_time(date_time.to_exact_time());
+            try {
+                Component.DateTime date_time = new DateTime.rrule_until(rrule, dtstart, strict);
+                if (date_time.is_date)
+                    set_recurrence_end_date(date_time.to_date());
+                else
+                    set_recurrence_end_exact_time(date_time.to_exact_time());
+            } catch (ComponentError comperr) {
+                if (!(comperr is ComponentError.UNAVAILABLE))
+                    throw comperr;
+            }
         }
         
         switch (rrule.week_start) {
diff --git a/src/host/host-create-update-event.vala b/src/host/host-create-update-event.vala
index 2f5d3be..68f92ae 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -66,6 +66,7 @@ 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;
@@ -141,17 +142,24 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
     }
     
     public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
-        // if jumping back, message is null but want to keep using current Event
-        if (reason != Toolkit.Card.Jump.BACK) {
-            if (message != null) {
-                event = message as Component.Event;
-                assert(event != null);
-            } else {
-                event = new Component.Event.blank();
+        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;
         }
         
-        update_controls();
+        assert(event != null);
+        
+        if (update)
+            update_controls();
     }
     
     private void update_controls() {
@@ -178,6 +186,13 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
             selected_date_span = new Calendar.DateSpan(Calendar.System.today, Calendar.System.today);
             initial_start_time = Calendar.System.now.to_wall_time();
             initial_end_time = Calendar.System.now.adjust_time(1, Calendar.TimeUnit.HOUR).to_wall_time();
+            
+            // set in Component.Event as well, to at least initialize it for use elsewhere while
+            // editing (such as the RRULE)
+            event.set_event_exact_time_span(new Calendar.ExactTimeSpan(
+                new Calendar.ExactTime(Calendar.Timezone.local, Calendar.System.today, initial_start_time),
+                new Calendar.ExactTime(Calendar.Timezone.local, Calendar.System.today, initial_end_time)
+            ));
         }
         
         // initialize start and end time controls (as in, wall clock time)
@@ -311,6 +326,8 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
             );
         }
         
+        event.make_recurring(rrule);
+        
         if (is_update)
             update_event_async.begin(null);
         else
diff --git a/src/host/host-create-update-recurring.vala b/src/host/host-create-update-recurring.vala
index 9e42a74..1ff0f3b 100644
--- a/src/host/host-create-update-recurring.vala
+++ b/src/host/host-create-update-recurring.vala
@@ -23,6 +23,37 @@ 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; } }
@@ -100,8 +131,6 @@ 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;
@@ -153,32 +182,28 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         return true;
     }
     
-    public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
-        if (message != null)
-            event = (Component.Event) message;
-        
-        // *must* have an Event by this point, whether from before or due to this jump
-        assert(event != null);
+    public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? msg) {
+        Message message = (Message) msg;
         
         // need to use the master component in order to update the master RRULE
-        if (!can_update_recurring(event)) {
+        if (!can_update_recurring(message.event)) {
             jump_back();
             
             return;
         }
         
-        update_controls();
+        update_controls(message);
     }
     
     public static bool can_update_recurring(Component.Event event) {
         return event.is_master_instance || (event.master is Component.Event);
     }
     
-    private void update_controls() {
-        master = (event.is_master_instance ? event : event.master) as Component.Event;
-        assert(master != null);
+    private void update_controls(Message message) {
+        Component.Event master = message.master;
+        Component.RecurrenceRule? rrule = message.rrule;
         
-        make_recurring_checkbutton.active = (master.rrule != null);
+        make_recurring_checkbutton.active = (rrule != null);
         
         // some defaults that may not be set even if an RRULE is present
         
@@ -195,7 +220,7 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             checkbutton.active = false;
         
         // set remaining defaults if not a recurring event
-        if (master.rrule == null) {
+        if (rrule == null) {
             repeats_combobox.active = Repeats.DAILY;
             every_entry.text = "1";
             never_radiobutton.active = true;
@@ -204,20 +229,24 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         }
         
         // "Repeats" combobox
-        switch (master.rrule.freq) {
+        switch (rrule.freq) {
             case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
                 repeats_combobox.active = Repeats.DAILY;
             break;
             
+            // TODO: Don't allow for editing weekly rules with anything but BYDAY or BYDAY where
+            // the position value is non-zero
             case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
                 repeats_combobox.active = Repeats.WEEKLY;
             break;
             
             // TODO: Don't support MONTHLY RRULEs with multiple ByRules or ByRules we can't
             // represent ... basically, non-simple repeating rules
+            // 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 = 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;
+                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;
                 
                 if (by_day && !by_monthday)
                     repeats_combobox.active = Repeats.DAY_OF_THE_WEEK;
@@ -237,12 +266,12 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         }
         
         // "Every" entry
-        every_entry.text = master.rrule.interval.to_string();
+        every_entry.text = 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(master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY));
+                Component.RecurrenceRule.decode_days(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)) {
@@ -255,16 +284,16 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         }
         
         // "Ends" choices
-        if (!master.rrule.has_duration) {
+        if (!rrule.has_duration) {
             never_radiobutton.active = true;
-        } else if (master.rrule.count > 0) {
+        } else if (rrule.count > 0) {
             after_radiobutton.active = true;
             after_entry.text = master.rrule.count.to_string();
         } else {
-            assert(master.rrule.until_date != null || master.rrule.until_exact_time != null);
+            assert(rrule.until_date != null || rrule.until_exact_time != null);
             
             ends_on_radiobutton.active = true;
-            end_date = master.rrule.get_recurrence_end_date();
+            end_date = rrule.get_recurrence_end_date();
         }
     }
     
@@ -368,7 +397,76 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     
     [GtkCallback]
     private void on_ok_button_clicked() {
-        jump_back();
+        jump_to_card_by_name(CreateUpdateEvent.ID, make_rrule());
+    }
+    
+    private MessageOut? make_message_out() {
+        if (!make_recurring_checkbutton.active)
+            return null;
+        
+        iCal.icalrecurrencetype_frequency freq;
+        switch (repeats_combobox.active) {
+            case Repeats.DAILY:
+                freq = iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE;
+            break;
+            
+            case Repeats.WEEKLY:
+                freq = iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE;
+            break;
+            
+            case Repeats.DAY_OF_THE_WEEK:
+            case Repeats.DAY_OF_THE_MONTH:
+                freq = iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE;
+            break;
+            
+            case Repeats.YEARLY:
+                freq = iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE;
+            break;
+            
+            default:
+                assert_not_reached();
+        }
+        
+        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) {
+                if (on_day_checkbuttons[dow].active)
+                    by_day[dow] = 0;
+            }
+            
+            rrule.set_by_rule(Component.RecurrenceRule.ByRule.DAY,
+                Component.RecurrenceRule.encode_days(by_day));
+        }
+        
+        if (rrule.is_monthly) {
+            if (repeats_combobox.active == Repeats.DAY_OF_THE_WEEK) {
+                Gee.HashMap<Calendar.DayOfWeek?, int> by_day = new Gee.HashMap<Calendar.DayOfWeek?, int>();
+                by_day[start_date.day_of_week] = 
start_date.week_of(Calendar.System.first_of_week).week_of_month;
+                rrule.set_by_rule(Component.RecurrenceRule.ByRule.DAY,
+                    Component.RecurrenceRule.encode_days(by_day));
+            } else {
+                Gee.Collection<int> by_month_day = new Gee.ArrayList<int>();
+                by_month_day.add(start_date.day_of_month.value);
+                rrule.set_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY, by_month_day);
+            }
+        }
+        
+        return new MessageOut(rrule, start_date);
     }
 }
 
diff --git a/src/host/host-quick-create-event.vala b/src/host/host-quick-create-event.vala
index 869e7f2..2bf45b8 100644
--- a/src/host/host-quick-create-event.vala
+++ b/src/host/host-quick-create-event.vala
@@ -44,7 +44,7 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
     }
     
     public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
-        event = message as Component.Event;
+        event = (message != null) ? message as Component.Event : null;
         
         // if initial date/times supplied, reveal to the user and change the example
         string eg;
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index b20bc33..90e65a2 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -74,6 +74,10 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     }
     
     private void build_display() {
+        debug("VEVENT:\n%s\n", event.source);
+        if (event.master != null)
+            debug("VEVENT MASTER:\n%s\n", event.master.source);
+        
         // summary
         set_label(null, summary_text, event.summary);
         
diff --git a/src/rc/create-update-recurring.ui b/src/rc/create-update-recurring.ui
index 2a6456f..302c5ec 100644
--- a/src/rc/create-update-recurring.ui
+++ b/src/rc/create-update-recurring.ui
@@ -526,41 +526,4 @@
       </packing>
     </child>
   </template>
-  <object class="GtkBox" id="ends_on_box">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="spacing">4</property>
-    <child>
-      <object class="GtkRadioButton" id="ends_on_radiobutton1">
-        <property name="label" translatable="yes" comments="As in, an event &quot;ends on&quot; a 
date">_On</property>
-        <property name="visible">True</property>
-        <property name="can_focus">True</property>
-        <property name="receives_default">False</property>
-        <property name="use_underline">True</property>
-        <property name="xalign">0</property>
-        <property name="active">True</property>
-        <property name="draw_indicator">True</property>
-        <property name="group">never_radiobutton</property>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-        <property name="position">0</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkButton" id="end_date_button1">
-        <property name="label">(none)</property>
-        <property name="visible">True</property>
-        <property name="can_focus">True</property>
-        <property name="receives_default">True</property>
-        <signal name="clicked" handler="on_date_button_clicked" object="CaliforniaHostCreateUpdateRecurring" 
swapped="no"/>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-        <property name="position">1</property>
-      </packing>
-    </child>
-  </object>
 </interface>
diff --git a/src/util/util-numeric.vala b/src/util/util-numeric.vala
new file mode 100644
index 0000000..518ab0d
--- /dev/null
+++ b/src/util/util-numeric.vala
@@ -0,0 +1,17 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+namespace California.Numeric {
+
+/**
+ * Returns the value if it is greater than or equal to floor, floor otherwise.
+ */
+public inline int floor_int(int value, int floor) {
+    return (value >= floor) ? value : floor;
+}
+
+}
+


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