/*
 Copyright (C) 2008-2010 Christian Dywan <christian@twotoasts.de>
 Copyright (C) 2008-2010 Dale Whittaker <dayul@users.sf.net>

 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.
*/

#include "midori-locationaction.h"

#include "marshal.h"
#include "midori-browser.h"
#include "midori-searchaction.h"
#include "midori-app.h"
#include "midori-platform.h"
#include <midori/midori-core.h>

#include "config.h"
#include <string.h>
#include <glib/gi18n.h>
#include <gdk/gdkkeysyms.h>

#include <sqlite3.h>

struct _MidoriLocationAction
{
    GtkAction parent_instance;

    GIcon* icon;
    gchar* text;
    KatzeArray* search_engines;
    gdouble progress;
    gchar* secondary_icon;
    gchar* tooltip;
    gchar* placeholder;

    gchar* key;
    MidoriAutocompleter* autocompleter;
    GtkWidget* popup;
    GtkWidget* treeview;
    GtkTreeModel* completion_model;
    gint completion_index;
    GtkWidget* entry;
    KatzeArray* history;
};

struct _MidoriLocationActionClass
{
    GtkActionClass parent_class;
};

G_DEFINE_TYPE (MidoriLocationAction, midori_location_action, GTK_TYPE_ACTION)

enum
{
    PROP_0,

    PROP_PROGRESS,
    PROP_SECONDARY_ICON,
    PROP_HISTORY,
    PROP_PLACEHOLDER_TEXT
};

