[california] Import .ics file from the command-line: Closes bgo#725764



commit 232c5133c6644a171a6934f0dc4bc7b58a33d488
Author: Jim Nelson <jim yorba org>
Date:   Tue Apr 15 13:56:02 2014 -0700

    Import .ics file from the command-line: Closes bgo#725764
    
    California now accepts an .ics file on the command-line, parses it,
    asks the user which calendar to add it to, and adds it.  Some problems
    remain, in particular updated events don't seem to update properly
    under EDS w/ Google Calendar.

 src/Makefile.am                                    |   10 ++-
 src/activator/activator-instance-list.vala         |    2 +-
 .../activator-google-calendar-list-pane.vala       |    4 +-
 src/application/california-application.vala        |   68 ++++++++++-
 src/application/california-commandline.vala        |   67 ++++++++++
 src/backing/backing-calendar-source.vala           |    6 +
 src/backing/eds/backing-eds-calendar-source.vala   |    8 ++
 src/california-resources.xml                       |    6 +
 src/component/component-event.vala                 |   46 +++++++-
 src/component/component-icalendar.vala             |  132 ++++++++++++++++++++
 src/component/component-instance.vala              |    6 +-
 src/host/host-calendar-list-item.vala              |   48 +++++++
 src/host/host-import-calendar.vala                 |   70 +++++++++++
 src/rc/calendar-import.ui                          |  129 +++++++++++++++++++
 src/rc/calendar-list-item.ui                       |   47 +++++++
 src/toolkit/toolkit-listbox-model.vala             |   74 ++++++-----
 .../toolkit-mutable-widget.vala}                   |    9 +-
 vapi/libecal-1.2.vapi                              |   20 ++--
 vapi/libecal-1.2/libecal-1.2.metadata              |    5 +-
 vapi/libical.vapi                                  |   14 +-
 20 files changed, 705 insertions(+), 66 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 074b53d..ee35640 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -22,6 +22,7 @@ california_VALASOURCES = \
        activator/webcal/activator-webcal-pane.vala \
        \
        application/california-application.vala \
+       application/california-commandline.vala \
        application/california-resource.vala \
        application/main.vala \
        \
@@ -73,12 +74,15 @@ california_VALASOURCES = \
        component/component-date-time.vala \
        component/component-error.vala \
        component/component-event.vala \
+       component/component-icalendar.vala \
        component/component-instance.vala \
        component/component-uid.vala \
        component/component-vtype.vala \
        \
        host/host.vala \
+       host/host-calendar-list-item.vala \
        host/host-create-update-event.vala \
+       host/host-import-calendar.vala \
        host/host-main-window.vala \
        host/host-show-event.vala \
        \
@@ -91,12 +95,12 @@ california_VALASOURCES = \
        toolkit/toolkit-calendar-popup.vala \
        toolkit/toolkit-card.vala \
        toolkit/toolkit-deck.vala \
-       toolkit/toolkit-listbox-model.vala \
        toolkit/toolkit-deck-window.vala \
+       toolkit/toolkit-listbox-model.vala \
+       toolkit/toolkit-mutable-widget.vala \
        toolkit/toolkit-popup.vala \
        \
        util/util-gfx.vala \
-       util/util-interfaces.vala \
        util/util-memory.vala \
        util/util-string.vala \
        util/util-uri.vala \
@@ -118,6 +122,8 @@ california_SOURCES = \
 california_RC = \
        rc/activator-list.ui \
        rc/app-menu.interface \
+       rc/calendar-import.ui \
+       rc/calendar-list-item.ui \
        rc/calendar-manager-list.ui \
        rc/calendar-manager-list-item.ui \
        rc/create-update-event.ui \
