diff --git a/core/plugins.vala b/core/plugins.vala index 515d7de01..5e5445aec 100644 --- a/core/plugins.vala +++ b/core/plugins.vala @@ -10,6 +10,29 @@ */ namespace Midori { + public class Extension : Object { + internal static List extensions = null; + + public string id { get; set; } + public string name { get; set; } + public string description { get; set; } + public Icon icon { get; set; default = new ThemedIcon.with_default_fallbacks ("libpeas-plugin-symbolic"); } + public bool available { get; set; default = false; } + public bool active { get { + return Midori.CoreSettings.get_default ().get_plugin_enabled (id); + } set { + Midori.CoreSettings.get_default ().set_plugin_enabled (id, value); + } } + + construct { + extensions.append (this); + } + + internal Extension (string id, string name, string description, Icon icon) { + Object (id: id, name: name, description: description, icon: icon); + } + } + public class Plugins : Peas.Engine, Loggable { public string builtin_path { get; construct set; } @@ -39,6 +62,24 @@ namespace Midori { var settings = CoreSettings.get_default (); foreach (var plugin in get_plugin_list ()) { debug ("Found plugin %s", plugin.get_name ()); + if (!plugin.is_builtin ()) { + var extension = new Extension ( + "lib%s.so".printf (plugin.get_module_name ()), plugin.get_name (), + plugin.get_description (), new ThemedIcon.with_default_fallbacks (plugin.get_icon_name ())); + try { + extension.available = plugin.is_available (); + extension.notify["active"].connect (() => { + if (extension.active) { + try_load_plugin (plugin); + } + else { + try_unload_plugin (plugin); + } + }); + } catch (Error error) { + critical ("Failed to prepare plugin %s", plugin.get_module_name ()); + } + } if (plugin.is_builtin () || settings.get_plugin_enabled ("lib%s.so".printf (plugin.get_module_name ()))) { if (!try_load_plugin (plugin)) { diff --git a/core/preferences.vala b/core/preferences.vala index faa65c98d..e245a676f 100644 --- a/core/preferences.vala +++ b/core/preferences.vala @@ -208,7 +208,41 @@ namespace Midori { add (_("Privacy"), box); box = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); - box.add (new PeasGtk.PluginManagerView (null)); + var plugins = new Gtk.ListBox (); + plugins.selection_mode = Gtk.SelectionMode.NONE; + foreach (var extension in Extension.extensions) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4); + extension.bind_property ("available", row, "sensitive", BindingFlags.SYNC_CREATE); + checkbox = new Gtk.CheckButton (); + extension.bind_property ("active", checkbox, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + row.pack_start (checkbox, false, false, 4); + var icon = new Gtk.Image.from_gicon (extension.icon, Gtk.IconSize.BUTTON); + // Icon size only applies to named icons + if (extension.icon is Gdk.Pixbuf) { + int icon_width = 16, icon_height = 16; + Gtk.icon_size_lookup ((Gtk.IconSize)icon.icon_size, out icon_width, out icon_height); + // Take scale factor into account + icon_width *= scale_factor; + icon_height *= scale_factor; + icon.pixbuf = ((Gdk.Pixbuf)extension.icon).scale_simple (icon_width, icon_height, Gdk.InterpType.BILINEAR); + } + row.pack_start (icon, false, false, 4); + var label = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); + var name = new Gtk.Label (extension.name); + name.ellipsize = Pango.EllipsizeMode.END; + name.xalign = 0.0f; + label.pack_start (name); + var description = new Gtk.Label (extension.description); + description.ellipsize = Pango.EllipsizeMode.END; + description.xalign = 0.0f; + label.pack_start (description); + row.pack_end (label, true, true, 4); + plugins.add (row); + } + var scrolled = new Gtk.ScrolledWindow (null, null); + scrolled.min_content_height = 333; + scrolled.add (plugins); + box.add (scrolled); box.show_all (); add (_("Extensions"), box); diff --git a/extensions/web-extensions.vala b/extensions/web-extensions.vala index 0b1b99ae1..23413822c 100644 --- a/extensions/web-extensions.vala +++ b/extensions/web-extensions.vala @@ -10,12 +10,10 @@ */ namespace WebExtension { - public class Extension : Object { + public class Extension : Midori.Extension { HashTable _files; public File file { get; protected set; } - public string name { get; set; } - public string description { get; set; } public string? background_page { get; owned set; } public List background_scripts { get; owned set; } public List content_scripts { get; owned set; } @@ -24,7 +22,7 @@ namespace WebExtension { public Action? sidebar { get; set; } public Extension (File file) { - Object (file: file, name: file.get_basename ()); + Object (id: file.get_basename (), file: file, name: file.get_basename ()); } public void add_resource (string resource, Bytes data) { @@ -79,6 +77,8 @@ namespace WebExtension { // "WebExtensionExtensionManager::extension_added" is not a value type public signal void extension_added (Object extension); + public signal void extension_removed (Object extension); + string? pick_default_icon (Json.Object action) { if (action.has_member ("default_icon")) { var node = action.get_member ("default_icon"); @@ -103,21 +103,17 @@ namespace WebExtension { return _default; } - public async void load_from_folder (WebKit.UserContentManager content, File folder) throws Error { + public async void load_from_folder (File folder) throws Error { debug ("Load web extensions from %s", folder.get_path ()); var enumerator = yield folder.enumerate_children_async (FileAttribute.STANDARD_NAME, 0); FileInfo info; while ((info = enumerator.next_file ()) != null) { var file = folder.get_child (info.get_name ()); string id = file.get_basename (); - if (!Midori.CoreSettings.get_default ().get_plugin_enabled (id)) { - continue; - } var extension = extensions.lookup (id); if (extension == null) { InputStream? stream = null; - extension = new Extension (file); try { // Try reading from a ZIP archive ie. .crx (Chrome/ Opera/ Vivaldi), .nex (Opera) or .xpi (Firefox) @@ -126,6 +122,7 @@ namespace WebExtension { var archive = new Archive.Read (); archive.support_format_zip (); if (archive.open_filename (file.get_path (), 10240) == Archive.Result.OK) { + extension = new Extension (file); unowned Archive.Entry entry; while (archive.next_header (out entry) == Archive.Result.OK) { if (entry.pathname () == "manifest.json") { @@ -153,6 +150,7 @@ namespace WebExtension { // If we find a manifest, this is a web extension var manifest_file = file.get_child ("manifest.json"); if (manifest_file.query_exists ()) { + extension = new Extension (file); stream = new DataInputStream (yield manifest_file.read_async ()); } else { continue; @@ -167,6 +165,9 @@ namespace WebExtension { if (manifest.has_member ("name")) { extension.name = manifest.get_string_member ("name"); } + if (manifest.has_member ("description")) { + extension.description = manifest.get_string_member ("description"); + } if (manifest.has_member ("background")) { var background = manifest.get_object_member ("background"); @@ -193,8 +194,21 @@ namespace WebExtension { } } + if (manifest.has_member ("icons")) { + var node = manifest.get_member ("icons"); + if (node.get_node_type () == Json.NodeType.OBJECT) { + foreach (var size in node.get_object ().get_members ()) { + var image = yield extension.get_resource (node.get_object ().get_string_member (size)); + // Note: The from_bytes variant has no autodetection + var image_stream = new MemoryInputStream.from_data (image.get_data (), free); + extension.icon = yield new Gdk.Pixbuf.from_stream_async (image_stream, null); + break; + } + } + } + if (manifest.has_member ("sidebar_action")) { - var sidebar = manifest.has_member ("sidebar_action") ? manifest.get_object_member ("sidebar_action") : null; + var sidebar = manifest.get_object_member ("sidebar_action"); if (sidebar != null) { extension.sidebar = new Action ( pick_default_icon (sidebar), @@ -220,33 +234,20 @@ namespace WebExtension { } } + extension.available = true; + extension.notify["active"].connect (() => { + if (extension.active) { + extension_added (extension); + } else { + extension_removed (extension); + } + }); extensions.insert (id, extension); - extension_added (extension); - } catch (Error error) { - warning ("Failed to load extension '%s': %s\n", extension.name, error.message); - } - } - - foreach (var filename in extension.content_scripts) { - try { - var script = yield extension.get_resource (filename); - content.add_script (new WebKit.UserScript ((string)(script.get_data ()), - WebKit.UserContentInjectedFrames.TOP_FRAME, - WebKit.UserScriptInjectionTime.END, - null, null)); - } catch (Error error) { - warning ("Failed to inject content script for '%s': %s", extension.name, filename); - } - } - foreach (var filename in extension.content_styles) { - try { - var stylesheet = yield extension.get_resource (filename); - content.add_style_sheet (new WebKit.UserStyleSheet ((string)(stylesheet.get_data ()), - WebKit.UserContentInjectedFrames.TOP_FRAME, - WebKit.UserStyleLevel.USER, - null, null)); + if (extension.active) { + extension_added (extension); + } } catch (Error error) { - warning ("Failed to inject content stylesheet for '%s': %s", extension.name, filename); + warning ("Failed to load extension '%s': %s\n", extension != null ? extension.name : id, error.message); } } } @@ -363,7 +364,7 @@ namespace WebExtension { tooltip_text = extension.browser_action.title ?? extension.name; visible = true; focus_on_click = false; - var icon = new Gtk.Image.from_icon_name ("midori-symbolic", Gtk.IconSize.BUTTON); + var icon = new Gtk.Image.from_icon_name ("libpeas-plugin-symbolic", Gtk.IconSize.BUTTON); icon.use_fallback = true; icon.visible = true; if (extension.browser_action.icon != null) { @@ -430,6 +431,31 @@ namespace WebExtension { } async void install_extension (Extension extension) throws Error { + var content = browser.tab.get_user_content_manager (); + foreach (var filename in extension.content_scripts) { + try { + var script = yield extension.get_resource (filename); + content.add_script (new WebKit.UserScript ((string)(script.get_data ()), + WebKit.UserContentInjectedFrames.TOP_FRAME, + WebKit.UserScriptInjectionTime.END, + null, null)); + } catch (Error error) { + warning ("Failed to inject content script for '%s': %s", extension.name, filename); + } + } + + foreach (var filename in extension.content_styles) { + try { + var stylesheet = yield extension.get_resource (filename); + content.add_style_sheet (new WebKit.UserStyleSheet ((string)(stylesheet.get_data ()), + WebKit.UserContentInjectedFrames.TOP_FRAME, + WebKit.UserStyleLevel.USER, + null, null)); + } catch (Error error) { + warning ("Failed to inject content stylesheet for '%s': %s", extension.name, filename); + } + } + if (extension.browser_action != null) { browser.add_button (new Button (extension as Extension)); } @@ -476,14 +502,6 @@ namespace WebExtension { extension_scheme.begin (request); }); - var manager = ExtensionManager.get_default (); - manager.extension_added.connect ((extension) => { - install_extension.begin ((Extension)extension); - }); - manager.foreach ((extension) => { - install_extension.begin ((Extension)extension); - }); - browser.tabs.add.connect (tab_added); if (browser.tab != null) { tab_added (browser.tab); @@ -494,18 +512,34 @@ namespace WebExtension { browser.tabs.add.disconnect (tab_added); var manager = ExtensionManager.get_default (); - var tab = widget as Midori.Tab; + manager.extension_added.connect ((extension) => { + install_extension.begin ((Extension)extension); + }); + manager.extension_removed.connect ((extension) => { + ((Extension)extension).available = false; + }); + manager.foreach ((extension) => { + if (extension.active) { + install_extension.begin ((Extension)extension); + } + }); + } + } + + public class App : Peas.ExtensionBase, Midori.AppActivatable { + public Midori.App app { owned get; set; } - var content = tab.get_user_content_manager (); + public void activate () { + var manager = ExtensionManager.get_default (); // Try and load plugins from build folder var builtin_path = ((Midori.App)Application.get_default ()).exec_path.get_parent ().get_child ("extensions"); - manager.load_from_folder.begin (content, builtin_path); + manager.load_from_folder.begin (builtin_path); // System-wide plugins - manager.load_from_folder.begin (content, File.new_for_path (Config.PLUGINDIR)); + manager.load_from_folder.begin (File.new_for_path (Config.PLUGINDIR)); // Plugins installed by the user string user_path = Path.build_path (Path.DIR_SEPARATOR_S, Environment.get_user_data_dir (), Config.PROJECT_NAME, "extensions"); - manager.load_from_folder.begin (content, File.new_for_path (user_path)); + manager.load_from_folder.begin (File.new_for_path (user_path)); } } } @@ -514,4 +548,6 @@ namespace WebExtension { public void peas_register_types(TypeModule module) { ((Peas.ObjectModule)module).register_extension_type ( typeof (Midori.BrowserActivatable), typeof (WebExtension.Browser)); + ((Peas.ObjectModule)module).register_extension_type ( + typeof (Midori.AppActivatable), typeof (WebExtension.App)); }