enum
{
    ACTIVE_CHANGED,
    FOCUS_IN,
    FOCUS_OUT,
    SECONDARY_ICON_RELEASED,
    RESET_URI,
    SUBMIT_URI,
    KEY_PRESS_EVENT,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

static void
midori_location_action_finalize (GObject* object);

static void
midori_location_action_set_property (GObject*      object,
                                     guint         prop_id,
                                     const GValue* value,
                                     GParamSpec*   pspec);

static void
midori_location_action_get_property (GObject*    object,
                                     guint       prop_id,
                                     GValue*     value,
                                     GParamSpec* pspec);

static void
midori_location_action_activate (GtkAction* object);

static GtkWidget*
midori_location_action_create_tool_item (GtkAction* action);

static void
midori_location_action_connect_proxy (GtkAction* action,
                                      GtkWidget* proxy);

static void
midori_location_action_disconnect_proxy (GtkAction* action,
                                         GtkWidget* proxy);

static void
midori_location_action_popdown_completion (MidoriLocationAction* location_action);

extern GtkMenu* 
midori_search_action_get_menu (GtkWidget* entry,
                               MidoriSearchAction *search_action,
                               void (*change_cb)(GtkWidget*, MidoriSearchAction*));

static void
midori_location_action_class_init (MidoriLocationActionClass* class)
{
    GObjectClass* gobject_class;
    GtkActionClass* action_class;

    /**
     * MidoriLocationAction:focus-in:
     *
     * The focus-in signal is emitted when the entry obtains the focus.
     *
     * Since 0.1.8
     */
    signals[FOCUS_IN] = g_signal_new ("focus-in",
                                      G_TYPE_FROM_CLASS (class),
                                      (GSignalFlags) (G_SIGNAL_RUN_LAST),
                                      0,
                                      0,
                                      NULL,
                                      g_cclosure_marshal_VOID__VOID,
                                      G_TYPE_NONE, 0);

    signals[FOCUS_OUT] = g_signal_new ("focus-out",
                                       G_TYPE_FROM_CLASS (class),
                                       (GSignalFlags) (G_SIGNAL_RUN_LAST),
                                       0,
                                       0,
                                       NULL,
                                       g_cclosure_marshal_VOID__VOID,
                                       G_TYPE_NONE, 0);

    /**
     * MidoriLocationAction:secondary-icon-released:
     *
     * The secondary-icon-released signal is emitted when the mouse button
     * is released above the secondary icon.
     *
     * Since 0.1.10 a signal handler can return %TRUE to stop signal
     * emission, for instance to suppress default behavior.
     */
    signals[SECONDARY_ICON_RELEASED] = g_signal_new ("secondary-icon-released",
                                       G_TYPE_FROM_CLASS (class),
                                       (GSignalFlags) (G_SIGNAL_RUN_LAST),
                                       0,
                                       g_signal_accumulator_true_handled,
                                       NULL,
                                       midori_cclosure_marshal_BOOLEAN__OBJECT,
                                       G_TYPE_BOOLEAN, 1,
                                       GTK_TYPE_WIDGET);

    signals[RESET_URI] = g_signal_new ("reset-uri",
                                       G_TYPE_FROM_CLASS (class),
                                       (GSignalFlags) (G_SIGNAL_RUN_LAST),
                                       0,
                                       0,
                                       NULL,
                                       g_cclosure_marshal_VOID__VOID,
                                       G_TYPE_NONE, 0);

    signals[SUBMIT_URI] = g_signal_new ("submit-uri",
                                        G_TYPE_FROM_CLASS (class),
                                        (GSignalFlags) (G_SIGNAL_RUN_LAST),
                                        0,
                                        0,
                                        NULL,
                                        midori_cclosure_marshal_VOID__STRING_BOOLEAN,
                                        G_TYPE_NONE, 2,
                                        G_TYPE_STRING,
                                        G_TYPE_BOOLEAN);

    /**
     * MidoriLocationAction:key-press-event:
     *
     * A key (combination) was pressed in an entry of the action.
     *
     * Since 0.5.8
     */
    signals[KEY_PRESS_EVENT] = g_signal_new ("key-press-event",
                                             G_TYPE_FROM_CLASS (class),
                                             (GSignalFlags) (G_SIGNAL_RUN_LAST),
                                             0,
                                             0,
                                             NULL,
                                             midori_cclosure_marshal_BOOLEAN__POINTER,
                                             G_TYPE_BOOLEAN, 1,
                                             GDK_TYPE_EVENT);

    gobject_class = G_OBJECT_CLASS (class);
    gobject_class->finalize = midori_location_action_finalize;
    gobject_class->set_property = midori_location_action_set_property;
    gobject_class->get_property = midori_location_action_get_property;

    action_class = GTK_ACTION_CLASS (class);
    action_class->activate = midori_location_action_activate;
    action_class->create_tool_item = midori_location_action_create_tool_item;
    action_class->connect_proxy = midori_location_action_connect_proxy;
    action_class->disconnect_proxy = midori_location_action_disconnect_proxy;

    g_object_class_install_property (gobject_class,
                                     PROP_PROGRESS,
                                     g_param_spec_double (
                                     "progress",
                                     "Progress",
                                     "The current progress of the action",
                                     0.0, 1.0, 0.0,
                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property (gobject_class,
                                     PROP_SECONDARY_ICON,
                                     g_param_spec_string (
                                     "secondary-icon",
                                     "Secondary",
                                     "The stock ID of the secondary icon",
                                     NULL,
                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    /**
     * MidoriLocationAction:history:
     *
     * The list of history items.
     *
     * This is actually a reference to a history instance.
     *
     * Since 0.1.8
     */
    g_object_class_install_property (gobject_class,
                                     PROP_HISTORY,
                                     g_param_spec_object (
                                     "history",
                                     "History",
                                     "The list of history items",
                                     KATZE_TYPE_ARRAY,
                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    /**
     * MidoriLocationAction:placeholder-text:
     *
     * Hint displayed if entry text is empty.
     */
    g_object_class_install_property (gobject_class,
                                     PROP_PLACEHOLDER_TEXT,
                                     g_param_spec_string (
                                     "placeholder-text",
                                     "Placeholder Text",
                                     "Hint displayed if entry text is empty",
                                     NULL,
                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

gchar*
midori_location_action_render_uri (gchar**      keys,
                                   const gchar* uri_escaped)
{
    gchar* uri_unescaped = midori_uri_unescape (uri_escaped);
    gchar* uri = g_strescape (uri_unescaped, NULL);
    g_free (uri_unescaped);

    gchar* stripped_uri = midori_uri_strip_prefix_for_display (uri);
    gchar* temp;
    gchar* temp_iter = temp = g_utf8_strdown (stripped_uri, -1);
    gchar* desc_iter = stripped_uri;
    gint key_idx = 0;
    gchar* key = keys[key_idx];
    gchar* start;
    gchar* desc_uri = NULL;
    while (key && (start = strstr (temp_iter, key)))
    {
        gsize len = strlen (key);
        if (len)
        {
            gint offset = (start - temp_iter);
            gchar* skey = g_strndup (desc_iter + offset, len);
            gchar** parts = g_strsplit (desc_iter, skey, 2);
            if (parts[0] && parts[1])
            {
                if (desc_uri)
                {
                    gchar* temp_markup = g_markup_printf_escaped ("%s<b>%s</b>", parts[0], skey);
                    gchar* temp_concat = g_strconcat (desc_uri, temp_markup, NULL);
                    g_free (temp_markup);
                    katze_assign (desc_uri, temp_concat);
                }
                else
                    desc_uri = g_markup_printf_escaped ("%s<b>%s</b>", parts[0], skey);
            }
            g_strfreev (parts);
            g_free (skey);

            offset += len;
            temp_iter += offset;
            desc_iter += offset;
        }
        key_idx++;
        key = keys[key_idx];
        if (key == NULL)
            break;
    }
    if (key)
        katze_assign (desc_uri,g_markup_escape_text (stripped_uri, -1));
    else
    {
        gchar* temp_markup = g_markup_escape_text (desc_iter, -1);
        gchar* temp_concat = g_strconcat (desc_uri, temp_markup, NULL);
        g_free (temp_markup);
        katze_assign (desc_uri, temp_concat);
    }
    g_free (temp);
    g_free (stripped_uri);
    return desc_uri;
}

gchar*
midori_location_action_render_title (gchar**      keys,
                                     const gchar* title)
{
    gchar* temp;
    gchar* temp_iter = temp = g_utf8_strdown (title, -1);
    const gchar* desc_iter = title;
    gint key_idx = 0;
    gchar* key = keys[key_idx];
    gchar* start;
    gchar* desc_title = NULL;
    while (key && (start = strstr (temp_iter, key)))
    {
        gsize len = strlen (key);
        if (len)
        {
            gint offset = (start - temp_iter);
            gchar* skey = g_strndup (desc_iter + offset, len);
            gchar** parts = g_strsplit (desc_iter, skey, 2);
            if (parts[0] && parts[1])
            {
                if (desc_title)
                {
                    gchar* temp_markup = g_markup_printf_escaped ("%s<b>%s</b>", parts[0], skey);
                    gchar* temp_concat = g_strconcat (desc_title, temp_markup, NULL);
                    g_free (temp_markup);
                    katze_assign (desc_title, temp_concat);
                }
                else
                    desc_title = g_markup_printf_escaped ("%s<b>%s</b>", parts[0], skey);
            }
            g_strfreev (parts);
            g_free (skey);

            offset += len;
            temp_iter += offset;
            desc_iter += offset;
        }
        key_idx++;
        key = keys[key_idx];
        if (key == NULL)
            break;
    }
    if (key)
        katze_assign (desc_title, g_markup_escape_text (title, -1));
    else
    {
        gchar* temp_markup = g_markup_escape_text (desc_iter, -1);
        gchar* temp_concat = g_strconcat (desc_title, temp_markup, NULL);
        g_free (temp_markup);
        katze_assign (desc_title, temp_concat);
    }
    g_free (temp);
    return desc_title;
}

static void
midori_location_entry_render_title_cb (GtkCellLayout*   layout,
                                       GtkCellRenderer* renderer,
                                       GtkTreeModel*    model,
                                       GtkTreeIter*     iter,
                                       gpointer         data)
{
    MidoriLocationAction* action = data;
    gchar* title;
    gchar* desc;

    gtk_tree_model_get (model, iter,
        MIDORI_AUTOCOMPLETER_COLUMNS_MARKUP, &title,
        -1);

    if (strchr (title, '\n')) /* A search engine or action suggestion */
    {
        gchar** parts = g_strsplit (title, "\n", 2);
        desc = g_strdup (parts[0]);
        g_strfreev (parts);
    }
    else
    {
        gchar* key = g_utf8_strdown (action->key ? action->key : "", -1);
        gchar** keys = g_strsplit_set (key, " %", -1);
        g_free (key);
        desc = midori_location_action_render_title (keys, title);
        g_strfreev (keys);
    }

    g_object_set (renderer, "markup", desc,
        "ellipsize-set", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    g_free (desc);
    g_free (title);
}

static void
midori_location_entry_render_uri_cb (GtkCellLayout*   layout,
                                     GtkCellRenderer* renderer,
                                     GtkTreeModel*    model,
                                     GtkTreeIter*     iter,
                                     gpointer         data)
{
    MidoriLocationAction* action = data;
    gchar* title;
    gchar* uri_escaped;
    gchar* desc;

    gtk_tree_model_get (model, iter,
        MIDORI_AUTOCOMPLETER_COLUMNS_MARKUP, &title,
        MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri_escaped,
        -1);

    if (strchr (title, '\n')) /* A search engine or action suggestion */
    {
        gchar** parts = g_strsplit (title, "\n", 2);
        desc = g_strdup (parts[1]);
        g_strfreev (parts);
    }
    else
    {
        gchar* key = g_utf8_strdown (action->key ? action->key : "", -1);
        gchar** keys = g_strsplit_set (key, " %", -1);
        g_free (key);
        desc = midori_location_action_render_uri (keys, uri_escaped);
        g_strfreev (keys);
        g_free (uri_escaped);
    }

    g_object_set (renderer, "markup", desc,
        "ellipsize-set", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    g_free (desc);
    g_free (title);
}

static void
midori_location_action_popup_position (MidoriLocationAction* action,
                                       gint                  matches)
{
    GtkWidget* popup = action->popup;
    GtkWidget* widget = action->entry;
    GdkWindow* window = gtk_widget_get_window (widget);
    gint wx, wy, items;
    GtkRequisition menu_req;
    GtkRequisition widget_req;
    GdkScreen* screen;
    gint monitor_num;
    GdkRectangle monitor;
    GtkAllocation alloc;
    gint height, sep, width, toplevel_height;
    GtkWidget* scrolled = gtk_widget_get_parent (action->treeview);
    GtkWidget* toplevel;

    if (!window)
        return;

    gtk_widget_get_allocation (widget, &alloc);
    #if GTK_CHECK_VERSION (3, 0, 0)
    gtk_widget_get_preferred_size (widget, &widget_req, NULL);
    #else
    gtk_widget_size_request (widget, &widget_req);
    #endif
    gdk_window_get_origin (window, &wx, &wy);

    #if GTK_CHECK_VERSION (3, 0, 0)
    wx += alloc.x;
    wy += alloc.y + (alloc.height - widget_req.height) / 2;
    #endif

    gtk_tree_view_column_cell_get_size (
        gtk_tree_view_get_column (GTK_TREE_VIEW (action->treeview), 0),
        NULL, NULL, NULL, NULL, &height);
    if (height == 0)
        return;
    gtk_widget_style_get (action->treeview, "vertical-separator", &sep, NULL);
    height += sep;

    /* Constrain to screen/ window size */
    screen = gtk_widget_get_screen (widget);
    monitor_num = gdk_screen_get_monitor_at_window (screen, window);
    gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor);
    toplevel = gtk_widget_get_toplevel (widget);
    gtk_window_get_size (GTK_WINDOW (toplevel), NULL, &toplevel_height);
    toplevel_height = MIN (toplevel_height, monitor.height);
    if (wy > toplevel_height / 2)
        items = MIN (matches, ((monitor.y + wy) / height) - 1);
    else
        items = MIN (matches, ((toplevel_height - wy) / height) - 1);
    width = MIN (alloc.width, monitor.width);

    gtk_tree_view_columns_autosize (GTK_TREE_VIEW (action->treeview));
    #if GTK_CHECK_VERSION (3, 0, 0)
    gtk_widget_set_size_request (scrolled, width, -1);
    gtk_scrolled_window_set_min_content_width (GTK_SCROLLED_WINDOW (scrolled), width);
    gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scrolled), items * height);
    gtk_widget_get_preferred_size (popup, &menu_req, NULL);
    #else
    gtk_widget_set_size_request (scrolled, width, items * height);
    gtk_widget_size_request (popup, &menu_req);
    #endif

    if (wx < monitor.x)
        wx = monitor.x;
    else if (wx + menu_req.width > monitor.x + monitor.width)
        wx = monitor.x + monitor.width - menu_req.width;

    if (wy + widget_req.height + menu_req.height <= monitor.y + monitor.height ||
        wy - monitor.y < (monitor.y + monitor.height) - (wy + widget_req.height))
        wy += widget_req.height;
    else
        wy -= menu_req.height;

    gtk_window_move (GTK_WINDOW (popup), wx, wy);
}

static void
midori_location_action_entry_set_text (GtkWidget*   entry,
                                       const gchar* text)
{
    /* Retain selection/ primary clipboard when replacing text ie. switching tabs */
    gchar* selection = NULL;
    GtkClipboard* clipboard;

    if (gtk_widget_get_realized (entry))
    {
        clipboard = gtk_widget_get_clipboard (entry, GDK_SELECTION_PRIMARY);
        int start, end;
        if (gtk_clipboard_get_owner (clipboard) == G_OBJECT (entry)
         && gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start, &end))
            selection = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
    }

    gtk_entry_set_text (GTK_ENTRY (entry), text);

    if (selection != NULL)
    {
        gtk_clipboard_set_text (clipboard, selection, strlen (selection));
        g_free (selection);
    }
}

static void
midori_location_action_complete (MidoriLocationAction* action,
                                 gboolean              new_tab,
                                 const gchar*          uri)
{
    if (midori_autocompleter_can_action (action->autocompleter, uri))
        midori_autocompleter_action (action->autocompleter, uri, action->key, NULL, NULL);
    else
    {
        midori_location_action_popdown_completion (action);
        midori_location_action_entry_set_text (action->entry, uri);
        g_signal_emit (action, signals[SUBMIT_URI], 0, uri, new_tab);
    }
}

static gboolean
midori_location_action_treeview_button_press_cb (GtkWidget*            treeview,
                                                 GdkEventButton*       event,
                                                 MidoriLocationAction* action)
{
    GtkTreePath* path;

    if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview),
        event->x, event->y, &path, NULL, NULL, NULL))
    {
        GtkTreeIter iter;
        gchar* uri;

        gtk_tree_model_get_iter (action->completion_model, &iter, path);
        gtk_tree_path_free (path);
        gtk_tree_model_get (action->completion_model, &iter,
            MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1);
        midori_location_action_complete (action,
            MIDORI_MOD_NEW_TAB (event->state), uri);
        g_free (uri);

        return TRUE;
    }

    return FALSE;
}

static void
midori_location_action_populated_suggestions_cb (MidoriAutocompleter*  autocompleter,
                                                 guint                 count,
                                                 MidoriLocationAction* action)
{
    GtkTreePath* path = gtk_tree_path_new_first ();
    gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (action->treeview), path, NULL,
        FALSE, 0.0, 0.0);
    gtk_tree_path_free (path);
    midori_location_action_popup_position (action, count);
}

void
midori_app_set_browsers (MidoriApp*     app,
                         KatzeArray*    browsers,
                         MidoriBrowser* browser);

static gboolean
midori_location_action_popup_timeout_cb (gpointer data)
{
    MidoriLocationAction* action = data;
    GtkTreeViewColumn* column;

    if (!gtk_widget_has_focus (action->entry))
        return FALSE;

    /* Empty string or starting with a space means: no completion */
    if (!(action->key && *action->key && *action->key != ' '))
    {
        midori_location_action_popdown_completion (action);
        return FALSE;
    }

    if (action->autocompleter == NULL)
    {
        MidoriApp* app = midori_app_new_proxy (NULL);
        MidoriBrowser* browser = midori_browser_get_for_widget (action->entry);
        g_object_set (app,
            "history", action->history,
            "search-engines", action->search_engines,
            NULL);
        /* FIXME: tabs of multiple windows */
        KatzeArray* browsers = katze_array_new (MIDORI_TYPE_BROWSER);
        katze_array_add_item (browsers, browser);
        midori_app_set_browsers (app, browsers, browser);
        action->autocompleter = midori_autocompleter_new (G_OBJECT (app));
        g_signal_connect (action->autocompleter, "populated",
            G_CALLBACK (midori_location_action_populated_suggestions_cb), action);
        g_object_unref (app);
        midori_autocompleter_add (action->autocompleter,
            MIDORI_COMPLETION (midori_view_completion_new ()));
        /* FIXME: Currently HistoryCompletion doesn't work in memory */
        if (action->history != NULL)
            midori_autocompleter_add (action->autocompleter,
                MIDORI_COMPLETION (midori_history_completion_new ()));
        midori_autocompleter_add (action->autocompleter,
            MIDORI_COMPLETION (midori_search_completion_new ()));
    }

    if (!midori_autocompleter_can_complete (action->autocompleter, action->key))
    {
        midori_location_action_popdown_completion (action);
        return FALSE;
    }

    midori_autocompleter_complete (action->autocompleter, action->key, NULL, NULL);

    if (G_UNLIKELY (!action->popup))
    {
        GtkWidget* popup;
        GtkWidget* popup_frame;
        GtkWidget* scrolled;
        GtkWidget* treeview;
        GtkCellRenderer* renderer;

        action->completion_model = (GtkTreeModel*)midori_autocompleter_get_model (action->autocompleter);

        popup = gtk_window_new (GTK_WINDOW_POPUP);
        gtk_window_set_type_hint (GTK_WINDOW (popup), GDK_WINDOW_TYPE_HINT_COMBO);
        /* Window managers may ignore programmatic resize without this */
        gtk_window_set_resizable (GTK_WINDOW (popup), FALSE);
        popup_frame = gtk_frame_new (NULL);
        gtk_frame_set_shadow_type (GTK_FRAME (popup_frame), GTK_SHADOW_ETCHED_IN);
        gtk_container_add (GTK_CONTAINER (popup), popup_frame);
        scrolled = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
            "hscrollbar-policy", GTK_POLICY_NEVER,
            "vscrollbar-policy", GTK_POLICY_AUTOMATIC, NULL);
        gtk_container_add (GTK_CONTAINER (popup_frame), scrolled);
        treeview = gtk_tree_view_new_with_model (action->completion_model);
        gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE);
        gtk_tree_view_set_hover_selection (GTK_TREE_VIEW (treeview), TRUE);
        gtk_container_add (GTK_CONTAINER (scrolled), treeview);
        g_signal_connect (treeview, "button-press-event",
            G_CALLBACK (midori_location_action_treeview_button_press_cb), action);
        /* a nasty hack to get the completions treeview to size nicely */
        gtk_widget_set_size_request (gtk_scrolled_window_get_vscrollbar (
            GTK_SCROLLED_WINDOW (scrolled)), -1, 0);
        action->treeview = treeview;
        #if !GTK_CHECK_VERSION (3, 4, 0)
        gtk_widget_realize (action->treeview);
        #endif

        column = gtk_tree_view_column_new ();
        renderer = gtk_cell_renderer_pixbuf_new ();
        gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), renderer, FALSE);
        gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (column), renderer,
            "gicon", MIDORI_AUTOCOMPLETER_COLUMNS_ICON,
            "stock-size", MIDORI_AUTOCOMPLETER_COLUMNS_SIZE,
            "yalign", MIDORI_AUTOCOMPLETER_COLUMNS_YALIGN,
            "cell-background", MIDORI_AUTOCOMPLETER_COLUMNS_BACKGROUND,
            NULL);
        renderer = gtk_cell_renderer_text_new ();
        gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), renderer, TRUE);
        gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (column), renderer,
            "cell-background", MIDORI_AUTOCOMPLETER_COLUMNS_BACKGROUND,
            NULL);
        gtk_tree_view_column_set_expand (column, TRUE);
        gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), renderer,
            midori_location_entry_render_title_cb, action, NULL);

        renderer = gtk_cell_renderer_text_new ();
        gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), renderer, TRUE);
        gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (column), renderer,
            "cell-background", MIDORI_AUTOCOMPLETER_COLUMNS_BACKGROUND, NULL);
        gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), renderer,
            midori_location_entry_render_uri_cb, action, NULL);
        gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

        action->popup = popup;
        g_signal_connect (popup, "destroy",
            G_CALLBACK (gtk_widget_destroyed), &action->popup);
        gtk_widget_show_all (popup_frame);
    }

    if (!gtk_widget_get_visible (action->popup))
    {
        GtkWidget* toplevel = gtk_widget_get_toplevel (action->entry);
        gtk_window_set_screen (GTK_WINDOW (action->popup),
                               gtk_widget_get_screen (action->entry));
        gtk_window_set_transient_for (GTK_WINDOW (action->popup), GTK_WINDOW (toplevel));
        #if GTK_CHECK_VERSION (3, 4, 0)
        gtk_window_set_attached_to (GTK_WINDOW (action->popup), action->entry);
        #endif
        gtk_widget_show (action->popup);
    }

    return FALSE;
}

