[california/wip/727120-webcal] First pass



commit c777dc3ef6ace2a6a0c76adf0a353f1fc825b0f9
Author: Jim Nelson <jim yorba org>
Date:   Wed Mar 26 18:05:42 2014 -0700

    First pass
    
    This gets the UI in place and some basic plumbing for the backend
    code to create new calendars and subscriptions.

 configure.ac                                       |    4 +-
 po/POTFILES.in                                     |    2 +
 src/Makefile.am                                    |    8 +
 src/application/california-application.vala        |   11 ++
 src/backing/backing-activator.vala                 |   52 ++++++
 src/backing/backing-manager.vala                   |   20 ++-
 src/backing/backing-webcal-subscribable.vala       |   31 ++++
 src/backing/backing.vala                           |    6 +-
 src/backing/eds/backing-eds-store.vala             |   63 +++++++-
 .../webcal/backing-webcal-activator-pane.vala      |   80 +++++++++
 src/backing/webcal/backing-webcal-activator.vala   |   24 +++
 src/california-resources.xml                       |    3 +
 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/app-menu.interface                          |    5 +
 src/rc/webcal-subscribe.ui                         |  185 ++++++++++++++++++++
 src/util/util-uri.vala                             |   71 ++++++++
 20 files changed, 590 insertions(+), 14 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..bb73f5a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,6 +1,7 @@
 [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/host/host-create-update-event.vala
 src/host/host-main-window.vala
@@ -10,3 +11,4 @@ src/host/host-show-event.vala
 [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..b8ee931 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -13,17 +13,22 @@ 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-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 \
        \
@@ -78,6 +83,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 \
@@ -99,6 +105,7 @@ california_RC = \
        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 +125,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..a92a8d2 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,13 @@ public class Application : Gtk.Application {
         dialog.destroy();
     }
     
+    private void on_new_calendar() {
+        Host.ModalWindow modal = new Host.ModalWindow(main_window);
+        modal.content_area.add(Backing.Manager.instance.primary_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-manager.vala b/src/backing/backing-manager.vala
index 4ce555b..8a7929f 100644
--- a/src/backing/backing-manager.vala
+++ b/src/backing/backing-manager.vala
@@ -17,7 +17,10 @@ public class Manager : BaseObject {
     
     public bool is_open { get; private set; default = false; }
     
+    public Activator primary_activator { get; private set; }
+    
     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 +45,27 @@ 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);
+        
+        if (primary_activator == null)
+            primary_activator = activator;
+    }
+    
+    /**
      * Asynchronously open the { link Manager}.
      *
      * This must be called before any other operation on the Manager (unless noted).
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..acb3c2d 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"), 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..776b3a7 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);
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..bb94266
--- /dev/null
+++ b/src/backing/webcal/backing-webcal-activator-pane.vala
@@ -0,0 +1,80 @@
+/* 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) {
+        // TODO: More thorough URL validation
+        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..41751b3 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -15,5 +15,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-create-update-event.vala b/src/host/host-create-update-event.vala
index 55606ac..6ebe994 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -262,12 +262,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/app-menu.interface b/src/rc/app-menu.interface
index 564080b..d607e13 100644
--- a/src/rc/app-menu.interface
+++ b/src/rc/app-menu.interface
@@ -3,6 +3,11 @@
     <menu id="app-menu">
         <section>
             <item>
+                <attribute name="label" translatable="yes">_Add a new calendar...</attribute>
+                <attribute name="action">app.new-calendar</attribute>
+                <attribute name="accel">&lt;Primary&gt;n</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..863a23f
--- /dev/null
+++ b/src/rc/webcal-subscribe.ui
@@ -0,0 +1,185 @@
+<?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="pane_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_bottom">8</property>
+        <property name="label" translatable="yes">Subscribe to Web Calendar</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">2</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="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">3</property>
+        <property name="width">2</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">False</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">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <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">1</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">2</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">2</property>
+        <property name="width">1</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;
+}
+
+}
+


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