[california/wip/725763-google] Google subscribe steps work all the way to actually subscribing



commit 328a392ae04251f022ad001a690bf770c8df5c18
Author: Jim Nelson <jim yorba org>
Date:   Wed Apr 2 19:33:49 2014 -0700

    Google subscribe steps work all the way to actually subscribing
    
    Some problem with constructing the URI, but seems close.

 configure.ac                                       |    4 +
 po/POTFILES.in                                     |    5 +
 src/Makefile.am                                    |   12 ++
 src/activator/activator-window.vala                |    5 +-
 src/activator/activator.vala                       |    1 +
 .../activator-google-authenticating-pane.vala      |  150 ++++++++++++++++
 .../activator-google-calendar-list-pane.vala       |  178 ++++++++++++++++++++
 .../google/activator-google-login-pane.vala        |   56 ++++++
 src/activator/google/activator-google.vala         |   31 ++++
 src/backing/eds/backing-eds-store.vala             |    2 +-
 src/california-resources.xml                       |    9 +
 src/rc/google-authenticating.ui                    |   95 +++++++++++
 src/rc/google-calendar-list.ui                     |  162 ++++++++++++++++++
 src/rc/google-login.ui                             |  133 +++++++++++++++
 src/util/util-deck.vala                            |  132 +++++++++++++++
 src/util/util-listbox-model.vala                   |  113 +++++++++++++
 16 files changed, 1086 insertions(+), 2 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index b5ce289..dcdd335 100644
--- a/configure.ac
+++ b/configure.ac
@@ -29,6 +29,8 @@ GTK_REQUIRED=3.10.7
 GEE_REQUIRED=0.10.5
 ECAL_REQUIRED=3.8.5
 LIBSOUP_REQUIRED=2.44