static void
midori_location_action_popup_completion (MidoriLocationAction* action,
                                         GtkWidget*            entry,
                                         gchar*                key)
{
    katze_assign (action->key, key);
    if (action->entry != entry)
    {
        action->entry = entry;
        g_signal_connect (entry, "destroy",
            G_CALLBACK (gtk_widget_destroyed), &action->entry);
    }
    g_idle_add (midori_location_action_popup_timeout_cb, action);
}

static void
midori_location_action_popdown_completion (MidoriLocationAction* location_action)
{
    if (G_LIKELY (location_action->popup))
    {
        gtk_widget_hide (location_action->popup);
        katze_assign (location_action->key, NULL);
        gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (
            GTK_TREE_VIEW (location_action->treeview)));
    }
    location_action->completion_index = -1;
}

static GtkWidget*
midori_location_action_entry_for_proxy (GtkWidget* proxy)
{
    GtkWidget* alignment = gtk_bin_get_child (GTK_BIN (proxy));
    GtkWidget* entry = gtk_bin_get_child (GTK_BIN (alignment));
    return entry;
}

static void
midori_location_action_init (MidoriLocationAction* location_action)
{
    location_action->progress = 0.0;
    location_action->completion_index = -1;
}

static void
midori_location_action_finalize (GObject* object)
{
    MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (object);

    katze_object_assign (location_action->icon, NULL);
    katze_assign (location_action->text, NULL);
    katze_assign (location_action->secondary_icon, NULL);
    katze_assign (location_action->tooltip, NULL);
    katze_assign (location_action->placeholder, NULL);
    katze_object_assign (location_action->search_engines, NULL);
    katze_assign (location_action->autocompleter, NULL);

    katze_assign (location_action->key, NULL);
    if (location_action->popup)
    {
        gtk_widget_destroy (location_action->popup);
        location_action->popup = NULL;
    }
    katze_object_assign (location_action->history, NULL);

    G_OBJECT_CLASS (midori_location_action_parent_class)->finalize (object);
}

