Commit 76304ef3 authored by Jim Nelson's avatar Jim Nelson

#3170: Plugins can now be enabled/disabled via the Preferences dialog.

parent 2759ed8b
......@@ -10,11 +10,11 @@ public class FacebookService : Object, Spit.Pluggable, Spit.Publishing.Service {
Spit.Publishing.CURRENT_API_VERSION);
}
public string get_id() {
public unowned string get_id() {
return "org.yorba.shotwell.publishing.facebook";
}
public string get_pluggable_name() {
public unowned string get_pluggable_name() {
return "Facebook";
}
......@@ -26,6 +26,9 @@ public class FacebookService : Object, Spit.Pluggable, Spit.Publishing.Service {
info.website_url = "http://www.yorba.org";
}
public void activation(bool enabled) {
}
public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) {
return new Publishing.Facebook.FacebookPublisher(this, host);
}
......
......@@ -10,11 +10,11 @@ public class PicasaService : Object, Spit.Pluggable, Spit.Publishing.Service {
Spit.Publishing.CURRENT_API_VERSION);
}
public string get_id() {
public unowned string get_id() {
return "org.yorba.shotwell.publishing.picasa";
}
public string get_pluggable_name() {
public unowned string get_pluggable_name() {
return "Picasa Web Albums";
}
......@@ -34,6 +34,9 @@ public class PicasaService : Object, Spit.Pluggable, Spit.Publishing.Service {
return (Spit.Publishing.Publisher.MediaType.PHOTO |
Spit.Publishing.Publisher.MediaType.VIDEO);
}
public void activation(bool enabled) {
}
}
namespace Publishing.Picasa {
......
......@@ -18,19 +18,19 @@ private class ShotwellPublishingCoreServices : Object, Spit.Module {
~ShotwellPublishingCoreServices() {
}
public string get_name() {
return "Core Publishing Services";
public unowned string get_module_name() {
return _("Core Publishing Services");
}
public string get_version() {
public unowned string get_version() {
return _VERSION;
}
public string get_id() {
public unowned string get_id() {
return "org.yorba.shotwell.publishing.core_services";
}
public Spit.Pluggable[]? get_pluggables() {
public unowned Spit.Pluggable[]? get_pluggables() {
return pluggables;
}
}
......@@ -58,7 +58,7 @@ public unowned Spit.Module? spit_entry_point(int host_min_spit_interface, int ho
public void g_module_unload() {
if (core_services != null)
debug("%s %s unloaded", core_services.get_name(), core_services.get_version());
debug("%s %s unloaded", core_services.get_module_name(), core_services.get_version());
else
debug("core_services unloaded prior to spit_entry_point being called");
......
......@@ -8,11 +8,11 @@
using Spit;
private class CrumbleEffectDescriptor : ShotwellTransitionDescriptor {
public override string get_id() {
public override unowned string get_id() {
return "org.yorba.shotwell.transitions.crumble";
}
public override string get_pluggable_name() {
public override unowned string get_pluggable_name() {
return _("Crumble");
}
......
......@@ -8,11 +8,11 @@
using Spit;
private class FadeEffectDescriptor : ShotwellTransitionDescriptor {
public override string get_id() {
public override unowned string get_id() {
return "org.yorba.shotwell.transitions.fade";
}
public override string get_pluggable_name() {
public override unowned string get_pluggable_name() {
return _("Fade");
}
......
......@@ -8,11 +8,11 @@
using Spit;
private class SlideEffectDescriptor : ShotwellTransitionDescriptor {
public override string get_id() {
public override unowned string get_id() {
return "org.yorba.shotwell.transitions.slide";
}
public override string get_pluggable_name() {
public override unowned string get_pluggable_name() {
return _("Slide");
}
......
......@@ -15,24 +15,24 @@ private class ShotwellTransitions : Object, Spit.Module {
pluggables += new CrumbleEffectDescriptor();
}
public string get_name() {
return "Shotwell Transitions";
public unowned string get_module_name() {
return _("Core Slideshow Transitions");
}
public string get_version() {
public unowned string get_version() {
return _VERSION;
}
public string get_id() {
public unowned string get_id() {
return "org.yorba.shotwell.transitions";
}
public Spit.Pluggable[]? get_pluggables() {
public unowned Spit.Pluggable[]? get_pluggables() {
return pluggables;
}
}
private ShotwellTransitions? spitwad = null;
private ShotwellTransitions? module = null;
// This entry point is required for all SPIT modules.
public unowned Spit.Module? spit_entry_point(int host_min_spit_interface, int host_max_spit_interface,
......@@ -42,19 +42,19 @@ public unowned Spit.Module? spit_entry_point(int host_min_spit_interface, int ho
if (module_spit_interface == Spit.UNSUPPORTED_INTERFACE)
return null;
if (spitwad == null)
spitwad = new ShotwellTransitions();
if (module == null)
module = new ShotwellTransitions();
return spitwad;
return module;
}
public void g_module_unload() {
if (spitwad != null)
debug("%s %s unloaded", spitwad.get_name(), spitwad.get_version());
if (module != null)
debug("%s %s unloaded", module.get_module_name(), module.get_version());
else
debug("spitter unloaded prior to spit_entry_point being called");
spitwad = null;
module = null;
}
// This is here to keep valac happy.
......@@ -68,9 +68,9 @@ public abstract class ShotwellTransitionDescriptor : Object, Spit.Pluggable, Spi
Spit.Transitions.CURRENT_INTERFACE);
}
public abstract string get_id();
public abstract unowned string get_id();
public abstract string get_pluggable_name();
public abstract unowned string get_pluggable_name();
public void get_info(out Spit.PluggableInfo info) {
info.authors = "Maxim Kartashev, Jim Nelson";
......@@ -84,6 +84,9 @@ public abstract class ShotwellTransitionDescriptor : Object, Spit.Pluggable, Spi
info.website_url = "http://www.yorba.org";
}
public void activation(bool enabled) {
}
public abstract Spit.Transitions.Effect create(Spit.HostInterface host);
}
......@@ -269,8 +269,19 @@ public class Config {
return get_int("/apps/shotwell/sharing/picasa/default_size", 3) - 1;
}
private string make_plugin_path(string domain, string id, string key) {
return "%s/%s/%s/%s".printf(PATH_SHOTWELL, domain, id, key);
public static string? clean_plugin_id(string id) {
string cleaned = id.replace("/", "_");
cleaned = cleaned.strip();
return !is_string_empty(cleaned) ? cleaned : null;
}
private static string make_plugin_path(string domain, string id, string key) {
string? cleaned_id = clean_plugin_id(id);
if (cleaned_id == null)
cleaned_id = "default";
return "%s/%s/%s/%s".printf(PATH_SHOTWELL, domain, cleaned_id, key);
}
public bool get_plugin_bool(string domain, string id, string key, bool def) {
......@@ -309,6 +320,14 @@ public class Config {
unset(make_plugin_path(domain, id, key));
}
public bool is_plugin_enabled(string id, bool def) {
return get_bool("/apps/shotwell/plugins/%s/enabled".printf(clean_plugin_id(id)), def);
}
public void set_plugin_enabled(string id, bool enabled) {
set_bool("/apps/shotwell/plugins/%s/enabled".printf(clean_plugin_id(id)), enabled);
}
public string? get_publishing_string(string domain, string key, string? default_value = null) {
return get_string("/apps/shotwell/sharing/%s/%s".printf(domain, key), default_value);
}
......
......@@ -1663,6 +1663,7 @@ public class PreferencesDialog {
}
private static PreferencesDialog preferences_dialog;
private Gtk.Dialog dialog;
private Gtk.Builder builder;
private Gtk.Adjustment bg_color_adjustment;
......@@ -1681,16 +1682,17 @@ public class PreferencesDialog {
private GLib.DateTime example_date = new GLib.DateTime.local(2009, 3, 10, 18, 16, 11);
private Gtk.CheckButton lowercase;
private Gtk.Button close_button;
private Plugins.ManifestWidgetMediator plugins_mediator = new Plugins.ManifestWidgetMediator();
private PreferencesDialog() {
builder = AppWindow.create_builder();
dialog = builder.get_object("preferences_dialog") as Gtk.Dialog;
dialog.set_parent_window(AppWindow.get_instance().get_parent_window());
dialog.set_transient_for(AppWindow.get_instance());
dialog.delete_event.connect(on_delete);
dialog.response.connect(on_close);
bg_color_adjustment = builder.get_object("bg_color_adjustment") as Gtk.Adjustment;
bg_color_adjustment.set_value(bg_color_adjustment.get_upper() -
Config.get_instance().get_bg_color().red);
......@@ -1726,7 +1728,10 @@ public class PreferencesDialog {
lowercase = builder.get_object("lowercase") as Gtk.CheckButton;
lowercase.toggled.connect(on_lowercase_toggled);
Gtk.Bin plugin_manifest_container = builder.get_object("plugin-manifest-bin") as Gtk.Bin;
plugin_manifest_container.add(plugins_mediator.widget);
populate_preference_options();
photo_editor_combo.changed.connect(on_photo_editor_changed);
......
......@@ -66,7 +66,7 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public const string ENHANCE = "shotwell-auto-enhance";
public const string CROP_PIVOT_RETICLE = "shotwell-crop-pivot-reticle";
public const string PUBLISH = "applications-internet";
public const string MERGE = "shotwell-merge-events";
public const string MERGE = "shotwell-merge-events";
public const string ICON_APP = "shotwell.svg";
public const string ICON_APP16 = "shotwell-16.svg";
......@@ -673,6 +673,13 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
return noninterpretable_badge_pixbuf;
}
public Gtk.IconTheme get_icon_theme_engine() {
Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default();
icon_theme.append_search_path(AppDirs.get_resources_dir().get_child("icons").get_path());
return icon_theme;
}
// This method returns a reference to a cached pixbuf that may be shared throughout the system.
// If the pixbuf is to be modified, make a copy of it.
public Gdk.Pixbuf? get_icon(string name, int scale = DEFAULT_ICON_SCALE) {
......
......@@ -122,14 +122,14 @@ public class Sidebar : Gtk.TreeView {
popup_menu.connect(on_context_menu_keypress);
icon_theme = Gtk.IconTheme.get_default();
icon_theme.append_search_path(AppDirs.get_resources_dir().get_child("icons").get_path());
icon_theme = Resources.get_icon_theme_engine();
icon_theme.changed.connect(on_theme_change);
}
~Sidebar() {
text.editing_canceled.disconnect(on_editing_canceled);
text.editing_started.disconnect(on_editing_started);
icon_theme.changed.disconnect(on_theme_change);
}
public void place_cursor(SidebarPage page) {
......
......@@ -1239,16 +1239,23 @@ public class ServiceFactory {
string, ServiceCapabilities>();
private ServiceFactory() {
load_wrapped_services();
Publishing.Glue.GlueFactory.get_instance().wrapped_services_changed.connect(
load_wrapped_services);
}
private void load_wrapped_services() {
caps_map.clear();
// in addition to the baked-in services above, add services dynamically loaded from
// plugins. since everything involving plugins is written in terms of the new publishing
// API, we have to use the glue code.
add_caps(new FlickrConnector.Capabilities());
add_caps(new YandexConnector.Capabilities());
add_caps(new YouTubeConnector.Capabilities());
add_caps(new PiwigoConnector.Capabilities());
// in addition to the baked-in services above, add services dynamically loaded from
// plugins. since everything involving plugins is written in terms of the new publishing
// API, we have to use the glue code.
Publishing.Glue.GlueFactory glue_factory = Publishing.Glue.GlueFactory.get_instance();
ServiceCapabilities[] caps = glue_factory.get_wrapped_services();
ServiceCapabilities[] caps = Publishing.Glue.GlueFactory.get_instance().get_wrapped_services();
foreach (ServiceCapabilities current_caps in caps)
add_caps(current_caps);
}
......
/* Copyright 2011 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.
*/
// Need this due to this bug:
// https://bugzilla.gnome.org/show_bug.cgi?id=642635
extern bool gtk_tree_view_column_cell_get_position(Gtk.TreeViewColumn column, Gtk.CellRenderer renderer,
out int start_pos, out int width);
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;
if (!get_pluggable_info(id, out 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_licensed_wordwrapped;
about_dialog.logo = info.icon;
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(int) // 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;
pluggable.get_info(out info);
icon = (info.icon != null) ? info.icon : Resources.get_icon(Resources.ICON_APP,
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;
}
private bool get_renderer_from_pos(int x, int y, out Gtk.TreePath path, out Gtk.CellRenderer renderer) {
// get the TreePath and column for the position
Gtk.TreeViewColumn column;
if (!get_path_at_pos(x, y, out path, out column, null, null))
return false;
Gdk.Rectangle cell_area = Gdk.Rectangle();
get_cell_area(path, column, out cell_area);
int conv_x, conv_y;
convert_bin_window_to_widget_coords(cell_area.x, cell_area.y, out conv_x, out conv_y);
int pixel_x = conv_x;
foreach (Gtk.CellRenderer column_renderer in column.get_cells()) {
int x_offset, width;
if (!gtk_tree_view_column_cell_get_position(column, column_renderer, out x_offset, out width))
continue;
if (x >= pixel_x && x <= (pixel_x + width)) {
renderer = column_renderer;
return true;
}
pixel_x += width;
}
return false;
}
// 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 against the renderers, and treat a hit against the checkbox renderer as a
// toggle event. Can't rely on the "toggle" signal here, however, because that's being fired
// whenever the row is clicked, and can't easily suppress it here because that causes the
// selection mechanism to fail. Could simulate selection here, but now this little hack has
// grown into a reimplementation of default behavior.
public override bool button_press_event(Gdk.EventButton event) {
Gtk.TreePath path;
Gtk.CellRenderer renderer;
if (!get_renderer_from_pos((int) event.x, (int) event.y, out path, out renderer))
return base.button_press_event(event);
if (!(renderer is Gtk.CellRendererToggle))
return base.button_press_event(event);
// checkbox was clicked, reflect that in the model
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;
}
}
}
......@@ -15,14 +15,29 @@ private const string[] SHARED_LIB_EXTS = { "so", "la" };
private const int MIN_SPIT_INTERFACE = 0;
private const int MAX_SPIT_INTERFACE = 0;
private class SpitModule {
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 unowned Spit.Module? spitmodule = null;
public unowned Spit.Module? spit_module = null;
public int spit_interface = Spit.UNSUPPORTED_INTERFACE;
public string? id = null;
private SpitModule(File file) {
private ModuleRep(File file) {
this.file = file;
module = Module.open(file.get_path(), ModuleFlags.BIND_LAZY);
......@@ -31,22 +46,73 @@ private class SpitModule {
// 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 SpitModule? open(File file) {
SpitModule spit_module = new SpitModule(file);
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