[california] Update/edit recurring events: Bug #725786
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Update/edit recurring events: Bug #725786
- Date: Thu, 17 Jul 2014 02:40:58 +0000 (UTC)
commit 1c187bfa3fdb5e44954ca403644f1357c885297b
Author: Jim Nelson <jim yorba org>
Date: Wed Jul 16 19:36:50 2014 -0700
Update/edit recurring events: Bug #725786
This rather large commit allows for the user to update (edit)
recurring events. It also allows for recurring events to be created
with a GUI display rather than the Quick Add interface.
This commit fleshes out the notion of a master Instance versus a
generated Instance, important for editing. It also introduces a
simple widget for rotating GtkButtonBoxes to allow the user to make
a second-level decision without a separate dialog box.
Updating "this and all future events" will be ticketed for later,
as this patch was getting too beefy to allow for more functionality
to bleed in.
po/POTFILES.in | 2 +
src/Makefile.am | 5 +
src/activator/activator-instance-list.vala | 2 +-
.../activator-google-authenticating-pane.vala | 2 +-
.../activator-google-calendar-list-pane.vala | 2 +-
.../google/activator-google-login-pane.vala | 2 +-
src/activator/webcal/activator-webcal-pane.vala | 2 +-
.../backing-calendar-source-subscription.vala | 114 +++--
src/backing/backing-calendar-source.vala | 6 +-
src/backing/backing-error.vala | 6 +-
.../backing-eds-calendar-source-subscription.vala | 156 ++++--
src/backing/eds/backing-eds-calendar-source.vala | 9 +-
src/california-resources.xml | 3 +
src/collection/collection-iterable.vala | 13 +
src/collection/collection.vala | 14 +
src/component/component-date-time.vala | 111 +++--
src/component/component-event.vala | 173 +++---
src/component/component-instance.vala | 229 ++++++++-
src/component/component-recurrence-rule.vala | 59 ++-
src/host/host-create-update-event.vala | 159 +++++-
src/host/host-create-update-recurring.vala | 585 ++++++++++++++++++++
src/host/host-main-window.vala | 31 +-
src/host/host-quick-create-event.vala | 44 +-
src/host/host-show-event.vala | 23 +-
src/manager/manager-calendar-list.vala | 2 +-
src/rc/create-update-event.ui | 65 +--
src/rc/create-update-recurring.ui | 551 ++++++++++++++++++
src/tests/tests-iterable.vala | 45 ++
src/tests/tests.vala | 1 +
src/toolkit/toolkit-calendar-popup.vala | 26 +-
src/toolkit/toolkit-card.vala | 31 +-
src/toolkit/toolkit-deck.vala | 24 +-
src/toolkit/toolkit-rotating-button-box.vala | 89 +++
src/util/util-numeric.vala | 17 +
vapi/libecal-1.2.vapi | 3 +-
vapi/libecal-1.2/libecal-1.2.metadata | 5 +-
36 files changed, 2225 insertions(+), 386 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d6deb65..e3a056f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -11,6 +11,7 @@ src/calendar/calendar-date.vala
src/calendar/calendar.vala
src/component/component.vala
src/host/host-create-update-event.vala
+src/host/host-create-update-recurring.vala
src/host/host-import-calendar.vala
src/host/host-main-window.vala
src/host/host-show-event.vala
@@ -22,6 +23,7 @@ src/view/week/week-controller.vala
[type: gettext/glade]src/rc/calendar-manager-list-item.ui
[type: gettext/glade]src/rc/calendar-manager-list.ui
[type: gettext/glade]src/rc/create-update-event.ui
+[type: gettext/glade]src/rc/create-update-recurring.ui
[type: gettext/glade]src/rc/google-authenticating.ui
[type: gettext/glade]src/rc/google-calendar-list.ui
[type: gettext/glade]src/rc/google-login.ui
diff --git a/src/Makefile.am b/src/Makefile.am
index f4c5fd0..3976255 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -92,6 +92,7 @@ california_VALASOURCES = \
host/host.vala \
host/host-calendar-list-item.vala \
host/host-create-update-event.vala \
+ host/host-create-update-recurring.vala \
host/host-import-calendar.vala \
host/host-main-window.vala \
host/host-quick-create-event.vala \
@@ -107,6 +108,7 @@ california_VALASOURCES = \
tests/tests-calendar-month-of-year.vala \
tests/tests-calendar-month-span.vala \
tests/tests-calendar-wall-time.vala \
+ tests/tests-iterable.vala \
tests/tests-quick-add.vala \
tests/tests-quick-add-recurring.vala \
tests/tests-string.vala \
@@ -126,12 +128,14 @@ california_VALASOURCES = \
toolkit/toolkit-motion-event.vala \
toolkit/toolkit-mutable-widget.vala \
toolkit/toolkit-popup.vala \
+ toolkit/toolkit-rotating-button-box.vala \
toolkit/toolkit-stack-model.vala \
\
util/util.vala \
util/util-gfx.vala \
util/util-markup.vala \
util/util-memory.vala \
+ util/util-numeric.vala \
util/util-scheduled.vala \
util/util-string.vala \
util/util-uri.vala \
@@ -175,6 +179,7 @@ california_RC = \
rc/calendar-manager-list.ui \
rc/calendar-manager-list-item.ui \
rc/create-update-event.ui \
+ rc/create-update-recurring.ui \
rc/google-authenticating.ui \
rc/google-calendar-list.ui \
rc/google-login.ui \
diff --git a/src/activator/activator-instance-list.vala b/src/activator/activator-instance-list.vala
index c5d560a..324deed 100644
--- a/src/activator/activator-instance-list.vala
+++ b/src/activator/activator-instance-list.vala
@@ -43,7 +43,7 @@ public class InstanceList : Gtk.Grid, Toolkit.Card {
return true;
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
}
private void on_item_activated(Instance activator) {
diff --git a/src/activator/google/activator-google-authenticating-pane.vala
b/src/activator/google/activator-google-authenticating-pane.vala
index e950173..cc9fbcc 100644
--- a/src/activator/google/activator-google-authenticating-pane.vala
+++ b/src/activator/google/activator-google-authenticating-pane.vala
@@ -56,7 +56,7 @@ public class GoogleAuthenticatingPane : Gtk.Grid, Toolkit.Card {
app_id = "yorba-california-%s".printf(Application.VERSION);
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
Message? credentials = message as Message;
assert(credentials != null);
diff --git a/src/activator/google/activator-google-calendar-list-pane.vala
b/src/activator/google/activator-google-calendar-list-pane.vala
index 8d37d7a..6dc545f 100644
--- a/src/activator/google/activator-google-calendar-list-pane.vala
+++ b/src/activator/google/activator-google-calendar-list-pane.vala
@@ -67,7 +67,7 @@ public class GoogleCalendarListPane : Gtk.Grid, Toolkit.Card {
return label;
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
Message? feeds = message as Message;
assert(feeds != null);
diff --git a/src/activator/google/activator-google-login-pane.vala
b/src/activator/google/activator-google-login-pane.vala
index 856db52..15d66fc 100644
--- a/src/activator/google/activator-google-login-pane.vala
+++ b/src/activator/google/activator-google-login-pane.vala
@@ -34,7 +34,7 @@ internal class GoogleLoginPane : Gtk.Grid, Toolkit.Card {
BindingFlags.SYNC_CREATE, on_entry_changed);
}
- public void jumped_to(Toolkit.Card? from, Value? msg) {
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? msg) {
password_entry.text = "";
}
diff --git a/src/activator/webcal/activator-webcal-pane.vala b/src/activator/webcal/activator-webcal-pane.vala
index c66dc91..4d3b31e 100644
--- a/src/activator/webcal/activator-webcal-pane.vala
+++ b/src/activator/webcal/activator-webcal-pane.vala
@@ -46,7 +46,7 @@ internal class WebCalActivatorPane : Gtk.Grid, Toolkit.Card {
BindingFlags.SYNC_CREATE, on_entry_changed);
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
}
private bool on_entry_changed(Binding binding, Value source_value, ref Value target_value) {
diff --git a/src/backing/backing-calendar-source-subscription.vala
b/src/backing/backing-calendar-source-subscription.vala
index 12a1342..0ffd6f7 100644
--- a/src/backing/backing-calendar-source-subscription.vala
+++ b/src/backing/backing-calendar-source-subscription.vala
@@ -9,15 +9,13 @@ namespace California.Backing {
/**
* A subscription to an active timespan of interest of a calendar.
*
- * CalendarSourceSubscription generates { link Component.Instance}s of the various events in a
- * window of time in a calendar. Note that for recurring events, there is no interface to directly
- * edit or remove the "original" iCalendar source. Rather, the caller should update or remove
- * generated instances of that recurring event with flags to indicate how the original is to be
- * altered to match these changes. See { link CalendarSource} for the interface to make these
- * alterations.
+ * The subscription notifies of calendar updates via its signals. It can list complete or partial
+ * collections of { link Component.Instances} it has reported via those signals.
*
- * The subscription can notify of calendar event updates and list a complete or partial collections
- * of the same.
+ * CalendarSourceSubscription generates { link Component.Instance}s of the various events in a
+ * window of time in a calendar. Both master instances and generated recurring instances are
+ * reported through the interface. Operations performed on instances should be done with the
+ * subscription's { link CalendarSource}.
*/
public abstract class CalendarSourceSubscription : BaseObject {
@@ -50,22 +48,63 @@ public abstract class CalendarSourceSubscription : BaseObject {
/**
* Fired as existing { link Component.Instance}s are discovered when starting a subscription.
*
+ * Like { link instance_added}, this is only fired for master Instances which do not
+ * generate recurring instances and generated Instances. See { link master_discovered} to
+ * be notified of all master Instances.
+ *
* This is fired while { link start} is working, either in the foreground or in the background.
* It won't fire until start() is invoked.
+ *
+ * @see master_discovered
*/
public signal void instance_discovered(Component.Instance instance);
/**
- * Indicates that an { link Instance} within the { link window} has been added to the calendar.
+ * Fired as existing master { link Component.Instance}s are discovered when starting a
+ * subscription.
+ *
+ * This is fired for all discovered master Instances whether or not they may generate
+ * Instances for this subscription.
+ *
+ * This is fired while { link start} is working, either in the foreground or in the background.
+ * It won't fire until start() is invoked.
+ *
+ * @see instance_discovered
+ */
+ public signal void master_discovered(Component.Instance instance);
+
+ /**
+ * Fired when a { link Component.Instance} within the { link window} has been added to the
+ * { link CalendarSource}.
+ *
+ * This is fired only for master Instances which do not generate recurring Instances and for
+ * generated Instances. See { link master_added} to be notified of all master Instances.
*
* The signal is fired for both local additions (added through this interface) and remote
* additions.
*
* This signal won't fire until { link start} is called.
+ *
+ * @see master_added
*/
public signal void instance_added(Component.Instance instance);
/**
+ * Fired as existing master { link Component.Instance}s are added to the { link CalendarSource}.
+ *
+ * This is fired for all discovered master Instances whether or not they may generate
+ * Instances for this subscription.
+ *
+ * The signal is fired for both local additions (added through this interface) and remote
+ * additions.
+ *
+ * This signal won't fire until { link start} is called.
+ *
+ * @see instance_added
+ */
+ public signal void master_added(Component.Instance instance);
+
+ /**
* Indicates that an { link Instance} within the { link date_window} has been removed from the
* calendar.
*
@@ -143,10 +182,17 @@ public abstract class CalendarSourceSubscription : BaseObject {
* @see instance_discovered
*/
protected virtual void notify_instance_discovered(Component.Instance instance) {
- if (add_instance(instance))
- instance_discovered(instance);
- else
+ if (!add_instance(instance)) {
debug("Cannot add discovered component %s to %s: already known", instance.to_string(),
to_string());
+
+ return;
+ }
+
+ if (instance.is_master_instance)
+ master_discovered(instance);
+
+ if (!instance.can_generate_instances)
+ instance_discovered(instance);
}
/**
@@ -156,10 +202,17 @@ public abstract class CalendarSourceSubscription : BaseObject {
* @see instance_added
*/
protected virtual void notify_instance_added(Component.Instance instance) {
- if (add_instance(instance))
- instance_added(instance);
- else
+ if (!add_instance(instance)) {
debug("Cannot add component %s to %s: already known", instance.to_string(), to_string());
+
+ return;
+ }
+
+ if (instance.is_master_instance)
+ master_added(instance);
+
+ if (!instance.can_generate_instances)
+ instance_added(instance);
}
/**
@@ -195,13 +248,13 @@ public abstract class CalendarSourceSubscription : BaseObject {
* Notify that the { link Component.Instance}s have been dropped due to the { link Source} going
* unavailable.
*/
- protected virtual void notify_instance_dropped(Component.Instance instance) {
+ protected virtual void notify_instance_dropped(Component.UID uid) {
Gee.Collection<Component.Instance> removed_instances;
- if (remove_instance(instance.uid, out removed_instances)) {
+ if (remove_instance(uid, out removed_instances)) {
foreach (Component.Instance removed_instance in removed_instances)
instance_dropped(removed_instance);
} else {
- debug("Cannot notify dropped component %s in %s: not known", instance.to_string(), to_string());
+ debug("Cannot notify dropped component %s in %s: not known", uid.to_string(), to_string());
}
}
@@ -264,7 +317,7 @@ public abstract class CalendarSourceSubscription : BaseObject {
// the multimap
debug("Dropping %d instances to %s: unavailable", instances.size, calendar.to_string());
foreach (Component.Instance instance in instances.get_values().to_array())
- notify_instance_dropped(instance);
+ notify_instance_dropped(instance.uid);
}
/**
@@ -284,29 +337,6 @@ public abstract class CalendarSourceSubscription : BaseObject {
return instances.contains(uid) ? instances.get(uid) : null;
}
- /**
- * Returns the seen { link Component.Instance} matching the supplied (possibly partially
- * filled-out) Instance.
- *
- * This is for duplicate detection, especially if the { link Backing} is receiving raw iCal
- * source and needs to verify if it's been parsed and introduced into the system.
- *
- * A blank Instance with partial fields filled out can be supplied.
- */
- public Component.Instance? has_instance(Component.Instance instance) {
- Gee.Collection<Component.Instance>? seen_instances = for_uid(instance.uid);
- if (seen_instances == null || seen_instances.size == 0)
- return null;
-
- // for every instance matching its UID, look for the original
- foreach (Component.Instance seen_instance in seen_instances) {
- if (seen_instance.equal_to(instance))
- return seen_instance;
- }
-
- return null;
- }
-
public override string to_string() {
return "%s::%s".printf(calendar.to_string(), window.to_string());
}
diff --git a/src/backing/backing-calendar-source.vala b/src/backing/backing-calendar-source.vala
index 4e6589b..fbfb6dd 100644
--- a/src/backing/backing-calendar-source.vala
+++ b/src/backing/backing-calendar-source.vala
@@ -19,7 +19,7 @@ namespace California.Backing {
public abstract class CalendarSource : Source {
/**
- * The affected range of a modification or removal operation.
+ * The affected range of a removal operation.
*
* Note that zero (0) does ''not'' mean "none", it means { link AffectedInstances.THIS}. The
* additional enums merely expand the scope of the default, which is the supplied instance.
@@ -64,6 +64,10 @@ public abstract class CalendarSource : Source {
/**
* Updates an existing { link Component} instance on the backing { link CalendarSource}.
*
+ * To update all { link Instance}s of a recurring { link Instance}, submit the
+ * { link Component.Instance.master} with modifications rather than one of its generated
+ * instances. Submit a generated instance to update only that one.
+ *
* Outstanding { link CalendarSourceSubscriptions} will eventually report the changes when
* ready.
*/
diff --git a/src/backing/backing-error.vala b/src/backing/backing-error.vala
index 0857010..3f30d3a 100644
--- a/src/backing/backing-error.vala
+++ b/src/backing/backing-error.vala
@@ -22,7 +22,11 @@ public errordomain BackingError {
/**
* The method or object is unavailable due to a state change (not open or removed).
*/
- UNAVAILABLE
+ UNAVAILABLE,
+ /**
+ * The object or identifier is not recognized.
+ */
+ UNKNOWN
}
}
diff --git a/src/backing/eds/backing-eds-calendar-source-subscription.vala
b/src/backing/eds/backing-eds-calendar-source-subscription.vala
index b3003a6..ab1d582 100644
--- a/src/backing/eds/backing-eds-calendar-source-subscription.vala
+++ b/src/backing/eds/backing-eds-calendar-source-subscription.vala
@@ -11,17 +11,21 @@ namespace California.Backing {
*/
internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
+ private delegate void InstanceNotifier(Component.Instance instance);
+
private E.CalClientView view;
+ private string sexp;
// this is different than "active", which gets set when start completes
private bool started = false;
private Error? start_err = null;
// Called from EdsCalendarSource.subscribe_async(). The CalClientView should not be started
public EdsCalendarSourceSubscription(EdsCalendarSource eds_calendar, Calendar.ExactTimeSpan window,
- E.CalClientView view) {
+ E.CalClientView view, string sexp) {
base (eds_calendar, window);
this.view = view;
+ this.sexp = sexp;
}
~EdsCalendarSourceSubscription() {
@@ -82,36 +86,36 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
// next
view.start();
- // prime with the list of known events
- view.client.generate_instances(
- window.start_exact_time.to_time_t(),
- window.end_exact_time.to_time_t(),
- cancellable,
- on_instance_generated,
- on_generate_finished);
+ discovery_async.begin(cancellable);
}
- private bool on_instance_generated(E.CalComponent eds_component, time_t instance_start,
- time_t instance_end) {
+ private async void discovery_async(Cancellable? cancellable) {
+ SList<unowned iCal.icalcomponent> ical_components;
try {
- Component.Event? event = Component.Instance.convert(calendar, eds_component.get_icalcomponent())
- as Component.Event;
- if (event != null)
- notify_instance_discovered(event);
+ yield view.client.get_object_list(sexp, cancellable, out ical_components);
} catch (Error err) {
- debug("Unable to generate discovered event for %s: %s", to_string(), err.message);
+ start_err = err;
+
+ start_failed(err);
+
+ return;
}
- return true;
- }
-
- private void on_generate_finished() {
+ // process all known objects within the sexp range
+ on_objects_discovered_added(ical_components, notify_instance_discovered);
+
// only set when generation (start) is finished
active = true;
}
- private void on_objects_added(SList<weak iCal.icalcomponent> objects) {
- foreach (weak iCal.icalcomponent ical_component in objects) {
+ private void on_objects_added(SList<unowned iCal.icalcomponent> ical_components) {
+ // process all added objects
+ on_objects_discovered_added(ical_components, notify_instance_added);
+ }
+
+ private void on_objects_discovered_added(SList<unowned iCal.icalcomponent> ical_components,
+ InstanceNotifier notifier) {
+ foreach (unowned iCal.icalcomponent ical_component in ical_components) {
if (String.is_empty(ical_component.get_uid()))
continue;
@@ -121,47 +125,53 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
if (has_uid(uid))
notify_instance_removed(uid);
- // if no recurrences, add this alone
- if (!E.Util.component_has_recurrences(ical_component)) {
- add_instance(ical_component);
-
+ // add all instances, master and generated
+ Component.Instance? master = add_instance(null, ical_component, notifier);
+ if (master == null)
+ continue;
+
+ // if no recurrences, done
+ if (!E.Util.component_has_recurrences(ical_component))
continue;
- }
// generate recurring instances
- view.client.generate_instances_for_object(
+ view.client.generate_instances_for_object_sync(
ical_component,
window.start_exact_time.to_time_t(),
window.end_exact_time.to_time_t(),
- null,
- on_instance_added,
- null);
+ (eds_component, start, end) => {
+ add_instance(master, eds_component.get_icalcomponent(), notifier);
+
+ return true;
+ }
+ );
}
}
- private bool on_instance_added(E.CalComponent eds_component, time_t instance_start,
- time_t instance_end) {
- add_instance(eds_component.get_icalcomponent());
-
- return true;
- }
-
// Assumes all existing events with UID/RID have been removed already
- private void add_instance(iCal.icalcomponent ical_component) {
+ private Component.Instance? add_instance(Component.Instance? master, iCal.icalcomponent ical_component,
+ InstanceNotifier notifier) {
// convert the added component into a new Event
- Component.Event? added_event;
+ Component.Event? added_event = null;
try {
added_event = Component.Instance.convert(calendar, ical_component) as Component.Event;
- if (added_event != null)
- notify_instance_added(added_event);
+ if (added_event != null) {
+ // assign the master (if this isn't the master already)
+ added_event.master = master;
+
+ // notify of didscovery/addition
+ notifier(added_event);
+ }
} catch (Error err) {
debug("Unable to process added event: %s", err.message);
}
+
+ return added_event;
}
- private void on_objects_modified(SList<weak iCal.icalcomponent> objects) {
- SList<weak iCal.icalcomponent> add_list = new SList<weak iCal.icalcomponent>();
- foreach (weak iCal.icalcomponent ical_component in objects) {
+ private void on_objects_modified(SList<unowned iCal.icalcomponent> ical_components) {
+ SList<unowned iCal.icalcomponent> add_list = new SList<unowned iCal.icalcomponent>();
+ foreach (unowned iCal.icalcomponent ical_component in ical_components) {
// if not an instance and has recurring, treat as an add (which removes and adds generated
// instances)
if (!E.Util.component_is_instance(ical_component) &&
E.Util.component_has_recurrences(ical_component)) {
@@ -178,37 +188,61 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
if (!has_uid(uid))
continue;
- // find original for this one
+ Component.DateTime? rid = null;
+ try {
+ rid = new Component.DateTime(ical_component, iCal.icalproperty_kind.RECURRENCEID_PROPERTY);
+ } catch (ComponentError comperr) {
+ if (!(comperr is ComponentError.UNAVAILABLE)) {
+ debug("Unable to get RID of modified component: %s", comperr.message);
+
+ continue;
+ }
+ }
+
+ // get all instances known for this UID to find original to alter
Gee.Collection<Component.Instance>? instances = for_uid(uid);
- if (instances == null || instances.size == 0)
- continue;
- foreach (Component.Instance instance in instances) {
- Component.Event? known_event = instance as Component.Event;
- if (known_event == null)
+ // if no RID, then only one should be returned
+ Component.Instance? instance = null;
+ if (rid == null) {
+ instance = traverse<Component.Instance>(instances).one();
+ if (instance == null) {
+ debug("%d instances found for modified instance, expected 1",
Collection.size(instances));
+
continue;
-
- try {
- known_event.full_update(ical_component, null);
- } catch (Error err) {
- debug("Unable to update event %s: %s", known_event.to_string(), err.message);
+ }
+ } else {
+ // if RID != null, then find the matching instance
+ instance = traverse<Component.Instance>(instances)
+ .first_matching(inst => inst.rid != null && inst.rid.equal_to(rid));
+ if (instance == null) {
+ debug("Cannot find instance with UID %s RID %s, skipping", uid.to_string(),
rid.to_string());
continue;
}
+ }
+
+ Component.Event? modified_event = instance as Component.Event;
+ if (modified_event == null)
+ continue;
+
+ try {
+ modified_event.full_update(ical_component, null);
+ } catch (Error err) {
+ debug("Unable to update event %s: %s", modified_event.to_string(), err.message);
- notify_instance_altered(known_event);
+ continue;
}
- if (instances.size > 1)
- debug("Warning: updated %d modified events, expecting only 1", instances.size);
+ notify_instance_altered(modified_event);
}
- // add any recurring events
+ // remove and re-add any recurring events
on_objects_added(add_list);
}
- private void on_objects_removed(SList<weak E.CalComponentId?> ids) {
- foreach (weak E.CalComponentId id in ids)
+ private void on_objects_removed(SList<unowned E.CalComponentId?> ids) {
+ foreach (unowned E.CalComponentId id in ids)
notify_instance_removed(new Component.UID(id.uid));
}
}
diff --git a/src/backing/eds/backing-eds-calendar-source.vala
b/src/backing/eds/backing-eds-calendar-source.vala
index 440327e..3533c8d 100644
--- a/src/backing/eds/backing-eds-calendar-source.vala
+++ b/src/backing/eds/backing-eds-calendar-source.vala
@@ -135,7 +135,7 @@ internal class EdsCalendarSource : CalendarSource {
E.CalClientView view;
yield client.get_view(sexp, cancellable, out view);
- return new EdsCalendarSourceSubscription(this, window, view);
+ return new EdsCalendarSourceSubscription(this, window, view, sexp);
}
public override async Component.UID? create_component_async(Component.Instance instance,
@@ -152,7 +152,10 @@ internal class EdsCalendarSource : CalendarSource {
Cancellable? cancellable = null) throws Error {
check_open();
- yield client.modify_object(instance.ical_component, E.CalObjModType.THIS, cancellable);
+ E.CalObjModType modtype =
+ instance.can_generate_instances ? E.CalObjModType.ALL : E.CalObjModType.THIS;
+
+ yield client.modify_object(instance.ical_component, modtype, cancellable);
}
public override async void remove_all_instances_async(Component.UID uid,
@@ -171,7 +174,7 @@ internal class EdsCalendarSource : CalendarSource {
// include an EXDATE in the original iCal source ... I don't quite understand the benefit of
// this, as this suggests (a) other calendar clients won't learn of the removal and (b) the
// instance will be re-generated the next time the user runs an EDS calendar client. In
- // either case, ONLY maps to our desired effect by adding an EXDATE to the iCal source.
+ // either case, THIS maps to our desired effect by adding an EXDATE to the iCal source.
switch (affected) {
case CalendarSource.AffectedInstances.THIS:
yield client.remove_object(uid.value, rid.value, E.CalObjModType.THIS, cancellable);
diff --git a/src/california-resources.xml b/src/california-resources.xml
index e52d60d..edf38ea 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -22,6 +22,9 @@
<file compressed="false">rc/create-update-event.ui</file>
</gresource>
<gresource prefix="/org/yorba/california">
+ <file compressed="false">rc/create-update-recurring.ui</file>
+ </gresource>
+ <gresource prefix="/org/yorba/california">
<file compressed="true">rc/google-authenticating.ui</file>
</gresource>
<gresource prefix="/org/yorba/california">
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 395247c..5648456 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -191,6 +191,19 @@ public class Iterable<G> : Object {
.map<A>(g => { return (A) g; }));
}
+ /**
+ * Returns the first element in the { link Iterable} if and only if it is the only one,
+ * otherwise returns null.
+ */
+ public G? one() {
+ if (!i.next())
+ return null;
+
+ G element = i get();
+
+ return !i.next() ? element : null;
+ }
+
public G? first() {
return (i.next() ? i get() : null);
}
diff --git a/src/collection/collection.vala b/src/collection/collection.vala
index ba28f21..6557566 100644
--- a/src/collection/collection.vala
+++ b/src/collection/collection.vala
@@ -18,5 +18,19 @@ public void terminate() {
return;
}
+/**
+ * Returns true if the Collection is null or empty (zero elements).
+ */
+public inline bool is_empty(Gee.Collection? c) {
+ return c == null || c.size == 0;
+}
+
+/**
+ * Returns the size of the Collection, zero if null.
+ */
+public inline int size(Gee.Collection? c) {
+ return !is_empty(c) ? c.size : 0;
+}
+
}
diff --git a/src/component/component-date-time.vala b/src/component/component-date-time.vala
index d52bd00..1ad627c 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -62,13 +62,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
public iCal.icalproperty_kind kind;
/**
- * Creates a new { link DateTime} for the iCal component of the property kind.
- *
- * Note that the only properties currently supported are:
- * * DTSTART_PROPERTY
- * * DTEND_PROPERTY
- * * DTSTAMP_PROPERTY
- * * RECURRENCEID_PROPERTY
+ * Creates a new { link DateTime} for the first iCal property of the kind.
*
* @throws ComponentError.UNAVAILABLE if not found
* @throws ComponentError.INVALID if not a valid DATE or DATE-TIME
@@ -79,32 +73,33 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
if (prop == null)
throw new ComponentError.UNAVAILABLE("No property of kind %s", ical_prop_kind.to_string());
- switch (ical_prop_kind) {
- case iCal.icalproperty_kind.DTSTAMP_PROPERTY:
- dt = prop.get_dtstamp();
- break;
-
- case iCal.icalproperty_kind.DTSTART_PROPERTY:
- dt = prop.get_dtstart();
- break;
-
- case iCal.icalproperty_kind.DTEND_PROPERTY:
- dt = prop.get_dtend();
+ init_from_property(prop);
+ }
+
+ public DateTime.from_property(iCal.icalproperty prop) throws ComponentError {
+ init_from_property(prop);
+ }
+
+ private void init_from_property(iCal.icalproperty prop) throws ComponentError {
+ unowned iCal.icalvalue prop_value = prop.get_value();
+ switch (prop_value.isa()) {
+ case iCal.icalvalue_kind.DATE_VALUE:
+ dt = prop_value.get_date();
break;
- case iCal.icalproperty_kind.RECURRENCEID_PROPERTY:
- dt = prop.get_recurrenceid();
+ case iCal.icalvalue_kind.DATETIME_VALUE:
+ dt = prop_value.get_datetime();
break;
default:
- assert_not_reached();
+ throw new ComponentError.INVALID("Not a DATE/DATE-TIME value: %s",
prop_value.isa().to_string());
}
if (iCal.icaltime_is_null_time(dt) != 0)
- throw new ComponentError.INVALID("DATE-TIME for %s is null time", ical_prop_kind.to_string());
+ throw new ComponentError.INVALID("DATE-TIME for %s is null time", prop.isa().to_string());
if (iCal.icaltime_is_valid_time(dt) == 0)
- throw new ComponentError.INVALID("DATE-TIME for %s is invalid", ical_prop_kind.to_string());
+ throw new ComponentError.INVALID("DATE-TIME for %s is invalid", prop.isa().to_string());
unowned iCal.icalparameter? param = prop.get_first_parameter(iCal.icalparameter_kind.TZID_PARAMETER);
if (param != null) {
@@ -120,45 +115,51 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
zone = new Calendar.OlsonZone(dt.zone->get_location());
}
- kind = ical_prop_kind;
+ kind = prop.isa();
value = prop.get_value_as_string();
}
/**
* Creates a new { link DateTime} for a component's RRULE UNTIL property.
+ *
+ * Strict will attempt to adhere to the MUSTs and SHALLs present in the iCal specification
+ * regarding RRULE's UNTIL property. See [[https://tools.ietf.org/html/rfc5545#section-3.3.10]]
*/
- public DateTime.rrule_until(iCal.icalrecurrencetype rrule, DateTime dtstart) throws ComponentError {
+ public DateTime.rrule_until(iCal.icalrecurrencetype rrule, DateTime dtstart, bool strict)
+ throws ComponentError {
if (iCal.icaltime_is_null_time(rrule.until) != 0)
- throw new ComponentError.INVALID("DATE-TIME for RRULE UNTIL is null time");
+ throw new ComponentError.UNAVAILABLE("DATE-TIME for RRULE UNTIL is null time");
- if (iCal.icaltime_is_valid_time(rrule.until) != 0)
- throw new ComponentError.INVALID("DATE-TIME for RRULE UNTIL is invalid");
+ if (iCal.icaltime_is_valid_time(rrule.until) == 0)
+ throw new ComponentError.UNAVAILABLE("DATE-TIME for RRULE UNTIL is invalid");
bool until_is_date = (iCal.icaltime_is_date(rrule.until) != 0);
bool until_is_utc = (iCal.icaltime_is_utc(rrule.until) != 0);
- // "The value of the UNTIL rule part MUST have the same value type as the 'DTSTART'
- // property."
- if (dtstart.is_date != until_is_date)
- throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same type
(DATE/DATE-TIME)");
-
- // "If the 'DTSTART' property is specified as a date with local time, then the UNTIL rule
- // part MUST also be specified as a date with local time."
- if (dtstart.is_utc != until_is_utc)
- throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same time type
(UTC/local)");
-
- // "if the 'DTSTART' property is specified as a date with UTC time or a date with local time
- // and a time zone reference, then the UNTIL rule part MUST be specified as a date with
- // UTC time."
- if (dtstart.is_date || (!dtstart.is_utc && dtstart.zone != null)) {
- if (!until_is_utc)
- throw new ComponentError.INVALID("RRULE UNTIL must be UTC for DTSTART DATE or w/ time zone");
+ if (strict) {
+ // "The value of the UNTIL rule part MUST have the same value type as the 'DTSTART'
+ // property."
+ if (dtstart.is_date != until_is_date)
+ throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same type
(DATE/DATE-TIME)");
+
+ // "If the 'DTSTART' property is specified as a date with local time, then the UNTIL rule
+ // part MUST also be specified as a date with local time."
+ if (dtstart.is_utc != until_is_utc)
+ throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same time type
(UTC/local)");
+
+ // "if the 'DTSTART' property is specified as a date with UTC time or a date with local time
+ // and a time zone reference, then the UNTIL rule part MUST be specified as a date with
+ // UTC time."
+ if (dtstart.is_date || (!dtstart.is_utc && dtstart.zone != null)) {
+ if (!until_is_utc)
+ throw new ComponentError.INVALID("RRULE UNTIL must be UTC for DTSTART DATE or w/ time
zone");
+ }
+
+ // "If specified as a DATE-TIME value, then it MUST be specified in a UTC time format."
+ if (!until_is_date && !until_is_utc)
+ throw new ComponentError.INVALID("RRULE DATE-TIME UNTIL must be UTC");
}
- // "If specified as a DATE-TIME value, then it MUST be specified in a UTC time format."
- if (!until_is_date && !until_is_utc)
- throw new ComponentError.INVALID("RRULE DATE-TIME UNTIL must be UTC");
-
kind = iCal.icalproperty_kind.RRULE_PROPERTY;
dt = rrule.until;
zone = (!until_is_date || until_is_utc) ? Calendar.OlsonZone.utc : null;
@@ -210,6 +211,20 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
}
/**
+ * Returns an iCal value for the { link DateTime}.
+ */
+ internal iCal.icalvalue to_ical_value() {
+ iCal.icalvalue prop_value = new iCal.icalvalue(
+ is_date ? iCal.icalvalue_kind.DATE_VALUE : iCal.icalvalue_kind.DATE_VALUE);
+ if (is_date)
+ prop_value.set_date(dt);
+ else
+ prop_value.set_datetime(dt);
+
+ return prop_value;
+ }
+
+ /**
* Convert two { link DateTime}s into a { link Calendar.DateSpan} or a
* { link Calendar.ExactTimeSpan} depending on what they represent.
*
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index 9a14b3d..b4c2d34 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -20,7 +20,6 @@ public class Event : Instance, Gee.Comparable<Event> {
public const string PROP_IS_ALL_DAY = "is-all-day";
public const string PROP_LOCATION = "location";
public const string PROP_STATUS = "status";
- public const string PROP_RRULE = "rrule";
public enum Status {
TENTATIVE,
@@ -87,15 +86,6 @@ public class Event : Instance, Gee.Comparable<Event> {
public Status status { get; set; default = Status.CONFIRMED; }
/**
- * { link RecurrenceRule} (RRULE) for { link Event}.
- *
- * If the RecurrenceRule is itself altered, that signal is reflected to { link Instance.altered}.
- *
- * @see make_recurring
- */
- public RecurrenceRule? rrule { get; private set; default = null; }
-
- /**
* Create an { link Event} { link Component} from an EDS CalComponent object.
*
* Throws a BackingError if the E.CalComponent's VTYPE is not VEVENT.
@@ -161,12 +151,6 @@ public class Event : Instance, Gee.Comparable<Event> {
status = Status.CONFIRMED;
break;
}
-
- try {
- make_recurring(new RecurrenceRule.from_ical(ical_component));
- } catch (ComponentError comperr) {
- // ignored; generally means no RRULE in component
- }
}
private void on_notify(ParamSpec pspec) {
@@ -235,15 +219,6 @@ public class Event : Instance, Gee.Comparable<Event> {
}
break;
- case PROP_RRULE:
- // always remove existing RRULE
- remove_all_properties(iCal.icalproperty_kind.RRULE_PROPERTY);
-
- // add new one, if added
- if (rrule != null)
- rrule.add_to_ical(ical_component);
- break;
-
default:
altered = false;
break;
@@ -254,6 +229,17 @@ public class Event : Instance, Gee.Comparable<Event> {
}
/**
+ * @inheritDoc
+ */
+ public override Component.Instance clone() throws Error {
+ Component.Event cloned_event = new Component.Event(calendar_source, ical_component);
+ if (master != null)
+ cloned_event.master = new Component.Event(master.calendar_source, master.ical_component);
+
+ return cloned_event;
+ }
+
+ /**
* Returns a { link Calendar.DateSpan} for the { link Event}.
*
* This will return a DateSpan whether the Event is a DATE or DATE-TIME VEVENT.
@@ -274,8 +260,12 @@ public class Event : Instance, Gee.Comparable<Event> {
* @see set_event_exact_time_span
*/
public void set_event_date_span(Calendar.DateSpan date_span) {
+ freeze_notify();
+
this.date_span = date_span;
exact_time_span = null;
+
+ thaw_notify();
}
/**
@@ -286,8 +276,72 @@ public class Event : Instance, Gee.Comparable<Event> {
* @see set_event_date_span
*/
public void set_event_exact_time_span(Calendar.ExactTimeSpan exact_time_span) {
+ freeze_notify();
+
this.exact_time_span = exact_time_span;
date_span = null;
+
+ thaw_notify();
+ }
+
+ /**
+ * Adjusts the dates of an { link Event} while preserving { link WallTime}, if present.
+ *
+ * This will preserve the DATE/DATE-TIME aspect of an Event while adjusting the start and
+ * end { link Calendar.Date}s. If a DATE Event, then this is functionally equivalent to
+ * { link set_event_date_span}. If a DATE-TIME event, then this is like
+ * { link set_event_exact_time_span} but without the hassle of preserving start and end times
+ * while changing the dates.
+ */
+ public void adjust_event_date_span(Calendar.DateSpan date_span) {
+ if (is_all_day) {
+ set_event_date_span(date_span);
+
+ return;
+ }
+
+ Calendar.ExactTime new_start_time = new Calendar.ExactTime(
+ exact_time_span.start_exact_time.tz,
+ date_span.start_date,
+ exact_time_span.start_exact_time.to_wall_time()
+ );
+
+ Calendar.ExactTime new_end_time = new Calendar.ExactTime(
+ exact_time_span.end_exact_time.tz,
+ date_span.end_date,
+ exact_time_span.end_exact_time.to_wall_time()
+ );
+
+ set_event_exact_time_span(new Calendar.ExactTimeSpan(new_start_time, new_end_time));
+ }
+
+ /**
+ * Convert an { link Event} from an all-day to a timed event by only adding the time.
+ *
+ * Returns with no changes if { link is_all_day} is false.
+ */
+ public void all_day_to_timed_event(Calendar.WallTime start_time, Calendar.WallTime end_time,
+ Calendar.Timezone timezone) {
+ if (!is_all_day)
+ return;
+
+ // create exact time span using these parameters
+ set_event_exact_time_span(
+ new Calendar.ExactTimeSpan(
+ new Calendar.ExactTime(timezone, date_span.start_date, start_time),
+ new Calendar.ExactTime(timezone, date_span.end_date, end_time)
+ )
+ );
+ }
+
+ /**
+ * Convert an { link Event} from a timed event to an all-day event by removing the time.
+ *
+ * Returns with no changes if { link is_all_day} is true.
+ */
+ public void timed_to_all_day_event() {
+ if (!is_all_day)
+ set_event_date_span(get_event_date_span(null));
}
/**
@@ -350,34 +404,6 @@ public class Event : Instance, Gee.Comparable<Event> {
}
/**
- * Add a { link RecurrenceRule} to the { link Event}.
- *
- * Pass null to make Event non-recurring.
- */
- public void make_recurring(RecurrenceRule? rrule) {
- if (this.rrule != null) {
- this.rrule.notify.disconnect(on_rrule_updated);
- this.rrule.by_rule_updated.disconnect(on_rrule_updated);
- }
-
- if (rrule != null) {
- rrule.notify.connect(on_rrule_updated);
- rrule.by_rule_updated.connect(on_rrule_updated);
- }
-
- this.rrule = rrule;
- }
-
- private void on_rrule_updated() {
- // remove old property, replace with new one
- remove_all_properties(iCal.icalproperty_kind.RRULE_PROPERTY);
- rrule.add_to_ical(ical_component);
-
- // count this as an alteration
- notify_altered(false);
- }
-
- /**
* @inheritDoc
*/
public override bool is_valid(bool and_useful) {
@@ -428,6 +454,13 @@ public class Event : Instance, Gee.Comparable<Event> {
if (compare != 0)
return compare;
+ // rid
+ if (rid != null && other.rid != null) {
+ compare = rid.compare_to(other.rid);
+ if (compare != 0)
+ return compare;
+ }
+
// summary
compare = strcmp(summary, other.summary);
if (compare != 0)
@@ -438,41 +471,15 @@ public class Event : Instance, Gee.Comparable<Event> {
if (compare != 0)
return compare;
- // if recurring, go by sequence number, as the UID and RID are the same for all instances
- if (is_recurring) {
- compare = sequence - other.sequence;
- if (compare != 0)
- return compare;
- }
+ // use sequence number if available
+ compare = sequence - other.sequence;
+ if (compare != 0)
+ return compare;
// stabilize with UIDs
return uid.compare_to(other.uid);
}
- public override bool equal_to(Component.Instance other) {
- Component.Event? other_event = other as Component.Event;
- if (other_event == null)
- return false;
-
- if (this == other_event)
- return true;
-
- if (is_recurring != other_event.is_recurring)
- return false;
-
- if (is_recurring && !rid.equal_to(other_event.rid))
- return false;
-
- if (sequence != other_event.sequence)
- return false;
-
- return base.equal_to(other);
- }
-
- public override uint hash() {
- return uid.hash() ^ ((rid != null) ? rid.hash() : 0) ^ sequence;
- }
-
public override string to_string() {
return "Event %s/rid=%s/%d \"%s\" (%s)".printf(
uid.to_string(),
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 56c86ab..aff3575 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -19,6 +19,10 @@ namespace California.Component {
* The second is to update the mutable properties themselves, which will then update the underlying
* iCal component.
*
+ * Instances produced by { link Backing.CalendarSourceSubscription}s will be updated by the
+ * subscription if the Instance is updated or removed locally or remotely. Cloned Instances,
+ * however, are not automatically updated. See { link clone}.
+ *
* Alarms will be contained within Instance components. Timezones are handled separately.
*
* Instance also offers a number of methods to convert iCal structures into internal objects.
@@ -29,8 +33,12 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
public const string PROP_DTSTAMP = "dtstamp";
public const string PROP_UID = "uid";
public const string PROP_ICAL_COMPONENT = "ical-component";
+ public const string PROP_RRULE = "rrule";
public const string PROP_RID = "rid";
+ public const string PROP_EXDATES = "exdates";
+ public const string PROP_RDATES = "rdates";
public const string PROP_SEQUENCE = "sequence";
+ public const string PROP_MASTER = "master";
protected const string PROP_IN_FULL_UPDATE = "in-full-update";
@@ -61,6 +69,15 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
public UID uid { get; private set; }
/**
+ * { link RecurrenceRule} (RRULE) for { link Instance}.
+ *
+ * If the RecurrenceRule is itself altered, that signal is reflected to { link altered}.
+ *
+ * @see make_recurring
+ */
+ public RecurrenceRule? rrule { get; private set; default = null; }
+
+ /**
* The RECURRENCE-ID of a recurring component.
*
* See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.4]]
@@ -68,11 +85,83 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
public Component.DateTime? rid { get; set; default = null; }
/**
- * Returns true if the { link Recurrable} is in fact a recurring instance.
+ * All EXDATEs (DATE-TIME exceptions for recurring instances) in the { link Instance}.
+ *
+ * Returns a read-only set of { link DateTime}s. Use { link set_exdates} to change.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.8.5.1]]
+ */
+ private Gee.SortedSet<DateTime>? _exdates = null;
+ public Gee.SortedSet<DateTime>? exdates {
+ owned get {
+ return Collection.is_empty(_exdates) ? null : _exdates.read_only_view;
+ }
+
+ set {
+ _exdates = value;
+ }
+ }
+
+ /**
+ * All RDATEs (DATE-TIMEs manually set for recurring instances) in the { link Instance}.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.8.5.2]]
+ */
+ private Gee.SortedSet<DateTime>? _rdates = null;
+ public Gee.SortedSet<DateTime>? rdates {
+ owned get {
+ return Collection.is_empty(_rdates) ? null : _rdates.read_only_view;
+ }
+
+ set {
+ _rdates = value;
+ }
+ }
+
+ /**
+ * Returns true if the { link Instance} is a master instance.
+ *
+ * A master instance is one that has not been generated from another Instance's recurring
+ * rule (RRULE). In practice, this means the Instance does not have a RECURRENCE-ID.
+ *
+ * @see rid
+ * @see rrule
+ */
+ public bool is_master_instance { get { return rid == null; } }
+
+ /**
+ * Returns true if the { link Instance} is a generated recurring instance.
+ *
+ * A generated recurring instance is one that has been artificially constructed from another
+ * Instance's recurring rule (RRULE). In practice, this means the Instance has a
+ * RECURRENCE-ID.
*
* @see rid
+ * @see Backing.CalendarSource.fetch_master_component_async
*/
- public bool is_recurring { get { return rid != null; } }
+ public bool is_generated_instance { get { return rid != null; } }
+
+ /**
+ * Returns true if the master { link Instance} can generate recurring instances.
+ *
+ * This indicates the Instance is a master Instance and can generate recurring instances from
+ * its RRULE. In practice, this means the Instance has no RECURRENCE-ID but does have an
+ * RRULE. (Generated instances will have the RRULE that construct them as well as a
+ * RECURRENCE-ID.)
+ *
+ * @see rid
+ * @see Backing.CalendarSource.fetch_master_component_async
+ */
+ public bool can_generate_instances { get { return rid == null && rrule != null; } }
+
+ /**
+ * If a generated { link Instance}, holds a reference to the master Instance that generated it.
+ *
+ * The { link Backing} should do everything it can to provide a master Instance for all
+ * generated Instances. However, if this is null it is not a guarantee this is a master
+ * Instance. Use { link is_master_instance}.
+ */
+ public Instance? master { get; internal set; default = null; }
/**
* The SEQUENCE of a VEVENT, VTODO, or VJOURNAL.
@@ -251,6 +340,17 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
sequence = ical_component.get_sequence();
+ try {
+ make_recurring(new RecurrenceRule.from_ical(ical_component, false));
+ } catch (ComponentError comperr) {
+ // ignored; generally means no RRULE in component
+ if (!(comperr is ComponentError.UNAVAILABLE))
+ debug("Unable to parse RRULE for %s: %s", to_string(), comperr.message);
+ }
+
+ exdates = get_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY);
+ rdates = get_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY);
+
// save own copy of component; no ownership transferrance w/ current bindings
if (_ical_component != ical_component)
_ical_component = ical_component.clone();
@@ -274,6 +374,29 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
ical_component.set_sequence(sequence);
break;
+ case PROP_RRULE:
+ // always remove existing RRULE
+ remove_all_properties(iCal.icalproperty_kind.RRULE_PROPERTY);
+
+ // add new one, if added
+ if (rrule != null)
+ rrule.add_to_ical(ical_component);
+ break;
+
+ case PROP_EXDATES:
+ if (Collection.is_empty(exdates))
+ remove_all_properties(iCal.icalproperty_kind.EXDATE_PROPERTY);
+ else
+ set_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY, exdates);
+ break;
+
+ case PROP_RDATES:
+ if (Collection.is_empty(rdates))
+ remove_all_properties(iCal.icalproperty_kind.RDATE_PROPERTY);
+ else
+ set_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY, rdates);
+ break;
+
default:
altered = false;
break;
@@ -284,6 +407,47 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
}
/**
+ * Make a detached copy of the { link Instance}.
+ *
+ * This produces an exact copy of the Instance at the time of the call. Unlike Instances
+ * produced by { link Backing.CalendarSourceSubscription}s, cloned Instances are not
+ * automatically updated as local and/or remote changes are made. This makes them good for
+ * editing (where a number of changes are made and stored in the Instance, only being submitted
+ * when the user gives the okay).
+ *
+ * Cloning will also clone the { link master}, if present.
+ */
+ public abstract Component.Instance clone() throws Error;
+
+ /**
+ * Add a { link RecurrenceRule} to the { link Instance}.
+ *
+ * Pass null to make the Instance non-recurring.
+ */
+ public void make_recurring(RecurrenceRule? rrule) {
+ if (this.rrule != null) {
+ this.rrule.notify.disconnect(on_rrule_updated);
+ this.rrule.by_rule_updated.disconnect(on_rrule_updated);
+ }
+
+ if (rrule != null) {
+ rrule.notify.connect(on_rrule_updated);
+ rrule.by_rule_updated.connect(on_rrule_updated);
+ }
+
+ this.rrule = rrule;
+ }
+
+ private void on_rrule_updated() {
+ // remove old property, replace with new one
+ remove_all_properties(iCal.icalproperty_kind.RRULE_PROPERTY);
+ rrule.add_to_ical(ical_component);
+
+ // count this as an alteration
+ notify_altered(false);
+ }
+
+ /**
* Returns an appropriate { link Component} instance for the iCalendar component.
*
* VCALENDARs should use { link Component.iCalendar}.
@@ -305,6 +469,46 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
}
/**
+ * Convenience method to convert a collection of DATE/DATE-TIME properties into a SortedSet of
+ * { link DateTime}s.
+ *
+ * @see set_multiple_date_times
+ */
+ protected Gee.SortedSet<DateTime>? get_multiple_date_times(iCal.icalproperty_kind kind) {
+ Gee.SortedSet<DateTime> date_times = new Gee.TreeSet<DateTime>();
+
+ unowned iCal.icalproperty? prop = ical_component.get_first_property(kind);
+ while (prop != null) {
+ try {
+ date_times.add(new DateTime.from_property(prop));
+ } catch (ComponentError comperr) {
+ debug("Unable to parse DATE/DATE-TIME for %s: %s", kind.to_string(), comperr.message);
+ }
+
+ prop = ical_component.get_next_property(kind);
+ }
+
+ return date_times.size > 0 ? date_times : null;
+ }
+
+ /**
+ * Convenience method to set (replace) a collection of DATE/DATE-TIME properties from a
+ * Collection of { link DateTime}s.
+ *
+ * @see get_multiple_date_times
+ */
+ protected void set_multiple_date_times(iCal.icalproperty_kind kind, Gee.Collection<DateTime> date_times)
{
+ remove_all_properties(kind);
+
+ foreach (DateTime date_time in date_times) {
+ iCal.icalproperty prop = new iCal.icalproperty(kind);
+ prop.set_value(date_time.to_ical_value());
+
+ ical_component.add_property(prop);
+ }
+ }
+
+ /**
* Convenience method to convert a { link Calendar.DateSpan} to a pair of iCal DATEs.
*
* dtend_inclusive indicates whether the dt_end should be treated as inclusive or exclusive
@@ -358,21 +562,34 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
}
/**
- * Equality is defined as { link Component.Instance}s having the same UID.
+ * Equality is defined as { link Component.Instance}s having the same { link uid}, { link rid},
+ * and { link sequence}.
*
* Subclasses should override this and { link hash} if more definite equality is necessary.
*/
public virtual bool equal_to(Instance other) {
- return (this != other) ? uid.equal_to(other.uid) : true;
+ if (this == other)
+ return true;
+
+ if (is_generated_instance != other.is_generated_instance)
+ return false;
+
+ if (is_generated_instance && !rid.equal_to(other.rid))
+ return false;
+
+ if (sequence != other.sequence)
+ return false;
+
+ return uid.equal_to(other.uid);
}
/**
- * Hash is calculated using the { link Instance} { link UID}.
+ * Hash is calculated using the { link Instance} { link UID}, { link rid}, and { link sequence}.
*
* Subclasses should override if they override { link equal_to}.
*/
public virtual uint hash() {
- return uid.hash();
+ return uid.hash() ^ ((rid != null) ? rid.hash() : 0) ^ sequence;
}
}
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index 289a4b3..9bd45e3 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -24,7 +24,7 @@ public class RecurrenceRule : BaseObject {
* Enumeration of various BY rules (BYSECOND, BYMINUTE, etc.)
*/
public enum ByRule {
- SECOND,
+ SECOND = 0,
MINUTE,
HOUR,
DAY,
@@ -32,7 +32,11 @@ public class RecurrenceRule : BaseObject {
YEAR_DAY,
WEEK_NUM,
MONTH,
- SET_POS
+ SET_POS,
+ /**
+ * The number of { link ByRule}s, this is not a valid value.
+ */
+ COUNT;
}
/**
@@ -137,7 +141,7 @@ public class RecurrenceRule : BaseObject {
this.freq = freq;
}
- internal RecurrenceRule.from_ical(iCal.icalcomponent ical_component) throws Error {
+ internal RecurrenceRule.from_ical(iCal.icalcomponent ical_component, bool strict) throws Error {
// need DTSTART for timezone purposes
DateTime dtstart = new DateTime(ical_component, iCal.icalproperty_kind.DTSTART_PROPERTY);
@@ -155,11 +159,16 @@ public class RecurrenceRule : BaseObject {
if (rrule.count > 0) {
set_recurrence_count(rrule.count);
} else {
- Component.DateTime date_time = new DateTime.rrule_until(rrule, dtstart);
- if (date_time.is_date)
- set_recurrence_end_date(date_time.to_date());
- else
- set_recurrence_end_exact_time(date_time.to_exact_time());
+ try {
+ Component.DateTime date_time = new DateTime.rrule_until(rrule, dtstart, strict);
+ if (date_time.is_date)
+ set_recurrence_end_date(date_time.to_date());
+ else
+ set_recurrence_end_exact_time(date_time.to_exact_time());
+ } catch (ComponentError comperr) {
+ if (!(comperr is ComponentError.UNAVAILABLE))
+ throw comperr;
+ }
}
switch (rrule.week_start) {
@@ -253,6 +262,23 @@ public class RecurrenceRule : BaseObject {
}
/**
+ * Returns the UNTIL property as a { link Calendar.Date}.
+ *
+ * If { link until_exact_time} is set, only the Date portion is returned.
+ *
+ * @returns null if neither { link until_date} or until_exact_time is set.
+ */
+ public Calendar.Date? get_recurrence_end_date() {
+ if (until_date != null)
+ return until_date;
+
+ if (until_exact_time != null)
+ return new Calendar.Date.from_exact_time(until_exact_time);
+
+ return null;
+ }
+
+ /**
* Sets the { link count} property.
*
* Also clears { link until_date} and { link until_exact_time}.
@@ -316,6 +342,8 @@ public class RecurrenceRule : BaseObject {
dow = Calendar.DayOfWeek.for(dow_value, Calendar.FirstOfWeek.SUNDAY);
} catch (CalendarError calerr) {
debug("Unable to decode day of week value %d: %s", dow_value, calerr.message);
+
+ return false;
}
}
@@ -465,6 +493,21 @@ public class RecurrenceRule : BaseObject {
}
/**
+ * Returns a Gee.Set of { link ByRule}s that are active, i.e. have defined rules.
+ */
+ public Gee.Set<ByRule> get_active_by_rules() {
+ Gee.Set<ByRule> active = new Gee.HashSet<ByRule>();
+ for (int ctr = 0; ctr < ByRule.COUNT; ctr++) {
+ ByRule by_rule = (ByRule) ctr;
+
+ if (get_by_set(by_rule).size > 0)
+ active.add(by_rule);
+ }
+
+ return active;
+ }
+
+ /**
* Converts a { link RecurrenceRule} into an iCalendar RRULE property and adds it to the
* iCal component.
*
diff --git a/src/host/host-create-update-event.vala b/src/host/host-create-update-event.vala
index 77694f7..f8a755d 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -8,6 +8,9 @@ namespace California.Host {
/**
* A blank "form" of widgets for the user to enter or update event details.
+ *
+ * Message IN: If creating a new event, send Component.Event.blank() (pre-filled with any known
+ * details). If updating an existing event, send Component.Event.clone().
*/
[GtkTemplate (ui = "/org/yorba/california/rc/create-update-event.ui")]
@@ -20,6 +23,9 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
private const int END_HOUR = 23;
private const int MIN_DIVISIONS = 15;
+ private const string FAMILY_NORMAL = "normal";
+ private const string FAMILY_RECURRING = "recurring";
+
public string card_id { get { return ID; } }
public string? title { get { return null; } }
@@ -56,7 +62,7 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
private Gtk.ComboBoxText calendar_combo;
[GtkChild]
- private Gtk.Button accept_button;
+ private Gtk.Box rotating_button_box_container;
public Calendar.DateSpan selected_date_span { get; set; }
@@ -69,6 +75,14 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
private Gtk.Button? last_date_button_touched = null;
private bool both_date_buttons_touched = false;
+ private Toolkit.RotatingButtonBox rotating_button_box = new Toolkit.RotatingButtonBox();
+
+ private Gtk.Button accept_button = new Gtk.Button();
+ private Gtk.Button cancel_button = new Gtk.Button.with_mnemonic(_("_Cancel"));
+ private Gtk.Button update_all_button = new Gtk.Button.with_mnemonic(_("Update A_ll Events"));
+ private Gtk.Button update_this_button = new Gtk.Button.with_mnemonic(_("Update _This Event"));
+ private Gtk.Button cancel_recurring_button = new Gtk.Button.with_mnemonic(_("_Cancel"));
+
public CreateUpdateEvent() {
// when selected_date_span updates, update date buttons as well
notify[PROP_SELECTED_DATE_SPAN].connect(() => {
@@ -101,16 +115,40 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
calendar_model.add(calendar_source);
}
+ accept_button.get_style_context().add_class("suggested-action");
+
+ accept_button.clicked.connect(on_accept_button_clicked);
+ cancel_button.clicked.connect(on_cancel_button_clicked);
+ update_all_button.clicked.connect(on_update_all_button_clicked);
+ update_this_button.clicked.connect(on_update_this_button_clicked);
+ cancel_recurring_button.clicked.connect(on_cancel_recurring_button_clicked);
+
+ rotating_button_box.pack_end(FAMILY_NORMAL, cancel_button);
+ rotating_button_box.pack_end(FAMILY_NORMAL, accept_button);
+
+ rotating_button_box.pack_end(FAMILY_RECURRING, cancel_recurring_button);
+ rotating_button_box.pack_end(FAMILY_RECURRING, update_all_button);
+ rotating_button_box.pack_end(FAMILY_RECURRING, update_this_button);
+
+ // The cancel-recurring-update button looks big compared to other buttons, so allow for the
+ // ButtonBox to reduce it in size
+
rotating_button_box.get_family_container(FAMILY_RECURRING).child_set_property(cancel_recurring_button,
+ "non-homogeneous", true);
+
+ rotating_button_box.expand = true;
+ rotating_button_box.halign = Gtk.Align.FILL;
+ rotating_button_box.valign = Gtk.Align.END;
+ rotating_button_box_container.add(rotating_button_box);
+
update_controls();
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
- if (message != null) {
- event = message as Component.Event;
- assert(event != null);
- } else {
- event = new Component.Event.blank();
- }
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
+ // if no message, leave everything as it is
+ if (message == null)
+ return;
+
+ event = (Component.Event) message;
update_controls();
}
@@ -139,6 +177,13 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
selected_date_span = new Calendar.DateSpan(Calendar.System.today, Calendar.System.today);
initial_start_time = Calendar.System.now.to_wall_time();
initial_end_time = Calendar.System.now.adjust_time(1, Calendar.TimeUnit.HOUR).to_wall_time();
+
+ // set in Component.Event as well, to at least initialize it for use elsewhere while
+ // editing (such as the RRULE)
+ event.set_event_exact_time_span(new Calendar.ExactTimeSpan(
+ new Calendar.ExactTime(Calendar.Timezone.local, Calendar.System.today, initial_start_time),
+ new Calendar.ExactTime(Calendar.Timezone.local, Calendar.System.today, initial_end_time)
+ ));
}
// initialize start and end time controls (as in, wall clock time)
@@ -190,6 +235,10 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
description_textview.buffer.text = event.description ?? "";
accept_button.label = is_update ? _("_Update") : _("C_reate");
+ accept_button.use_underline = true;
+
+ rotating_button_box.family = FAMILY_NORMAL;
+
original_calendar_source = event.calendar_source;
}
@@ -231,23 +280,64 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
}
[GtkCallback]
- private void on_accept_clicked() {
+ private void on_recurring_button_clicked() {
+ // update the component with what's in the controls now
+ update_component(event, true);
+
+ // send off to recurring editor
+ jump_to_card_by_name(CreateUpdateRecurring.ID, event);
+ }
+
+ private void on_accept_button_clicked() {
if (calendar_model.active == null)
return;
- event.calendar_source = calendar_model.active;
- event.summary = summary_entry.text;
- event.location = location_entry.text;
- event.description = description_textview.buffer.text;
+ // if updating a recurring event, need to ask about update scope
+ if (event.is_generated_instance && is_update) {
+ rotating_button_box.family = FAMILY_RECURRING;
+
+ return;
+ }
+
+ // create/update this instance of the event
+ create_update_event(event, true);
+ }
+
+ // TODO: Now that a clone is being used for editing, can directly bind controls properties to
+ // Event's properties and update that way ... doesn't quite work when updating the master event,
+ // however
+ private void update_component(Component.Event target, bool replace_dtstart) {
+ target.calendar_source = calendar_model.active;
+ target.summary = summary_entry.text;
+ target.location = location_entry.text;
+ target.description = description_textview.buffer.text;
+
+ // if updating the master, don't replace the dtstart/dtend, but do want to adjust it from
+ // DATE to DATE-TIME or vice-versa
+ if (!replace_dtstart) {
+ if (target.is_all_day != all_day_toggle.active) {
+ if (all_day_toggle.active) {
+ target.timed_to_all_day_event();
+ } else {
+ target.all_day_to_timed_event(
+ time_map.get(dtstart_time_combo.get_active_text()),
+ time_map.get(dtend_time_combo.get_active_text()),
+ Calendar.Timezone.local
+ );
+ }
+ }
+
+ return;
+ }
if (all_day_toggle.active) {
- event.set_event_date_span(selected_date_span);
+ target.set_event_date_span(selected_date_span);
} else {
// use existing timezone unless not specified in original event
- Calendar.Timezone tz = (event.exact_time_span != null)
- ? event.exact_time_span.start_exact_time.tz
+ Calendar.Timezone tz = (target.exact_time_span != null)
+ ? target.exact_time_span.start_exact_time.tz
: Calendar.Timezone.local;
- event.set_event_exact_time_span(
+ target.set_event_exact_time_span(
new Calendar.ExactTimeSpan(
new Calendar.ExactTime(tz, selected_date_span.start_date,
time_map.get(dtstart_time_combo.get_active_text())),
@@ -256,20 +346,35 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
)
);
}
+ }
+
+ private void create_update_event(Component.Event target, bool replace_dtstart) {
+ update_component(target, replace_dtstart);
if (is_update)
- update_event_async.begin(null);
+ update_event_async.begin(target, null);
else
- create_event_async.begin(null);
+ create_event_async.begin(target, null);
}
- [GtkCallback]
private void on_cancel_button_clicked() {
- jump_home_or_user_closed();
+ notify_user_closed();
+ }
+
+ private void on_update_all_button_clicked() {
+ create_update_event(event.is_master_instance ? event : (Component.Event) event.master, false);
+ }
+
+ private void on_update_this_button_clicked() {
+ create_update_event(event, true);
+ }
+
+ private void on_cancel_recurring_button_clicked() {
+ rotating_button_box.family = FAMILY_NORMAL;
}
- private async void create_event_async(Cancellable? cancellable) {
- if (event.calendar_source == null) {
+ private async void create_event_async(Component.Event target, Cancellable? cancellable) {
+ if (target.calendar_source == null) {
notify_failure(_("Unable to create event: calendar must be specified"));
return;
@@ -279,7 +384,7 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
Error? create_err = null;
try {
- yield event.calendar_source.create_component_async(event, cancellable);
+ yield event.calendar_source.create_component_async(target, cancellable);
} catch (Error err) {
create_err = err;
}
@@ -293,8 +398,8 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
}
// TODO: Delete from original source if not the same as the new source
- private async void update_event_async(Cancellable? cancellable) {
- if (event.calendar_source == null) {
+ private async void update_event_async(Component.Event target, Cancellable? cancellable) {
+ if (target.calendar_source == null) {
notify_failure(_("Unable to update event: calendar must be specified"));
return;
@@ -304,7 +409,7 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
Error? update_err = null;
try {
- yield event.calendar_source.update_component_async(event, cancellable);
+ yield event.calendar_source.update_component_async(target, cancellable);
} catch (Error err) {
update_err = err;
}
diff --git a/src/host/host-create-update-recurring.vala b/src/host/host-create-update-recurring.vala
new file mode 100644
index 0000000..ce6c4eb
--- /dev/null
+++ b/src/host/host-create-update-recurring.vala
@@ -0,0 +1,585 @@
+/* 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/create-update-recurring.ui")]
+public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
+ public const string ID = "CreateUpdateRecurring";
+
+ private const string PROP_START_DATE = "start-date";
+ private const string PROP_END_DATE = "end-date";
+
+ // DO NOT CHANGE VALUES UNLESS YOU KNOW WHAT YOU'RE DOING. These values are mirrored in the
+ // Glade file's repeats_combobox model.
+ private enum Repeats {
+ DAILY = 0,
+ WEEKLY = 1,
+ DAY_OF_THE_WEEK = 2,
+ DAY_OF_THE_MONTH = 3,
+ YEARLY = 4
+ }
+
+ public string card_id { get { return ID; } }
+
+ public string? title { get { return null; } }
+
+ public Gtk.Widget? default_widget { get { return ok_button; } }
+
+ public Gtk.Widget? initial_focus { get { return make_recurring_checkbutton; } }
+
+ public Calendar.Date? start_date { get; private set; default = null; }
+ public Calendar.Date? end_date { get; private set; default = null; }
+
+ [GtkChild]
+ private Gtk.CheckButton make_recurring_checkbutton;
+
+ [GtkChild]
+ private Gtk.Grid child_grid;
+
+ [GtkChild]
+ private Gtk.ComboBoxText repeats_combobox;
+
+ [GtkChild]
+ private Gtk.Entry every_entry;
+
+ [GtkChild]
+ private Gtk.Label every_label;
+
+ [GtkChild]
+ private Gtk.Label on_days_label;
+
+ [GtkChild]
+ private Gtk.Box on_days_box;
+
+ [GtkChild]
+ private Gtk.CheckButton sunday_checkbutton;
+
+ [GtkChild]
+ private Gtk.CheckButton monday_checkbutton;
+
+ [GtkChild]
+ private Gtk.CheckButton tuesday_checkbutton;
+
+ [GtkChild]
+ private Gtk.CheckButton wednesday_checkbutton;
+
+ [GtkChild]
+ private Gtk.CheckButton thursday_checkbutton;
+
+ [GtkChild]
+ private Gtk.CheckButton friday_checkbutton;
+
+ [GtkChild]
+ private Gtk.CheckButton saturday_checkbutton;
+
+ [GtkChild]
+ private Gtk.Button start_date_button;
+
+ [GtkChild]
+ private Gtk.RadioButton never_radiobutton;
+
+ [GtkChild]
+ private Gtk.RadioButton after_radiobutton;
+
+ [GtkChild]
+ private Gtk.Entry after_entry;
+
+ [GtkChild]
+ private Gtk.Label after_label;
+
+ [GtkChild]
+ private Gtk.RadioButton ends_on_radiobutton;
+
+ [GtkChild]
+ private Gtk.Label warning_label;
+
+ [GtkChild]
+ private Gtk.Button end_date_button;
+
+ [GtkChild]
+ private Gtk.Button ok_button;
+
+ private new Component.Event? event = null;
+ private Component.Event? master = null;
+ private Gee.HashMap<Calendar.DayOfWeek, Gtk.CheckButton> on_day_checkbuttons = new Gee.HashMap<
+ Calendar.DayOfWeek, Gtk.CheckButton>();
+ private bool blocking_insert_text_numbers_only_signal = false;
+
+ public CreateUpdateRecurring() {
+ // "Repeating event" checkbox activates almost every other control in this dialog
+ make_recurring_checkbutton.bind_property("active", child_grid, "sensitive",
+ BindingFlags.SYNC_CREATE);
+
+ // On Days and its checkbox are only visible when Repeats is set to Weekly
+ repeats_combobox.bind_property("active", on_days_label, "visible",
+ BindingFlags.SYNC_CREATE, transform_repeats_active_to_on_days_visible);
+ repeats_combobox.bind_property("active", on_days_box, "visible",
+ BindingFlags.SYNC_CREATE, transform_repeats_active_to_on_days_visible);
+
+ // Ends radio buttons need to make their assoc. controls sensitive when active
+ after_radiobutton.bind_property("active", after_entry, "sensitive",
+ BindingFlags.SYNC_CREATE);
+ ends_on_radiobutton.bind_property("active", end_date_button, "sensitive",
+ BindingFlags.SYNC_CREATE);
+
+ // use private Date properties to synchronize with date button labels
+ bind_property(PROP_START_DATE, start_date_button, "label", BindingFlags.SYNC_CREATE,
+ transform_date_to_string);
+ bind_property(PROP_END_DATE, end_date_button, "label", BindingFlags.SYNC_CREATE,
+ transform_date_to_string);
+
+ // map on-day checkboxes to days of week
+ on_day_checkbuttons[Calendar.DayOfWeek.SUN] = sunday_checkbutton;
+ on_day_checkbuttons[Calendar.DayOfWeek.MON] = monday_checkbutton;
+ on_day_checkbuttons[Calendar.DayOfWeek.TUE] = tuesday_checkbutton;
+ on_day_checkbuttons[Calendar.DayOfWeek.WED] = wednesday_checkbutton;
+ on_day_checkbuttons[Calendar.DayOfWeek.THU] = thursday_checkbutton;
+ on_day_checkbuttons[Calendar.DayOfWeek.FRI] = friday_checkbutton;
+ on_day_checkbuttons[Calendar.DayOfWeek.SAT] = saturday_checkbutton;
+
+ // Ok button's sensitivity is tied to a whole-lotta controls here
+ make_recurring_checkbutton.bind_property("active", ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ every_entry.bind_property("text", ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ repeats_combobox.bind_property("active", ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ foreach (Gtk.CheckButton checkbutton in on_day_checkbuttons.values) {
+ checkbutton.bind_property("active", ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ }
+ bind_property(PROP_START_DATE, ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ ends_on_radiobutton.bind_property("active", ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ bind_property(PROP_END_DATE, ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ after_radiobutton.bind_property("active", ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ after_entry.bind_property("text", ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
+ }
+
+ private bool transform_repeats_active_to_on_days_visible(Binding binding, Value source_value,
+ ref Value target_value) {
+ target_value = (repeats_combobox.active == Repeats.WEEKLY);
+
+ return true;
+ }
+
+ private bool transform_date_to_string(Binding binding, Value source_value, ref Value target_value) {
+ Calendar.Date? date = (Calendar.Date?) source_value;
+ target_value = (date != null) ? date.to_standard_string() : "";
+
+ return true;
+ }
+
+ private bool transform_to_ok_button_sensitive(Binding binding, Value source_value, ref Value
target_value) {
+ target_value = is_ok_ready();
+
+ return true;
+ }
+
+ // if controls are added or removed here, that needs to be reflected in the ctor by binding/
+ // unbinding to its properties
+ private bool is_ok_ready() {
+ // if not recurring, ok
+ if (!make_recurring_checkbutton.active)
+ return true;
+
+ // every entry must be positive value
+ if (String.is_empty(every_entry.text) || int.parse(every_entry.text) <= 0)
+ return false;
+
+ // if weekly, at least one checkbox must be active
+ if (repeats_combobox.active == Repeats.WEEKLY) {
+ if (!traverse<Gtk.CheckButton>(on_day_checkbuttons.values).any(checkbutton =>
checkbutton.active))
+ return false;
+ }
+
+ // need a start date
+ if (start_date == null)
+ return false;
+
+ // end date required if specified
+ if (ends_on_radiobutton.active && end_date == null)
+ return false;
+
+ // count required if specified
+ if (after_radiobutton.active) {
+ if (String.is_empty(after_entry.text) || int.parse(after_entry.text) <= 0)
+ return false;
+ }
+
+ return true;
+ }
+
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
+ assert(message != null);
+
+ event = (Component.Event) message;
+ master = event.is_master_instance ? event : (Component.Event) event.master;
+
+ update_controls();
+ }
+
+ private void update_controls() {
+ make_recurring_checkbutton.active = (master.rrule != null);
+
+ // some defaults that may not be set even if an RRULE is present
+
+ // "Ends ... After" entry
+ after_entry.text = "1";
+
+ // "Starts" and "Ends...On" entries
+ Calendar.DateSpan event_span = master.get_event_date_span(Calendar.Timezone.local);
+ start_date = event_span.start_date;
+ end_date = event_span.end_date;
+
+ // Clear all "On days" checkboxes for sanity's sake
+ foreach (Gtk.CheckButton checkbutton in on_day_checkbuttons.values)
+ checkbutton.active = false;
+
+ // set remaining defaults if not a recurring event
+ if (master.rrule == null) {
+ repeats_combobox.active = Repeats.DAILY;
+ every_entry.text = "1";
+ never_radiobutton.active = true;
+ warning_label.visible = false;
+
+ return;
+ }
+
+ // "Repeats" combobox
+ switch (master.rrule.freq) {
+ case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
+ repeats_combobox.active = Repeats.WEEKLY;
+ break;
+
+ case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
+ bool by_day = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
+ bool by_monthday = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size
0;
+
+ // fall back on month day of the week
+ if (!by_day && by_monthday)
+ repeats_combobox.active = Repeats.DAY_OF_THE_MONTH;
+ else
+ repeats_combobox.active = Repeats.DAY_OF_THE_WEEK;
+ break;
+
+ case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
+ repeats_combobox.active = Repeats.YEARLY;
+ break;
+
+ // Fall back on Daily for default, warning label is shown if anything not supported
+ case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
+ default:
+ repeats_combobox.active = Repeats.DAILY;
+ break;
+ }
+
+ // "Every" entry
+ every_entry.text = master.rrule.interval.to_string();
+
+ // "On days" week day checkboxes are only visible if a WEEKLY event
+ if (master.rrule.is_weekly) {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days =
+
Component.RecurrenceRule.decode_days(master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY));
+
+ // the presence of a "null" day means every or all days
+ if (by_days.has_key(null)) {
+ foreach (Gtk.CheckButton checkbutton in on_day_checkbuttons.values)
+ checkbutton.active = true;
+ } else {
+ foreach (Calendar.DayOfWeek dow in by_days.keys)
+ on_day_checkbuttons[dow].active = true;
+ }
+ }
+
+ // "Ends" choices
+ if (!master.rrule.has_duration) {
+ never_radiobutton.active = true;
+ } else if (master.rrule.count > 0) {
+ after_radiobutton.active = true;
+ after_entry.text = master.rrule.count.to_string();
+ } else {
+ assert(master.rrule.until_date != null || master.rrule.until_exact_time != null);
+
+ ends_on_radiobutton.active = true;
+ end_date = master.rrule.get_recurrence_end_date();
+ }
+
+ // look for RRULEs that our editor cannot deal with
+ string? supported = is_supported_rrule();
+ if (supported != null)
+ debug("Unsupported RRULE: %s", supported);
+
+ warning_label.visible = supported != null;
+ }
+
+ // Returns a logging string for why not reported, null if supported
+ private string? is_supported_rrule() {
+ // only some frequencies support, and in some of those, certain requirements
+ switch (master.rrule.freq) {
+ case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
+ case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
+ // do nothing, continue
+ break;
+
+ case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
+ // can only hold BYDAY rules and all BYDAY rules must be zero
+ Gee.Set<Component.RecurrenceRule.ByRule> active = master.rrule.get_active_by_rules();
+ if (!active.contains(Component.RecurrenceRule.ByRule.DAY))
+ return "weekly-not-byday";
+
+ if (active.size > 1)
+ return "weekly-multiple-byrules";
+
+ foreach (int day in master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY)) {
+ int position;
+ if (!Component.RecurrenceRule.decode_day(day, null, out position))
+ return "weekly-undecodeable-byday";
+
+ if (position != 0)
+ return "weekly-nonzero-byday-position";
+ }
+ break;
+
+ // Must be a "simple" monthly recurrence
+ case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
+ bool by_day = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
+ bool by_monthday = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size
0;
+
+ // can support one and only one
+ if (by_day == by_monthday)
+ return "monthly-byday-and-bymonthday";
+
+ if (master.rrule.get_active_by_rules().size > 1)
+ return "monthly-multiple-byrules";
+ break;
+
+ default:
+ return "unsupported-frequency";
+ }
+
+ // do not support editing w/ EXDATEs
+ if (!Collection.is_empty(master.exdates))
+ return "exdates";
+
+ // do not support editing w/ RDATEs
+ if (!Collection.is_empty(master.rdates))
+ return "rdates";
+
+ return null;
+ }
+
+ [GtkCallback]
+ private void on_repeats_combobox_changed() {
+ on_repeats_combobox_or_every_entry_changed();
+ }
+
+ [GtkCallback]
+ private void on_every_entry_changed() {
+ on_repeats_combobox_or_every_entry_changed();
+ }
+
+ private void on_repeats_combobox_or_every_entry_changed() {
+ int every_count = !String.is_empty(every_entry.text) ? int.parse(every_entry.text) : 1;
+ every_count = every_count.clamp(1, int.MAX);
+
+ unowned string text;
+ switch (repeats_combobox.active) {
+ case Repeats.DAY_OF_THE_MONTH:
+ case Repeats.DAY_OF_THE_WEEK:
+ text = ngettext("month", "months", every_count);
+ break;
+
+ case Repeats.WEEKLY:
+ text = ngettext("week", "weeks", every_count);
+ break;
+
+ case Repeats.YEARLY:
+ text = ngettext("year", "years", every_count);
+ break;
+
+ case Repeats.DAILY:
+ default:
+ text = ngettext("day", "days", every_count);
+ break;
+ }
+
+ every_label.label = text;
+ }
+
+ [GtkCallback]
+ private void on_after_entry_changed() {
+ int after_count = !String.is_empty(after_entry.text) ? int.parse(after_entry.text) : 1;
+ after_count = after_count.clamp(1, int.MAX);
+
+ after_label.label = ngettext("event", "events", after_count);
+ }
+
+ [GtkCallback]
+ private void on_date_button_clicked(Gtk.Button date_button) {
+ bool is_dtstart = (date_button == start_date_button);
+
+ Toolkit.CalendarPopup popup = new Toolkit.CalendarPopup(date_button,
+ is_dtstart ? start_date : end_date);
+
+ popup.date_activated.connect((date) => {
+ if (is_dtstart)
+ start_date = date;
+ else
+ end_date = date;
+ });
+
+ popup.dismissed.connect(() => {
+ popup.destroy();
+ });
+
+ popup.show_all();
+ }
+
+ [GtkCallback]
+ private void on_insert_text_numbers_only(Gtk.Editable editable, string new_text, int new_text_length,
+ ref int position) {
+ // prevent recursion when our modified text is inserted (i.e. allow the base handler to
+ // deal new text directly)
+ if (blocking_insert_text_numbers_only_signal)
+ return;
+
+ // filter out everything not a number
+ string numbers_only = from_string(new_text)
+ .filter(ch => ch.isdigit())
+ .to_string(ch => ch.to_string());
+
+ // insert new text into place, ensure this handler doesn't attempt to process this
+ // modified text ... would use SignalHandler.block_by_func() and unblock_by_func(), but
+ // the bindings are ungood
+ if (!String.is_empty(numbers_only)) {
+ blocking_insert_text_numbers_only_signal = true;
+ editable.insert_text(numbers_only, numbers_only.length, ref position);
+ blocking_insert_text_numbers_only_signal = false;
+ }
+
+ // don't let the base handler have at the original text
+ Signal.stop_emission_by_name(editable, "insert-text");
+ }
+
+ [GtkCallback]
+ private void on_cancel_button_clicked() {
+ jump_back();
+ }
+
+ [GtkCallback]
+ private void on_ok_button_clicked() {
+ update_master();
+ jump_to_card_by_name(CreateUpdateEvent.ID, event);
+ }
+
+ private void update_master() {
+ if (!make_recurring_checkbutton.active) {
+ master.make_recurring(null);
+
+ return;
+ }
+
+ iCal.icalrecurrencetype_frequency freq;
+ switch (repeats_combobox.active) {
+ case Repeats.DAILY:
+ freq = iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE;
+ break;
+
+ case Repeats.WEEKLY:
+ freq = iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE;
+ break;
+
+ case Repeats.DAY_OF_THE_WEEK:
+ case Repeats.DAY_OF_THE_MONTH:
+ freq = iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE;
+ break;
+
+ case Repeats.YEARLY:
+ freq = iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE;
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ Component.RecurrenceRule rrule = new Component.RecurrenceRule(freq);
+ rrule.interval = Numeric.floor_int(int.parse(every_entry.text), 1);
+
+ if (rrule.is_weekly) {
+ Gee.HashMap<Calendar.DayOfWeek?, int> by_day = new Gee.HashMap<Calendar.DayOfWeek?, int>();
+ foreach (Calendar.DayOfWeek dow in on_day_checkbuttons.keys) {
+ if (on_day_checkbuttons[dow].active)
+ by_day[dow] = 0;
+ }
+
+ // although control sensitivity should prevent this from happening, be double-sure to
+ // prevent infinite loops below
+ if (by_day.size == 0)
+ by_day[start_date.day_of_week] = 0;
+
+ rrule.set_by_rule(Component.RecurrenceRule.ByRule.DAY,
+ Component.RecurrenceRule.encode_days(by_day));
+
+ // need to also update the start date to fall on one of the selected days of the week
+ // start by looking backward
+ Calendar.Date new_start_date = start_date.prior(true, (date) => {
+ return date.day_of_week in by_day.keys;
+ });
+
+ // if start date is prior to today's day, move forward
+ if (new_start_date.compare_to(Calendar.System.today) < 0) {
+ new_start_date = start_date.upcoming(true, (date) => {
+ return date.day_of_week in by_day.keys;
+ });
+ }
+
+ start_date = new_start_date;
+ }
+
+ // set start and end dates (which may actually be date-times, so use adjust)
+ if (never_radiobutton.active) {
+ // no duration
+ master.adjust_event_date_span(start_date.to_date_span());
+ rrule.set_recurrence_end_date(null);
+ } else if (ends_on_radiobutton.active) {
+ master.adjust_event_date_span(new Calendar.DateSpan(start_date, end_date));
+ rrule.set_recurrence_end_date(end_date);
+ } else {
+ assert(after_radiobutton.active);
+
+ master.adjust_event_date_span(start_date.to_date_span());
+ rrule.set_recurrence_count(Numeric.floor_int(int.parse(after_entry.text), 1));
+ }
+
+ if (rrule.is_monthly) {
+ if (repeats_combobox.active == Repeats.DAY_OF_THE_WEEK) {
+ Gee.HashMap<Calendar.DayOfWeek?, int> by_day = new Gee.HashMap<Calendar.DayOfWeek?, int>();
+ by_day[start_date.day_of_week] =
start_date.week_of(Calendar.System.first_of_week).week_of_month;
+ rrule.set_by_rule(Component.RecurrenceRule.ByRule.DAY,
+ Component.RecurrenceRule.encode_days(by_day));
+ } else {
+ Gee.Collection<int> by_month_day = new Gee.ArrayList<int>();
+ by_month_day.add(start_date.day_of_month.value);
+ rrule.set_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY, by_month_day);
+ }
+ }
+
+ // remove EXDATEs and RDATEs, those are not currently supported
+ master.exdates = null;
+ master.rdates = null;
+
+ master.make_recurring(rrule);
+ }
+}
+
+}
+
diff --git a/src/host/host-main-window.vala b/src/host/host-main-window.vala
index 71a2cac..a753a18 100644
--- a/src/host/host-main-window.vala
+++ b/src/host/host-main-window.vala
@@ -351,6 +351,10 @@ public class MainWindow : Gtk.ApplicationWindow {
Toolkit.spin_event_loop();
});
+ deck_window.deck.failure.connect((msg) => {
+ Application.instance.error_message(msg);
+ });
+
deck_window.show_all();
deck_window.run();
deck_window.destroy();
@@ -424,12 +428,21 @@ public class MainWindow : Gtk.ApplicationWindow {
}
private void quick_create_event(Component.Event? initial, Gtk.Widget relative_to, Gdk.Point?
for_location) {
- QuickCreateEvent quick_create = new QuickCreateEvent(initial);
+ QuickCreateEvent quick_create = new QuickCreateEvent();
+
CreateUpdateEvent create_update = new CreateUpdateEvent();
create_update.is_update = false;
+ CreateUpdateRecurring create_update_recurring = new CreateUpdateRecurring();
+
Toolkit.Deck deck = new Toolkit.Deck();
- deck.add_cards(iterate<Toolkit.Card>(quick_create, create_update).to_array_list());
+ deck.add_cards(
+ iterate<Toolkit.Card>(quick_create, create_update, create_update_recurring)
+ .to_array_list()
+ );
+
+ // initialize the Deck with the initial event (if any)
+ deck.go_home(initial);
show_deck(relative_to, for_location, deck);
}
@@ -438,12 +451,18 @@ public class MainWindow : Gtk.ApplicationWindow {
Gdk.Point? for_location) {
ShowEvent show_event = new ShowEvent();
- CreateUpdateEvent create_update_event = new CreateUpdateEvent();
- create_update_event.is_update = true;
+ CreateUpdateEvent create_update = new CreateUpdateEvent();
+ create_update.is_update = true;
+
+ CreateUpdateRecurring create_update_recurring = new CreateUpdateRecurring();
Toolkit.Deck deck = new Toolkit.Deck();
- deck.add_card(show_event);
- deck.add_card(create_update_event);
+ deck.add_cards(
+ iterate<Toolkit.Card>(show_event, create_update, create_update_recurring)
+ .to_array_list()
+ );
+
+ // "initialize" the Deck with the requested Event (because ShowEvent is first, it's home)
deck.go_home(event);
show_deck(relative_to, for_location, deck);
diff --git a/src/host/host-quick-create-event.vala b/src/host/host-quick-create-event.vala
index 267a0b0..4ce6d9b 100644
--- a/src/host/host-quick-create-event.vala
+++ b/src/host/host-quick-create-event.vala
@@ -40,15 +40,18 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
private Toolkit.ComboBoxTextModel<Backing.CalendarSource> model;
- public QuickCreateEvent(Component.Event? initial) {
- event = initial;
+ public QuickCreateEvent() {
+ }
+
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
+ event = (message != null) ? message as Component.Event : null;
// if initial date/times supplied, reveal to the user and change the example
string eg;
- if (initial != null && (initial.date_span != null || initial.exact_time_span != null)) {
+ if (event != null && (event.date_span != null || event.exact_time_span != null)) {
when_box.visible = true;
- when_text_label.label = initial.get_event_time_pretty_string(Calendar.Timezone.local);
- if (initial.date_span != null)
+ when_text_label.label = event.get_event_time_pretty_string(Calendar.Timezone.local);
+ if (event.date_span != null)
eg = _("Example: Dinner at Tadich Grill 7:30pm");
else
eg = _("Example: Dinner at Tadich Grill");
@@ -76,9 +79,6 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
calendar_combo_box.active = 0;
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
- }
-
[GtkCallback]
private void on_details_entry_icon_release(Gtk.Entry entry, Gtk.EntryIconPosition icon,
Gdk.Event event) {
@@ -97,11 +97,7 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
string details = details_entry.text.strip();
if (String.is_empty(details)) {
- // jump to Create/Update dialog and remove this Card from the Deck ... this ensures
- // that if the user presses Cancel in the Create/Update dialog the Deck exits rather
- // than returns here (via jump_home_or_user_closed())
- jump_to_card_by_name(CreateUpdateEvent.ID, event);
- deck.remove_cards(iterate<Toolkit.Card>(this).to_array_list());
+ create_empty_event();
return;
}
@@ -110,14 +106,22 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
event);
event = parser.event;
- if (event.is_valid(true)) {
+ if (event.is_valid(true))
create_event_async.begin(null);
- } else {
- // see note above about why the Deck jumps to Create/Update and then this Card is
- // removed
- jump_to_card_by_name(CreateUpdateEvent.ID, event);
- deck.remove_cards(iterate<Toolkit.Card>(this).to_array_list());
- }
+ else
+ create_empty_event();
+ }
+
+ private void create_empty_event() {
+ // Must pass some kind of event to create/update, so use blank if required
+ if (event == null)
+ event = new Component.Event.blank();
+
+ // jump to Create/Update dialog and remove this Card from the Deck ... this ensures
+ // that if the user presses Cancel in the Create/Update dialog the Deck exits rather
+ // than returns here (via jump_home_or_user_closed())
+ jump_to_card_by_name(CreateUpdateEvent.ID, event);
+ deck.remove_cards(iterate<Toolkit.Card>(this).to_array_list());
}
private async void create_event_async(Cancellable? cancellable) {
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index d16301f..21c5db0 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -6,6 +6,10 @@
namespace California.Host {
+/**
+ * MESSAGE IN: Send the Component.Event to be displayed.
+ */
+
[GtkTemplate (ui = "/org/yorba/california/rc/show-event.ui")]
public class ShowEvent : Gtk.Grid, Toolkit.Card {
public const string ID = "ShowEvent";
@@ -63,7 +67,8 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
Calendar.System.instance.today_changed.disconnect(build_display);
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
+ // no message, don't update display
if (message == null)
return;
@@ -86,13 +91,10 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
// description
set_label(null, description_text, Markup.linkify(escape(event.description), linkify_delegate));
- // don't current support updating recurring events properly; see
- // https://bugzilla.gnome.org/show_bug.cgi?id=725786
bool read_only = event.calendar_source != null && event.calendar_source.read_only;
- bool updatable = !event.is_recurring && !read_only;
- update_button.visible = updatable;
- update_button.no_show_all = updatable;
+ update_button.visible = !read_only;
+ update_button.no_show_all = !read_only;
remove_button.visible = !read_only;
remove_button.no_show_all = !read_only;
@@ -142,7 +144,7 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
//
// TODO: Gtk.Stack would be a better widget for this animation, but it's unavailable in
// Glade as of GTK+ 3.12.
- if (event.is_recurring) {
+ if (event.is_generated_instance) {
button_box_revealer.reveal_child = false;
remove_recurring_revealer.reveal_child = true;
@@ -175,7 +177,12 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
[GtkCallback]
private void on_update_button_clicked() {
- jump_to_card_by_name(CreateUpdateEvent.ID, event);
+ // pass a clone of the existing event for editing
+ try {
+ jump_to_card_by_name(CreateUpdateEvent.ID, event.clone() as Component.Event);
+ } catch (Error err) {
+ notify_failure(_("Unable to update event: %s").printf(err.message));
+ }
}
[GtkCallback]
diff --git a/src/manager/manager-calendar-list.vala b/src/manager/manager-calendar-list.vala
index 8287e7d..be48193 100644
--- a/src/manager/manager-calendar-list.vala
+++ b/src/manager/manager-calendar-list.vala
@@ -53,7 +53,7 @@ internal class CalendarList : Gtk.Grid, Toolkit.Card {
Backing.Manager.instance.notify[Backing.Manager.PROP_IS_OPEN].disconnect(on_manager_opened_closed);
}
- public void jumped_to(Toolkit.Card? from, Value? message) {
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
}
private void on_manager_opened_closed() {
diff --git a/src/rc/create-update-event.ui b/src/rc/create-update-event.ui
index e12c832..92b2637 100644
--- a/src/rc/create-update-event.ui
+++ b/src/rc/create-update-event.ui
@@ -127,10 +127,26 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
- <property name="pack_type">end</property>
<property name="position">6</property>
</packing>
</child>
+ <child>
+ <object class="GtkButton" id="recurring_button">
+ <property name="label" translatable="yes">Re_peats...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_left">8</property>
+ <property name="use_underline">True</property>
+ <signal name="clicked" handler="on_recurring_button_clicked"
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
</object>
<packing>
<property name="left_attach">0</property>
@@ -252,53 +268,14 @@
</packing>
</child>
<child>
- <object class="GtkButtonBox" id="button_box">
+ <object class="GtkBox" id="rotating_button_box_container">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="valign">end</property>
<property name="margin_top">8</property>
- <property name="vexpand">True</property>
- <property name="spacing">6</property>
- <property name="homogeneous">True</property>
- <property name="layout_style">end</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="can_default">True</property>
- <property name="receives_default">True</property>
- <property name="use_underline">True</property>
- <property name="image_position">bottom</property>
- <signal name="clicked" handler="on_cancel_button_clicked"
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
+ <property name="hexpand">True</property>
+ <property name="vexpand">False</property>
<child>
- <object class="GtkButton" id="accept_button">
- <property name="label" translatable="yes">C_reate</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="image_position">bottom</property>
- <signal name="clicked" handler="on_accept_clicked" object="CaliforniaHostCreateUpdateEvent"
swapped="no"/>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack_type">end</property>
- <property name="position">1</property>
- </packing>
+ <placeholder/>
</child>
</object>
<packing>
diff --git a/src/rc/create-update-recurring.ui b/src/rc/create-update-recurring.ui
new file mode 100644
index 0000000..205cef3
--- /dev/null
+++ b/src/rc/create-update-recurring.ui
@@ -0,0 +1,551 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <template class="CaliforniaHostCreateUpdateRecurring" parent="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">8</property>
+ <property name="margin_right">8</property>
+ <property name="margin_top">8</property>
+ <property name="margin_bottom">8</property>
+ <property name="row_spacing">6</property>
+ <child>
+ <object class="GtkCheckButton" id="make_recurring_checkbutton">
+ <property name="label" translatable="yes">_Repeating event</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="valign">start</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="child_grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">16</property>
+ <property name="row_spacing">8</property>
+ <property name="column_spacing">8</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Re_peats</property>
+ <property name="use_underline">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <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>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Every</property>
+ <property name="use_underline">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </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>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">En_ds</property>
+ <property name="use_underline">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <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>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Starts</property>
+ <property name="use_underline">True</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>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="on_days_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_On days</property>
+ <property name="use_underline">True</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>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="repeats_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <items>
+ <item id="0" translatable="yes">Daily</item>
+ <item id="1" translatable="yes">Weekly</item>
+ <item id="2" translatable="yes">Day of the week</item>
+ <item id="3" translatable="yes">Day of the month</item>
+ <item id="4" translatable="yes">Yearly</item>
+ </items>
+ <signal name="changed" handler="on_repeats_combobox_changed"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ </object>
+ <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>
+ <object class="GtkBox" id="on_days_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">8</property>
+ <child>
+ <object class="GtkCheckButton" id="sunday_checkbutton">
+ <property name="label" translatable="yes">_Sun</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="monday_checkbutton">
+ <property name="label" translatable="yes">_Mon</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="tuesday_checkbutton">
+ <property name="label" translatable="yes">_Tues</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="wednesday_checkbutton">
+ <property name="label" translatable="yes">_Wed</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="thursday_checkbutton">
+ <property name="label" translatable="yes">T_hu</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="friday_checkbutton">
+ <property name="label" translatable="yes">_Fri</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="saturday_checkbutton">
+ <property name="label" translatable="yes">S_at</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">16</property>
+ <child>
+ <object class="GtkRadioButton" id="never_radiobutton">
+ <property name="label" translatable="yes">_Never</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="on_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkRadioButton" id="ends_on_radiobutton">
+ <property name="label" translatable="yes" comments="As in, an event "ends on"
a date">_On</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">never_radiobutton</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="end_date_button">
+ <property name="label">(none)</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="clicked" handler="on_date_button_clicked"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</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">1</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="GtkRadioButton" id="after_radiobutton">
+ <property name="label" translatable="yes" comments="As in, "After n
events"">Aft_er</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">never_radiobutton</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="after_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="max_length">4</property>
+ <property name="activates_default">True</property>
+ <property name="width_chars">5</property>
+ <property name="input_purpose">number</property>
+ <signal name="changed" handler="on_after_entry_changed"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ <signal name="insert-text" handler="on_insert_text_numbers_only"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="after_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label">(none)</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="start_date_button">
+ <property name="label">(none)</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="halign">start</property>
+ <signal name="clicked" handler="on_date_button_clicked"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="every_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkEntry" id="every_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="max_length">4</property>
+ <property name="activates_default">True</property>
+ <property name="width_chars">5</property>
+ <property name="input_purpose">number</property>
+ <signal name="changed" handler="on_every_entry_changed"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ <signal name="insert-text" handler="on_insert_text_numbers_only"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="every_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label">(none)</property>
+ </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">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ <property name="height">1</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="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="spacing">8</property>
+ <property name="homogeneous">True</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="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="label" translatable="yes">_OK</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>
+ <signal name="clicked" handler="on_ok_button_clicked"
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </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">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="warning_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="margin_left">16</property>
+ <property name="margin_top">8</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">WARNING: California cannot edit this event's recurring
criteria.
+• Press Cancel to keep the current criteria.
+• Press OK to overwrite the existing criteria with your changes.</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </template>
+</interface>
diff --git a/src/tests/tests-iterable.vala b/src/tests/tests-iterable.vala
new file mode 100644
index 0000000..905c721
--- /dev/null
+++ b/src/tests/tests-iterable.vala
@@ -0,0 +1,45 @@
+/* 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.Tests {
+
+private class Iterable : UnitTest.Harness {
+ public Iterable() {
+ add_case("one-zero", one_zero);
+ add_case("one-one", one_one);
+ add_case("one_many", one_many);
+ }
+
+ protected override void setup() throws Error {
+ Collection.init();
+ }
+
+ protected override void teardown() {
+ Collection.terminate();
+ }
+
+ private bool one_zero() throws Error {
+ return traverse<int?>(new Gee.ArrayList<int?>()).one() == null;
+ }
+
+ private bool one_one() throws Error {
+ Gee.ArrayList<int?> list = new Gee.ArrayList<int?>();
+ list.add(1);
+
+ return traverse<int?>(list).one() == 1;
+ }
+
+ private bool one_many() throws Error {
+ Gee.ArrayList<int?> list = new Gee.ArrayList<int?>();
+ list.add(1);
+ list.add(2);
+
+ return traverse<int?>(list).one() == null;
+ }
+}
+
+}
+
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index b8dd74c..24c171e 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -12,6 +12,7 @@ public int run(string[] args) {
LogLevelFlags.LEVEL_WARNING | LogLevelFlags.LEVEL_ERROR | LogLevelFlags.LEVEL_CRITICAL);
UnitTest.Harness.register(new String());
+ UnitTest.Harness.register(new Iterable());
UnitTest.Harness.register(new CalendarDate());
UnitTest.Harness.register(new CalendarMonthSpan());
UnitTest.Harness.register(new CalendarMonthOfYear());
diff --git a/src/toolkit/toolkit-calendar-popup.vala b/src/toolkit/toolkit-calendar-popup.vala
index 0c0633c..9bfe8a3 100644
--- a/src/toolkit/toolkit-calendar-popup.vala
+++ b/src/toolkit/toolkit-calendar-popup.vala
@@ -25,13 +25,19 @@ public class CalendarPopup : Popup {
/**
* Fired when the user selects a day of a month and year.
*
- * In current implementation the { link Popup} will be { link dismissed} with any selection.
- * Future work may allow the user to single-click on a day but require another action to
- * dismiss the Popup. Best for users to subscribe to { link dismissed} as well as this signal.
+ * @see date_activated
*/
public signal void date_selected(Calendar.Date date);
/**
+ * Fired when the user activates (double-clicks) a day of a month and year.
+ *
+ * Note that a double-click will result in { link date_selected} followed by this signal
+ * followed by { link dismissed}.
+ */
+ public signal void date_activated(Calendar.Date date);
+
+ /**
* inheritDoc
*/
public CalendarPopup(Gtk.Widget relative_to, Calendar.Date initial_date) {
@@ -41,16 +47,17 @@ public class CalendarPopup : Popup {
calendar.month = initial_date.month.value - 1;
calendar.year = initial_date.year.value;
- calendar.day_selected.connect(on_day_selected);
+ calendar.day_selected.connect(() => {
+ on_day_selected(false);
+ });
calendar.day_selected_double_click.connect(() => {
- on_day_selected();
- dismiss();
+ on_day_selected(true);
});
add(calendar);
}
- private void on_day_selected() {
+ private void on_day_selected(bool activated) {
Calendar.Date date;
try {
date = new Calendar.Date(
@@ -65,6 +72,11 @@ public class CalendarPopup : Popup {
}
date_selected(date);
+
+ if (activated) {
+ date_activated(date);
+ dismiss();
+ }
}
}
diff --git a/src/toolkit/toolkit-card.vala b/src/toolkit/toolkit-card.vala
index ed7ef81..8e2daee 100644
--- a/src/toolkit/toolkit-card.vala
+++ b/src/toolkit/toolkit-card.vala
@@ -15,6 +15,30 @@ namespace California.Toolkit {
public interface Card : Gtk.Widget {
/**
+ * Enumerates the various reasons a { link Card} may be jumped to.
+ */
+ public enum Jump {
+ /**
+ * The { link Card} was jumped to because it's home and { link jump_home} was fired by
+ * another Card.
+ */
+ HOME,
+ /**
+ * The { link Card} was jumped to because another Card fired { link jump_back} and this is
+ * the previous Card in the { link Deck}.
+ */
+ BACK,
+ /**
+ * The { link Card} was jumped directly to by another Card, either by { link card_id} or
+ * by an object instance.
+ *
+ * @see jump_to_card
+ * @see jump_to_card_by_name
+ */
+ DIRECT
+ }
+
+ /**
* Each { link Card} has its own identifier that should be unique within the { link Deck}.
*
* In the Gtk.Stack, this is its name.
@@ -124,7 +148,9 @@ public interface Card : Gtk.Widget {
* the Deck.
*
* message may be null even if the Card expects one; generally this means { link jump_back}
- * or { link jump_home} was invoked, resulting in this Card being activated.
+ * or { link jump_home} was invoked, resulting in this Card being activated. The supplied
+ * { link Jump} reason is useful for context. There are code paths where { link Jump.HOME}
+ * accepts a message; { link Jump.BACK} will never supply a message.
*
* Due to some mechanism inside of GSignal or Vala, it's possible for a caller to pass null
* that gets translated into a Value object holding a null pointer. Deck will watch for this
@@ -137,7 +163,8 @@ public interface Card : Gtk.Widget {
* This is called before dealing with { link default_widget} and { link initial_focus}, so
* changes to those properties in this call, if need be.
*/
- public abstract void jumped_to(Card? from, Value? message);
+ // TODO: Use a JumpContext object instead.
+ public abstract void jumped_to(Card? from, Jump reason, Value? message);
/**
* Dismiss the { link Deck} due to the user requesting it be closed or cancelled.
diff --git a/src/toolkit/toolkit-deck.vala b/src/toolkit/toolkit-deck.vala
index c738acf..6592690 100644
--- a/src/toolkit/toolkit-deck.vala
+++ b/src/toolkit/toolkit-deck.vala
@@ -80,7 +80,7 @@ public class Deck : Gtk.Stack {
private void on_child_to_top() {
// disconnect from previous top card and push onto nav stack
if (top != null) {
- top.jump_to_card.disconnect(on_jump_to_card);
+ top.jump_to_card.disconnect(on_jump_to_card_instance);
top.jump_to_card_by_name.disconnect(on_jump_to_card_by_name);
top.jump_back.disconnect(on_jump_back);
top.jump_home.disconnect(on_jump_home);
@@ -95,7 +95,7 @@ public class Deck : Gtk.Stack {
// make new visible child top Card and connect to its signals
top = visible_child as Card;
if (top != null) {
- top.jump_to_card.connect(on_jump_to_card);
+ top.jump_to_card.connect(on_jump_to_card_instance);
top.jump_to_card_by_name.connect(on_jump_to_card_by_name);
top.jump_back.connect(on_jump_back);
top.jump_home.connect(on_jump_home);
@@ -151,7 +151,7 @@ public class Deck : Gtk.Stack {
if (set_home_visible && home != null) {
set_visible_child(home);
- home.jumped_to(null, null);
+ home.jumped_to(null, Card.Jump.HOME, null);
}
}
@@ -187,7 +187,7 @@ public class Deck : Gtk.Stack {
if (displaying && top == null && home != null) {
navigation_stack.clear();
set_visible_child(home);
- home.jumped_to(null, null);
+ home.jumped_to(null, Card.Jump.HOME, null);
}
}
@@ -223,10 +223,10 @@ public class Deck : Gtk.Stack {
navigation_stack.clear();
set_visible_child(home);
- home.jumped_to(null, strip_null_value(message));
+ home.jumped_to(null, Card.Jump.HOME, strip_null_value(message));
}
- private void on_jump_to_card(Card card, Card next, Value? message) {
+ private void on_jump_to_card(Card card, Card next, Card.Jump reason, Value? message) {
// do nothing if already visible
if (get_visible_child() == next) {
debug("Already showing card %s", next.card_id);
@@ -242,13 +242,17 @@ public class Deck : Gtk.Stack {
}
set_visible_child(next);
- next.jumped_to(card, strip_null_value(message));
+ next.jumped_to(card, reason, strip_null_value(message));
+ }
+
+ private void on_jump_to_card_instance(Card card, Card next, Value? message) {
+ on_jump_to_card(card, next, Card.Jump.DIRECT, message);
}
private void on_jump_to_card_by_name(Card card, string name, Value? message) {
Card? next = names.get(name);
if (next != null)
- on_jump_to_card(card, next, message);
+ on_jump_to_card(card, next, Card.Jump.DIRECT, message);
else
GLib.message("Card %s not found in Deck", name);
}
@@ -256,7 +260,7 @@ public class Deck : Gtk.Stack {
private void on_jump_back(Card card) {
// if still not empty, next card is "back", so pop that off and jump to it
if (!navigation_stack.is_empty)
- on_jump_to_card(card, navigation_stack.poll_head(), null);
+ on_jump_to_card(card, navigation_stack.poll_head(), Card.Jump.BACK, null);
}
private void on_jump_home(Card card) {
@@ -264,7 +268,7 @@ public class Deck : Gtk.Stack {
navigation_stack.clear();
if (home != null)
- on_jump_to_card(card, home, null);
+ on_jump_to_card(card, home, Card.Jump.HOME, null);
else
message("No home card in Deck");
}
diff --git a/src/toolkit/toolkit-rotating-button-box.vala b/src/toolkit/toolkit-rotating-button-box.vala
new file mode 100644
index 0000000..13778f7
--- /dev/null
+++ b/src/toolkit/toolkit-rotating-button-box.vala
@@ -0,0 +1,89 @@
+/* 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.Toolkit {
+
+/**
+ * RotatingButtonBox is a specialty widget for displaying groups ("families") of buttons, with each
+ * family silding (rotating) into view when required.
+ *
+ * Each family of Gtk.Buttons are held in Gtk.ButtonBoxes. They are always laid out horizontally
+ * with an END layout style and fixed spacing. This widget is designed specifically for buttons
+ * which populate the bottom edge of a dialog or popover.
+ *
+ * Families are created on-demand. The direction of them sliding into view is determined by the
+ * order they are created, i.e. the first family created is to the "left" of subsequent families.
+ *
+ * Families are described by a string name. Family names are case-sensitive.
+ */
+
+public class RotatingButtonBox : Gtk.Stack {
+ public const string PROP_FAMILY = "family";
+
+ public Gtk.Orientation ORIENTATION = Gtk.Orientation.HORIZONTAL;
+ public Gtk.ButtonBoxStyle LAYOUT_STYLE = Gtk.ButtonBoxStyle.END;
+ public int SPACING = 8;
+
+ /**
+ * The family name currently visible.
+ */
+ public string? family { get; set; }
+
+ private Gee.HashMap<string, Gtk.ButtonBox> button_boxes = new Gee.HashMap<string, Gtk.ButtonBox>();
+
+ public RotatingButtonBox() {
+ homogeneous = true;
+ transition_duration = SLOW_STACK_TRANSITION_DURATION_MSEC;
+ transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
+
+ bind_property("visible-child-name", this, PROP_FAMILY,
+ BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
+ }
+
+ /**
+ * Pack a Gtk.Button at the start of a particular family, creating the family if necessary.
+ *
+ * See Gtk.Box.pack_start().
+ */
+ public void pack_start(string family, Gtk.Button button) {
+ get_family_container(family).pack_start(button);
+ }
+
+ /**
+ * Pack a Gtk.Button at the end of a particular family, creating the family if necessary.
+ *
+ * See Gtk.Box.pack_end().
+ */
+ public void pack_end(string family, Gtk.Button button) {
+ get_family_container(family).pack_end(button);
+ }
+
+ /**
+ * Direct access to the Gtk.ButtonBox holding the named family.
+ *
+ * If the family doesn't exist, it will be created.
+ */
+ public Gtk.ButtonBox get_family_container(string family) {
+ if (button_boxes.has_key(family))
+ return button_boxes.get(family);
+
+ // create new family of buttons
+ Gtk.ButtonBox button_box = new Gtk.ButtonBox(ORIENTATION);
+ button_box.layout_style = LAYOUT_STYLE;
+ button_box.spacing = SPACING;
+
+ // add to internal lookup
+ button_boxes.set(family, button_box);
+
+ // add to Gtk.Stack using the family name
+ add_named(button_box, family);
+
+ return button_box;
+ }
+}
+
+}
+
diff --git a/src/util/util-numeric.vala b/src/util/util-numeric.vala
new file mode 100644
index 0000000..518ab0d
--- /dev/null
+++ b/src/util/util-numeric.vala
@@ -0,0 +1,17 @@
+/* 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.Numeric {
+
+/**
+ * Returns the value if it is greater than or equal to floor, floor otherwise.
+ */
+public inline int floor_int(int value, int floor) {
+ return (value >= floor) ? value : floor;
+}
+
+}
+
diff --git a/vapi/libecal-1.2.vapi b/vapi/libecal-1.2.vapi
index 4d9a21a..d692d2e 100644
--- a/vapi/libecal-1.2.vapi
+++ b/vapi/libecal-1.2.vapi
@@ -53,7 +53,8 @@ namespace E {
public unowned string get_local_attachment_store ();
[CCode (finish_name = "e_cal_client_get_object_finish")]
public async void get_object (string uid, string? rid, GLib.Cancellable? cancellable, out
iCal.icalcomponent out_icalcomp) throws GLib.Error;
- public async bool get_object_list (string sexp, GLib.Cancellable? cancellable) throws
GLib.Error;
+ [CCode (finish_name = "e_cal_client_get_object_list_finish")]
+ public async bool get_object_list (string sexp, GLib.Cancellable? cancellable, out
GLib.SList<weak iCal.icalcomponent> out_icalcomps) throws GLib.Error;
public async bool get_object_list_as_comps (string sexp, GLib.Cancellable? cancellable)
throws GLib.Error;
public bool get_object_list_as_comps_sync (string sexp, GLib.SList out_ecalcomps,
GLib.Cancellable? cancellable) throws GLib.Error;
public bool get_object_list_sync (string sexp, GLib.SList out_icalcomps, GLib.Cancellable?
cancellable) throws GLib.Error;
diff --git a/vapi/libecal-1.2/libecal-1.2.metadata b/vapi/libecal-1.2/libecal-1.2.metadata
index e93a428..2577912 100644
--- a/vapi/libecal-1.2/libecal-1.2.metadata
+++ b/vapi/libecal-1.2/libecal-1.2.metadata
@@ -89,9 +89,10 @@ e_cal_client_get_object.cancellable nullable="1"
e_cal_client_get_object_finish type_name="void"
e_cal_client_get_object_finish.out_icalcomp is_out="1" transfer_ownership="1"
-e_cal_client_get_object_list async="1"
+e_cal_client_get_object_list async="1" finish_name="e_cal_client_get_object_list_finish"
e_cal_client_get_object_list.cancellable nullable="1"
-e_cal_client_get_object_list_finish.icalcomps is_out="1" value_owned="1" type_arguments="iCal.icalcomponent"
+
+e_cal_client_get_object_list_finish.out_icalcomps is_out="1" transfer_ownership="1" type_arguments="unowned
iCal.icalcomponent"
e_cal_client_get_object_list_as_comps async="1"
e_cal_client_get_object_list_as_comps.cancellable nullable="1"
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]