[california] Add/remove attendees from event: Bug #731543



commit 6006f22ff1a723977c8df506a22682b5b0babe71
Author: Jim Nelson <jim yorba org>
Date:   Thu Nov 13 15:14:16 2014 -0800

    Add/remove attendees from event: Bug #731543
    
    This adds *basic* support for adding/removing attendees from an event.
    Attendees can only be added by their email address and there's no
    provision for sending invitations or editing organizers (yet).

 po/POTFILES.in                          |    2 +
 po/POTFILES.skip                        |    1 +
 src/Makefile.am                         |    2 +
 src/california-resources.xml            |    3 +
 src/collection/collection-iterable.vala |   12 ++
 src/component/component-instance.vala   |    6 +-
 src/component/component-person.vala     |   96 ++++++++++++++++--
 src/host/host-attendees-editor.vala     |  135 +++++++++++++++++++++++++
 src/host/host-create-update-event.vala  |   41 ++++++++
 src/host/host-main-window.vala          |    5 +-
 src/host/host-show-event.vala           |    5 +
 src/rc/attendees-editor.ui              |  168 +++++++++++++++++++++++++++++++
 src/rc/create-update-event.ui           |   53 ++++++++--
 src/toolkit/toolkit-listbox-model.vala  |   17 +++-
 src/util/util-uri.vala                  |    9 ++
 vapi/libical.vapi                       |    4 +-
 16 files changed, 533 insertions(+), 26 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b458ec9..d286503 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -15,6 +15,7 @@ src/calendar/calendar-exact-time-span.vala
 src/calendar/calendar.vala
 src/component/component.vala
 src/component/component-recurrence-rule.vala
+src/host/host-attendees-editor.vala
 src/host/host-create-update-event.vala
 src/host/host-create-update-recurring.vala
 src/host/host-import-calendar.vala
@@ -27,6 +28,7 @@ src/view/month/month-controller.vala
 src/view/week/week-controller.vala
 [type: gettext/glade]src/rc/activator-list.ui
 [type: gettext/glade]src/rc/app-menu.interface
+[type: gettext/glade]src/rc/attendees-editor.ui
 [type: gettext/glade]src/rc/calendar-import.ui
 [type: gettext/glade]src/rc/calendar-manager-list-item.ui
 [type: gettext/glade]src/rc/calendar-manager-list.ui
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index ac5b36b..4c5659f 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -13,6 +13,7 @@ src/calendar/calendar-exact-time-span.c
 src/component/component.c
 src/component/component-event.c
 src/component/component-recurrence-rule.c
+src/host/host-attendees-editor.c
 src/host/host-create-update-event.c
 src/host/host-import-calendar.c
 src/host/host-main-window.c
diff --git a/src/Makefile.am b/src/Makefile.am
index d4dc27a..f5fc807 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -97,6 +97,7 @@ california_VALASOURCES = \
        component/component-vtype.vala \
        \
        host/host.vala \
+       host/host-attendees-editor.vala \
        host/host-calendar-list-item.vala \
        host/host-create-update-event.vala \
        host/host-create-update-recurring.vala \
@@ -192,6 +193,7 @@ california_SOURCES = \
 california_RC = \
        rc/activator-list.ui \
        rc/app-menu.interface \
+       rc/attendees-editor.ui \
        rc/calendar-import.ui \
        rc/calendar-list-item.ui \
        rc/calendar-manager-list.ui \
diff --git a/src/california-resources.xml b/src/california-resources.xml
index c09f7c7..dd29fad 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -7,6 +7,9 @@
         <file compressed="true">rc/app-menu.interface</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/attendees-editor.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
         <file compressed="true">rc/calendar-import.ui</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 9a67a6d..cd1afbd 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -285,6 +285,18 @@ public class Iterable<G> : Object {
         return true;
     }
     