static void
midori_location_action_set_property (GObject*      object,
                                     guint         prop_id,
                                     const GValue* value,
                                     GParamSpec*   pspec)
{
    MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (object);

    switch (prop_id)
    {
    case PROP_PROGRESS:
        midori_location_action_set_progress (location_action,
            g_value_get_double (value));
        break;
    case PROP_SECONDARY_ICON:
        midori_location_action_set_secondary_icon (location_action,
            g_value_get_string (value));
        break;
    case PROP_HISTORY:
    {
        katze_assign (location_action->history, g_value_dup_object (value));
        break;
    }
    case PROP_PLACEHOLDER_TEXT:
        katze_assign (location_action->placeholder, g_strdup(g_value_get_string (value)));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
midori_location_action_get_property (GObject*    object,
                                     guint       prop_id,
                                     GValue*     value,
                                     GParamSpec* pspec)
{
    MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (object);

    switch (prop_id)
    {
    case PROP_PROGRESS:
        g_value_set_double (value, location_action->progress);
        break;
    case PROP_SECONDARY_ICON:
        g_value_set_string (value, location_action->secondary_icon);
        break;
    case PROP_HISTORY:
        g_value_set_object (value, location_action->history);
        break;
    case PROP_PLACEHOLDER_TEXT:
        g_value_set_string (value, location_action->placeholder);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
midori_location_action_activate (GtkAction* action)
{
    GSList* proxies;
    GtkWidget* entry;

    proxies = gtk_action_get_proxies (action);

    for (; proxies != NULL; proxies = g_slist_next (proxies))
    if (GTK_IS_TOOL_ITEM (proxies->data))
    {
        entry = midori_location_action_entry_for_proxy (proxies->data);

        /* Obviously only one widget can end up with the focus.
        Yet we can't predict which one that is, can we? */
        gtk_widget_grab_focus (entry);
    }

    if (GTK_ACTION_CLASS (midori_location_action_parent_class)->activate)
        GTK_ACTION_CLASS (midori_location_action_parent_class)->activate (action);
}

static void
midori_location_action_entry_drag_data_get_cb (GtkWidget*        entry,
                                               GdkDragContext*   context,
                                               GtkSelectionData* data,
                                               guint             info,
                                               guint32           time,
                                               GtkAction*        action)
{
    if (gtk_entry_get_current_icon_drag_source (GTK_ENTRY (entry)) == GTK_ENTRY_ICON_PRIMARY)
    {
        const gchar* uri = gtk_entry_get_text (GTK_ENTRY (entry));
        gchar** uris = g_strsplit (uri, uri, 1);
        gtk_selection_data_set_uris (data, uris);
        g_strfreev (uris);
    }
}

static void
midori_location_action_entry_set_secondary_icon (GtkEntry*    entry,
                                                 const gchar* stock_id)
{
    GtkStockItem stock_item;
    if (stock_id && gtk_stock_lookup (stock_id, &stock_item))
        gtk_entry_set_icon_from_stock (entry, GTK_ENTRY_ICON_SECONDARY, stock_id);
    else
        gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, stock_id);
}

static GtkWidget*
midori_location_action_create_tool_item (GtkAction* action)
{
    MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (action);
    GtkWidget* toolitem;
    GtkWidget* alignment;
    GtkWidget* entry;

    GtkTargetList *targetlist;

    toolitem = GTK_WIDGET (gtk_tool_item_new ());
    gtk_tool_item_set_expand (GTK_TOOL_ITEM (toolitem), TRUE);

    alignment = gtk_alignment_new (0.0f, 0.5f, 1.0f, 0.1f);
    gtk_widget_show (alignment);
    gtk_container_add (GTK_CONTAINER (toolitem), alignment);

    entry = gtk_entry_new ();
    #if GTK_CHECK_VERSION (3, 6, 0)
    gtk_entry_set_input_purpose (GTK_ENTRY (entry), GTK_INPUT_PURPOSE_URL);
    #endif
    gtk_entry_set_icon_activatable (GTK_ENTRY (entry),
         GTK_ENTRY_ICON_PRIMARY, TRUE);
    gtk_entry_set_icon_activatable (GTK_ENTRY (entry),
         GTK_ENTRY_ICON_SECONDARY, TRUE);

    if (location_action->text != NULL)
        gtk_entry_set_text (GTK_ENTRY (entry), location_action->text);
    midori_location_action_entry_set_secondary_icon (GTK_ENTRY (entry), location_action->secondary_icon);
    gtk_entry_set_icon_from_gicon (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, location_action->icon);
    gtk_entry_set_icon_tooltip_text (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, location_action->tooltip);
    gtk_entry_set_placeholder_text(GTK_ENTRY (entry), location_action->placeholder);

    targetlist = gtk_target_list_new (NULL, 0);
    gtk_target_list_add_uri_targets (targetlist, 0);
    gtk_entry_set_icon_drag_source (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, targetlist, GDK_ACTION_ASK | GDK_ACTION_COPY | GDK_ACTION_LINK);
    gtk_target_list_unref (targetlist);
    g_signal_connect (entry, "drag-data-get",
        G_CALLBACK (midori_location_action_entry_drag_data_get_cb), action);
    gtk_widget_show (entry);
    gtk_container_add (GTK_CONTAINER (alignment), entry);

    return toolitem;
}

static void
midori_location_action_changed_cb (GtkEntry*             entry,
                                   MidoriLocationAction* location_action)
{
    katze_assign (location_action->text, g_strdup (gtk_entry_get_text (entry)));
}

static void
midori_location_action_move_cursor_cb (GtkEntry*             entry,
                                       GtkMovementStep       step,
                                       gint                  count,
                                       gboolean              extend_selection,
                                       MidoriLocationAction* action)
{
    gchar* text = g_strdup (pango_layout_get_text (gtk_entry_get_layout (entry)));
    /* Update entry with the completed text */
    gtk_entry_set_text (entry, text);
    g_free (text);
    midori_location_action_popdown_completion (action);
}

static void
midori_location_action_backspace_cb (GtkWidget*            entry,
                                     MidoriLocationAction* action)
{
    gchar* key = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
    midori_location_action_popup_completion (action, entry, key);
    action->completion_index = -1;
}

static void
midori_location_action_paste_clipboard_cb (GtkWidget*            entry,
                                           MidoriLocationAction* action)
{
    gchar* key = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
    midori_location_action_popup_completion (action, entry, key);
    action->completion_index = -1;
}

static gboolean
midori_location_action_button_press_event_cb (GtkEntry*             entry,
                                              GdkEventKey*          event,
                                              MidoriLocationAction* action)
{
    if (action->popup && gtk_widget_get_visible (action->popup))
    {
        midori_location_action_popdown_completion (action);

        /* Allow button handling, for context menu and selection */
        return FALSE;
    }

    return FALSE;
}

static gboolean
midori_location_action_key_press_event_cb (GtkEntry*    entry,
                                           GdkEventKey* event,
                                           GtkAction*   action)
{
    gboolean handled = FALSE;
    g_signal_emit (action, signals[KEY_PRESS_EVENT], 0, event, &handled);
    if (handled)
        return TRUE;

    GtkWidget* widget = GTK_WIDGET (entry);
    MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (action);
    const gchar* text;
    gboolean is_enter = FALSE;

    switch (event->keyval)
    {
    case GDK_KEY_ISO_Enter:
    case GDK_KEY_KP_Enter:
    case GDK_KEY_Return:
        is_enter = TRUE;
    case GDK_KEY_Left:
    case GDK_KEY_KP_Left:
    case GDK_KEY_Right:
    case GDK_KEY_KP_Right:
        if (location_action->popup && gtk_widget_get_visible (location_action->popup))
        {
            GtkTreeModel* model = location_action->completion_model;
            GtkTreeIter iter;
            gint selected = location_action->completion_index;
            if (selected > -1 &&
                gtk_tree_model_iter_nth_child (model, &iter, NULL, selected))
            {
                gchar* uri;
                gtk_tree_model_get (model, &iter,
                    MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1);

                if (is_enter)
                    midori_location_action_complete (location_action,
                            MIDORI_MOD_NEW_TAB (event->state), uri);
                else
                {
                    midori_location_action_popdown_completion (location_action);
                    midori_location_action_entry_set_text (GTK_WIDGET (entry), uri);
                }

                g_free (uri);
                return TRUE;
            }
            midori_location_action_popdown_completion (location_action);
        }

        if (is_enter && (text = gtk_entry_get_text (entry)) && *text)
        {
            g_signal_emit (action, signals[SUBMIT_URI], 0, text,
                           MIDORI_MOD_NEW_TAB (event->state));
        }
        break;
    case GDK_KEY_Escape:
    {
        if (location_action->popup && gtk_widget_get_visible (location_action->popup))
        {
            midori_location_action_popdown_completion (location_action);
            text = gtk_entry_get_text (entry);
            pango_layout_set_text (gtk_entry_get_layout (entry), text, -1);
            return TRUE;
        }

        g_signal_emit (action, signals[RESET_URI], 0);
        /* Return FALSE to allow Escape to stop loading */
        return FALSE;
    }
    case GDK_KEY_Delete:
    case GDK_KEY_KP_Delete:
    {
        gint selected = location_action->completion_index;
        GtkTreeModel* model = location_action->completion_model;
        GtkTreeIter iter;

        if (selected > -1 &&
            gtk_tree_model_iter_nth_child (model, &iter, NULL, selected))
            {
                gchar* uri;
                gchar* sqlcmd;
                sqlite3* db;
                gchar* errmsg;
                gint result;

                gtk_tree_model_get (model, &iter,
                    MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1);
                sqlcmd = sqlite3_mprintf ("DELETE FROM history "
                                          "WHERE uri = '%q'", uri);
                g_free (uri);
                db = g_object_get_data (G_OBJECT (location_action->history), "db");
                result = sqlite3_exec (db, sqlcmd, NULL, NULL, &errmsg);
                sqlite3_free (sqlcmd);
                if (result == SQLITE_ERROR)
                {
                    gtk_list_store_set (GTK_LIST_STORE (model), &iter,
                        MIDORI_AUTOCOMPLETER_COLUMNS_URI, errmsg, -1);
                    sqlite3_free (errmsg);
                    break;
                }
                if (result != SQLITE_OK || sqlite3_changes (db) == 0)
                    break;
                if (!gtk_list_store_remove (GTK_LIST_STORE (model), &iter))
                {
                    midori_location_action_popdown_completion (location_action);
                    break;
                }
                /* Fall through to advance the selection */
            }
        else
            break;
    }
    case GDK_KEY_Down:
    case GDK_KEY_KP_Down:
    case GDK_KEY_Up:
    case GDK_KEY_KP_Up:
    case GDK_KEY_Tab:
    case GDK_KEY_ISO_Left_Tab:
    case GDK_KEY_Page_Down:
    case GDK_KEY_Page_Up:
    {
        if ((event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down) &&
           !(location_action->popup && gtk_widget_get_visible (location_action->popup)))
            return TRUE;
        if (location_action->popup && gtk_widget_get_visible (location_action->popup))
        {
            GtkTreeModel* model = location_action->completion_model;
            gint matches = gtk_tree_model_iter_n_children (model, NULL);
            GtkTreePath* path;
            GtkTreeIter iter;
            gint selected = location_action->completion_index;

            if (event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down
             || ((event->keyval == GDK_KEY_Tab  || event->keyval == GDK_KEY_ISO_Left_Tab)
             && !(event->state & GDK_SHIFT_MASK)))
            {
                selected = selected + 1;
                if (selected == matches)
                    selected = -1;
            }
            else if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up
                  || ((event->keyval == GDK_KEY_Tab  || event->keyval == GDK_KEY_ISO_Left_Tab)
                  && (event->state & GDK_SHIFT_MASK)))
            {
                if (selected == -1)
                    selected = matches - 1;
                else
                    selected = selected - 1;
            }
            else if (event->keyval == GDK_KEY_Page_Down)
            {
                if (selected == -1)
                    selected = 0;
                else if (selected < matches - 1)
                    selected = MIN (selected + 14, matches -1);
                else
                    selected = -1;
            }
            else if (event->keyval == GDK_KEY_Page_Up)
            {
                if (selected == -1)
                    selected = matches - 1;
                else if (selected > 0)
                    selected = MAX (selected - 14, 0);
                else
                    selected = -1;
            }
            else if (event->keyval != GDK_KEY_KP_Delete && event->keyval != GDK_KEY_Delete)
                g_assert_not_reached ();

            if (selected != -1)
            {
                path = gtk_tree_path_new_from_indices (selected, -1);
                gtk_tree_view_set_cursor (GTK_TREE_VIEW (location_action->treeview),
                                          path, NULL, FALSE);
                gtk_tree_path_free (path);

                if (gtk_tree_model_iter_nth_child (model, &iter, NULL, selected))
                {
                    gchar* uri;
                    gtk_tree_model_get (model, &iter,
                        MIDORI_AUTOCOMPLETER_COLUMNS_URI, &uri, -1);
                    /* Update the layout without actually changing the text */
                    pango_layout_set_text (gtk_entry_get_layout (entry), uri, -1);
                    g_free (uri);
                }
            }
            else
                gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (location_action->treeview)));

            location_action->completion_index = selected;
            return TRUE;
        }

        /* Allow Tab to handle focus if the popup is closed */
        if (event->keyval == GDK_KEY_Tab  || event->keyval == GDK_KEY_ISO_Left_Tab)
            return FALSE;
        return TRUE;
    }
    default:
    {
        gunichar character;
        gchar buffer[7];
        gint length;
        gchar* key;

        character = gdk_keyval_to_unicode (event->keyval);
        /* Don't trigger completion on control characters */
        if (!character || event->is_modifier)
            return FALSE;

        length = g_unichar_to_utf8 (character, buffer);
        buffer[length] = '\0';
        key = g_strconcat (gtk_entry_get_text (entry), buffer, NULL);
        midori_location_action_popup_completion (location_action, widget, key);
        location_action->completion_index = -1;
        return FALSE;
    }
    }
    return FALSE;
}

