[california] Export single event as an iCalendar: Bug #734155



commit 9db43223bdbe4f7ee42a560be31c52fc7c4c4a20
Author: Jim Nelson <jim yorba org>
Date:   Fri Oct 24 15:18:37 2014 -0700

    Export single event as an iCalendar: Bug #734155
    
    A single event can be saved to a .ics file from the Show Event
    popover.  This is good enough for viewing/saving event source for
    debugging, so this also closes bug #727265.

 src/application/california-application.vala |    5 ++-
 src/component/component-icalendar.vala      |   66 +++++++++++++++++++++++++-
 src/component/component.vala                |   21 +++++++++
 src/host/host-show-event.vala               |   39 +++++++++++++++-
 src/host/host.vala                          |    2 +
 5 files changed, 128 insertions(+), 5 deletions(-)
---
diff --git a/src/application/california-application.vala b/src/application/california-application.vala
index d65c5d9..6e646ee 100644
--- a/src/application/california-application.vala
+++ b/src/application/california-application.vala
@@ -121,7 +121,10 @@ public class Application : Gtk.Application {
         }
     }
     
-    private Host.MainWindow? main_window = null;
+    /**
+     * The { link MainWindow}.
+     */
+    public Host.MainWindow? main_window { get; private set; default = null; }
     
     private Application() {
         Object (application_id: ID);
diff --git a/src/component/component-icalendar.vala b/src/component/component-icalendar.vala
index a6daa9a..c629f8b 100644
--- a/src/component/component-icalendar.vala
+++ b/src/component/component-icalendar.vala
@@ -62,6 +62,8 @@ public class iCalendar : BaseObject {
     
     /**
      * VEVENTS within the VCALENDAR.
+     *
+     * Don't add { link Event} objects directly to this list.
      */
     public Gee.List<Event> events { get; private set; default = new Gee.ArrayList<Event>(); }
     
@@ -72,11 +74,69 @@ public class iCalendar : BaseObject {
     public iCal.icalcomponent ical_component { get { return _ical_component; } }
     
     /**
-     * Create an { link iCalendar} representation of the iCal component.
+     * Returns the iCal source for this { link iCalendar}.
+     */
+    public string source { get { return ical_component.as_ical_string(); } }
+    
+    /**
+     * Creates a new { link iCalendar}.
+     *
+     * As iCalendar is currently immutable, { link Instance}s must be added here.  It's possible
+     * later modifications will allow for Instances to be added and removed dynamically.
+     */
+    public iCalendar(iCal.icalproperty_method method, string? prodid, string? version, string? calscale,
+        Gee.List<Instance>? instances) {
+        this.prodid = prodid;
+        this.version = version;
+        this.calscale = calscale;
+        this.method = method;
+        
+        _ical_component = new iCal.icalcomponent(iCal.icalcomponent_kind.VCALENDAR_COMPONENT);
+        
+        if (prodid != null && !String.is_empty(prodid)) {
+            iCal.icalproperty prop = new iCal.icalproperty(iCal.icalproperty_kind.PRODID_PROPERTY);
+            prop.set_prodid(prodid);
+            _ical_component.add_property(prop);
+        }
+        
+        if (version != null && !String.is_empty(version)) {
+            iCal.icalproperty prop = new iCal.icalproperty(iCal.icalproperty_kind.VERSION_PROPERTY);
+            prop.set_version(version);
+            _ical_component.add_property(prop);
+        }
+        
+        if (calscale != null && !String.is_empty(calscale)) {
+            iCal.icalproperty prop = new iCal.icalproperty(iCal.icalproperty_kind.CALSCALE_PROPERTY);
+            prop.set_calscale(prodid);
+            _ical_component.add_property(prop);
+        }
+        
+        // METHOD is required ... not checking for NONE, if that's how the user wants to go, so
+        // be it
+        iCal.icalproperty prop = new iCal.icalproperty(iCal.icalproperty_kind.METHOD_PROPERTY);
+        prop.set_method(method);
+        _ical_component.add_property(prop);
+        
+        //
+        // contained components
+        //
+        
+        foreach (Instance instance in instances) {
+            // store copies because ownership is not being transferred
+            _ical_component.add_component(instance.ical_component.clone());
+            
+            Event? event = instance as Event;
+            if (event != null)
+                events.add(event);
+        }
+    }
+    
+    /**
+     * Create an { link iCalendar} representation of an existing iCal component.
      *
      * @throws ComponentError.INVALID if root is not a VCALENDAR.
      */
-    private iCalendar(owned iCal.icalcomponent root) throws Error {
+    private iCalendar.take(owned iCal.icalcomponent root) throws Error {
         if (root.isa() != iCal.icalcomponent_kind.VCALENDAR_COMPONENT)
             throw new ComponentError.INVALID("Not a VCALENDAR");
         
@@ -133,7 +193,7 @@ public class iCalendar : BaseObject {
         if (ical_component == null)
             throw new ComponentError.INVALID("Unable to parse VCALENDAR (%db)".printf(str.length));
         
-        return new iCalendar((owned) ical_component);
+        return new iCalendar.take((owned) ical_component);
     }
     
     public override string to_string() {
diff --git a/src/component/component.vala b/src/component/component.vala
index d1004f1..7327f2d 100644
--- a/src/component/component.vala
+++ b/src/component/component.vala
@@ -15,6 +15,23 @@
 
 namespace California.Component {
 
+/**
+ * iCalendar PRODID (Product Identifier).
+ *
+ * { link init} ''must'' be called before referencing this string.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.7.3]]
+ * and [[https://en.wikipedia.org/wiki/Formal_Public_Identifier]]
+ */
+public static string ICAL_PRODID;
+
+/**
+ * iCalendar version this application adheres to.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.7.4]]
+ */
+public const string ICAL_VERSION = "2.0";
+
 private int init_count = 0;
 
 private string TODAY;
@@ -47,6 +64,8 @@ public void init() throws Error {
     Collection.init();
     Calendar.init();
     
+    ICAL_PRODID = "-//Yorba Foundation//NONSGML California Calendar %s//EN".printf(Application.VERSION);
+    
     // Used by quick-add to indicate the user wants to create an event for today.
     // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
     TODAY = _("today").casefold();
@@ -187,6 +206,8 @@ public void terminate() {
     UNIT_WEEKDAYS = UNIT_WEEKENDS = UNIT_YEARS = UNIT_MONTHS = UNIT_WEEKS = UNIT_DAYS = UNIT_HOURS
         = UNIT_MINS = null;
     
+    ICAL_PRODID = null;
+    
     Calendar.terminate();
     Collection.terminate();
 }
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 937606a..76de1d1 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -69,6 +69,8 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         Gtk.IconSize.BUTTON);
     private Gtk.Button remove_button = new Gtk.Button.from_icon_name("user-trash-symbolic",
         Gtk.IconSize.BUTTON);
+    private Gtk.Button export_button = new Gtk.Button.from_icon_name("document-save-symbolic",
+        Gtk.IconSize.BUTTON);
     
     private Gtk.Label delete_label = new Gtk.Label(_("Delete"));
     private Gtk.Button remove_all_button = new Gtk.Button.with_mnemonic(_("A_ll Events"));
@@ -82,10 +84,12 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         Calendar.System.instance.today_changed.connect(build_display);
         
         update_button.tooltip_text = _("Edit event");
+        export_button.tooltip_text = _("Export event as .ics");
         remove_button.tooltip_text = _("Delete event");
-        update_button.relief = remove_button.relief = Gtk.ReliefStyle.NONE;
+        update_button.relief = remove_button.relief = export_button.relief = Gtk.ReliefStyle.NONE;
         
         action_box.pack_end(update_button, false, false);
+        action_box.pack_end(export_button, false, false);
         action_box.pack_end(remove_button, false, false);
         
         remove_this_button.get_style_context().add_class("destructive-action");
@@ -93,6 +97,7 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         remove_all_button.get_style_context().add_class("destructive-action");
         
         update_button.clicked.connect(on_update_button_clicked);
+        export_button.clicked.connect(on_export_button_clicked);
         remove_button.clicked.connect(on_remove_button_clicked);
         remove_all_button.clicked.connect(on_remove_all_button_clicked);
         remove_this_button.clicked.connect(on_remove_this_button_clicked);
@@ -242,6 +247,38 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         notify_user_closed();
     }
     
+    private void on_export_button_clicked() {
+        // Export as a self-contained iCalendar
+        Component.iCalendar icalendar = new Component.iCalendar(iCal.icalproperty_method.PUBLISH,
+            Component.ICAL_PRODID, Component.ICAL_VERSION, null,
+            iterate<Component.Instance>(event).to_array_list());
+        
+        Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog(_("Export event as .ics"),
+            Application.instance.main_window, Gtk.FileChooserAction.SAVE,
+            _("_Cancel"), Gtk.ResponseType.CANCEL,
+            _("E_xport"), Gtk.ResponseType.ACCEPT);
+        // This is the suggested filename for saving (exporting) an event.  The .ics file extension
+        // should always be present no matter the translation, as many systems rely on it to detect
+        // the file type
+        dialog.set_current_name(_("event.ics"));
+        dialog.do_overwrite_confirmation = true;
+        
+        dialog.show_all();
+        int response = dialog.run();
+        string filename = dialog.get_filename();
+        dialog.destroy();
+        
+        if (response != Gtk.ResponseType.ACCEPT)
+            return;
+        
+        try {
+            FileUtils.set_contents(filename, icalendar.source);
+        } catch (Error err) {
+            Application.instance.error_message(Application.instance.main_window,
+                _("Unable to export event as file: %s").printf(err.message));
+        }
+    }
+    
     private async void remove_events_async(Component.DateTime? rid,
         Backing.CalendarSource.AffectedInstances affected) {
         Gdk.Cursor? cursor = Toolkit.set_busy(this);
diff --git a/src/host/host.vala b/src/host/host.vala
index a1fada4..0e5d330 100644
--- a/src/host/host.vala
+++ b/src/host/host.vala
@@ -23,12 +23,14 @@ public void init() throws Error {
     Backing.init();
     Calendar.init();
     Toolkit.init();
+    Component.init();
 }
 
 public void terminate() {
     if (!Unit.do_terminate(ref init_count))
         return;
     
+    Component.terminate();
     View.terminate();
     Backing.terminate();
     Calendar.terminate();


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