29ecae9ebfeb54b982e05f28292fcf6108cbe42c
[WebKit-https.git] / Tools / MiniBrowser / gtk / BrowserTab.c
1 /*
2  * Copyright (C) 2016 Igalia S.L.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE)
27 #include "cmakeconfig.h"
28 #endif
29 #include "BrowserTab.h"
30
31 #include "BrowserSearchBar.h"
32 #include "BrowserWindow.h"
33 #include <string.h>
34
35 enum {
36     PROP_0,
37
38     PROP_VIEW
39 };
40
41 struct _BrowserTab {
42     GtkBox parent;
43
44     WebKitWebView *webView;
45     BrowserSearchBar *searchBar;
46     GtkWidget *statusLabel;
47     gboolean wasSearchingWhenEnteredFullscreen;
48     gboolean inspectorIsVisible;
49     GtkWidget *fullScreenMessageLabel;
50     guint fullScreenMessageLabelId;
51
52     /* Tab Title */
53     GtkWidget *titleBox;
54     GtkWidget *titleLabel;
55     GtkWidget *titleSpinner;
56     GtkWidget *titleCloseButton;
57 };
58
59 static GHashTable *userMediaPermissionGrantedOrigins;
60 struct _BrowserTabClass {
61     GtkBoxClass parent;
62 };
63
64 G_DEFINE_TYPE(BrowserTab, browser_tab, GTK_TYPE_BOX)
65
66 typedef struct {
67     WebKitPermissionRequest *request;
68     gchar *origin;
69 } PermissionRequestData;
70
71 static PermissionRequestData *permissionRequestDataNew(WebKitPermissionRequest *request, gchar *origin)
72 {
73     PermissionRequestData *data = g_malloc0(sizeof(PermissionRequestData));
74
75     data->request = g_object_ref(request);
76     data->origin = origin;
77
78     return data;
79 }
80
81 static void permissionRequestDataFree(PermissionRequestData *data)
82 {
83     g_clear_object(&data->request);
84     g_clear_pointer(&data->origin, g_free);
85     g_free(data);
86 }
87
88 static gchar *getWebViewOrigin(WebKitWebView *webView)
89 {
90     WebKitSecurityOrigin *origin = webkit_security_origin_new_for_uri(webkit_web_view_get_uri(webView));
91     gchar *originStr = webkit_security_origin_to_string(origin);
92     webkit_security_origin_unref(origin);
93
94     return originStr;
95 }
96
97 static void titleChanged(WebKitWebView *webView, GParamSpec *pspec, BrowserTab *tab)
98 {
99     const char *title = webkit_web_view_get_title(webView);
100     if (title && *title)
101         gtk_label_set_text(GTK_LABEL(tab->titleLabel), title);
102 }
103
104 static void isLoadingChanged(WebKitWebView *webView, GParamSpec *paramSpec, BrowserTab *tab)
105 {
106     if (webkit_web_view_is_loading(webView)) {
107         gtk_spinner_start(GTK_SPINNER(tab->titleSpinner));
108         gtk_widget_show(tab->titleSpinner);
109     } else {
110         gtk_spinner_stop(GTK_SPINNER(tab->titleSpinner));
111         gtk_widget_hide(tab->titleSpinner);
112     }
113 }
114
115 static gboolean decidePolicy(WebKitWebView *webView, WebKitPolicyDecision *decision, WebKitPolicyDecisionType decisionType, BrowserTab *tab)
116 {
117     if (decisionType != WEBKIT_POLICY_DECISION_TYPE_RESPONSE)
118         return FALSE;
119
120     WebKitResponsePolicyDecision *responseDecision = WEBKIT_RESPONSE_POLICY_DECISION(decision);
121     if (webkit_response_policy_decision_is_mime_type_supported(responseDecision))
122         return FALSE;
123
124     WebKitWebResource *mainResource = webkit_web_view_get_main_resource(webView);
125     WebKitURIRequest *request = webkit_response_policy_decision_get_request(responseDecision);
126     const char *requestURI = webkit_uri_request_get_uri(request);
127     if (g_strcmp0(webkit_web_resource_get_uri(mainResource), requestURI))
128         return FALSE;
129
130     webkit_policy_decision_download(decision);
131     return TRUE;
132 }
133
134 static void removeChildIfInfoBar(GtkWidget *child, GtkContainer *tab)
135 {
136     if (GTK_IS_INFO_BAR(child))
137         gtk_container_remove(tab, child);
138 }
139
140 static void loadChanged(WebKitWebView *webView, WebKitLoadEvent loadEvent, BrowserTab *tab)
141 {
142     if (loadEvent != WEBKIT_LOAD_STARTED)
143         return;
144
145     gtk_container_foreach(GTK_CONTAINER(tab), (GtkCallback)removeChildIfInfoBar, tab);
146 }
147
148 static GtkWidget *createInfoBarQuestionMessage(const char *title, const char *text)
149 {
150     GtkWidget *dialog = gtk_info_bar_new_with_buttons("No", GTK_RESPONSE_NO, "Yes", GTK_RESPONSE_YES, NULL);
151     gtk_info_bar_set_message_type(GTK_INFO_BAR(dialog), GTK_MESSAGE_QUESTION);
152
153     GtkWidget *contentBox = gtk_info_bar_get_content_area(GTK_INFO_BAR(dialog));
154     gtk_orientable_set_orientation(GTK_ORIENTABLE(contentBox), GTK_ORIENTATION_VERTICAL);
155     gtk_box_set_spacing(GTK_BOX(contentBox), 0);
156
157     GtkWidget *label = gtk_label_new(NULL);
158     gchar *markup = g_strdup_printf("<span size='xx-large' weight='bold'>%s</span>", title);
159     gtk_label_set_markup(GTK_LABEL(label), markup);
160     g_free(markup);
161     gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
162     gtk_label_set_selectable(GTK_LABEL(label), TRUE);
163     gtk_misc_set_alignment(GTK_MISC(label), 0., 0.5);
164     gtk_box_pack_start(GTK_BOX(contentBox), label, FALSE, FALSE, 2);
165     gtk_widget_show(label);
166
167     label = gtk_label_new(text);
168     gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
169     gtk_label_set_selectable(GTK_LABEL(label), TRUE);
170     gtk_misc_set_alignment(GTK_MISC(label), 0., 0.5);
171     gtk_box_pack_start(GTK_BOX(contentBox), label, FALSE, FALSE, 0);
172     gtk_widget_show(label);
173
174     return dialog;
175 }
176
177 static void tlsErrorsDialogResponse(GtkWidget *dialog, gint response, BrowserTab *tab)
178 {
179     if (response == GTK_RESPONSE_YES) {
180         const char *failingURI = (const char *)g_object_get_data(G_OBJECT(dialog), "failingURI");
181         GTlsCertificate *certificate = (GTlsCertificate *)g_object_get_data(G_OBJECT(dialog), "certificate");
182         SoupURI *uri = soup_uri_new(failingURI);
183         webkit_web_context_allow_tls_certificate_for_host(webkit_web_view_get_context(tab->webView), certificate, uri->host);
184         soup_uri_free(uri);
185         webkit_web_view_load_uri(tab->webView, failingURI);
186     }
187     gtk_widget_destroy(dialog);
188 }
189
190 static gboolean loadFailedWithTLSerrors(WebKitWebView *webView, const char *failingURI, GTlsCertificate *certificate, GTlsCertificateFlags errors, BrowserTab *tab)
191 {
192     gchar *text = g_strdup_printf("Failed to load %s: Do you want to continue ignoring the TLS errors?", failingURI);
193     GtkWidget *dialog = createInfoBarQuestionMessage("Invalid TLS Certificate", text);
194     g_free(text);
195     g_object_set_data_full(G_OBJECT(dialog), "failingURI", g_strdup(failingURI), g_free);
196     g_object_set_data_full(G_OBJECT(dialog), "certificate", g_object_ref(certificate), g_object_unref);
197
198     g_signal_connect(dialog, "response", G_CALLBACK(tlsErrorsDialogResponse), tab);
199
200     gtk_box_pack_start(GTK_BOX(tab), dialog, FALSE, FALSE, 0);
201     gtk_box_reorder_child(GTK_BOX(tab), dialog, 0);
202     gtk_widget_show(dialog);
203
204     return TRUE;
205 }
206
207 static void permissionRequestDialogResponse(GtkWidget *dialog, gint response, PermissionRequestData *requestData)
208 {
209     switch (response) {
210     case GTK_RESPONSE_YES:
211         if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(requestData->request))
212             g_hash_table_add(userMediaPermissionGrantedOrigins, g_strdup(requestData->origin));
213
214         webkit_permission_request_allow(requestData->request);
215         break;
216     default:
217         if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(requestData->request))
218             g_hash_table_remove(userMediaPermissionGrantedOrigins, requestData->origin);
219
220         webkit_permission_request_deny(requestData->request);
221         break;
222     }
223
224     gtk_widget_destroy(dialog);
225     g_clear_pointer(&requestData, permissionRequestDataFree);
226 }
227
228 static gboolean decidePermissionRequest(WebKitWebView *webView, WebKitPermissionRequest *request, BrowserTab *tab)
229 {
230     const gchar *title = NULL;
231     gchar *text = NULL;
232
233     if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST(request)) {
234         title = "Geolocation request";
235         text = g_strdup("Allow geolocation request?");
236     } else if (WEBKIT_IS_NOTIFICATION_PERMISSION_REQUEST(request)) {
237         title = "Notification request";
238         text = g_strdup("Allow notifications request?");
239     } else if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(request)) {
240         title = "UserMedia request";
241         gboolean is_for_audio_device = webkit_user_media_permission_is_for_audio_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request));
242         gboolean is_for_video_device = webkit_user_media_permission_is_for_video_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request));
243         const char *mediaType = NULL;
244         if (is_for_audio_device) {
245             if (is_for_video_device)
246                 mediaType = "audio/video";
247             else
248                 mediaType = "audio";
249         } else if (is_for_video_device)
250             mediaType = "video";
251         text = g_strdup_printf("Allow access to %s device?", mediaType);
252     } else if (WEBKIT_IS_INSTALL_MISSING_MEDIA_PLUGINS_PERMISSION_REQUEST(request)) {
253         title = "Media plugin missing request";
254         text = g_strdup_printf("The media backend was unable to find a plugin to play the requested media:\n%s.\nAllow to search and install the missing plugin?",
255             webkit_install_missing_media_plugins_permission_request_get_description(WEBKIT_INSTALL_MISSING_MEDIA_PLUGINS_PERMISSION_REQUEST(request)));
256     } else if (WEBKIT_IS_DEVICE_INFO_PERMISSION_REQUEST(request)) {
257         char* origin = getWebViewOrigin(webView);
258         if (g_hash_table_contains(userMediaPermissionGrantedOrigins, origin)) {
259             webkit_permission_request_allow(request);
260             g_free(origin);
261             return TRUE;
262         }
263         g_free(origin);
264         return FALSE;
265     } else {
266         g_print("%s request not handled\n", G_OBJECT_TYPE_NAME(request));
267         return FALSE;
268     }
269
270     GtkWidget *dialog = createInfoBarQuestionMessage(title, text);
271     g_free(text);
272     g_signal_connect(dialog, "response", G_CALLBACK(permissionRequestDialogResponse), permissionRequestDataNew(request,
273         getWebViewOrigin(webView)));
274
275     gtk_box_pack_start(GTK_BOX(tab), dialog, FALSE, FALSE, 0);
276     gtk_box_reorder_child(GTK_BOX(tab), dialog, 0);
277     gtk_widget_show(dialog);
278
279     return TRUE;
280 }
281
282 #if GTK_CHECK_VERSION(3, 12, 0)
283 static void colorChooserRGBAChanged(GtkColorChooser *colorChooser, GParamSpec *paramSpec, WebKitColorChooserRequest *request)
284 {
285     GdkRGBA rgba;
286     gtk_color_chooser_get_rgba(colorChooser, &rgba);
287     webkit_color_chooser_request_set_rgba(request, &rgba);
288 }
289
290 static void popoverColorClosed(GtkWidget *popover, WebKitColorChooserRequest *request)
291 {
292     webkit_color_chooser_request_finish(request);
293 }
294
295 static void colorChooserRequestFinished(WebKitColorChooserRequest *request, GtkWidget *popover)
296 {
297     g_object_unref(request);
298     gtk_widget_destroy(popover);
299 }
300
301 static gboolean runColorChooserCallback(WebKitWebView *webView, WebKitColorChooserRequest *request, BrowserTab *tab)
302 {
303     GtkWidget *popover = gtk_popover_new(GTK_WIDGET(webView));
304
305     GdkRectangle rectangle;
306     webkit_color_chooser_request_get_element_rectangle(request, &rectangle);
307     gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rectangle);
308
309     GtkWidget *colorChooser = gtk_color_chooser_widget_new();
310     GdkRGBA rgba;
311     webkit_color_chooser_request_get_rgba(request, &rgba);
312     gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(colorChooser), &rgba);
313     g_signal_connect(colorChooser, "notify::rgba", G_CALLBACK(colorChooserRGBAChanged), request);
314     gtk_container_add(GTK_CONTAINER(popover), colorChooser);
315     gtk_widget_show(colorChooser);
316
317     g_object_ref(request);
318     g_signal_connect_object(popover, "hide", G_CALLBACK(popoverColorClosed), request, 0);
319     g_signal_connect_object(request, "finished", G_CALLBACK(colorChooserRequestFinished), popover, 0);
320
321     gtk_widget_show(popover);
322
323     return TRUE;
324 }
325 #endif /* GTK_CHECK_VERSION(3, 12, 0) */
326
327 static gboolean inspectorOpenedInWindow(WebKitWebInspector *inspector, BrowserTab *tab)
328 {
329     tab->inspectorIsVisible = TRUE;
330     return FALSE;
331 }
332
333 static gboolean inspectorClosed(WebKitWebInspector *inspector, BrowserTab *tab)
334 {
335     tab->inspectorIsVisible = FALSE;
336     return FALSE;
337 }
338
339 static void browserTabSetProperty(GObject *object, guint propId, const GValue *value, GParamSpec *pspec)
340 {
341     BrowserTab *tab = BROWSER_TAB(object);
342
343     switch (propId) {
344     case PROP_VIEW:
345         tab->webView = g_value_get_object(value);
346         break;
347     default:
348         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec);
349     }
350 }
351
352 static void browserTabFinalize(GObject *gObject)
353 {
354     BrowserTab *tab = BROWSER_TAB(gObject);
355
356     if (tab->fullScreenMessageLabelId)
357         g_source_remove(tab->fullScreenMessageLabelId);
358
359     G_OBJECT_CLASS(browser_tab_parent_class)->finalize(gObject);
360 }
361
362 static void browser_tab_init(BrowserTab *tab)
363 {
364     gtk_orientable_set_orientation(GTK_ORIENTABLE(tab), GTK_ORIENTATION_VERTICAL);
365 }
366
367 static void browserTabConstructed(GObject *gObject)
368 {
369     BrowserTab *tab = BROWSER_TAB(gObject);
370
371     G_OBJECT_CLASS(browser_tab_parent_class)->constructed(gObject);
372
373     tab->searchBar = BROWSER_SEARCH_BAR(browser_search_bar_new(tab->webView));
374     gtk_box_pack_start(GTK_BOX(tab), GTK_WIDGET(tab->searchBar), FALSE, FALSE, 0);
375
376     GtkWidget *overlay = gtk_overlay_new();
377     gtk_box_pack_start(GTK_BOX(tab), overlay, TRUE, TRUE, 0);
378     gtk_widget_show(overlay);
379
380     tab->statusLabel = gtk_label_new(NULL);
381     gtk_widget_set_halign(tab->statusLabel, GTK_ALIGN_START);
382     gtk_widget_set_valign(tab->statusLabel, GTK_ALIGN_END);
383     gtk_widget_set_margin_left(tab->statusLabel, 1);
384     gtk_widget_set_margin_right(tab->statusLabel, 1);
385     gtk_widget_set_margin_top(tab->statusLabel, 1);
386     gtk_widget_set_margin_bottom(tab->statusLabel, 1);
387     gtk_overlay_add_overlay(GTK_OVERLAY(overlay), tab->statusLabel);
388
389     tab->fullScreenMessageLabel = gtk_label_new(NULL);
390     gtk_widget_set_halign(tab->fullScreenMessageLabel, GTK_ALIGN_CENTER);
391     gtk_widget_set_valign(tab->fullScreenMessageLabel, GTK_ALIGN_CENTER);
392     gtk_widget_set_no_show_all(tab->fullScreenMessageLabel, TRUE);
393     gtk_overlay_add_overlay(GTK_OVERLAY(overlay), tab->fullScreenMessageLabel);
394
395     gtk_container_add(GTK_CONTAINER(overlay), GTK_WIDGET(tab->webView));
396     gtk_widget_show(GTK_WIDGET(tab->webView));
397
398     tab->titleBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
399
400     GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
401     gtk_widget_set_halign(hbox, GTK_ALIGN_CENTER);
402
403     tab->titleSpinner = gtk_spinner_new();
404     gtk_box_pack_start(GTK_BOX(hbox), tab->titleSpinner, FALSE, FALSE, 0);
405
406     tab->titleLabel = gtk_label_new(NULL);
407     gtk_label_set_ellipsize(GTK_LABEL(tab->titleLabel), PANGO_ELLIPSIZE_END);
408     gtk_label_set_single_line_mode(GTK_LABEL(tab->titleLabel), TRUE);
409     gtk_misc_set_padding(GTK_MISC(tab->titleLabel), 0, 0);
410     gtk_box_pack_start(GTK_BOX(hbox), tab->titleLabel, FALSE, FALSE, 0);
411     gtk_widget_show(tab->titleLabel);
412
413     gtk_box_pack_start(GTK_BOX(tab->titleBox), hbox, TRUE, TRUE, 0);
414     gtk_widget_show(hbox);
415
416     tab->titleCloseButton = gtk_button_new();
417     g_signal_connect_swapped(tab->titleCloseButton, "clicked", G_CALLBACK(gtk_widget_destroy), tab);
418     gtk_button_set_relief(GTK_BUTTON(tab->titleCloseButton), GTK_RELIEF_NONE);
419     gtk_button_set_focus_on_click(GTK_BUTTON(tab->titleCloseButton), FALSE);
420
421     GtkWidget *image = gtk_image_new_from_icon_name("window-close-symbolic", GTK_ICON_SIZE_MENU);
422     gtk_container_add(GTK_CONTAINER(tab->titleCloseButton), image);
423     gtk_widget_show(image);
424
425     gtk_box_pack_start(GTK_BOX(tab->titleBox), tab->titleCloseButton, FALSE, FALSE, 0);
426     gtk_widget_show(tab->titleCloseButton);
427
428     g_signal_connect(tab->webView, "notify::title", G_CALLBACK(titleChanged), tab);
429     g_signal_connect(tab->webView, "notify::is-loading", G_CALLBACK(isLoadingChanged), tab);
430     g_signal_connect(tab->webView, "decide-policy", G_CALLBACK(decidePolicy), tab);
431     g_signal_connect(tab->webView, "load-changed", G_CALLBACK(loadChanged), tab);
432     g_signal_connect(tab->webView, "load-failed-with-tls-errors", G_CALLBACK(loadFailedWithTLSerrors), tab);
433     g_signal_connect(tab->webView, "permission-request", G_CALLBACK(decidePermissionRequest), tab);
434 #if GTK_CHECK_VERSION(3, 12, 0)
435     g_signal_connect(tab->webView, "run-color-chooser", G_CALLBACK(runColorChooserCallback), tab);
436 #endif
437
438     WebKitWebInspector *inspector = webkit_web_view_get_inspector(tab->webView);
439     g_signal_connect(inspector, "open-window", G_CALLBACK(inspectorOpenedInWindow), tab);
440     g_signal_connect(inspector, "closed", G_CALLBACK(inspectorClosed), tab);
441
442     if (webkit_web_view_is_editable(tab->webView))
443         webkit_web_view_load_html(tab->webView, "<html></html>", "file:///");
444 }
445
446 static void browser_tab_class_init(BrowserTabClass *klass)
447 {
448     GObjectClass *gobjectClass = G_OBJECT_CLASS(klass);
449     gobjectClass->constructed = browserTabConstructed;
450     gobjectClass->set_property = browserTabSetProperty;
451     gobjectClass->finalize = browserTabFinalize;
452
453     if (!userMediaPermissionGrantedOrigins)
454         userMediaPermissionGrantedOrigins = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
455
456     g_object_class_install_property(
457         gobjectClass,
458         PROP_VIEW,
459         g_param_spec_object(
460             "view",
461             "View",
462             "The web view of this tab",
463             WEBKIT_TYPE_WEB_VIEW,
464             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
465 }
466
467 static char *getInternalURI(const char *uri)
468 {
469     /* Internally we use minibrowser-about: as about: prefix is ignored by WebKit. */
470     if (g_str_has_prefix(uri, "about:") && !g_str_equal(uri, "about:blank"))
471         return g_strconcat(BROWSER_ABOUT_SCHEME, uri + strlen ("about"), NULL);
472
473     return g_strdup(uri);
474 }
475
476 /* Public API. */
477 GtkWidget *browser_tab_new(WebKitWebView *view)
478 {
479     g_return_val_if_fail(WEBKIT_IS_WEB_VIEW(view), NULL);
480
481     return GTK_WIDGET(g_object_new(BROWSER_TYPE_TAB, "view", view, NULL));
482 }
483
484 WebKitWebView *browser_tab_get_web_view(BrowserTab *tab)
485 {
486     g_return_val_if_fail(BROWSER_IS_TAB(tab), NULL);
487
488     return tab->webView;
489 }
490
491 void browser_tab_load_uri(BrowserTab *tab, const char *uri)
492 {
493     g_return_if_fail(BROWSER_IS_TAB(tab));
494     g_return_if_fail(uri);
495
496     if (!g_str_has_prefix(uri, "javascript:")) {
497         char *internalURI = getInternalURI(uri);
498         webkit_web_view_load_uri(tab->webView, internalURI);
499         g_free(internalURI);
500         return;
501     }
502
503     webkit_web_view_run_javascript(tab->webView, strstr(uri, "javascript:"), NULL, NULL, NULL);
504 }
505
506 GtkWidget *browser_tab_get_title_widget(BrowserTab *tab)
507 {
508     g_return_val_if_fail(BROWSER_IS_TAB(tab), NULL);
509
510     return tab->titleBox;
511 }
512
513 void browser_tab_set_status_text(BrowserTab *tab, const char *text)
514 {
515     g_return_if_fail(BROWSER_IS_TAB(tab));
516
517     gtk_label_set_text(GTK_LABEL(tab->statusLabel), text);
518     gtk_widget_set_visible(tab->statusLabel, !!text);
519 }
520
521 void browser_tab_toggle_inspector(BrowserTab *tab)
522 {
523     g_return_if_fail(BROWSER_IS_TAB(tab));
524
525     WebKitWebInspector *inspector = webkit_web_view_get_inspector(tab->webView);
526     if (!tab->inspectorIsVisible) {
527         webkit_web_inspector_show(inspector);
528         tab->inspectorIsVisible = TRUE;
529     } else
530         webkit_web_inspector_close(inspector);
531 }
532
533 void browser_tab_start_search(BrowserTab *tab)
534 {
535     g_return_if_fail(BROWSER_IS_TAB(tab));
536
537     if (!gtk_widget_get_visible(GTK_WIDGET(tab->searchBar)))
538         browser_search_bar_open(tab->searchBar);
539 }
540
541 void browser_tab_stop_search(BrowserTab *tab)
542 {
543     g_return_if_fail(BROWSER_IS_TAB(tab));
544
545     if (gtk_widget_get_visible(GTK_WIDGET(tab->searchBar)))
546         browser_search_bar_close(tab->searchBar);
547 }
548
549 void browser_tab_add_accelerators(BrowserTab *tab, GtkAccelGroup *accelGroup)
550 {
551     g_return_if_fail(BROWSER_IS_TAB(tab));
552     g_return_if_fail(GTK_IS_ACCEL_GROUP(accelGroup));
553
554     browser_search_bar_add_accelerators(tab->searchBar, accelGroup);
555 }
556
557 static gboolean fullScreenMessageTimeoutCallback(BrowserTab *tab)
558 {
559     gtk_widget_hide(tab->fullScreenMessageLabel);
560     tab->fullScreenMessageLabelId = 0;
561     return FALSE;
562 }
563
564 void browser_tab_enter_fullscreen(BrowserTab *tab)
565 {
566     g_return_if_fail(BROWSER_IS_TAB(tab));
567
568     const gchar *titleOrURI = webkit_web_view_get_title(tab->webView);
569     if (!titleOrURI || !titleOrURI[0])
570         titleOrURI = webkit_web_view_get_uri(tab->webView);
571
572     gchar *message = g_strdup_printf("%s is now full screen. Press ESC or f to exit.", titleOrURI);
573     gtk_label_set_text(GTK_LABEL(tab->fullScreenMessageLabel), message);
574     g_free(message);
575
576     gtk_widget_show(tab->fullScreenMessageLabel);
577
578     tab->fullScreenMessageLabelId = g_timeout_add_seconds(2, (GSourceFunc)fullScreenMessageTimeoutCallback, tab);
579     g_source_set_name_by_id(tab->fullScreenMessageLabelId, "[WebKit] fullScreenMessageTimeoutCallback");
580
581     tab->wasSearchingWhenEnteredFullscreen = gtk_widget_get_visible(GTK_WIDGET(tab->searchBar));
582     browser_tab_stop_search(tab);
583 }
584
585 void browser_tab_leave_fullscreen(BrowserTab *tab)
586 {
587     g_return_if_fail(BROWSER_IS_TAB(tab));
588
589     if (tab->fullScreenMessageLabelId) {
590         g_source_remove(tab->fullScreenMessageLabelId);
591         tab->fullScreenMessageLabelId = 0;
592     }
593
594     gtk_widget_hide(tab->fullScreenMessageLabel);
595
596     if (tab->wasSearchingWhenEnteredFullscreen) {
597         /* Opening the search bar steals the focus. Usually, we want
598          * this but not when coming back from fullscreen.
599          */
600         GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(tab)));
601         GtkWidget *focusWidget = gtk_window_get_focus(window);
602         browser_tab_start_search(tab);
603         gtk_window_set_focus(window, focusWidget);
604     }
605 }
606
607 void browser_tab_set_background_color(BrowserTab *tab, GdkRGBA *rgba)
608 {
609     g_return_if_fail(BROWSER_IS_TAB(tab));
610     g_return_if_fail(rgba);
611
612     GdkRGBA viewRGBA;
613     webkit_web_view_get_background_color(tab->webView, &viewRGBA);
614     if (gdk_rgba_equal(rgba, &viewRGBA))
615         return;
616
617     webkit_web_view_set_background_color(tab->webView, rgba);
618 }