summaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/DataImportsInterfaces.vala489
-rw-r--r--src/plugins/ManifestWidget.vala282
-rw-r--r--src/plugins/Plugins.vala436
-rw-r--r--src/plugins/PublishingInterfaces.vala605
-rw-r--r--src/plugins/SpitInterfaces.vala367
-rw-r--r--src/plugins/StandardHostInterface.vala84
-rw-r--r--src/plugins/TransitionsInterfaces.vala300
-rw-r--r--src/plugins/mk/interfaces.mk29
-rw-r--r--src/plugins/mk/plugins.mk35
9 files changed, 2627 insertions, 0 deletions
diff --git a/src/plugins/DataImportsInterfaces.vala b/src/plugins/DataImportsInterfaces.vala
new file mode 100644
index 0000000..154503b
--- /dev/null
+++ b/src/plugins/DataImportsInterfaces.vala
@@ -0,0 +1,489 @@
+/* Copyright 2011-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.
+ */
+
+/**
+ * Shotwell Pluggable Data Imports API
+ *
+ * The Shotwell Pluggable Data Imports API allows you to write plugins that import
+ * information from other media library databases to help migration to Shotwell.
+ * The Shotwell distribution includes import support for F-Spot.
+ * To enable Shotwell to import from additional libaries, developers like you write
+ * data import plugins, dynamically-loadable shared objects that are linked into the
+ * Shotwell process at runtime. Data import plugins are just one of several kinds of
+ * plugins supported by {@link Spit}, the Shotwell Pluggable Interfaces Technology.
+ */
+namespace Spit.DataImports {
+
+/**
+ * The current version of the Pluggable Data Import API
+ */
+public const int CURRENT_INTERFACE = 0;
+
+/**
+ * The error domain for alien databases
+ */
+public errordomain DataImportError {
+ /**
+ * Indicates that the version of the external database being imported is
+ * not supported by this version of the plugin.
+ *
+ * This occurs for example when trying to import an F-Spot database that
+ * has a version that is more recent than what the current plugin supports.
+ */
+ UNSUPPORTED_VERSION
+}
+
+/**
+ * Represents a module that is able to import data from a specific database format.
+ *
+ * Developers of data import plugins provide a class that implements this interface. At
+ * any given time, only one DataImporter can be running. When a data importer is running, it
+ * has exclusive use of the shared user-interface and
+ * configuration services provided by the {@link PluginHost}. Data importers are created in
+ * a non-running state and do not begin running until start( ) is invoked. Data importers
+ * run until stop( ) is invoked.
+ */
+public interface DataImporter : GLib.Object {
+ /**
+ * Returns a {@link Service} object describing the service to which this connects.
+ */
+ public abstract Service get_service();
+
+ /**
+ * Makes this data importer enter the running state and endows it with exclusive access
+ * to the shared services provided by the {@link PluginHost}. Through the host’s interface,
+ * this data importer can install user interface panes and query configuration information.
+ */
+ public abstract void start();
+
+ /**
+ * Returns true if this data importer is in the running state; false otherwise.
+ */
+ public abstract bool is_running();
+
+ /**
+ * Causes this data importer to enter a non-running state. This data importer should stop all
+ * data access operations and cease use of the shared services provided by the {@link PluginHost}.
+ */
+ public abstract void stop();
+
+ /**
+ * Causes this data importer to enter start the import of a library.
+ */
+ public abstract void on_library_selected(ImportableLibrary library);
+
+ /**
+ * Causes this data importer to enter start the import of a library file.
+ */
+ public abstract void on_file_selected(File file);
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Represents a library of importable media items.
+ *
+ * Developers of data import plugins provide a class that implements this interface.
+ */
+public interface ImportableLibrary : GLib.Object {
+ public abstract string get_display_name();
+}
+
+/**
+ * Represents an importable media item such as a photo or a video file.
+ *
+ * Developers of data import plugins provide a class that implements this interface.
+ */
+public interface ImportableMediaItem : GLib.Object {
+ public abstract ImportableTag[] get_tags();
+
+ public abstract ImportableEvent? get_event();
+
+ public abstract ImportableRating get_rating();
+
+ public abstract string? get_title();
+
+ public abstract string get_folder_path();
+
+ public abstract string get_filename();
+}
+
+/**
+ * Represents an importable tag.
+ *
+ * Developers of data import plugins provide a class that implements this interface.
+ */
+public interface ImportableTag : GLib.Object {
+ public abstract string get_name();
+
+ public abstract ImportableTag? get_parent();
+}
+
+/**
+ * Represents an importable event.
+ *
+ * Developers of data import plugins provide a class that implements this interface.
+ */
+public interface ImportableEvent : GLib.Object {
+ public abstract string get_name();
+}
+
+/**
+ * Represents an importable rating value.
+ *
+ * Developers of data import plugins provide a class that implements this interface.
+ * Note that the value returned by the get_value method should be a value between
+ * 1 and 5, unless the rating object is unrated or rejected, in which case the
+ * value is unspecified.
+ */
+public interface ImportableRating : GLib.Object {
+ public abstract bool is_unrated();
+
+ public abstract bool is_rejected();
+
+ public abstract int get_value();
+}
+
+/**
+ * Encapsulates a pane that can be installed in the on-screen import dialog box to
+ * communicate status to and to get information from the user.
+ *
+ */
+public interface DialogPane : GLib.Object {
+
+ /**
+ * Describes how the on-screen publishing dialog box should look and behave when an associated
+ * pane is installed in the on-screen publishing dialog box.
+ */
+ public enum GeometryOptions {
+
+ /**
+ * When the associated pane is installed, the on-screen publishing dialog box will be
+ * sized normally and will not allow the user to change its size.
+ */
+ NONE = 0,
+
+ /**
+ * If this bit is set, when the associated pane is installed, the on-screen publishing
+ * dialog box will grow to a larger size.
+ */
+ EXTENDED_SIZE = 1 << 0,
+
+ /**
+ * If this bit is set, when the associated pane is installed, the on-screen publishing
+ * dialog box will allow the user to change its size.
+ */
+ RESIZABLE = 1 << 1,
+
+ /**
+ * If this bit is set, when the associated pane is installed, the on-screen publishing
+ * dialog box will grow to accommodate a full-width 1024 pixel web page. If both
+ * EXTENDED_SIZE and COLOSSAL_SIZE are set, EXTENDED_SIZE takes precedence.
+ */
+ COLOSSAL_SIZE = 1 << 2;
+ }
+
+ /**
+ * Returns the Gtk.Widget that is this pane's on-screen representation.
+ */
+ public abstract Gtk.Widget get_widget();
+
+ /**
+ * Returns a {@link GeometryOptions} bitfield describing how the on-screen publishing dialog
+ * box should look and behave when this pane is installed.
+ */
+ public abstract GeometryOptions get_preferred_geometry();
+
+ /**
+ * Invoked automatically by Shotwell when this pane has been installed into the on-screen
+ * publishing dialog box and become visible to the user.
+ */
+ public abstract void on_pane_installed();
+
+ /**
+ * Invoked automatically by Shotwell when this pane has been removed from the on-screen
+ * publishing dialog box and is no longer visible to the user.
+ */
+ public abstract void on_pane_uninstalled();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Called by the data imports system at the end of an import batch to report
+ * to the plugin the number of items that were really imported. This enables
+ * the plugin to display a final message to the user. However, the plugin
+ * should not rely on this callback being called in order to clean up.
+ */
+public delegate void ImportedItemsCountCallback(int imported_items_count);
+
+/**
+ * Manages and provides services for data import plugins.
+ *
+ * Implemented inside Shotwell, the PluginHost provides an interface through which the
+ * developers of data import plugins can query and make changes to the import
+ * environment. Plugins can use the services of the PluginHost only when their
+ * {@link DataImporter} is in the running state. This ensures that non-running data importers
+ * don’t destructively interfere with the actively running importer.
+ */
+public interface PluginHost : GLib.Object, Spit.HostInterface {
+
+ /**
+ * Specifies the label text on the push button control that appears in the
+ * lower-right-hand corner of the on-screen publishing dialog box.
+ */
+ public enum ButtonMode {
+ CLOSE = 0,
+ CANCEL = 1
+ }
+
+ /**
+ * Notifies the user that an unrecoverable import error has occurred and halts
+ * the import process.
+ *
+ * @param err An error object that describes the kind of error that occurred.
+ */
+ public abstract void post_error(Error err);
+
+ /**
+ * Notifies the user that an unrecoverable import error has occurred and halts
+ * the import process.
+ *
+ * @param msg A message that describes the kind of error that occurred.
+ */
+ public abstract void post_error_message(string msg);
+
+ /**
+ * Starts the import process.
+ *
+ * Calling this method starts the import activity for this host.
+ */
+ public abstract void start_importing();
+
+ /**
+ * Halts the import process.
+ *
+ * Calling this method stops all import activity and hides the on-screen import
+ * dialog box.
+ */
+ public abstract void stop_importing();
+
+ /**
+ * Returns a reference to the {@link DataImporter} object that this is currently hosting.
+ */
+ public abstract DataImporter get_data_importer();
+
+ /**
+ * Attempts to install a pane in the on-screen data import dialog box, making the pane visible
+ * and allowing it to interact with the user.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param pane the pane to install
+ *
+ * @param mode allows you to set the text displayed on the close/cancel button in the
+ * lower-right-hand corner of the on-screen data import dialog box when pane is installed.
+ * If mode is ButtonMode.CLOSE, the button will have the title "Close." If mode is
+ * ButtonMode.CANCEL, the button will be titled "Cancel." You should set mode depending on
+ * whether a cancellable action is in progress. For example, if your importer is in the
+ * middle of processing 3 of 8 videos, then mode should be ButtonMode.CANCEL. However, if
+ * the processing operation has completed and the success pane is displayed, then mode
+ * should be ButtonMode.CLOSE, because all cancellable actions have already
+ * occurred.
+ */
+ public abstract void install_dialog_pane(Spit.DataImports.DialogPane pane,
+ ButtonMode mode = ButtonMode.CANCEL);
+
+ /**
+ * Attempts to install a pane in the on-screen data import dialog box that contains
+ * static text.
+ *
+ * The text appears centered in the data import dialog box and is drawn in
+ * the system font. This is a convenience method only; similar results could be
+ * achieved by manually constructing a Gtk.Label widget, wrapping it inside a
+ * {@link DialogPane}, and installing it manually with a call to
+ * install_dialog_pane( ). To provide visual consistency across data import services,
+ * however, always use this convenience method instead of constructing label panes when
+ * you need to display static text to the user.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param message the text to show in the pane
+ *
+ * @param mode allows you to set the text displayed on the close/cancel button in the
+ * lower-right-hand corner of the on-screen data import dialog box when pane is installed.
+ * If mode is ButtonMode.CLOSE, the button will have the title "Close." If mode is
+ * ButtonMode.CANCEL, the button will be titled "Cancel." You should set mode depending on
+ * whether a cancellable action is in progress. For example, if your importer is in the
+ * middle of processing 3 of 8 videos, then mode should be ButtonMode.CANCEL. However, if
+ * the processing operation has completed and the success pane is displayed, then mode
+ * should be ButtonMode.CLOSE, because all cancellable actions have already
+ * occurred.
+ */
+ public abstract void install_static_message_pane(string message,
+ ButtonMode mode = ButtonMode.CANCEL);
+
+ /**
+ * Attempts to install a library selection pane that presents a list of
+ * discovered libraries to the user.
+ *
+ * When the user clicks the “OK” button, you’ll be notified of the user’s action through
+ * the 'on_library_selected' callback if a discovered library was selected or through
+ * the 'on_file_selected' callback if a file was selected.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param welcome_message the text to be displayed above the list of discovered
+ * libraries.
+ *
+ * @param discovered_libraries the list of importable libraries that the plugin
+ * has discovered in well known locations.
+ *
+ * @param file_select_label the label to display for the file selection
+ * option. If this label is null, the
+ * user will not be presented with a file selection option.
+ */
+ public abstract void install_library_selection_pane(
+ string welcome_message,
+ ImportableLibrary[] discovered_libraries,
+ string? file_select_label
+ );
+
+ /**
+ * Attempts to install a progress pane that provides the user with feedback
+ * on import preparation.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param message the text to be displayed above the progress bar.
+ */
+ public abstract void install_import_progress_pane(
+ string message
+ );
+
+ /**
+ * Update the progress bar installed by install_import_progress_pane.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param progress a value between 0.0 and 1.0 identifying progress for the
+ * plugin.
+ *
+ * @param progress_label the text to be displayed below the progress bar. If that
+ * parameter is null, the message will be left unchanged.
+ */
+ public abstract void update_import_progress_pane(
+ double progress,
+ string? progress_message = null
+ );
+
+ /**
+ * Sends an importable media item to the host in order to prepare it for import
+ * and update the progress bar installed by install_import_progress_pane.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param item the importable media item to prepare for import.
+ *
+ * @param progress a value between 0.0 and 1.0 identifying progress for the
+ * plugin.
+ *
+ * @param host_progress_delta the amount of progress the host should update
+ * the progress bar during import preparation. Plugins should ensure that
+ * a proportion of progress for each media item is set aside for the host
+ * in oder to ensure a smoother update to the progress bar.
+ *
+ * @param progress_message the text to be displayed below the progress bar. If that
+ * parameter is null, the message will be left unchanged.
+ */
+ public abstract void prepare_media_items_for_import(
+ ImportableMediaItem[] items,
+ double progress,
+ double host_progress_delta = 0.0,
+ string? progress_message = null
+ );
+
+ /**
+ * Finalize the import sequence for the plugin. This tells the host that
+ * all media items have been processed and that the plugin has finished all
+ * import work. Once this method has been called, all resources used by the
+ * plugin for import should be released and the plugin should be back to the
+ * state it had just after running the start method. The host will then display
+ * the final message and show progress as fully complete. In a standard import
+ * scenario, the user is expected to click the Close button to dismiss the
+ * dialog. On first run, the host may call the LibrarySelectedCallback again
+ * to import another library handled by the same plugin.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param finalize_message the text to be displayed below the progress bar. If that
+ * parameter is null, the message will be left unchanged.
+ */
+ public abstract void finalize_import(
+ ImportedItemsCountCallback report_imported_items_count,
+ string? finalize_message = null
+ );
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Describes the features and capabilities of a data import service.
+ *
+ * Developers of data import plugins provide a class that implements this interface.
+ */
+public interface Service : Object, Spit.Pluggable {
+ /**
+ * A factory method that instantiates and returns a new {@link DataImporter} object
+ * that this Service describes.
+ */
+ public abstract Spit.DataImports.DataImporter create_data_importer(Spit.DataImports.PluginHost host);
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+}
+
diff --git a/src/plugins/ManifestWidget.vala b/src/plugins/ManifestWidget.vala
new file mode 100644
index 0000000..54f2e56
--- /dev/null
+++ b/src/plugins/ManifestWidget.vala
@@ -0,0 +1,282 @@
+/* Copyright 2011-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 Plugins {
+
+public class ManifestWidgetMediator {
+ public Gtk.Widget widget {
+ get {
+ return builder.get_object("plugin-manifest") as Gtk.Widget;
+ }
+ }
+
+ private Gtk.Button about_button {
+ get {
+ return builder.get_object("about-plugin-button") as Gtk.Button;
+ }
+ }
+
+ private Gtk.ScrolledWindow list_bin {
+ get {
+ return builder.get_object("plugin-list-scrolled-window") as Gtk.ScrolledWindow;
+ }
+ }
+
+ private Gtk.Builder builder = AppWindow.create_builder();
+ private ManifestListView list = new ManifestListView();
+
+ public ManifestWidgetMediator() {
+ list_bin.add_with_viewport(list);
+
+ about_button.clicked.connect(on_about);
+ list.get_selection().changed.connect(on_selection_changed);
+
+ set_about_button_sensitivity();
+ }
+
+ ~ManifestWidgetMediator() {
+ about_button.clicked.disconnect(on_about);
+ list.get_selection().changed.disconnect(on_selection_changed);
+ }
+
+ private void on_about() {
+ string[] ids = list.get_selected_ids();
+ if (ids.length == 0)
+ return;
+
+ string id = ids[0];
+
+ Spit.PluggableInfo info = Spit.PluggableInfo();
+ if (!get_pluggable_info(id, ref info)) {
+ warning("Unable to retrieve information for plugin %s", id);
+
+ return;
+ }
+
+ // prepare authors names (which are comma-delimited by the plugin) for the about box
+ // (which wants an array of names)
+ string[]? authors = null;
+ if (info.authors != null) {
+ string[] split = info.authors.split(",");
+ for (int ctr = 0; ctr < split.length; ctr++) {
+ string stripped = split[ctr].strip();
+ if (!is_string_empty(stripped)) {
+ if (authors == null)
+ authors = new string[0];
+
+ authors += stripped;
+ }
+ }
+ }
+
+ Gtk.AboutDialog about_dialog = new Gtk.AboutDialog();
+ about_dialog.authors = authors;
+ about_dialog.comments = info.brief_description;
+ about_dialog.copyright = info.copyright;
+ about_dialog.license = info.license;
+ about_dialog.wrap_license = info.is_license_wordwrapped;
+ about_dialog.logo = (info.icons != null && info.icons.length > 0) ? info.icons[0] :
+ Resources.get_icon(Resources.ICON_GENERIC_PLUGIN);
+ about_dialog.program_name = get_pluggable_name(id);
+ about_dialog.translator_credits = info.translators;
+ about_dialog.version = info.version;
+ about_dialog.website = info.website_url;
+ about_dialog.website_label = info.website_name;
+
+ about_dialog.run();
+
+ about_dialog.destroy();
+ }
+
+ private void on_selection_changed() {
+ set_about_button_sensitivity();
+ }
+
+ private void set_about_button_sensitivity() {
+ // have to get the array and then get its length rather than do so in one call due to a
+ // bug in Vala 0.10:
+ // list.get_selected_ids().length -> uninitialized value
+ // this appears to be fixed in Vala 0.11
+ string[] ids = list.get_selected_ids();
+ about_button.sensitive = (ids.length == 1);
+ }
+}
+
+private class ManifestListView : Gtk.TreeView {
+ private const int ICON_SIZE = 24;
+ private const int ICON_X_PADDING = 6;
+ private const int ICON_Y_PADDING = 2;
+
+ private enum Column {
+ ENABLED,
+ CAN_ENABLE,
+ ICON,
+ NAME,
+ ID,
+ N_COLUMNS
+ }
+
+ private Gtk.TreeStore store = new Gtk.TreeStore(Column.N_COLUMNS,
+ typeof(bool), // ENABLED
+ typeof(bool), // CAN_ENABLE
+ typeof(Gdk.Pixbuf), // ICON
+ typeof(string), // NAME
+ typeof(string) // ID
+ );
+
+ public ManifestListView() {
+ set_model(store);
+
+ Gtk.CellRendererToggle checkbox_renderer = new Gtk.CellRendererToggle();
+ checkbox_renderer.radio = false;
+ checkbox_renderer.activatable = true;
+
+ Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf();
+ icon_renderer.stock_size = Gtk.IconSize.MENU;
+ icon_renderer.xpad = ICON_X_PADDING;
+ icon_renderer.ypad = ICON_Y_PADDING;
+
+ Gtk.CellRendererText text_renderer = new Gtk.CellRendererText();
+
+ Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
+ column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE);
+ column.pack_start(checkbox_renderer, false);
+ column.pack_start(icon_renderer, false);
+ column.pack_end(text_renderer, true);
+
+ column.add_attribute(checkbox_renderer, "active", Column.ENABLED);
+ column.add_attribute(checkbox_renderer, "visible", Column.CAN_ENABLE);
+ column.add_attribute(icon_renderer, "pixbuf", Column.ICON);
+ column.add_attribute(text_renderer, "text", Column.NAME);
+
+ append_column(column);
+
+ set_headers_visible(false);
+ set_enable_search(false);
+ set_rules_hint(true);
+ set_show_expanders(true);
+ set_reorderable(false);
+ set_enable_tree_lines(false);
+ set_grid_lines(Gtk.TreeViewGridLines.NONE);
+ get_selection().set_mode(Gtk.SelectionMode.BROWSE);
+
+ Gtk.IconTheme icon_theme = Resources.get_icon_theme_engine();
+
+ // create a list of plugins (sorted by name) that are separated by extension points (sorted
+ // by name)
+ foreach (ExtensionPoint extension_point in get_extension_points(compare_extension_point_names)) {
+ Gtk.TreeIter category_iter;
+ store.append(out category_iter, null);
+
+ Gdk.Pixbuf? icon = null;
+ if (extension_point.icon_name != null) {
+ Gtk.IconInfo? icon_info = icon_theme.lookup_by_gicon(
+ new ThemedIcon(extension_point.icon_name), ICON_SIZE, 0);
+ if (icon_info != null) {
+ try {
+ icon = icon_info.load_icon();
+ } catch (Error err) {
+ warning("Unable to load icon %s: %s", extension_point.icon_name, err.message);
+ }
+ }
+ }
+
+ store.set(category_iter, Column.NAME, extension_point.name, Column.CAN_ENABLE, false,
+ Column.ICON, icon);
+
+ Gee.Collection<Spit.Pluggable> pluggables = get_pluggables_for_type(
+ extension_point.pluggable_type, compare_pluggable_names, true);
+ foreach (Spit.Pluggable pluggable in pluggables) {
+ bool enabled;
+ if (!get_pluggable_enabled(pluggable.get_id(), out enabled))
+ continue;
+
+ Spit.PluggableInfo info = Spit.PluggableInfo();
+ pluggable.get_info(ref info);
+
+ icon = (info.icons != null && info.icons.length > 0)
+ ? info.icons[0]
+ : Resources.get_icon(Resources.ICON_GENERIC_PLUGIN, ICON_SIZE);
+
+ Gtk.TreeIter plugin_iter;
+ store.append(out plugin_iter, category_iter);
+
+ store.set(plugin_iter, Column.ENABLED, enabled, Column.NAME, pluggable.get_pluggable_name(),
+ Column.ID, pluggable.get_id(), Column.CAN_ENABLE, true, Column.ICON, icon);
+ }
+ }
+
+ expand_all();
+ }
+
+ public string[] get_selected_ids() {
+ string[] ids = new string[0];
+
+ List<Gtk.TreePath> selected = get_selection().get_selected_rows(null);
+ foreach (Gtk.TreePath path in selected) {
+ Gtk.TreeIter iter;
+ string? id = get_id_at_path(path, out iter);
+ if (id != null)
+ ids += id;
+ }
+
+ return ids;
+ }
+
+ private string? get_id_at_path(Gtk.TreePath path, out Gtk.TreeIter iter) {
+ if (!store.get_iter(out iter, path))
+ return null;
+
+ unowned string id;
+ store.get(iter, Column.ID, out id);
+
+ return id;
+ }
+
+ // Because we want each row to left-align and not for each column to line up in a grid
+ // (otherwise the checkboxes -- hidden or not -- would cause the rest of the row to line up
+ // along the icon's left edge), we put all the renderers into a single column. However, the
+ // checkbox renderer then triggers its "toggle" signal any time the row is single-clicked,
+ // whether or not the actual checkbox hit-tests.
+ //
+ // The only way found to work around this is to capture the button-down event and do our own
+ // hit-testing.
+ public override bool button_press_event(Gdk.EventButton event) {
+ Gtk.TreePath path;
+ Gtk.TreeViewColumn col;
+ int cellx;
+ int celly;
+ if (!get_path_at_pos((int) event.x, (int) event.y, out path, out col, out cellx,
+ out celly))
+ return base.button_press_event(event);
+
+ // Perform custom hit testing as described above. The first cell in the column is offset
+ // from the left edge by whatever size the group description icon is allocated (including
+ // padding).
+ if (cellx < (ICON_SIZE + ICON_X_PADDING) || cellx > (2 * (ICON_X_PADDING + ICON_SIZE)))
+ return base.button_press_event(event);
+
+ Gtk.TreeIter iter;
+ string? id = get_id_at_path(path, out iter);
+ if (id == null)
+ return base.button_press_event(event);
+
+ bool enabled;
+ if (!get_pluggable_enabled(id, out enabled))
+ return base.button_press_event(event);
+
+ // toggle and set
+ enabled = !enabled;
+ set_pluggable_enabled(id, enabled);
+
+ store.set(iter, Column.ENABLED, enabled);
+
+ return true;
+ }
+}
+
+}
+
diff --git a/src/plugins/Plugins.vala b/src/plugins/Plugins.vala
new file mode 100644
index 0000000..d0f9185
--- /dev/null
+++ b/src/plugins/Plugins.vala
@@ -0,0 +1,436 @@
+/* Copyright 2011-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 Plugins {
+
+// GModule doesn't have a truly generic way to determine if a file is a shared library by extension,
+// so these are hard-coded
+private const string[] SHARED_LIB_EXTS = { "so", "la" };
+
+// Although not expecting this system to last very long, these ranges declare what versions of this
+// interface are supported by the current implementation.
+private const int MIN_SPIT_INTERFACE = 0;
+private const int MAX_SPIT_INTERFACE = 0;
+
+public class ExtensionPoint {
+ public GLib.Type pluggable_type { get; private set; }
+ // name is user-visible
+ public string name { get; private set; }
+ public string? icon_name { get; private set; }
+ public string[]? core_ids { get; private set; }
+
+ public ExtensionPoint(Type pluggable_type, string name, string? icon_name, string[]? core_ids) {
+ this.pluggable_type = pluggable_type;
+ this.name = name;
+ this.icon_name = icon_name;
+ this.core_ids = core_ids;
+ }
+}
+
+private class ModuleRep {
+ public File file;
+ public Module? module;
+ public Spit.Module? spit_module = null;
+ public int spit_interface = Spit.UNSUPPORTED_INTERFACE;
+ public string? id = null;
+
+ private ModuleRep(File file) {
+ this.file = file;
+
+ module = Module.open(file.get_path(), ModuleFlags.BIND_LAZY);
+ }
+
+ ~ModuleRep() {
+ // ensure that the Spit.Module is destroyed before the GLib.Module
+ spit_module = null;
+ }
+
+ // Have to use this funky static factory because GModule is a compact class and has no copy
+ // constructor. The handle must be kept open for the lifetime of the application (or until
+ // the module is ready to be discarded), as dropping the reference will unload the binary.
+ public static ModuleRep? open(File file) {
+ ModuleRep module_rep = new ModuleRep(file);
+
+ return (module_rep.module != null) ? module_rep : null;
+ }
+}
+
+private class PluggableRep {
+ public Spit.Pluggable pluggable { get; private set; }
+ public string id { get; private set; }
+ public bool is_core { get; private set; default = false; }
+ public bool activated { get; private set; default = false; }
+
+ private bool enabled = false;
+
+ // Note that creating a PluggableRep does not activate it.
+ public PluggableRep(Spit.Pluggable pluggable) {
+ this.pluggable = pluggable;
+ id = pluggable.get_id();
+ }
+
+ public void activate() {
+ // determine if a core pluggable (which is only known after all the extension points
+ // register themselves)
+ is_core = is_core_pluggable(pluggable);
+
+ FuzzyPropertyState saved_state = Config.Facade.get_instance().is_plugin_enabled(id);
+ enabled = ((is_core && (saved_state != FuzzyPropertyState.DISABLED)) ||
+ (!is_core && (saved_state == FuzzyPropertyState.ENABLED)));
+
+ // inform the plugin of its activation state
+ pluggable.activation(enabled);
+
+ activated = true;
+ }
+
+ public bool is_enabled() {
+ return enabled;
+ }
+
+ // Returns true if value changed, false otherwise
+ public bool set_enabled(bool enabled) {
+ if (enabled == this.enabled)
+ return false;
+
+ this.enabled = enabled;
+ Config.Facade.get_instance().set_plugin_enabled(id, enabled);
+ pluggable.activation(enabled);
+
+ return true;
+ }
+}
+
+private File[] search_dirs;
+private Gee.HashMap<string, ModuleRep> module_table;
+private Gee.HashMap<string, PluggableRep> pluggable_table;
+private Gee.HashMap<Type, ExtensionPoint> extension_points;
+private Gee.HashSet<string> core_ids;
+
+public void init() throws Error {
+ search_dirs = new File[0];
+ search_dirs += AppDirs.get_user_plugins_dir();
+ search_dirs += AppDirs.get_system_plugins_dir();
+
+ module_table = new Gee.HashMap<string, ModuleRep>();
+ pluggable_table = new Gee.HashMap<string, PluggableRep>();
+ extension_points = new Gee.HashMap<Type, ExtensionPoint>();
+ core_ids = new Gee.HashSet<string>();
+
+ // do this after constructing member variables so accessors don't blow up if GModule isn't
+ // supported
+ if (!Module.supported()) {
+ warning("Plugins not support: GModule not supported on this platform.");
+
+ return;
+ }
+
+ foreach (File dir in search_dirs) {
+ try {
+ search_for_plugins(dir);
+ } catch (Error err) {
+ debug("Unable to search directory %s for plugins: %s", dir.get_path(), err.message);
+ }
+ }
+}
+
+public void terminate() {
+ search_dirs = null;
+ pluggable_table = null;
+ module_table = null;
+ extension_points = null;
+ core_ids = null;
+}
+
+public class Notifier {
+ private static Notifier? instance = null;
+
+ public signal void pluggable_activation(Spit.Pluggable pluggable, bool enabled);
+
+ private Notifier() {
+ }
+
+ public static Notifier get_instance() {
+ if (instance == null)
+ instance = new Notifier();
+
+ return instance;
+ }
+}
+
+public void register_extension_point(Type type, string name, string? icon_name, string[]? core_ids) {
+ // if this assertion triggers, it means this extension point has already registered
+ assert(!extension_points.has_key(type));
+
+ extension_points.set(type, new ExtensionPoint(type, name, icon_name, core_ids));
+
+ // add core IDs to master list
+ if (core_ids != null) {
+ foreach (string core_id in core_ids)
+ Plugins.core_ids.add(core_id);
+ }
+
+ // activate all the pluggables for this extension point
+ foreach (PluggableRep pluggable_rep in pluggable_table.values) {
+ if (!pluggable_rep.pluggable.get_type().is_a(type))
+ continue;
+
+ pluggable_rep.activate();
+ Notifier.get_instance().pluggable_activation(pluggable_rep.pluggable, pluggable_rep.is_enabled());
+ }
+}
+
+public Gee.Collection<Spit.Pluggable> get_pluggables(bool include_disabled = false) {
+ Gee.Collection<Spit.Pluggable> all = new Gee.HashSet<Spit.Pluggable>();
+ foreach (PluggableRep pluggable_rep in pluggable_table.values) {
+ if (pluggable_rep.activated && (include_disabled || pluggable_rep.is_enabled()))
+ all.add(pluggable_rep.pluggable);
+ }
+
+ return all;
+}
+
+public bool is_core_pluggable(Spit.Pluggable pluggable) {
+ return core_ids.contains(pluggable.get_id());
+}
+
+private ModuleRep? get_module_for_pluggable(Spit.Pluggable needle) {
+ foreach (ModuleRep module_rep in module_table.values) {
+ Spit.Pluggable[]? pluggables = module_rep.spit_module.get_pluggables();
+ if (pluggables != null) {
+ foreach (Spit.Pluggable pluggable in pluggables) {
+ if (pluggable == needle)
+ return module_rep;
+ }
+ }
+ }
+
+ return null;
+}
+
+public string? get_pluggable_module_id(Spit.Pluggable needle) {
+ ModuleRep? module_rep = get_module_for_pluggable(needle);
+
+ return (module_rep != null) ? module_rep.spit_module.get_id() : null;
+}
+
+public Gee.Collection<ExtensionPoint> get_extension_points(owned CompareDataFunc? compare_func = null) {
+ Gee.Collection<ExtensionPoint> sorted = new Gee.TreeSet<ExtensionPoint>((owned) compare_func);
+ sorted.add_all(extension_points.values);
+
+ return sorted;
+}
+
+public Gee.Collection<Spit.Pluggable> get_pluggables_for_type(Type type,
+ owned CompareDataFunc? compare_func = null, bool include_disabled = false) {
+ // if this triggers it means the extension point didn't register itself at init() time
+ assert(extension_points.has_key(type));
+
+ Gee.Collection<Spit.Pluggable> for_type = new Gee.TreeSet<Spit.Pluggable>((owned) compare_func);
+ foreach (PluggableRep pluggable_rep in pluggable_table.values) {
+ if (pluggable_rep.activated
+ && pluggable_rep.pluggable.get_type().is_a(type)
+ && (include_disabled || pluggable_rep.is_enabled())) {
+ for_type.add(pluggable_rep.pluggable);
+ }
+ }
+
+ return for_type;
+}
+
+public string? get_pluggable_name(string id) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+
+ return (pluggable_rep != null && pluggable_rep.activated)
+ ? pluggable_rep.pluggable.get_pluggable_name() : null;
+}
+
+public bool get_pluggable_info(string id, ref Spit.PluggableInfo info) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+ if (pluggable_rep == null || !pluggable_rep.activated)
+ return false;
+
+ pluggable_rep.pluggable.get_info(ref info);
+
+ return true;
+}
+
+public bool get_pluggable_enabled(string id, out bool enabled) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+ if (pluggable_rep == null || !pluggable_rep.activated) {
+ enabled = false;
+
+ return false;
+ }
+
+ enabled = pluggable_rep.is_enabled();
+
+ return true;
+}
+
+public void set_pluggable_enabled(string id, bool enabled) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+ if (pluggable_rep == null || !pluggable_rep.activated)
+ return;
+
+ if (pluggable_rep.set_enabled(enabled))
+ Notifier.get_instance().pluggable_activation(pluggable_rep.pluggable, enabled);
+}
+
+public File get_pluggable_module_file(Spit.Pluggable pluggable) {
+ ModuleRep? module_rep = get_module_for_pluggable(pluggable);
+
+ return (module_rep != null) ? module_rep.file : null;
+}
+
+public int compare_pluggable_names(void *a, void *b) {
+ Spit.Pluggable *apluggable = (Spit.Pluggable *) a;
+ Spit.Pluggable *bpluggable = (Spit.Pluggable *) b;
+
+ return apluggable->get_pluggable_name().collate(bpluggable->get_pluggable_name());
+}
+
+public int compare_extension_point_names(void *a, void *b) {
+ ExtensionPoint *apoint = (ExtensionPoint *) a;
+ ExtensionPoint *bpoint = (ExtensionPoint *) b;
+
+ return apoint->name.collate(bpoint->name);
+}
+
+private bool is_shared_library(File file) {
+ string name, ext;
+ disassemble_filename(file.get_basename(), out name, out ext);
+
+ foreach (string shared_ext in SHARED_LIB_EXTS) {
+ if (ext == shared_ext)
+ return true;
+ }
+
+ return false;
+}
+
+private void search_for_plugins(File dir) throws Error {
+ debug("Searching %s for plugins ...", dir.get_path());
+
+ // build a set of module names sans file extension ... this is to deal with the question of
+ // .so vs. .la existing in the same directory (and letting GModule deal with the problem)
+ FileEnumerator enumerator = dir.enumerate_children(Util.FILE_ATTRIBUTES,
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+ for (;;) {
+ FileInfo? info = enumerator.next_file(null);
+ if (info == null)
+ break;
+
+ if (info.get_is_hidden())
+ continue;
+
+ File file = dir.get_child(info.get_name());
+
+ switch (info.get_file_type()) {
+ case FileType.DIRECTORY:
+ try {
+ search_for_plugins(file);
+ } catch (Error err) {
+ warning("Unable to search directory %s for plugins: %s", file.get_path(), err.message);
+ }
+ break;
+
+ case FileType.REGULAR:
+ if (is_shared_library(file))
+ load_module(file);
+ break;
+
+ default:
+ // ignored
+ break;
+ }
+ }
+}
+
+private void load_module(File file) {
+ ModuleRep? module_rep = ModuleRep.open(file);
+ if (module_rep == null) {
+ critical("Unable to load module %s: %s", file.get_path(), Module.error());
+
+ return;
+ }
+
+ // look for the well-known entry point
+ void *entry;
+ if (!module_rep.module.symbol(Spit.ENTRY_POINT_NAME, out entry)) {
+ critical("Unable to load module %s: well-known entry point %s not found", file.get_path(),
+ Spit.ENTRY_POINT_NAME);
+
+ return;
+ }
+
+ Spit.EntryPoint spit_entry_point = (Spit.EntryPoint) entry;
+
+ assert(MIN_SPIT_INTERFACE <= Spit.CURRENT_INTERFACE && Spit.CURRENT_INTERFACE <= MAX_SPIT_INTERFACE);
+ Spit.EntryPointParams params = Spit.EntryPointParams();
+ params.host_min_spit_interface = MIN_SPIT_INTERFACE;
+ params.host_max_spit_interface = MAX_SPIT_INTERFACE;
+ params.module_spit_interface = Spit.UNSUPPORTED_INTERFACE;
+ params.module_file = file;
+
+ module_rep.spit_module = spit_entry_point(&params);
+ if (params.module_spit_interface == Spit.UNSUPPORTED_INTERFACE) {
+ critical("Unable to load module %s: module reports no support for SPIT interfaces %d to %d",
+ file.get_path(), MIN_SPIT_INTERFACE, MAX_SPIT_INTERFACE);
+
+ return;
+ }
+
+ if (params.module_spit_interface < MIN_SPIT_INTERFACE || params.module_spit_interface > MAX_SPIT_INTERFACE) {
+ critical("Unable to load module %s: module reports unsupported SPIT version %d (out of range %d to %d)",
+ file.get_path(), module_rep.spit_interface, MIN_SPIT_INTERFACE, MAX_SPIT_INTERFACE);
+
+ return;
+ }
+
+ module_rep.spit_interface = params.module_spit_interface;
+
+ // verify type (as best as possible; still potential to segfault inside GType here)
+ if (!(module_rep.spit_module is Spit.Module))
+ module_rep.spit_module = null;
+
+ if (module_rep.spit_module == null) {
+ critical("Unable to load module %s (SPIT %d): no spit module returned", file.get_path(),
+ module_rep.spit_interface);
+
+ return;
+ }
+
+ // if module has already been loaded, drop this one (search path is set up to load user-installed
+ // binaries prior to system binaries)
+ module_rep.id = prepare_input_text(module_rep.spit_module.get_id(), PrepareInputTextOptions.DEFAULT, -1);
+ if (module_rep.id == null) {
+ critical("Unable to load module %s (SPIT %d): invalid or empty module name",
+ file.get_path(), module_rep.spit_interface);
+
+ return;
+ }
+
+ if (module_table.has_key(module_rep.id)) {
+ critical("Not loading module %s (SPIT %d): module with name \"%s\" already loaded",
+ file.get_path(), module_rep.spit_interface, module_rep.id);
+
+ return;
+ }
+
+ debug("Loaded SPIT module \"%s %s\" (%s) [%s]", module_rep.spit_module.get_module_name(),
+ module_rep.spit_module.get_version(), module_rep.id, file.get_path());
+
+ // stash in module table by their ID
+ module_table.set(module_rep.id, module_rep);
+
+ // stash pluggables in pluggable table by their ID
+ foreach (Spit.Pluggable pluggable in module_rep.spit_module.get_pluggables())
+ pluggable_table.set(pluggable.get_id(), new PluggableRep(pluggable));
+}
+
+}
+
diff --git a/src/plugins/PublishingInterfaces.vala b/src/plugins/PublishingInterfaces.vala
new file mode 100644
index 0000000..ca74597
--- /dev/null
+++ b/src/plugins/PublishingInterfaces.vala
@@ -0,0 +1,605 @@
+/* Copyright 2011-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.
+ */
+
+/**
+ * Shotwell Pluggable Publishing API
+ *
+ * The Shotwell Pluggable Publishing API allows you to write plugins that upload
+ * photos and videos to web services. The Shotwell distribution includes publishing
+ * support for four core services: Facebook, Flickr, Picasa Web Albums, and YouTube.
+ * To enable Shotwell to connect to additional services, developers like you write
+ * publishing plugins, dynamically-loadable shared objects that are linked into the
+ * Shotwell process at runtime. Publishing plugins are just one of several kinds of
+ * plugins supported by {@link Spit}, the Shotwell Pluggable Interfaces Technology.
+ */
+namespace Spit.Publishing {
+
+/**
+ * The current version of the Pluggable Publishing API
+ */
+public const int CURRENT_INTERFACE = 0;
+
+/**
+ * Defines different kinds of errors that can occur during publishing.
+ */
+public errordomain PublishingError {
+ /**
+ * Indicates that no communications channel could be opened to the remote host.
+ *
+ * This error occurs, for example, when no network connection is available or
+ * when a DNS lookup fails.
+ */
+ NO_ANSWER,
+
+ /**
+ * Indicates that a communications channel to the remote host was previously opened, but
+ * the remote host can no longer be reached.
+ *
+ * This error occurs, for example, when the network is disconnected during a publishing
+ * interaction.
+ */
+ COMMUNICATION_FAILED,
+
+ /**
+ * Indicates that a communications channel to the remote host was opened and
+ * is active, but that messages sent to or from the remote host can't be understood.
+ *
+ * This error occurs, for example, when attempting to interact with a RESTful host
+ * via XML-RPC.
+ */
+ PROTOCOL_ERROR,
+
+ /**
+ * Indicates that the remote host has received a well-formed message that has caused
+ * a server-side error.
+ *
+ * This error occurs, for example, when the remote host receives a message that should
+ * be signed but isn't.
+ */
+ SERVICE_ERROR,
+
+ /**
+ * Indicates that the remote host has sent the local client back a well-formed response,
+ * but the response can't be understood.
+ *
+ * This error occurs, for example, when the remote host sends a response in an XML grammar
+ * different from that expected by the local client.
+ */
+ MALFORMED_RESPONSE,
+
+ /**
+ * Indicates that the local client can't access a file or files in local storage.
+ *
+ * This error occurs, for example, when the local client attempts to read binary data
+ * out of a photo or video file that doesn't exist.
+ */
+ LOCAL_FILE_ERROR,
+
+ /**
+ * Indicates that the remote host has rejected the session identifier used by the local
+ * client as out-of-date. The local client should acquire a new session identifier.
+ */
+ EXPIRED_SESSION
+}
+
+/**
+ * Represents a connection to a publishing service.
+ *
+ * Developers of publishing plugins provide a class that implements this interface. At
+ * any given time, only one Publisher can be running. When a publisher is running, it is
+ * allowed to access the network and has exclusive use of the shared user-interface and
+ * configuration services provided by the {@link PluginHost}. Publishers are created in
+ * a non-running state and do not begin running until start( ) is invoked. Publishers
+ * run until stop( ) is invoked.
+ */
+public interface Publisher : GLib.Object {
+ /**
+ * Describes the kinds of media a publishing service supports.
+ *
+ * Values can be masked together, for example: {{{(MediaType.PHOTO | MediaType.VIDEO)}}}
+ * indicates that a publishing service supports the upload of both photos and videos.
+ */
+ public enum MediaType {
+ NONE = 0,
+ PHOTO = 1 << 0,
+ VIDEO = 1 << 1
+ }
+
+ /**
+ * Returns a {@link Service} object describing the service to which this connects.
+ */
+ public abstract Service get_service();
+
+ /**
+ * Makes this publisher enter the running state and endows it with exclusive access
+ * to the shared services provided by the {@link PluginHost}. Through the host’s interface,
+ * this publisher can install user interface panes and query configuration information.
+ * Only running services should perform network operations.
+ */
+ public abstract void start();
+
+ /**
+ * Returns true if this publisher is in the running state; false otherwise.
+ */
+ public abstract bool is_running();
+
+ /**
+ * Causes this publisher to enter a non-running state. This publisher should stop all
+ * network operations and cease use of the shared services provided by the {@link PluginHost}.
+ */
+ public abstract void stop();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Encapsulates a pane that can be installed in the on-screen publishing dialog box to
+ * communicate status to and to get information from the user.
+ *
+ */
+public interface DialogPane : GLib.Object {
+
+ /**
+ * Describes how the on-screen publishing dialog box should look and behave when an associated
+ * pane is installed in the on-screen publishing dialog box.
+ */
+ public enum GeometryOptions {
+
+ /**
+ * When the associated pane is installed, the on-screen publishing dialog box will be
+ * sized normally and will not allow the user to change its size.
+ */
+ NONE = 0,
+
+ /**
+ * If this bit is set, when the associated pane is installed, the on-screen publishing
+ * dialog box will grow to a larger size.
+ */
+ EXTENDED_SIZE = 1 << 0,
+
+ /**
+ * If this bit is set, when the associated pane is installed, the on-screen publishing
+ * dialog box will allow the user to change its size.
+ */
+ RESIZABLE = 1 << 1,
+
+ /**
+ * If this bit is set, when the associated pane is installed, the on-screen publishing
+ * dialog box will grow to accommodate a full-width 1024 pixel web page. If both
+ * EXTENDED_SIZE and COLOSSAL_SIZE are set, EXTENDED_SIZE takes precedence.
+ */
+ COLOSSAL_SIZE = 1 << 2;
+ }
+
+ /**
+ * Returns the Gtk.Widget that is this pane's on-screen representation.
+ */
+ public abstract Gtk.Widget get_widget();
+
+ /**
+ * Returns a {@link GeometryOptions} bitfield describing how the on-screen publishing dialog
+ * box should look and behave when this pane is installed.
+ */
+ public abstract GeometryOptions get_preferred_geometry();
+
+ /**
+ * Invoked automatically by Shotwell when this pane has been installed into the on-screen
+ * publishing dialog box and become visible to the user.
+ */
+ public abstract void on_pane_installed();
+
+ /**
+ * Invoked automatically by Shotwell when this pane has been removed from the on-screen
+ * publishing dialog box and is no longer visible to the user.
+ */
+ public abstract void on_pane_uninstalled();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Enables its caller to report to the user on the progress of a publishing operation.
+ *
+ * @param file_number the sequence number of media item that the publishing system is currently
+ * working with, starting at 1. For example, if the user chooses to publish
+ * 4 photos, these photos would have sequence numbers 1, 2, 3, and 4.
+ *
+ * @param fraction_complete the fraction of the current publishing operation that has been
+ * completed, from 0.0 to 1.0, inclusive.
+ */
+public delegate void ProgressCallback(int file_number, double fraction_complete);
+
+/**
+ * Called by the publishing system when the user clicks the 'Login' button in a service welcome
+ * pane.
+ */
+public delegate void LoginCallback();
+
+/**
+ * Manages and provides services for publishing plugins.
+ *
+ * Implemented inside Shotwell, the PluginHost provides an interface through which the
+ * developers of publishing plugins can query and make changes to the publishing
+ * environment. For example, through the PluginHost, plugins can get a list of the photos
+ * and videos to be published, install and remove user-interface panes in the publishing
+ * dialog box, and request that the items to be uploaded be serialized to a temporary
+ * directory on disk. Plugins can use the services of the PluginHost only when their
+ * {@link Publisher} is in the running state. This ensures that non-running publishers
+ * don’t destructively interfere with the actively running publisher.
+ */
+public interface PluginHost : GLib.Object, Spit.HostInterface {
+
+ /**
+ * Specifies the label text on the push button control that appears in the
+ * lower-right-hand corner of the on-screen publishing dialog box.
+ */
+ public enum ButtonMode {
+ CLOSE = 0,
+ CANCEL = 1
+ }
+
+ /**
+ * Notifies the user that an unrecoverable publishing error has occurred and halts
+ * the publishing process.
+ *
+ * @param err An error object that describes the kind of error that occurred.
+ */
+ public abstract void post_error(Error err);
+
+ /**
+ * Halts the publishing process.
+ *
+ * Calling this method stops all network activity and hides the on-screen publishing
+ * dialog box.
+ */
+ public abstract void stop_publishing();
+
+ /**
+ * Returns a reference to the {@link Publisher} object that this is currently hosting.
+ */
+ public abstract Publisher get_publisher();
+
+ /**
+ * Attempts to install a pane in the on-screen publishing dialog box, making the pane visible
+ * and allowing it to interact with the user.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param pane the pane to install
+ *
+ * @param mode allows you to set the text displayed on the close/cancel button in the
+ * lower-right-hand corner of the on-screen publishing dialog box when pane is installed.
+ * If mode is ButtonMode.CLOSE, the button will have the title "Close." If mode is
+ * ButtonMode.CANCEL, the button will be titled "Cancel." You should set mode depending on
+ * whether a cancellable action is in progress. For example, if your publisher is in the
+ * middle of uploading 3 of 8 videos, then mode should be ButtonMode.CANCEL. However, if
+ * the publishing operation has completed and the success pane is displayed, then mode
+ * should be ButtonMode.CLOSE, because all cancellable publishing actions have already
+ * occurred.
+ */
+ public abstract void install_dialog_pane(Spit.Publishing.DialogPane pane,
+ ButtonMode mode = ButtonMode.CANCEL);
+
+ /**
+ * Attempts to install a pane in the on-screen publishing dialog box that contains
+ * static text.
+ *
+ * The text appears centered in the publishing dialog box and is drawn in
+ * the system font. This is a convenience method only; similar results could be
+ * achieved by manually constructing a Gtk.Label widget, wrapping it inside a
+ * {@link DialogPane}, and installing it manually with a call to
+ * install_dialog_pane( ). To provide visual consistency across publishing services,
+ * however, always use this convenience method instead of constructing label panes when
+ * you need to display static text to the user.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param message the text to show in the pane
+ *
+ * @param mode allows you to set the text displayed on the close/cancel button in the
+ * lower-right-hand corner of the on-screen publishing dialog box when pane is installed.
+ * If mode is ButtonMode.CLOSE, the button will have the title "Close." If mode is
+ * ButtonMode.CANCEL, the button will be titled "Cancel." You should set mode depending on
+ * whether a cancellable action is in progress. For example, if your publisher is in the
+ * middle of uploading 3 of 8 videos, then mode should be ButtonMode.CANCEL. However, if
+ * the publishing operation has completed and the success pane is displayed, then mode
+ * should be ButtonMode.CLOSE, because all cancellable publishing actions have already
+ * occurred.
+ */
+ public abstract void install_static_message_pane(string message,
+ ButtonMode mode = ButtonMode.CANCEL);
+
+ /**
+ * Works just like {@link install_static_message_pane} but allows markup to contain
+ * Pango text formatting tags as well as unstyled text.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param markup the text to show in the pane, marked up with Pango formatting tags.
+ *
+ * @param mode allows you to set the text displayed on the close/cancel button in the
+ * lower-right-hand corner of the on-screen publishing dialog box when pane is installed.
+ * If mode is ButtonMode.CLOSE, the button will have the title "Close." If mode is
+ * ButtonMode.CANCEL, the button will be titled "Cancel." You should set mode depending on
+ * whether a cancellable action is in progress. For example, if your publisher is in the
+ * middle of uploading 3 of 8 videos, then mode should be ButtonMode.CANCEL. However, if
+ * the publishing operation has completed and the success pane is displayed, then mode
+ * should be ButtonMode.CLOSE, because all cancellable publishing actions have already
+ * occurred.
+ */
+ public abstract void install_pango_message_pane(string markup,
+ ButtonMode mode = ButtonMode.CANCEL);
+
+ /**
+ * Attempts to install a pane in the on-screen publishing dialog box notifying the user
+ * that his or her publishing operation completed successfully.
+ *
+ * The text displayed depends on the type of media the current publishing service
+ * supports. To provide visual consistency across publishing services and to allow
+ * Shotwell to handle internationalization, always use this convenience method; don’t
+ * contruct and install success panes manually.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor
+ * this request.
+ */
+ public abstract void install_success_pane();
+
+ /**
+ * Attempts to install a pane displaying the static text “Fetching account information...”
+ * in the on-screen publishing dialog box, making it visible to the user.
+ *
+ * This is a convenience method only; similar results could be achieved by calling
+ * {@link install_static_message_pane} with an appropriate text argument. To provide
+ * visual consistency across publishing services and to allow Shotwell to handle
+ * internationalization, however, you should always use this convenience method whenever
+ * you need to tell the user that you’re querying account information over the network.
+ * Queries such as this are almost always performed immediately after the user has logged
+ * in to the remote service.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ */
+ public abstract void install_account_fetch_wait_pane();
+
+
+ /**
+ * Works just like {@link install_account_fetch_wait_pane} but displays the static text
+ * “Logging in...“
+ *
+ * As with {@link install_account_fetch_wait_pane}, this is a convenience method, but
+ * you should you use it provide to visual consistency and to let Shotwell handle
+ * internationalization. See the description of {@link install_account_fetch_wait_pane}
+ * for more information.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ */
+ public abstract void install_login_wait_pane();
+
+ /**
+ * Attempts to install a pane displaying the text 'welcome_message' above a push
+ * button labeled “Login” in the on-screen publishing dialog box, making it visible to the
+ * user.
+ *
+ * When the user clicks the “Login” button, you’ll be notified of the user’s action through
+ * the callback 'on_login_clicked'. Every Publisher should provide a welcome pane to
+ * introduce the service and explain service-specific features or restrictions. To provide
+ * visual consistency across publishing services and to allow Shotwell to handle
+ * internationalization, always use this convenience method; don’t contruct and install
+ * welcome panes manually.
+ *
+ * If an error has posted, the {@link PluginHost} will not honor this request.
+ *
+ * @param welcome_message the text to be displayed above a push button labeled “Login”
+ * in the on-screen publishing dialog box.
+ *
+ * @param on_login_clicked specifies the callback that is invoked when the user clicks
+ * the “Login” button.
+ */
+ public abstract void install_welcome_pane(string welcome_message,
+ LoginCallback on_login_clicked);
+
+ /**
+ * Toggles whether the service selector combo box in the upper-right-hand corner of the
+ * on-screen publishing dialog box is sensitive to input.
+ *
+ * Publishers should make the service selector box insensitive to input when they are performing
+ * non-interruptible file or network operations, since switching to another publishing
+ * service will halt whatever service is currently running. Under certain circumstances,
+ * the {@link PluginHost} may not honor this request.
+ *
+ * @param is_locked when is_locked is true, the service selector combo box is made insensitive.
+ * It appears greyed out and the user is prevented from switching to another publishing service.
+ * When is_locked is false, the combo box is sensitive, allowing the user to freely switch
+ * from the current service to another service.
+ */
+ public abstract void set_service_locked(bool is_locked);
+
+ /**
+ * Makes the designated widget the default widget for the publishing dialog.
+ *
+ * After a call to this method, the designated widget will be activated whenever the user
+ * presses the [ENTER] key anywhere in the on-screen publishing dialog box. Under certain
+ * circumstances, the {@link PluginHost} may not honor this request.
+ *
+ * @param widget a reference to the widget to designate as the default widget for the
+ * publishing dialog.
+ */
+ public abstract void set_dialog_default_widget(Gtk.Widget widget);
+
+ /**
+ * Returns an array of the publishable media items that the user has selected for upload to the
+ * remote service.
+ */
+ public abstract Publishable[] get_publishables();
+
+ /**
+ * Writes all of the publishable media items that the user has selected for upload to the
+ * remote service to a temporary directory on a local disk.
+ *
+ * You should call this method immediately before sending the publishable media items to the
+ * remote service over the network. Because serializing several megabytes of data is a
+ * potentially lengthy operation, calling this method installs an activity status pane in
+ * the on-screen publishing dialog box. The activity status pane displays a progress bar along
+ * with a string of informational text.
+ *
+ * Because sending items over the network to the remote service is also a potentially lengthy
+ * operation, you should leave the activity status pane installed in the on-screen publishing
+ * dialog box until this task is finished. Periodically during the sending process, you should
+ * report to the user on the progress of his or her upload. You can do this by invoking the
+ * returned {@link ProgressCallback} delegate.
+ *
+ * After calling this method, the activity status pane that this method installs remains
+ * displayed in the on-screen publishing dialog box until you install a new pane.
+ *
+ * @param content_major_axis when serializing publishable media items that are photos,
+ * ensure that neither the width nor the height of the serialized
+ * photo is greater than content_major_axis pixels. The value of
+ * this parameter has no effect on video publishables.
+ *
+ * @param strip_metadata when serializing publishable media items that are photos, if
+ * strip_metadata is true, all EXIF, IPTC, and XMP metadata will be
+ * removed from the serialized file. If strip_metadata is false, all
+ * metadata will be left intact. The value of this parameter has no
+ * effect on video publishables.
+ */
+ public abstract ProgressCallback? serialize_publishables(int content_major_axis,
+ bool strip_metadata = false);
+
+ /**
+ * Returns a {@link Publisher.MediaType} bitfield describing which kinds of media are present
+ * in the set of publishable media items that the user has selected for upload to the remote
+ * service.
+ */
+ public abstract Spit.Publishing.Publisher.MediaType get_publishable_media_type();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Describes an underlying media item (such as a photo or a video) that your plugin
+ * uploads to a remote publishing service.
+ */
+public interface Publishable : GLib.Object {
+
+ public static const string PARAM_STRING_BASENAME = "basename";
+ public static const string PARAM_STRING_TITLE = "title";
+ public static const string PARAM_STRING_COMMENT = "comment";
+ public static const string PARAM_STRING_EVENTCOMMENT= "eventcomment";
+
+ /**
+ * Returns a handle to the file on disk to which this publishable's data has been
+ * serialized.
+ *
+ * You should use this file handle to read into memory the binary data you will send over
+ * the network to the remote publishing service when this publishable is uploaded.
+ */
+ public abstract GLib.File? get_serialized_file();
+
+ /**
+ * Returns a name that can be used to identify this publishable to the remote service.
+ * If the publishing host cannot derive a sensible name, this method will
+ * return an empty string. Plugins should be able to handle that situation
+ * and provide a fallback value. One possible option for a fallback is:
+ * get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME)
+ */
+ public abstract string get_publishing_name();
+
+ /**
+ * Returns a string value from the publishable corresponding with the parameter name
+ * provided, or null if there is no value for this name.
+ */
+ public abstract string? get_param_string(string name);
+
+ /**
+ * Returns an array of strings that should be used to tag or mark this publishable on the
+ * remote service, or null if this publishable has no tags or markings.
+ */
+ public abstract string[] get_publishing_keywords();
+
+ /**
+ * Returns the kind of media item this publishable encapsulates.
+ */
+ public abstract Spit.Publishing.Publisher.MediaType get_media_type();
+
+ /**
+ * Returns the creation timestamp on the file.
+ */
+ public abstract GLib.DateTime get_exposure_date_time();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Describes the features and capabilities of a remote publishing service.
+ *
+ * Developers of publishing plugins provide a class that implements this interface.
+ */
+public interface Service : Object, Spit.Pluggable {
+ /**
+ * A factory method that instantiates and returns a new {@link Publisher} object that
+ * encapsulates a connection to the remote publishing service that this Service describes.
+ */
+ public abstract Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host);
+
+ /**
+ * Returns the kinds of media that this service can work with.
+ */
+ public abstract Spit.Publishing.Publisher.MediaType get_supported_media();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+}
+
diff --git a/src/plugins/SpitInterfaces.vala b/src/plugins/SpitInterfaces.vala
new file mode 100644
index 0000000..f2fce6f
--- /dev/null
+++ b/src/plugins/SpitInterfaces.vala
@@ -0,0 +1,367 @@
+/* Copyright 2011-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.
+ */
+
+/**
+ * Shotwell Pluggable Interface Technology (SPIT)
+ *
+ * This is the front-end interface for all modules (i.e. .so/.la files) that allows for Shotwell
+ * to query them for information and to get a list of all plug-ins stored in the module. This
+ * is named Shotwell Pluggable Interface Technology (SPIT). This is intended only to last long
+ * enough for another generic plug-in library (most likely Peas) to be used later.
+ *
+ * The Spit namespace is used for all interfaces and code that are made available to plugins or
+ * are exposed by plugins.
+ *
+ * More information can be found at [[https://wiki.gnome.org/Apps/Shotwell/Architecture/WritingPlugins]]
+ */
+namespace Spit {
+
+/**
+ * Reserved interface value denoting an unsupported interface version.
+ *
+ * All interface versions should be zero-based and incrementing.
+ */
+public const int UNSUPPORTED_INTERFACE = -1;
+
+/**
+ * Current version of the SPIT interface.
+ */
+public const int CURRENT_INTERFACE = 0;
+
+/**
+ * A utility function for checking host interfaces against one's own and returning the right value.
+ *
+ * Note that this only works if the caller operates on only one interface version (and cannot mutate
+ * between multiple ones).
+ *
+ * @param min_host_interface The minimum supported host interface version.
+ * @param max_host_interface The maximum supported host interface version.
+ * @param plugin_interface The interface version supported by the Pluggable.
+ *
+ * @return The plugin's interface version if supported, {@link UNSUPPORTED_INTERFACE} otherwise.
+ */
+public int negotiate_interfaces(int min_host_interface, int max_host_interface, int plugin_interface) {
+ return (min_host_interface > plugin_interface || max_host_interface < plugin_interface)
+ ? UNSUPPORTED_INTERFACE : plugin_interface;
+}
+
+/**
+ * SPIT entry point parameters.
+ *
+ * The host application passes a pointer to this structure for the module's information.
+ * The pointer should //not// be held, as it may be freed or reused by the host application
+ * after calling the entry point. The module should copy any information it may need (or hold
+ * a GObject reference) in its own memory space.
+ *
+ * Note that the module //must// fill in the module_spit_interface field with the SPIT interface
+ * version it understands prior to returning control.
+ */
+public struct EntryPointParams {
+ /**
+ * The host's minimum supported interface version.
+ */
+ public int host_min_spit_interface;
+ /**
+ * The host's maximum supported interface version.
+ */
+ public int host_max_spit_interface;
+ /**
+ * The module returns here the interface version of SPIT it supports,
+ * {@link UNSUPPORTED_INTERFACE} otherwise.
+ */
+ public int module_spit_interface;
+ /**
+ * A File object representing the library file (.so/la.) that the plugin was loaded from.
+ */
+ public File module_file;
+}
+
+/**
+ * SPIT API entry point.
+ *
+ * Host application passes in the minimum and maximum version of the SPIT
+ * interface it supports (values are inclusive) in the {@link EntryPointParams} struct.
+ * The module returns the version it wishes to use and a pointer to a {@link Spit.Module} (which
+ * will remain ref'ed by the host as long as the module is loaded in memory). The module should
+ * return {@link UNSUPPORTED_INTERFACE} if the min/max are out of its range and null for its
+ * Spit.Module. ({@link negotiate_interfaces} is good for dealing with this.)
+ *
+ * @return A {@link Spit.Module} if the interface negotiation is acceptable, null otherwise.
+ */
+[CCode (has_target = false)]
+public delegate Module? EntryPoint(EntryPointParams *params);
+
+/**
+ * SPIT entry point name, which matches {@link EntryPoint}'s interface
+ */
+public const string ENTRY_POINT_NAME = "spit_entry_point";
+
+/**
+ * A Module represents the resources of an entire dynamically-linked module (i.e. a .so/.la).
+ *
+ * A module holds zero or more Shotwell plugins ({@link Pluggable}). Once the module has been
+ * loaded into process space this object is retrieved by Shotwell. All calls to the module and
+ * its plugins are resolved through this interface.
+ *
+ * Note: The module is responsible for holding the reference to the Module object, of which there
+ * should be only one in the library file. The module should implement a g_module_unload method
+ * and drop the reference there.
+ */
+public interface Module : Object {
+ /**
+ * Returns a user-visible string describing the module.
+ */
+ public abstract unowned string get_module_name();
+
+ /**
+ * Returns a user-visible string describing the module version.
+ *
+ * Note that this may be programmatically interpreted at some point, so use a widespread
+ * versioning scheme.
+ */
+ public abstract unowned string get_version();
+
+ /**
+ * Returns a unique identifier for this module.
+ *
+ * This is used to differentiate between multiple
+ * installed versions and to determine which one should be used (i.e. if a module is available
+ * in a system directory and a user directory). This name is case-sensitive.
+ *
+ * Best practice: use a reverse-DNS-order scheme, a la Java's packages
+ * (i.e. "org.yorba.shotwell.frotz").
+ */
+ public abstract unowned string get_id();
+
+ /**
+ * Returns an array of {@link Pluggable} that represent each plugin available in the module.
+ *
+ * May return NULL or an empty array.
+ */
+ public abstract unowned Pluggable[]? get_pluggables();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * A structure holding an assortment of information about a {@link Pluggable}.
+ */
+public struct PluggableInfo {
+ public string? version;
+ public string? brief_description;
+ /**
+ * A comma-delimited list of the authors of this {@link Pluggable}.
+ */
+ public string? authors;
+ public string? copyright;
+ public string? license;
+ public bool is_license_wordwrapped;
+ public string? website_url;
+ public string? website_name;
+ public string? translators;
+ /**
+ * An icon representing this plugin at one or more sizes. Shotwell may select an icon
+ * according to the size that closest fits the control its being drawn in.
+ */
+ public Gdk.Pixbuf[]? icons;
+}
+
+/**
+ * A generic interface to all Shotwell plugins.
+ *
+ * Each plugin in a module needs to implement this interface at a minimum. Extension
+ * points may have (and probably will have) specific interface requirements as well.
+ */
+public interface Pluggable : Object {
+ /**
+ * Pluggable interface version negotiation.
+ *
+ * Like the {@link EntryPoint}, this mechanism allows for the host to negotiate with the Pluggable
+ * for its interface version. If the pluggable does not support an interface between the
+ * two ranges (inclusive), it should return {@link UNSUPPORTED_INTERFACE}.
+ *
+ * Note that this is ''not'' a negotiation of the SPIT interface versions (which is the
+ * responsibility of {@link EntryPoint}. Rather, each extension point is expected to version
+ * its own cluster of interfaces. It is that interface version that is being negotiated here.
+ *
+ * {@link negotiate_interfaces} can be used to implement this method.
+ *
+ * @param min_host_interface The host's minimum supported interface version number
+ * //for this Pluggable's intended extension point//.
+ * @param max_host_interface The host's maximum supported interface version number
+ * //for this Pluggable's intended extension point//.
+ *
+ * @return The version number supported by the host and the Pluggable or
+ * {@link UNSUPPORTED_INTERFACE}.
+ */
+ public abstract int get_pluggable_interface(int min_host_interface, int max_host_interface);
+
+ /**
+ * Returns a unique identifier for this Pluggable.
+ *
+ * Like {@link Module.get_id}, best practice is to use a reverse-DNS-order scheme to avoid
+ * conflicts.
+ */
+ public abstract unowned string get_id();
+
+ /**
+ * Returns a user-visible name for the Pluggable.
+ */
+ public abstract unowned string get_pluggable_name();
+
+ /**
+ * Returns extra information about the Pluggable that is used to identify it to the user.
+ */
+ public abstract void get_info(ref PluggableInfo info);
+
+ /**
+ * Called when the Pluggable is enabled (activated) or disabled (deactivated).
+ *
+ * activation will be called at the start of the program if the user previously
+ * enabled/disabled it as well as during program execution if the user changes its state. Note
+ * that disabling a Pluggable does not require destroying existing resources or objects
+ * the Pluggable has previously handed off to the host.
+ *
+ * This is purely informational. The Pluggable should acquire any long-term resources
+ * it may be holding onto here, or wait until an extension-specific call is made to it.
+ *
+ * @param enabled ``true`` if the Pluggable has been enabled, ``false`` otherwise.
+ */
+ public abstract void activation(bool enabled);
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * An interface to common services supplied by the host (Shotwell).
+ *
+ * Each {@link Pluggable} is offered a HostInterface for needs common to most plugins.
+ *
+ * Note that
+ * a HostInterface is not explicitly handed to the Pluggable through the SPIT interface, but is expected
+ * to be offered to the Pluggable through an interface applicable to the extension point. This
+ * also allows the extension point to extend HostInterface to offer other services applicable to the
+ * type of plugin.
+ */
+public interface HostInterface : Object {
+ /**
+ * Returns a File object representing the library file (.so/la.) that the plugin was loaded
+ * from.
+ */
+ public abstract File get_module_file();
+
+ /**
+ * Get a boolean from a persistent configuration store.
+ *
+ * @param key The name of the value to be retrieved.
+ * @param def The default value (returned if the key has not been previously set).
+ *
+ * @return The value associated with key, def if not set.
+ */
+ public abstract bool get_config_bool(string key, bool def);
+
+ /**
+ * Store a boolean in a persistent configuration store.
+ *
+ * @param key The name of the value to be stored.
+ * @param val The value to be stored.
+ */
+ public abstract void set_config_bool(string key, bool val);
+
+ /**
+ * Get an integer from a persistent configuration store.
+ *
+ * @param key The name of the value to be retrieved.
+ * @param def The default value (returned if the key has not been previously set).
+ *
+ * @return The value associated with key, def if not set.
+ */
+ public abstract int get_config_int(string key, int def);
+
+ /**
+ * Store an integer in a persistent configuration store.
+ *
+ * @param key The name of the value to be stored.
+ * @param val The value to be stored.
+ */
+ public abstract void set_config_int(string key, int val);
+
+ /**
+ * Get a string from a persistent configuration store.
+ *
+ * @param key The name of the value to be retrieved.
+ * @param def The default value (returned if the key has not been previously set).
+ *
+ * @return The value associated with key, def if not set.
+ */
+ public abstract string? get_config_string(string key, string? def);
+
+ /**
+ * Store a string in a persistent configuration store.
+ *
+ * @param key The name of the value to be stored.
+ * @param val The value to be stored.
+ */
+ public abstract void set_config_string(string key, string? val);
+
+ /**
+ * Get a double from a persistent configuration store.
+ *
+ * @param key The name of the value to be retrieved.
+ * @param def The default value (returned if the key has not been previously set).
+ *
+ * @return The value associated with key, def if not set.
+ */
+ public abstract double get_config_double(string key, double def);
+
+ /**
+ * Store a double in a persistent configuration store.
+ *
+ * @param key The name of the value to be stored.
+ * @param val The value to be stored.
+ */
+ public abstract void set_config_double(string key, double val);
+
+ /**
+ * Delete the value from the persistent configuration store.
+ */
+ public abstract void unset_config_key(string key);
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+}
+
diff --git a/src/plugins/StandardHostInterface.vala b/src/plugins/StandardHostInterface.vala
new file mode 100644
index 0000000..9bfc0aa
--- /dev/null
+++ b/src/plugins/StandardHostInterface.vala
@@ -0,0 +1,84 @@
+/* Copyright 2011-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 Plugins {
+
+public class StandardHostInterface : Object, Spit.HostInterface {
+ private string config_domain;
+ private string config_id;
+ private File module_file;
+ private Spit.PluggableInfo info;
+
+ public StandardHostInterface(Spit.Pluggable pluggable, string config_domain) {
+ this.config_domain = config_domain;
+ config_id = parse_key(pluggable.get_id());
+ module_file = get_pluggable_module_file(pluggable);
+ pluggable.get_info(ref info);
+ }
+
+ private static string parse_key(string id) {
+ // special case: legacy plugins (Web publishers moved into SPIT) have special names
+ // new plugins will use their full ID
+ switch (id) {
+ case "org.yorba.shotwell.publishing.facebook":
+ return "facebook";
+
+ case "org.yorba.shotwell.publishing.picasa":
+ return "picasa";
+
+ case "org.yorba.shotwell.publishing.flickr":
+ return "flickr";
+
+ case "org.yorba.shotwell.publishing.youtube":
+ return "youtube";
+
+ default:
+ return id;
+ }
+ }
+
+ public File get_module_file() {
+ return module_file;
+ }
+
+ public bool get_config_bool(string key, bool def) {
+ return Config.Facade.get_instance().get_plugin_bool(config_domain, config_id, key, def);
+ }
+
+ public void set_config_bool(string key, bool val) {
+ Config.Facade.get_instance().set_plugin_bool(config_domain, config_id, key, val);
+ }
+
+ public int get_config_int(string key, int def) {
+ return Config.Facade.get_instance().get_plugin_int(config_domain, config_id, key, def);
+ }
+
+ public void set_config_int(string key, int val) {
+ Config.Facade.get_instance().set_plugin_int(config_domain, config_id, key, val);
+ }
+
+ public string? get_config_string(string key, string? def) {
+ return Config.Facade.get_instance().get_plugin_string(config_domain, config_id, key, def);
+ }
+
+ public void set_config_string(string key, string? val) {
+ Config.Facade.get_instance().set_plugin_string(config_domain, config_id, key, val);
+ }
+
+ public double get_config_double(string key, double def) {
+ return Config.Facade.get_instance().get_plugin_double(config_domain, config_id, key, def);
+ }
+
+ public void set_config_double(string key, double val) {
+ Config.Facade.get_instance().set_plugin_double(config_domain, config_id, key, val);
+ }
+
+ public void unset_config_key(string key) {
+ Config.Facade.get_instance().unset_plugin_key(config_domain, config_id, key);
+ }
+}
+
+}
diff --git a/src/plugins/TransitionsInterfaces.vala b/src/plugins/TransitionsInterfaces.vala
new file mode 100644
index 0000000..eae76cf
--- /dev/null
+++ b/src/plugins/TransitionsInterfaces.vala
@@ -0,0 +1,300 @@
+/* Copyright 2011-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.
+ */
+
+/**
+ * Transitions are used in Shotwell for interstitial effects in slideshow mode. They may
+ * also be used elsewhere in future releases.
+ *
+ * Plugin writers should start by implementing a {@link Descriptor} which in turn Shotwell uses
+ * to instantiate an {@link Effect}.
+ */
+namespace Spit.Transitions {
+
+/**
+ * The current version of the Transitions plugin interface.
+ */
+public const int CURRENT_INTERFACE = 0;
+
+/**
+ * Direction indicates what direction (animated motion) the {@link Effect} should simulate the
+ * images are moving, if appropriate.
+ *
+ * The direction indicates from what side or corner of the screen the new image should come in from.
+ * Thus, a LEFT slide means the current image exits via the left-hand edge of the screen and the
+ * new image moves into place from the right-hand edge.
+ *
+ * UP, DOWN, and diagonals may be added at some point.
+ */
+public enum Direction {
+ LEFT = 0,
+ RIGHT = 1,
+
+ /**
+ * Convenience definition (for LTR readers).
+ */
+ FORWARD = LEFT,
+
+ /**
+ * Convenience definition (for LTR readers).
+ */
+ BACKWARD = RIGHT
+}
+
+/**
+ * Visuals contains the pertinent drawing information for the transition that must occur.
+ *
+ * A Visuals object is supplied to {@link Effect} at the start of the transition and during each
+ * call to paint to the screen.
+ *
+ * Note that if starting with a blank screen, from_pixbuf will be null and from_pos will be
+ * zeroed. The transition should be considered to start from a blank screen of the supplied
+ * background color.
+ *
+ * Also note that if transitioning to a blank screen, to_pixbuf will be null and to_pos will be
+ * zeroed. Like the prior case, the transition should move toward a blank screen of the background
+ * color.
+ */
+public class Visuals : Object {
+ /**
+ * Returns the starting pixbuf (the pixbuf currently on the display).
+ *
+ * If transitioning from a blank screen, this will return null.
+ */
+ public Gdk.Pixbuf? from_pixbuf { get; private set; }
+
+ /**
+ * Returns the position of the starting pixbuf on the display.
+ *
+ * If transitioning from a blank screen, this will be zeroed.
+ */
+ public Gdk.Rectangle from_pos { get; private set; }
+
+ /**
+ * Returns the ending pixbuf (the pixbuf that the transition should result in).
+ *
+ * If transitioning to a blank screen, this will return null.
+ */
+ public Gdk.Pixbuf? to_pixbuf { get; private set; }
+
+ /**
+ * Returns the position of the ending pixbuf on the display.
+ *
+ * If transitioning to a blank screen, this will be zeroed.
+ */
+ public Gdk.Rectangle to_pos { get; private set; }
+
+ /**
+ * Returns the background color of the viewport.
+ */
+ public Gdk.RGBA bg_color { get; private set; }
+
+ public Visuals(Gdk.Pixbuf? from_pixbuf, Gdk.Rectangle from_pos, Gdk.Pixbuf? to_pixbuf,
+ Gdk.Rectangle to_pos, Gdk.RGBA bg_color) {
+ this.from_pixbuf = from_pixbuf;
+ this.from_pos = from_pos;
+ this.to_pixbuf = to_pixbuf;
+ this.to_pos = to_pos;
+ this.bg_color = bg_color;
+ }
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * Motion contains all the pertinent information regarding the animation of the transition.
+ *
+ * Some of Motion's information may not apply to a transition effect (such as Direction for a
+ * fade effect).
+ */
+public class Motion : Object {
+ /**
+ * Returns the direction the transition should occur in (if pertinent to the {@link Effect}.
+ */
+ public Direction direction { get; private set; }
+
+ /**
+ * Returns the frames per second of the {@link Effect}.
+ */
+ public int fps { get; private set; }
+
+ /**
+ * Returns the amount of time the transition should take (in milliseconds).
+ */
+ public int duration_msec { get; private set; }
+
+ /**
+ * Returns the number of frames that should be required to perform the transition in the
+ * expected {@link duration_msec}.
+ */
+ public int total_frames {
+ get {
+ return (int) ((double) fps * ((double) duration_msec / 1000.0));
+ }
+ }
+
+ /**
+ * Returns the approximate time between each frame draw (in milliseconds).
+ */
+ public int tick_msec {
+ get {
+ return (int) (1000.0 / (double) fps);
+ }
+ }
+
+ public Motion(Direction direction, int fps, int duration_msec) {
+ this.direction = direction;
+ this.fps = fps;
+ this.duration_msec = duration_msec;
+ }
+
+ /**
+ * Returns a value from 0.0 to 1.0 that represents the percentage of the transition's completion
+ * for the specified frame.
+ */
+ public double get_alpha(int frame_number) {
+ return (double) frame_number / (double) total_frames;
+ }
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * A Descriptor offers a factory method for creating {@link Effect} instances.
+ */
+public interface Descriptor : Object, Spit.Pluggable {
+ /**
+ * Returns an instance of the {@link Effect} this descriptor represents.
+ */
+ public abstract Effect create(Spit.HostInterface host);
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+/**
+ * An Effect represents an interstitial effect that is used to transition the display from one
+ * image to another.
+ *
+ * An Effect must hold state so that it knows what it should be drawn at any call to {@link paint}
+ * (which is called regularly during a transition). That is, it should be able to draw any frame of
+ * the transition at any time. The same frame may need to be drawn multiple times, or the host
+ * may skip ahead and ask for a frame well ahead of the last requested one.
+ *
+ * ''Frame numbers are one-based throughout this interface''. This is because the initial state (the
+ * blank viewport or the starting pixbuf) is frame zero. The Effect is never called to paint this
+ * frame. The Effect is also not called to paint the final frame (a blank viewport or the ending
+ * pixbuf).
+ *
+ * If the Effect uses background threads for its work, it should use the appropriate primitives
+ * for critical sections. All calls to this interface will be from the context of the main UI
+ * thread. ''None of these calls should block.''
+ *
+ * If the Details object needs to be held by the Effect, its reference to it should be dropped at
+ * the end of the cycle (or shortly thereafter).
+ *
+ * An instance may be reused and should be prepared for restarts.
+ */
+public interface Effect : Object {
+ /**
+ * Returns frames per second (FPS) information for this effect.
+ *
+ * If the min_fps is not met, the Effect may be cancelled or the host will skip ahead.
+ *
+ * @param desired_fps The desired FPS of the transition. Return zero if no
+ * transition is to occur (instantaneous or null transition).
+ * @param min_fps The minimum FPS before the effect is consider "ruined".
+ * Return zero if any FPS is acceptable.
+ */
+ public abstract void get_fps(out int desired_fps, out int min_fps);
+
+ /**
+ * Called when the effect is starting.
+ *
+ * All state should be reset. The frame number, which is not supplied, is one.
+ */
+ public abstract void start(Visuals visuals, Motion motion);
+
+ /**
+ * Return true if the Effect needs the background cleared prior to calling {@link paint}.
+ */
+ public abstract bool needs_clear_background();
+
+ /**
+ * Called when the effect needs to paint (i.e. an expose or draw event has occurred).
+ *
+ * This call should ''not'' advance the state of the effect (i.e. it may be called more than
+ * once for the same frame).
+ *
+ * @param ctx The Cairo context the Effect should use to paint the transition.
+ * @param width The width (in pixels) of the Cairo surface.
+ * @param height The height (in pixels) of the Cairo surface.
+ * @param frame_number The ''one-based'' frame being drawn.
+ */
+ public abstract void paint(Visuals visuals, Motion motion, Cairo.Context ctx, int width,
+ int height, int frame_number);
+
+ /**
+ * Called to notify the effect that the state of the transition should advance to the specified
+ * frame number.
+ *
+ * Note: There is no guarantee frame numbers will be consecutive between calls
+ * to next, especially if the transition clock is attempting to catch up.
+ *
+ * @param frame_number The ''one-based'' frame being advanced to.
+ */
+ public abstract void advance(Visuals visuals, Motion motion, int frame_number);
+
+ /**
+ * Called if the Effect should halt the transition.
+ *
+ * It only needs to reset state if {@link start} is called again.
+ */
+ public abstract void cancel();
+
+ //
+ // For future expansion.
+ //
+ protected virtual void reserved0() {}
+ protected virtual void reserved1() {}
+ protected virtual void reserved2() {}
+ protected virtual void reserved3() {}
+ protected virtual void reserved4() {}
+ protected virtual void reserved5() {}
+ protected virtual void reserved6() {}
+ protected virtual void reserved7() {}
+}
+
+}
+
diff --git a/src/plugins/mk/interfaces.mk b/src/plugins/mk/interfaces.mk
new file mode 100644
index 0000000..34be1eb
--- /dev/null
+++ b/src/plugins/mk/interfaces.mk
@@ -0,0 +1,29 @@
+
+PLUGIN_INTERFACES := \
+ src/plugins/SpitInterfaces.vala \
+ src/plugins/TransitionsInterfaces.vala \
+ src/plugins/PublishingInterfaces.vala \
+ src/plugins/DataImportsInterfaces.vala
+
+PLUGIN_PKG_REQS := \
+ gobject-2.0 \
+ glib-2.0 \
+ gdk-3.0 \
+ gtk+-3.0 \
+ gee-0.8
+
+PLUGIN_VAPI := plugins/shotwell-plugin-dev-1.0.vapi
+PLUGIN_HEADER := $(PLUGIN_VAPI:.vapi=.h)
+PLUGIN_DEPS := $(PLUGIN_VAPI:.vapi=.deps)
+
+$(PLUGIN_DEPS): src/plugins/mk/interfaces.mk
+ rm -f $@
+ $(foreach pkg,$(PLUGIN_PKG_REQS),`echo $(pkg) >> $@`)
+
+$(PLUGIN_HEADER): $(PLUGIN_VAPI)
+
+$(PLUGIN_VAPI): $(PLUGIN_INTERFACES) src/plugins/mk/interfaces.mk
+ $(call check_valac_version)
+ $(VALAC) -c $(VALAFLAGS) -X -DGETTEXT_PACKAGE='"shotwell"' -X -I. $(foreach pkg,$(PLUGIN_PKG_REQS),--pkg=$(pkg)) --includedir=plugins --vapi=$@ --header=$(basename $@).h $(PLUGIN_INTERFACES)
+ $(foreach src,$(PLUGIN_INTERFACES),`rm $(notdir $(src)).o`)
+
diff --git a/src/plugins/mk/plugins.mk b/src/plugins/mk/plugins.mk
new file mode 100644
index 0000000..903dd8e
--- /dev/null
+++ b/src/plugins/mk/plugins.mk
@@ -0,0 +1,35 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Plugins
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := plugins
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala.
+UNIT_FILES := \
+ PublishingInterfaces.vala \
+ SpitInterfaces.vala \
+ TransitionsInterfaces.vala \
+ StandardHostInterface.vala \
+ ManifestWidget.vala \
+ DataImportsInterfaces.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES := \
+ Util
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC := \
+ mk/interfaces.mk
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+