[california] Display Attendees and Organizers in Show Event dialog: Bug #731543



commit b5a8267d5edcdb08ab72728f973b64c764569020
Author: Jim Nelson <jim yorba org>
Date:   Wed Nov 5 17:17:01 2014 -0800

    Display Attendees and Organizers in Show Event dialog: Bug #731543
    
    Attendees/Organizers can only be displayed.  The second half of this
    ticket is to allow adding/editing the same in an event.

 src/Makefile.am                         |    1 +
 src/collection/collection-iterable.vala |   42 ++++++-
 src/component/component-instance.vala   |  113 ++++++++++++++++
 src/component/component-person.vala     |  166 +++++++++++++++++++++++
 src/component/component.vala            |    2 +
 src/host/host-show-event.vala           |   29 ++++
 src/rc/create-update-event.ui           |  217 ++++++++++++++++---------------
 src/rc/show-event.ui                    |   89 +++++++++----
 src/util/util-string.vala               |    4 +
 src/util/util-uri.vala                  |   19 +++
 src/util/util.vala                      |    2 +
 11 files changed, 551 insertions(+), 133 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 1bb8c3b..d4dc27a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -91,6 +91,7 @@ california_VALASOURCES = \
        component/component-event.vala \
        component/component-icalendar.vala \
        component/component-instance.vala \
