/* * 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 "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", "MqTabPage", "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", "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); obj_properties[PROP_DATA] = g_param_spec_pointer( "data", "Resource data", "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) { 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 = web_view->scheme_methods->rewrite_uri( web_view, &web_view->scheme, uri); webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), web_view->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)); }