[california/wip/732930-explain] Added explanation to recurring editor



commit 4e9a7c40116938c1c643282f479b0ba274796127
Author: Jim Nelson <jim yorba org>
Date:   Fri Sep 5 14:49:58 2014 -0700

    Added explanation to recurring editor

 src/calendar/calendar-date.vala              |   13 ++++-
 src/collection/collection-iterable.vala      |    9 +++
 src/collection/collection.vala               |   13 ++++
 src/component/component-recurrence-rule.vala |   91 +++++++++++++++++++++-----
 src/host/host-create-update-recurring.vala   |   46 +++++++++++---
 src/host/host-show-event.vala                |    4 +-
 src/rc/create-update-recurring.ui            |   27 +++++++-
 7 files changed, 173 insertions(+), 30 deletions(-)
---
diff --git a/src/calendar/calendar-date.vala b/src/calendar/calendar-date.vala
index 2f8da5c..1bf41ce 100644
--- a/src/calendar/calendar-date.vala
+++ b/src/calendar/calendar-date.vala
@@ -41,10 +41,17 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
          */
         COMPACT,
         /**
-         * Indicates that the year should be included in the return date string.
+         * Indicates that the year should be included in the returned date string.
          */
         INCLUDE_YEAR,
         /**
+         * Indicates that the year should be included in the returned string if it's not the current
+         * year.
+         *
+         * { link INCLUDE_YEAR} overrides this flag to always return the year in the string.
+         */
+        INCLUDE_OTHER_YEAR,
+        /**
          * Indicates that the localized string for "Today" should not be used if the date matches
          * { link System.today}.
          */
@@ -345,12 +352,16 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
         bool compact = (flags & PrettyFlag.COMPACT) != 0;
         bool abbrev = (flags & PrettyFlag.ABBREV) != 0;
         bool with_year = (flags & PrettyFlag.INCLUDE_YEAR) != 0;
+        bool with_other_year = (flags & PrettyFlag.INCLUDE_OTHER_YEAR) != 0;
         bool no_today = (flags & PrettyFlag.NO_TODAY) != 0;
         bool no_dow = (flags & PrettyFlag.NO_DAY_OF_WEEK) != 0;
         
         if (!no_today && !with_year && equal_to(System.today))
             return _("Today");
         
