/* * 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.h" #include "notebook.h" #include "tab-page.h" #include "web-view-schemes/schemes.h" struct _MqWebView { WebKitWebView parent_instance; MqTabPage *tab_page; gchar *uri; MqConfig *config; WebKitHitTestResult *hit_test_result; WebKitHitTestResult *mouse_target_hit_test_result; GtkComboBox *save_type_combo_box; GFile *save_file; }; enum { PROP_TAB_PAGE = 1, PROP_DISPLAY_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 WKCMA(ACTION) \ WEBKIT_CONTEXT_MENU_ACTION_##ACTION static void menu_open_link_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_web_view_load_uri(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_notebook_insert_child( MQ_NOTEBOOK(gtk_widget_get_parent( GTK_WIDGET(web_view->tab_page))), webkit_hit_test_result_get_link_uri(web_view->hit_test_result), web_view->tab_page, !mq_config_get_boolean(web_view->config, "tabs.background")); } 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( mq_tab_page_get_application(web_view->tab_page), uris); } static void menu_open_image_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_web_view_load_uri(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_notebook_insert_child( MQ_NOTEBOOK(gtk_widget_get_parent( GTK_WIDGET(web_view->tab_page))), webkit_hit_test_result_get_image_uri(web_view->hit_test_result), web_view->tab_page, !mq_config_get_boolean(web_view->config, "tabs.background")); } 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( mq_tab_page_get_application(web_view->tab_page), uris); } static void menu_open_video_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_web_view_load_uri(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_notebook_insert_child( MQ_NOTEBOOK(gtk_widget_get_parent( GTK_WIDGET(web_view->tab_page))), webkit_hit_test_result_get_media_uri(web_view->hit_test_result), web_view->tab_page, !mq_config_get_boolean(web_view->config, "tabs.background")); } 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( mq_tab_page_get_application(web_view->tab_page), uris); } static void menu_open_audio_activate_cb(GtkAction G_GNUC_UNUSED *action, MqWebView *web_view) { mq_web_view_load_uri(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_notebook_insert_child( MQ_NOTEBOOK(gtk_widget_get_parent( GTK_WIDGET(web_view->tab_page))), webkit_hit_test_result_get_media_uri(web_view->hit_test_result), web_view->tab_page, !mq_config_get_boolean(web_view->config, "tabs.background")); } 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( mq_tab_page_get_application(web_view->tab_page), uris); } #define ITEM_DECLS \ GtkAction *action; \ WebKitContextMenuItem *menu_item; #define ITEM_DECLS_NO_CUSTOM \ WebKitContextMenuItem *menu_item; #define NEW_CUSTOM_ITEM(NAME, LABEL) \ G_STMT_START { \ /* 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); \ } G_STMT_END #define NEW_STOCK_ITEM(STOCK) \ G_STMT_START { \ menu_item = webkit_context_menu_item_new_from_stock_action( \ WEBKIT_CONTEXT_MENU_ACTION_##STOCK); \ webkit_context_menu_append(context_menu, menu_item); \ } G_STMT_END #define NEW_SEPARATOR_ITEM() \ G_STMT_START { \ webkit_context_menu_append(context_menu, \ webkit_context_menu_item_new_separator()); \ } G_STMT_END #define RESTORE_ITEMS(ITEMS) \ G_STMT_START { \ for (; ITEMS; ITEMS = ITEMS->next) { \ webkit_context_menu_append(context_menu, ITEMS->data); \ g_object_unref(ITEMS->data); \ } \ } G_STMT_END 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) \ G_STMT_START { \ g_object_ref(items->data); \ ITEMS = g_list_prepend(ITEMS, items->data); \ } G_STMT_END static gboolean context_menu_cb(MqWebView *web_view, WebKitContextMenu *context_menu, GdkEvent G_GNUC_UNUSED *event, WebKitHitTestResult *hit_test_result) { 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 (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 FALSE; } static void uri_cb(WebKitWebView *wk_web_view, GParamSpec G_GNUC_UNUSED *param_spec) { const gchar *uri; MqWebView *web_view; web_view = MQ_WEB_VIEW(wk_web_view); if (web_view->uri) { g_free(web_view->uri); } uri = webkit_web_view_get_uri(wk_web_view); if (g_str_has_prefix(uri, "mq-about:")) { web_view->uri = g_strconcat("about:", uri + strlen("mq-about:"), NULL); } else { web_view->uri = g_strdup(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 gchar * rewrite_uri(const gchar *uri) { if (g_str_has_prefix(uri, "about:")) { return g_strconcat("mq-about:", uri + strlen("about:"), NULL); } else { return g_strdup(uri); } } static void constructed(GObject *object) { MqWebView *web_view; gchar *rw_uri; 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)); webkit_web_view_set_settings(WEBKIT_WEB_VIEW(web_view), mq_application_get_webkit_settings( mq_tab_page_get_application(web_view->tab_page))); if (web_view->uri) { rw_uri = rewrite_uri(web_view->uri); webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), rw_uri); g_free(rw_uri); } else { new_tab_page = mq_config_get_string(web_view->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(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); /* FIXME: This doesn't seem to be working. */ gtk_widget_grab_focus(GTK_WIDGET(web_view)); } static void finalize(GObject *object) { MqWebView *web_view; web_view = MQ_WEB_VIEW(object); if (web_view->uri) { g_free(web_view->uri); } 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; 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); g_object_class_install_properties(object_class, N_PROPERTIES, obj_properties); } static void mq_web_view_init(MqWebView *web_view) { web_view->hit_test_result = 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); } 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; if (!uri) { /* Happens during object construction. */ return; } if (web_view->uri) { g_free(web_view->uri); } web_view->uri = g_strdup(uri); if (g_str_has_prefix(uri, "about:")) { rw_uri = rewrite_uri(web_view->uri); 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), uri); } g_object_notify_by_pspec(G_OBJECT(web_view), obj_properties[PROP_DISPLAY_URI]); } 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); } static void save_html_replace_cb(GFile *file, GAsyncResult *result, guchar *data) { g_file_replace_contents_finish(file, result, NULL, NULL); /* TODO: Error handling? */ g_free(data); } static void save_html_get_data_cb(WebKitWebResource *resource, GAsyncResult *result, MqWebView *web_view) { guchar *data; gsize length; data = webkit_web_resource_get_data_finish(resource, result, &length, NULL); /* TODO: Error handling? */ g_file_replace_contents_async(web_view->save_file, (gchar *) data, length, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, (GAsyncReadyCallback) save_html_replace_cb, data); } static void save_mhtml_cb(WebKitWebView *web_view, GAsyncResult *result) { webkit_web_view_save_to_file_finish(web_view, result, NULL); } static gchar * get_extension(gchar *filename) { gchar *extension; extension = strrchr(filename, '.'); if (extension) { return extension; } else { return filename + strlen(filename); } } static gboolean extension_is_html(const gchar *extension) { return extension[0] == '.' && extension[1] == 'h' && extension[2] == 't' && extension[3] == 'm' && (extension[4] == '\0' || (extension[4] == 'l' && extension[5] == '\0')); } static gboolean extension_is_mhtml(const gchar *extension) { return extension[0] == '.' && extension[1] == 'm' && extension[2] == 'h' && extension[3] == 't' && (extension[4] == '\0' || (extension[4] == 'm' && (extension[5] == '\0' || (extension[5] == 'l' && extension[6] == '\0')))); } static void save_html(MqWebView *web_view, GFile *file) { web_view->save_file = file; webkit_web_resource_get_data( webkit_web_view_get_main_resource(WEBKIT_WEB_VIEW(web_view)), NULL, (GAsyncReadyCallback) save_html_get_data_cb, web_view); } static void save_mhtml(MqWebView *web_view, GFile *file) { webkit_web_view_save_to_file(WEBKIT_WEB_VIEW(web_view), file, WEBKIT_SAVE_MODE_MHTML, NULL, (GAsyncReadyCallback) save_mhtml_cb, NULL); } static gchar * get_clean_title(MqWebView *web_view) { const gchar *title; title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(web_view)); if (!title || title[0] == '\0') { title = web_view->uri; } if (!title || title[0] == '\0') { title = "page"; } return g_strdelimit(g_strdup(title), #ifdef G_OS_WIN32 "/\\:*\"?<>|", #else "/", #endif '_'); } static void save_type_changed_cb(GtkComboBox *combo_box, GtkFileChooser *chooser) { gchar *name; gchar *extension; const gchar *active_id; gchar *new_name; name = gtk_file_chooser_get_current_name(chooser); extension = get_extension(name); active_id = gtk_combo_box_get_active_id(combo_box); if (g_strcmp0(active_id, "html") == 0) { if (!extension_is_html(extension)) { /* Extension is not "htm" or "html". */ extension[0] = '\0'; /* Remove extension. */ new_name = g_strconcat(name, ".html", NULL); gtk_file_chooser_set_current_name(chooser, new_name); g_free(new_name); } } else if (g_strcmp0(active_id, "mhtml") == 0) { if (!extension_is_mhtml(extension)) { /* Extension is not "mht", "mhtm", or "mhtml". */ extension[0] = '\0'; /* Remove extension. */ new_name = g_strconcat(name, ".mhtml", NULL); gtk_file_chooser_set_current_name(chooser, new_name); g_free(new_name); } } g_free(name); } static void save_response_cb(GtkWidget *dialog, gint response_id, MqWebView *web_view) { gchar *dir; gchar *filename; GFile *file; gchar *extension; const gchar *active_id; 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.downloads", dir); g_free(dir); mq_config_save(web_view->config); } filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog)); file = g_file_new_for_path(filename); active_id = gtk_combo_box_get_active_id( web_view->save_type_combo_box); if (g_strcmp0(active_id, "detect") == 0) { extension = get_extension(filename); if (extension_is_html(extension)) { save_html(web_view, file); } else { save_mhtml(web_view, file); } } else if (g_strcmp0(active_id, "html") == 0) { save_html(web_view, file); } else if (g_strcmp0(active_id, "mhtml") == 0) { save_mhtml(web_view, file); } g_free(filename); } gtk_widget_destroy(dialog); } void mq_web_view_save(MqWebView *web_view) { GtkWidget *dialog; GtkFileChooser *chooser; gchar *dir; gchar *title; gchar *filename; GtkWidget *type_combo_box; GtkWidget *type_box; dialog = gtk_file_chooser_dialog_new("Save File", GTK_WINDOW(mq_tab_page_get_window(web_view->tab_page)), GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE); dir = mq_config_get_string(web_view->config, "directories.downloads"); gtk_file_chooser_set_current_folder(chooser, dir); g_free(dir); title = get_clean_title(web_view); filename = g_strconcat(title, ".mhtml", NULL); g_free(title); gtk_file_chooser_set_current_name(chooser, filename); g_free(filename); mq_web_view_add_html_mhtml_file_chooser_filters(chooser); type_combo_box = gtk_combo_box_text_new(); gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(type_combo_box), "detect", "By extension"); gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(type_combo_box), "html", "HTML document only"); gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(type_combo_box), "mhtml", "MHTML archive with associated resources"); gtk_combo_box_set_active(GTK_COMBO_BOX(type_combo_box), 0); g_signal_connect(type_combo_box, "changed", G_CALLBACK(save_type_changed_cb), chooser); web_view->save_type_combo_box = GTK_COMBO_BOX(type_combo_box); type_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start(GTK_BOX(type_box), gtk_label_new("Save as file type:"), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(type_box), type_combo_box, FALSE, FALSE, 0); gtk_file_chooser_set_extra_widget(chooser, type_box); g_signal_connect(dialog, "response", G_CALLBACK(save_response_cb), web_view); gtk_widget_show_all(dialog); }