From f6c63159857a99d0871584c4e56832b5e5188058 Mon Sep 17 00:00:00 2001 From: Patrick McDermott Date: Wed, 11 Oct 2017 17:16:38 -0400 Subject: MqWebView: New class --- (limited to 'src/web-view.c') diff --git a/src/web-view.c b/src/web-view.c new file mode 100644 index 0000000..0feaeb9 --- /dev/null +++ b/src/web-view.c @@ -0,0 +1,653 @@ +/* + * 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" + +struct _MqWebView { + WebKitWebView parent_instance; + MqTab *tab; + const gchar *uri; + WebKitHitTestResult *hit_test_result; + WebKitHitTestResult *mouse_target_hit_test_result; +}; + +enum { + PROP_TAB = 1, + PROP_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 PARENT_CLASS WEBKIT_WEB_VIEW_CLASS(mq_web_view_parent_class) + +#define WKCMA(ACTION) \ + WEBKIT_CONTEXT_MENU_ACTION_##ACTION + +static void +get_property(GObject *object, guint property_id, GValue *value, + GParamSpec G_GNUC_UNUSED *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_URI: + g_value_set_string(value, web_view->uri); /* TODO */ + break; + } +} + +static void +set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec G_GNUC_UNUSED *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_URI: + web_view->uri = g_value_get_string(value); + break; + } +} + +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(web_view->tab->application, 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(web_view->tab->application, 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(web_view->tab->application, 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(web_view->tab->application, 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(WebKitWebView *wk_web_view, WebKitContextMenu *context_menu, + GdkEvent *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 PARENT_CLASS->context_menu(WEBKIT_WEB_VIEW(web_view), + context_menu, event, hit_test_result); +} + +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(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_event(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 GTK_WIDGET_CLASS(mq_web_view_parent_class)-> + button_press_event(widget, event); + } + + 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 GTK_WIDGET_CLASS(mq_web_view_parent_class)-> + button_press_event(widget, event); + } 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 +mq_web_view_class_init(MqWebViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + object_class->get_property = get_property; + object_class->set_property = set_property; + widget_class->button_press_event = button_press_event; + PARENT_CLASS->context_menu = context_menu; + PARENT_CLASS->mouse_target_changed = mouse_target_changed; + + obj_properties[PROP_TAB] = g_param_spec_pointer( + "tab", "MqTab", "Parent MqTab instance", + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB); + obj_properties[PROP_URI] = g_param_spec_string( + "uri", "URI", "URI to load", + "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | + 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) +{ + gchar *rw_uri; + MqConfig *config; + gchar *new_tab_page; + + 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 { + config = mq_application_get_config( + mq_tab_get_application(web_view->tab)); + new_tab_page = mq_config_get_string(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(config, "tabs.home")); + } else if (g_strcmp0(new_tab_page, "blank") == 0) { + /* Don't load any URI. */ + } else { + g_assert_not_reached(); + } + } + + webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(web_view), + mq_config_get_double( + mq_application_get_config( + mq_tab_get_application(web_view->tab)), + "zoom.default")); + + 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)); + + web_view->hit_test_result = 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? */ + "uri", uri, + NULL); +} + +GtkWidget * +mq_web_view_get_container(MqWebView *web_view) +{ + return GTK_WIDGET(web_view); +} + +WebKitWebView * +mq_web_view_get_web_view(MqWebView *web_view) +{ + return WEBKIT_WEB_VIEW(web_view); +} -- cgit v0.9.1