diff --git a/src/activator/activator-instance-list.vala b/src/activator/activator-instance-list.vala
index e8583e1..b24736b 100644
--- a/src/activator/activator-instance-list.vala
+++ b/src/activator/activator-instance-list.vala
@@ -27,7 +27,7 @@ public class InstanceList : Gtk.Grid, Toolkit.Card {
     private Toolkit.ListBoxModel<Instance> model;
     
     public InstanceList() {
-        model = new Toolkit.ListBoxModel<Instance>(listbox, model_presentation, activator_comparator);
+        model = new Toolkit.ListBoxModel<Instance>(listbox, model_presentation, null, activator_comparator);
         model.add_many(activators);
         
         model.activated.connect(on_item_activated);
diff --git a/src/activator/google/activator-google-calendar-list-pane.vala 
b/src/activator/google/activator-google-calendar-list-pane.vala
index 31643e4..5dacd1a 100644
--- a/src/activator/google/activator-google-calendar-list-pane.vala
+++ b/src/activator/google/activator-google-calendar-list-pane.vala
@@ -55,9 +55,9 @@ public class GoogleCalendarListPane : Gtk.Grid, Toolkit.Card {
         unowned_calendars_listbox.set_placeholder(create_placeholder());
         
         own_calendars_model = new Toolkit.ListBoxModel<GData.CalendarCalendar>(own_calendars_listbox,
-            entry_to_widget, entry_comparator);
+            entry_to_widget, null, entry_comparator);
         unowned_calendars_model = new Toolkit.ListBoxModel<GData.CalendarCalendar>(unowned_calendars_listbox,
-            entry_to_widget, entry_comparator);
+            entry_to_widget, null, entry_comparator);
     }
     
     private static Gtk.Widget create_placeholder() {
diff --git a/src/application/california-application.vala b/src/application/california-application.vala
index 85e8efc..3b7387b 100644
--- a/src/application/california-application.vala
+++ b/src/application/california-application.vala
@@ -16,11 +16,12 @@ namespace California {
 
 public class Application : Gtk.Application {
     public const string TITLE = _("California");
-    public const string DESCRIPTION = _("Desktop Calendar");
+    public const string DESCRIPTION = _("GNOME 3 Calendar");
     public const string COPYRIGHT = _("Copyright 2014 Yorba Foundation");
     public const string VERSION = PACKAGE_VERSION;
     public const string WEBSITE_NAME = _("Visit California's home page");
     public const string WEBSITE_URL = "https://wiki.gnome.org/Apps/California";;
+    public const string BUGREPORT_URL = "https://bugzilla.gnome.org/enter_bug.cgi?product=california";;
     public const string ID = "org.yorba.california";
     public const string ICON_NAME = "x-office-calendar";
     
@@ -29,11 +30,16 @@ public class Application : Gtk.Application {
         null
     };
     
+    // public application menu actions; note their "app." prefix which does not
+    // match the actions in the action_entries table
     public const string ACTION_NEW_CALENDAR = "app.new-calendar";
     public const string ACTION_CALENDAR_MANAGER = "app.calendar-manager";
     public const string ACTION_ABOUT = "app.about";
     public const string ACTION_QUIT = "app.quit";
     
+    // internal actions; no "app." prefix
+    private const string ACTION_PROCESS_FILE = "process-file";
+    
     private static Application? _instance = null;
     public static Application instance {
         get {
@@ -42,10 +48,14 @@ public class Application : Gtk.Application {
     }
     
     private static const ActionEntry[] action_entries = {
+        // public actions
         { "new-calendar", on_new_calendar },
         { "calendar-manager", on_calendar_manager },
         { "about", on_about },
-        { "quit", on_quit }
+        { "quit", on_quit },
+        
+        // internal
+        { ACTION_PROCESS_FILE, on_process_file, "s" }
     };
     
     private Host.MainWindow? main_window = null;
@@ -59,6 +69,11 @@ public class Application : Gtk.Application {
     public override bool local_command_line(ref unowned string[] args, out int exit_status) {
         exec_file = File.new_for_path(Posix.realpath(Environment.find_program_in_path(args[0])));
         
+        // process arguments now, prior to register and activate; if true is returned before that,
+        // the application will exit with the exit code
+        if (!Commandline.parse(args, out exit_status))
+            return true;
+        
         try {
             register();
         } catch (Error err) {
@@ -67,6 +82,13 @@ public class Application : Gtk.Application {
         
         activate();
         
+        // tell the primary instance (which this instance may not be) about the command-line options
+        // it should act upon
+        if (Commandline.files != null) {
+            foreach (string file in Commandline.files)
+                activate_action(ACTION_PROCESS_FILE, file);
+        }
+        
         exit_status = 0;
         
         return true;
@@ -139,6 +161,48 @@ public class Application : Gtk.Application {
         Manager.Window.display(main_window);
     }
     
+    private void on_process_file(SimpleAction action, Variant? variant) {
+        if (variant == null)
+            return;
+        
+        // TODO: Support URIs
+        File file = File.new_for_commandline_arg((string) variant);
+        if (!file.is_native() || file.get_path() == null)
+            return;
+        
+        Component.iCalendar ical;
+        try {
+            MappedFile mmap = new MappedFile(file.get_path(), false);
+            ical = Component.iCalendar.parse((string) mmap.get_contents());
+        } catch (Error err) {
+            message("Unable to add %s: %s", file.get_path(), err.message);
+            
+            return;
+        }
+        
+        debug("Parsed %s", ical.to_string());
+        
+        // Ask the user to select a calendar to import it into
+        main_window.present_with_time(Gdk.CURRENT_TIME);
+        Host.ImportCalendar importer = new Host.ImportCalendar(main_window, ical);
+        Gtk.ResponseType response_type = (Gtk.ResponseType) importer.run();
+        importer.destroy();
+        
+        if (response_type != Gtk.ResponseType.OK || importer.chosen == null)
+            return;
+        
+        importer.chosen.import_icalendar_async.begin(ical, null, on_import_completed);
+    }
+    
+    private void on_import_completed(Object? object, AsyncResult result) {
+        Backing.CalendarSource calendar_source = (Backing.CalendarSource) object;
+        try {
+            calendar_source.import_icalendar_async.end(result);
+        } catch (Error err) {
+            debug("Unable to import iCalendar: %s", err.message);
+        }
+    }
+    
     private void on_about() {
         Gtk.show_about_dialog(main_window,
             "program-name", TITLE,
diff --git a/src/application/california-commandline.vala b/src/application/california-commandline.vala
new file mode 100644
index 0000000..324d5a4
--- /dev/null
+++ b/src/application/california-commandline.vala
@@ -0,0 +1,67 @@
+/* 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.Commandline {
+
+private const string PARAMS = _("[.ics FILE...]");
+
+public bool show_version = false;
+public Gee.List<string>? files = null;
+
+private const OptionEntry[] options = {
+    { "version", 'V', 0, OptionArg.NONE, ref show_version, N_("Display program version"), null },
+    { null }
+};
+
+/**
+ * Parse the command-line and process the obvious options, converting the remaining to options
+ * which are used by the remainder of the application.
+ *
+ * Returns false if the process should exit with the returned exit code.
+ */
+public bool parse(string[] args, out int exitcode) {
+    OptionContext context = new OptionContext(PARAMS);
+    context.set_help_enabled(true);
+    context.add_main_entries(options, null);
+    context.set_summary(Application.DESCRIPTION);
+    context.set_description("%s\n\n%s\n\t%s\n".printf(
+        Application.COPYRIGHT,
+        _("Please report problems and requests to:"),
+        Application.BUGREPORT_URL));
+    
+    try {
+        context.parse(ref args);
+    } catch (OptionError opterr) {
+        stdout.printf(_("Unknown options: %s\n").printf(opterr.message));
+        stdout.printf("\n%s".printf(context.get_help(true, null)));
+        
+        exitcode = 1;
+        
+        return false;
+    }
+    
+    // convert remaining arguments into files (although note that no sanity checking is
+    // performed)
+    for (int ctr = 1; ctr < args.length; ctr++) {
+        if (files == null)
+            files = new Gee.ArrayList<string>();
+        
+        files.add(args[ctr]);
+    }
+    
+    exitcode = 0;
+    
+    if (show_version) {
+        stdout.printf("%s %s\n", Application.TITLE, Application.VERSION);
+        
+        return false;
+    }
+    
+    return true;
+}
+
+}
+
diff --git a/src/backing/backing-calendar-source.vala b/src/backing/backing-calendar-source.vala
index 7f97a9b..1cd6cde 100644
--- a/src/backing/backing-calendar-source.vala
+++ b/src/backing/backing-calendar-source.vala
@@ -52,6 +52,12 @@ public abstract class CalendarSource : Source {
      */
     public abstract async void remove_component_async(Component.UID uid,
         Cancellable? cancellable = null) throws Error;
+    
+    /**
+     * Imports a { link Component.iCalendar} into the { link CalendarSource}.
+     */
+    public abstract async void import_icalendar_async(Component.iCalendar ical, Cancellable? cancellable = 
null)
+        throws Error;
 }
 
 }
diff --git a/src/backing/eds/backing-eds-calendar-source.vala 
b/src/backing/eds/backing-eds-calendar-source.vala
index 35a2966..9888257 100644
--- a/src/backing/eds/backing-eds-calendar-source.vala
+++ b/src/backing/eds/backing-eds-calendar-source.vala
@@ -167,6 +167,14 @@ internal class EdsCalendarSource : CalendarSource {
         // TODO: Fix remove_object() bindings so async is possible
         client.remove_object_sync(uid.value, null, E.CalObjModType.THIS, cancellable);
     }
+    
+    public override async void import_icalendar_async(Component.iCalendar ical, Cancellable? cancellable = 
null)
+        throws Error {
+        check_open();
+        
+        // TODO: Fix receive_objects() bindings so async is possible
+        client.receive_objects_sync(ical.ical_component, cancellable);
+    }
 }
 
 }
diff --git a/src/california-resources.xml b/src/california-resources.xml
index f544e1b..03bb412 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -7,6 +7,12 @@
         <file compressed="true">rc/app-menu.interface</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
+        <file compressed="true">rc/calendar-import.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
+        <file compressed="false">rc/calendar-list-item.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
         <file compressed="false">rc/calendar-manager-list.ui</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index 83930dc..08f0e69 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -18,6 +18,13 @@ public class Event : Instance, Gee.Comparable<Event> {
     public const string PROP_EXACT_TIME_SPAN = "exact-time-span";
     public const string PROP_DATE_SPAN = "date-span";
     public const string PROP_IS_ALL_DAY = "is-all-day";
+    public const string PROP_STATUS = "status";
+    
+    public enum Status {
+        TENTATIVE,
+        CONFIRMED,
+        CANCELLED
+    }
     
     /**
      * Summary (title) of { link Event}.
@@ -55,11 +62,16 @@ public class Event : Instance, Gee.Comparable<Event> {
     public bool is_all_day { get; private set; }
     
     /**
+     * Status (confirmation) of an { link Event}.
+     */
+    public Status status { get; set; default = Status.CONFIRMED; }
+    
+    /**
      * Create an { link Event} { link Component} from an EDS CalComponent object.
      *
      * Throws a BackingError if the E.CalComponent's VTYPE is not VEVENT.
      */
-    public Event(Backing.CalendarSource calendar_source, iCal.icalcomponent ical_component) throws Error {
+    public Event(Backing.CalendarSource? calendar_source, iCal.icalcomponent ical_component) throws Error {
         base (calendar_source, ical_component, iCal.icalcomponent_kind.VEVENT_COMPONENT);
         
         // remainder of state is initialized in update_from_component()
@@ -105,6 +117,21 @@ public class Event : Instance, Gee.Comparable<Event> {
         
         // need to set this here because on_notify() doesn't update inside full update
         is_all_day = (date_span != null);
+        
+        switch (ical_component.get_status()) {
+            case iCal.icalproperty_status.TENTATIVE:
+                status = Status.TENTATIVE;
+            break;
+            
+            case iCal.icalproperty_status.CANCELLED:
+                status = Status.CANCELLED;
+            break;
+            
+            case iCal.icalproperty_status.CONFIRMED:
+            default:
+                status = Status.CONFIRMED;
+            break;
+        }
     }
     
     private void on_notify(ParamSpec pspec) {
@@ -148,6 +175,23 @@ public class Event : Instance, Gee.Comparable<Event> {
                 is_all_day = (date_span != null);
             break;
             
+            case PROP_STATUS:
+                switch(status) {
+                    case Status.TENTATIVE:
+                        ical_component.set_status(iCal.icalproperty_status.TENTATIVE);
+                    break;
+                    
+                    case Status.CANCELLED:
+                        ical_component.set_status(iCal.icalproperty_status.CANCELLED);
+                    break;
+                    
+                    case Status.CONFIRMED:
+                    default:
+                        ical_component.set_status(iCal.icalproperty_status.CONFIRMED);
+                    break;
+                }
+            break;
+            
             default:
                 altered = false;
             break;
diff --git a/src/component/component-icalendar.vala b/src/component/component-icalendar.vala
new file mode 100644
index 0000000..e9e9b92
--- /dev/null
+++ b/src/component/component-icalendar.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.Component {
+
+/**
+ * An immutable representation of an iCalendar VCALENDAR component.
+ *
+ * Note that a iCalendar is not considered an { link Instance}; it's a container which holds
+ * Instances.  Although iCalendar is immutable, there is no guarantee that the Instances it
+ * hold will be.
+ *
+ * Also note that iCalendar currently is not associated with a { link Backing.CalendarSource}.
+ * If the feature is ever added where a CalendarSource can cough up its entire VCALENDAR,
+ * then that might make sense.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.6]].
+ */
+
+public class iCalendar : BaseObject {
+    /**
+     * The VCALENDAR's PRODID.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.7.3]]
+     */
+    public string? prodid { get; private set; default = null; }
+    
+    /**
+     * The VCALENDAR's VERSION.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.7.4].  In particular,
+     * read the Purpose section.
+     */
+    public string? version { get; private set; default = null; }
+    
+    /**
+     * The VCALENDAR's METHOD.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.7.2]]
+     */
+    public iCal.icalproperty_method method { get; private set; default = iCal.icalproperty_method.NONE; }
+    
+    /**
+     * The VCALENDAR's CALSCALE.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.7.1]]
+     */
+    public string? calscale { get; private set; default = null; }
+    
+    /**
+     * VEVENTS within the VCALENDAR.
+     */
+    public Gee.List<Event> events { get; private set; default = new Gee.ArrayList<Event>(); }
+    
+    /**
+     * The iCal VCALENDAR this iCalendar represents.
+     */
+    private iCal.icalcomponent _ical_component;
+    public iCal.icalcomponent ical_component { get { return _ical_component; } }
+    
+    /**
+     * Create an { link iCalendar} representation of the iCal component.
+     *
+     * @throws ComponentError.INVALID if root is not a VCALENDAR.
+     */
+    private iCalendar(owned iCal.icalcomponent root) throws Error {
+        if (root.isa() != iCal.icalcomponent_kind.VCALENDAR_COMPONENT)
+            throw new ComponentError.INVALID("Not a VCALENDAR");
+        
+        //
+        // VCALENDAR properties
+        //
+        
+        unowned iCal.icalproperty? prop = root.get_first_property(iCal.icalproperty_kind.PRODID_PROPERTY);
+        if (prop != null)
+            prodid = prop.get_prodid();
+        
+        prop = root.get_first_property(iCal.icalproperty_kind.VERSION_PROPERTY);
+        if (prop != null)
+            version = prop.get_version();
+        
+        prop = root.get_first_property(iCal.icalproperty_kind.CALSCALE_PROPERTY);
+        if (prop != null)
+            calscale = prop.get_calscale();
+        
+        prop = root.get_first_property(iCal.icalproperty_kind.METHOD_PROPERTY);
+        if (prop != null)
+            method = prop.get_method();
+        
+        //
+        // Contained components
+        //
+        
+        // VEVENTS
+        unowned iCal.icalcomponent? child_component = root.get_first_component(
+            iCal.icalcomponent_kind.VEVENT_COMPONENT);
+        while (child_component != null) {
+            events.add(Instance.convert(null, child_component) as Event);
+            child_component = root.get_next_component(iCal.icalcomponent_kind.VEVENT_COMPONENT);
+        }
+        
+        // take ownership
+        _ical_component = (owned) root;
+    }
+    
+    /**
+     * Returns an appropriate { link Calendar} instance for the string of iCalendar data.
+     *
+     * @throws ComponentError if data is unrecognized
+     */
+    public static iCalendar parse(string? str) throws Error {
+        if (String.is_empty(str))
+            throw new ComponentError.INVALID("Empty VCALENDAR string");
+        
+        iCal.icalcomponent? ical_component = iCal.icalparser.parse_string(str);
+        if (ical_component == null)
+            throw new ComponentError.INVALID("Unable to parse VCALENDAR (%db)".printf(str.length));
+        
+        return new iCalendar((owned) ical_component);
+    }
+    
+    public override string to_string() {
+        return "iCalendar|%s|%s|%s|%s (%d events)".printf(prodid, version, calscale, method.to_string(),
+            events.size);
+    }
+}
+
+}
+
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index f9769d7..037f851 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -115,7 +115,7 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
      * external invocation of full_update() (such as immutable data) should update that state after
      * the base constructor returns.
      */
-    protected Instance(Backing.CalendarSource calendar_source, iCal.icalcomponent ical_component,
+    protected Instance(Backing.CalendarSource? calendar_source, iCal.icalcomponent ical_component,
         iCal.icalcomponent_kind kind) throws Error {
         if (ical_component.isa() != kind) {
             throw new ComponentError.MISMATCH("Cannot create VTYPE %s from component of VTYPE %s",
@@ -260,9 +260,11 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     /**
      * Returns an appropriate { link Component} instance for the iCalendar component.
      *
+     * VCALENDARs should use { link Component.iCalendar}.
+     *
      * @returns null if the component is not represented in this namespace (yet).
      */
-    public static Component.Instance? convert(Backing.CalendarSource calendar_source,
+    public static Component.Instance? convert(Backing.CalendarSource? calendar_source,
         iCal.icalcomponent ical_component) throws Error {
         switch (ical_component.isa()) {
             case iCal.icalcomponent_kind.VEVENT_COMPONENT:
diff --git a/src/host/host-calendar-list-item.vala b/src/host/host-calendar-list-item.vala
new file mode 100644
index 0000000..9fc5213
--- /dev/null
+++ b/src/host/host-calendar-list-item.vala
@@ -0,0 +1,48 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+namespace California.Host {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/calendar-list-item.ui")]
+public class CalendarListItem : Gtk.Grid, Toolkit.MutableWidget {
+    public Backing.CalendarSource calendar_source { get; private set; }
+    
+    [GtkChild]
+    private Gtk.Image color_image;
+    
+    [GtkChild]
+    private Gtk.Label title_label;
+    
+    public CalendarListItem(Backing.CalendarSource calendar_source) {
+        this.calendar_source = calendar_source;
+        
+        set_title();
+        set_color();
+        
+        calendar_source.notify[Backing.Source.PROP_TITLE].connect(set_title);
+        calendar_source.notify[Backing.Source.PROP_COLOR].connect(set_color);
+    }
+    
+    ~CalendarListItem() {
+        calendar_source.notify[Backing.Source.PROP_TITLE].disconnect(set_title);
+        calendar_source.notify[Backing.Source.PROP_COLOR].disconnect(set_color);
+    }
+    
+    private void set_title() {
+        title_label.label = calendar_source.title;
+        mutated();
+    }
+    
+    private void set_color() {
+        Gdk.Pixbuf pixbuf = new Gdk.Pixbuf(Gdk.Colorspace.RGB, false, 8, color_image.width_request,
+            color_image.height_request);
+        pixbuf.fill(Gfx.rgba_to_pixel(calendar_source.color_as_rgba()));
+        color_image.set_from_pixbuf(pixbuf);
+    }
+}
+
+}
+
diff --git a/src/host/host-import-calendar.vala b/src/host/host-import-calendar.vala
new file mode 100644
index 0000000..5c58bac
--- /dev/null
+++ b/src/host/host-import-calendar.vala
@@ -0,0 +1,70 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+namespace California.Host {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/calendar-import.ui")]
+public class ImportCalendar : Gtk.Dialog {
+    public Component.iCalendar ical { get; private set;}
+    
+    public Backing.CalendarSource? chosen { get; private set; default = null; }
+    
+    [GtkChild]
+    private Gtk.Label title_label;
+    
+    [GtkChild]
+    private Gtk.ListBox calendar_listbox;
+    
+    [GtkChild]
+    private Gtk.Button import_button;
+    
+    private Toolkit.ListBoxModel<Backing.CalendarSource> model;
+    
+    public ImportCalendar(Gtk.Window parent, Component.iCalendar ical) {
+        this.ical = ical;
+        
+        transient_for = parent;
+        modal = true;
+        resizable = false;
+        
+        title_label.label = ngettext("Select calendar to import event into:",
+            "Select calendar to import events into:", ical.events.size);
+        
+        model = new Toolkit.ListBoxModel<Backing.CalendarSource>(calendar_listbox, model_presentation,
+            model_filter);
+        model.add_many(Backing.Manager.instance.get_sources_of_type<Backing.CalendarSource>());
+        
+        on_row_selected();
+        calendar_listbox.row_selected.connect(on_row_selected);
+    }
+    
+    private Gtk.Widget model_presentation(Backing.CalendarSource calendar_source) {
+        return new CalendarListItem(calendar_source);
+    }
+    
+    private bool model_filter(Backing.CalendarSource calendar_source) {
+        return calendar_source.visible;
+    }
+    
+    private void on_row_selected() {
+        import_button.sensitive = (calendar_listbox.get_selected_row() != null);
+    }
+    
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        response(Gtk.ResponseType.CANCEL);
+    }
+    
+    [GtkCallback]
+    private void on_import_button_clicked() {
+        chosen = model.selected;
+        
+        response(Gtk.ResponseType.OK);
+    }
+}
+
+}
+
diff --git a/src/rc/calendar-import.ui b/src/rc/calendar-import.ui
new file mode 100644
index 0000000..7e7bb60
--- /dev/null
+++ b/src/rc/calendar-import.ui
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaHostImportCalendar" parent="GtkDialog">
+    <property name="can_focus">False</property>
+    <property name="type_hint">dialog</property>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="dialog-vbox1">
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">2</property>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox" id="dialog-action_area1">
+            <property name="can_focus">False</property>
+            <property name="valign">center</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="CaliforniaHostImportCalendar" 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="import_button">
+                <property name="label" translatable="yes">_Import</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_import_button_clicked" 
object="CaliforniaHostImportCalendar" 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="expand">False</property>
+            <property name="fill">False</property>
+            <property name="pack_type">end</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkGrid" id="grid1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_left">8</property>
+            <property name="margin_right">8</property>
+            <property name="margin_top">8</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <property name="row_spacing">8</property>
+            <child>
+              <object class="GtkLabel" id="title_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="label">(label)</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="scrolledwindow1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="shadow_type">in</property>
+                <property name="min_content_width">200</property>
+                <property name="min_content_height">300</property>
+                <child>
+                  <object class="GtkViewport" id="viewport1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkListBox" id="calendar_listbox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">True</property>
+                        <property name="vexpand">True</property>
+                        <property name="activate_on_single_click">False</property>
+                      </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>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/rc/calendar-list-item.ui b/src/rc/calendar-list-item.ui
new file mode 100644
index 0000000..7009559
--- /dev/null
+++ b/src/rc/calendar-list-item.ui
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaHostCalendarListItem" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_left">4</property>
+    <property name="margin_right">4</property>
+    <property name="margin_top">4</property>
+    <property name="margin_bottom">4</property>
+    <property name="column_spacing">8</property>
+    <child>
+      <object class="GtkLabel" id="title_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label">(dummy title)</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="GtkImage" id="color_image">
+        <property name="width_request">20</property>
+        <property name="height_request">20</property>
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+        <property name="stock">gtk-missing-image</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>
+  </template>
+</interface>
diff --git a/src/toolkit/toolkit-listbox-model.vala b/src/toolkit/toolkit-listbox-model.vala
index ebbd947..6c46b74 100644
--- a/src/toolkit/toolkit-listbox-model.vala
+++ b/src/toolkit/toolkit-listbox-model.vala
@@ -12,8 +12,9 @@ namespace California.Toolkit {
  * 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.
+ * ListModelModel watches for Gtk.Widgets generated by { link ModelPresentation} to implement the
+ * { link MutableWidget} interface.  If they do, they can fire its "mutate" signal to indicate that
+ * the model needs to re-sort or re-filter the item.
  */
 
 public class ListBoxModel<G> : BaseObject {
@@ -26,6 +27,11 @@ public class ListBoxModel<G> : BaseObject {
      */
     public delegate Gtk.Widget ModelPresentation<G>(G item);
     
+    /**
+     * Returns true if the item should be considered "visible" by the Gtk.ListBox.
+     */
+    public delegate bool ModelFilter<G>(G item);
+    
     public Gtk.ListBox listbox { get; private set; }
     
     /**
@@ -39,6 +45,7 @@ public class ListBoxModel<G> : BaseObject {
     public G? selected { get; private set; default = null; }
     
     private unowned ModelPresentation model_presentation;
+    private unowned ModelFilter? model_filter;
     private unowned CompareDataFunc<G>? comparator;
     private Gee.HashMap<G, Gtk.ListBoxRow> items;
     
@@ -70,37 +77,32 @@ public class ListBoxModel<G> : BaseObject {
      * 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) {
+        ModelFilter<G>? model_filter = null, 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.model_filter = model_filter;
         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);
+        if (model_filter != null)
+            listbox.set_filter_func(listbox_filter_func);
         listbox.row_activated.connect(on_row_activated);
         listbox.row_selected.connect(on_row_selected);
     }
     
     ~ListBoxModel() {
+        listbox.remove.disconnect(on_listbox_removed);
         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
@@ -109,13 +111,13 @@ public class ListBoxModel<G> : BaseObject {
         if (items.has_key(item))
             return false;
         
-        Mutable? mutable = item as Mutable;
-        if (mutable != null)
-            mutable.mutated.connect(on_mutated);
-        
-        // item -> Gtk.ListBoxRow
+        // item -> Gtk.ListBoxRow, with MutableWidget support
         Gtk.ListBoxRow row = new Gtk.ListBoxRow();
-        row.add(model_presentation(item));
+        Gtk.Widget widget = model_presentation(item);
+        MutableWidget? mutable = widget as MutableWidget;
+        if (mutable != null)
+            mutable.mutated.connect(() => { row.changed(); });
+        row.add(mutable);
         
         // mappings
         row.set_data<G>(KEY, item);
@@ -179,10 +181,6 @@ public class ListBoxModel<G> : BaseObject {
         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);
         
@@ -199,6 +197,21 @@ public class ListBoxModel<G> : BaseObject {
     }
     
     /**
+     * Call to indicate that the contents of the item has mutated, i.e. changed or been altered,
+     * in such a way to affect sorting or filtering.
+     */
+    public void mutated(G item) {
+        Gtk.ListBoxRow? row = items.get(item);
+        if (row == null) {
+            message("Mutable not found in ListBoxRow");
+            
+            return;
+        }
+        
+        row.changed();
+    }
+    
+    /**
      * Clears all items from the { link ListBoxModel}.
      *
      * Each removed item generates a { link removed} signal.
@@ -231,6 +244,10 @@ public class ListBoxModel<G> : BaseObject {
         return Gee.Functions.get_compare_func_for(typeof(G))(item_a, item_b);
     }
     
+    private bool listbox_filter_func(Gtk.ListBoxRow row) {
+        return model_filter(row.get_data<G>(KEY));
+    }
+    
     private void on_row_activated(Gtk.ListBoxRow row) {
         activated(row.get_data<G>(KEY));
     }
@@ -239,17 +256,6 @@ public class ListBoxModel<G> : BaseObject {
         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";
     }
diff --git a/src/util/util-interfaces.vala b/src/toolkit/toolkit-mutable-widget.vala
similarity index 58%
rename from src/util/util-interfaces.vala
rename to src/toolkit/toolkit-mutable-widget.vala
index 9c98230..597eb08 100644
--- a/src/util/util-interfaces.vala
+++ b/src/toolkit/toolkit-mutable-widget.vala
@@ -4,15 +4,16 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-namespace California {
+namespace California.Toolkit {
 
 /**
- * A { link Mutable} is an Object which can internally change state (i.e. is no immutable).
+ * A { link MutableWidget} is a Gtk.Widget whose internal state can change and affect its sort
+ * order or filtering.
  */
 
-public interface Mutable : Object {
+public interface MutableWidget : Gtk.Widget {
     /**
-     * Fired when important internal state has changed.
+     * Fired when internal state has changed which may affect sorting or filtering.
      *
      * This can be used by collections and other containers to update their own state, such as
      * re-sorting or re-applying filters.
diff --git a/vapi/libecal-1.2.vapi b/vapi/libecal-1.2.vapi
index e6b8807..f49e3f9 100644
--- a/vapi/libecal-1.2.vapi
+++ b/vapi/libecal-1.2.vapi
@@ -48,7 +48,7 @@ namespace E {
                public async bool get_object_list_as_comps (string sexp, GLib.Cancellable? cancellable) 
throws GLib.Error;
                public bool get_object_list_as_comps_sync (string sexp, GLib.SList out_ecalcomps, 
GLib.Cancellable? cancellable) throws GLib.Error;
                public bool get_object_list_sync (string sexp, GLib.SList out_icalcomps, GLib.Cancellable? 
cancellable) throws GLib.Error;
-               public bool get_object_sync (string uid, string rid, out unowned iCal.icalcomponent 
out_icalcomp, GLib.Cancellable? cancellable) throws GLib.Error;
+               public bool get_object_sync (string uid, string? rid, out iCal.icalcomponent out_icalcomp, 
GLib.Cancellable? cancellable) throws GLib.Error;
                public async bool get_objects_for_uid (string uid, GLib.Cancellable? cancellable) throws 
GLib.Error;
                public bool get_objects_for_uid_sync (string uid, GLib.SList out_ecalcomps, GLib.Cancellable? 
cancellable) throws GLib.Error;
                public E.CalClientSourceType get_source_type ();
@@ -339,15 +339,6 @@ namespace E {
                MODIFIED,
                DELETED
        }
-       [CCode (cheader_filename = "libecal/libecal.h", cprefix = "E_CAL_CLIENT_ERROR_", has_type_id = false)]
-       public enum CalClientError {
-               NO_SUCH_CALENDAR,
-               OBJECT_NOT_FOUND,
-               INVALID_OBJECT,
-               UNKNOWN_USER,
-               OBJECT_ID_ALREADY_EXISTS,
-               INVALID_RANGE
-       }
        [CCode (cheader_filename = "libecal/libecal.h", cprefix = "E_CAL_CLIENT_SOURCE_TYPE_")]
        public enum CalClientSourceType {
                EVENTS,
@@ -536,6 +527,15 @@ namespace E {
                Journal,
                AnyType
        }
+       [CCode (cheader_filename = "libecal/libecal.h", cprefix = "E_CAL_CLIENT_ERROR_")]
+       public errordomain CalClientError {
+               NO_SUCH_CALENDAR,
+               OBJECT_NOT_FOUND,
+               INVALID_OBJECT,
+               UNKNOWN_USER,
+               OBJECT_ID_ALREADY_EXISTS,
+               INVALID_RANGE
+       }
        [CCode (cheader_filename = "libecal/libecal.h", instance_pos = 3.9)]
        public delegate bool CalRecurInstanceFn (E.CalComponent comp, time_t instance_start, time_t 
instance_end);
        [CCode (cheader_filename = "libecal/libecal.h")]
diff --git a/vapi/libecal-1.2/libecal-1.2.metadata b/vapi/libecal-1.2/libecal-1.2.metadata
index d68e0c8..9756b90 100644
--- a/vapi/libecal-1.2/libecal-1.2.metadata
+++ b/vapi/libecal-1.2/libecal-1.2.metadata
@@ -37,6 +37,8 @@ e_cal_client_discard_alarm_sync.cancellable nullable="1"
 
 e_cal_client_error_create transfer_ownership="1"
 
+ECalClientError errordomain="1"
+
 e_cal_client_free_ecalcomp_slist.ecalcomps type_arguments="E.CalComponent"
 e_cal_client_free_icalcomp_slist.ecalcomps type_arguments="iCal.CalComponent"
 
@@ -86,8 +88,9 @@ e_cal_client_get_object_list_as_comps_sync.ecalcomps is_out="1" value_owned="1"
 e_cal_client_get_object_list_sync.icalcomps is_out="1" value_owned="1" type_arguments="iCal.icalcomponent"
 e_cal_client_get_object_list_sync.cancellable nullable="1"
 
-e_cal_client_get_object_sync.icalcomp value_owned="1"
+e_cal_client_get_object_sync.out_icalcomp value_owned="1"
 e_cal_client_get_object_sync.cancellable nullable="1"
+e_cal_client_get_object_sync.rid nullable="1"
 
 e_cal_client_get_objects_for_uid async="1"
 e_cal_client_get_objects_for_uid.cancellable nullable="1"
diff --git a/vapi/libical.vapi b/vapi/libical.vapi
index 7950841..14ea598 100644
--- a/vapi/libical.vapi
+++ b/vapi/libical.vapi
@@ -85,9 +85,9 @@ namespace iCal {
                [CCode (cname = "icalcomponent_get_comment")]
                public unowned string get_comment ();
                [CCode (cname = "icalcomponent_get_current_component")]
-               public unowned iCal.icalcomponent get_current_component ();
+               public unowned iCal.icalcomponent? get_current_component ();
                [CCode (cname = "icalcomponent_get_current_property")]
-               public unowned iCal.icalproperty get_current_property ();
+               public unowned iCal.icalproperty? get_current_property ();
                [CCode (cname = "icalcomponent_get_description")]
                public unowned string get_description ();
                [CCode (cname = "icalcomponent_get_dtend")]
@@ -101,7 +101,7 @@ namespace iCal {
                [CCode (cname = "icalcomponent_get_duration")]
                public unowned iCal.icaldurationtype get_duration ();
                [CCode (cname = "icalcomponent_get_first_component")]
-               public unowned iCal.icalcomponent get_first_component (iCal.icalcomponent_kind kind);
+               public unowned iCal.icalcomponent? get_first_component (iCal.icalcomponent_kind kind);
                [CCode (cname = "icalcomponent_get_first_property")]
                public unowned iCal.icalproperty? get_first_property (iCal.icalproperty_kind kind);
                [CCode (cname = "icalcomponent_get_first_real_component")]
@@ -113,9 +113,9 @@ namespace iCal {
                [CCode (cname = "icalcomponent_get_method")]
                public iCal.icalproperty_method get_method ();
                [CCode (cname = "icalcomponent_get_next_component")]
-               public unowned iCal.icalcomponent get_next_component (iCal.icalcomponent_kind kind);
+               public unowned iCal.icalcomponent? get_next_component (iCal.icalcomponent_kind kind);
                [CCode (cname = "icalcomponent_get_next_property")]
-               public unowned iCal.icalproperty get_next_property (iCal.icalproperty_kind kind);
+               public unowned iCal.icalproperty? get_next_property (iCal.icalproperty_kind kind);
                [CCode (cname = "icalcomponent_get_parent")]
                public unowned iCal.icalcomponent get_parent ();
                [CCode (cname = "icalcomponent_get_recurrenceid")]
@@ -481,9 +481,9 @@ namespace iCal {
                [CCode (cname = "icalparser_get_state")]
                public iCal.icalparser_state get_state ();
                [CCode (cname = "icalparser_parse")]
-               public unowned iCal.icalcomponent parse (GLib.Callback line_gen_func);
+               public iCal.icalcomponent? parse (GLib.Callback line_gen_func);
                [CCode (cname = "icalparser_parse_string")]
-               public static unowned iCal.icalcomponent parse_string (string str);
+               public static iCal.icalcomponent? parse_string (string str);
                [CCode (cname = "icalparser_parse_value")]
                public static unowned iCal.icalvalue parse_value (iCal.icalvalue_kind kind, string str, out 
unowned iCal.icalcomponent errors);
                [CCode (cname = "icalparser_set_gen_data")]


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