static void
midori_location_action_preedit_changed_cb (GtkWidget*   entry,
                                           const gchar* preedit,
                                           GtkAction*   action)
{
    MidoriLocationAction* location_action = MIDORI_LOCATION_ACTION (action);
    gchar* key = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
    midori_location_action_popup_completion (location_action, entry, key);
}

static gboolean
midori_location_action_focus_in_event_cb (GtkWidget*   widget,
                                          GdkEventKey* event,
                                          GtkAction*   action)
{
    g_signal_emit (action, signals[FOCUS_IN], 0);
    return FALSE;
}

static gboolean
midori_location_action_focus_out_event_cb (GtkWidget*   widget,
                                           GdkEventKey* event,
                                           GtkAction*   action)
{
    midori_location_action_popdown_completion (MIDORI_LOCATION_ACTION (action));
    g_signal_emit (action, signals[FOCUS_OUT], 0);
    return FALSE;
}

#ifdef HAVE_GCR
    #define GCR_API_SUBJECT_TO_CHANGE
    #include <gcr/gcr.h>
#endif

#if defined (HAVE_LIBSOUP_2_34_0)
#ifndef HAVE_WEBKIT2
static GHashTable* message_map = NULL;
void
midori_map_add_message (SoupMessage* message)
{
    SoupURI* uri = soup_message_get_uri (message);
    if (message_map == NULL)
        message_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
    g_return_if_fail (uri && uri->host);
    g_hash_table_insert (message_map, g_strdup (uri->host), g_object_ref (message));
}

