diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
commit | 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch) | |
tree | d2e54377d14d604356c86862a326f64ae64dadd6 /src/plugins |
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/plugins')
-rw-r--r-- | src/plugins/DataImportsInterfaces.vala | 489 | ||||
-rw-r--r-- | src/plugins/ManifestWidget.vala | 282 | ||||
-rw-r--r-- | src/plugins/Plugins.vala | 436 | ||||
-rw-r--r-- | src/plugins/PublishingInterfaces.vala | 605 | ||||
-rw-r--r-- | src/plugins/SpitInterfaces.vala | 367 | ||||
-rw-r--r-- | src/plugins/StandardHostInterface.vala | 84 | ||||
-rw-r--r-- | src/plugins/TransitionsInterfaces.vala | 300 | ||||
-rw-r--r-- | src/plugins/mk/interfaces.mk | 29 | ||||
-rw-r--r-- | src/plugins/mk/plugins.mk | 35 |
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(¶ms); + 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 + |