+       component/component-person.vala \
        component/component-recurrence-rule.vala \
        component/component-uid.vala \
        component/component-vtype.vala \
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index d51e82d..9a67a6d 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -103,7 +103,7 @@ public class Iterable<G> : Object {
     /**
      * For { link to_string}.
      */
-    public delegate string? ToString<G>(G element);
+    public delegate string? ToString<G>(G element, bool is_first, bool is_last);
     
     /**
      * For simple iteration of the { link Iterable}.
@@ -111,6 +111,11 @@ public class Iterable<G> : Object {
     public delegate void Iterate<G>(G element);
     
     /**
+     * For simple transformation of elements in the { link Iterable}.
+     */
+    public delegate G Transform<G>(G element);
+    
+    /**
      * For mapping a single value of one type to multiple values of another.
      */
     public delegate Gee.Collection<A> Bloom<A, G>(G element);
@@ -166,6 +171,30 @@ public class Iterable<G> : Object {
         return new Iterable<G>(list.iterator());
     }
     
+    /**
+     * Like { link iterate}, called for each element, but adds the returned (possibly transformed)
+     * element.
+     */
+    public Iterable<G> transform(Transform<G> transformer) {
+        Gee.ArrayList<G> list = new Gee.ArrayList<G>();
+        foreach (G g in this)
+            list.add(transformer(g));
+        
+        return new Iterable<G>(list.iterator());
+    }
+    
+    /**
+     * Sorts the elements of the { link Iterable} so that the next iteration they are in the
+     * comparator's order.
+     */
+    public Iterable<G> sort(owned Gee.EqualDataFunc<G>? equal_func = null,
+        owned CompareDataFunc<G>? compare_func = null) {
+        Gee.ArrayList<G> sorted = to_array_list(equal_func);
+        sorted.sort(compare_func);
+        
+        return new Iterable<G>(sorted.iterator());
+    }
+    
     public Iterable<A> map<A>(Gee.MapFunc<A, G> f) {
         return new Iterable<A>(i.map<A>(f));
     }
@@ -365,12 +394,19 @@ public class Iterable<G> : Object {
      *
      * If { link ToString} returns null or an empty string, nothing is appended to the final string.
      *
+     * is_first is passed true to ToString if the string is the first element of the Iterable.  If
+     * prior elements resulted in null being returned, then is_first will continue to be true.  In
+     * other words, is_first is true if the built string so far is empty.
+     *
+     * is_last is only true when the last element of the Iterable has been reached.
+     *
      * If the final string is empty, null is returned instead.
      */
     public string? to_string(ToString<G> string_cb) {
         StringBuilder builder = new StringBuilder();
-        foreach (G element in this) {
-            string? str = string_cb(element);
+        Gee.Iterator<G> iter = iterator();
+        while (iter.next()) {
+            string? str = string_cb(iter.get(), String.is_empty(builder.str), !iter.has_next());
             if (!String.is_empty(str))
                 builder.append(str);
         }
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 89cc3c2..97f6933 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -39,6 +39,8 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public const string PROP_RDATES = "rdates";
     public const string PROP_SEQUENCE = "sequence";
     public const string PROP_MASTER = "master";
+    public const string PROP_ORGANIZERS = "organizers";
+    public const string PROP_ATTENDEES = "attendees";
     
     protected const string PROP_IN_FULL_UPDATE = "in-full-update";
     
@@ -171,6 +173,31 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public int sequence { get; set; default = 0; }
     
     /**
+     * ORGANIZERs of a VEVENT, VTODO, or VJOURNAL.
+     *
+     * To add or remove organizers, use { link add_organizers}, { link remove_organizers}, and
+     * { link clear_organizers}. Because those methods always update the property itself and not
+     * merely modify the list, the property can be watched for changes with the "notify" and/or
+     * "altered" signals.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.3]]  In particular, note that the
+     * { link organizer} must be specified in group-scheduled calendar entity.
+     */
+    public Gee.Set<Person> organizers { get; private set; default = new Gee.HashSet<Person>(); }
+    
+    /**
+     * ATTENDEEs for a VEVENT, VTODO, or VJOURNAL.
+     *
+     * To add or remove attendees, use { link add_attendees}, { link remove_attendees}, and
+     * { link clear_attendees}. Because those methods always update the property itself and not
+     * merely modify the list, the property can be watched for changes with the "notify" and/or
+     * "altered" signals.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.1]]
+     */
+    public Gee.Set<Person> attendees { get; private set; default = new Gee.HashSet<Person>(); }
+    
+    /**
      * The iCal component being represented by this { link Instance}.
      */
     private iCal.icalcomponent _ical_component;
@@ -352,11 +379,30 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         exdates = get_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY);
         rdates = get_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY);
         
+        persons_from_component(ical_component, organizers, iCal.icalproperty_kind.ORGANIZER_PROPERTY);
+        persons_from_component(ical_component, attendees, iCal.icalproperty_kind.ATTENDEE_PROPERTY);
+        
         // save own copy of component; no ownership transferrance w/ current bindings
         if (_ical_component != ical_component)
             _ical_component = ical_component.clone();
     }
     
+    private void persons_from_component(iCal.icalcomponent ical_component, Gee.Set<Person> persons,
+        iCal.icalproperty_kind kind) {
+        persons.clear();
+        
+        unowned iCal.icalproperty? prop = ical_component.get_first_property(kind);
+        while (prop != null) {
+            try {
+                persons.add(new Person.from_property(prop));
+            } catch (Error err) {
+                debug("Unable to parse %s for %s: %s", kind.to_string(), to_string(), err.message);
+            }
+            
+            prop = ical_component.get_next_property(kind);
+        }
+    }
+    
     private void on_notify(ParamSpec pspec) {
         // don't worry if in full update, that call is supposed to update properties
         if (in_full_update)
@@ -398,6 +444,18 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
                     set_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY, rdates);
             break;
             
+            case PROP_ORGANIZERS:
+                remove_all_properties(iCal.icalproperty_kind.ORGANIZER_PROPERTY);
+                foreach (Person organizer in organizers)
+                    ical_component.add_property(organizer.as_ical_property());
+            break;
+            
+            case PROP_ATTENDEES:
+                remove_all_properties(iCal.icalproperty_kind.ATTENDEE_PROPERTY);
+                foreach (Person attendee in attendees)
+                    ical_component.add_property(attendee.as_ical_property());
+            break;
+            
             default:
                 altered = false;
             break;
@@ -449,6 +507,61 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     }
     
     /**
+     * Add one or more { link Person}s as { link organizers}.
+     */
+    public void add_organizers(Gee.Collection<Person> to_add) {
+        organizers = add_persons(organizers, to_add);
+    }
+    
+    /**
+     * Remove one or more { link Person}s as (@link organizers}.
+     */
+    public void remove_organizers(Gee.Collection<Person> to_remove) {
+        organizers = remove_persons(organizers, to_remove);
+    }
+    
+    /*
+     * Removes all { link organizers}.
+     */
+    public void clear_organizers() {
+        organizers = new Gee.HashSet<Person>();
+    }
+    
+    /**
+     * Add one or more { link Person}s as { link attendees}.
+     */
+    public void add_attendees(Gee.Collection<Person> to_add) {
+        attendees = add_persons(attendees, to_add);
+    }
+    
+    /**
+     * Remove one or more { link Person}s as (@link attendees}.
+     */
+    public void remove_attendees(Gee.Collection<Person> to_remove) {
+        attendees = remove_persons(attendees, to_remove);
+    }
+    
+    /*
+     * Removes all { link attendees}.
+     */
+    public void clear_attendees() {
+        attendees = new Gee.HashSet<Person>();
+    }
+    
+    private Gee.Set<Person> add_persons(Gee.Set<Person> existing, Gee.Collection<Person> to_add) {
+        Gee.Set<Person> copy = traverse<Person>(attendees).to_hash_set();
+        copy.add_all(to_add);
+        
+        return copy;
+    }
+    
+    private Gee.Set<Person> remove_persons(Gee.Set<Person> existing, Gee.Collection<Person> to_remove) {
+        return traverse<Person>(existing)
+            .filter(person => !to_remove.contains(person))
+            .to_hash_set();
+    }
+    
+    /**
      * Returns an appropriate { link Component} instance for the iCalendar component.
      *
      * VCALENDARs should use { link Component.iCalendar}.
diff --git a/src/component/component-person.vala b/src/component/component-person.vala
new file mode 100644
index 0000000..4ccfcf7
--- /dev/null
+++ b/src/component/component-person.vala
@@ -0,0 +1,166 @@
+/* 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.Component {
+
+/**
+ * An immutable representation of an iCalendar CAL-ADDRESS (ATTENDEE, ORGANIZER, etc.)
+ *
+ * Person is not guaranteed to represent an individual per se, but it always represents an RFC822
+ * mailbox (i.e. email address), which may be a group list address, multiuser mailbox, etc.
+ *
+ * For equality purposes, only the { link mailto} is used.  All other parameters are ignored when
+ * comparing Persons for equality.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.3]],
+ * [[https://tools.ietf.org/html/rfc5545#section-3.8.4.1]],
+ * [[https://tools.ietf.org/html/rfc5545#section-3.8.4.3]],
+ * [[https://tools.ietf.org/html/rfc5545#section-3.2.2]], and more.
+ */
+
+public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
+    /**
+     * The mailto: of the { link Person}, the only required value for the property.
+     */
+    public Soup.URI mailto { get; private set; }
+    
+    /**
+     * The CN (common name) for the { link Person}.
+     */
+    public string? common_name { get; private set; default = null; }
+    
+    /**
+     * The { link mailto} URI as a text string.
+     *
+     * @see mailbox
+     */
+    public string mailto_text { owned get { return mailto.to_string(false); } }
+    
+    /**
+     * The { link mailto} as a simple (unadorned) RFC822 mailbox (i.e. email address).
+     *
+     * This does not include the "mailto:"; scheme nor the { link common_name}, i.e.
+     * "bob example com"
+     */
+    public string mailbox { get { return mailto.path; } }
+    
+    /**
+     * The { link mailto} as a complete (adorned) RFC822 mailbox (i.e. email address) with
+     * user-readable name, if supplied.
+     *
+     * This does not include the "mailto:"; scheme but it will include the { link common_name} if
+     * present, i.e. "Bob Jones <bob example com>".
+     */
+    public string full_mailbox { get; private set; }
+    
+    private Gee.HashSet<string> parameters = new Gee.HashSet<string>(String.ci_hash, String.ci_equal);
+    
+    /**
+     * Create an { link Person} with the required { link mailto} and optional { link common_name}.
+     */
+    public Person(Soup.URI mailto, string? common_name) throws ComponentError {
+        validate_mailto(mailto);
+        
+        this.mailto = mailto;
+        this.common_name = common_name;
+        full_mailbox = make_full_address(mailto, common_name);
+        
+        // store in parameters in case object is serialized as an iCal property.
+        if (!String.is_empty(common_name))
+            parameters.add(new iCal.icalparameter.cn(common_name).as_ical_string());
+    }
+    
+    internal Person.from_property(iCal.icalproperty prop) throws Error {
+        unowned iCal.icalvalue? prop_value = prop.get_value();
+        if (prop_value == null || prop_value.is_valid() == 0) {
+            throw new ComponentError.INVALID("Property of kind %s has no associated value",
+                prop.isa().to_string());
+        }
+        
+        if (prop_value.isa() != iCal.icalvalue_kind.CALADDRESS_VALUE) {
+            throw new ComponentError.INVALID("Property of kind %s has value of kind %s",
+                prop.isa().to_string(), prop_value.isa().to_string());
+        }
+        
+        string uri = prop_value.get_caladdress();
+        if (String.is_empty(uri))
+            throw new ComponentError.INVALID("Invalid Person property: no CAL-ADDRESS value");
+        
+        mailto = URI.parse(uri);
+        validate_mailto(mailto);
+        
+        // load parameters into local table
+        unowned iCal.icalparameter? param = prop.get_first_parameter(iCal.icalparameter_kind.ANY_PARAMETER);
+        while (param != null) {
+            parameters.add(param.as_ical_string());
+            
+            // parse parameter into well-known (common) property
+            switch (param.isa()) {
+                case iCal.icalparameter_kind.CN_PARAMETER:
+                    common_name = param.get_cn();
+                break;
+                
+                default:
+                    // fall-through
+                break;
+            }
+            
+            param = prop.get_next_parameter(iCal.icalparameter_kind.ANY_PARAMETER);
+        }
+        
+        full_mailbox = make_full_address(mailto, common_name);
+    }
+    
+    private static void validate_mailto(Soup.URI uri) throws ComponentError {
+        if (uri.scheme != "mailto" || String.is_empty(uri.path))
+            throw new ComponentError.INVALID("Invalid mailto: %s", uri.to_string(false));
+    }
+    
+    private static string make_full_address(Soup.URI mailto, string? common_name) {
+        // watch for common name simply being the email address
+        if (String.is_empty(common_name) || String.ascii_ci_equal(mailto.path, common_name))
+            return mailto.path;
+        
+        return "%s <%s>".printf(common_name, mailto.path);
+    }
+    
+    internal iCal.icalproperty as_ical_property() {
+        iCal.icalproperty prop = new iCal.icalproperty.attendee(mailto_text);
+        foreach (string parameter in parameters)
+            prop.add_parameter(new iCal.icalparameter.from_string(parameter));
+        
+        return prop;
+    }
+    
+    public uint hash() {
+        return String.ci_hash(mailto.path);
+    }
+    
+    public bool equal_to(Person other) {
+        return (this != other) ? String.ci_equal(mailto.path, other.mailto.path) : true;
+    }
+    
+    public int compare_to(Person other) {
+        if (this == other)
+            return 0;
+        
+        // if a common name is supplied, use that first, but need to stabilize sort
+        if (!String.is_empty(common_name) && !String.is_empty(other.common_name)) {
+            int compare = String.stricmp(common_name, other.common_name);
+            if (compare != 0)
+                return compare;
+        }
+        
+        return String.stricmp(mailbox, other.mailbox);
+    }
+    
+    public override string to_string() {
+        return mailto_text;
+    }
+}
+
+}
+
diff --git a/src/component/component.vala b/src/component/component.vala
index 7327f2d..09a601b 100644
--- a/src/component/component.vala
+++ b/src/component/component.vala
@@ -63,6 +63,7 @@ public void init() throws Error {
     // external unit init
     Collection.init();
     Calendar.init();
+    Util.init();
     
     ICAL_PRODID = "-//Yorba Foundation//NONSGML California Calendar %s//EN".printf(Application.VERSION);
     
@@ -208,6 +209,7 @@ public void terminate() {
     
     ICAL_PRODID = null;
     
+    Util.terminate();
     Calendar.terminate();
     Collection.terminate();
 }
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 640c72d..2c76355 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -43,6 +43,18 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     private Gtk.Label where_text;
     
     [GtkChild]
+    private Gtk.Label organizers_label;
+    
+    [GtkChild]
+    private Gtk.Label organizers_text;
+    
+    [GtkChild]
+    private Gtk.Label attendees_label;
+    
+    [GtkChild]
+    private Gtk.Label attendees_text;
+    
+    [GtkChild]
     private Gtk.Label calendar_label;
     
     [GtkChild]
@@ -155,6 +167,19 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         set_label(when_label, when_text, event.get_event_time_pretty_string(Calendar.Date.PrettyFlag.NONE,
             Calendar.ExactTimeSpan.PrettyFlag.NONE, Calendar.Timezone.local));
         
+        // organizers as a sorted LF-delimited string
+        string organizers = traverse<Component.Person>(event.organizers)
+            .sort()
+            .to_string(stringify_person) ?? "";
+        set_label(organizers_label, organizers_text, organizers);
+        
+        // attendees as a sort LF-delimited string w/ organizers removed
+        string attendees = traverse<Component.Person>(event.attendees)
+            .filter(person => !event.organizers.contains(person))
+            .sort()
+            .to_string(stringify_person) ?? "";
+        set_label(attendees_label, attendees_text, attendees);
+        
         // calendar
         set_label(calendar_label, calendar_text, event.calendar_source != null ? event.calendar_source.title 
: null);
         
@@ -197,6 +222,10 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         return true;
     }
     
