/* * * Copyright (C) 2010-2011 Colomban Wendling * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "gwh-browser.h" #include "config.h" #include #include #include #include #include #include #include #include "gwh-utils.h" #include "gwh-settings.h" #include "gwh-keybindings.h" #include "gwh-plugin.h" #if ! GTK_CHECK_VERSION (2, 18, 0) # ifndef gtk_widget_set_visible # define gtk_widget_set_visible(w, v) \ ((v) ? gtk_widget_show (w) : gtk_widget_hide (w)) # endif /* defined (gtk_widget_set_visible) */ # ifndef gtk_widget_get_visible # define gtk_widget_get_visible(w) \ (GTK_WIDGET_VISIBLE (w)) # endif /* defined (gtk_widget_get_visible) */ #endif /* GTK_CHECK_VERSION (2, 18, 0) */ #if ! GTK_CHECK_VERSION (2, 20, 0) # ifndef gtk_widget_get_mapped # define gtk_widget_get_mapped(w) \ (GTK_WIDGET_MAPPED ((w))) # endif /* defined (gtk_widget_get_mapped) */ #endif /* GTK_CHECK_VERSION (2, 20, 0) */ #if ! GTK_CHECK_VERSION (3, 0, 0) /* the GtkComboBoxText API is actually available in 2.24, but Geany's * gtkcompat.h hides it on < 3.0.0 to keep ABI compatibility on all 2.x, so we * need to use the old API there too. */ # define gtk_combo_box_get_entry_text_column(c) \ (gtk_combo_box_entry_get_text_column (GTK_COMBO_BOX_ENTRY (c))) #endif /* GTK_CHECK_VERSION (3, 0, 0) */ #if ! GTK_CHECK_VERSION (3, 0, 0) static void combo_box_text_remove_all (GtkComboBoxText *combo_box) { GtkListStore *store; g_return_if_fail (GTK_IS_COMBO_BOX_TEXT (combo_box)); store = GTK_LIST_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX (combo_box))); gtk_list_store_clear (store); } # define gtk_combo_box_text_remove_all combo_box_text_remove_all #endif /* GTK_CHECK_VERSION (3, 0, 0) */ #if GTK_CHECK_VERSION (3, 0, 0) /* alias GtkObject, we implement the :destroy signal */ # define GtkObject GtkWidget # define GtkObjectClass GtkWidgetClass # define GTK_OBJECT_CLASS GTK_WIDGET_CLASS #endif /* GTK_CHECK_VERSION (3, 0, 0) */ struct _GwhBrowserPrivate { GwhSettings *settings; GIcon *default_icon; GtkWidget *toolbar; GtkWidget *paned; GtkWidget *web_view; WebKitWebInspector *inspector; GtkWidget *inspector_view; /* the widget that should be shown to * display the inspector, not necessarily * a WebKitWebView */ GtkWidget *inspector_window; gint inspector_window_x; gint inspector_window_y; GtkWidget *inspector_web_view; GtkWidget *url_entry; GtkWidget *url_combo; GtkToolItem *item_prev; GtkToolItem *item_next; GtkToolItem *item_cancel; GtkToolItem *item_reload; GtkToolItem *item_inspector; GtkWidget *statusbar; gchar *hovered_link; }; enum { PROP_0, PROP_INSPECTOR_TRANSIENT_FOR, PROP_ORIENTATION, PROP_URI, PROP_WEB_VIEW, PROP_TOOLBAR }; enum { POPULATE_POPUP, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = {0}; G_DEFINE_TYPE_WITH_CODE (GwhBrowser, gwh_browser, GTK_TYPE_VBOX, G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) static void inspector_set_detached (GwhBrowser *self, gboolean detached); static void set_location_icon (GwhBrowser *self, const gchar *icon_uri) { gboolean success = FALSE; if (icon_uri) { GdkPixbuf *icon; icon = gwh_pixbuf_new_from_uri (icon_uri, NULL); if (icon) { gtk_entry_set_icon_from_pixbuf (GTK_ENTRY (self->priv->url_entry), GTK_ENTRY_ICON_PRIMARY, icon); g_object_unref (icon); success = TRUE; } } if (! success) { if (G_UNLIKELY (self->priv->default_icon == NULL)) { gchar *ctype; ctype = g_content_type_from_mime_type ("text/html"); self->priv->default_icon = g_content_type_get_icon (ctype); g_free (ctype); } gtk_entry_set_icon_from_gicon (GTK_ENTRY (self->priv->url_entry), GTK_ENTRY_ICON_PRIMARY, self->priv->default_icon); } } static gchar * get_web_inspector_window_geometry (GwhBrowser *self) { return gwh_get_window_geometry (GTK_WINDOW (self->priv->inspector_window), self->priv->inspector_window_x, self->priv->inspector_window_y); } static void set_web_inspector_window_geometry (GwhBrowser *self, const gchar *geometry) { gwh_set_window_geometry (GTK_WINDOW (self->priv->inspector_window), geometry, &self->priv->inspector_window_x, &self->priv->inspector_window_y); } /* settings change notifications */ static void on_settings_browser_last_uri_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { gchar *uri; g_object_get (object, pspec->name, &uri, NULL); gwh_browser_set_uri (self, uri); g_free (uri); } static void on_settings_browser_bookmarks_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { gchar **bookmarks; g_return_if_fail (GWH_IS_BROWSER (self)); gtk_combo_box_text_remove_all (GTK_COMBO_BOX_TEXT (self->priv->url_combo)); bookmarks = gwh_browser_get_bookmarks (self); if (bookmarks) { gchar **p; for (p = bookmarks; *p; p++) { gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (self->priv->url_combo), *p); } g_strfreev (bookmarks); } } static void on_settings_browser_orientation_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { GtkOrientation orientation; g_object_get (object, pspec->name, &orientation, NULL); if (orientation != gtk_orientable_get_orientation (GTK_ORIENTABLE (self))) { gtk_orientable_set_orientation (GTK_ORIENTABLE (self), orientation); } } static void on_settings_inspector_window_geometry_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { gchar *geometry; g_object_get (object, pspec->name, &geometry, NULL); set_web_inspector_window_geometry (self, geometry); g_free (geometry); } static void on_settings_inspector_detached_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { gboolean detached; g_object_get (object, pspec->name, &detached, NULL); inspector_set_detached (self, detached); } static void on_settings_wm_windows_skip_taskbar_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { gboolean skips_taskbar; g_object_get (object, pspec->name, &skips_taskbar, NULL); gtk_window_set_skip_taskbar_hint (GTK_WINDOW (self->priv->inspector_window), skips_taskbar); } static void on_settings_wm_windows_type_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { gint type; gboolean remap = gtk_widget_get_mapped (self->priv->inspector_window); /* We need to remap the window if we change its type because it's not doable * when mapped. So, hack around. */ g_object_get (object, pspec->name, &type, NULL); if (remap) { gtk_widget_unmap (self->priv->inspector_window); } gtk_window_set_type_hint (GTK_WINDOW (self->priv->inspector_window), type); if (remap) { gtk_widget_map (self->priv->inspector_window); } } /* web inspector events handling */ #define INSPECTOR_DETACHED(self) \ (gtk_bin_get_child (GTK_BIN ((self)->priv->inspector_window)) != NULL) #define INSPECTOR_VISIBLE(self) \ (gtk_widget_get_visible ((self)->priv->inspector_view)) static void inspector_set_visible (GwhBrowser *self, gboolean visible) { if (visible != INSPECTOR_VISIBLE (self)) { if (visible) { webkit_web_inspector_show (self->priv->inspector); } else { webkit_web_inspector_close (self->priv->inspector); } } } static void inspector_hide_window (GwhBrowser *self) { if (gtk_widget_get_visible (self->priv->inspector_window)) { gtk_window_get_position (GTK_WINDOW (self->priv->inspector_window), &self->priv->inspector_window_x, &self->priv->inspector_window_y); gtk_widget_hide (self->priv->inspector_window); } } static void inspector_show_window (GwhBrowser *self) { if (! gtk_widget_get_visible (self->priv->inspector_window)) { gtk_widget_show (self->priv->inspector_window); gtk_window_move (GTK_WINDOW (self->priv->inspector_window), self->priv->inspector_window_x, self->priv->inspector_window_y); } } static void inspector_set_detached (GwhBrowser *self, gboolean detached) { if (detached != INSPECTOR_DETACHED (self)) { if (detached) { gtk_widget_reparent (self->priv->inspector_view, self->priv->inspector_window); if (INSPECTOR_VISIBLE (self)) { inspector_show_window (self); } } else { gtk_widget_reparent (self->priv->inspector_view, self->priv->paned); inspector_hide_window (self); } g_object_set (self->priv->settings, "inspector-detached", detached, NULL); } } static WebKitWebView * on_inspector_inspect_web_view (WebKitWebInspector *inspector, WebKitWebView *view, GwhBrowser *self) { if (self->priv->inspector_web_view) { gtk_widget_destroy (self->priv->inspector_web_view); } self->priv->inspector_web_view = webkit_web_view_new (); gtk_widget_show (self->priv->inspector_web_view); gtk_container_add (GTK_CONTAINER (self->priv->inspector_view), self->priv->inspector_web_view); return WEBKIT_WEB_VIEW (self->priv->inspector_web_view); } static gboolean on_inspector_show_window (WebKitWebInspector *inspector, GwhBrowser *self) { gtk_widget_show (self->priv->inspector_view); if (INSPECTOR_DETACHED (self)) { inspector_show_window (self); } gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON (self->priv->item_inspector), TRUE); return TRUE; } static gboolean on_inspector_close_window (WebKitWebInspector *inspector, GwhBrowser *self) { gtk_widget_hide (self->priv->inspector_view); inspector_hide_window (self); gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON (self->priv->item_inspector), FALSE); gtk_widget_grab_focus (gtk_widget_get_toplevel (self->priv->web_view)); return TRUE; } static gboolean on_inspector_detach_window (WebKitWebInspector *inspector, GwhBrowser *self) { inspector_set_detached (self, TRUE); return TRUE; } static gboolean on_inspector_attach_window (WebKitWebInspector *inspector, GwhBrowser *self) { inspector_set_detached (self, FALSE); return TRUE; } static gboolean on_inspector_window_delete_event (GtkWidget *window, GdkEvent *event, GwhBrowser *self) { webkit_web_inspector_close (self->priv->inspector); return TRUE; } /* web view events hanlding */ static void on_item_inspector_toggled (GtkToggleToolButton *button, GwhBrowser *self) { inspector_set_visible (self, gtk_toggle_tool_button_get_active (button)); } static void on_url_entry_activate (GtkEntry *entry, GwhBrowser *self) { gwh_browser_set_uri (self, gtk_entry_get_text (entry)); } static void on_url_combo_active_notify (GtkComboBox *combo, GParamSpec *pspec, GwhBrowser *self) { if (gtk_combo_box_get_active (combo) != -1) { const gchar *uri = gtk_entry_get_text (GTK_ENTRY (self->priv->url_entry)); gwh_browser_set_uri (self, uri); } } static void on_item_bookmark_toggled (GtkCheckMenuItem *item, GwhBrowser *self) { if (gtk_check_menu_item_get_active (item)) { gwh_browser_add_bookmark (self, gwh_browser_get_uri (self)); } else { gwh_browser_remove_bookmark (self, gwh_browser_get_uri (self)); } } static void on_url_entry_icon_press (GtkEntry *entry, GtkEntryIconPosition icon_pos, GdkEventButton *event, GwhBrowser *self) { if (icon_pos == GTK_ENTRY_ICON_PRIMARY) { GtkWidget *menu = gtk_menu_new (); GtkWidget *item; const gchar *uri = gwh_browser_get_uri (self); item = gtk_check_menu_item_new_with_mnemonic (_("Bookmark this website")); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), gwh_browser_has_bookmark (self, uri)); g_signal_connect (item, "toggled", G_CALLBACK (on_item_bookmark_toggled), self); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); gtk_widget_show (item); gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, event->button, event->time); } } static gboolean on_entry_completion_match_selected (GtkEntryCompletion *comp, GtkTreeModel *model, GtkTreeIter *iter, GwhBrowser *self) { gint column = gtk_entry_completion_get_text_column (comp); gchar *row; gtk_tree_model_get (model, iter, column, &row, -1); /* set the entry value too in the unlikely case the selected URI is the * currently viewed one, in which case set_uri() won't change it */ gtk_entry_set_text (GTK_ENTRY (self->priv->url_entry), row); gwh_browser_set_uri (self, row); g_free (row); return TRUE; } static void update_history (GwhBrowser *self) { WebKitWebView *web_view = WEBKIT_WEB_VIEW (self->priv->web_view); gtk_widget_set_sensitive (GTK_WIDGET (self->priv->item_prev), webkit_web_view_can_go_back (web_view)); gtk_widget_set_sensitive (GTK_WIDGET (self->priv->item_next), webkit_web_view_can_go_forward (web_view)); } static void update_load_status (GwhBrowser *self) { gboolean loading = FALSE; WebKitWebView *web_view = WEBKIT_WEB_VIEW (self->priv->web_view); switch (webkit_web_view_get_load_status (web_view)) { case WEBKIT_LOAD_PROVISIONAL: case WEBKIT_LOAD_COMMITTED: case WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT: loading = TRUE; break; case WEBKIT_LOAD_FINISHED: case WEBKIT_LOAD_FAILED: loading = FALSE; break; } gtk_widget_set_sensitive (GTK_WIDGET (self->priv->item_reload), ! loading); gtk_widget_set_visible (GTK_WIDGET (self->priv->item_reload), ! loading); gtk_widget_set_sensitive (GTK_WIDGET (self->priv->item_cancel), loading); gtk_widget_set_visible (GTK_WIDGET (self->priv->item_cancel), loading); update_history (self); } static void on_web_view_load_status_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { update_load_status (self); } static gboolean on_web_view_load_error (WebKitWebView *web_view, WebKitWebFrame *web_frame, gchar *uri, gpointer web_error, GwhBrowser *self) { update_load_status (self); return FALSE; /* we didn't really handled the error, so return %FALSE */ } static void on_web_view_uri_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { const gchar *uri; uri = webkit_web_view_get_uri (WEBKIT_WEB_VIEW (self->priv->web_view)); gtk_entry_set_text (GTK_ENTRY (self->priv->url_entry), uri); g_object_set (self->priv->settings, "browser-last-uri", uri, NULL); update_history (self); } static void on_web_view_icon_uri_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { const gchar *icon_uri; icon_uri = webkit_web_view_get_icon_uri (WEBKIT_WEB_VIEW (self->priv->web_view)); set_location_icon (self, icon_uri); } static void on_web_view_progress_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { gdouble value; value = webkit_web_view_get_progress (WEBKIT_WEB_VIEW (self->priv->web_view)); if (value >= 1.0) { value = 0.0; } gtk_entry_set_progress_fraction (GTK_ENTRY (self->priv->url_entry), value); } static void on_item_flip_orientation_activate (GtkMenuItem *item, GwhBrowser *self) { gtk_orientable_set_orientation (GTK_ORIENTABLE (self), gtk_orientable_get_orientation (GTK_ORIENTABLE (self)) == GTK_ORIENTATION_VERTICAL ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL); } static void on_item_zoom_100_activate (GtkMenuItem *item, GwhBrowser *self) { webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (self->priv->web_view), 1.0); } static void on_item_full_content_zoom_activate (GtkCheckMenuItem *item, GwhBrowser *self) { webkit_web_view_set_full_content_zoom (WEBKIT_WEB_VIEW (self->priv->web_view), gtk_check_menu_item_get_active (item)); } static void on_web_view_populate_popup (WebKitWebView *view, GtkMenu *menu, GwhBrowser *self) { GtkWidget *item; GtkWidget *submenu; #define ADD_SEPARATOR(menu) \ item = gtk_separator_menu_item_new (); \ gtk_widget_show (item); \ gtk_menu_shell_append (GTK_MENU_SHELL (menu), item) ADD_SEPARATOR (menu); /* Zoom menu */ submenu = gtk_menu_new (); item = gtk_menu_item_new_with_mnemonic (_("_Zoom")); gtk_widget_show (item); gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); /* zoom in */ item = gtk_image_menu_item_new_from_stock (GTK_STOCK_ZOOM_IN, NULL); g_signal_connect_swapped (item, "activate", G_CALLBACK (webkit_web_view_zoom_in), view); gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); /* zoom out */ item = gtk_image_menu_item_new_from_stock (GTK_STOCK_ZOOM_OUT, NULL); g_signal_connect_swapped (item, "activate", G_CALLBACK (webkit_web_view_zoom_out), view); gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); /* zoom 1:1 */ ADD_SEPARATOR (submenu); item = gtk_image_menu_item_new_from_stock (GTK_STOCK_ZOOM_100, NULL); g_signal_connect (item, "activate", G_CALLBACK (on_item_zoom_100_activate), self); gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); /* full content zoom */ ADD_SEPARATOR (submenu); item = gtk_check_menu_item_new_with_mnemonic (_("Full-_content zoom")); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), webkit_web_view_get_full_content_zoom (view)); g_signal_connect (item, "activate", G_CALLBACK (on_item_full_content_zoom_activate), self); gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); /* show zoom sumbenu */ gtk_widget_show_all (submenu); ADD_SEPARATOR (menu); item = gtk_menu_item_new_with_label (_("Flip panes orientation")); g_signal_connect (item, "activate", G_CALLBACK (on_item_flip_orientation_activate), self); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); if (! INSPECTOR_VISIBLE (self) || INSPECTOR_DETACHED (self)) { gtk_widget_set_sensitive (item, FALSE); } #undef ADD_SEPARATOR g_signal_emit (self, signals[POPULATE_POPUP], 0, menu); } static gboolean on_web_view_scroll_event (GtkWidget *widget, GdkEventScroll *event, GwhBrowser *self) { guint mods = event->state & gtk_accelerator_get_default_mod_mask (); gboolean handled = FALSE; if (mods == GDK_CONTROL_MASK) { handled = TRUE; switch (event->direction) { case GDK_SCROLL_DOWN: webkit_web_view_zoom_out (WEBKIT_WEB_VIEW (self->priv->web_view)); break; case GDK_SCROLL_UP: webkit_web_view_zoom_in (WEBKIT_WEB_VIEW (self->priv->web_view)); break; default: handled = FALSE; } } return handled; } static void on_orientation_notify (GObject *object, GParamSpec *pspec, GwhBrowser *self) { g_object_set (G_OBJECT (self->priv->settings), "browser-orientation", gtk_orientable_get_orientation (GTK_ORIENTABLE (self)), NULL); } static void gwh_browser_destroy (GtkObject *object) { GwhBrowser *self = GWH_BROWSER (object); gchar *geometry; /* save the setting now because we can't really set it at the time it changed, * but it's not a problem, anyway probably nobody but us is interested by the * geometry of our inspector window. */ geometry = get_web_inspector_window_geometry (self); g_object_set (self->priv->settings, "inspector-window-geometry", geometry, NULL); g_free (geometry); /* remove signal handlers that might get called during the destruction phase * but that rely on stuff that might already heave been destroyed */ g_signal_handlers_disconnect_matched (self->priv->inspector, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, self); g_signal_handlers_disconnect_matched (self->priv->web_view, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, self); g_signal_handlers_disconnect_matched (self->priv->settings, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, self); /* also destroy the window, since it has no parent that will tell it to die */ gtk_widget_destroy (self->priv->inspector_window); GTK_OBJECT_CLASS (gwh_browser_parent_class)->destroy (object); } static void gwh_browser_finalize (GObject *object) { GwhBrowser *self = GWH_BROWSER (object); if (self->priv->default_icon) { g_object_unref (self->priv->default_icon); } g_object_unref (self->priv->settings); g_object_unref (self->priv->statusbar); g_free (self->priv->hovered_link); G_OBJECT_CLASS (gwh_browser_parent_class)->finalize (object); } static void gwh_browser_constructed (GObject *object) { GwhBrowser *self = GWH_BROWSER (object); if (G_OBJECT_CLASS (gwh_browser_parent_class)->constructed) { G_OBJECT_CLASS (gwh_browser_parent_class)->constructed (object); } /* a bit ugly, fake notifications */ g_object_notify (G_OBJECT (self->priv->settings), "browser-last-uri"); g_object_notify (G_OBJECT (self->priv->settings), "browser-bookmarks"); g_object_notify (G_OBJECT (self->priv->settings), "browser-orientation"); g_object_notify (G_OBJECT (self->priv->settings), "inspector-window-geometry"); } static void gwh_browser_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { switch (prop_id) { case PROP_INSPECTOR_TRANSIENT_FOR: g_value_set_object (value, gwh_browser_get_inspector_transient_for (GWH_BROWSER (object))); break; case PROP_URI: g_value_set_string (value, gwh_browser_get_uri (GWH_BROWSER (object))); break; case PROP_ORIENTATION: g_value_set_enum (value, gtk_orientable_get_orientation (GTK_ORIENTABLE (GWH_BROWSER (object)->priv->paned))); break; case PROP_TOOLBAR: g_value_set_object (value, GWH_BROWSER (object)->priv->toolbar); break; case PROP_WEB_VIEW: g_value_set_object (value, GWH_BROWSER (object)->priv->web_view); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gwh_browser_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { switch (prop_id) { case PROP_INSPECTOR_TRANSIENT_FOR: gwh_browser_set_inspector_transient_for (GWH_BROWSER (object), g_value_get_object (value)); break; case PROP_URI: gwh_browser_set_uri (GWH_BROWSER (object), g_value_get_string (value)); break; case PROP_ORIENTATION: gtk_orientable_set_orientation (GTK_ORIENTABLE (GWH_BROWSER (object)->priv->paned), g_value_get_enum (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gwh_browser_show_all (GtkWidget *widget) { /* don't show_all() on this widget, it would show the cancel or reload * button */ gtk_widget_show (widget); } static void gwh_browser_class_init (GwhBrowserClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkObjectClass *gtkobject_class = GTK_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->finalize = gwh_browser_finalize; object_class->constructed = gwh_browser_constructed; object_class->get_property = gwh_browser_get_property; object_class->set_property = gwh_browser_set_property; gtkobject_class->destroy = gwh_browser_destroy; widget_class->show_all = gwh_browser_show_all; signals[POPULATE_POPUP] = g_signal_new ("populate-popup", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GwhBrowserClass, populate_popup), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, GTK_TYPE_MENU); g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation"); g_object_class_install_property (object_class, PROP_INSPECTOR_TRANSIENT_FOR, g_param_spec_object ("inspector-transient-for", "Inspector transient for", "The parent window of the inspector when detached", GTK_TYPE_WINDOW, G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_URI, g_param_spec_string ("uri", "URI", "The browser's URI", NULL, G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_WEB_VIEW, g_param_spec_object ("web-view", "Web view", "The browser's web view", WEBKIT_TYPE_WEB_VIEW, G_PARAM_READABLE)); g_object_class_install_property (object_class, PROP_TOOLBAR, g_param_spec_object ("toolbar", "Toolbar", "The browser's toolbar", GTK_TYPE_TOOLBAR, G_PARAM_READABLE)); g_type_class_add_private (klass, sizeof (GwhBrowserPrivate)); } /* a GtkEntryCompletionMatchFunc matching anywhere in the haystack */ static gboolean url_completion_match_func (GtkEntryCompletion *comp, const gchar *key, GtkTreeIter *iter, gpointer dummy) { GtkTreeModel *model = gtk_entry_completion_get_model (comp); gint column = gtk_entry_completion_get_text_column (comp); gchar *row = NULL; gboolean match = FALSE; gtk_tree_model_get (model, iter, column, &row, -1); if (row) { SETPTR (row, g_utf8_normalize (row, -1, G_NORMALIZE_DEFAULT)); SETPTR (row, g_utf8_casefold (row, -1)); match = strstr (row, key) != NULL; g_free (row); } return match; } static GtkWidget * create_toolbar (GwhBrowser *self) { GtkWidget *toolbar; GtkToolItem *item; GtkEntryCompletion *comp; toolbar = g_object_new (GTK_TYPE_TOOLBAR, "icon-size", GTK_ICON_SIZE_MENU, "toolbar-style", GTK_TOOLBAR_ICONS, NULL); self->priv->item_prev = gtk_tool_button_new_from_stock (GTK_STOCK_GO_BACK); gtk_tool_item_set_tooltip_text (self->priv->item_prev, _("Back")); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), self->priv->item_prev, -1); gtk_widget_show (GTK_WIDGET (self->priv->item_prev)); self->priv->item_next = gtk_tool_button_new_from_stock (GTK_STOCK_GO_FORWARD); gtk_tool_item_set_tooltip_text (self->priv->item_next, _("Forward")); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), self->priv->item_next, -1); gtk_widget_show (GTK_WIDGET (self->priv->item_next)); self->priv->item_cancel = gtk_tool_button_new_from_stock (GTK_STOCK_CANCEL); gtk_tool_item_set_tooltip_text (self->priv->item_cancel, _("Cancel loading")); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), self->priv->item_cancel, -1); /* don't show cancel */ self->priv->item_reload = gtk_tool_button_new_from_stock (GTK_STOCK_REFRESH); gtk_tool_item_set_tooltip_text (self->priv->item_reload, _("Reload current page")); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), self->priv->item_reload, -1); gtk_widget_show (GTK_WIDGET (self->priv->item_reload)); self->priv->url_combo = gtk_combo_box_text_new_with_entry (); item = gtk_tool_item_new (); gtk_tool_item_set_is_important (item, TRUE); gtk_container_add (GTK_CONTAINER (item), self->priv->url_combo); gtk_tool_item_set_expand (item, TRUE); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), item, -1); gtk_widget_show_all (GTK_WIDGET (item)); self->priv->url_entry = gtk_bin_get_child (GTK_BIN (self->priv->url_combo)); set_location_icon (self, NULL); gtk_entry_set_icon_tooltip_text (GTK_ENTRY (self->priv->url_entry), GTK_ENTRY_ICON_PRIMARY, _("Website information and settings")); comp = gtk_entry_completion_new (); gtk_entry_completion_set_model (comp, gtk_combo_box_get_model (GTK_COMBO_BOX (self->priv->url_combo))); gtk_entry_completion_set_text_column (comp, gtk_combo_box_get_entry_text_column (GTK_COMBO_BOX (self->priv->url_combo))); gtk_entry_completion_set_match_func (comp, url_completion_match_func, NULL, NULL); gtk_entry_set_completion (GTK_ENTRY (self->priv->url_entry), comp); self->priv->item_inspector = gtk_toggle_tool_button_new_from_stock (GTK_STOCK_INFO); gtk_tool_button_set_label (GTK_TOOL_BUTTON (self->priv->item_inspector), _("Web inspector")); gtk_tool_item_set_tooltip_text (self->priv->item_inspector, _("Toggle web inspector")); gtk_toolbar_insert (GTK_TOOLBAR (toolbar), self->priv->item_inspector, -1); gtk_widget_show (GTK_WIDGET (self->priv->item_inspector)); gtk_widget_set_sensitive (GTK_WIDGET (self->priv->item_prev), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (self->priv->item_next), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (self->priv->item_cancel), FALSE); g_signal_connect_swapped (G_OBJECT (self->priv->item_prev), "clicked", G_CALLBACK (webkit_web_view_go_back), self->priv->web_view); g_signal_connect_swapped (G_OBJECT (self->priv->item_next), "clicked", G_CALLBACK (webkit_web_view_go_forward), self->priv->web_view); g_signal_connect_swapped (G_OBJECT (self->priv->item_cancel), "clicked", G_CALLBACK (webkit_web_view_stop_loading), self->priv->web_view); g_signal_connect_swapped (G_OBJECT (self->priv->item_reload), "clicked", G_CALLBACK (webkit_web_view_reload), self->priv->web_view); g_signal_connect (G_OBJECT (self->priv->item_inspector), "toggled", G_CALLBACK (on_item_inspector_toggled), self); g_signal_connect (G_OBJECT (self->priv->url_entry), "activate", G_CALLBACK (on_url_entry_activate), self); g_signal_connect (G_OBJECT (self->priv->url_entry), "icon-press", G_CALLBACK (on_url_entry_icon_press), self); g_signal_connect (G_OBJECT (self->priv->url_combo), "notify::active", G_CALLBACK (on_url_combo_active_notify), self); g_signal_connect (G_OBJECT (comp), "match-selected", G_CALLBACK (on_entry_completion_match_selected), self); return toolbar; } static GtkWidget * create_inspector_window (GwhBrowser *self) { gboolean skips_taskbar; gboolean window_type; g_object_get (self->priv->settings, "wm-secondary-windows-skip-taskbar", &skips_taskbar, "wm-secondary-windows-type", &window_type, NULL); self->priv->inspector_window_x = self->priv->inspector_window_y = 0; self->priv->inspector_window = g_object_new (GTK_TYPE_WINDOW, "type", GTK_WINDOW_TOPLEVEL, "skip-taskbar-hint", skips_taskbar, "type-hint", window_type, "title", _("Web inspector"), NULL); g_signal_connect (self->priv->inspector_window, "delete-event", G_CALLBACK (on_inspector_window_delete_event), self); g_signal_connect (self->priv->settings, "notify::wm-secondary-windows-skip-taskbar", G_CALLBACK (on_settings_wm_windows_skip_taskbar_notify), self); g_signal_connect (self->priv->settings, "notify::wm-secondary-windows-type", G_CALLBACK (on_settings_wm_windows_type_notify), self); return self->priv->inspector_window; } static guint get_statusbar_context_id (GtkStatusbar *statusbar) { static guint id = 0; if (id == 0) { id = gtk_statusbar_get_context_id (statusbar, "gwh-browser-hovered-link"); } return id; } static void on_web_view_hovering_over_link (WebKitWebView *view, gchar *title, gchar *uri, GwhBrowser *self) { GtkStatusbar *statusbar = GTK_STATUSBAR (self->priv->statusbar); if (self->priv->hovered_link) { gtk_statusbar_pop (statusbar, get_statusbar_context_id (statusbar)); g_free (self->priv->hovered_link); self->priv->hovered_link = NULL; } if (uri && *uri) { self->priv->hovered_link = g_strdup (uri); gtk_statusbar_push (statusbar, get_statusbar_context_id (statusbar), self->priv->hovered_link); } } static void on_web_view_leave_notify_event (GtkWidget *widget, GdkEventCrossing *event, GwhBrowser *self) { if (self->priv->hovered_link) { GtkStatusbar *statusbar = GTK_STATUSBAR (self->priv->statusbar); gtk_statusbar_pop (statusbar, get_statusbar_context_id (statusbar)); } } static void on_web_view_enter_notify_event (GtkWidget *widget, GdkEventCrossing *event, GwhBrowser *self) { if (self->priv->hovered_link) { GtkStatusbar *statusbar = GTK_STATUSBAR (self->priv->statusbar); gtk_statusbar_push (statusbar, get_statusbar_context_id (statusbar), self->priv->hovered_link); } } static void on_web_view_realize (GtkWidget *widget, GwhBrowser *self) { gtk_widget_add_events (widget, GDK_LEAVE_NOTIFY_MASK | GDK_ENTER_NOTIFY_MASK); } static void gwh_browser_init (GwhBrowser *self) { GtkWidget *scrolled; WebKitWebSettings *wkws; gboolean inspector_detached; self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GWH_TYPE_BROWSER, GwhBrowserPrivate); self->priv->default_icon = NULL; /* web view need to be created first because we use it in create_toolbar() */ self->priv->web_view = webkit_web_view_new (); wkws = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (self->priv->web_view)); g_object_set (wkws, "enable-developer-extras", TRUE, NULL); self->priv->settings = gwh_settings_get_default (); g_object_get (self->priv->settings, "inspector-detached", &inspector_detached, NULL); self->priv->toolbar = create_toolbar (self); self->priv->paned = gtk_vpaned_new (); scrolled = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add (GTK_CONTAINER (scrolled), self->priv->web_view); gtk_box_pack_start (GTK_BOX (self), self->priv->toolbar, FALSE, TRUE, 0); gtk_widget_show (self->priv->toolbar); gtk_box_pack_start (GTK_BOX (self), self->priv->paned, TRUE, TRUE, 0); gtk_paned_pack1 (GTK_PANED (self->priv->paned), scrolled, TRUE, TRUE); gtk_widget_show_all (self->priv->paned); self->priv->inspector_view = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (self->priv->inspector_view), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); self->priv->inspector_web_view = NULL; self->priv->inspector_window = create_inspector_window (self); gtk_container_add (GTK_CONTAINER (inspector_detached ? self->priv->inspector_window : self->priv->paned), self->priv->inspector_view); self->priv->statusbar = ui_lookup_widget (geany->main_widgets->window, "statusbar"); if (self->priv->statusbar) { g_object_ref (self->priv->statusbar); } else { /* in the unlikely case we can't get the Geany statusbar, fake one */ self->priv->statusbar = gtk_statusbar_new (); } self->priv->hovered_link = NULL; g_signal_connect (self, "notify::orientation", G_CALLBACK (on_orientation_notify), self); self->priv->inspector = webkit_web_view_get_inspector (WEBKIT_WEB_VIEW (self->priv->web_view)); g_signal_connect (self->priv->inspector, "inspect-web-view", G_CALLBACK (on_inspector_inspect_web_view), self); g_signal_connect (self->priv->inspector, "show-window", G_CALLBACK (on_inspector_show_window), self); g_signal_connect (self->priv->inspector, "close-window", G_CALLBACK (on_inspector_close_window), self); g_signal_connect (self->priv->inspector, "detach-window", G_CALLBACK (on_inspector_detach_window), self); g_signal_connect (self->priv->inspector, "attach-window", G_CALLBACK (on_inspector_attach_window), self); g_signal_connect (G_OBJECT (self->priv->web_view), "notify::progress", G_CALLBACK (on_web_view_progress_notify), self); g_signal_connect (G_OBJECT (self->priv->web_view), "notify::uri", G_CALLBACK (on_web_view_uri_notify), self); g_signal_connect (G_OBJECT (self->priv->web_view), "notify::load-status", G_CALLBACK (on_web_view_load_status_notify), self); g_signal_connect (G_OBJECT (self->priv->web_view), "notify::load-error", G_CALLBACK (on_web_view_load_error), self); g_signal_connect (G_OBJECT (self->priv->web_view), "notify::icon-uri", G_CALLBACK (on_web_view_icon_uri_notify), self); g_signal_connect (G_OBJECT (self->priv->web_view), "populate-popup", G_CALLBACK (on_web_view_populate_popup), self); g_signal_connect (G_OBJECT (self->priv->web_view), "scroll-event", G_CALLBACK (on_web_view_scroll_event), self); g_signal_connect (G_OBJECT (self->priv->web_view), "hovering-over-link", G_CALLBACK (on_web_view_hovering_over_link), self); g_signal_connect (G_OBJECT (self->priv->web_view), "leave-notify-event", G_CALLBACK (on_web_view_leave_notify_event), self); g_signal_connect (G_OBJECT (self->priv->web_view), "enter-notify-event", G_CALLBACK (on_web_view_enter_notify_event), self); g_signal_connect_after (self->priv->web_view, "realize", G_CALLBACK (on_web_view_realize), self); g_signal_connect (self->priv->web_view, "key-press-event", G_CALLBACK (gwh_keybindings_handle_event), self); g_signal_connect (self->priv->inspector_view, "key-press-event", G_CALLBACK (gwh_keybindings_handle_event), self); gtk_widget_grab_focus (self->priv->url_entry); g_signal_connect (self->priv->settings, "notify::browser-last-uri", G_CALLBACK (on_settings_browser_last_uri_notify), self); g_signal_connect (self->priv->settings, "notify::browser-bookmarks", G_CALLBACK (on_settings_browser_bookmarks_notify), self); g_signal_connect (self->priv->settings, "notify::browser-orientation", G_CALLBACK (on_settings_browser_orientation_notify), self); g_signal_connect (self->priv->settings, "notify::inspector-detached", G_CALLBACK (on_settings_inspector_detached_notify), self); g_signal_connect (self->priv->settings, "notify::inspector-window-geometry", G_CALLBACK (on_settings_inspector_window_geometry_notify), self); } /*----------------------------- Begin public API -----------------------------*/ GtkWidget * gwh_browser_new (void) { return g_object_new (GWH_TYPE_BROWSER, NULL); } void gwh_browser_set_uri (GwhBrowser *self, const gchar *uri) { gchar *real_uri; gchar *scheme; g_return_if_fail (GWH_IS_BROWSER (self)); g_return_if_fail (uri != NULL); real_uri = g_strdup (uri); scheme = g_uri_parse_scheme (real_uri); if (! scheme) { gchar *tmp; tmp = g_strconcat ("http://", uri, NULL); g_free (real_uri); real_uri = tmp; } g_free (scheme); if (g_strcmp0 (real_uri, gwh_browser_get_uri (self)) != 0) { webkit_web_view_open (WEBKIT_WEB_VIEW (self->priv->web_view), real_uri); g_object_notify (G_OBJECT (self), "uri"); } g_free (real_uri); } const gchar * gwh_browser_get_uri (GwhBrowser *self) { g_return_val_if_fail (GWH_IS_BROWSER (self), NULL); return webkit_web_view_get_uri (WEBKIT_WEB_VIEW (self->priv->web_view)); } GtkToolbar * gwh_browser_get_toolbar (GwhBrowser *self) { g_return_val_if_fail (GWH_IS_BROWSER (self), NULL); return GTK_TOOLBAR (self->priv->toolbar); } WebKitWebView * gwh_browser_get_web_view (GwhBrowser *self) { g_return_val_if_fail (GWH_IS_BROWSER (self), NULL); return WEBKIT_WEB_VIEW (self->priv->web_view); } void gwh_browser_reload (GwhBrowser *self) { g_return_if_fail (GWH_IS_BROWSER (self)); webkit_web_view_reload (WEBKIT_WEB_VIEW (self->priv->web_view)); } void gwh_browser_set_inspector_transient_for (GwhBrowser *self, GtkWindow *window) { g_return_if_fail (GWH_IS_BROWSER (self)); g_return_if_fail (window == NULL || GTK_IS_WINDOW (window)); gtk_window_set_transient_for (GTK_WINDOW (self->priv->inspector_window), window); } GtkWindow * gwh_browser_get_inspector_transient_for (GwhBrowser *self) { g_return_val_if_fail (GWH_IS_BROWSER (self), NULL); return gtk_window_get_transient_for (GTK_WINDOW (self->priv->inspector_window)); } void gwh_browser_toggle_inspector (GwhBrowser *self) { g_return_if_fail (GWH_IS_BROWSER (self)); inspector_set_visible (self, ! INSPECTOR_VISIBLE (self)); } gchar ** gwh_browser_get_bookmarks (GwhBrowser *self) { gchar **bookmarks = NULL; g_return_val_if_fail (GWH_IS_BROWSER (self), NULL); g_object_get (self->priv->settings, "browser-bookmarks", &bookmarks, NULL); return bookmarks; } static void gwh_browser_set_bookmarks (GwhBrowser *self, gchar **bookmarks) { g_object_set (self->priv->settings, "browser-bookmarks", bookmarks, NULL); } static gint strv_index (gchar **strv, const gchar *str) { g_return_val_if_fail (str != NULL, -1); if (strv) { gint idx; for (idx = 0; *strv; strv++, idx++) { if (strcmp (str, *strv) == 0) { return idx; } } } return -1; } gboolean gwh_browser_has_bookmark (GwhBrowser *self, const gchar *uri) { gchar **bookmarks = NULL; gboolean exists = FALSE; g_return_val_if_fail (GWH_IS_BROWSER (self), FALSE); g_return_val_if_fail (uri != NULL, FALSE); bookmarks = gwh_browser_get_bookmarks (self); exists = strv_index (bookmarks, uri) >= 0; g_strfreev (bookmarks); return exists; } static const gchar * uri_skip_scheme (const gchar *uri) { if (g_ascii_isalpha (*uri)) { do { uri++; } while (*uri == '+' || *uri == '-' || *uri == '.' || g_ascii_isalnum (*uri)); /* this is not strictly correct but good enough for what we do */ while (*uri == ':' || *uri == '/') uri++; } return uri; } static int sort_uris (gconstpointer a, gconstpointer b) { const gchar *uri1 = uri_skip_scheme (*(const gchar *const *) a); const gchar *uri2 = uri_skip_scheme (*(const gchar *const *) b); return g_ascii_strcasecmp (uri1, uri2); } void gwh_browser_add_bookmark (GwhBrowser *self, const gchar *uri) { gchar **bookmarks = NULL; g_return_if_fail (GWH_IS_BROWSER (self)); g_return_if_fail (uri != NULL); bookmarks = gwh_browser_get_bookmarks (self); if (strv_index (bookmarks, uri) < 0) { gsize length = bookmarks ? g_strv_length (bookmarks) : 0; bookmarks = g_realloc (bookmarks, (length + 2) * sizeof *bookmarks); bookmarks[length] = g_strdup (uri); bookmarks[length + 1] = NULL; /* it would be faster to insert directly at the right place but who cares */ qsort (bookmarks, length + 1, sizeof *bookmarks, sort_uris); gwh_browser_set_bookmarks (self, bookmarks); } g_strfreev (bookmarks); } void gwh_browser_remove_bookmark (GwhBrowser *self, const gchar *uri) { gchar **bookmarks = NULL; gint idx; g_return_if_fail (GWH_IS_BROWSER (self)); g_return_if_fail (uri != NULL); bookmarks = gwh_browser_get_bookmarks (self); idx = strv_index (bookmarks, uri); if (idx >= 0) { gsize length = g_strv_length (bookmarks); memmove (&bookmarks[idx], &bookmarks[idx + 1], (length - (gsize) idx) * sizeof *bookmarks); gwh_browser_set_bookmarks (self, bookmarks); } g_strfreev (bookmarks); }