SoupMessage*
midori_map_get_message (SoupMessage* message)
{
    SoupURI* uri = soup_message_get_uri (message);
    SoupMessage* full;
    g_return_val_if_fail (uri && uri->host, message);
    if (message_map == NULL)
        message_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
    full = g_hash_table_lookup (message_map, uri->host);
    if (full != NULL)
        return full;
    return message;
}
#endif
#endif

#ifdef HAVE_GCR
typedef enum {
    MIDORI_CERT_TRUST,
    MIDORI_CERT_REVOKE,
    MIDORI_CERT_EXPORT,
} MidoriCertTrust;

static void
midori_location_action_cert_response_cb (GtkWidget*      dialog,
                                         gint            response,
                                         GcrCertificate* gcr_cert)
{
    gchar* peer = g_object_get_data (G_OBJECT (gcr_cert), "peer");
    GError* error = NULL;
    if (response == MIDORI_CERT_TRUST)
        gcr_trust_add_pinned_certificate (gcr_cert, GCR_PURPOSE_SERVER_AUTH, peer, NULL, &error);
    else if (response == MIDORI_CERT_REVOKE)
        gcr_trust_remove_pinned_certificate (gcr_cert, GCR_PURPOSE_SERVER_AUTH, peer, NULL, &error);
    else if (response == MIDORI_CERT_EXPORT)
    {
        /* FIXME: Would be nice if GcrCertificateExporter became public */
        gchar* filename = g_strconcat (peer, ".crt", NULL);
        GtkWidget* export_dialog = (GtkWidget*)midori_file_chooser_dialog_new (_("Export certificate"),
            NULL, GTK_FILE_CHOOSER_ACTION_SAVE);
        gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (export_dialog), TRUE);
        gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (export_dialog), filename);
        g_free (filename);

        if (gtk_dialog_run (GTK_DIALOG (export_dialog)) == GTK_RESPONSE_OK)
        {
            gsize n_data;
            gconstpointer data = gcr_certificate_get_der_data (gcr_cert, &n_data);
            g_return_if_fail (data);
            filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (export_dialog));
            g_file_set_contents (filename, data, n_data, NULL);
            g_free (filename);
        }
        gtk_widget_destroy (export_dialog);
    }
    if (error != NULL)
    {
        g_warning ("Error %s trust: %s", response == MIDORI_CERT_TRUST ?
                   "granting" : "revoking", error->message);
        g_error_free (error);
    }
    gtk_widget_hide (dialog);
}

#if GTK_CHECK_VERSION (3, 12, 0)
static void
midori_location_action_button_cb (GtkWidget* button,
                                  GtkWidget* dialog)
{
    GcrCertificate* gcr_cert = g_object_get_data (G_OBJECT (dialog), "gcr-cert");
    const gchar* label = gtk_button_get_label (GTK_BUTTON (button));
    gint response;
    if (!strcmp (label, _("_Don't trust this website")))
        response = MIDORI_CERT_REVOKE;
    else if (!strcmp (label, _("_Trust this website")))
        response = MIDORI_CERT_TRUST;
    else if (!strcmp (label, _("_Export certificate")))
        response = MIDORI_CERT_EXPORT;
    else
        g_assert_not_reached ();
    midori_location_action_cert_response_cb (dialog, response, gcr_cert);
}
#endif
#endif

#if GTK_CHECK_VERSION (3, 12, 0)
static gboolean
midori_location_action_popover_button_press_event_cb (GtkWidget*      widget,
                                                      GdkEventButton* event,
                                                      gpointer        user_data)
{
    return GDK_EVENT_STOP;
}

static gboolean
midori_location_action_popover_button_release_event_cb (GtkWidget*      widget,
                                                        GdkEventButton* event,
                                                        gpointer        user_data)
{
    /* The default GtkPopOver button-press doesn't work with
       GcrCertificateDetailsWidget and fails with an assertion:
       gtk_widget_is_ancestor: assertion 'GTK_IS_WIDGET (widget)' */
    GtkWidget* child = gtk_bin_get_child (GTK_BIN (widget));
    GtkWidget* event_widget = gtk_get_event_widget ((GdkEvent*)event);
    if (child && event->window == gtk_widget_get_window (widget))
    {
        GtkAllocation child_alloc;
        gtk_widget_get_allocation (child, &child_alloc);
        if (event->x < child_alloc.x ||
         event->x > child_alloc.x + child_alloc.width ||
         event->y < child_alloc.y ||
         event->y > child_alloc.y + child_alloc.height)
        gtk_widget_hide (widget);
    }
    else if (event_widget && !gtk_widget_is_ancestor (event_widget, widget))
        gtk_widget_hide (widget);
    return GDK_EVENT_STOP;
}
#endif

const gchar*
midori_location_action_tls_flags_to_string (GTlsCertificateFlags tls_flags)
{
    if (tls_flags & G_TLS_CERTIFICATE_UNKNOWN_CA)
        return _("The signing certificate authority is not known.");
    else if (tls_flags & G_TLS_CERTIFICATE_BAD_IDENTITY)
        return _("The certificate does not match the expected identity of the site that it was retrieved from.");
    else if(tls_flags & G_TLS_CERTIFICATE_NOT_ACTIVATED)
        return _("The certificate's activation time is still in the future.");
    else if (tls_flags & G_TLS_CERTIFICATE_EXPIRED)
        return _("The certificate has expired");
    else if (tls_flags & G_TLS_CERTIFICATE_REVOKED)
        return _("The certificate has been revoked according to the GTlsConnection's certificate revocation list.");
    else if (tls_flags & G_TLS_CERTIFICATE_INSECURE)
        return _("The certificate's algorithm is considered insecure.");
    else if (tls_flags & G_TLS_CERTIFICATE_GENERIC_ERROR)
        return _("Some other error occurred validating the certificate.");
    else if (tls_flags == 0)
        g_return_val_if_reached ("GTLSCertificateFlags is 0");
    g_return_val_if_reached ("Unknown GTLSCertificateFlags value");
}

