/*
* 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 "web-view.h"
#include
#include
#include
#include
#include
#include "config/config.h"
#include "i18n.h"
#include "notebook.h"
#include "tab-page.h"
#include "web-view-schemes/schemes.h"
struct _MqWebView {
WebKitWebView parent_instance;
MqTabPage *tab_page;
MqWebViewScheme scheme;
MqWebViewSchemeMethods *scheme_methods;
gchar *uri;
MqConfig *config;
WebKitHitTestResult *mouse_target_hit_test_result;
guchar *data;
gsize data_length;
};
enum {
PROP_TAB_PAGE = 1,
PROP_DISPLAY_URI,
PROP_DATA,
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)
static void
load_changed_cb(MqWebView *web_view, WebKitLoadEvent G_GNUC_UNUSED load_event)
{
g_free(web_view->data);
web_view->data = NULL;
}
static gboolean
context_menu_cb(MqWebView *web_view, WebKitContextMenu *context_menu,
GdkEvent *event, WebKitHitTestResult *hit_test_result)
{
return web_view->scheme_methods->context_menu(web_view,
&web_view->scheme, context_menu, event, hit_test_result);
}
static void
uri_cb(MqWebView *web_view, GParamSpec G_GNUC_UNUSED *param_spec)
{
const gchar *uri;
if (web_view->uri) {
g_free(web_view->uri);
}
uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(web_view));
mq_web_view_scheme_set_methods(web_view, &web_view->scheme,
&web_view->scheme_methods, uri);
web_view->uri = web_view->scheme_methods->display_uri(
web_view, &web_view->scheme, uri);
g_object_notify_by_pspec(G_OBJECT(web_view),
obj_properties[PROP_DISPLAY_URI]);
}
static void
clipboard_text_recv_cb(GtkClipboard G_GNUC_UNUSED *clipboard,
const gchar *text, MqWebView *web_view)
{
mq_web_view_load_uri(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_notebook_insert_child(
MQ_NOTEBOOK(gtk_widget_get_parent(
GTK_WIDGET(web_view->tab_page))),
webkit_hit_test_result_get_link_uri(hit_test_result),
web_view->tab_page,
!mq_config_get_boolean(web_view->config,
"tabs.background"));
} 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 gboolean
decide_response_policy(MqWebView G_GNUC_UNUSED *web_view,
WebKitResponsePolicyDecision *decision)
{
if (webkit_response_policy_decision_is_mime_type_supported(decision)) {
webkit_policy_decision_use(WEBKIT_POLICY_DECISION(decision));
} else {
webkit_policy_decision_download(
WEBKIT_POLICY_DECISION(decision));
}
return TRUE;
}
static gboolean
decide_policy_cb(MqWebView *web_view, WebKitPolicyDecision *decision,
WebKitPolicyDecisionType decision_type)
{
switch (decision_type) {
case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
/* TODO */
return FALSE;
case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
/* TODO */
return FALSE;
case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
return decide_response_policy(web_view,
WEBKIT_RESPONSE_POLICY_DECISION(decision));
default:
return FALSE;
}
}
static void
constructed(GObject *object)
{
MqWebView *web_view;
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_page_get_application(web_view->tab_page));
if (!web_view->uri) {
new_tab_page = mq_config_get_string(web_view->config,
"tabs.new");
if (g_strcmp0(new_tab_page, "home") == 0) {
mq_web_view_load_uri(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);
}
static void
finalize(GObject *object)
{
MqWebView *web_view;
web_view = MQ_WEB_VIEW(object);
g_free(web_view->uri);
g_free(web_view->data);
G_OBJECT_CLASS(mq_web_view_parent_class)->finalize(object);
}
static void
get_property(GObject *object, guint property_id, GValue *value,
GParamSpec *param_spec)
{
MqWebView *web_view;
web_view = MQ_WEB_VIEW(object);
switch (property_id) {
case PROP_TAB_PAGE:
g_value_set_object(value, web_view->tab_page);
break;
case PROP_DISPLAY_URI:
g_value_set_string(value, web_view->uri);
break;
case PROP_DATA:
g_value_set_pointer(value,
mq_web_view_get_data(web_view, NULL));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id,
param_spec);
break;
}
}
static void
set_property(GObject *object, guint property_id, const GValue *value,
GParamSpec *param_spec)
{
MqWebView *web_view;
web_view = MQ_WEB_VIEW(object);
switch (property_id) {
case PROP_TAB_PAGE:
web_view->tab_page = g_value_get_object(value);
break;
case PROP_DISPLAY_URI:
mq_web_view_load_uri(web_view,
g_value_get_string(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id,
param_spec);
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_PAGE] = g_param_spec_object(
"tab-page",
P_("MqTabPage"),
P_("The parent MqTabPage instance"),
MQ_TYPE_TAB_PAGE,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB);
obj_properties[PROP_DISPLAY_URI] = g_param_spec_string(
"display-uri",
P_("URI"),
P_("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);
obj_properties[PROP_DATA] = g_param_spec_pointer(
"data",
P_("Resource data"),
P_("The main resource's data"),
G_PARAM_READABLE |
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)
{
g_signal_connect(web_view, "load-changed",
G_CALLBACK(load_changed_cb), 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);
g_signal_connect(web_view, "decide-policy",
G_CALLBACK(decide_policy_cb), NULL);
}
MqWebView *
mq_web_view_new(MqTabPage *tab_page, const gchar *uri)
{
return g_object_new(MQ_TYPE_WEB_VIEW,
"tab-page", tab_page, /* TODO: Use gtk_widget_get_parent()? */
"display-uri", uri,
"web-context", webkit_web_context_get_default(),
NULL);
}
MqWebViewScheme *
mq_web_view_get_scheme(MqWebView *web_view)
{
return &web_view->scheme;
}
MqConfig *
mq_web_view_get_config(MqWebView *web_view)
{
return web_view->config;
}
MqTabPage *
mq_web_view_get_tab_page(MqWebView *web_view)
{
return web_view->tab_page;
}
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;
mq_web_view_scheme_set_methods(web_view, &web_view->scheme,
&web_view->scheme_methods, uri);
if (!uri) {
/* Happens during object construction. */
return;
}
if (web_view->uri) {
g_free(web_view->uri);
}
web_view->uri = g_strdup(uri);
rw_uri = web_view->scheme_methods->rewrite_uri(
web_view, &web_view->scheme, uri);
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), rw_uri);
g_free(rw_uri);
g_object_notify_by_pspec(G_OBJECT(web_view),
obj_properties[PROP_DISPLAY_URI]);
}
static void
get_data_cb(WebKitWebResource *resource, GAsyncResult *result,
MqWebView *web_view)
{
web_view->data = webkit_web_resource_get_data_finish(resource, result,
&web_view->data_length, NULL); /* TODO: Error handling? */
g_object_notify_by_pspec(G_OBJECT(web_view),
obj_properties[PROP_DATA]);
}
guchar *
mq_web_view_get_data(MqWebView *web_view, gsize *length)
{
if (!web_view->data) {
webkit_web_resource_get_data(webkit_web_view_get_main_resource(
WEBKIT_WEB_VIEW(web_view)),
NULL, (GAsyncReadyCallback) get_data_cb, web_view);
}
if (length) {
*length = web_view->data_length;
}
return web_view->data;
}
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);
}
void
mq_web_view_add_html_mhtml_file_chooser_filters(GtkFileChooser *chooser)
{
GtkFileFilter *filter;
filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, _("All files"));
gtk_file_filter_add_pattern(filter, "*");
gtk_file_chooser_add_filter(chooser, filter);
filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, _("All Web pages"));
gtk_file_filter_add_pattern(filter, "*.htm");
gtk_file_filter_add_pattern(filter, "*.html");
gtk_file_filter_add_pattern(filter, "*.mht");
gtk_file_filter_add_pattern(filter, "*.mhtm");
gtk_file_filter_add_pattern(filter, "*.mhtml");
gtk_file_chooser_add_filter(chooser, filter);
filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, _("HTML documents (*.htm, *.html)"));
gtk_file_filter_add_pattern(filter, "*.htm");
gtk_file_filter_add_pattern(filter, "*.html");
gtk_file_chooser_add_filter(chooser, filter);
filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter,
_("MHTML documents (*.mht, *.mhtm, *.mhtml)"));
gtk_file_filter_add_pattern(filter, "*.mht");
gtk_file_filter_add_pattern(filter, "*.mhtm");
gtk_file_filter_add_pattern(filter, "*.mhtml");
gtk_file_chooser_add_filter(chooser, filter);
}
static void
open_response_cb(GtkWidget *dialog, gint response_id, MqWebView *web_view)
{
gchar *dir;
gchar *filename;
gchar *uri;
if (response_id == GTK_RESPONSE_ACCEPT) {
dir = gtk_file_chooser_get_current_folder(
GTK_FILE_CHOOSER(dialog));
if (dir) {
mq_config_set_string(web_view->config,
"directories.open-file", dir);
g_free(dir);
mq_config_save(web_view->config);
}
filename = gtk_file_chooser_get_filename(
GTK_FILE_CHOOSER(dialog));
uri = g_strconcat("file://", filename, NULL);
g_free(filename);
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), uri);
g_free(uri);
}
gtk_widget_destroy(dialog);
}
void
mq_web_view_open(MqWebView *web_view)
{
GtkWidget *dialog;
GtkFileChooser *chooser;
gchar *dir;
dialog = gtk_file_chooser_dialog_new(_("Open File"),
GTK_WINDOW(mq_tab_page_get_window(web_view->tab_page)),
GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
chooser = GTK_FILE_CHOOSER(dialog);
dir = mq_config_get_string(web_view->config, "directories.open-file");
if (dir && dir[0]) {
gtk_file_chooser_set_current_folder(chooser, dir);
}
/* TODO: Consider setting chooser's folder from current file-scheme URI,
* if any. */
g_free(dir);
mq_web_view_add_html_mhtml_file_chooser_filters(chooser);
g_signal_connect(dialog, "response",
G_CALLBACK(open_response_cb), web_view);
gtk_widget_show_all(dialog);
}
void
mq_web_view_save(MqWebView *web_view)
{
web_view->scheme_methods->save_file(web_view, &web_view->scheme);
}
void
mq_web_view_grab_focus(MqWebView *web_view)
{
gtk_widget_grab_focus(GTK_WIDGET(web_view));
}