+    /**
+     * The total number of items held in the { link Iterable}.
+     */
+    public int count() {
+        int count = 0;
+        Gee.Iterator<G> iter = iterator();
+        while (iter.next())
+            count++;
+        
+        return count;
+    }
+    
     public int count_matching(owned Gee.Predicate<G> f) {
         int count = 0;
         foreach (G g in this) {
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 97f6933..1334ddc 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -548,14 +548,14 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         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();
+    private static Gee.Set<Person> add_persons(Gee.Set<Person> existing, Gee.Collection<Person> to_add) {
+        Gee.Set<Person> copy = traverse<Person>(existing).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) {
+    private static 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();
diff --git a/src/component/component-person.vala b/src/component/component-person.vala
index 4ccfcf7..d38554d 100644
--- a/src/component/component-person.vala
+++ b/src/component/component-person.vala
@@ -23,6 +23,19 @@ namespace California.Component {
 
 public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
     /**
+     * The relationship of this { link Person} to the { link Instance}.
+     */
+    public enum Relationship {
+        ORGANIZER,
+        ATTENDEE
+    }
+    
+    /**
+     * The { link Person}'s { link Relationship} to the { link Instance}.
+     */
+    public Relationship relationship { get; private set; }
+    
+    /**
      * The mailto: of the { link Person}, the only required value for the property.
      */
     public Soup.URI mailto { get; private set; }
@@ -33,6 +46,16 @@ public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
     public string? common_name { get; private set; default = null; }
     
     /**
+     * The participation ROLE for the { link Person}.
+     */
+    public iCal.icalparameter_role role { get; private set; default = 
iCal.icalparameter_role.REQPARTICIPANT; }
+    
+    /**
+     * RSVP required for the { link Person}.
+     */
+    public bool rsvp { get; private set; default = false; }
+    
+    /**
      * The { link mailto} URI as a text string.
      *
      * @see mailbox
@@ -59,21 +82,46 @@ public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
     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}.
+     * Create a { link Person} with the required { link mailto} and optional { link common_name}.
      */
-    public Person(Soup.URI mailto, string? common_name) throws ComponentError {
+    public Person(Relationship relationship, Soup.URI mailto, string? common_name = null,
+        iCal.icalparameter_role role = iCal.icalparameter_role.REQPARTICIPANT, bool rsvp = false)
+        throws ComponentError {
         validate_mailto(mailto);
         
+        this.relationship = relationship;
         this.mailto = mailto;
         this.common_name = common_name;
+        this.role = role;
+        this.rsvp = rsvp;
         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());
+        
+        if (role != iCal.icalparameter_role.REQPARTICIPANT)
+            parameters.add(new iCal.icalparameter.role(role).as_ical_string());
+        
+        if (rsvp)
+            parameters.add(new iCal.icalparameter.rsvp(iCal.icalparameter_rsvp.TRUE).as_ical_string());
     }
     
     internal Person.from_property(iCal.icalproperty prop) throws Error {
+        switch (prop.isa()) {
+            case iCal.icalproperty_kind.ATTENDEE_PROPERTY:
+                relationship = Relationship.ATTENDEE;
+            break;
+            
+            case iCal.icalproperty_kind.ORGANIZER_PROPERTY:
+                relationship = Relationship.ORGANIZER;
+            break;
+            
+            default:
+                throw new ComponentError.INVALID("Property must be an ATTENDEE or ORGANIZER: %s",
+                    prop.isa().to_string());
+        }
+        
         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",
@@ -103,6 +151,14 @@ public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
                     common_name = param.get_cn();
                 break;
                 
+                case iCal.icalparameter_kind.ROLE_PARAMETER:
+                    role = param.get_role();
+                break;
+                
+                case iCal.icalparameter_kind.RSVP_PARAMETER:
+                    rsvp = param.get_rsvp() == iCal.icalparameter_rsvp.TRUE;
+                break;
+                
                 default:
                     // fall-through
                 break;
@@ -115,7 +171,7 @@ public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
     }
     
     private static void validate_mailto(Soup.URI uri) throws ComponentError {
-        if (uri.scheme != "mailto" || String.is_empty(uri.path))
+        if (!String.ci_equal(uri.scheme, "mailto") || String.is_empty(uri.path) || 
!URI.is_valid_mailbox(uri.path))
             throw new ComponentError.INVALID("Invalid mailto: %s", uri.to_string(false));
     }
     
@@ -128,19 +184,37 @@ public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
     }
     
     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));
+        iCal.icalproperty prop;
+        switch (relationship) {
+            case Relationship.ATTENDEE:
+                prop = new iCal.icalproperty.attendee(mailto_text);
+            break;
+            
+            case Relationship.ORGANIZER:
+                prop = new iCal.icalproperty.organizer(mailto_text);
+            break;
+            
+            default:
+                assert_not_reached();
+        }
+        
+        foreach (string parameter in parameters) {
+            iCal.icalparameter param = new iCal.icalparameter.from_string(parameter);
+            prop.add_parameter((owned) param);
+        }
         
         return prop;
     }
     
     public uint hash() {
-        return String.ci_hash(mailto.path);
+        return String.ci_hash(mailto.path) ^ relationship;
     }
     
     public bool equal_to(Person other) {
-        return (this != other) ? String.ci_equal(mailto.path, other.mailto.path) : true;
+        if (this == other)
+            return true;
+        
+        return relationship == other.relationship && String.ci_equal(mailto.path, other.mailto.path);
     }
     
     public int compare_to(Person other) {
@@ -154,7 +228,11 @@ public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
                 return compare;
         }
         