+        if (!with_year && with_other_year && !year.equal_to(System.today.year))
+            with_year = true;
+        
         unowned string fmt;
         if (abbrev) {
             if (no_dow)
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index d51e82d..2e237a1 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -256,6 +256,15 @@ public class Iterable<G> : Object {
         return true;
     }
     
+    public bool contains_all(Gee.Collection<G> c) {
+        foreach (G g in this) {
+            if (!c.contains(g))
+                return false;
+        }
+        
+        return true;
+    }
+    
     public int count_matching(owned Gee.Predicate<G> f) {
         int count = 0;
         foreach (G g in this) {
diff --git a/src/collection/collection.vala b/src/collection/collection.vala
index 6557566..1bf5dcd 100644
--- a/src/collection/collection.vala
+++ b/src/collection/collection.vala
@@ -26,6 +26,19 @@ public inline bool is_empty(Gee.Collection? c) {
 }
 
 /**
+ * Returns true if the two Collections contains all the same elements and the same number of elements.
+ */
+public bool equal<G>(Gee.Collection<G>? a, Gee.Collection<G>? b) {
+    if (a == b)
+        return true;
+    
+    if (size(a) != size(b))
+        return false;
+    
+    return traverse<G>(a).contains_all(b);
+}
+
+/**
  * Returns the size of the Collection, zero if null.
  */
 public inline int size(Gee.Collection? c) {
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index 8a72ce2..4c86e24 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -20,6 +20,14 @@ public class RecurrenceRule : BaseObject {
     public const string PROP_INTERVAL = "interval";
     public const string PROP_FIRST_OF_WEEK = "first-of-week";
     
+    private const Calendar.Date.PrettyFlag UNTIL_DATE_PRETTY_FLAGS =
+        Calendar.Date.PrettyFlag.ABBREV
+        | Calendar.Date.PrettyFlag.NO_DAY_OF_WEEK
+        | Calendar.Date.PrettyFlag.INCLUDE_OTHER_YEAR;
+    
+    private const Calendar.WallTime.PrettyFlag UNTIL_TIME_PRETTY_FLAGS =
+        Calendar.WallTime.PrettyFlag.NONE;
+    
     /**
      * Enumeration of various BY rules (BYSECOND, BYMINUTE, etc.)
      */
@@ -583,10 +591,10 @@ public class RecurrenceRule : BaseObject {
      *
      * Returns null if the RRULE is beyond the comprehension of this parser.
      */
-    public string? explain() {
+    public string? explain(Calendar.Date start_date) {
         switch (freq) {
             case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
-                return explain_simple(ngettext("day", "%d days", interval).printf(interval));
+                return explain_daily(ngettext("day", "%d days", interval).printf(interval));
             
             case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
                 return explain_weekly(ngettext("week", "%d weeks", interval).printf(interval));
@@ -608,7 +616,7 @@ public class RecurrenceRule : BaseObject {
                     return explain_monthly_bymonthday(unit);
             
             case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
-                return explain_simple(ngettext("year", "%d years", interval).printf(interval));
+                return explain_yearly(ngettext("year", "%d years", interval).printf(interval), start_date);
             
             default:
                 return null;
@@ -616,7 +624,7 @@ public class RecurrenceRule : BaseObject {
         
     }
     
-    private string explain_simple(string units) {
+    private string explain_daily(string units) {
         if (count > 0) {
             // As in, "Repeats every day, 2 times"
             return _("Repeats every %s, %s").printf(units,
@@ -627,15 +635,15 @@ public class RecurrenceRule : BaseObject {
         if (until_date != null) {
             // As in, "Repeats every week until Sept. 2, 2014"
             return _("Repeats every %s until %s").printf(units,
-                until_date.to_pretty_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR)
+                until_date.to_pretty_string(UNTIL_DATE_PRETTY_FLAGS)
             );
         }
         
         if (until_exact_time != null) {
             // As in, "Repeats every month until Sept. 2, 2014, 8:00pm"
             return _("Repeats every %s until %s, %s").printf(units,
-                until_exact_time.to_pretty_date_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR),
-                until_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE)
+                until_exact_time.to_pretty_date_string(UNTIL_DATE_PRETTY_FLAGS),
+                until_exact_time.to_pretty_time_string(UNTIL_TIME_PRETTY_FLAGS)
             );
         }
         
@@ -656,10 +664,24 @@ public class RecurrenceRule : BaseObject {
             dows.add(dow);
         }
         
-        // must be one to work
+        // must be at least one to work
         if (dows.size == 0)
             return null;
         
+        // look for expressible patterns
+        if (dows.size == Calendar.DayOfWeek.COUNT)
+            return _("every day");
+        
+        Gee.Collection<Calendar.DayOfWeek> weekend_days =
+            from_array<Calendar.DayOfWeek>(Calendar.DayOfWeek.weekend_days).to_array_list();
+        if (Collection.equal<Calendar.DayOfWeek>(weekend_days, dows))
+            return _("weekend days");
+        
+        Gee.Collection<Calendar.DayOfWeek> weekdays =
+            from_array<Calendar.DayOfWeek>(Calendar.DayOfWeek.weekdays).to_array_list();
+        if (Collection.equal<Calendar.DayOfWeek>(weekdays, dows))
+            return _("weekdays");
+        
         // assemble a text list of days
         StringBuilder days_of_the_week = new StringBuilder();
         bool first = true;
@@ -669,7 +691,7 @@ public class RecurrenceRule : BaseObject {
                 days_of_the_week.append(_(", "));
             }
             
-            days_of_the_week.append(dow.full_name);
+            days_of_the_week.append(dow.abbrev_name);
             first = false;
         }
         
@@ -696,15 +718,15 @@ public class RecurrenceRule : BaseObject {
         if (until_date != null) {
             // As in, "Repeats every week on Thursday until Sept. 2, 2014"
             return _("Repeats every %s on %s until %s").printf(units, days_of_the_week,
-                until_date.to_pretty_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR)
+                until_date.to_pretty_string(UNTIL_DATE_PRETTY_FLAGS)
             );
         }
         
         if (until_exact_time != null) {
             // As in, "Repeats every week on Friday, Saturday until Sept. 2, 2014, 8:00pm"
             return _("Repeats every %s on %s until %s, %s").printf(units, days_of_the_week,
-                until_exact_time.to_pretty_date_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR),
-                until_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE)
+                until_exact_time.to_pretty_date_string(UNTIL_DATE_PRETTY_FLAGS),
+                until_exact_time.to_pretty_time_string(UNTIL_TIME_PRETTY_FLAGS)
             );
         }
         
@@ -773,15 +795,15 @@ public class RecurrenceRule : BaseObject {
         if (until_date != null) {
             // As in, "Repeats every month on the second Monday until Sept. 2, 2014"
             return _("Repeats every %s on the %s until %s").printf(units, day,
-                until_date.to_pretty_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR)
+                until_date.to_pretty_string(UNTIL_DATE_PRETTY_FLAGS)
             );
         }
         
         if (until_exact_time != null) {
             // As in, "Repeats every month on the last Friday until Sept. 2, 2014, 8:00pm"
             return _("Repeats every %s on the %s until %s, %s").printf(units, day,
-                until_exact_time.to_pretty_date_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR),
-                until_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE)
+                until_exact_time.to_pretty_date_string(UNTIL_DATE_PRETTY_FLAGS),
+                until_exact_time.to_pretty_time_string(UNTIL_TIME_PRETTY_FLAGS)
             );
         }
         
@@ -808,15 +830,15 @@ public class RecurrenceRule : BaseObject {
         if (until_date != null) {
             // As in, "Repeats every month on day 21 until Sept. 2, 2014"
             return _("Repeats every %s on %s until %s").printf(units, day,
-                until_date.to_pretty_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR)
+                until_date.to_pretty_string(UNTIL_DATE_PRETTY_FLAGS)
             );
         }
         
         if (until_exact_time != null) {
             // As in, "Repeats every month on day 20 until Sept. 2, 2014, 8:00pm"
             return _("Repeats every %s on %s until %s, %s").printf(units, day,
-                until_exact_time.to_pretty_date_string(Calendar.Date.PrettyFlag.INCLUDE_YEAR),
-                until_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE)
+                until_exact_time.to_pretty_date_string(UNTIL_DATE_PRETTY_FLAGS),
+                until_exact_time.to_pretty_time_string(UNTIL_TIME_PRETTY_FLAGS)
             );
         }
         
@@ -824,6 +846,39 @@ public class RecurrenceRule : BaseObject {
         return _("Repeats every %s on %s").printf(units, day);
     }
     
+    private string explain_yearly(string units, Calendar.Date start_date) {
+        string date = start_date.to_pretty_string(
+            Calendar.Date.PrettyFlag.NO_DAY_OF_WEEK
+            | Calendar.Date.PrettyFlag.NO_TODAY
+            | Calendar.Date.PrettyFlag.ABBREV
+        );
+        
+        if (count > 0) {
+            // As in, "Repeats every year on 3 March 2014, 2 times"
+            return _("Repeats every %s on %s, %s").printf(units, date,
+                ngettext("%d time", "%d times", count).printf(count)
+            );
+        }
+        
+        if (until_date != null) {
+            // As in, "Repeats every year on 3 March 2014 until Sept. 2, 2014"
+            return _("Repeats every %s on %s until %s").printf(units, date,
+                until_date.to_pretty_string(UNTIL_DATE_PRETTY_FLAGS)
+            );
+        }
+        
+        if (until_exact_time != null) {
+            // As in, "Repeats every year on 3 March 2014 until Sept. 2, 2014, 8:00pm"
+            return _("Repeats every %s on %s until %s, %s").printf(units, date,
+                until_exact_time.to_pretty_date_string(UNTIL_DATE_PRETTY_FLAGS),
+                until_exact_time.to_pretty_time_string(UNTIL_TIME_PRETTY_FLAGS)
+            );
+        }
+        
+        // As in, "Repeats every year on 3 March 2014"
+        return _("Repeats every %s on %s").printf(units, date);
+    }
+    
     public override string to_string() {
         return "RRULE %s".printf(freq.to_string());
     }
diff --git a/src/host/host-create-update-recurring.vala b/src/host/host-create-update-recurring.vala
index 3e202ac..09b905c 100644
--- a/src/host/host-create-update-recurring.vala
+++ b/src/host/host-create-update-recurring.vala
@@ -95,6 +95,9 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     private Gtk.RadioButton ends_on_radiobutton;
     
     [GtkChild]
+    private Gtk.Label recurring_explanation_label;
+    
+    [GtkChild]
     private Gtk.Label warning_label;
     
     [GtkChild]
@@ -132,6 +135,10 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         bind_property(PROP_END_DATE, end_date_button, "label", BindingFlags.SYNC_CREATE,
             transform_date_to_string);
         
+        // update recurring explanation when start/end date changes
+        notify[PROP_START_DATE].connect(on_update_explanation);
+        notify[PROP_END_DATE].connect(on_update_explanation);
+        
         // map on-day checkboxes to days of week
         on_day_checkbuttons[Calendar.DayOfWeek.SUN] = sunday_checkbutton;
         on_day_checkbuttons[Calendar.DayOfWeek.MON] = monday_checkbutton;
@@ -141,6 +148,10 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         on_day_checkbuttons[Calendar.DayOfWeek.FRI] = friday_checkbutton;
         on_day_checkbuttons[Calendar.DayOfWeek.SAT] = saturday_checkbutton;
         
+        // updating any of them updates the recurring explanation
+        foreach (Gtk.CheckButton checkbutton in on_day_checkbuttons.values)
+            checkbutton.toggled.connect(on_update_explanation);
+        
         numeric_filter.connect_to(every_entry);
         numeric_filter.connect_to(after_entry);
         
@@ -247,6 +258,8 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         foreach (Gtk.CheckButton checkbutton in on_day_checkbuttons.values)
             checkbutton.active = false;
         
+        update_explanation(master.rrule, master.get_event_date_span(Calendar.Timezone.local).start_date);
+        
         // set remaining defaults if not a recurring event
         if (master.rrule == null) {
             repeats_combobox.active = Repeats.DAILY;
@@ -324,6 +337,13 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         warning_label.visible = supported != null;
     }
     
+    private void update_explanation(Component.RecurrenceRule? rrule, Calendar.Date? start_date) {
+        string? explanation = (rrule != null && start_date != null) ? rrule.explain(start_date) : null;
+        recurring_explanation_label.label = explanation;
+        recurring_explanation_label.visible = !String.is_empty(explanation);
+        recurring_explanation_label.no_show_all = String.is_empty(explanation);
+    }
+    
     // Returns a logging string for why not reported, null if supported
     private string? is_supported_rrule() {
         // only some frequencies support, and in some of those, certain requirements
@@ -381,6 +401,11 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     }
     
     [GtkCallback]
+    private void on_update_explanation() {
+        update_explanation(make_recurring_checkbutton.active ? make_rrule() : null, start_date);
+    }
+    
+    [GtkCallback]
     private void on_repeats_combobox_changed() {
         on_repeats_combobox_or_every_entry_changed();
     }
@@ -458,13 +483,7 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         jump_to_card_by_name(CreateUpdateEvent.ID, event);
     }
     
-    private void update_master() {
-        if (!make_recurring_checkbutton.active) {
-            master.make_recurring(null);
-            
-            return;
-        }
-        
+    private Component.RecurrenceRule make_rrule() {
         iCal.icalrecurrencetype_frequency freq;
         switch (repeats_combobox.active) {
             case Repeats.DAILY:
@@ -519,7 +538,9 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
                 });
             }
             
-            start_date = new_start_date;
+            // avoid property change notification, as this can start a signal storm
+            if (!start_date.equal_to(new_start_date))
+                start_date = new_start_date;
         }
         
         // set start and end dates (which may actually be date-times, so use adjust)
@@ -556,11 +577,18 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             }
         }
         
+        return rrule;
+    }
+    
+    private void update_master() {
         // remove EXDATEs and RDATEs, those are not currently supported
         master.exdates = null;
         master.rdates = null;
         
-        master.make_recurring(rrule);
+        if (!make_recurring_checkbutton.active)
+            master.make_recurring(null);
+        else
+            master.make_recurring(make_rrule());
     }
 }
 
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 0bce204..937606a 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -157,7 +157,9 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         set_label(null, description_text, Markup.linkify(escape(event.description), linkify_delegate));
         
         // recurring explanation (if appropriate)
