/* * Web view * * Copyright (C) 2017 Patrick McDermott * * This file is part of Marquee. * * Marquee 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. * * Marquee 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 Marquee. If not, see . */ #include #include #include #include #include #include "web-view.h" #include "tab.h" #include "config.h" struct _MqWebView { WebKitWebView parent_instance; MqTab *tab; gchar *uri; MqConfig *config; WebKitHitTestResult *hit_test_result; WebKitHitTestResult *mouse_target_hit_test_result; }; enum { PROP_TAB = 1, PROP_REWRITTEN_URI, N_PROPERTIES }; static GParamSpec *obj_properties[N_PROPERTIES] = {NULL,}; struct _MqWebViewClass { WebKitWebViewClass parent_class; }; G_DEFINE_TYPE(MqWebView, mq_web_view, WEBKIT_TYPE_WEB_VIEW) #define WKCMA(ACTION) \ WEBKIT_CONTEXT_MENU_ACTION_##ACTION static void menu_open_link_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), webkit_hit_test_result_get_link_uri( web_view->hit_test_result)); } static void menu_open_link_tab_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_tab_new_relative( webkit_hit_test_result_get_link_uri(web_view->hit_test_result), web_view->tab); } static void menu_open_link_win_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { const gchar *uris[2] = { webkit_hit_test_result_get_link_uri(web_view->hit_test_result), NULL }; mq_application_add_window(mq_tab_get_application(web_view->tab), uris); } static void menu_open_image_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), webkit_hit_test_result_get_image_uri( web_view->hit_test_result)); } static void menu_open_image_tab_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_tab_new_relative( webkit_hit_test_result_get_image_uri(web_view->hit_test_result), web_view->tab); } static void menu_open_image_win_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { const gchar *uris[2] = { webkit_hit_test_result_get_image_uri(web_view->hit_test_result), NULL }; mq_application_add_window(mq_tab_get_application(web_view->tab), uris); } static void menu_open_video_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), webkit_hit_test_result_get_media_uri( web_view->hit_test_result)); } static void menu_open_video_tab_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_tab_new_relative( webkit_hit_test_result_get_media_uri(web_view->hit_test_result), web_view->tab); } static void menu_open_video_win_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { const gchar *uris[2] = { webkit_hit_test_result_get_media_uri(web_view->hit_test_result), NULL }; mq_application_add_window(mq_tab_get_application(web_view->tab), uris); } static void menu_open_audio_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), webkit_hit_test_result_get_media_uri( web_view->hit_test_result)); } static void menu_open_audio_tab_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_tab_new_relative( webkit_hit_test_result_get_media_uri(web_view->hit_test_result), web_view->tab); } static void menu_open_audio_win_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { const gchar *uris[2] = { webkit_hit_test_result_get_media_uri(web_view->hit_test_result), NULL }; mq_application_add_window(mq_tab_get_application(web_view->tab), uris); } #define ITEM_DECLS \ GtkAction *action; \ WebKitContextMenuItem *menu_item; #define ITEM_DECLS_NO_CUSTOM \ WebKitContextMenuItem *menu_item; #define NEW_CUSTOM_ITEM(NAME, LABEL) \ do { \ /* Don't blame me; blame WebKitGTK+ for using GtkAction. */ \ G_GNUC_BEGIN_IGNORE_DEPRECATIONS \ action = gtk_action_new(#NAME, (LABEL), NULL, NULL); \ G_GNUC_END_IGNORE_DEPRECATIONS \ g_signal_connect(action, "activate", \ G_CALLBACK(menu_##NAME##_activate_cb), web_view); \ menu_item = webkit_context_menu_item_new(action); \ webkit_context_menu_append(context_menu, menu_item); \ } while (0) #define NEW_STOCK_ITEM(STOCK) \ do { \ menu_item = webkit_context_menu_item_new_from_stock_action( \ WEBKIT_CONTEXT_MENU_ACTION_##STOCK); \ webkit_context_menu_append(context_menu, menu_item); \ } while (0) #define NEW_SEPARATOR_ITEM() \ do { \ webkit_context_menu_append(context_menu, \ webkit_context_menu_item_new_separator()); \ } while (0) #define RESTORE_ITEMS(ITEMS) \ do { \ for (; ITEMS; ITEMS = ITEMS->next) { \ webkit_context_menu_append(context_menu, ITEMS->data); \ g_object_unref(ITEMS->data); \ } \ } while (0) static void context_menu_link_cb(WebKitContextMenu *context_menu, MqWebView *web_view) { ITEM_DECLS NEW_CUSTOM_ITEM(open_link, "_Open Link"); NEW_CUSTOM_ITEM(open_link_tab, "Open Link in New _Tab"); NEW_CUSTOM_ITEM(open_link_win, "Open Link in New _Window"); NEW_SEPARATOR_ITEM(); /* --- */ NEW_STOCK_ITEM(DOWNLOAD_LINK_TO_DISK); /* _Download Linked File */ NEW_STOCK_ITEM(COPY_LINK_TO_CLIPBOARD); /* Copy Link Loc_ation */ } static void context_menu_image_cb(WebKitContextMenu *context_menu, MqWebView *web_view) { ITEM_DECLS NEW_CUSTOM_ITEM(open_image, "Open _Image"); NEW_CUSTOM_ITEM(open_image_tab, "Open Image in New Tab"); NEW_CUSTOM_ITEM(open_image_win, "Open Image in New Window"); NEW_SEPARATOR_ITEM(); /* --- */ NEW_STOCK_ITEM(DOWNLOAD_IMAGE_TO_DISK); /* Sa_ve Image As */ NEW_STOCK_ITEM(COPY_IMAGE_TO_CLIPBOARD); /* Cop_y Image */ NEW_STOCK_ITEM(COPY_IMAGE_URL_TO_CLIPBOARD); /* Copy Image _Address */ } static void context_menu_media_cb(WebKitContextMenu *context_menu, GList *media_ctrl_items, GList *media_toggle_items, gboolean is_video, MqWebView *web_view) { ITEM_DECLS /* _Play/_Pause, _Mute */ RESTORE_ITEMS(media_ctrl_items); /* _Toggle Media Controls, Toggle Media _Loop Playback, Switch Video to * _Fullscreen */ RESTORE_ITEMS(media_toggle_items); NEW_SEPARATOR_ITEM(); /* --- */ if (is_video) { NEW_CUSTOM_ITEM(open_video, "_Open Video"); NEW_CUSTOM_ITEM(open_video_tab, "Open Video in New Tab"); NEW_CUSTOM_ITEM(open_video_win, "Open Video in New Window"); NEW_SEPARATOR_ITEM(); /* --- */ NEW_STOCK_ITEM(DOWNLOAD_VIDEO_TO_DISK); /* Download _Video */ /* Cop_y Video Link Location */ NEW_STOCK_ITEM(COPY_VIDEO_LINK_TO_CLIPBOARD); } else { NEW_CUSTOM_ITEM(open_audio, "_Open Audio"); NEW_CUSTOM_ITEM(open_audio_tab, "Open Audio in New Tab"); NEW_CUSTOM_ITEM(open_audio_win, "Open Audio in New Window"); NEW_SEPARATOR_ITEM(); /* --- */ NEW_STOCK_ITEM(DOWNLOAD_AUDIO_TO_DISK); /* Download _Audio */ /* Cop_y Audio Link Location */ NEW_STOCK_ITEM(COPY_AUDIO_LINK_TO_CLIPBOARD); } } static void context_menu_editable_cb(WebKitContextMenu *context_menu, GList *spell_repl_items, GList *spell_ctrl_items, GList *edit_items, GList *input_items, MqWebView G_GNUC_UNUSED *web_view) { ITEM_DECLS_NO_CUSTOM RESTORE_ITEMS(spell_repl_items); /* Spelling suggestions */ NEW_SEPARATOR_ITEM(); /* --- */ RESTORE_ITEMS(spell_ctrl_items); /* _Ignore Spelling, _Learn Spelling */ NEW_SEPARATOR_ITEM(); /* --- */ RESTORE_ITEMS(edit_items); /* Cu_t, _Copy, _Paste, _Delete */ NEW_SEPARATOR_ITEM(); /* --- */ NEW_STOCK_ITEM(SELECT_ALL); /* Select _All */ NEW_SEPARATOR_ITEM(); /* --- */ RESTORE_ITEMS(input_items); /* _Insert Unicode Character */ } static void context_menu_document_cb(WebKitContextMenu *context_menu, GList *nav_items, MqWebView G_GNUC_UNUSED *web_view) { ITEM_DECLS_NO_CUSTOM RESTORE_ITEMS(nav_items); /* _Back, _Forward, _Stop, _Reload */ NEW_SEPARATOR_ITEM(); /* --- */ NEW_STOCK_ITEM(SELECT_ALL); /* Select _All */ NEW_SEPARATOR_ITEM(); /* --- */ /* View Page Source */ } #define PRESERVE_ITEM(ITEMS) \ do { \ g_object_ref(items->data); \ ITEMS = g_list_prepend(ITEMS, items->data); \ } while (0) static gboolean context_menu_cb(WebKitWebView *wk_web_view, WebKitContextMenu *context_menu, GdkEvent G_GNUC_UNUSED *event, WebKitHitTestResult *hit_test_result) { MqWebView *web_view; GList *items; GList *nav_items; GList *edit_items; GList *input_items; GList *spell_repl_items; GList *spell_ctrl_items; GList *media_ctrl_items; GList *media_toggle_items; gboolean is_selection; gboolean is_video; WebKitContextMenuAction stock_action; WebKitHitTestResultContext context; gboolean context_handled; WebKitContextMenuItem *menu_item; web_view = MQ_WEB_VIEW(wk_web_view); /* Get more hints about the context, since WebKit doesn't describe * context very well in hit test results. Also, preserve menu items * that aren't easy to reproduce (e.g. the Unicode menu and spelling * guesses). */ items = webkit_context_menu_get_items(context_menu); nav_items = NULL; edit_items = NULL; input_items = NULL; spell_repl_items = NULL; spell_ctrl_items = NULL; media_ctrl_items = NULL; media_toggle_items = NULL; is_selection = FALSE; is_video = FALSE; for (; items; items = items->next) { stock_action = webkit_context_menu_item_get_stock_action( items->data); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch" switch (stock_action) { case WKCMA(GO_BACK): case WKCMA(GO_FORWARD): case WKCMA(STOP): case WKCMA(RELOAD): PRESERVE_ITEM(nav_items); break; case WKCMA(COPY): PRESERVE_ITEM(edit_items); is_selection = TRUE; break; case WKCMA(CUT): case WKCMA(PASTE): case WKCMA(DELETE): PRESERVE_ITEM(edit_items); break; case WKCMA(INPUT_METHODS): case WKCMA(UNICODE): PRESERVE_ITEM(input_items); break; case WKCMA(SPELLING_GUESS): case WKCMA(NO_GUESSES_FOUND): PRESERVE_ITEM(spell_repl_items); break; case WKCMA(IGNORE_SPELLING): case WKCMA(LEARN_SPELLING): case WKCMA(IGNORE_GRAMMAR): PRESERVE_ITEM(spell_ctrl_items); break; case WKCMA(OPEN_VIDEO_IN_NEW_WINDOW): case WKCMA(COPY_VIDEO_LINK_TO_CLIPBOARD): case WKCMA(DOWNLOAD_VIDEO_TO_DISK): is_video = TRUE; break; case WKCMA(OPEN_AUDIO_IN_NEW_WINDOW): case WKCMA(COPY_AUDIO_LINK_TO_CLIPBOARD): case WKCMA(DOWNLOAD_AUDIO_TO_DISK): is_video = FALSE; break; case WKCMA(TOGGLE_MEDIA_CONTROLS): case WKCMA(TOGGLE_MEDIA_LOOP): case WKCMA(ENTER_VIDEO_FULLSCREEN): PRESERVE_ITEM(media_toggle_items); break; case WKCMA(MEDIA_PLAY): case WKCMA(MEDIA_PAUSE): case WKCMA(MEDIA_MUTE): PRESERVE_ITEM(media_ctrl_items); break; } #pragma GCC diagnostic pop } nav_items = g_list_reverse(nav_items); edit_items = g_list_reverse(edit_items); input_items = g_list_reverse(input_items); spell_repl_items = g_list_reverse(spell_repl_items); spell_ctrl_items = g_list_reverse(spell_ctrl_items); media_ctrl_items = g_list_reverse(media_ctrl_items); media_toggle_items = g_list_reverse(media_toggle_items); /* Clear the menu. */ webkit_context_menu_remove_all(context_menu); /* Get the reported context (which isn't very descriptive) and save the * hit test result for use by action callbacks. */ context = webkit_hit_test_result_get_context(hit_test_result); if (web_view->hit_test_result) { g_object_unref(web_view->hit_test_result); } web_view->hit_test_result = hit_test_result; g_object_ref(web_view->hit_test_result); context_handled = FALSE; /* Build the context menu. */ if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) { context_menu_link_cb(context_menu, web_view); context_handled = TRUE; } if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE) { if (context_handled) { NEW_SEPARATOR_ITEM(); } context_menu_image_cb(context_menu, web_view); context_handled = TRUE; } if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_MEDIA) { context_menu_media_cb(context_menu, media_ctrl_items, media_toggle_items, is_video, web_view); context_handled = TRUE; } if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE) { context_menu_editable_cb(context_menu, spell_repl_items, spell_ctrl_items, edit_items, input_items, web_view); context_handled = TRUE; } if (!context_handled && context & WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) { if (is_selection) { RESTORE_ITEMS(edit_items); /* _Copy */ context_handled = TRUE; } else { context_menu_document_cb(context_menu, nav_items, web_view); context_handled = TRUE; } } if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) { if (context_handled) { NEW_SEPARATOR_ITEM(); } NEW_STOCK_ITEM(INSPECT_ELEMENT); /* Inspect Element */ } /* Propagate the event further and show the context menu. */ return FALSE; } static void uri_cb(WebKitWebView *wk_web_view, GParamSpec G_GNUC_UNUSED *paramspec) { const gchar *uri; MqWebView *web_view; web_view = MQ_WEB_VIEW(wk_web_view); if (web_view->uri) { g_free(web_view->uri); } uri = webkit_web_view_get_uri(wk_web_view); if (g_str_has_prefix(uri, "mq-about:")) { web_view->uri = g_strconcat("about:", uri + strlen("mq-about:"), NULL); } else { web_view->uri = g_strdup(uri); } g_object_notify_by_pspec(G_OBJECT(web_view), obj_properties[PROP_REWRITTEN_URI]); } static void clipboard_text_recv_cb(GtkClipboard G_GNUC_UNUSED *clipboard, const gchar *text, MqWebView *web_view) { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), text); } /* This callback is a hack to determine on middle mouse click whether to open a * link in a new tab or to load a URI from the primary clipboard. The WebKit1 * API provided webkit_web_view_get_hit_test_result() which would have been * easier. */ static void mouse_target_changed_cb(WebKitWebView *wk_web_view, WebKitHitTestResult *hit_test_result, guint G_GNUC_UNUSED modifiers) { MqWebView *web_view; web_view = MQ_WEB_VIEW(wk_web_view); web_view->mouse_target_hit_test_result = hit_test_result; g_object_ref(web_view->mouse_target_hit_test_result); } static gboolean button_press_cb(GtkWidget *widget, GdkEventButton *event) { MqWebView *web_view; WebKitHitTestResult *hit_test_result; GtkClipboard *clipboard; web_view = MQ_WEB_VIEW(widget); /* Make sure this is a middle mouse button press event. */ if (event->button != 2) { return FALSE; } hit_test_result = web_view->mouse_target_hit_test_result; if (webkit_hit_test_result_context_is_link(hit_test_result)) { mq_tab_new_relative( webkit_hit_test_result_get_link_uri(hit_test_result), web_view->tab); } else if (webkit_hit_test_result_context_is_editable(hit_test_result)){ /* Let WebKit handle pasting from the primary clipboard into an * editable element. */ g_object_unref(hit_test_result); return FALSE; } else { clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); gtk_clipboard_request_text(clipboard, (GtkClipboardTextReceivedFunc) clipboard_text_recv_cb, web_view); } g_object_unref(hit_test_result); return TRUE; } static void constructed(GObject *object) { MqWebView *web_view; gchar *rw_uri; gchar *new_tab_page; if (G_OBJECT_CLASS(mq_web_view_parent_class)->constructed) { G_OBJECT_CLASS(mq_web_view_parent_class)->constructed(object); } web_view = MQ_WEB_VIEW(object); web_view->config = mq_application_get_config( mq_tab_get_application(web_view->tab)); webkit_web_view_set_settings(WEBKIT_WEB_VIEW(web_view), mq_application_get_webkit_settings( mq_tab_get_application(web_view->tab))); if (web_view->uri) { if (g_str_has_prefix(web_view->uri, "about:")) { rw_uri = g_strconcat("mq-about:", web_view->uri + strlen("about:"), NULL); webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), rw_uri); g_free(rw_uri); } else { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), web_view->uri); } } else { new_tab_page = mq_config_get_string(web_view->config, "tabs.new"); if (g_strcmp0(new_tab_page, "home") == 0) { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), mq_config_get_string(web_view->config, "tabs.home")); } else if (g_strcmp0(new_tab_page, "blank") == 0) { /* Don't load any URI. */ } else { g_assert_not_reached(); } } mq_web_view_zoom_reset(web_view); gtk_widget_set_vexpand(GTK_WIDGET(web_view), TRUE); /* FIXME: This doesn't seem to be working. */ gtk_widget_grab_focus(GTK_WIDGET(web_view)); } static void finalize(GObject *object) { MqWebView *web_view; web_view = MQ_WEB_VIEW(object); if (web_view->uri) { g_free(web_view->uri); } } static void get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { MqWebView *web_view; web_view = MQ_WEB_VIEW(object); switch (property_id) { case PROP_TAB: g_value_set_pointer(value, web_view->tab); break; case PROP_REWRITTEN_URI: g_value_set_string(value, web_view->uri); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { MqWebView *web_view; web_view = MQ_WEB_VIEW(object); switch (property_id) { case PROP_TAB: web_view->tab = g_value_get_pointer(value); break; case PROP_REWRITTEN_URI: mq_web_view_load_uri(web_view, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void mq_web_view_class_init(MqWebViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = constructed; object_class->finalize = finalize; object_class->get_property = get_property; object_class->set_property = set_property; obj_properties[PROP_TAB] = g_param_spec_pointer( "tab", "MqTab", "The parent MqTab instance", G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB); obj_properties[PROP_REWRITTEN_URI] = g_param_spec_string( "rewritten-uri", "URI", "The current active URI of the Web view, " "with \"mq-about:\" rewritten to \"about:\"", "", G_PARAM_READWRITE | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB); g_object_class_install_properties(object_class, N_PROPERTIES, obj_properties); } static void mq_web_view_init(MqWebView *web_view) { web_view->hit_test_result = NULL; g_signal_connect(web_view, "context-menu", G_CALLBACK(context_menu_cb), NULL); g_signal_connect(web_view, "notify::uri", G_CALLBACK(uri_cb), NULL); g_signal_connect(web_view, "mouse-target-changed", G_CALLBACK(mouse_target_changed_cb), NULL); g_signal_connect(web_view, "button-press-event", G_CALLBACK(button_press_cb), NULL); } MqWebView * mq_web_view_new(MqTab *tab, const gchar *uri) { return g_object_new(MQ_TYPE_WEB_VIEW, "tab", tab, /* TODO: Use gtk_widget_get_parent() instead? */ "rewritten-uri", uri, "web-context", webkit_web_context_get_default(), NULL); } const gchar * mq_web_view_get_uri(MqWebView *web_view) { return web_view->uri; } void mq_web_view_load_uri(MqWebView *web_view, const gchar *uri) { gchar *rw_uri; if (!uri) { /* Happens during object construction. */ return; } if (web_view->uri) { g_free(web_view->uri); } web_view->uri = g_strdup(uri); if (g_str_has_prefix(uri, "about:")) { rw_uri = g_strconcat("mq-about:", uri + strlen("about:"), NULL); webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), rw_uri); g_free(rw_uri); } else { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), uri); } g_object_notify_by_pspec(G_OBJECT(web_view), obj_properties[PROP_REWRITTEN_URI]); } void mq_web_view_zoom_in(MqWebView *web_view) { gdouble zoom_level; zoom_level = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(web_view)); zoom_level += 0.1; if (zoom_level < 0) { zoom_level = G_MAXDOUBLE; } webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(web_view), zoom_level); } void mq_web_view_zoom_out(MqWebView *web_view) { gdouble zoom_level; zoom_level = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(web_view)); zoom_level -= 0.1; if (zoom_level < 0) { zoom_level = 0; } webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(web_view), zoom_level); } void mq_web_view_zoom_reset(MqWebView *web_view) { gdouble zoom_level; zoom_level = mq_config_get_double(web_view->config, "zoom.default"); webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(web_view), zoom_level); }