[california] Subscribe to WebCal calendar: Closes bgo#727120
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Subscribe to WebCal calendar: Closes bgo#727120
- Date: Fri, 28 Mar 2014 01:51:46 +0000 (UTC)
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"><Primary>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]