-        string? explanation = (event.rrule != null) ? event.rrule.explain() : null;
+        string? explanation = (event.rrule != null)
+            ? event.rrule.explain(event.get_event_date_span(Calendar.Timezone.local).start_date)
+            : null;
         recurring_explanation_label.label = explanation ?? "";
         recurring_explanation_label.visible = !String.is_empty(explanation);
         recurring_explanation_label.no_show_all = String.is_empty(explanation);
diff --git a/src/rc/create-update-recurring.ui b/src/rc/create-update-recurring.ui
index 1e52fc5..ae1b67a 100644
--- a/src/rc/create-update-recurring.ui
+++ b/src/rc/create-update-recurring.ui
@@ -19,6 +19,7 @@
         <property name="use_underline">True</property>
         <property name="xalign">0</property>
         <property name="draw_indicator">True</property>
+        <signal name="toggled" handler="on_update_explanation" object="CaliforniaHostCreateUpdateRecurring" 
swapped="no"/>
       </object>
       <packing>
         <property name="left_attach">0</property>
@@ -137,6 +138,7 @@
               <item id="4" translatable="yes">Yearly</item>
             </items>
             <signal name="changed" handler="on_repeats_combobox_changed" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+            <signal name="changed" handler="on_update_explanation" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
           </object>
           <packing>
             <property name="left_attach">1</property>
