[california/wip/725763-google] Google subscribe steps work all the way to actually subscribing
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725763-google] Google subscribe steps work all the way to actually subscribing
- Date: Thu, 3 Apr 2014 02:34:34 +0000 (UTC)
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]