[california] Display Attendees and Organizers in Show Event dialog: Bug #731543
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Display Attendees and Organizers in Show Event dialog: Bug #731543
- Date: Thu, 6 Nov 2014 01:18:01 +0000 (UTC)
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]