static void
midori_location_action_show_page_info (GtkWidget* widget,
                                       GtkBox*    box,
                                       GtkWidget* dialog)
{
    GTlsCertificate* tls_cert;
    GTlsCertificateFlags tls_flags;
    gchar* hostname;

    MidoriBrowser* browser = midori_browser_get_for_widget (widget);
    MidoriView* view = MIDORI_VIEW (midori_browser_get_current_tab (browser));
    #ifdef HAVE_WEBKIT2
    void* request = NULL;
    #else
    WebKitWebView* web_view = WEBKIT_WEB_VIEW (midori_view_get_web_view (view));
    WebKitWebFrame* web_frame = webkit_web_view_get_main_frame (web_view);
    WebKitWebDataSource* source = webkit_web_frame_get_data_source (web_frame);
    WebKitNetworkRequest* request = webkit_web_data_source_get_request (source);
    #endif
    midori_view_get_tls_info (view, request, &tls_cert, &tls_flags, &hostname);
    if (tls_cert == NULL)
    {
        g_free (hostname);
        return;
    }

    #ifdef HAVE_GCR
    GByteArray* der_cert;
    GcrCertificate* gcr_cert;

    g_object_get (tls_cert, "certificate", &der_cert, NULL);
    gcr_cert = gcr_simple_certificate_new (
        der_cert->data, der_cert->len);
    g_byte_array_unref (der_cert);

    #if GTK_CHECK_VERSION (3, 0, 0)
    GtkWidget* details;
    details = (GtkWidget*)gcr_certificate_details_widget_new (gcr_cert);
    gtk_widget_show (details);
    gtk_container_add (GTK_CONTAINER (box), details);
    #endif

    #if GTK_CHECK_VERSION (3, 12, 0)
    GtkWidget* button;
    GtkWidget* actions = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start (GTK_BOX (box), actions, FALSE, FALSE, 0);
    if (gcr_trust_is_certificate_pinned (gcr_cert, GCR_PURPOSE_SERVER_AUTH, hostname, NULL, NULL))
    {
        button = gtk_button_new_with_mnemonic (_("_Don't trust this website"));
        gtk_box_pack_start (GTK_BOX (actions), button, FALSE, FALSE, 0);
        g_signal_connect (button, "clicked", G_CALLBACK (midori_location_action_button_cb), dialog);
    }
    else if (tls_flags > 0)
    {
        button = gtk_button_new_with_mnemonic (_("_Trust this website"));
        gtk_box_pack_start (GTK_BOX (actions), button, FALSE, FALSE, 0);
        g_signal_connect (button, "clicked", G_CALLBACK (midori_location_action_button_cb), dialog);
    }
    button = gtk_button_new_with_mnemonic (_("_Export certificate"));
    g_signal_connect (button, "clicked", G_CALLBACK (midori_location_action_button_cb), dialog);
    gtk_box_pack_end (GTK_BOX (actions), button, FALSE, FALSE, 0);
    gtk_widget_show_all (actions);
    #else
    if (gcr_trust_is_certificate_pinned (gcr_cert, GCR_PURPOSE_SERVER_AUTH, hostname, NULL, NULL))
        gtk_dialog_add_buttons (GTK_DIALOG (dialog),
            ("_Don't trust this website"), MIDORI_CERT_REVOKE, NULL);
    else if (tls_flags > 0)
        gtk_dialog_add_buttons (GTK_DIALOG (dialog),
            ("_Trust this website"), MIDORI_CERT_TRUST, NULL);
    gtk_container_child_set (GTK_CONTAINER (gtk_dialog_get_action_area (GTK_DIALOG (dialog))),
        gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Export certificate"), MIDORI_CERT_EXPORT),
        "secondary", TRUE, NULL);
    g_signal_connect (dialog, "response",
        G_CALLBACK (midori_location_action_cert_response_cb), gcr_cert);
    #endif

    g_object_set_data_full (G_OBJECT (gcr_cert), "peer", hostname, (GDestroyNotify)g_free);
    g_object_set_data_full (G_OBJECT (dialog), "gcr-cert", gcr_cert, (GDestroyNotify)g_object_unref);
    #endif

    /* With GTK+2 the scrolled contents can't communicate a natural size to the window */
    #if !GTK_CHECK_VERSION (3, 0, 0)
    gtk_window_set_default_size (GTK_WINDOW (dialog), 250, 200);
    #endif
    if (!g_tls_certificate_get_issuer (tls_cert))
        gtk_box_pack_start (box, gtk_label_new (_("Self-signed")), FALSE, FALSE, 0);

    if (tls_flags > 0)
    {
        const gchar* tls_error = midori_location_action_tls_flags_to_string (tls_flags);
        gtk_box_pack_start (box, gtk_label_new (tls_error), FALSE, FALSE, 0);
    }

    g_object_unref (tls_cert);
}

static void
midori_location_action_engine_activate_cb (GtkWidget*          menuitem,
                                           MidoriSearchAction* search_action)
{
    KatzeItem* item;

    item = (KatzeItem*)g_object_get_data (G_OBJECT (menuitem), "engine");
    midori_search_action_set_default_item (search_action, item);
}

void
midori_location_action_icon_released_cb (GtkWidget*           widget,
                                         GtkEntryIconPosition icon_pos,
                                         gint                 button,
                                         GtkAction*           action)
{
    /* The dialog should "toggle" like a menu, as far as users go */
    MidoriBrowser* browser = midori_browser_get_for_widget (widget);
    GtkActionGroup* actions = midori_browser_get_action_group (browser);
    MidoriSearchAction *search_action = MIDORI_SEARCH_ACTION (
        gtk_action_group_get_action (actions, "Search") 
    );

    static GtkWidget* dialog = NULL;
    if (icon_pos == GTK_ENTRY_ICON_PRIMARY && dialog != NULL)
    {
        gtk_widget_destroy (dialog);
        return; // Previously code was running on and the widget was being rebuilt
    }

    if (icon_pos == GTK_ENTRY_ICON_PRIMARY)
    {
        /* No "security" window for blank pages */
        if (midori_uri_is_blank (MIDORI_LOCATION_ACTION (action)->text))
        {
            GtkMenu* menu = midori_search_action_get_menu (widget,
                                                           search_action, 
                                                           midori_location_action_engine_activate_cb );
            katze_widget_popup (widget, menu, NULL, KATZE_MENU_POSITION_LEFT);
            return;
        }

        GtkWidget* content_area;
        GtkWidget* hbox;

        #if GTK_CHECK_VERSION (3, 12, 0)
        dialog = gtk_popover_new (widget);
        content_area = gtk_vbox_new (FALSE, 6);
        gtk_container_add (GTK_CONTAINER (dialog), content_area);
        g_signal_connect (dialog, "button-press-event",
            G_CALLBACK (midori_location_action_popover_button_press_event_cb), NULL);
        g_signal_connect (dialog, "button-release-event",
            G_CALLBACK (midori_location_action_popover_button_release_event_cb), NULL);

        GdkRectangle icon_rect;
        gtk_entry_get_icon_area (GTK_ENTRY (widget), icon_pos, &icon_rect);
        gtk_popover_set_relative_to (GTK_POPOVER (dialog), widget);
        gtk_popover_set_pointing_to (GTK_POPOVER (dialog), &icon_rect);
        g_signal_connect (dialog, "closed", G_CALLBACK (gtk_widget_destroyed), &dialog);
        #else
        const gchar* title = _("Security details");
        dialog = gtk_dialog_new_with_buttons (title, GTK_WINDOW (gtk_widget_get_toplevel (widget)),
            GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_NO_SEPARATOR, NULL, NULL);
        content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
        g_signal_connect (dialog, "destroy", G_CALLBACK (gtk_widget_destroyed), &dialog);
        #endif
        gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
        hbox = gtk_hbox_new (FALSE, 6);
        gtk_box_pack_start (GTK_BOX (hbox), gtk_image_new_from_gicon (
            gtk_entry_get_icon_gicon (GTK_ENTRY (widget), icon_pos), GTK_ICON_SIZE_DIALOG), FALSE, FALSE, 0);
        gtk_box_pack_start (GTK_BOX (hbox),
            gtk_label_new (gtk_entry_get_icon_tooltip_text (GTK_ENTRY (widget), icon_pos)), FALSE, FALSE, 0);
        gtk_box_pack_start (GTK_BOX (content_area), hbox, FALSE, FALSE, 0);
        #ifdef HAVE_LIBSOUP_2_34_0
        midori_location_action_show_page_info (widget, GTK_BOX (content_area), dialog);
        #endif
        gtk_widget_show_all (dialog);
    }
    if (icon_pos == GTK_ENTRY_ICON_SECONDARY)
    {
        gboolean result;
        g_signal_emit (action, signals[SECONDARY_ICON_RELEASED], 0,
                       widget, &result);
    }
}

static void
midori_location_action_populate_popup_cb (GtkWidget*            entry,
                                          GtkMenuShell*         menu,
                                          MidoriLocationAction* location_action)
{
    MidoriBrowser* browser = midori_browser_get_for_widget (entry);
    GtkActionGroup* actions = midori_browser_get_action_group (browser);
    GtkWidget* menuitem;
    GtkClipboard* clipboard = gtk_clipboard_get_for_display (
        gtk_widget_get_display (entry),GDK_SELECTION_CLIPBOARD);

    menuitem = gtk_separator_menu_item_new ();
    gtk_widget_show (menuitem);
    gtk_menu_shell_append (menu, menuitem);
    menuitem = gtk_action_create_menu_item (
        gtk_action_group_get_action (actions, "ManageSearchEngines"));
    GtkWidget* accel_label = gtk_bin_get_child (GTK_BIN (menuitem));
    if (accel_label != NULL)
        gtk_accel_label_set_accel_closure (GTK_ACCEL_LABEL (accel_label), NULL);
    gtk_menu_shell_append (menu, menuitem);
    menuitem = gtk_action_create_menu_item (
        gtk_action_group_get_action (actions, "PasteProceed"));
    accel_label = gtk_bin_get_child (GTK_BIN (menuitem));
    if (accel_label != NULL)
        gtk_accel_label_set_accel_closure (GTK_ACCEL_LABEL (accel_label), NULL);
    /* Insert menu item after default Paste menu item */
    gtk_menu_shell_insert (menu, menuitem, 3);
    if (!gtk_clipboard_wait_is_text_available (clipboard))
        gtk_widget_set_sensitive (menuitem, FALSE);
}

