/*
   Copyright (C) 2014 Christian Dywan <christian@twotoasts.de>

   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<File> ();
            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 ("<b>%s</b>\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<AppInfo> 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<AppInfo> ();
            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<AppInfo> 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 = false;
            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 ();
            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 (() => {
                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 (string content_type in ContentType.list_registered ())
                launcher_added (content_type);
            foreach (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 ("<b>%s</b>\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) {
            return open_with_type (uri, get_content_type (uri, null), tab, NextStep.TRY_OPEN);
        }

        bool navigation_requested (Midori.Tab tab, string uri) {
            if (uri.has_prefix ("file://") || Midori.URI.is_http (uri) || Midori.URI.is_blank (uri))
                return false;

            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);

            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 <christian@twotoasts.de>");

            this.activate.connect (activated);
            this.deactivate.connect (deactivated);
        }
    }
}

public Midori.Extension extension_init () {
    return new ExternalApplications.Manager ();
}

