[california] Import .ics file from the command-line: Closes bgo#725764
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Import .ics file from the command-line: Closes bgo#725764
- Date: Tue, 15 Apr 2014 20:57:42 +0000 (UTC)
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]