static void
midori_location_action_connect_proxy (GtkAction* action,
                                      GtkWidget* proxy)
{
    GTK_ACTION_CLASS (midori_location_action_parent_class)->connect_proxy (
        action, proxy);

    if (GTK_IS_TOOL_ITEM (proxy))
    {
        GtkWidget* entry = midori_location_action_entry_for_proxy (proxy);
        gtk_entry_set_progress_fraction (GTK_ENTRY (entry),
            MIDORI_LOCATION_ACTION (action)->progress);

        g_object_connect (entry,
                      "signal::changed",
                      midori_location_action_changed_cb, action,
                      "signal::move-cursor",
                      midori_location_action_move_cursor_cb, action,
                      "signal-after::backspace",
                      midori_location_action_backspace_cb, action,
                      "signal-after::paste-clipboard",
                      midori_location_action_paste_clipboard_cb, action,
                      "signal::button-press-event",
                      midori_location_action_button_press_event_cb, action,
                      "signal::key-press-event",
                      midori_location_action_key_press_event_cb, action,
                      "signal-after::preedit-changed",
                      midori_location_action_preedit_changed_cb, action,
                      "signal::focus-in-event",
                      midori_location_action_focus_in_event_cb, action,
                      "signal::focus-out-event",
                      midori_location_action_focus_out_event_cb, action,
                      "signal::icon-release",
                      midori_location_action_icon_released_cb, action,
                      "signal::populate-popup",
                      midori_location_action_populate_popup_cb, action,
                      NULL);
    }
}

static void
midori_location_action_disconnect_proxy (GtkAction* action,
                                         GtkWidget* proxy)
{
    /* FIXME: This is wrong */
    g_signal_handlers_disconnect_by_func (proxy,
        G_CALLBACK (gtk_action_activate), action);

    GTK_ACTION_CLASS (midori_location_action_parent_class)->disconnect_proxy
        (action, proxy);
}

/**
 * midori_location_action_get_text:
 * @location_action: a #MidoriLocationAction
 *
 * Retrieves the current text, which may be the current URI or
 * anything typed in the entry.
 *
 * Return value: the current text
 *
 * Since: 0.2.0
 **/
const gchar*
midori_location_action_get_text (MidoriLocationAction* location_action)
{
    g_return_val_if_fail (MIDORI_IS_LOCATION_ACTION (location_action), NULL);

    return location_action->text;
}

/**
 * midori_location_action_set_text:
 * @location_action: a #MidoriLocationAction
 * @text: a string
 *
 * Sets the entry text to @text.
 *
 * Since: 0.2.0
 **/
void
midori_location_action_set_text (MidoriLocationAction* location_action,
                                 const gchar*          text)
{
    GSList* proxies;

    g_return_if_fail (MIDORI_IS_LOCATION_ACTION (location_action));
    g_return_if_fail (text != NULL);

    midori_location_action_popdown_completion (location_action);

    katze_assign (location_action->text, g_strdup (text));

    if (!(proxies = gtk_action_get_proxies (GTK_ACTION (location_action))))
        return;

    for (; proxies != NULL; proxies = g_slist_next (proxies))
    if (GTK_IS_TOOL_ITEM (proxies->data))
    {
        GtkWidget* entry = midori_location_action_entry_for_proxy (proxies->data);
        midori_location_action_entry_set_text (entry, text);
    }
}

/**
 * midori_location_action_set_search_engines:
 * @location_action: a #MidoriLocationAction
 * @search_engines: a #KatzeArray
 *
 * Assigns the specified search engines to the location action.
 * Search engines will appear as actions in the completion.
 *
 * Since: 0.1.6
 **/
void
midori_location_action_set_search_engines (MidoriLocationAction* location_action,
                                           KatzeArray*           search_engines)
{
    g_return_if_fail (MIDORI_IS_LOCATION_ACTION (location_action));

    if (search_engines)
        g_object_ref (search_engines);

    katze_object_assign (location_action->search_engines, search_engines);
}

gdouble
midori_location_action_get_progress (MidoriLocationAction* location_action)
{
    g_return_val_if_fail (MIDORI_IS_LOCATION_ACTION (location_action), 0.0);

    return location_action->progress;
}

void
midori_location_action_set_progress (MidoriLocationAction* location_action,
                                     gdouble               progress)
{
    GSList* proxies;

    g_return_if_fail (MIDORI_IS_LOCATION_ACTION (location_action));

    location_action->progress = CLAMP (progress, 0.0, 1.0);

    proxies = gtk_action_get_proxies (GTK_ACTION (location_action));

    for (; proxies != NULL; proxies = g_slist_next (proxies))
    if (GTK_IS_TOOL_ITEM (proxies->data))
    {
        GtkWidget* entry = midori_location_action_entry_for_proxy (proxies->data);
        gtk_entry_set_progress_fraction (GTK_ENTRY (entry), location_action->progress);
    }
}

/**
 * midori_location_action_set_secondary_icon:
 * @location_action: a #MidoriLocationAction
 * @icon: a stock ID, or an icon name
 *
 * Sets the secondary, ie right hand side icon.
 *
 * Since 0.4.6 @icon can be a stock ID or an icon name.
 **/
void
midori_location_action_set_secondary_icon (MidoriLocationAction* location_action,
                                           const gchar*          stock_id)
{
    GSList* proxies;

    g_return_if_fail (MIDORI_IS_LOCATION_ACTION (location_action));

    katze_assign (location_action->secondary_icon, g_strdup (stock_id));

    proxies = gtk_action_get_proxies (GTK_ACTION (location_action));

    for (; proxies != NULL; proxies = g_slist_next (proxies))
    if (GTK_IS_TOOL_ITEM (proxies->data))
    {
        GtkWidget* entry = midori_location_action_entry_for_proxy (proxies->data);
        midori_location_action_entry_set_secondary_icon (GTK_ENTRY (entry), stock_id);
    }
}

/**
 * midori_location_set_action_primary_icon:
 * @location_action: a #MidoriLocationAction
 * @icon: a list of icon names, preferred icon first
 * @tooltip: The tooltip to show
 *
 * The primary icon is mutually exclusive with the security hint.
 *
 * Since: 0.5.1
 **/
void
midori_location_action_set_primary_icon (MidoriLocationAction* location_action,
                                         GIcon*                icon,
                                         const gchar*          tooltip)
{
    GSList* proxies;

    g_return_if_fail (MIDORI_IS_LOCATION_ACTION (location_action));
    g_return_if_fail (G_IS_ICON (icon));
    g_return_if_fail (tooltip != NULL);

    katze_object_assign (location_action->icon, g_object_ref (icon));
    katze_assign (location_action->tooltip, g_strdup (tooltip));

    proxies = gtk_action_get_proxies (GTK_ACTION (location_action));

    for (; proxies != NULL; proxies = g_slist_next (proxies))
    if (GTK_IS_TOOL_ITEM (proxies->data))
    {
        GtkWidget* entry = midori_location_action_entry_for_proxy (proxies->data);
        gtk_entry_set_icon_from_gicon (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, icon);
        gtk_entry_set_icon_tooltip_text (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, tooltip);
    }
}

/**
 * midori_location_action_set_security_hint:
 * @location_action: a #MidoriLocationAction
 * @hint: a security hint
 *
 * Sets a security hint on the action, so that the security status
 * can be reflected visually.
 *
 * Since: 0.2.5
 **/
void
midori_location_action_set_security_hint (MidoriLocationAction* location_action,
                                          MidoriSecurity        hint)
{
    GIcon* icon;
    gchar* tooltip;

    g_return_if_fail (MIDORI_IS_LOCATION_ACTION (location_action));

    if (hint == MIDORI_SECURITY_UNKNOWN)
    {
        gchar* icon_names[] = { "channel-insecure-symbolic", "lock-insecure", "dialog-information", NULL };
        icon = g_themed_icon_new_from_names (icon_names, -1);
        tooltip = _("Not verified");
    }
    else if (hint == MIDORI_SECURITY_TRUSTED)
    {
        gchar* icon_names[] = { "channel-secure-symbolic", "lock-secure", "locked", NULL };
        icon = g_themed_icon_new_from_names (icon_names, -1);
        tooltip = _("Verified and encrypted connection");
    }
    else if (hint == MIDORI_SECURITY_NONE)
    {
        icon = g_themed_icon_new_with_default_fallbacks ("text-html-symbolic");
        tooltip = _("Open, unencrypted connection");
    }
    else
        g_assert_not_reached ();

    midori_location_action_set_primary_icon (location_action, icon, tooltip);
    g_object_unref (icon);
}
