[california] Subscribe to WebCal calendar: Closes bgo#727120



commit 5cda6c2d80afba3c09b060c428d17364680be812
Author: Jim Nelson <jim yorba org>
Date:   Thu Mar 27 18:49:49 2014 -0700

    Subscribe to WebCal calendar: Closes bgo#727120
    
    User can now add a WebCal calendar to California (and EDS) through
    a dialog box accessed from the AppMenu.
    
    This also fixes the issue where new calendars added to EDS (either
    via California or externally) are not displayed in the application
    (bgo#726845).

 configure.ac                                       |    4 +-
 po/POTFILES.in                                     |    4 +
 src/Makefile.am                                    |   11 ++
 src/application/california-application.vala        |   20 +++
 src/backing/backing-activator.vala                 |   52 ++++++
 .../backing-calendar-source-subscription.vala      |    1 +
 .../backing-calendar-subscription-manager.vala     |  176 ++++++++++++++++++++
 src/backing/backing-manager.vala                   |   24 +++-
 src/backing/backing-store.vala                     |    4 +-
 src/backing/backing-webcal-subscribable.vala       |   31 ++++
 src/backing/backing.vala                           |    6 +-
 src/backing/eds/backing-eds-store.vala             |   67 +++++++-
 .../webcal/backing-webcal-activator-pane.vala      |   79 +++++++++
 src/backing/webcal/backing-webcal-activator.vala   |   24 +++
 src/california-resources.xml                       |    6 +
 src/host/host-activator-list.vala                  |   58 +++++++
 src/host/host-create-update-event.vala             |    5 +-
 src/host/host-interaction.vala                     |   17 ++-
 src/host/host-modal-window.vala                    |    9 +-
 src/host/host-show-event.vala                      |    6 +-
 src/manager/manager-calendar-list.vala             |    2 +-
 src/rc/activator-list.ui                           |   88 ++++++++++
 src/rc/app-menu.interface                          |    4 +
 src/rc/webcal-subscribe.ui                         |  168 +++++++++++++++++++
 src/util/util-uri.vala                             |   71 ++++++++
 src/view/month/month-controllable.vala             |   46 ++---
 26 files changed, 937 insertions(+), 46 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 4b8777f..c16a8ab 100644
--- a/configure.ac
+++ b/configure.ac
@@ -27,13 +27,15 @@ GLIB_REQUIRED=2.38.0
 GTK_REQUIRED=3.10.7
 GEE_REQUIRED=0.10.5
 ECAL_REQUIRED=3.8.5
+LIBSOUP_REQUIRED=2.45
 
 PKG_CHECK_MODULES(CALIFORNIA, \
        glib-2.0 >= $GLIB_REQUIRED \
        gobject-2.0 >= $GLIB_REQUIRED \
        gtk+-3.0 >= $GTK_REQUIRED \
        gee-0.8 >= $GEE_REQUIRED \
-       libecal-1.2 >= $ECAL_REQUIRED
+       libecal-1.2 >= $ECAL_REQUIRED \
+       libsoup-2.4 >= $LIBSOUP_REQUIRED \
 )
 AC_SUBST(CALIFORNIA_CFLAGS)
 AC_SUBST(CALIFORNIA_LIBS)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b5cd281..a5969b5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,12 +1,16 @@
 [encoding: UTF-8]
 # List of source files which contain translatable strings.
 src/application/california-application.vala
+src/backing/backing.vala
 src/calendar/calendar.vala
+src/calendar/calendar-date.vala
 src/host/host-create-update-event.vala
 src/host/host-main-window.vala
 src/host/host-show-event.vala
+[type: gettext/glade]src/rc/activator-list.ui
 [type: gettext/glade]src/rc/app-menu.interface
 [type: gettext/glade]src/rc/calendar-manager-list.ui
 [type: gettext/glade]src/rc/calendar-manager-list-item.ui
 [type: gettext/glade]src/rc/create-update-event.ui
 [type: gettext/glade]src/rc/show-event.ui
+[type: gettext/glade]src/rc/webcal-subscribe.ui
diff --git a/src/Makefile.am b/src/Makefile.am
index f523b5a..9f3fbca 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -13,17 +13,23 @@ california_VALASOURCES = \
        application/main.vala \
        \
        backing/backing.vala \
+       backing/backing-activator.vala \
        backing/backing-calendar-source.vala \
        backing/backing-calendar-source-subscription.vala \
+       backing/backing-calendar-subscription-manager.vala \
        backing/backing-error.vala \
        backing/backing-manager.vala \
        backing/backing-source.vala \
        backing/backing-store.vala \
+       backing/backing-webcal-subscribable.vala \
        \
        backing/eds/backing-eds-calendar-source.vala \
        backing/eds/backing-eds-calendar-source-subscription.vala \
        backing/eds/backing-eds-store.vala \
        \
+       backing/webcal/backing-webcal-activator.vala \
+       backing/webcal/backing-webcal-activator-pane.vala \
+       \
        base/base-object.vala \
        base/base-unit.vala \
        \
@@ -61,6 +67,7 @@ california_VALASOURCES = \
        component/component-vtype.vala \
        \
        host/host.vala \
+       host/host-activator-list.vala \
        host/host-calendar-popup.vala \
        host/host-color-chooser-popup.vala \
        host/host-create-update-event.vala \
@@ -78,6 +85,7 @@ california_VALASOURCES = \
        util/util-gfx.vala \
        util/util-memory.vala \
        util/util-string.vala \
+       util/util-uri.vala \
        \
        view/view.vala \
        view/view-controllable.vala \
@@ -94,11 +102,13 @@ california_SOURCES = \
        $(NULL)
 
 california_RC = \
+       rc/activator-list.ui \
        rc/app-menu.interface \
        rc/calendar-manager-list.ui \
        rc/calendar-manager-list-item.ui \
        rc/create-update-event.ui \
        rc/show-event.ui \
+       rc/webcal-subscribe.ui \
        $(NULL)
 
 california_OPTIONAL_VALAFLAGS =
@@ -118,6 +128,7 @@ california_VALAFLAGS = \
        --pkg libedataserver-1.2 \
        --pkg libecal-1.2 \
        --pkg libical \
+       --pkg libsoup-2.4 \
        $(NULL)
 
 california_CFLAGS = \
diff --git a/src/application/california-application.vala b/src/application/california-application.vala
index 129b1ce..13e9924 100644
--- a/src/application/california-application.vala
+++ b/src/application/california-application.vala
@@ -29,6 +29,7 @@ public class Application : Gtk.Application {
         null
     };
     
+    public const string ACTION_NEW_CALENDAR = "app.new-calendar";
     public const string ACTION_CALENDAR_MANAGER = "app.calendar-manager";
     public const string ACTION_ABOUT = "app.about";
     public const string ACTION_QUIT = "app.quit";
@@ -41,6 +42,7 @@ public class Application : Gtk.Application {
     }
     
     private static const ActionEntry[] action_entries = {
+        { "new-calendar", on_new_calendar },
         { "calendar-manager", on_calendar_manager },
         { "about", on_about },
         { "quit", on_quit }
@@ -85,6 +87,7 @@ public class Application : Gtk.Application {
         try {
             Host.init();
             Manager.init();
+            Backing.init();
         } catch (Error err) {
             error_message(_("Unable to open California: %s").printf(err.message));
             quit();
@@ -100,6 +103,7 @@ public class Application : Gtk.Application {
         main_window = null;
         
         // unit termination
+        Backing.terminate();
         Manager.terminate();
         Host.terminate();
         
@@ -127,6 +131,22 @@ public class Application : Gtk.Application {
         dialog.destroy();
     }
     
+    private void on_new_calendar() {
+        Host.ModalWindow modal = new Host.ModalWindow(main_window);
+        Host.ActivatorList list = new Host.ActivatorList();
+        modal.content_area.add(list);
+        
+        // when a Backing.Activator is selected from the list, swap out the list for the
+        // Activator's own interaction
+        list.selected.connect(activator => {
+            modal.content_area.remove(list);
+            modal.content_area.add(activator.create_interaction(null));
+        });
+        
+        modal.run();
+        modal.destroy();
+    }
+    
     private void on_calendar_manager() {
         Manager.Window.display(main_window);
     }
diff --git a/src/backing/backing-activator.vala b/src/backing/backing-activator.vala
new file mode 100644
index 0000000..ff6c60d
--- /dev/null
+++ b/src/backing/backing-activator.vala
@@ -0,0 +1,52 @@
+/* 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.Backing {
+
+/**
+ * Locates, validates, and authorizes access to a { link Source}.
+ *
+ * Actovators are decoupled from the Backing.Source itself because it's possible for Activators
+ * to be used for multiple backings.  For example, a Google Calendar Activator can be used to
+ * locate the user's calendar information, which can then be passed on to the EDS or a GData
+ * backing.
+ */
+
+public abstract class Activator : BaseObject {
+    public const string PROP_TITLE = "title";
+    public const string PROP_STORE = "store";
+    
+    /**
+     * The user-visible title of this { link Activator} indicating what service or type of service
+     * it can prepare a subscription for.
+     */
+    public string title { get; private set; }
+    
+    /**
+     * The { link Store} this { link Activator} will create the new { link Source} in.
+     *
+     * It's up to the subclass to determine which Stores will work with its information.
+     */
+    public Store store { get; private set; }
+    
+    protected Activator(string title, Store store) {
+        this.title = title;
+        this.store = store;
+    }
+    
+    /**
+     * Return a { link Host.Interaction} that guides the user through the steps to create a
+     * { link Source}.
+     */
+    public abstract Host.Interaction create_interaction(Soup.URI? supplied_uri);
+    
+    public override string to_string() {
+        return title;
+    }
+}
+
+}
+
diff --git a/src/backing/backing-calendar-source-subscription.vala 
b/src/backing/backing-calendar-source-subscription.vala
index 90245d5..b54fea9 100644
--- a/src/backing/backing-calendar-source-subscription.vala
+++ b/src/backing/backing-calendar-source-subscription.vala
@@ -255,6 +255,7 @@ public abstract class CalendarSourceSubscription : BaseObject {
         
         // Use to_array() so no iteration troubles when notify_instance_dropped removes it from
         // 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);
     }
diff --git a/src/backing/backing-calendar-subscription-manager.vala 
b/src/backing/backing-calendar-subscription-manager.vala
new file mode 100644
index 0000000..c4851ef
--- /dev/null
+++ b/src/backing/backing-calendar-subscription-manager.vala
@@ -0,0 +1,176 @@
+/* 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.Backing {
+
+/**
+ * Subscribe to all { link CalendarSource}s and their { link Component.Instance}s for a specific
+ * span of time.
+ *
+ * This class manages the signals and { link CalendarSourceSubscription}s for all registered
+ * calendars and converts their important events into a set of symmetric signals.  It also
+ * automatically subscribes to new calendars (and unsubscribes to dropped ones) notifying of changes
+ * through the same set of signals  Callers should simply subscribe to those signals and let them
+ * drive the application.
+ *
+ * The time span { link window} cannot be altered once the object is created.
+ */
+
+public class CalendarSubscriptionManager : BaseObject {
+    /**
+     * The time span for all managed subscriptions.
+     */
+    public Calendar.ExactTimeSpan window { get; private set; }
+    
+    /**
+     * Indicates a { link CalendarSource} was added to the manager, either listed when first
+     * created or detected at runtime afterwards.
+     */
+    public signal void calendar_added(Backing.CalendarSource calendar);
+    
+    /**
+     * Indicates the { link CalendarSource} was removed from the manager.
+     */
+    public signal void calendar_removed(Backing.CalendarSource calendar);
+    
+    /**
+     * Indicates the { link Component.Instance} was generated by one of the managed subscriptions,
+     * either generated (discovered) when first opened or added later.
+     */
+    public signal void instance_added(Component.Instance instance);
+    
+    /**
+     * Indicates the { link Component.Instance} was removed by one of the managed subscriptions,
+     * either due to the { link CalendarSource} being made unavailable or removal by the user.
+     */
+    public signal void instance_removed(Component.Instance instance);
+    
+    /**
+     * An error was returned when attempting to subscribe to the { link CalendarSource}.
+     */
+    public signal void subscription_error(Backing.CalendarSource calendar, Error err);
+    
+    private Gee.ArrayList<Backing.CalendarSourceSubscription> subscriptions = new Gee.ArrayList<
+        Backing.CalendarSourceSubscription>();
+    private Cancellable cancellable = new Cancellable();
+    
+    /**
+     * Create a new { link CalendarSubscriptionManager}.
+     *
+     * The { link window} cannot be modified once created.
+     *
+     * Events will not be signalled until { link start} is called.
+     */
+    public CalendarSubscriptionManager(Calendar.ExactTimeSpan window) {
+        this.window = window;
+    }
+    
+    ~CalendarSubscriptionManager() {
+        // cancel any outstanding subscription starts
+        cancellable.cancel();
+        
+        // drop signals on objects that will persist after this object's destruction
+        foreach (Backing.Store store in Backing.Manager.instance.get_stores()) {
+            store.source_added.disconnect(on_source_added);
+            store.source_removed.disconnect(on_source_removed);
+        }
+    }
+    
+    /**
+     * Generate subscriptions and begin firing signals.
+     *
+     * There is no "stop" method.  Destroying the object will cancel all subscriptions, although
+     * signals will not be fired at that time.
+     */
+    public void start() {
+        foreach (Backing.Store store in Backing.Manager.instance.get_stores()) {
+            // watch each store for future added sources
+            store.source_added.connect(on_source_added);
+            store.source_removed.connect(on_source_removed);
+            
+            foreach (Backing.Source source in store.get_sources_of_type<Backing.CalendarSource>())
+                add_calendar((Backing.CalendarSource) source);
+        }
+    }
+    
+    private void on_source_added(Backing.Source source) {
+        Backing.CalendarSource? calendar = source as Backing.CalendarSource;
+        if (calendar != null)
+            add_calendar(calendar);
+    }
+    
+    private void add_calendar(Backing.CalendarSource calendar) {
+        // report calendar as added to subscription
+        calendar_added(calendar);
+        
+        // start generating instances on this calendar
+        calendar.subscribe_async.begin(window, cancellable, on_subscribed);
+    }
+    
+    // Since this might be called after the dtor has finished (cancelling the operation), don't
+    // touch the "this" ref unless the Error is shown not to be a cancellation
+    private void on_subscribed(Object? source, AsyncResult result) {
+        Backing.CalendarSource calendar = (Backing.CalendarSource) source;
+        
+        try {
+            Backing.CalendarSourceSubscription subscription = calendar.subscribe_async.end(result);
+            
+            // okay to use "this" ref
+            subscriptions.add(subscription);
+            
+            subscription.instance_discovered.connect(on_instance_added);
+            subscription.instance_added.connect(on_instance_added);
+            subscription.instance_removed.connect(on_instance_removed);
+            subscription.instance_dropped.connect(on_instance_removed);
+            subscription.start_failed.connect(on_error);
+            
+            // this will start signals firing for event changes
+            subscription.start();
+        } catch (Error err) {
+            debug("Unable to subscribe to %s: %s", calendar.to_string(), err.message);
+            
+            // only fire -- or even touch "this" -- if not a cancellation
+            if (!(err is IOError.CANCELLED))
+                subscription_error(calendar, err);
+        }
+    }
+    
+    private void on_instance_added(Component.Instance instance) {
+        instance_added(instance);
+    }
+    
+    private void on_instance_removed(Component.Instance instance) {
+        instance_removed(instance);
+    }
+    
+    private void on_error(CalendarSourceSubscription subscription, Error err) {
+        subscription_error(subscription.calendar, err);
+    }
+    
+    // Don't need to do much here as all instances are dropped prior to the source being removed
+    private void on_source_removed(Backing.Source source) {
+        Backing.CalendarSource? calendar = source as Backing.CalendarSource;
+        if (calendar == null)
+            return;
+        
+        // drop all related subscriptions ... their instances should've been dropped via the
+        // "instance-dropped" signal, so no signal their removal here
+        Gee.Iterator<CalendarSourceSubscription> iter = subscriptions.iterator();
+        while (iter.next()) {
+            if (iter.get().calendar == calendar)
+                iter.remove();
+        }
+        
+        calendar_removed(calendar);
+    }
+    
+    public override string to_string() {
+        return "%s window=%s".printf(get_class().get_type().name(), window.to_string());
+    }
+}
+
+}
+
diff --git a/src/backing/backing-manager.vala b/src/backing/backing-manager.vala
index 4ce555b..b54a3be 100644
--- a/src/backing/backing-manager.vala
+++ b/src/backing/backing-manager.vala
@@ -18,6 +18,7 @@ public class Manager : BaseObject {
     public bool is_open { get; private set; default = false; }
     
     private Gee.List<Store> stores = new Gee.ArrayList<Store>();
+    private Gee.List<Activator> activators = new Gee.ArrayList<Activator>();
     
     /**
      * Fired when a { link Store} cannot be opened.
@@ -42,12 +43,24 @@ public class Manager : BaseObject {
      *
      * TODO: A plugin system may make sense here.
      */
-    internal void register(Store store) {
+    internal void register_store(Store store) {
         if (!stores.contains(store))
             stores.add(store);
     }
     
     /**
+     * The various { link Activators} are registered in { link Backing.init}.
+     *
+     * This *must* be called prior to { link open_async} for them to be opened properly.
+     *
+     * TODO: A plugin system may make sense here.
+     */
+    internal void register_activator(Activator activator) {
+        if (!activators.contains(activator))
+            activators.add(activator);
+    }
+    
+    /**
      * Asynchronously open the { link Manager}.
      *
      * This must be called before any other operation on the Manager (unless noted).
@@ -103,6 +116,15 @@ public class Manager : BaseObject {
     }
     
     /**
+     * Returns a read-only list of all available { link Activator}s.
+     *
+     * Must only be called wheil the { link Manager} is open.
+     */
+    public Gee.List<Activator> get_activators() {
+        return activators.read_only_view;
+    }
+    
+    /**
      * Returns a read-only list of all available { link Store}s.
      *
      * Must only be called while the { link Manager} is open.
diff --git a/src/backing/backing-store.vala b/src/backing/backing-store.vala
index 46bfb29..527b0a2 100644
--- a/src/backing/backing-store.vala
+++ b/src/backing/backing-store.vala
@@ -29,7 +29,7 @@ public abstract class Store : BaseObject {
      *
      * Also fired in { link open_async} when Sources are discovered.
      */
-    public virtual signal void added(Source source) {
+    public virtual signal void source_added(Source source) {
         debug("%s: added %s", to_string(), source.to_string());
     }
     
@@ -40,7 +40,7 @@ public abstract class Store : BaseObject {
      *
      * Also called in { link close_async} when internal refs are being dropped.
      */
-    public virtual signal void removed(Source source) {
+    public virtual signal void source_removed(Source source) {
         debug("%s: removed %s", to_string(), source.to_string());
     }
     
diff --git a/src/backing/backing-webcal-subscribable.vala b/src/backing/backing-webcal-subscribable.vala
new file mode 100644
index 0000000..970d119
--- /dev/null
+++ b/src/backing/backing-webcal-subscribable.vala
@@ -0,0 +1,31 @@
+/* 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.Backing {
+
+/**
+ * Interface allowing for a { link Store} to subscribe to WebCal calendars.
+ *
+ * See [[https://web.archive.org/web/20071222113039/http://larry.cannell.org/webcal]] for more
+ * information about WebCal.
+ */
+
+public interface WebCalSubscribable : Store {
+    /**
+     * Subscribe to a WebCal link, creating a new { link CalendarSource} in the process.
+     *
+     * "title" is the display name of the new subscription and should probably be supplied by the
+     * user.
+     *
+     * The CalendarSource is not returned; rather, callers should be subscribed to the
+     * { link Store.added} signal for notification.
+     */
+    public abstract async void subscribe_webcal_async(string title, Soup.URI uri,
+        string color, Cancellable? cancellable) throws Error;
+}
+
+}
+
diff --git a/src/backing/backing.vala b/src/backing/backing.vala
index d5c5e9d..14f833e 100644
--- a/src/backing/backing.vala
+++ b/src/backing/backing.vala
@@ -39,8 +39,10 @@ public void init() throws Error {
     // internal class init
     Manager.init();
     
-    // Register all Stores here
-    Manager.instance.register(new EdsStore());
+    // Register all Stores and Activators here
+    EdsStore eds_store = new EdsStore();
+    Manager.instance.register_store(eds_store);
+    Manager.instance.register_activator(new WebCalActivator(_("Web calendar (.ics)"), eds_store));
     
     // open Manager, pumping event loop until it completes (possibly w/ error)
     Manager.instance.open_async.begin(null, on_backing_manager_opened);
diff --git a/src/backing/eds/backing-eds-store.vala b/src/backing/eds/backing-eds-store.vala
index 79c7173..474bfc4 100644
--- a/src/backing/eds/backing-eds-store.vala
+++ b/src/backing/eds/backing-eds-store.vala
@@ -10,7 +10,7 @@ namespace California.Backing {
  * An interface to the EDS source registry.
  */
 
-internal class EdsStore : Store {
+internal class EdsStore : Store, WebCalSubscribable {
     private E.SourceRegistry? registry = null;
     private Gee.HashMap<E.Source, Source> sources = new Gee.HashMap<E.Source, Source>();
     
@@ -41,6 +41,67 @@ internal class EdsStore : Store {
         is_open = false;
     }
     
+    /**
+     * @inheritDoc
+     *
+     * TODO: Authentication is not properly handled.
+     */
+    public async void subscribe_webcal_async(string title, Soup.URI uri, string color,
+        Cancellable? cancellable) throws Error {
+        if (!is_open)
+            throw new BackingError.UNAVAILABLE("EDS not open");
+        
+        E.Source scratch = new E.Source(null, null);
+        scratch.parent = "webcal-stub";
+        scratch.enabled = true;
+        scratch.display_name = title;
+        
+        // required
+        E.SourceCalendar? calendar = scratch.get_extension(E.SOURCE_EXTENSION_CALENDAR)
+            as E.SourceCalendar;
+        if (calendar == null)
+            throw new BackingError.UNAVAILABLE("No SourceCalendar extension for scratch source");
+        calendar.backend_name = "webcal";
+        calendar.selected = true;
+        calendar.color = color;
+        
+        // required
+        E.SourceWebdav? webdav = scratch.get_extension(E.SOURCE_EXTENSION_WEBDAV_BACKEND)
+            as E.SourceWebdav;
+        if (webdav == null)
+            throw new BackingError.UNAVAILABLE("No SourceWebdav extension for scratch source");
+        webdav.resource_path = uri.path;
+        webdav.resource_query = uri.query;
+        
+        // required
+        E.SourceAuthentication? auth = scratch.get_extension(E.SOURCE_EXTENSION_AUTHENTICATION)
+            as E.SourceAuthentication;
+        if (auth == null)
+            throw new BackingError.UNAVAILABLE("No SourceAuthentication extension for scratch source");
+        auth.host = uri.host;
+        auth.port = uri.port;
+        auth.method = "none";
+        
+        // optional w/ baked-in defaults
+        E.SourceOffline? offline = scratch.get_extension(E.SOURCE_EXTENSION_OFFLINE)
+            as E.SourceOffline;
+        if (offline != null)
+            offline.stay_synchronized = true;
+        
+        // optional w/ baked-in defaults
+        E.SourceRefresh? refresh = scratch.get_extension(E.SOURCE_EXTENSION_REFRESH)
+            as E.SourceRefresh;
+        if (refresh != null) {
+            refresh.enabled = true;
+            refresh.interval_minutes = 1;
+        }
+        
+        List<E.Source> sources = new List<E.Source>();
+        sources.append(scratch);
+        // TODO: Properly bind async version of this call
+        registry.create_sources_sync(sources, cancellable);
+    }
+    
     public override Gee.List<Source> get_sources() {
         Gee.List<Source> list = new Gee.ArrayList<Source>();
         list.add_all(sources.values.read_only_view);
@@ -69,7 +130,7 @@ internal class EdsStore : Store {
         
         sources.set(eds_source, calendar);
         
-        added(calendar);
+        source_added(calendar);
     }
     
     // since the registry ref may have been dropped (in close_async), it shouldn't be ref'd here
@@ -79,8 +140,8 @@ internal class EdsStore : Store {
         if (sources.unset(eds_source, out source)) {
             assert(source != null);
             
-            removed(source);
             source.set_unavailable();
+            source_removed(source);
         }
         
         // close in background
diff --git a/src/backing/webcal/backing-webcal-activator-pane.vala 
b/src/backing/webcal/backing-webcal-activator-pane.vala
new file mode 100644
index 0000000..e942344
--- /dev/null
+++ b/src/backing/webcal/backing-webcal-activator-pane.vala
@@ -0,0 +1,79 @@
+/* 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.Backing {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/webcal-subscribe.ui")]
+internal class WebCalActivatorPane : Gtk.Grid, Host.Interaction {
+    public Gtk.Widget? default_widget { get { return subscribe_button; } }
+    
+    [GtkChild]
+    private Gtk.ColorButton color_button;
+    
+    [GtkChild]
+    private Gtk.Entry name_entry;
+    
+    [GtkChild]
+    private Gtk.Entry url_entry;
+    
+    [GtkChild]
+    private Gtk.Button subscribe_button;
+    
+    private WebCalSubscribable store;
+    
+    public WebCalActivatorPane(WebCalSubscribable store, Soup.URI? supplied_url) {
+        this.store = store;
+        
+        if (supplied_url != null) {
+            url_entry.text = supplied_url.to_string(false);
+            url_entry.sensitive = false;
+        }
+        
+        name_entry.bind_property("text-length", subscribe_button, "sensitive",
+            BindingFlags.SYNC_CREATE, on_entry_changed);
+        url_entry.bind_property("text-length", subscribe_button, "sensitive",
+            BindingFlags.SYNC_CREATE, on_entry_changed);
+    }
+    
+    private bool on_entry_changed(Binding binding, Value source_value, ref Value target_value) {
+        target_value =
+            name_entry.text_length > 0 
+            && url_entry.text_length > 0
+            && URI.is_valid(url_entry.text, { "http://";, "https://";, "webcal://" });
+        
+        return true;
+    }
+    
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        dismissed(true);
+    }
+    
+    [GtkCallback]
+    private void on_subscribe_button_clicked() {
+        sensitive = false;
+        
+        subscribe_async.begin();
+    }
+    
+    private async void subscribe_async() {
+        Gdk.Color color;
+        color_button.get_color(out color);
+        
+        try {
+            yield store.subscribe_webcal_async(name_entry.text, URI.parse(url_entry.text),
+                Gfx.rgb_to_uint8_rgb_string(color), null);
+            completed();
+        } catch (Error err) {
+            debug("Unable to create subscription to %s: %s", url_entry.text, err.message);
+        }
+        
+        dismissed(true);
+    }
+}
+
+}
+
diff --git a/src/backing/webcal/backing-webcal-activator.vala 
b/src/backing/webcal/backing-webcal-activator.vala
new file mode 100644
index 0000000..8423c88
--- /dev/null
+++ b/src/backing/webcal/backing-webcal-activator.vala
@@ -0,0 +1,24 @@
+/* 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.Backing {
+
+internal class WebCalActivator : Activator {
+    private WebCalSubscribable webcal_store;
+    
+    public WebCalActivator(string title, WebCalSubscribable store) {
+        base (title, store);
+        
+        webcal_store = store;
+    }
+    
+    public override Host.Interaction create_interaction(Soup.URI? supplied_uri) {
+        return new WebCalActivatorPane(webcal_store, supplied_uri);
+    }
+}
+
+}
+
diff --git a/src/california-resources.xml b/src/california-resources.xml
index 3a514fb..9635cc3 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -1,6 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
     <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/activator-list.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
         <file compressed="true">rc/app-menu.interface</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
@@ -15,5 +18,8 @@
     <gresource prefix="/org/yorba/california">
         <file compressed="false">rc/show-event.ui</file>
     </gresource>
+    <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/webcal-subscribe.ui</file>
+    </gresource>
 </gresources>
 
diff --git a/src/host/host-activator-list.vala b/src/host/host-activator-list.vala
new file mode 100644
index 0000000..85ed143
--- /dev/null
+++ b/src/host/host-activator-list.vala
@@ -0,0 +1,58 @@
+/* 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/activator-list.ui")]
+public class ActivatorList : Gtk.Grid, Host.Interaction {
+    private class ActivatorListItem : Gtk.Label {
+        public Backing.Activator activator;
+        
+        public ActivatorListItem(Backing.Activator activator) {
+            this.activator = activator;
+            
+            label = activator.title;
+            xalign = 0.0f;
+            margin = 4;
+        }
+    }
+    
+    public Gtk.Widget? default_widget { get { return add_button; } }
+    
+    [GtkChild]
+    private Gtk.ListBox listbox;
+    
+    [GtkChild]
+    private Gtk.Button add_button;
+    
+    public signal void selected(Backing.Activator activator);
+    
+    public ActivatorList() {
+        foreach (Backing.Activator activator in Backing.Manager.instance.get_activators())
+            listbox.add(new ActivatorListItem(activator));
+        
+        show_all();
+    }
+    
+    [GtkCallback]
+    private void on_listbox_row_activated(Gtk.ListBoxRow? row) {
+        if (row != null)
+            selected(((ActivatorListItem) row.get_child()).activator);
+    }
+    
+    [GtkCallback]
+    private void on_add_button_clicked() {
+        on_listbox_row_activated(listbox.get_selected_row());
+    }
+    
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        dismissed(true);
+    }
+}
+
+}
+
diff --git a/src/host/host-create-update-event.vala b/src/host/host-create-update-event.vala
index 59a4968..5efd9f8 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -269,12 +269,13 @@ public class CreateUpdateEvent : Gtk.Grid, Interaction {
         else
             create_event(event);
         
-        dismissed();
+        completed();
+        dismissed(true);
     }
     
     [GtkCallback]
     private void on_cancel_button_clicked() {
-        dismissed();
+        dismissed(true);
     }
 }
 
diff --git a/src/host/host-interaction.vala b/src/host/host-interaction.vala
index afebc36..efef7ce 100644
--- a/src/host/host-interaction.vala
+++ b/src/host/host-interaction.vala
@@ -27,12 +27,25 @@ public interface Interaction : Gtk.Widget {
     public abstract Gtk.Widget? default_widget { get; }
     
     /**
-     * Fired when the user has cancelled, closed, or dismissed the { link Interaction}.
+     * Fired when the interaction is cancelled, closed, or dismissed the { link Interaction},
+     * whether due to programmatic reasons or by user request.
      *
      * This should be called by implementing classes even if other signals suggest or imply that
      * the Interaction is dismissed, so a single signal handler can deal with cleanup.
      */
-    public signal void dismissed();
+    public signal void dismissed(bool user_request);
+    
+    /**
+     * Fired when the { link Interaction} has completed successfully.
+     *
+     * This should only be fired if the Interaction requires valid input from the user to perform
+     * some intensive operation.  Merely displaying information and closing the Interaction
+     * should simply fire { link dismissed}.
+     *
+     * "completed" implies that dismissed will be called shortly thereafter, meaning all
+     * cleanup can be handled there.
+     */
+    public signal void completed();
 }
 
 }
diff --git a/src/host/host-modal-window.vala b/src/host/host-modal-window.vala
index 45992d5..96d9e9c 100644
--- a/src/host/host-modal-window.vala
+++ b/src/host/host-modal-window.vala
@@ -20,6 +20,7 @@ public class ModalWindow : Gtk.Dialog {
     public Gtk.Box content_area { get; private set; }
     
     private Interaction? primary = null;
+    private Gtk.ResponseType response_type = Gtk.ResponseType.CLOSE;
     
     public ModalWindow(Gtk.Window? parent) {
         transient_for = parent;
@@ -41,6 +42,7 @@ public class ModalWindow : Gtk.Dialog {
             if (primary == null)
                 primary = interaction;
             
+            interaction.completed.connect(on_interaction_completed);
             interaction.dismissed.connect(on_interaction_dismissed);
         }
     }
@@ -51,12 +53,17 @@ public class ModalWindow : Gtk.Dialog {
             if (primary == interaction)
                 primary = null;
             
+            interaction.completed.disconnect(on_interaction_completed);
             interaction.dismissed.disconnect(on_interaction_dismissed);
         }
     }
     
+    private void on_interaction_completed() {
+        response_type = Gtk.ResponseType.OK;
+    }
+    
     private void on_interaction_dismissed() {
-        response(Gtk.ResponseType.CLOSE);
+        response(response_type);
     }
     
     public override void show() {
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 60ac876..af612c1 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -127,18 +127,18 @@ public class ShowEvent : Gtk.Grid, Interaction {
     [GtkCallback]
     private void on_remove_button_clicked() {
         remove_event(event);
-        dismissed();
+        dismissed(true);
     }
     
     [GtkCallback]
     private void on_update_button_clicked() {
         update_event(event);
-        dismissed();
+        dismissed(true);
     }
     
     [GtkCallback]
     private void on_close_button_clicked() {
-        dismissed();
+        dismissed(true);
     }
 }
 
diff --git a/src/manager/manager-calendar-list.vala b/src/manager/manager-calendar-list.vala
index 61ccf47..4f396a9 100644
--- a/src/manager/manager-calendar-list.vala
+++ b/src/manager/manager-calendar-list.vala
@@ -61,7 +61,7 @@ public class CalendarList : Gtk.Grid, Host.Interaction {
     
     [GtkCallback]
     private void on_close_button_clicked() {
-        dismissed();
+        dismissed(true);
     }
 }
 
diff --git a/src/rc/activator-list.ui b/src/rc/activator-list.ui
new file mode 100644
index 0000000..bb76ada
--- /dev/null
+++ b/src/rc/activator-list.ui
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaHostActivatorList" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="row_spacing">8</property>
+    <child>
+      <object class="GtkViewport" id="viewport">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GtkListBox" id="listbox">
+            <property name="width_request">300</property>
+            <property name="height_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <property name="activate_on_single_click">False</property>
+            <signal name="row-activated" handler="on_listbox_row_activated" 
object="CaliforniaHostActivatorList" swapped="no"/>
+          </object>
+        </child>
+      </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="GtkButtonBox" id="button_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="spacing">8</property>
+        <property name="homogeneous">True</property>
+        <property name="baseline_position">bottom</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="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <signal name="clicked" handler="on_cancel_button_clicked" object="CaliforniaHostActivatorList" 
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="add_button">
+            <property name="label" translatable="yes">_Add</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_add_button_clicked" object="CaliforniaHostActivatorList" 
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">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/rc/app-menu.interface b/src/rc/app-menu.interface
index 564080b..c71aa40 100644
--- a/src/rc/app-menu.interface
+++ b/src/rc/app-menu.interface
@@ -3,6 +3,10 @@
     <menu id="app-menu">
         <section>
             <item>
+                <attribute name="label" translatable="yes">_Add calendar...</attribute>
+                <attribute name="action">app.new-calendar</attribute>
+            </item>
+            <item>
                 <attribute name="label" translatable="yes">_Calendars</attribute>
                 <attribute name="action">app.calendar-manager</attribute>
                 <attribute name="accel">&lt;Primary&gt;l</attribute>
diff --git a/src/rc/webcal-subscribe.ui b/src/rc/webcal-subscribe.ui
new file mode 100644
index 0000000..5d1692b
--- /dev/null
+++ b/src/rc/webcal-subscribe.ui
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaBackingWebCalActivatorPane" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="row_spacing">8</property>
+    <property name="column_spacing">8</property>
+    <child>
+      <object class="GtkLabel" id="name_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">_Name:</property>
+        <property name="use_underline">True</property>
+        <property name="ellipsize">start</property>
+      </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="url_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="margin_right">8</property>
+        <property name="hexpand">False</property>
+        <property name="vexpand">False</property>
+        <property name="label" translatable="yes">_URL:</property>
+        <property name="use_underline">True</property>
+        <property name="ellipsize">end</property>
+      </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="GtkBox" id="box2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">8</property>
+        <child>
+          <object class="GtkEntry" id="name_entry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="activates_default">True</property>
+            <property name="width_chars">40</property>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkColorButton" id="color_button">
+            <property name="width_request">16</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="halign">end</property>
+            <property name="valign">end</property>
+            <property name="title" translatable="yes">Select a color for the Web calendar</property>
+            <property name="rgba">rgb(32,32,204)</property>
+          </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">0</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="box3">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkEntry" id="url_entry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hexpand">True</property>
+            <property name="activates_default">True</property>
+            <property name="width_chars">40</property>
+            <property name="input_purpose">url</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>
+    <child>
+      <object class="GtkButtonBox" id="button_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">end</property>
+        <property name="margin_top">8</property>
+        <property name="spacing">8</property>
+        <property name="homogeneous">True</property>
+        <property name="baseline_position">bottom</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="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <signal name="clicked" handler="on_cancel_button_clicked" 
object="CaliforniaBackingWebCalActivatorPane" 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="subscribe_button">
+            <property name="label" translatable="yes">_Subscribe</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_subscribe_button_clicked" 
object="CaliforniaBackingWebCalActivatorPane" 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">2</property>
+        <property name="width">2</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/util/util-uri.vala b/src/util/util-uri.vala
new file mode 100644
index 0000000..3f9b004
--- /dev/null
+++ b/src/util/util-uri.vala
@@ -0,0 +1,71 @@
+/* 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.
+ */
+
+errordomain URIError {
+    INVALID
+}
+
+/**
+ * A collection of URI-related methods.
+ */
+
+namespace California.URI {
+
+/**
+ * Basic validation of a string intended to be parsed as an absolute URI.
+ *
+ * If null or an empty array is passed for "supported_schemes", then the only character checked
+ * for is the presence of a colon separating the scheme from the remainder of the URI.
+ *
+ * If "supported_schemes" are specified, then the entire scheme (name and separator) should be
+ * included, i.e. "http://";, "mailto:";, etc.
+ */
+public bool is_valid(string? uri, string[]? supported_schemes) {
+    // strip leading and trailing whitespace
+    string? stripped = (uri != null) ? uri.strip() : null;
+    if (String.is_empty(stripped))
+        return false;
+    
+    if (supported_schemes == null || supported_schemes.length == 0) {
+        if (!stripped.contains(":"))
+            return false;
+    } else {
+        bool found = false;
+        foreach (string scheme in supported_schemes) {
+            if (stripped.has_prefix(scheme)) {
+                found = true;
+                
+                break;
+            }
+        }
+        
+        if (!found)
+            return false;
+    }
+    
+    // finally, let Soup.URI decide
+    Soup.URI? parsed = new Soup.URI(uri);
+    
+    return parsed != null;
+}
+
+/**
+ * Checked creation of a Soup.URI object.
+ *
+ * This shouldn't be used to create "empty" Soup.URI objects.
+ *
+ * @throws URIError
+ */
+public Soup.URI parse(string uri) throws Error {
+    Soup.URI? parsed = new Soup.URI(uri);
+    if (parsed == null)
+        throw new URIError.INVALID("Invalid URI: %s", uri);
+    
+    return parsed;
+}
+
+}
+
diff --git a/src/view/month/month-controllable.vala b/src/view/month/month-controllable.vala
index 6528522..3707df8 100644
--- a/src/view/month/month-controllable.vala
+++ b/src/view/month/month-controllable.vala
@@ -65,8 +65,7 @@ public class Controllable : Gtk.Grid, View.Controllable {
     public Calendar.Date default_date { get; protected set; }
     
     private Gee.HashMap<Calendar.Date, Cell> date_to_cell = new Gee.HashMap<Calendar.Date, Cell>();
-    private Gee.ArrayList<Backing.CalendarSourceSubscription> subscriptions = new Gee.ArrayList<
-        Backing.CalendarSourceSubscription>();
+    private Backing.CalendarSubscriptionManager? subscriptions = null;
     private Gdk.EventType button_press_type = Gdk.EventType.NOTHING;
     private Gdk.Point button_press_point = Gdk.Point();
     
@@ -275,34 +274,25 @@ public class Controllable : Gtk.Grid, View.Controllable {
         Calendar.ExactTimeSpan time_window = new Calendar.ExactTimeSpan.from_date_span(window,
             Calendar.Timezone.local);
         
-        // clear current subscriptions and generate new subscriptions for new window
-        subscriptions.clear();
-        foreach (Backing.Store store in Backing.Manager.instance.get_stores()) {
-            foreach (Backing.Source source in store.get_sources_of_type<Backing.CalendarSource>()) {
-                Backing.CalendarSource calendar = (Backing.CalendarSource) source;
-                calendar.notify[Backing.Source.PROP_VISIBLE].connect(queue_draw);
-                calendar.subscribe_async.begin(time_window, null, on_subscribed);
-            }
-        }
+        // create new subscription manager, subscribe to its signals, and let them drive
+        subscriptions = null;
+        subscriptions = new Backing.CalendarSubscriptionManager(time_window);
+        subscriptions.calendar_added.connect(on_calendar_added);
+        subscriptions.calendar_removed.connect(on_calendar_removed);
+        subscriptions.instance_added.connect(on_instance_added);
+        subscriptions.instance_removed.connect(on_instance_removed);
+        
+        subscriptions.start();
     }
     
-    private void on_subscribed(Object? source, AsyncResult result) {
-        Backing.CalendarSource calendar = (Backing.CalendarSource) source;
-        
-        try {
-            Backing.CalendarSourceSubscription subscription = calendar.subscribe_async.end(result);
-            subscriptions.add(subscription);
-            
-            subscription.instance_discovered.connect(on_instance_added);
-            subscription.instance_added.connect(on_instance_added);
-            subscription.instance_removed.connect(on_instance_removed);
-            subscription.instance_dropped.connect(on_instance_removed);
-            
-            // this will start signals firing for event changes
-            subscription.start();
-        } catch (Error err) {
-            debug("Unable to subscribe to %s: %s", calendar.to_string(), err.message);
-        }
+    private void on_calendar_added(Backing.CalendarSource calendar) {
+        calendar.notify[Backing.Source.PROP_VISIBLE].connect(queue_draw);
+        calendar.notify[Backing.Source.PROP_COLOR].connect(queue_draw);
+    }
+    
+    private void on_calendar_removed(Backing.CalendarSource calendar) {
+        calendar.notify[Backing.Source.PROP_VISIBLE].disconnect(queue_draw);
+        calendar.notify[Backing.Source.PROP_COLOR].disconnect(queue_draw);
     }
     
     private void on_instance_added(Component.Instance instance) {


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