+GDATA_REQUIRED=0.14.0
+GOA_REQUIRED=3.8.3
 
 PKG_CHECK_MODULES(CALIFORNIA, \
        glib-2.0 >= $GLIB_REQUIRED \
@@ -37,6 +39,8 @@ PKG_CHECK_MODULES(CALIFORNIA, \
        gee-0.8 >= $GEE_REQUIRED \
        libecal-1.2 >= $ECAL_REQUIRED \
        libsoup-2.4 >= $LIBSOUP_REQUIRED \
+       libgdata >= $GDATA_REQUIRED \
+       goa-1.0 >= $GOA_REQUIRED \
 )
 AC_SUBST(CALIFORNIA_CFLAGS)
 AC_SUBST(CALIFORNIA_LIBS)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 7fbe95b..933329c 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,6 +1,8 @@
 [encoding: UTF-8]
 # List of source files which contain translatable strings.
 src/activator/activator.vala
+src/activator/google/google-authenticating-pane.vala
+src/activator/google/google-calendar-list-pane.vala
 src/application/california-application.vala
 src/calendar/calendar.vala
 src/calendar/calendar-date.vala
@@ -12,5 +14,8 @@ src/host/host-show-event.vala
 [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/google-authenticating.ui
+[type: gettext/glade]src/rc/google-calendar-list.ui
+[type: gettext/glade]src/rc/google-login.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 fad4e73..45b4967 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -13,6 +13,11 @@ california_VALASOURCES = \
        activator/activator-instance-list.vala \
        activator/activator-window.vala \
        \
+       activator/google/activator-google.vala \
+       activator/google/activator-google-authenticating-pane.vala \
+       activator/google/activator-google-calendar-list-pane.vala \
+       activator/google/activator-google-login-pane.vala \
+       \
        activator/webcal/activator-webcal.vala \
        activator/webcal/activator-webcal-pane.vala \
        \
@@ -85,7 +90,9 @@ california_VALASOURCES = \
        manager/manager-calendar-list-item.vala \
        manager/manager-window.vala \
        \
+       util/util-deck.vala \
        util/util-gfx.vala \
+       util/util-listbox-model.vala \
        util/util-memory.vala \
        util/util-string.vala \
        util/util-uri.vala \
@@ -110,6 +117,9 @@ california_RC = \
        rc/calendar-manager-list.ui \
        rc/calendar-manager-list-item.ui \
        rc/create-update-event.ui \
+       rc/google-authenticating.ui \
+       rc/google-calendar-list.ui \
+       rc/google-login.ui \
        rc/show-event.ui \
        rc/webcal-subscribe.ui \
        $(NULL)
@@ -132,6 +142,8 @@ california_VALAFLAGS = \
        --pkg libecal-1.2 \
        --pkg libical \
        --pkg libsoup-2.4 \
+       --pkg libgdata \
+       --pkg=Goa-1.0 \
        $(NULL)
 
 california_CFLAGS = \
diff --git a/src/activator/activator-window.vala b/src/activator/activator-window.vala
index 89fdea7..7385ae6 100644
--- a/src/activator/activator-window.vala
+++ b/src/activator/activator-window.vala
@@ -22,7 +22,10 @@ public class Window : Host.ModalWindow {
         // Activator's own interaction
         list.selected.connect(activator => {
             content_area.remove(list);
-            content_area.add(activator.create_interaction(null));
+            
+            Host.Interaction interaction = activator.create_interaction(null);
+            interaction.show_all();
+            content_area.add(interaction);
         });
         
         content_area.add(list);
diff --git a/src/activator/activator.vala b/src/activator/activator.vala
index 2ab4c2d..a709cbe 100644
--- a/src/activator/activator.vala
+++ b/src/activator/activator.vala
@@ -33,6 +33,7 @@ public void init() throws Error {
         as Backing.EdsStore;
     assert(eds_store != null);
     activators.add(new WebCalActivator(_("Web calendar (.ics)"), eds_store));
+    activators.add(new GoogleActivator(_("Google Calendar"), eds_store));
 }
 
 public void terminate() {
diff --git a/src/activator/google/activator-google-authenticating-pane.vala 
b/src/activator/google/activator-google-authenticating-pane.vala
new file mode 100644
index 0000000..75efcb2
--- /dev/null
+++ b/src/activator/google/activator-google-authenticating-pane.vala
@@ -0,0 +1,150 @@
+/* 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.Activator {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/google-authenticating.ui")]
+public class GoogleAuthenticatingPane : Gtk.Grid, Card {
+    public const string ID = "GoogleAuthenticatingPane";
+    
+    private const int SUCCESS_DELAY_MSEC = 1500;
+    
+    public class Message : BaseObject {
+        public string username { get; private set; }
+        public string password { get; private set; }
+        
+        public Message(string username, string password) {
+            this.username = username;
+            this.password = password;
+        }
+        
+        public override string to_string() {
+            return "Google:%s".printf(username);
+        }
+    }
+    
+    public string card_id { get { return ID; } }
+    
+    public string? title { get { return null; } }
+    
+    [GtkChild]
+    private Gtk.Spinner spinner;
+    
+    [GtkChild]
+    private Gtk.Label message_label;
+    
+    [GtkChild]
+    private Gtk.Button cancel_button;
+    
+    [GtkChild]
+    private Gtk.Button again_button;
+    
+    private Cancellable cancellable = new Cancellable();
+    
+    public GoogleAuthenticatingPane() {
+    }
+    
+    public void jumped_to(Card from, Value? message) {
+        Message? credentials = message as Message;
+        assert(credentials != null);
+        
+        cancel_button.sensitive = true;
+        again_button.sensitive = false;
+        
+        cancellable = new Cancellable();
+        login_async.begin(credentials);
+    }
+    
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        // spinner's active property doubles as flag if async operation is in progress
+        if (!spinner.active) {
+            dismissed(true);
+            
+            return;
+        }
+        
+        cancellable.cancel();
+        cancel_button.sensitive = false;
+    }
+    
+    [GtkCallback]
+    private void on_again_button_clicked() {
+        jump_back();
+    }
+    
+    private async void login_async(Message credentials) {
+        spinner.active = true;
+        
+        GData.ClientLoginAuthorizer authorizer = new GData.ClientLoginAuthorizer("yorba-california-0.1.0",
+            typeof(GData.CalendarService));
+        authorizer.captcha_challenge.connect(uri => { debug("CAPTCHA required: %s", uri); return ""; } );
+        
+        try {
+            if (!yield authorizer.authenticate_async(credentials.username, credentials.password, 
cancellable)) {
+                login_failed(_("Unable to authenticate with Google Calendar service"));
+                
+                return;
+            }
+        } catch (Error err) {
+            if (err is IOError.CANCELLED)
+                login_cancelled();
+            else if (err is GData.ClientLoginAuthorizerError.BAD_AUTHENTICATION)
+                login_failed(_("Unable to authenticate: Incorrect account name or password"));
+            else
+                login_failed(_("Unable to authenticate: %s").printf(err.message));
+            
+            spinner.active = false;
+            
+            return;
+        }
+        
+        GData.CalendarService calservice = new GData.CalendarService(authorizer);
+        
+        GData.Feed own_calendars;
+        GData.Feed all_calendars;
+        try {
+            own_calendars = calservice.query_own_calendars(null, cancellable, null);
+            all_calendars = calservice.query_all_calendars(null, cancellable, null);
+        } catch (Error err) {
+            if (err is IOError.CANCELLED)
+                login_cancelled();
+            else
+                login_failed(_("Unable to retrieve calendar list: %s").printf(err.message));
+            
+            spinner.active = false;
+            
+            return;
+        }
+        
+        spinner.active = false;
+        
+        message_label.label = _("Authenticated");
+        
+        // depending on network conditions, this pane can come and go quite quickly; this brief
+        // delay gives the user a chance to see what's transpired
+        Timeout.add(SUCCESS_DELAY_MSEC, () => {
+            jump_to_card_by_name(GoogleCalendarListPane.ID, new GoogleCalendarListPane.Message(
+                own_calendars, all_calendars));
+            
+            return false;
+        });
+    }
+    
+    private void login_failed(string msg) {
+        message_label.label = msg;
+        spinner.active = false;
+        again_button.sensitive = true;
+    }
+    
+    private void login_cancelled() {
+        spinner.active = false;
+        dismissed(true);
+    }
+}
+
+}
+
diff --git a/src/activator/google/activator-google-calendar-list-pane.vala 
b/src/activator/google/activator-google-calendar-list-pane.vala
new file mode 100644
index 0000000..ea765dc
--- /dev/null
+++ b/src/activator/google/activator-google-calendar-list-pane.vala
@@ -0,0 +1,178 @@
+/* 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.Activator {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/google-calendar-list.ui")]
+public class GoogleCalendarListPane : Gtk.Grid, Card {
+    public const string ID = "GoogleCalendarListPane";
+    
+    public class Message : BaseObject {
+        public GData.Feed own_calendars { get; private set; }
+        public GData.Feed all_calendars { get; private set; }
+        
+        public Message(GData.Feed own_calendars, GData.Feed all_calendars) {
+            this.own_calendars = own_calendars;
+            this.all_calendars = all_calendars;
+        }
+        
+        public override string to_string() {
+            return "Google Calendar feeds";
+        }
+    }
+    
+    public string card_id { get { return ID; } }
+    
+    public string? title { get { return null; } }
+    
+    [GtkChild]
+    private Gtk.ListBox own_calendars_listbox;
+    
+    [GtkChild]
+    private Gtk.ListBox unowned_calendars_listbox;
+    
+    [GtkChild]
+    private Gtk.Button subscribe_button;
+    
+    private Backing.WebCalSubscribable store;
+    private ListBoxModel<GData.CalendarCalendar> own_calendars_model;
+    private ListBoxModel<GData.CalendarCalendar> unowned_calendars_model;
+    
+    public GoogleCalendarListPane(Backing.WebCalSubscribable store) {
+        this.store = store;
+        
+        own_calendars_listbox.set_placeholder(create_placeholder());
+        unowned_calendars_listbox.set_placeholder(create_placeholder());
+        
+        own_calendars_model = new ListBoxModel<GData.CalendarCalendar>(own_calendars_listbox,
+            entry_to_widget, entry_comparator);
+        unowned_calendars_model = new ListBoxModel<GData.CalendarCalendar>(unowned_calendars_listbox,
+            entry_to_widget, entry_comparator);
+    }
+    
+    private static Gtk.Widget create_placeholder() {
+        Gtk.Label label = new Gtk.Label(null);
+        label.set_markup("<i>" + _("None") + "</i>");
+        
+        return label;
+    }
+    
+    public void jumped_to(Card from, Value? message) {
+        Message? feeds = message as Message;
+        assert(feeds != null);
+        
+        own_calendars_model.clear();
+        unowned_calendars_model.clear();
+        subscribe_button.sensitive = false;
+        
+        // add all "own" calendars, keeping track of id to not add them when traversing all calendars
+        Gee.HashSet<string> own_ids = new Gee.HashSet<string>();
+        foreach (GData.Entry entry in feeds.own_calendars.get_entries()) {
+            GData.CalendarCalendar calendar = (GData.CalendarCalendar) entry;
+            
+            own_calendars_model.add(calendar);
+            own_ids.add(calendar.id);
+        }
+        
+        // add everything not in previous list
+        foreach (GData.Entry entry in feeds.all_calendars.get_entries()) {
+            GData.CalendarCalendar calendar = (GData.CalendarCalendar) entry;
+            
+            if (!own_ids.contains(calendar.id))
+                unowned_calendars_model.add(calendar);
+        }
+    }
+    
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        dismissed(true);
+    }
+    
+    [GtkCallback]
+    private void on_subscribe_button_clicked() {
+        if (own_calendars_model.selected != null)
+            subscribe_async.begin(own_calendars_model.selected);
+        else if (unowned_calendars_model.selected != null)
+            subscribe_async.begin(unowned_calendars_model.selected);
+    }
+    
+    private Gtk.Widget entry_to_widget(GData.CalendarCalendar calendar) {
+        Gtk.Label label = new Gtk.Label(calendar.title);
+        label.xalign = 0.0f;
+        
+        return label;
+    }
+    
+    private int entry_comparator(GData.CalendarCalendar a, GData.CalendarCalendar b) {
+        return String.stricmp(a.title, b.title);
+    }
+    
+    [GtkCallback]
+    private void on_listbox_selected(Gtk.ListBox listbox, Gtk.ListBoxRow? row) {
+        // make sure there's only one selection between the two listboxes
+        if (row != null) {
+            if (listbox == own_calendars_listbox)
+                unowned_calendars_listbox.select_row(null);
+            else
+                own_calendars_listbox.select_row(null);
+        }
+        
+        subscribe_button.sensitive = (row != null);
+    }
+    
+    private async void subscribe_async(GData.CalendarCalendar calendar) {
+        subscribe_button.sensitive = false;
+        
+        // convert feed URI into an iCal URI
+        Soup.URI? uri = null;
+        string? errmsg = null;
+        try {
+            uri = URI.parse(calendar.content_uri);
+            
+            string[] elements = Soup.URI.decode(uri.path).split("/");
+            string? resource_name = null;
+            foreach (string element in elements) {
+                if (element == "feeds") {
+                    resource_name = element;
+                } else if (resource_name != null) {
+                    resource_name = element;
+                    
+                    break;
+                }
+            }
+            
+            if (resource_name == null) {
+                errmsg = "Not a feed URI or no resource name found";
+            } else {
+                debug("resource name = %s", resource_name);
+                uri.set_path("/calendar/dav/%s/events".printf(Soup.URI.encode(resource_name, null)));
+            }
+        } catch (Error err) {
+            errmsg = err.message;
+        }
+        
+        if (uri == null || errmsg != null) {
+            debug("Bad calendar URI %s: %s", calendar.content_uri, errmsg);
+            
+            dismissed(false);
+            
+            return;
+        }
+        
+        debug("Subscribing to %s", uri.to_string(false));
+        try {
+            yield store.subscribe_webcal_async(calendar.title, uri,calendar.color.to_hexadecimal(),
+                null);
+            completed();
+        } catch (Error err) {
+            debug("Unable to create subscription to %s: %s", calendar.content_uri, err.message);
+        }
+        
+        dismissed(true);
+    }
+}
+
+}
diff --git a/src/activator/google/activator-google-login-pane.vala 
b/src/activator/google/activator-google-login-pane.vala
new file mode 100644
index 0000000..1968407
--- /dev/null
+++ b/src/activator/google/activator-google-login-pane.vala
@@ -0,0 +1,56 @@
+/* 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.Activator {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/google-login.ui")]
+internal class GoogleLoginPane : Gtk.Grid, Card {
+    public const string ID = "GoogleLoginPane";
+    
+    public string card_id { get { return ID; } }
+    
+    public string? title { get { return null; } }
+    
+    [GtkChild]
+    private Gtk.Entry account_entry;
+    
+    [GtkChild]
+    private Gtk.Entry password_entry;
+    
+    [GtkChild]
+    private Gtk.Button login_button;
+    
+    public GoogleLoginPane() {
+        account_entry.bind_property("text-length", login_button, "sensitive",
+            BindingFlags.SYNC_CREATE, on_entry_changed);
+        password_entry.bind_property("text-length", login_button, "sensitive",
+            BindingFlags.SYNC_CREATE, on_entry_changed);
+    }
+    
+    public void jumped_to(Card from, Value? msg) {
+        password_entry.text = "";
+    }
+    
+    private bool on_entry_changed(Binding binding, Value source_value, ref Value target_value) {
+        target_value = account_entry.text_length > 0 && password_entry.text_length > 0;
+        
+        return true;
+    }
+    
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        dismissed(true);
+    }
+    
+    [GtkCallback]
+    private void on_login_button_clicked() {
+        jump_to_card_by_name(GoogleAuthenticatingPane.ID, new GoogleAuthenticatingPane.Message(
+            account_entry.text, password_entry.text));
+    }
+}
+
+}
+
diff --git a/src/activator/google/activator-google.vala b/src/activator/google/activator-google.vala
new file mode 100644
index 0000000..503fa53
--- /dev/null
+++ b/src/activator/google/activator-google.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.Activator {
+
+internal class GoogleActivator : Instance {
+    private Backing.WebCalSubscribable webcal_store;
+    
+    public GoogleActivator(string title, Backing.WebCalSubscribable store) {
+        base (title, store);
+        
+        webcal_store = store;
+    }
+    
+    public override Host.Interaction create_interaction(Soup.URI? supplied_uri) {
+        Deck deck = new Deck();
+        Gee.List<Card> cards = new Gee.ArrayList<Card>();
+        cards.add(new GoogleLoginPane());
+        cards.add(new GoogleAuthenticatingPane());
+        cards.add(new GoogleCalendarListPane(webcal_store));
+        deck.add_cards(cards);
+        
+        return deck;
+    }
+}
+
+}
+
diff --git a/src/backing/eds/backing-eds-store.vala b/src/backing/eds/backing-eds-store.vala
index 474bfc4..3631fff 100644
--- a/src/backing/eds/backing-eds-store.vala
+++ b/src/backing/eds/backing-eds-store.vala
@@ -52,7 +52,7 @@ internal class EdsStore : Store, WebCalSubscribable {
             throw new BackingError.UNAVAILABLE("EDS not open");
         
         E.Source scratch = new E.Source(null, null);
-        scratch.parent = "webcal-stub";
+        scratch.parent = "google-stub";
         scratch.enabled = true;
         scratch.display_name = title;
         
diff --git a/src/california-resources.xml b/src/california-resources.xml
index 9635cc3..f544e1b 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -16,6 +16,15 @@
         <file compressed="false">rc/create-update-event.ui</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/google-authenticating.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/google-calendar-list.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/google-login.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
         <file compressed="false">rc/show-event.ui</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
diff --git a/src/rc/google-authenticating.ui b/src/rc/google-authenticating.ui
new file mode 100644
index 0000000..87a0cc8
--- /dev/null
+++ b/src/rc/google-authenticating.ui
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaActivatorGoogleAuthenticatingPane" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="row_spacing">8</property>
+    <child>
+      <object class="GtkButtonBox" id="buttonbox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">end</property>
+        <property name="margin_top">8</property>
+        <property name="hexpand">True</property>
+        <property name="spacing">8</property>
+        <property name="homogeneous">True</property>
+        <property name="layout_style">end</property>
+        <child>
+          <object class="GtkButton" id="cancel_button">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <signal name="clicked" handler="on_cancel_button_clicked" 
object="CaliforniaActivatorGoogleAuthenticatingPane" 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="again_button">
+            <property name="label" translatable="yes">_Try again</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_again_button_clicked" 
object="CaliforniaActivatorGoogleAuthenticatingPane" swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="message_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="vexpand">True</property>
+        <property name="label" translatable="yes">Authenticating…</property>
+        <property name="wrap">True</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="GtkAlignment" id="spinner_alignment">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GtkSpinner" id="spinner">
+            <property name="height_request">48</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="valign">center</property>
+          </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>
+  </template>
+</interface>
diff --git a/src/rc/google-calendar-list.ui b/src/rc/google-calendar-list.ui
new file mode 100644
index 0000000..a5ca801
--- /dev/null
+++ b/src/rc/google-calendar-list.ui
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaActivatorGoogleCalendarListPane" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="row_spacing">8</property>
+    <child>
+      <object class="GtkLabel" id="my_calendars_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">My calendars:</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">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScrolledWindow" id="own_calendars_scrolledwindow">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <property name="min_content_height">100</property>
+        <child>
+          <object class="GtkViewport" id="own_calendars_viewport">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkListBox" id="own_calendars_listbox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="selection_mode">browse</property>
+                <property name="activate_on_single_click">False</property>
+                <signal name="row-selected" handler="on_listbox_selected" swapped="no"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButtonBox" id="buttonbox">
+        <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="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="CaliforniaActivatorGoogleCalendarListPane" 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="CaliforniaActivatorGoogleCalendarListPane" 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">4</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="other_calendars_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_top">8</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Other available calendars:</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScrolledWindow" id="unowned_calendars_scrolledwindow">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <property name="min_content_height">100</property>
+        <child>
+          <object class="GtkViewport" id="unowned_calendars_viewport">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkListBox" id="unowned_calendars_listbox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="selection_mode">browse</property>
+                <property name="activate_on_single_click">False</property>
+                <signal name="row-selected" handler="on_listbox_selected" swapped="no"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">3</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/rc/google-login.ui b/src/rc/google-login.ui
new file mode 100644
index 0000000..1ba6cfb
--- /dev/null
+++ b/src/rc/google-login.ui
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaActivatorGoogleLoginPane" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="row_spacing">8</property>
+    <child>
+      <object class="GtkButtonBox" id="buttonbox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">end</property>
+        <property name="margin_top">8</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="spacing">8</property>
+        <property name="homogeneous">True</property>
+        <property name="layout_style">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="CaliforniaActivatorGoogleLoginPane" 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="login_button">
+            <property name="label" translatable="yes">_Login</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="has_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <property name="xalign">0.62999999523162842</property>
+            <property name="yalign">0.52999997138977051</property>
+            <signal name="clicked" handler="on_login_button_clicked" 
object="CaliforniaActivatorGoogleLoginPane" 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>
+    <child>
+      <object class="GtkLabel" id="account_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">1</property>
+        <property name="xpad">8</property>
+        <property name="label" translatable="yes">Google _account name:</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">account_entry</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="GtkEntry" id="account_entry">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="has_focus">True</property>
+        <property name="is_focus">True</property>
+        <property name="activates_default">True</property>
+        <property name="width_chars">24</property>
+        <property name="input_purpose">email</property>
+      </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="GtkLabel" id="password_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">1</property>
+        <property name="xpad">8</property>
+        <property name="label" translatable="yes">Google _password:</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">password_entry</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="GtkEntry" id="password_entry">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="visibility">False</property>
+        <property name="activates_default">True</property>
+        <property name="width_chars">24</property>
+        <property name="input_purpose">password</property>
+      </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>
+  </template>
+</interface>
diff --git a/src/util/util-deck.vala b/src/util/util-deck.vala
new file mode 100644
index 0000000..2cd2693
--- /dev/null
+++ b/src/util/util-deck.vala
@@ -0,0 +1,132 @@
+/* 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 {
+
+public interface Card : Gtk.Widget {
+    public abstract string card_id { get; }
+    
+    public abstract string? title { get; }
+    
+    public signal void jump_to_card(Card next, Value? message);
+    
+    public signal void jump_to_card_by_name(string name, Value? message);
+    
+    public signal void jump_back();
+    
+    public signal void dismissed(bool user_request);
+    
+    public signal void completed();
+    
+    public abstract void jumped_to(Card from, Value? message);
+}
+
+public class Deck : Gtk.Stack, Host.Interaction {
+    /**
+     * @inheritedDoc
+     */
+    public Gtk.Widget? default_widget { get { return null; } }
+    
+    public int size { get { return names.size; } }
+    
+    private Card? top = null;
+    private Gee.Deque<Card> navigation_stack = new Gee.LinkedList<Card>();
+    private Gee.HashMap<string, Card> names = new Gee.HashMap<string, Card>();
+    
+    public Deck() {
+        transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
+        notify["visible-child"].connect(on_child_to_top);
+    }
+    
+    private void on_child_to_top() {
+        // disconnect from previous top card and push onto nav stack
+        if (top != null) {
+            top.jump_to_card.disconnect(on_jump_to_card);
+            top.jump_to_card_by_name.disconnect(on_jump_to_card_by_name);
+            top.jump_back.disconnect(on_jump_back);
+            top.dismissed.disconnect(on_dismissed);
+            top.completed.disconnect(on_completed);
+            
+            navigation_stack.offer_head(top);
+            top = null;
+        }
+        
+        // make new visible child top Card and connect to its signals
+        top = visible_child as Card;
+        if (top != null) {
+            top.jump_to_card.connect(on_jump_to_card);
+            top.jump_to_card_by_name.connect(on_jump_to_card_by_name);
+            top.jump_back.connect(on_jump_back);
+            top.dismissed.connect(on_dismissed);
+            top.completed.connect(on_completed);
+        }
+    }
+    
+    public void add_cards(Gee.List<Card> cards) {
+        if (cards.size == 0)
+            return;
+        
+        bool set_first_visible = size == 0;
+        
+        // add each Card using the title if possible, otherwise by ID
+        foreach (Card card in cards) {
+            // each card must have a unique name
+            assert(!String.is_empty(card.card_id));
+            assert(!names.has_key(card.card_id));
+            
+            if (String.is_empty(card.title))
+                add_named(card, card.card_id);
+            else
+                add_titled(card, card.card_id, card.title);
+            
+            names.set(card.card_id, card);
+        }
+        
+        if (set_first_visible)
+            set_visible_child(cards[0]);
+    }
+    
+    private void on_jump_to_card(Card card, Card next, Value? message) {
+        // do nothing if already visible
+        if (get_visible_child() == next)
+            return;
+        
+        // do nothing if not registered with this Deck
+        if (!names.values.contains(next)) {
+            GLib.message("Card %s not registered with Deck", next.card_id);
+            
+            return;
+        }
+        
+        set_visible_child(next);
+        next.jumped_to(card, message);
+    }
+    
+    private void on_jump_to_card_by_name(Card card, string name, Value? message) {
+        Card? next = names.get(name);
+        if (next != null)
+            on_jump_to_card(card, next, message);
+        else
+            GLib.message("Card %s not found in Deck", name);
+    }
+    
+    private void on_jump_back(Card card) {
+        // if still not empty, next card is "back", so pop that off and jump to it
+        if (!navigation_stack.is_empty)
+            on_jump_to_card(card, navigation_stack.poll_head(), null);
+    }
+    
+    private void on_dismissed(bool user_request) {
+        dismissed(user_request);
+    }
+    
+    private void on_completed() {
+        completed();
+    }
+}
+
+}
+
diff --git a/src/util/util-listbox-model.vala b/src/util/util-listbox-model.vala
new file mode 100644
index 0000000..370c9d9
--- /dev/null
+++ b/src/util/util-listbox-model.vala
@@ -0,0 +1,113 @@
+/* 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 {
+
+public interface Mutable : Object {
+    public signal void mutated();
+}
+
+public class ListBoxModel<G> : BaseObject {
+    public const string PROP_SELECTED = "selected";
+    
+    private const string KEY = "org.yorba.california.listbox-model.model";
+    
+    public delegate Gtk.Widget ModelPresentation<G>(G item);
+    
+    public Gtk.ListBox listbox { get; private set; }
+    
+    public int size { get { return items.size; } }
+    
+    public G? selected { get; private set; default = null; }
+    
+    private unowned ModelPresentation model_presentation;
+    private unowned CompareDataFunc<G>? comparator;
+    private Gee.HashSet<G> items;
+    
+    public signal void added(G item);
+    
+    public signal void activated(G item);
+    
+    public ListBoxModel(Gtk.ListBox listbox, ModelPresentation<G> model_presentation,
+        CompareDataFunc<G>? comparator = null, owned Gee.HashDataFunc<G>? hash_func = null,
+        owned Gee.EqualDataFunc<G>? equal_func = null) {
+        this.listbox = listbox;
+        this.model_presentation = model_presentation;
+        this.comparator = comparator;
+        
+        items = new Gee.HashSet<G>((owned) hash_func, (owned) equal_func);
+        
+        listbox.set_sort_func(listbox_sort_func);
+        listbox.row_activated.connect(on_row_activated);
+        listbox.row_selected.connect(on_row_selected);
+    }
+    
+    ~ListBoxModel() {
+        listbox.row_activated.disconnect(on_row_activated);
+        listbox.row_selected.disconnect(on_row_selected);
+        
+        foreach (G item in items) {
+            Mutable? mutable = item as Mutable;
+            if (mutable != null)
+                mutable.mutated.disconnect(on_mutated);
+        }
+    }
+    
+    public bool add(G item) {
+        if (!items.add(item))
+            return false;
+        
+        Mutable? mutable = item as Mutable;
+        if (mutable != null)
+            mutable.mutated.connect(on_mutated);
+        
+        Gtk.Widget widget = model_presentation(item);
+        widget.set_data<G>(KEY, item);
+        
+        listbox.add(widget);
+        widget.show_all();
+        
+        added(item);
+        
+        return true;
+    }
+    
+    public bool contains(G item) {
+        return items.contains(item);
+    }
+    
+    public void clear() {
+    }
+    
+    private int listbox_sort_func(Gtk.ListBoxRow a, Gtk.ListBoxRow b) {
+        unowned G item_a = a.get_child().get_data<G>(KEY);
+        unowned G item_b = b.get_child().get_data<G>(KEY);
+        
+        if (comparator != null)
+            return comparator(item_a, item_b);
+        
+        return Gee.Functions.get_compare_func_for(typeof(G))(item_a, item_b);
+    }
+    
+    private void on_row_activated(Gtk.ListBoxRow row) {
+        activated(row.get_child().get_data<G>(KEY));
+    }
+    
+    private void on_row_selected(Gtk.ListBoxRow? row) {
+        selected = (row != null) ? row.get_child().get_data<G>(KEY) : null;
+    }
+    
+    private void on_mutated() {
+        listbox.invalidate_sort();
+    }
+    
+    public override string to_string() {
+        return "ListboxModel";
+    }
+}
+
+}
+



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