[california] Add/remove attendees from event: Bug #731543
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Add/remove attendees from event: Bug #731543
- Date: Thu, 13 Nov 2014 23:15:21 +0000 (UTC)
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]