+    private string? stringify_person(Component.Person person, bool is_first, bool is_last) {
+        return "%s%s".printf(person.full_mailbox, is_last ? "" : "\n");
+    }
+    
     // Note that text is not escaped, up to caller to determine if necessary or not.
     private void set_label(Gtk.Label? label, Gtk.Label text, string? str) {
         if (!String.is_empty(str)) {
diff --git a/src/rc/create-update-event.ui b/src/rc/create-update-event.ui
index b85cc36..895cf49 100644
--- a/src/rc/create-update-event.ui
+++ b/src/rc/create-update-event.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.16.1 -->
+<!-- Generated with glade 3.18.3 -->
 <interface>
   <requires lib="gtk+" version="3.10"/>
   <template class="CaliforniaHostCreateUpdateEvent" parent="GtkGrid">
@@ -23,8 +23,6 @@
       <packing>
         <property name="left_attach">1</property>
         <property name="top_attach">0</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
@@ -40,8 +38,6 @@
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">0</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
@@ -57,8 +53,6 @@
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">1</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
@@ -105,40 +99,110 @@
       <packing>
         <property name="left_attach">1</property>
         <property name="top_attach">1</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
-      <object class="GtkEntry" id="location_entry">
+      <object class="GtkLabel" id="repeats_label">
         <property name="visible">True</property>
-        <property name="can_focus">True</property>
-        <property name="activates_default">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">1</property>
+        <property name="label" translatable="yes">Recurrence</property>
+        <style>
+          <class name="dim-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="recurring_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">4</property>
+        <child>
+          <object class="GtkLabel" id="recurring_explanation_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="label">(none)</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="recurring_button">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="tooltip_text" translatable="yes">Add or remove recurrences of the 
event</property>
+            <property name="relief">none</property>
+            <signal name="clicked" handler="on_recurring_button_clicked" 
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
+            <child>
+              <object class="GtkImage" id="image2">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">rotation-allowed-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
       </object>
       <packing>
         <property name="left_attach">1</property>
-        <property name="top_attach">3</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
+        <property name="top_attach">2</property>
       </packing>
     </child>
     <child>
-      <object class="GtkLabel" id="location_label">
+      <object class="GtkBox" id="rotating_button_box_container">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_top">8</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">False</property>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">7</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="calendar_label">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
         <property name="xalign">1</property>
-        <property name="label" translatable="yes">_Location</property>
-        <property name="use_underline">True</property>
-        <property name="single_line_mode">True</property>
+        <property name="label" translatable="yes">Calendar</property>
         <style>
           <class name="dim-label"/>
         </style>
       </object>
       <packing>
         <property name="left_attach">0</property>
-        <property name="top_attach">3</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
+        <property name="top_attach">6</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkComboBoxText" id="calendar_combo">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">6</property>
       </packing>
     </child>
     <child>
@@ -157,9 +221,7 @@
       </object>
       <packing>
         <property name="left_attach">0</property>
-        <property name="top_attach">4</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
+        <property name="top_attach">5</property>
       </packing>
     </child>
     <child>
@@ -186,122 +248,67 @@
       </object>
       <packing>
         <property name="left_attach">1</property>
-        <property name="top_attach">4</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
+        <property name="top_attach">5</property>
       </packing>
     </child>
     <child>
-      <object class="GtkLabel" id="calendar_label">
+      <object class="GtkLabel" id="location_label">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
         <property name="xalign">1</property>
-        <property name="label" translatable="yes">Calendar</property>
+        <property name="label" translatable="yes">_Location</property>
+        <property name="use_underline">True</property>
+        <property name="single_line_mode">True</property>
         <style>
           <class name="dim-label"/>
         </style>
       </object>
       <packing>
         <property name="left_attach">0</property>
-        <property name="top_attach">5</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
+        <property name="top_attach">4</property>
       </packing>
     </child>
     <child>
-      <object class="GtkComboBoxText" id="calendar_combo">
+      <object class="GtkEntry" id="location_entry">
         <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="halign">start</property>
+        <property name="can_focus">True</property>
+        <property name="activates_default">True</property>
       </object>
       <packing>
         <property name="left_attach">1</property>
-        <property name="top_attach">5</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkBox" id="rotating_button_box_container">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="margin_top">8</property>
-        <property name="hexpand">True</property>
-        <property name="vexpand">False</property>
-        <child>
-          <placeholder/>
-        </child>
-      </object>
-      <packing>
-        <property name="left_attach">0</property>
-        <property name="top_attach">6</property>
-        <property name="width">2</property>
-        <property name="height">1</property>
+        <property name="top_attach">4</property>
       </packing>
     </child>
     <child>
-      <object class="GtkLabel" id="repeats_label">
-        <property name="visible">True</property>
+      <object class="GtkLabel" id="attendees_label">
         <property name="can_focus">False</property>
+        <property name="no_show_all">True</property>
         <property name="xalign">1</property>
-        <property name="label" translatable="yes">Recurrence</property>
+        <property name="label" translatable="yes">_Guests</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">attendees_entry</property>
         <style>
           <class name="dim-label"/>
         </style>
       </object>
       <packing>
         <property name="left_attach">0</property>
-        <property name="top_attach">2</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
+        <property name="top_attach">3</property>
       </packing>
     </child>
     <child>
-      <object class="GtkBox" id="recurring_box">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="spacing">4</property>
-        <child>
-          <object class="GtkLabel" id="recurring_explanation_label">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="xalign">0</property>
-            <property name="label">(none)</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="recurring_button">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <property name="tooltip_text" translatable="yes">Add or remove recurrences of the 
event</property>
-            <property name="relief">none</property>
-            <signal name="clicked" handler="on_recurring_button_clicked" 
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
-            <child>
-              <object class="GtkImage" id="image2">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="icon_name">rotation-allowed-symbolic</property>
-              </object>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="position">1</property>
-          </packing>
-        </child>
+      <object class="GtkEntry" id="attendees_entry">
+        <property name="can_focus">True</property>
+        <property name="no_show_all">True</property>
+        <property name="tooltip_text" translatable="yes">For example:
+alice example com, bob example com</property>
+        <property name="activates_default">True</property>
+        <property name="caps_lock_warning">False</property>
+        <property name="placeholder_text" translatable="yes">Email address(es)</property>
       </object>
       <packing>
         <property name="left_attach">1</property>
-        <property name="top_attach">2</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
+        <property name="top_attach">3</property>
       </packing>
     </child>
   </template>
diff --git a/src/rc/show-event.ui b/src/rc/show-event.ui
index f09531f..450532f 100644
--- a/src/rc/show-event.ui
+++ b/src/rc/show-event.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.16.1 -->
+<!-- Generated with glade 3.18.3 -->
 <interface>
   <requires lib="gtk+" version="3.10"/>
   <template class="CaliforniaHostShowEvent" parent="GtkGrid">
@@ -24,8 +24,6 @@
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">0</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
@@ -67,8 +65,6 @@
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">2</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
@@ -91,8 +87,6 @@
           <packing>
             <property name="left_attach">0</property>
             <property name="top_attach">0</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
           </packing>
         </child>
         <child>
@@ -109,8 +103,6 @@
           <packing>
             <property name="left_attach">0</property>
             <property name="top_attach">1</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
           </packing>
         </child>
         <child>
@@ -125,8 +117,6 @@
           <packing>
             <property name="left_attach">1</property>
             <property name="top_attach">0</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
           </packing>
         </child>
         <child>
@@ -141,8 +131,37 @@
           <packing>
             <property name="left_attach">1</property>
             <property name="top_attach">1</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="organizers_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">1</property>
+            <property name="yalign">0</property>
+            <property name="label" translatable="yes">Organizers</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="organizers_text">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="yalign">0</property>
+            <property name="label">(empty)</property>
+            <property name="selectable">True</property>
+            <property name="ellipsize">end</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">2</property>
           </packing>
         </child>
         <child>
@@ -157,9 +176,7 @@
           </object>
           <packing>
             <property name="left_attach">0</property>
-            <property name="top_attach">2</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
+            <property name="top_attach">4</property>
           </packing>
         </child>
         <child>
@@ -173,17 +190,43 @@
           </object>
           <packing>
             <property name="left_attach">1</property>
-            <property name="top_attach">2</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
+            <property name="top_attach">4</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="attendees_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">1</property>
+            <property name="yalign">0</property>
+            <property name="label" translatable="yes" context="Attendees of an event (who are not organizing 
it)">Guests</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">3</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="attendees_text">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="yalign">0</property>
+            <property name="label">(empty)</property>
+            <property name="selectable">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">3</property>
           </packing>
         </child>
       </object>
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">1</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
@@ -201,8 +244,6 @@
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">4</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
     <child>
@@ -219,8 +260,6 @@
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">3</property>
-        <property name="width">1</property>
-        <property name="height">1</property>
       </packing>
     </child>
   </template>
diff --git a/src/util/util-string.vala b/src/util/util-string.vala
index b4ca45e..8aa676f 100644
--- a/src/util/util-string.vala
+++ b/src/util/util-string.vala
@@ -24,6 +24,10 @@ public bool ci_equal(string a, string b) {
     return stricmp(a, b) == 0;
 }
 
+public bool ascii_ci_equal(string a, string b) {
+    return a.ascii_casecmp(b) == 0;
+}
+
 /**
  * Removes redundant whitespace (including tabs and newlines) and strips whitespace from beginning
  * and end of string.
diff --git a/src/util/util-uri.vala b/src/util/util-uri.vala
index 83390ed..0a61e1a 100644
--- a/src/util/util-uri.vala
+++ b/src/util/util-uri.vala
@@ -14,6 +14,18 @@ errordomain URIError {
 
 namespace California.URI {
 
+private Regex email_regex;
+
+internal void init() throws Error {
+    // http://www.regular-expressions.info/email.html
+    // matches john dep aol museum not john aol   com
+    email_regex = new Regex("[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\\.)+[A-Z]{2,5}", RegexCompileFlags.CASELESS);
+}
+
+internal void terminate() {
+    email_regex = null;
+}
+
 /**
  * Basic validation of a string intended to be parsed as an absolute URI.
  *
@@ -57,5 +69,12 @@ public Soup.URI parse(string uri) throws Error {
     return parsed;
 }
 
+/**
+ * Validates a string as a valid RFC822 mailbox (i.e. email) address.
+ */
+public bool is_valid_mailbox(string str) {
+    return email_regex.match(str);
+}
+
 }
 
diff --git a/src/util/util.vala b/src/util/util.vala
index 8bc0b78..66337b3 100644
--- a/src/util/util.vala
+++ b/src/util/util.vala
@@ -14,12 +14,14 @@ public void init() throws Error {
     
     // internal init
     Markup.init();
+    URI.init();
 }
 
 public void terminate() {
     if (!Unit.do_terminate(ref init_count))
         return;
     
+    URI.terminate();
     Markup.terminate();
 }
 



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