[california] Subscribe to Google Calendar: Closes bgo#725763
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Subscribe to Google Calendar: Closes bgo#725763
- Date: Fri, 4 Apr 2014 00:28:40 +0000 (UTC)
commit 5b7dbebf2aacee55f659b1d30a9c16e6551adb4e
Author: Jim Nelson <jim yorba org>
Date: Thu Apr 3 17:27:28 2014 -0700
Subscribe to Google Calendar: Closes bgo#725763
This also takes better advantage of GtkStack to present steps to
the user for subscribing to any calendar.
configure.ac | 4 +
po/POTFILES.in | 5 +
src/Makefile.am | 13 +
src/activator/activator-instance-list.vala | 64 +++--
src/activator/activator-instance.vala | 11 +-
src/activator/activator-window.vala | 18 +-
src/activator/activator.vala | 9 +-
.../activator-google-authenticating-pane.vala | 158 +++++++++
.../activator-google-calendar-list-pane.vala | 188 +++++++++++
.../google/activator-google-login-pane.vala | 60 ++++
src/activator/google/activator-google.vala | 31 ++
src/activator/webcal/activator-webcal-pane.vala | 17 +-
src/activator/webcal/activator-webcal.vala | 9 +-
src/backing/backing-caldav-subscribable.vala | 30 ++
src/backing/backing-webcal-subscribable.vala | 2 +-
src/backing/eds/backing-eds-store.vala | 34 ++-
src/california-resources.xml | 9 +
src/rc/activator-list.ui | 3 +-
src/rc/google-authenticating.ui | 95 ++++++
src/rc/google-calendar-list.ui | 162 +++++++++
src/rc/google-login.ui | 133 ++++++++
src/rc/webcal-subscribe.ui | 1 +
src/util/util-deck.vala | 341 ++++++++++++++++++++
src/util/util-listbox-model.vala | 276 ++++++++++++++++
24 files changed, 1618 insertions(+), 55 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..071c509 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 \
\
@@ -21,6 +26,7 @@ california_VALASOURCES = \
application/main.vala \
\
backing/backing.vala \
+ backing/backing-caldav-subscribable.vala \
backing/backing-calendar-source.vala \
backing/backing-calendar-source-subscription.vala \
backing/backing-calendar-subscription-manager.vala \
@@ -85,7 +91,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 +118,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 +143,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-instance-list.vala b/src/activator/activator-instance-list.vala
index 2c94778..f7c3bcb 100644
--- a/src/activator/activator-instance-list.vala
+++ b/src/activator/activator-instance-list.vala
@@ -7,51 +7,75 @@
namespace California.Activator {
[GtkTemplate (ui = "/org/yorba/california/rc/activator-list.ui")]
-public class InstanceList : Gtk.Grid, Host.Interaction {
- private class Item : Gtk.Label {
- public Activator.Instance activator;
-
- public Item(Activator.Instance activator) {
- this.activator = activator;
-
- label = activator.title;
- xalign = 0.0f;
- margin = 4;
- }
- }
+public class InstanceList : Gtk.Grid, Card {
+ public const string ID = "ActivatorInstanceList";
+
+ public string card_id { get { return ID; } }
+
+ public string? title { get { return null; } }
public Gtk.Widget? default_widget { get { return add_button; } }
+ public Gtk.Widget? initial_focus { get { return listbox; } }
+
[GtkChild]
private Gtk.ListBox listbox;
[GtkChild]
private Gtk.Button add_button;
- public signal void selected(Activator.Instance activator);
+ private ListBoxModel<Instance> model;
public InstanceList() {
- foreach (Activator.Instance activator in activators)
- listbox.add(new Item(activator));
+ model = new ListBoxModel<Instance>(listbox, model_presentation, activator_comparator);
+ model.add_many(activators);
+
+ model.activated.connect(on_item_activated);
+ model.bind_property(ListBoxModel.PROP_SELECTED, add_button, "sensitive", BindingFlags.SYNC_CREATE,
+ selected_to_sensitive);
show_all();
}
- [GtkCallback]
- private void on_listbox_row_activated(Gtk.ListBoxRow? row) {
- if (row != null)
- selected(((Item) row.get_child()).activator);
+ private bool selected_to_sensitive(Binding binding, Value source_value, ref Value target_value) {
+ target_value = (model.selected != null);
+
+ return true;
+ }
+
+ public void jumped_to(Card? from, Value? message) {
+ }
+
+ private void on_item_activated(Instance activator) {
+ start(activator);
}
[GtkCallback]
private void on_add_button_clicked() {
- on_listbox_row_activated(listbox.get_selected_row());
+ if (model.selected != null)
+ start(model.selected);
}
[GtkCallback]
private void on_cancel_button_clicked() {
dismissed(true);
}
+
+ private void start(Instance activator) {
+ jump_to_card_by_name(activator.first_card_id, null);
+ }
+
+ private Gtk.Widget model_presentation(Instance activator) {
+ Gtk.Label label = new Gtk.Label(activator.title);
+ label.xalign = 0.0f;
+ label.margin = 4;
+
+ return label;
+ }
+
+ private int activator_comparator(Instance a, Instance b) {
+ return String.stricmp(a.title, b.title);
+ }
}
}
diff --git a/src/activator/activator-instance.vala b/src/activator/activator-instance.vala
index 01d84e0..ae9393b 100644
--- a/src/activator/activator-instance.vala
+++ b/src/activator/activator-instance.vala
@@ -33,16 +33,23 @@ public abstract class Instance : BaseObject {
*/
public Backing.Store store { get; private set; }
+ /**
+ * The { link Card.card_id} of the first Card returns by { link create_cards}.
+ */
+ public abstract string first_card_id { get; }
+
protected Instance(string title, Backing.Store store) {
this.title = title;
this.store = store;
}
/**
- * Return a { link Host.Interaction} that guides the user through the steps to create a
+ * Return a collection of { link Cards} that guides the user through the steps to create a
* { link Backing.Source}.
+ *
+ * The first Card will be jumped to initially.
*/
- public abstract Host.Interaction create_interaction(Soup.URI? supplied_uri);
+ public abstract Gee.List<Card> create_cards(Soup.URI? supplied_uri);
public override string to_string() {
return title;
diff --git a/src/activator/activator-window.vala b/src/activator/activator-window.vala
index 89fdea7..ed14006 100644
--- a/src/activator/activator-window.vala
+++ b/src/activator/activator-window.vala
@@ -13,19 +13,21 @@ namespace California.Activator {
public class Window : Host.ModalWindow {
private static Activator.Window? instance = null;
+ private Deck deck = new Deck();
+
private Window(Gtk.Window? parent) {
base (parent);
- InstanceList list = new InstanceList();
+ // The Deck is pre-populated with each of their Cards, with the InstanceList jumping to
+ // the right set when asked to (and acting as home)
+ Gee.List<Card> cards = new Gee.ArrayList<Card>();
+ cards.add(new InstanceList());
+ foreach (Instance activator in activators)
+ cards.add_all(activator.create_cards(null));
- // when an Activator instance is selected from the list, swap out the list for the
- // Activator's own interaction
- list.selected.connect(activator => {
- content_area.remove(list);
- content_area.add(activator.create_interaction(null));
- });
+ deck.add_cards(cards);
- content_area.add(list);
+ content_area.add(deck);
}
public static void display(Gtk.Window? parent) {
diff --git a/src/activator/activator.vala b/src/activator/activator.vala
index 2ab4c2d..27bddf7 100644
--- a/src/activator/activator.vala
+++ b/src/activator/activator.vala
@@ -18,7 +18,7 @@ namespace California.Activator {
private int init_count = 0;
-private Gee.TreeSet<Instance> activators;
+private Gee.List<Instance> activators;
public void init() throws Error {
if (!Unit.do_init(ref init_count))
@@ -26,13 +26,14 @@ public void init() throws Error {
Backing.init();
- activators = new Gee.TreeSet<Instance>(activator_comparator);
+ activators = new Gee.ArrayList<Instance>();
// All Instances that work with EDS
Backing.EdsStore? eds_store = Backing.Manager.instance.get_store_of_type<Backing.EdsStore>()
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() {
@@ -44,9 +45,5 @@ public void terminate() {
Backing.terminate();
}
-private int activator_comparator(Instance a, Instance b) {
- return String.stricmp(a.title, b.title);
-}
-
}
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..ca9800f
--- /dev/null
+++ b/src/activator/google/activator-google-authenticating-pane.vala
@@ -0,0 +1,158 @@
+/* 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);
+ }
+ }
+
+ private static string? app_id = null;
+
+ public string card_id { get { return ID; } }
+
+ public string? title { get { return null; } }
+
+ public Gtk.Widget? default_widget { get { return again_button; } }
+
+ public Gtk.Widget? initial_focus { 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() {
+ if (app_id == null)
+ app_id = "yorba-california-%s".printf(Application.VERSION);
+ }
+
+ 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) {
+ jump_home();
+
+ 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(app_id,
+ 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(
+ credentials.username, 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;
+ jump_home();
+ }
+}
+
+}
+
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..f50afa7
--- /dev/null
+++ b/src/activator/google/activator-google-calendar-list-pane.vala
@@ -0,0 +1,188 @@
+/* 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 string username { get; private set; }
+ public GData.Feed own_calendars { get; private set; }
+ public GData.Feed all_calendars { get; private set; }
+
+ public Message(string username, GData.Feed own_calendars, GData.Feed all_calendars) {
+ this.username = username;
+ 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; } }
+
+ public Gtk.Widget? default_widget { get { return subscribe_button; } }
+
+ public Gtk.Widget? initial_focus { 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.CalDAVSubscribable store;
+ private string? username = null;
+ private ListBoxModel<GData.CalendarCalendar> own_calendars_model;
+ private ListBoxModel<GData.CalendarCalendar> unowned_calendars_model;
+
+ public GoogleCalendarListPane(Backing.CalDAVSubscribable 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;
+
+ username = feeds.username;
+
+ // 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() {
+ jump_home();
+ }
+
+ [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);
+
+ // look for first path element after "/feeds/", which is the resource name of the
+ // calendar
+ string[] elements = Soup.URI.decode(uri.path).split("/");
+ string? resource_name = null;
+ for (int ctr = 0; ctr < elements.length; ctr++) {
+ if (elements[ctr] == "feeds") {
+ if (ctr < elements.length - 1)
+ resource_name = elements[ctr + 1];
+
+ break;
+ }
+ }
+
+ if (resource_name == null)
+ errmsg = _("Bad Google URI \"%s\"").printf(uri.to_string(false));
+ else
+ uri.set_path("/calendar/dav/%s/events".printf(Soup.URI.encode(resource_name, null)));
+ } catch (Error err) {
+ errmsg = err.message;
+ }
+
+ if (errmsg != null) {
+ Application.instance.error_message(_("Unable to subscribe to %s: %s").printf(
+ calendar.title, errmsg));
+
+ dismissed(false);
+
+ return;
+ }
+
+ debug("Subscribing to %s", uri.to_string(false));
+ try {
+ yield store.subscribe_caldav_async(calendar.title, uri, username,
+ calendar.color.to_hexadecimal(), null);
+ completed();
+ } catch (Error err) {
+ Application.instance.error_message(_("Unable to subscribe to %s: %s").printf(
+ calendar.title, 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..438af3e
--- /dev/null
+++ b/src/activator/google/activator-google-login-pane.vala
@@ -0,0 +1,60 @@
+/* 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; } }
+
+ public Gtk.Widget? default_widget { get { return login_button; } }
+
+ public Gtk.Widget? initial_focus { get { return account_entry; } }
+
+ [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() {
+ jump_home();
+ }
+
+ [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..973c3aa
--- /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 {
+ public override string first_card_id { get { return GoogleLoginPane.ID; } }
+
+ private Backing.CalDAVSubscribable caldav_store;
+
+ public GoogleActivator(string title, Backing.CalDAVSubscribable store) {
+ base (title, store);
+
+ caldav_store = store;
+ }
+
+ public override Gee.List<Card> create_cards(Soup.URI? supplied_uri) {
+ Gee.List<Card> cards = new Gee.ArrayList<Card>();
+ cards.add(new GoogleLoginPane());
+ cards.add(new GoogleAuthenticatingPane());
+ cards.add(new GoogleCalendarListPane(caldav_store));
+
+ return cards;
+ }
+}
+
+}
+
diff --git a/src/activator/webcal/activator-webcal-pane.vala b/src/activator/webcal/activator-webcal-pane.vala
index 500794c..b1cb10e 100644
--- a/src/activator/webcal/activator-webcal-pane.vala
+++ b/src/activator/webcal/activator-webcal-pane.vala
@@ -7,9 +7,17 @@
namespace California.Activator {
[GtkTemplate (ui = "/org/yorba/california/rc/webcal-subscribe.ui")]
-internal class WebCalActivatorPane : Gtk.Grid, Host.Interaction {
+internal class WebCalActivatorPane : Gtk.Grid, Card {
+ public const string ID = "WebCalActivatorPane";
+
+ public string card_id { get { return ID; } }
+
+ public string? title { get { return null; } }
+
public Gtk.Widget? default_widget { get { return subscribe_button; } }
+ public Gtk.Widget? initial_focus { get { return name_entry; } }
+
[GtkChild]
private Gtk.ColorButton color_button;
@@ -38,6 +46,9 @@ internal class WebCalActivatorPane : Gtk.Grid, Host.Interaction {
BindingFlags.SYNC_CREATE, on_entry_changed);
}
+ public void jumped_to(Card? from, Value? message) {
+ }
+
private bool on_entry_changed(Binding binding, Value source_value, ref Value target_value) {
target_value =
name_entry.text_length > 0
@@ -49,7 +60,7 @@ internal class WebCalActivatorPane : Gtk.Grid, Host.Interaction {
[GtkCallback]
private void on_cancel_button_clicked() {
- dismissed(true);
+ jump_home();
}
[GtkCallback]
@@ -65,7 +76,7 @@ internal class WebCalActivatorPane : Gtk.Grid, Host.Interaction {
try {
yield store.subscribe_webcal_async(name_entry.text, URI.parse(url_entry.text),
- Gfx.rgb_to_uint8_rgb_string(color), null);
+ null, 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);
diff --git a/src/activator/webcal/activator-webcal.vala b/src/activator/webcal/activator-webcal.vala
index c232adb..9d91dda 100644
--- a/src/activator/webcal/activator-webcal.vala
+++ b/src/activator/webcal/activator-webcal.vala
@@ -7,6 +7,8 @@
namespace California.Activator {
internal class WebCalActivator : Instance {
+ public override string first_card_id { get { return WebCalActivatorPane.ID; } }
+
private Backing.WebCalSubscribable webcal_store;
public WebCalActivator(string title, Backing.WebCalSubscribable store) {
@@ -15,8 +17,11 @@ internal class WebCalActivator : Instance {
webcal_store = store;
}
- public override Host.Interaction create_interaction(Soup.URI? supplied_uri) {
- return new WebCalActivatorPane(webcal_store, supplied_uri);
+ public override Gee.List<Card> create_cards(Soup.URI? supplied_uri) {
+ Gee.List<Card> cards = new Gee.ArrayList<Card>();
+ cards.add(new WebCalActivatorPane(webcal_store, supplied_uri));
+
+ return cards;
}
}
diff --git a/src/backing/backing-caldav-subscribable.vala b/src/backing/backing-caldav-subscribable.vala
new file mode 100644
index 0000000..e876281
--- /dev/null
+++ b/src/backing/backing-caldav-subscribable.vala
@@ -0,0 +1,30 @@
+/* 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 CalDAV calendars.
+ *
+ * See [[http://caldav.calconnect.org/]] for more information about CalDAV.
+ */
+
+public interface CalDAVSubscribable : Store {
+ /**
+ * Subscribe to a CalDAV 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_caldav_async(string title, Soup.URI uri, string? username,
+ string color, Cancellable? cancellable) throws Error;
+}
+
+}
+
diff --git a/src/backing/backing-webcal-subscribable.vala b/src/backing/backing-webcal-subscribable.vala
index 970d119..6b029e0 100644
--- a/src/backing/backing-webcal-subscribable.vala
+++ b/src/backing/backing-webcal-subscribable.vala
@@ -23,7 +23,7 @@ public interface WebCalSubscribable : Store {
* 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,
+ public abstract async void subscribe_webcal_async(string title, Soup.URI uri, string? username,
string color, Cancellable? cancellable) throws Error;
}
diff --git a/src/backing/eds/backing-eds-store.vala b/src/backing/eds/backing-eds-store.vala
index 474bfc4..2b6efaf 100644
--- a/src/backing/eds/backing-eds-store.vala
+++ b/src/backing/eds/backing-eds-store.vala
@@ -4,13 +4,15 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
+extern void e_source_webdav_set_soup_uri(E.SourceWebdav webdav, Soup.URI uri);
+
namespace California.Backing {
/**
* An interface to the EDS source registry.
*/
-internal class EdsStore : Store, WebCalSubscribable {
+internal class EdsStore : Store, WebCalSubscribable, CalDAVSubscribable {
private E.SourceRegistry? registry = null;
private Gee.HashMap<E.Source, Source> sources = new Gee.HashMap<E.Source, Source>();
@@ -43,16 +45,28 @@ internal class EdsStore : Store, WebCalSubscribable {
/**
* @inheritDoc
- *
- * TODO: Authentication is not properly handled.
*/
- public async void subscribe_webcal_async(string title, Soup.URI uri, string color,
+ public async void subscribe_webcal_async(string title, Soup.URI uri, string? username, string color,
Cancellable? cancellable) throws Error {
+ yield subscribe_eds_async(title, uri, username, color, "webcal", cancellable);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public async void subscribe_caldav_async(string title, Soup.URI uri, string? username, string color,
+ Cancellable? cancellable) throws Error {
+ yield subscribe_eds_async(title, uri, username, color, "caldav", cancellable);
+ }
+
+ private async void subscribe_eds_async(string title, Soup.URI uri, string? username, string color,
+ string backend_name, 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";
+ // Surprise -- Google gets special treatment
+ scratch.parent = uri.host.has_suffix("google.com") ? "google-stub" : "webcal-stub";
scratch.enabled = true;
scratch.display_name = title;
@@ -61,7 +75,7 @@ internal class EdsStore : Store, WebCalSubscribable {
as E.SourceCalendar;
if (calendar == null)
throw new BackingError.UNAVAILABLE("No SourceCalendar extension for scratch source");
- calendar.backend_name = "webcal";
+ calendar.backend_name = backend_name;
calendar.selected = true;
calendar.color = color;
@@ -70,17 +84,15 @@ internal class EdsStore : Store, WebCalSubscribable {
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;
+ // nice method that takes care of setting things correctly in a lot of other extensions
+ e_source_webdav_set_soup_uri(webdav, uri);
// 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";
+ auth.user = username;
// optional w/ baked-in defaults
E.SourceOffline? offline = scratch.get_extension(E.SOURCE_EXTENSION_OFFLINE)
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/activator-list.ui b/src/rc/activator-list.ui
index da0d3bc..37d5cba 100644
--- a/src/rc/activator-list.ui
+++ b/src/rc/activator-list.ui
@@ -21,7 +21,6 @@
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="activate_on_single_click">False</property>
- <signal name="row-activated" handler="on_listbox_row_activated"
object="CaliforniaActivatorInstanceList" swapped="no"/>
</object>
</child>
</object>
@@ -43,7 +42,7 @@
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
- <property name="label" translatable="yes">_Cancel</property>
+ <property name="label" translatable="yes">_Close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
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/rc/webcal-subscribe.ui b/src/rc/webcal-subscribe.ui
index a20ddd1..df8749a 100644
--- a/src/rc/webcal-subscribe.ui
+++ b/src/rc/webcal-subscribe.ui
@@ -117,6 +117,7 @@
<property name="can_focus">False</property>
<property name="valign">end</property>
<property name="margin_top">8</property>
+ <property name="vexpand">True</property>
<property name="spacing">8</property>
<property name="homogeneous">True</property>
<property name="baseline_position">bottom</property>
diff --git a/src/util/util-deck.vala b/src/util/util-deck.vala
new file mode 100644
index 0000000..c564152
--- /dev/null
+++ b/src/util/util-deck.vala
@@ -0,0 +1,341 @@
+/* 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 {
+
+/**
+ * A Card is a single pane of widget(s) in a { link Deck}.
+ *
+ * The navigation of Cards is tracked within their Deck, and Cards can request navigation via their
+ * various signals. They're also notified when nevigation which affects them is made.
+ */
+
+public interface Card : Gtk.Widget {
+ /**
+ * Each { link Card} has its own identifier that should be unique within the { link Deck}.
+ *
+ * In the Gtk.Stack, this is its name.
+ */
+ public abstract string card_id { get; }
+
+ /**
+ * A user-visible string that may be used elsewhere in the application.
+ *
+ * Gtk.StackSwitcher uses this title. { link Deck} does not use the title in any way.
+ */
+ public abstract string? title { get; }
+
+ /**
+ * The widget the { link Card} wants to be default when navigated to.
+ *
+ * The widget must have can-default set to true.
+ */
+ public abstract Gtk.Widget? default_widget { get; }
+
+ /**
+ * The widget the { link Card} wants to have initial focus when navigated to.
+ *
+ * Focus is set after { link default_widget} is handled, so if this widget has receives-default
+ * set to true, it will get the default as well.
+ *
+ * The widget must have can-focus set to true.
+ */
+ public abstract Gtk.Widget? initial_focus { get; }
+
+ /**
+ * Returns the { link Deck} this { link Card} is registered to, if any.
+ */
+ public Deck? deck { get { return parent as Deck; } }
+
+ /**
+ * Fired when the { link Card} wishes to jump to another Card in the same { link Deck.}
+ *
+ * Each Card can accept a message which parameterizes its activation. It's up to Cards
+ * navigating to the new one to construct and pass an appropriate message.
+ *
+ * @see jump_to_card_by_name
+ */
+ public signal void jump_to_card(Card next, Value? message);
+
+ /**
+ * Fired when the { link Card} wishes to jump to another Card by its name.
+ *
+ * @see jump_to_card
+ */
+ public signal void jump_to_card_by_name(string name, Value? message);
+
+ /**
+ * Fired when the { link Card} wishes to jump to the previous Card in the { link Deck}.
+ *
+ * Note that this Card's position in the navigation stack is lost; there is no "jump forward".
+ */
+ public signal void jump_back();
+
+ /**
+ * Fired when the { link Card} wishes to jump to the first Card in the { link Deck}.
+ *
+ * This clears the Deck's navigation stack, meaning { link jump_back} will not return to
+ * this Card.
+ */
+ public signal void jump_home();
+
+ /**
+ * Fired when the { link Deck}'s work is cancelled, closed, or dismissed, whether due to
+ * programmatic reasons or by user request.
+ *
+ * Implementing classes should fire this after firing the { link completed signal} so
+ * subscribers can maintain their cleanup in a single handler.
+ */
+ public signal void dismissed(bool user_request);
+
+ /**
+ * Fired when the { link Deck}'s work has completed successfully.
+ *
+ * This should only be fired if the Deck requires valid input from the user to perform
+ * some intensive operation. Merely displaying information and closing the Deck
+ * 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();
+
+ /**
+ * Called by { link Deck} when the { link Card} has been activated, i.e. put to the "top" of
+ * the Deck.
+ *
+ * message may be null even if the Card expects one; generally this means { link jump_back}
+ * or { link jump_home} was invoked, resulting in this Card being activated.
+ *
+ * This is called before dealing with { link default_widget} and { link initial_focus}, so
+ * changes to those properties in this call, if need be.
+ */
+ public abstract void jumped_to(Card? from, Value? message);
+}
+
+/**
+ * A Deck is a collection of { link Card}s maintained within a Gtk.Stack.
+ *
+ * Cards control navigation through their various signals, which Deck monitors and acts upon.
+ * It also notifies Cards of nagivation changes which affect them via their abstract methods.
+ */
+
+public class Deck : Gtk.Stack, Host.Interaction {
+ /**
+ * @inheritedDoc
+ */
+ public Gtk.Widget? default_widget { get { return null; } }
+
+ /**
+ * The number of { link Card}s registered to the { link Deck}.
+ */
+ public int size { get { return list.size; } }
+
+ /**
+ * All registered { link Card}s returned as a read-only List.
+ */
+ public Gee.List<Card> cards { owned get { return list.read_only_view; } }
+
+ /**
+ * The home { link Card}.
+ */
+ public Card? home { owned get { return (list.size > 0) ? list[0] : null; } }
+
+ /**
+ * The current displayed { link Card}.
+ */
+ public Card? top { get; private set; default = null; }
+
+ private Gee.List<Card> list = new Gee.LinkedList<Card>();
+ private Gee.Deque<Card> navigation_stack = new Gee.LinkedList<Card>();
+ private Gee.HashMap<string, Card> names = new Gee.HashMap<string, Card>();
+
+ /**
+ * Create a new { link Deck}.
+ *
+ * By default the Deck configures the underlying Gtk.Stack to slide left and right, depending
+ * on the position of the { link Card}s. This can be changed, but the recommended
+ * transition types are SLIDE_LEFT_RIGHT and SLIDE_UP_DOWN.
+ */
+ public Deck() {
+ transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
+ notify["visible-child"].connect(on_child_to_top);
+ }
+
+ ~Deck() {
+ foreach (Card card in names.values)
+ card.map.disconnect(on_card_mapped);
+ }
+
+ 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.jump_home.disconnect(on_jump_home);
+ 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.jump_home.connect(on_jump_home);
+ top.dismissed.connect(on_dismissed);
+ top.completed.connect(on_completed);
+ }
+ }
+
+ /**
+ * Add { link Card}s to the { link Deck}.
+ *
+ * Cards can be added in multiple batches, but the ordering is important as it dictates how
+ * they're presented to the user via transitions and slides.
+ *
+ * The first Card added is the "home" Card. The Deck will automatically show it first.
+ */
+ public void add_cards(Gee.List<Card> cards) {
+ if (cards.size == 0)
+ return;
+
+ // if empty, first card is home and should be made visible when added
+ bool set_home_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);
+
+ // deal with initial_focus and default_widget when mapped, as the calls aren't
+ // guaranteed to work during programmatic navigation (especially for the first card,
+ // i.e. home)
+ card.map.connect(on_card_mapped);
+
+ // add in order to ensure order is preserved if sparsely removed later
+ list.add(card);
+ }
+
+ if (set_home_visible && home != null) {
+ set_visible_child(home);
+ home.jumped_to(null, null);
+ }
+ }
+
+ /**
+ * Removes { link Card}s from the { link Deck}.
+ *
+ * If the { link top} card is removed, the Deck will return { link home}, clearing the
+ * navigation stack in the process.
+ */
+ public void remove_cards(Gee.Iterable<Card> cards) {
+ bool displaying = top != null;
+
+ foreach (Card card in cards) {
+ if (!names.has_key(card.card_id)) {
+ message("Card %s not found in Deck", card.card_id);
+
+ continue;
+ }
+
+ card.map.disconnect(on_card_mapped);
+
+ remove(card);
+
+ if (top == card)
+ top = null;
+
+ navigation_stack.remove(card);
+ names.unset(card.card_id);
+ list.remove(card);
+ }
+
+ // if was displaying a Card and now not, jump home
+ if (displaying && top == null && home != null) {
+ navigation_stack.clear();
+ set_visible_child(home);
+ home.jumped_to(null, null);
+ }
+ }
+
+ private void on_jump_to_card(Card card, Card next, Value? message) {
+ // do nothing if already visible
+ if (get_visible_child() == next) {
+ debug("Already showing card %s", next.card_id);
+
+ 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_jump_home(Card card) {
+ // jumping home clears the navigation stack
+ navigation_stack.clear();
+
+ if (home != null)
+ on_jump_to_card(card, home, null);
+ else
+ message("No home card in Deck");
+ }
+
+ private void on_dismissed(bool user_request) {
+ dismissed(user_request);
+ }
+
+ private void on_completed() {
+ completed();
+ }
+
+ private void on_card_mapped(Gtk.Widget widget) {
+ Card card = (Card) widget;
+
+ if (card.default_widget != null && card.default_widget.can_default)
+ card.default_widget.grab_default();
+
+ if (card.initial_focus != null && card.initial_focus.can_focus)
+ card.initial_focus.grab_focus();
+ }
+}
+
+}
+
diff --git a/src/util/util-listbox-model.vala b/src/util/util-listbox-model.vala
new file mode 100644
index 0000000..fb8f0d5
--- /dev/null
+++ b/src/util/util-listbox-model.vala
@@ -0,0 +1,276 @@
+/* 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 {
+
+/**
+ * A { link Mutable} is an Object which can internally change state (i.e. is no immutable).
+ *
+ * { link ListBoxModel} recognizes when an Object supports this interface and will monitor its
+ * { link mutated} signal.
+ */
+
+public interface Mutable : Object {
+ /**
+ * Fired when important internal state has changed.
+ *
+ * This can be used by collections and other containers to update their own state, such as
+ * re-sorting or re-applying filters.
+ */
+ public signal void mutated();
+}
+
+/**
+ * A simple model for Gtk.ListBox.
+ *
+ * ListBoxModel is designed to make it easier to maintain a sorted list of objects and make sure
+ * the associated Gtk.ListBox is always up-to-date reflecting the state of the model.
+ *
+ * If the added objects implement the { link Mutable} interface, their { link Mutable.mutated}
+ * signsl is monitored. When fired, the listbox's sort and filters will be invalidated.
+ */
+
+public class ListBoxModel<G> : BaseObject {
+ public const string PROP_SELECTED = "selected";
+
+ private const string KEY = "org.yorba.california.listbox-model.model";
+
+ /**
+ * Returns a Gtk.Widget that is held by the Gtk.ListBox representing the particular item.
+ */
+ public delegate Gtk.Widget ModelPresentation<G>(G item);
+
+ public Gtk.ListBox listbox { get; private set; }
+
+ /**
+ * The number if items in the { link ListBoxModel}.
+ */
+ public int size { get { return items.size; } }
+
+ /**
+ * The item currently selected by the { link listbox}, null if no selection has been made.
+ */
+ public G? selected { get; private set; default = null; }
+
+ private unowned ModelPresentation model_presentation;
+ private unowned CompareDataFunc<G>? comparator;
+ private Gee.HashMap<G, Gtk.ListBoxRow> items;
+
+ /**
+ * Fired when an item is added to the { link ListBoxModel}.
+ *
+ * @see add
+ */
+ public signal void added(G item);
+
+ /**
+ * Fired when an item is removed from the { link ListBoxModel}.
+ *
+ * @see remove
+ */
+ public signal void removed(G item);
+
+ /**
+ * Fired when the { link listbox} activates an item.
+ *
+ * Gtk.ListBox can activate an item with a double- or single-click, depending on configuration.
+ */
+ public signal void activated(G item);
+
+ /**
+ * Create a { link ListBoxModel} and tie it to a Gtk.ListBox.
+ *
+ * The list will be sorted if a comparator is supplied, otherwise added items are appended to
+ * the list.
+ */
+ 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.HashMap<G, Gtk.ListBoxRow>((owned) hash_func, (owned) equal_func);
+
+ listbox.remove.connect(on_listbox_removed);
+ 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.keys) {
+ Mutable? mutable = item as Mutable;
+ if (mutable != null)
+ mutable.mutated.disconnect(on_mutated);
+ }
+ }
+
+ /**
+ * Add an item to the model, which in turns adds it to the { link listbox}.
+ *
+ * If the item implements the { link Mutable} interface, its { link Mutable.mutated} signal
+ * is monitored and will invalidate the listbox's sort and filters.
+ *
+ * Returns true if the model (and therefore the listbox) were altered due to the addition.
+ *
+ * @see added
+ */
+ public bool add(G item) {
+ if (items.has_key(item))
+ return false;
+
+ Mutable? mutable = item as Mutable;
+ if (mutable != null)
+ mutable.mutated.connect(on_mutated);
+
+ // item -> Gtk.ListBoxRow
+ Gtk.ListBoxRow row = new Gtk.ListBoxRow();
+ row.add(model_presentation(item));
+
+ // mappings
+ row.set_data<G>(KEY, item);
+ items.set(item, row);
+
+ listbox.add(row);
+ row.show_all();
+
+ added(item);
+
+ return true;
+ }
+
+ /**
+ * Add a collection of { link Card}s to the { link Deck}.
+ *
+ * Returns the number of Cards added.
+ *
+ * @see add
+ */
+ public int add_many(Gee.Iterable<G> items) {
+ int count = 0;
+ foreach (G item in items) {
+ if (add(item))
+ count++;
+ }
+
+ return count;
+ }
+
+ /**
+ * Removes an item from the model, which in turn removes it from the { link listbox}.
+ *
+ * Returns true if the model (and therefore the listbox) were altered due to the removal.
+ *
+ * @see removed
+ */
+ public bool remove(G item) {
+ return internal_remove(item, true);
+ }
+
+ /**
+ * Removes a collection of { link Card}s from the { link Deck}.
+ *
+ * Returns the number of Cards removed.
+ *
+ * @see remove
+ */
+ public int remove_many(Gee.Iterable<G> items) {
+ int count = 0;
+ foreach (G item in items) {
+ if (remove(item))
+ count++;
+ }
+
+ return count;
+ }
+
+ private bool internal_remove(G item, bool remove_from_listbox) {
+ Gtk.ListBoxRow row;
+ if (!items.unset(item, out row))
+ return false;
+
+ Mutable? mutable = item as Mutable;
+ if (mutable != null)
+ mutable.mutated.disconnect(on_mutated);
+
+ if (remove_from_listbox)
+ listbox.remove(row);
+
+ removed(item);
+
+ return true;
+ }
+
+ /**
+ * Returns true if the model holds the item.
+ */
+ public bool contains(G item) {
+ return items.has_key(item);
+ }
+
+ /**
+ * Clears all items from the { link ListBoxModel}.
+ *
+ * Each removed item generates a { link removed} signal.
+ */
+ public void clear() {
+ foreach (G item in items.keys)
+ remove(item);
+ }
+
+ // This can be called by our add() method or externally, so don't be too absolutist here
+ private void on_listbox_removed(Gtk.Widget widget) {
+ // get the actual widget, not the wrapping object
+ Gtk.ListBoxRow? row = widget as Gtk.ListBoxRow;
+ if (row == null) {
+ message("GtkListBox removed non-GtkListBoxRow child");
+
+ return;
+ }
+
+ internal_remove(row.get_data<G>(KEY), false);
+ }
+
+ private int listbox_sort_func(Gtk.ListBoxRow a, Gtk.ListBoxRow b) {
+ unowned G item_a = a.get_data<G>(KEY);
+ unowned G item_b = b.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_data<G>(KEY));
+ }
+
+ private void on_row_selected(Gtk.ListBoxRow? row) {
+ selected = (row != null) ? row.get_data<G>(KEY) : null;
+ }
+
+ private void on_mutated(Mutable mutable) {
+ Gtk.ListBoxRow? row = items.get((G) mutable);
+ if (row == null) {
+ message("Mutable not found in ListBoxRow");
+
+ return;
+ }
+
+ row.changed();
+ }
+
+ public override string to_string() {
+ return "ListboxModel";
+ }
+}
+
+}
+
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]