/* * Tab body * * 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 "tab-body.h" #include "tab.h" #define WKCMA(ACTION) \ WEBKIT_CONTEXT_MENU_ACTION_##ACTION static void menu_open_link_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { webkit_web_view_load_uri(body->web_view, webkit_hit_test_result_get_link_uri(body->hit_test_result)); } static void menu_open_link_tab_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { mq_tab_new_relative( webkit_hit_test_result_get_link_uri(body->hit_test_result), body->tab); } static void menu_open_link_win_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { const gchar *uris[2] = { webkit_hit_test_result_get_link_uri(body->hit_test_result), NULL }; mq_application_add_window(body->tab->application, uris); } static void menu_open_image_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { webkit_web_view_load_uri(body->web_view, webkit_hit_test_result_get_image_uri(body->hit_test_result)); } static void menu_open_image_tab_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { mq_tab_new_relative( webkit_hit_test_result_get_image_uri(body->hit_test_result), body->tab); } static void menu_open_image_win_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { const gchar *uris[2] = { webkit_hit_test_result_get_image_uri(body->hit_test_result), NULL }; mq_application_add_window(body->tab->application, uris); } static void menu_open_video_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { webkit_web_view_load_uri(body->web_view, webkit_hit_test_result_get_media_uri(body->hit_test_result)); } static void menu_open_video_tab_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { mq_tab_new_relative( webkit_hit_test_result_get_media_uri(body->hit_test_result), body->tab); } static void menu_open_video_win_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { const gchar *uris[2] = { webkit_hit_test_result_get_media_uri(body->hit_test_result), NULL }; mq_application_add_window(body->tab->application, uris); } static void menu_open_audio_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { webkit_web_view_load_uri(body->web_view, webkit_hit_test_result_get_media_uri(body->hit_test_result)); } static void menu_open_audio_tab_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { mq_tab_new_relative( webkit_hit_test_result_get_media_uri(body->hit_test_result), body->tab); } static void menu_open_audio_win_activate_cb(GtkAction __attribute__((unused)) *action, MqTabBody *body) { const gchar *uris[2] = { webkit_hit_test_result_get_media_uri(body->hit_test_result), NULL }; mq_application_add_window(body->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. */ \ _Pragma("GCC diagnostic push") \ _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")\ action = gtk_action_new(#NAME, (LABEL), NULL, NULL); \ _Pragma("GCC diagnostic pop") \ g_signal_connect(action, "activate", \ G_CALLBACK(menu_##NAME##_activate_cb), body); \ 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, MqTabBody *body) { 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, MqTabBody *body) { 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, MqTabBody *body) { 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 *edit_items, GList *input_items, MqTabBody __attribute__((unused)) *body) { ITEM_DECLS_NO_CUSTOM 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, MqTabBody __attribute__((unused)) *body) { 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 __attribute__((unused)) *web_view, WebKitContextMenu *context_menu, GdkEvent __attribute__((unused)) *event, WebKitHitTestResult *hit_test_result, MqTabBody *body) { GList *items; GList *nav_items; GList *edit_items; GList *input_items; GList *spell_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; /* 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_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): case WKCMA(IGNORE_SPELLING): case WKCMA(LEARN_SPELLING): case WKCMA(IGNORE_GRAMMAR): PRESERVE_ITEM(spell_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_items = g_list_reverse(spell_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 (body->hit_test_result) { g_object_unref(body->hit_test_result); } body->hit_test_result = hit_test_result; g_object_ref(body->hit_test_result); context_handled = FALSE; /* Build the context menu. */ if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) { context_menu_link_cb(context_menu, body); context_handled = TRUE; } if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE) { if (context_handled) { NEW_SEPARATOR_ITEM(); } context_menu_image_cb(context_menu, body); 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, body); context_handled = TRUE; } if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE) { context_menu_editable_cb(context_menu, edit_items, input_items, body); 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, body); 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 clipboard_text_recv_cb(GtkClipboard __attribute__((unused)) *clipboard, const gchar *text, MqTabBody *body) { webkit_web_view_load_uri(body->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 __attribute__((unused)) *web_view, WebKitHitTestResult *hit_test_result, guint __attribute__((unused)) modifiers, MqTabBody *body) { body->mouse_target_hit_test_result = hit_test_result; g_object_ref(body->mouse_target_hit_test_result); } static gboolean button_press_cb(WebKitWebView __attribute__((unused)) *web_view, GdkEvent *event, MqTabBody *body) { WebKitHitTestResult *hit_test_result; GtkClipboard *clipboard; /* Make sure this is a middle mouse button press event. */ if (event->type != GDK_BUTTON_PRESS || event->button.button != 2) { return FALSE; } hit_test_result = body->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), body->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, body); } g_object_unref(hit_test_result); return TRUE; } MqTabBody * mq_tab_body_new(MqTab *tab, const gchar *uri) { MqTabBody *body; gchar *rw_uri; body = malloc(sizeof(*body)); body->tab = tab; body->web_view = WEBKIT_WEB_VIEW(webkit_web_view_new()); if (uri && g_str_has_prefix(uri, "about:")) { rw_uri = g_strconcat("mq-about:", uri + strlen("about:"), NULL); } else { rw_uri = g_strdup(uri); } if (rw_uri) { webkit_web_view_load_uri(body->web_view, rw_uri); g_free(rw_uri); } body->container = GTK_WIDGET(body->web_view); gtk_widget_set_vexpand(body->container, TRUE); /* FIXME: This doesn't seem to be working. */ gtk_widget_grab_focus(body->container); body->hit_test_result = NULL; g_signal_connect(body->web_view, "context-menu", G_CALLBACK(context_menu_cb), body); g_signal_connect(body->web_view, "mouse-target-changed", G_CALLBACK(mouse_target_changed_cb), body); g_signal_connect(body->web_view, "button-press-event", G_CALLBACK(button_press_cb), body); return body; } GtkWidget * mq_tab_body_get_container(MqTabBody *body) { return body->container; } WebKitWebView * mq_tab_body_get_web_view(MqTabBody *body) { return body->web_view; }