-        return String.stricmp(mailbox, other.mailbox);
+        int compare = String.stricmp(mailbox, other.mailbox);
+        if (compare != 0)
+            return compare;
+        
+        return relationship - other.relationship;
     }
     
     public override string to_string() {
diff --git a/src/host/host-attendees-editor.vala b/src/host/host-attendees-editor.vala
new file mode 100644
index 0000000..51a12db
--- /dev/null
+++ b/src/host/host-attendees-editor.vala
@@ -0,0 +1,135 @@
+/* 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.Host {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/attendees-editor.ui")]
+public class AttendeesEditor : Gtk.Box, Toolkit.Card {
+    public const string ID = "CaliforniaHostAttendeesEditor";
+    
+    public string card_id { get { return ID; } }
+    
+    public string? title { get { return null; } }
+    
+    public Gtk.Widget? default_widget { get { return accept_button; } }
+    
+    public Gtk.Widget? initial_focus { get { return add_guest_entry; } }
+    
+    [GtkChild]
+    private Gtk.Entry add_guest_entry;
+    
+    [GtkChild]
+    private Gtk.Button add_guest_button;
+    
+    [GtkChild]
+    private Gtk.ListBox guest_listbox;
+    
+    [GtkChild]
+    private Gtk.Button remove_guest_button;
+    
+    [GtkChild]
+    private Gtk.Button accept_button;
+    
+    private new Component.Event? event = null;
+    private Toolkit.ListBoxModel<Component.Person> guest_model;
+    
+    public AttendeesEditor() {
+        guest_model = new Toolkit.ListBoxModel<Component.Person>(guest_listbox, model_presentation);
+        
+        add_guest_entry.bind_property("text", add_guest_button, "sensitive", BindingFlags.SYNC_CREATE,
+            transform_add_guest_text_to_button);
+        guest_model.bind_property(Toolkit.ListBoxModel.PROP_SELECTED, remove_guest_button, "sensitive",
+            BindingFlags.SYNC_CREATE, transform_list_selected_to_button);
+    }
+    
+    private bool transform_add_guest_text_to_button(Binding binding, Value source_value,
+        ref Value target_value) {
+        target_value = URI.is_valid_mailbox(add_guest_entry.text);
+        
+        return true;
+    }
+    
+    private bool transform_list_selected_to_button(Binding binding, Value source_value,
+        ref Value target_value) {
+        target_value = guest_model.selected != null;
+        
+        return true;
+    }
+    
+    public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
+        event = message as Component.Event;
+        if (event == null)
+            return;
+        
+        // clear list and add all attendees who are not organizers
+        guest_model.clear();
+        guest_model.add_many(traverse<Component.Person>(event.attendees)
+            .filter(attendee => !event.organizers.contains(attendee))
+            .to_array_list()
+        );
+    }
+    
+    [GtkCallback]
+    private bool on_add_guest_entry_focus_in_event() {
+        accept_button.has_default = false;
+        add_guest_button.has_default = true;
+        
+        return false;
+    }
+    
+    [GtkCallback]
+    private bool on_add_guest_entry_focus_out_event() {
+        add_guest_button.has_default = false;
+        accept_button.has_default = true;
+        
+        return false;
+    }
+    
+    [GtkCallback]
+    private void on_add_guest_button_clicked() {
+        string mailbox = add_guest_entry.text.strip();
+        if (!URI.is_valid_mailbox(mailbox))
+            return;
+        
+        try {
+            // add to model (which adds to listbox) and clear entry
+            guest_model.add(new Component.Person(Component.Person.Relationship.ATTENDEE,
+                URI.generate_mailto(mailbox)));
+            add_guest_entry.text = "";
+        } catch (Error err) {
+            debug("Unable to generate mailto from \"%s\": %s", mailbox, err.message);
+        }
+    }
+    
+    [GtkCallback]
+    private void on_remove_guest_button_clicked() {
+        if (guest_model.selected != null)
+            guest_model.remove(guest_model.selected);
+    }
+    
+    [GtkCallback]
+    private void on_accept_button_clicked() {
+        event.clear_attendees();
+        event.add_attendees(guest_model.all());
+        
+        jump_to_card_by_name(CreateUpdateEvent.ID, event);
+    }
+    
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        jump_back();
+    }
+    
+    private Gtk.Widget model_presentation(Component.Person person) {
+        Gtk.Label label = new Gtk.Label(person.full_mailbox);
+        label.xalign = 0.0f;
+        
+        return label;
+    }
+}
+
+}
+
diff --git a/src/host/host-create-update-event.vala b/src/host/host-create-update-event.vala
index f6dd38b..dd38e62 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -42,6 +42,9 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
     private Gtk.Entry location_entry;
     
     [GtkChild]
+    private Gtk.Label attendees_text;
+    
+    [GtkChild]
     private Gtk.TextView description_textview;
     
     [GtkChild]
@@ -90,6 +93,9 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
         update_this_button.clicked.connect(on_update_this_button_clicked);
         cancel_recurring_button.clicked.connect(on_cancel_recurring_button_clicked);
         
+        attendees_text.query_tooltip.connect(on_attendees_text_query_tooltip);
+        attendees_text.has_tooltip = true;
+        
         rotating_button_box.pack_end(FAMILY_NORMAL, cancel_button);
         rotating_button_box.pack_end(FAMILY_NORMAL, accept_button);
         
@@ -154,6 +160,14 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
         
         location_entry.text = event.location ?? "";
         description_textview.buffer.text = event.description ?? "";
+        attendees_text.label = traverse<Component.Person>(event.attendees)
+            .filter(attendee => !event.organizers.contains(attendee))
+            .sort()
+            .to_string(stringify_attendees);
+        if (String.is_empty(attendees_text.label)) {
+            // "None" as in "no people"
+            attendees_text.label = _("None");
+        }
         
         Component.Event master = event.is_master_instance ? event : (Component.Event) event.master;
         
@@ -174,6 +188,28 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
         rotating_button_box.family = FAMILY_NORMAL;
     }
     
+    private bool on_attendees_text_query_tooltip(Gtk.Widget widget, int x, int y, bool keyboard,
+        Gtk.Tooltip tooltip) {
+        if (!attendees_text.get_layout().is_ellipsized())
+            return false;
+        
+        tooltip.set_text(traverse<Component.Person>(event.attendees)
+            .filter(attendee => !event.organizers.contains(attendee))
+            .sort()
+            .to_string(stringify_attendees_tooltip));
+        
+        return true;
+    }
+    
+    private string? stringify_attendees(Component.Person person, bool is_first, bool is_last) {
+        // Email address followed by common separator, i.e. "alice example com, bob example com"
+        return !is_last ? _("%s, ").printf(person.full_mailbox) : person.full_mailbox;
+    }
+    
+    private string? stringify_attendees_tooltip(Component.Person person, bool is_first, bool is_last) {
+        return !is_last ? "%s\n".printf(person.full_mailbox) : person.full_mailbox;
+    }
+    
     private void on_update_time_summary() {
         // use the Message, not the Event, to load this up
         time_summary_label.visible = true;
@@ -207,6 +243,11 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
         jump_to_card_by_name(EventTimeSettings.ID, dt);
     }
     
+    [GtkCallback]
+    private void on_attendees_button_clicked() {
+        jump_to_card_by_name(AttendeesEditor.ID, event);
+    }
+    
     private void on_accept_button_clicked() {
         if (calendar_model.active == null)
             return;
diff --git a/src/host/host-main-window.vala b/src/host/host-main-window.vala
index 9ed6bb9..55ad35f 100644
--- a/src/host/host-main-window.vala
+++ b/src/host/host-main-window.vala
@@ -516,9 +516,12 @@ public class MainWindow : Gtk.ApplicationWindow {
         
         EventTimeSettings event_time_settings = new EventTimeSettings();
         
+        AttendeesEditor attendees_editor = new AttendeesEditor();
+        
         Toolkit.Deck deck = new Toolkit.Deck();
         deck.add_cards(
-            iterate<Toolkit.Card>(create_update, create_update_recurring, event_time_settings)
+            iterate<Toolkit.Card>(create_update, create_update_recurring, event_time_settings,
+                attendees_editor)
             .to_array_list()
         );
         
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 2c76355..8511eaa 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -171,6 +171,7 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         string organizers = traverse<Component.Person>(event.organizers)
             .sort()
             .to_string(stringify_person) ?? "";
+        organizers_label.label = ngettext("Organizer", "Organizers", event.organizers.size);
         set_label(organizers_label, organizers_text, organizers);
         
         // attendees as a sort LF-delimited string w/ organizers removed
@@ -178,6 +179,10 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
             .filter(person => !event.organizers.contains(person))
             .sort()
             .to_string(stringify_person) ?? "";
+        int attendee_count = traverse<Component.Person>(event.attendees)
+            .filter(person => !event.organizers.contains(person))
+            .count();
+        attendees_label.label = ngettext("Guest", "Guests", attendee_count);
         set_label(attendees_label, attendees_text, attendees);
         
         // calendar
diff --git a/src/rc/attendees-editor.ui b/src/rc/attendees-editor.ui
new file mode 100644
index 0000000..cfb058c
--- /dev/null
+++ b/src/rc/attendees-editor.ui
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="CaliforniaHostAttendeesEditor" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <property name="spacing">8</property>
+    <child>
+      <object class="GtkLabel" id="label1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Add / remove guests</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="box2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">4</property>
+        <child>
+          <object class="GtkEntry" id="add_guest_entry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="tooltip_text" translatable="yes">For example, bob example com</property>
+            <property name="activates_default">True</property>
+            <property name="placeholder_text" translatable="yes">Email address</property>
+            <property name="input_purpose">email</property>
+            <signal name="focus-in-event" handler="on_add_guest_entry_focus_in_event" 
object="CaliforniaHostAttendeesEditor" swapped="no"/>
+            <signal name="focus-out-event" handler="on_add_guest_entry_focus_out_event" 
object="CaliforniaHostAttendeesEditor" swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="add_guest_button">
+            <property name="label" translatable="yes">A_dd Guest</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="has_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <property name="xalign">0.60000002384185791</property>
+            <signal name="clicked" handler="on_add_guest_button_clicked" 
object="CaliforniaHostAttendeesEditor" swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="pack_type">end</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScrolledWindow" id="scrolledwindow1">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkViewport" id="viewport1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkListBox" id="guest_listbox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="activate_on_single_click">False</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="remove_guest_button">
+        <property name="label" translatable="yes">_Remove Guest</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="halign">end</property>
+        <property name="use_underline">True</property>
+        <signal name="clicked" handler="on_remove_guest_button_clicked" 
object="CaliforniaHostAttendeesEditor" swapped="no"/>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButtonBox" id="buttonbox1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="valign">end</property>
+        <property name="margin_top">8</property>
+        <property name="spacing">8</property>
+        <property name="layout_style">start</property>
+        <child>
+          <object class="GtkButton" id="cancel_button">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <signal name="clicked" handler="on_cancel_button_clicked" object="CaliforniaHostAttendeesEditor" 
swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="accept_button">
+            <property name="label" translatable="yes">_Accept</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <signal name="clicked" handler="on_accept_button_clicked" object="CaliforniaHostAttendeesEditor" 
swapped="no"/>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">4</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/rc/create-update-event.ui b/src/rc/create-update-event.ui
index 895cf49..43fb9d0 100644
--- a/src/rc/create-update-event.ui
+++ b/src/rc/create-update-event.ui
@@ -281,12 +281,12 @@
     </child>
     <child>
       <object class="GtkLabel" id="attendees_label">
+        <property name="visible">True</property>
         <property name="can_focus">False</property>
         <property name="no_show_all">True</property>
         <property name="xalign">1</property>
-        <property name="label" translatable="yes">_Guests</property>
+        <property name="label" translatable="yes">Invited Guests</property>
         <property name="use_underline">True</property>
-        <property name="mnemonic_widget">attendees_entry</property>
         <style>
           <class name="dim-label"/>
         </style>
@@ -297,14 +297,47 @@
       </packing>
     </child>
     <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 class="GtkBox" id="attendees_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">4</property>
+        <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="label">(none)</property>
+            <property name="ellipsize">end</property>
+            <property name="max_width_chars">64</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="attendees_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 and remove invited guests</property>
+            <property name="relief">none</property>
+            <signal name="clicked" handler="on_attendees_button_clicked" 
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
+            <child>
+              <object class="GtkImage" id="image3">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">mail-unread-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>
diff --git a/src/toolkit/toolkit-listbox-model.vala b/src/toolkit/toolkit-listbox-model.vala
index 951b8ad..efe3e95 100644
--- a/src/toolkit/toolkit-listbox-model.vala
+++ b/src/toolkit/toolkit-listbox-model.vala
@@ -197,6 +197,21 @@ public class ListBoxModel<G> : BaseObject {
     }
     
     /**
+     * A Gee.Set of all items in the { link ListBoxModel}, sorted if appropriate.
+     */
+    public Gee.Set<G> all() {
+        Gee.TreeSet<G> treeset;
+        if (comparator != null)
+            treeset = new Gee.TreeSet<G>(comparator);
+        else
+            treeset = new Gee.TreeSet<G>();
+        
+        treeset.add_all(items.keys);
+        
+        return treeset;
+    }
+    
+    /**
      * Returns the { link ModelPresentation} widget for the item.
      */
     public Gtk.Widget? get_widget_for_item(G item) {
@@ -227,7 +242,7 @@ public class ListBoxModel<G> : BaseObject {
      * Each removed item generates a { link removed} signal.
      */
     public void clear() {
-        foreach (G item in items.keys)
+        foreach (G item in items.keys.to_array())
             remove(item);
     }
     
diff --git a/src/util/util-uri.vala b/src/util/util-uri.vala
index 0a61e1a..cd92c53 100644
--- a/src/util/util-uri.vala
+++ b/src/util/util-uri.vala
@@ -76,5 +76,14 @@ public bool is_valid_mailbox(string str) {
     return email_regex.match(str);
 }
 
+/**
+ * Generates a valid mailto: Soup.URI given a mailbox (i.e. email) address.
+ *
+ * No validity checking is done here on the mailbox; use { link is_valid_mailbox}.
+ */
+public Soup.URI generate_mailto(string mailbox) throws Error {
+    return parse("mailto:%s".printf(GLib.Uri.escape_string(mailbox, "@")));
+}
+
 }
 
diff --git a/vapi/libical.vapi b/vapi/libical.vapi
index 5ad3c1a..9868c8c 100644
--- a/vapi/libical.vapi
+++ b/vapi/libical.vapi
@@ -499,7 +499,7 @@ namespace iCal {
                [CCode (cname = "icalproperty_new_action", has_construct_function = false)]
                public icalproperty.action (iCal.icalproperty_action v);
                [CCode (cname = "icalproperty_add_parameter")]
-               public void add_parameter (iCal.icalparameter parameter);
+               public void add_parameter (owned iCal.icalparameter parameter);
                [CCode (cname = "icalproperty_add_parameters")]
                public static void add_parameters (iCal.icalproperty prop, void* args);
                [CCode (cname = "icalproperty_new_allowconflict", has_construct_function = false)]
@@ -1009,7 +1009,7 @@ namespace iCal {
                [CCode (cname = "icalproperty_set_owner")]
                public void set_owner (string v);
                [CCode (cname = "icalproperty_set_parameter")]
-               public void set_parameter (iCal.icalparameter parameter);
+               public void set_parameter (owned iCal.icalparameter parameter);
                [CCode (cname = "icalproperty_set_parameter_from_string")]
                public void set_parameter_from_string (string name, string value);
                [CCode (cname = "icalproperty_set_percentcomplete")]


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