@@ -285,6 +287,7 @@
                 <property name="xalign">0</property>
                 <property name="active">True</property>
                 <property name="draw_indicator">True</property>
+                <signal name="toggled" handler="on_update_explanation" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -307,6 +310,7 @@
                     <property name="xalign">0</property>
                     <property name="draw_indicator">True</property>
                     <property name="group">never_radiobutton</property>
+                    <signal name="toggled" handler="on_update_explanation" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -351,6 +355,7 @@
                     <property name="active">True</property>
                     <property name="draw_indicator">True</property>
                     <property name="group">never_radiobutton</property>
+                    <signal name="toggled" handler="on_update_explanation" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -367,6 +372,7 @@
                     <property name="width_chars">5</property>
                     <property name="input_purpose">number</property>
                     <signal name="changed" handler="on_after_entry_changed" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+                    <signal name="changed" handler="on_update_explanation" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -431,6 +437,7 @@
                 <property name="width_chars">5</property>
                 <property name="input_purpose">number</property>
                 <signal name="changed" handler="on_every_entry_changed" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+                <signal name="changed" handler="on_update_explanation" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -516,7 +523,7 @@
       </object>
       <packing>
         <property name="left_attach">0</property>
-        <property name="top_attach">3</property>
+        <property name="top_attach">4</property>
         <property name="width">2</property>
         <property name="height">1</property>
       </packing>
@@ -538,6 +545,24 @@
       </object>
       <packing>
         <property name="left_attach">0</property>
+        <property name="top_attach">3</property>
+        <property name="width">2</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="recurring_explanation_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label">(empty)</property>
+        <property name="wrap">True</property>
+        <property name="max_width_chars">64</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
         <property name="top_attach">2</property>
         <property name="width">2</property>
         <property name="height">1</property>


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