/* Copyright (C) 2014 Christian Dywan This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. See the file COPYING for the full license text. */ #if HAVE_WIN32 namespace Sokoke { extern static Gdk.Pixbuf get_gdk_pixbuf_from_win32_executable (string path); } #endif namespace ExternalApplications { /* Spawn the application specified by @app_info on the uri, trying to remember the association between the content-type and the application chosen Returns whether the application was spawned successfully. */ bool open_app_info (AppInfo app_info, string uri, string content_type) { Midori.URI.recursive_fork_protection (uri, true); try { var uris = new List (); uris.append (File.new_for_uri (uri)); app_info.launch (uris, null); } catch (Error error) { warning ("Failed to open \"%s\": %s", uri, error.message); return false; } /* Failing to save the association is a non-fatal error so report success */ try { new Associations ().remember (content_type, app_info); } catch (Error error) { warning ("Failed to save association for \"%s\": %s", uri, error.message); } return true; } class Associations : Object { #if HAVE_WIN32 string config_dir; string filename; KeyFile keyfile; public Associations () { config_dir = Midori.Paths.get_extension_config_dir ("open-with"); filename = Path.build_filename (config_dir, "config"); keyfile = new KeyFile (); try { keyfile.load_from_file (filename, KeyFileFlags.NONE); } catch (FileError.NOENT exist_error) { /* It's no error if no config file exists */ } catch (Error error) { warning ("Failed to load associations: %s", error.message); } } /* Determine a handler command-line for @content_type and spawn it on @uri. Returns whether a handler was found and spawned successfully. */ public bool open (string content_type, string uri) { Midori.URI.recursive_fork_protection (uri, true); try { string commandline = keyfile.get_string ("mimes", content_type); if ("%u" in commandline) commandline = commandline.replace ("%u", Shell.quote (uri)); else if ("%F" in commandline) commandline = commandline.replace ("%F", Shell.quote (Filename.from_uri (uri))); return Process.spawn_command_line_async (commandline); } catch (KeyFileError error) { /* Not remembered before */ return false; } catch (Error error) { warning ("Failed to open \"%s\": %s", uri, error.message); return false; } } /* Save @app_info in the persistent store as the handler for @content_type */ public void remember (string content_type, AppInfo app_info) throws Error { keyfile.set_string ("mimes", content_type, get_commandline (app_info)); FileUtils.set_contents (filename, keyfile.to_data ()); } /* Save @commandline in the persistent store as the handler for @content_type */ public void remember_custom_commandline (string content_type, string commandline, string name, string uri) { keyfile.set_string ("mimes", content_type, commandline); try { FileUtils.set_contents (filename, keyfile.to_data ()); } catch (Error error) { warning ("Failed to remember custom command line for \"%s\": %s", uri, error.message); } open (content_type, uri); } } #else public Associations () { } /* Find a handler application for @content_type and spawn it on @uri. Returns whether a handler was found and spawned successfully. */ public bool open (string content_type, string uri) { var app_info = AppInfo.get_default_for_type (content_type, false); if (app_info == null) return false; return open_app_info (app_info, uri, content_type); } /* Save @app_info as the last-used handler for @content_type */ public void remember (string content_type, AppInfo app_info) throws Error { app_info.set_as_last_used_for_type (content_type); app_info.set_as_default_for_type (content_type); } /* Save @commandline as a new system MIME handler for @content_type */ public void remember_custom_commandline (string content_type, string commandline, string name, string uri) { try { var app_info = AppInfo.create_from_commandline (commandline, name, "%u" in commandline ? AppInfoCreateFlags.SUPPORTS_URIS : AppInfoCreateFlags.NONE); open_app_info (app_info, uri, content_type); } catch (Error error) { warning ("Failed to remember custom command line for \"%s\": %s", uri, error.message); } } } #endif static string get_commandline (AppInfo app_info) { return app_info.get_commandline () ?? app_info.get_executable (); } /* Generate markup of the application's name followed by a description line. */ static string describe_app_info (AppInfo app_info) { string name = app_info.get_display_name () ?? (Path.get_basename (app_info.get_executable ())); string desc = app_info.get_description () ?? get_commandline (app_info); return Markup.printf_escaped ("%s\n%s", name, desc); } static Icon? app_info_get_icon (AppInfo app_info) { #if HAVE_WIN32 return Sokoke.get_gdk_pixbuf_from_win32_executable (app_info.get_executable ()); #else return app_info.get_icon (); #endif } class CustomizerDialog : Gtk.Dialog { public Gtk.Entry name_entry; public Gtk.Entry commandline_entry; public CustomizerDialog (AppInfo app_info, Gtk.Widget widget) { var browser = Midori.Browser.get_for_widget (widget); transient_for = browser; title = _("Custom…"); #if !HAVE_GTK3 has_separator = false; #endif destroy_with_parent = true; set_icon_name (Gtk.STOCK_OPEN); resizable = false; add_buttons (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT); var vbox = new Gtk.VBox (false, 8); vbox.border_width = 8; (get_content_area () as Gtk.Box).pack_start (vbox, true, true, 8); var sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL); var label = new Gtk.Label (_("Name:")); sizegroup.add_widget (label); label.set_alignment (0.0f, 0.5f); vbox.pack_start (label, false, false, 0); name_entry = new Gtk.Entry (); name_entry.activates_default = true; sizegroup.add_widget (name_entry); vbox.pack_start (name_entry, true, true, 0); label = new Gtk.Label (_("Command Line:")); sizegroup.add_widget (label); label.set_alignment (0.0f, 0.5f); vbox.pack_start (label, false, false, 0); commandline_entry = new Gtk.Entry (); commandline_entry.activates_default = true; sizegroup.add_widget (name_entry); sizegroup.add_widget (commandline_entry); vbox.pack_start (commandline_entry, true, true, 0); get_content_area ().show_all (); set_default_response (Gtk.ResponseType.ACCEPT); name_entry.text = app_info.get_name (); commandline_entry.text = get_commandline (app_info); } } private class Chooser : Gtk.VBox { Gtk.ListStore store = new Gtk.ListStore (1, typeof (AppInfo)); Gtk.TreeView treeview; List available; string content_type; string uri; public Chooser (string uri, string content_type) { this.content_type = content_type; this.uri = uri; Gtk.TreeViewColumn column; treeview = new Gtk.TreeView.with_model (store); treeview.headers_visible = false; store.set_sort_column_id (0, Gtk.SortType.ASCENDING); store.set_sort_func (0, tree_sort_func); column = new Gtk.TreeViewColumn (); Gtk.CellRendererPixbuf renderer_icon = new Gtk.CellRendererPixbuf (); column.pack_start (renderer_icon, false); column.set_cell_data_func (renderer_icon, on_render_icon); treeview.append_column (column); column = new Gtk.TreeViewColumn (); column.set_sizing (Gtk.TreeViewColumnSizing.AUTOSIZE); Gtk.CellRendererText renderer_text = new Gtk.CellRendererText (); column.pack_start (renderer_text, true); column.set_expand (true); column.set_cell_data_func (renderer_text, on_render_text); treeview.append_column (column); treeview.row_activated.connect (row_activated); treeview.show (); var scrolled = new Gtk.ScrolledWindow (null, null); scrolled.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); scrolled.add (treeview); pack_start (scrolled); int height; treeview.create_pango_layout ("a\nb").get_pixel_size (null, out height); scrolled.set_size_request (-1, height * 5); treeview.button_release_event.connect (button_released); treeview.tooltip_text = _("Right-click a suggestion to customize it"); available = new List (); foreach (var app_info in AppInfo.get_all_for_type (content_type)) launcher_added (app_info, uri); if (store.iter_n_children (null) < 1) { foreach (var app_info in AppInfo.get_all ()) launcher_added (app_info, uri); } } bool button_released (Gdk.EventButton event) { if (event.button == 3) return show_popup_menu (event); return false; } bool show_popup_menu (Gdk.EventButton? event) { Gtk.TreeIter iter; if (treeview.get_selection ().get_selected (null, out iter)) { AppInfo app_info; store.get (iter, 0, out app_info); var menu = new Gtk.Menu (); var menuitem = new Gtk.ImageMenuItem.with_mnemonic (_("Custom…")); menuitem.image = new Gtk.Image.from_stock (Gtk.STOCK_EDIT, Gtk.IconSize.MENU); menuitem.activate.connect (() => { customize_app_info (app_info, content_type, uri); }); menu.append (menuitem); menu.show_all (); Katze.widget_popup (treeview, menu, null, Katze.MenuPos.CURSOR); return true; } return false; } void customize_app_info (AppInfo app_info, string content_type, string uri) { var dialog = new CustomizerDialog (app_info, this); bool accept = dialog.run () == Gtk.ResponseType.ACCEPT; if (accept) { string name = dialog.name_entry.text; string commandline = dialog.commandline_entry.text; new Associations ().remember_custom_commandline (content_type, commandline, name, uri); customized (app_info, content_type, uri); } dialog.destroy (); } public List get_available () { return available.copy (); } public AppInfo get_app_info () { Gtk.TreeIter iter; if (treeview.get_selection ().get_selected (null, out iter)) { AppInfo app_info; store.get (iter, 0, out app_info); return app_info; } assert_not_reached (); } void on_render_icon (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { AppInfo app_info; model.get (iter, 0, out app_info); renderer.set ("gicon", app_info_get_icon (app_info), "stock-size", Gtk.IconSize.DIALOG, "xpad", 4); } void on_render_text (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { AppInfo app_info; model.get (iter, 0, out app_info); renderer.set ("markup", describe_app_info (app_info), "ellipsize", Pango.EllipsizeMode.END); } void launcher_added (AppInfo app_info, string uri) { if (!app_info.should_show ()) return; Gtk.TreeIter iter; store.append (out iter); store.set (iter, 0, app_info); available.append (app_info); } int tree_sort_func (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) { AppInfo app_info1, app_info2; model.get (a, 0, out app_info1); model.get (b, 0, out app_info2); return strcmp (app_info1.get_display_name (), app_info2.get_display_name ()); } void row_activated (Gtk.TreePath path, Gtk.TreeViewColumn column) { Gtk.TreeIter iter; if (store.get_iter (out iter, path)) { AppInfo app_info; store.get (iter, 0, out app_info); selected (app_info); } } public signal void selected (AppInfo app_info); public signal void customized (AppInfo app_info, string content_type, string uri); } class ChooserDialog : Gtk.Dialog { public Chooser chooser { get; private set; } public ChooserDialog (string uri, string content_type, Gtk.Widget widget) { string filename; if (uri.has_prefix ("file://")) filename = Midori.Download.get_basename_for_display (uri); else filename = uri; var browser = Midori.Browser.get_for_widget (widget); transient_for = browser; title = _("Choose application"); #if !HAVE_GTK3 has_separator = false; #endif destroy_with_parent = true; set_icon_name (Gtk.STOCK_OPEN); resizable = true; add_buttons (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT); var vbox = new Gtk.VBox (false, 8); vbox.border_width = 8; (get_content_area () as Gtk.Box).pack_start (vbox, true, true, 8); var label = new Gtk.Label (_("Select an application to open \"%s\"".printf (filename))); label.ellipsize = Pango.EllipsizeMode.MIDDLE; vbox.pack_start (label, false, false, 0); if (uri == "") label.no_show_all = true; chooser = new Chooser (uri, content_type); vbox.pack_start (chooser, true, true, 0); get_content_area ().show_all (); Gtk.Requisition req; #if HAVE_GTK3 get_content_area ().get_preferred_size (null, out req); #else get_content_area ().size_request (out req); #endif (this as Gtk.Window).set_default_size (req.width*2, req.height*3/2); set_default_response (Gtk.ResponseType.ACCEPT); chooser.selected.connect ((app_info) => { response (Gtk.ResponseType.ACCEPT); }); chooser.customized.connect ((app_info, content_type, uri) => { response (Gtk.ResponseType.CANCEL); }); } public AppInfo? open_with () { show (); bool accept = run () == Gtk.ResponseType.ACCEPT; hide (); if (!accept) return null; return chooser.get_app_info (); } } class ChooserButton : Gtk.Button { public AppInfo? app_info { get; set; } public string? commandline { get; set; } ChooserDialog dialog; Gtk.Label app_name; Gtk.Image icon; public ChooserButton (string mime_type, string? commandline) { string content_type = ContentType.from_mime_type (mime_type); dialog = new ChooserDialog ("", content_type, this); app_info = null; foreach (var candidate in dialog.chooser.get_available ()) { if (get_commandline (candidate) == commandline) app_info = candidate; } var hbox = new Gtk.HBox (false, 4); icon = new Gtk.Image (); hbox.pack_start (icon, false, false, 0); app_name = new Gtk.Label (null); app_name.use_markup = true; app_name.ellipsize = Pango.EllipsizeMode.END; hbox.pack_start (app_name, true, true, 0); add (hbox); show_all (); update_label (); clicked.connect (() => { dialog.transient_for = this.get_toplevel () as Gtk.Window; app_info = dialog.open_with (); string new_commandline = app_info != null ? get_commandline (app_info) : null; commandline = new_commandline; selected (new_commandline); update_label (); }); } void update_label () { app_name.label = app_info != null ? describe_app_info (app_info).replace ("\n", " ") : _("None"); icon.set_from_gicon (app_info != null ? app_info_get_icon (app_info) : null, Gtk.IconSize.BUTTON); } public signal void selected (string? commandline); } class Types : Gtk.VBox { public Gtk.ListStore store = new Gtk.ListStore (2, typeof (string), typeof (AppInfo)); Gtk.TreeView treeview; public Types () { Gtk.TreeViewColumn column; treeview = new Gtk.TreeView.with_model (store); treeview.headers_visible = false; store.set_sort_column_id (0, Gtk.SortType.ASCENDING); store.set_sort_func (0, tree_sort_func); column = new Gtk.TreeViewColumn (); column.set_sizing (Gtk.TreeViewColumnSizing.AUTOSIZE); Gtk.CellRendererPixbuf renderer_type_icon = new Gtk.CellRendererPixbuf (); column.pack_start (renderer_type_icon, false); column.set_cell_data_func (renderer_type_icon, on_render_type_icon); treeview.append_column (column); column = new Gtk.TreeViewColumn (); column.set_sizing (Gtk.TreeViewColumnSizing.AUTOSIZE); Gtk.CellRendererText renderer_type_text = new Gtk.CellRendererText (); column.pack_start (renderer_type_text, true); column.set_cell_data_func (renderer_type_text, on_render_type_text); treeview.append_column (column); column = new Gtk.TreeViewColumn (); column.set_sizing (Gtk.TreeViewColumnSizing.AUTOSIZE); Gtk.CellRendererPixbuf renderer_icon = new Gtk.CellRendererPixbuf (); column.pack_start (renderer_icon, false); column.set_cell_data_func (renderer_icon, on_render_icon); treeview.append_column (column); column = new Gtk.TreeViewColumn (); column.set_sizing (Gtk.TreeViewColumnSizing.AUTOSIZE); Gtk.CellRendererText renderer_text = new Gtk.CellRendererText (); column.pack_start (renderer_text, true); column.set_expand (true); column.set_cell_data_func (renderer_text, on_render_text); treeview.append_column (column); treeview.row_activated.connect (row_activated); treeview.show (); var scrolled = new Gtk.ScrolledWindow (null, null); scrolled.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); scrolled.add (treeview); pack_start (scrolled); int height; treeview.create_pango_layout ("a\nb").get_pixel_size (null, out height); scrolled.set_size_request (-1, height * 5); foreach (unowned string content_type in ContentType.list_registered ()) launcher_added (content_type); foreach (unowned string scheme in Vfs.get_default ().get_supported_uri_schemes ()) launcher_added ("x-scheme-handler/" + scheme); treeview.size_allocate.connect_after ((allocation) => { treeview.columns_autosize (); }); } void on_render_type_icon (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { string content_type; store.get (iter, 0, out content_type); renderer.set ("gicon", ContentType.get_icon (content_type), "stock-size", Gtk.IconSize.BUTTON, "xpad", 4); } void on_render_type_text (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { string content_type; AppInfo app_info; store.get (iter, 0, out content_type, 1, out app_info); string desc, mime_type; if (content_type.has_prefix ("x-scheme-handler/")) { desc = content_type.split ("/")[1] + "://"; mime_type = ""; } else { desc = ContentType.get_description (content_type); mime_type = ContentType.get_mime_type (content_type); } renderer.set ("markup", Markup.printf_escaped ("%s\n%s", desc, mime_type), #if HAVE_GTK3 "max-width-chars", 30, #else "width-chars", 30, #endif "ellipsize", Pango.EllipsizeMode.END); } void on_render_icon (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { AppInfo app_info; model.get (iter, 1, out app_info); renderer.set ("gicon", app_info_get_icon (app_info), "stock-size", Gtk.IconSize.MENU, "xpad", 4); } void on_render_text (Gtk.CellLayout column, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { AppInfo app_info; model.get (iter, 1, out app_info); renderer.set ("markup", describe_app_info (app_info), "ellipsize", Pango.EllipsizeMode.END); } void launcher_added (string content_type) { var app_info = AppInfo.get_default_for_type (content_type, false); if (app_info == null) return; Gtk.TreeIter iter; store.append (out iter); store.set (iter, 0, content_type, 1, app_info); } int tree_sort_func (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) { string content_type1, content_type2; model.get (a, 0, out content_type1); model.get (b, 0, out content_type2); return strcmp (content_type1, content_type2); } void row_activated (Gtk.TreePath path, Gtk.TreeViewColumn column) { Gtk.TreeIter iter; if (store.get_iter (out iter, path)) { string content_type; store.get (iter, 0, out content_type); selected (content_type, iter); } } public signal void selected (string content_type, Gtk.TreeIter iter); } private class Manager : Midori.Extension { enum NextStep { TRY_OPEN, OPEN_WITH } bool open_uri (Midori.Tab tab, string uri) { string content_type = get_content_type (uri, null); return open_with_type (uri, content_type, tab, NextStep.TRY_OPEN); } bool navigation_requested (Midori.Tab tab, string uri) { /* FIXME: Make this a list of known/internal uris to pass-thru */ if (uri.has_prefix ("file://") || Midori.URI.is_http (uri) || Midori.URI.is_blank (uri)) return false; /* Don't find app for abp links, we should handle them internally */ if (uri.has_prefix("abp:")) return true; string content_type = get_content_type (uri, null); open_with_type (uri, content_type, tab, NextStep.TRY_OPEN); return true; } string get_content_type (string uri, string? mime_type) { if (!uri.has_prefix ("file://") && !Midori.URI.is_http (uri)) { string protocol = uri.split(":", 2)[0]; return "x-scheme-handler/" + protocol; } else if (mime_type == null) { string filename; bool uncertain; try { filename = Filename.from_uri (uri); } catch (Error error) { filename = uri; } return ContentType.guess (filename, null, out uncertain); } return ContentType.from_mime_type (mime_type); } /* Returns %TRUE if the attempt to download and open failed immediately, %FALSE otherwise */ bool open_with_type (string uri, string content_type, Gtk.Widget widget, NextStep next_step) { #if HAVE_WEBKIT2 return open_now (uri, content_type, widget, next_step); #else if (!Midori.URI.is_http (uri)) return open_now (uri, content_type, widget, next_step); /* Don't download websites */ if (content_type == "application/octet-stream") return open_now (uri, content_type, widget, next_step); var download = new WebKit.Download (new WebKit.NetworkRequest (uri)); download.destination_uri = Midori.Download.prepare_destination_uri (download, null); if (!Midori.Download.has_enough_space (download, download.destination_uri)) return false; download.notify["status"].connect ((pspec) => { if (download.status == WebKit.DownloadStatus.FINISHED) { open_now (download.destination_uri, content_type, widget, next_step); } else if (download.status == WebKit.DownloadStatus.ERROR) Midori.show_message_dialog (Gtk.MessageType.ERROR, _("Download error"), _("Cannot open '%s' because the download failed." ).printf (download.destination_uri), false); }); download.start (); return true; #endif } /* If @next_step is %NextStep.TRY_OPEN, tries to pick a handler automatically. If the automatic handler did not exist or could not run, asks for an application. Returns whether an application was found and launched successfully. */ bool open_now (string uri, string content_type, Gtk.Widget widget, NextStep next_step) { if (next_step == NextStep.TRY_OPEN && (new Associations ()).open (content_type, uri)) return true; /* if opening directly failed or wasn't tried, ask for an association */ if (open_with (uri, content_type, widget) != null) return true; return false; } /* Returns the application chosen to open the uri+content_type if the application was spawned successfully, %NULL if none was chosen or running was unsuccessful. */ AppInfo? open_with (string uri, string content_type, Gtk.Widget widget) { var dialog = new ChooserDialog (uri, content_type, widget); var app_info = dialog.open_with (); dialog.destroy (); if (uri == "") return app_info; if (app_info == null) return app_info; return open_app_info (app_info, uri, content_type) ? app_info : null; } void context_menu (Midori.Tab tab, WebKit.HitTestResult hit_test_result, Midori.ContextAction menu) { if ((hit_test_result.context & WebKit.HitTestResultContext.LINK) != 0) { string uri = hit_test_result.link_uri; var action = new Gtk.Action ("OpenWith", _("Open _with…"), null, null); action.activate.connect ((action) => { open_with_type (uri, get_content_type (uri, null), tab, NextStep.OPEN_WITH); }); menu.add (action); } #if !HAVE_WEBKIT2 if ((hit_test_result.context & WebKit.HitTestResultContext.IMAGE) != 0) { string uri = hit_test_result.image_uri; var action = new Gtk.Action ("OpenImageInViewer", _("Open in Image _Viewer"), null, null); action.activate.connect ((action) => { open_with_type (uri, get_content_type (uri, null), tab, NextStep.TRY_OPEN); }); menu.add (action); } #endif } void show_preferences (Katze.Preferences preferences) { var settings = get_app ().settings; var category = preferences.add_category (_("File Types"), Gtk.STOCK_FILE); preferences.add_group (null); var sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL); var label = new Gtk.Label (_("Text Editor")); sizegroup.add_widget (label); label.set_alignment (0.0f, 0.5f); preferences.add_widget (label, "indented"); var entry = new ChooserButton ("text/plain", settings.text_editor); sizegroup.add_widget (entry); entry.selected.connect ((commandline) => { settings.text_editor = commandline; }); preferences.add_widget (entry, "spanned"); label = new Gtk.Label (_("News Aggregator")); sizegroup.add_widget (label); label.set_alignment (0.0f, 0.5f); preferences.add_widget (label, "indented"); entry = new ChooserButton ("application/rss+xml", settings.news_aggregator); sizegroup.add_widget (entry); entry.selected.connect ((commandline) => { settings.news_aggregator = commandline; }); preferences.add_widget (entry, "spanned"); var types = new Types (); types.selected.connect ((content_type, iter) => { var app_info = open_with ("", content_type, preferences); if (app_info == null) return; try { app_info.set_as_default_for_type (content_type); types.store.set (iter, 1, app_info); } catch (Error error) { warning ("Failed to select default for \"%s\": %s", content_type, error.message); } }); category.pack_start (types, true, true, 0); types.show_all (); } public void tab_added (Midori.Browser browser, Midori.View view) { view.navigation_requested.connect_after (navigation_requested); view.open_uri.connect (open_uri); view.context_menu.connect (context_menu); } public void tab_removed (Midori.Browser browser, Midori.View view) { view.navigation_requested.disconnect (navigation_requested); view.open_uri.disconnect (open_uri); view.context_menu.disconnect (context_menu); } void browser_added (Midori.Browser browser) { foreach (var tab in browser.get_tabs ()) tab_added (browser, tab); browser.add_tab.connect (tab_added); browser.remove_tab.connect (tab_removed); browser.show_preferences.connect (show_preferences); } void activated (Midori.App app) { foreach (var browser in app.get_browsers ()) browser_added (browser); app.add_browser.connect (browser_added); } void browser_removed (Midori.Browser browser) { foreach (var tab in browser.get_tabs ()) tab_removed (browser, tab); browser.add_tab.disconnect (tab_added); browser.remove_tab.disconnect (tab_removed); browser.show_preferences.disconnect (show_preferences); } void deactivated () { var app = get_app (); foreach (var browser in app.get_browsers ()) browser_removed (browser); app.add_browser.disconnect (browser_added); } internal Manager () { GLib.Object (name: "External Applications", description: "Choose what to open unknown file types with", version: "0.1" + Midori.VERSION_SUFFIX, authors: "Christian Dywan "); this.activate.connect (activated); this.deactivate.connect (deactivated); } } } public Midori.Extension extension_init () { return new ExternalApplications.Manager (); }