/*
* 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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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 G_GNUC_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. */ \
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), 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 *spell_repl_items, GList *spell_ctrl_items, GList *edit_items,
GList *input_items, MqTabBody G_GNUC_UNUSED *body)
{
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,
MqTabBody G_GNUC_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 G_GNUC_UNUSED *web_view,
WebKitContextMenu *context_menu, GdkEvent G_GNUC_UNUSED *event,
WebKitHitTestResult *hit_test_result, MqTabBody *body)
{
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;
/* 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 (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, spell_repl_items,
spell_ctrl_items, 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 G_GNUC_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 G_GNUC_UNUSED *web_view,
WebKitHitTestResult *hit_test_result, guint G_GNUC_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 G_GNUC_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;
MqConfig *config;
gchar *new_tab_page;
body = malloc(sizeof(*body));
body->tab = tab;
body->web_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_settings(
mq_application_get_webkit_settings(
mq_tab_get_application(tab))));
if (uri) {
if (g_str_has_prefix(uri, "about:")) {
rw_uri = g_strconcat("mq-about:",
uri + strlen("about:"), NULL);
webkit_web_view_load_uri(body->web_view, rw_uri);
g_free(rw_uri);
} else {
webkit_web_view_load_uri(body->web_view, uri);
}
} else {
config = mq_application_get_config(mq_tab_get_application(tab));
new_tab_page = mq_config_get_string(config, "tabs.new");
if (g_strcmp0(new_tab_page, "home") == 0) {
webkit_web_view_load_uri(body->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(body->web_view, mq_config_get_double(
mq_application_get_config(mq_tab_get_application(tab)),
"zoom.default"));
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;
}