Serenity Operating System
1/*
2 * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021, networkException <networkexception@serenityos.org>
4 * Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
5 * Copyright (c) 2022, the SerenityOS developers.
6 *
7 * SPDX-License-Identifier: BSD-2-Clause
8 */
9
10#include "BrowserWindow.h"
11#include "BookmarksBarWidget.h"
12#include "Browser.h"
13#include "ConsoleWidget.h"
14#include "CookieJar.h"
15#include "InspectorWidget.h"
16#include "Tab.h"
17#include <AK/LexicalPath.h>
18#include <Applications/Browser/BrowserWindowGML.h>
19#include <LibConfig/Client.h>
20#include <LibCore/DateTime.h>
21#include <LibCore/StandardPaths.h>
22#include <LibGUI/Application.h>
23#include <LibGUI/Clipboard.h>
24#include <LibGUI/Icon.h>
25#include <LibGUI/InputBox.h>
26#include <LibGUI/Menu.h>
27#include <LibGUI/Menubar.h>
28#include <LibGUI/MessageBox.h>
29#include <LibGUI/Process.h>
30#include <LibGUI/SeparatorWidget.h>
31#include <LibGUI/Statusbar.h>
32#include <LibGUI/TabWidget.h>
33#include <LibGUI/ToolbarContainer.h>
34#include <LibGUI/Widget.h>
35#include <LibGfx/PNGWriter.h>
36#include <LibJS/Interpreter.h>
37#include <LibWeb/CSS/PreferredColorScheme.h>
38#include <LibWeb/Dump.h>
39#include <LibWeb/Layout/Viewport.h>
40#include <LibWeb/Loader/ResourceLoader.h>
41#include <LibWebView/OutOfProcessWebView.h>
42#include <LibWebView/WebContentClient.h>
43
44namespace Browser {
45
46static DeprecatedString bookmarks_file_path()
47{
48 StringBuilder builder;
49 builder.append(Core::StandardPaths::config_directory());
50 builder.append("/bookmarks.json"sv);
51 return builder.to_deprecated_string();
52}
53
54static DeprecatedString search_engines_file_path()
55{
56 StringBuilder builder;
57 builder.append(Core::StandardPaths::config_directory());
58 builder.append("/SearchEngines.json"sv);
59 return builder.to_deprecated_string();
60}
61
62BrowserWindow::BrowserWindow(CookieJar& cookie_jar, URL url)
63 : m_cookie_jar(cookie_jar)
64 , m_window_actions(*this)
65{
66 auto app_icon = GUI::Icon::default_icon("app-browser"sv);
67 m_bookmarks_bar = Browser::BookmarksBarWidget::construct(Browser::bookmarks_file_path(), true);
68
69 resize(730, 560);
70 set_icon(app_icon.bitmap_for_size(16));
71 set_title("Browser");
72
73 auto widget = set_main_widget<GUI::Widget>().release_value_but_fixme_should_propagate_errors();
74 widget->load_from_gml(browser_window_gml).release_value_but_fixme_should_propagate_errors();
75
76 auto& top_line = *widget->find_descendant_of_type_named<GUI::HorizontalSeparator>("top_line");
77
78 m_tab_widget = *widget->find_descendant_of_type_named<GUI::TabWidget>("tab_widget");
79 m_tab_widget->on_tab_count_change = [&top_line](size_t tab_count) {
80 top_line.set_visible(tab_count > 1);
81 };
82
83 m_tab_widget->on_change = [this](auto& active_widget) {
84 auto& tab = static_cast<Browser::Tab&>(active_widget);
85 set_window_title_for_tab(tab);
86 tab.did_become_active();
87 };
88
89 m_tab_widget->on_middle_click = [](auto& clicked_widget) {
90 auto& tab = static_cast<Browser::Tab&>(clicked_widget);
91 tab.on_tab_close_request(tab);
92 };
93
94 m_tab_widget->on_tab_close_click = [](auto& clicked_widget) {
95 auto& tab = static_cast<Browser::Tab&>(clicked_widget);
96 tab.on_tab_close_request(tab);
97 };
98
99 m_tab_widget->on_context_menu_request = [](auto& clicked_widget, const GUI::ContextMenuEvent& context_menu_event) {
100 auto& tab = static_cast<Browser::Tab&>(clicked_widget);
101 tab.context_menu_requested(context_menu_event.screen_position());
102 };
103
104 m_window_actions.on_create_new_tab = [this] {
105 create_new_tab(Browser::url_from_user_input(Browser::g_new_tab_url), true);
106 };
107
108 m_window_actions.on_create_new_window = [this] {
109 create_new_window(g_home_url);
110 };
111
112 m_window_actions.on_next_tab = [this] {
113 m_tab_widget->activate_next_tab();
114 };
115
116 m_window_actions.on_previous_tab = [this] {
117 m_tab_widget->activate_previous_tab();
118 };
119
120 for (size_t i = 0; i <= 7; ++i) {
121 m_window_actions.on_tabs.append([this, i] {
122 if (i >= m_tab_widget->tab_count())
123 return;
124 m_tab_widget->set_tab_index(i);
125 });
126 }
127 m_window_actions.on_tabs.append([this] {
128 m_tab_widget->activate_last_tab();
129 });
130
131 m_window_actions.on_show_bookmarks_bar = [](auto& action) {
132 Browser::BookmarksBarWidget::the().set_visible(action.is_checked());
133 Config::write_bool("Browser"sv, "Preferences"sv, "ShowBookmarksBar"sv, action.is_checked());
134 };
135
136 bool show_bookmarks_bar = Config::read_bool("Browser"sv, "Preferences"sv, "ShowBookmarksBar"sv, true);
137 m_window_actions.show_bookmarks_bar_action().set_checked(show_bookmarks_bar);
138 Browser::BookmarksBarWidget::the().set_visible(show_bookmarks_bar);
139
140 m_window_actions.on_vertical_tabs = [this](auto& action) {
141 m_tab_widget->set_tab_position(action.is_checked() ? GUI::TabWidget::TabPosition::Left : GUI::TabWidget::TabPosition::Top);
142 Config::write_bool("Browser"sv, "Preferences"sv, "VerticalTabs"sv, action.is_checked());
143 };
144
145 bool vertical_tabs = Config::read_bool("Browser"sv, "Preferences"sv, "VerticalTabs"sv, false);
146 m_window_actions.vertical_tabs_action().set_checked(vertical_tabs);
147 m_tab_widget->set_tab_position(vertical_tabs ? GUI::TabWidget::TabPosition::Left : GUI::TabWidget::TabPosition::Top);
148
149 build_menus();
150
151 create_new_tab(move(url), true);
152}
153
154void BrowserWindow::build_menus()
155{
156 auto& file_menu = add_menu("&File");
157 file_menu.add_action(WindowActions::the().create_new_tab_action());
158 file_menu.add_action(WindowActions::the().create_new_window_action());
159
160 auto close_tab_action = GUI::CommonActions::make_close_tab_action([this](auto&) {
161 active_tab().on_tab_close_request(active_tab());
162 },
163 this);
164 file_menu.add_action(close_tab_action);
165
166 file_menu.add_separator();
167 file_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
168 GUI::Application::the()->quit();
169 }));
170
171 auto& view_menu = add_menu("&View");
172 view_menu.add_action(WindowActions::the().show_bookmarks_bar_action());
173 view_menu.add_action(WindowActions::the().vertical_tabs_action());
174 view_menu.add_separator();
175 view_menu.add_action(GUI::CommonActions::make_zoom_in_action(
176 [this](auto&) {
177 auto& tab = active_tab();
178 tab.view().zoom_in();
179 },
180 this));
181 view_menu.add_action(GUI::CommonActions::make_zoom_out_action(
182 [this](auto&) {
183 auto& tab = active_tab();
184 tab.view().zoom_out();
185 },
186 this));
187 view_menu.add_action(GUI::CommonActions::make_reset_zoom_action(
188 [this](auto&) {
189 auto& tab = active_tab();
190 tab.view().reset_zoom();
191 },
192 this));
193 view_menu.add_separator();
194 view_menu.add_action(GUI::CommonActions::make_fullscreen_action(
195 [this](auto&) {
196 auto& tab = active_tab();
197 set_fullscreen(!is_fullscreen());
198
199 auto is_fullscreen = this->is_fullscreen();
200 tab_widget().set_bar_visible(!is_fullscreen && tab_widget().children().size() > 1);
201 tab.m_toolbar_container->set_visible(!is_fullscreen);
202 tab.m_statusbar->set_visible(!is_fullscreen);
203
204 if (is_fullscreen) {
205 tab.view().set_frame_thickness(0);
206 } else {
207 tab.view().set_frame_thickness(2);
208 }
209 },
210 this));
211
212 m_go_back_action = GUI::CommonActions::make_go_back_action([this](auto&) { active_tab().go_back(); }, this);
213 m_go_forward_action = GUI::CommonActions::make_go_forward_action([this](auto&) { active_tab().go_forward(); }, this);
214 m_go_home_action = GUI::CommonActions::make_go_home_action([this](auto&) { active_tab().load(Browser::url_from_user_input(g_home_url)); }, this);
215 m_go_home_action->set_status_tip("Go to home page");
216 m_reload_action = GUI::CommonActions::make_reload_action([this](auto&) { active_tab().reload(); }, this);
217 m_reload_action->set_status_tip("Reload current page");
218
219 auto& go_menu = add_menu("&Go");
220 go_menu.add_action(*m_go_back_action);
221 go_menu.add_action(*m_go_forward_action);
222 go_menu.add_action(*m_go_home_action);
223 go_menu.add_separator();
224 go_menu.add_action(*m_reload_action);
225
226 m_copy_selection_action = GUI::CommonActions::make_copy_action([this](auto&) {
227 auto& tab = active_tab();
228 auto selected_text = tab.view().selected_text();
229 if (!selected_text.is_empty())
230 GUI::Clipboard::the().set_plain_text(selected_text);
231 });
232
233 m_select_all_action = GUI::CommonActions::make_select_all_action([this](auto&) {
234 active_tab().view().select_all();
235 });
236
237 m_view_source_action = GUI::Action::create(
238 "View &Source", { Mod_Ctrl, Key_U }, g_icon_bag.code, [this](auto&) {
239 active_tab().view().get_source();
240 },
241 this);
242 m_view_source_action->set_status_tip("View source code of the current page");
243
244 m_inspect_dom_tree_action = GUI::Action::create(
245 "Inspect &DOM Tree", { Mod_None, Key_F12 }, g_icon_bag.dom_tree, [this](auto&) {
246 active_tab().show_inspector_window(Tab::InspectorTarget::Document);
247 },
248 this);
249 m_inspect_dom_tree_action->set_status_tip("Open inspector window for this page");
250
251 m_inspect_dom_node_action = GUI::Action::create(
252 "&Inspect Element", g_icon_bag.inspect, [this](auto&) {
253 active_tab().show_inspector_window(Tab::InspectorTarget::HoveredElement);
254 },
255 this);
256 m_inspect_dom_node_action->set_status_tip("Open inspector for this element");
257
258 m_take_visible_screenshot_action = GUI::Action::create(
259 "Take &Visible Screenshot"sv, g_icon_bag.filetype_image, [this](auto&) {
260 if (auto result = take_screenshot(ScreenshotType::Visible); result.is_error())
261 GUI::MessageBox::show_error(this, DeprecatedString::formatted("{}", result.error()));
262 },
263 this);
264 m_take_visible_screenshot_action->set_status_tip("Save a screenshot of the visible portion of the current tab to the Downloads directory"sv);
265
266 m_take_full_screenshot_action = GUI::Action::create(
267 "Take &Full Screenshot"sv, g_icon_bag.filetype_image, [this](auto&) {
268 if (auto result = take_screenshot(ScreenshotType::Full); result.is_error())
269 GUI::MessageBox::show_error(this, DeprecatedString::formatted("{}", result.error()));
270 },
271 this);
272 m_take_full_screenshot_action->set_status_tip("Save a screenshot of the entirety of the current tab to the Downloads directory"sv);
273
274 auto& inspect_menu = add_menu("&Inspect");
275 inspect_menu.add_action(*m_view_source_action);
276 inspect_menu.add_action(*m_inspect_dom_tree_action);
277
278 auto js_console_action = GUI::Action::create(
279 "Open &JS Console", { Mod_Ctrl, Key_I }, g_icon_bag.filetype_javascript, [this](auto&) {
280 active_tab().show_console_window();
281 },
282 this);
283 js_console_action->set_status_tip("Open JavaScript console for this page");
284 inspect_menu.add_action(js_console_action);
285
286 auto storage_window_action = GUI::Action::create(
287 "Open S&torage Inspector", g_icon_bag.cookie, [this](auto&) {
288 active_tab().show_storage_inspector();
289 },
290 this);
291 storage_window_action->set_status_tip("Show Storage inspector for this page");
292 inspect_menu.add_action(storage_window_action);
293
294 auto history_window_action = GUI::Action::create(
295 "Open &History Window", g_icon_bag.history, [this](auto&) {
296 active_tab().show_history_inspector();
297 },
298 this);
299 storage_window_action->set_status_tip("Show History inspector for this tab");
300 inspect_menu.add_action(history_window_action);
301
302 auto& settings_menu = add_menu("&Settings");
303
304 m_change_homepage_action = GUI::Action::create(
305 "Set Homepage URL...", g_icon_bag.go_home, [this](auto&) {
306 auto homepage_url = Config::read_string("Browser"sv, "Preferences"sv, "Home"sv, "about:blank"sv);
307 if (GUI::InputBox::show(this, homepage_url, "Enter URL"sv, "Change homepage URL"sv) == GUI::InputBox::ExecResult::OK) {
308 if (URL(homepage_url).is_valid()) {
309 Config::write_string("Browser"sv, "Preferences"sv, "Home"sv, homepage_url);
310 Browser::g_home_url = homepage_url;
311 } else {
312 GUI::MessageBox::show_error(this, "The URL you have entered is not valid"sv);
313 }
314 }
315 },
316 this);
317
318 settings_menu.add_action(*m_change_homepage_action);
319
320 auto load_search_engines_result = load_search_engines(settings_menu);
321 if (load_search_engines_result.is_error()) {
322 dbgln("Failed to open search-engines file: {}", load_search_engines_result.error());
323 }
324
325 auto& color_scheme_menu = settings_menu.add_submenu("&Color Scheme");
326 color_scheme_menu.set_icon(g_icon_bag.color_chooser);
327 {
328 auto current_setting = Web::CSS::preferred_color_scheme_from_string(Config::read_string("Browser"sv, "Preferences"sv, "ColorScheme"sv, "auto"sv));
329 m_color_scheme_actions.set_exclusive(true);
330
331 auto add_color_scheme_action = [&](auto& name, Web::CSS::PreferredColorScheme preference_value) {
332 auto action = GUI::Action::create_checkable(
333 name, [=, this](auto&) {
334 Config::write_string("Browser"sv, "Preferences"sv, "ColorScheme"sv, Web::CSS::preferred_color_scheme_to_string(preference_value));
335 active_tab().view().set_preferred_color_scheme(preference_value);
336 },
337 this);
338 if (current_setting == preference_value)
339 action->set_checked(true);
340 color_scheme_menu.add_action(action);
341 m_color_scheme_actions.add_action(action);
342 };
343
344 add_color_scheme_action("Follow system theme", Web::CSS::PreferredColorScheme::Auto);
345 add_color_scheme_action("Light", Web::CSS::PreferredColorScheme::Light);
346 add_color_scheme_action("Dark", Web::CSS::PreferredColorScheme::Dark);
347 }
348
349 settings_menu.add_separator();
350 auto open_settings_action = GUI::Action::create("Browser &Settings", Gfx::Bitmap::load_from_file("/res/icons/16x16/settings.png"sv).release_value_but_fixme_should_propagate_errors(),
351 [this](auto&) {
352 GUI::Process::spawn_or_show_error(this, "/bin/BrowserSettings"sv);
353 });
354 settings_menu.add_action(move(open_settings_action));
355
356 auto& debug_menu = add_menu("&Debug");
357 debug_menu.add_action(GUI::Action::create(
358 "Dump &DOM Tree", g_icon_bag.dom_tree, [this](auto&) {
359 active_tab().view().debug_request("dump-dom-tree");
360 },
361 this));
362 debug_menu.add_action(GUI::Action::create(
363 "Dump &Layout Tree", g_icon_bag.layout, [this](auto&) {
364 active_tab().view().debug_request("dump-layout-tree");
365 },
366 this));
367 debug_menu.add_action(GUI::Action::create(
368 "Dump S&tacking Context Tree", g_icon_bag.layers, [this](auto&) {
369 active_tab().view().debug_request("dump-stacking-context-tree");
370 },
371 this));
372 debug_menu.add_action(GUI::Action::create(
373 "Dump &Style Sheets", g_icon_bag.filetype_css, [this](auto&) {
374 active_tab().view().debug_request("dump-style-sheets");
375 },
376 this));
377 debug_menu.add_action(GUI::Action::create("Dump &History", { Mod_Ctrl, Key_H }, g_icon_bag.history, [this](auto&) {
378 active_tab().m_history.dump();
379 }));
380 debug_menu.add_action(GUI::Action::create("Dump C&ookies", g_icon_bag.cookie, [this](auto&) {
381 auto& tab = active_tab();
382 if (tab.on_dump_cookies)
383 tab.on_dump_cookies();
384 }));
385 debug_menu.add_action(GUI::Action::create("Dump Loc&al Storage", g_icon_bag.local_storage, [this](auto&) {
386 active_tab().view().debug_request("dump-local-storage");
387 }));
388 debug_menu.add_separator();
389 auto line_box_borders_action = GUI::Action::create_checkable(
390 "Line &Box Borders", [this](auto& action) {
391 active_tab().view().debug_request("set-line-box-borders", action.is_checked() ? "on" : "off");
392 },
393 this);
394 line_box_borders_action->set_checked(false);
395 debug_menu.add_action(line_box_borders_action);
396
397 debug_menu.add_separator();
398 debug_menu.add_action(GUI::Action::create("Collect &Garbage", { Mod_Ctrl | Mod_Shift, Key_G }, g_icon_bag.trash_can, [this](auto&) {
399 active_tab().view().debug_request("collect-garbage");
400 }));
401 debug_menu.add_action(GUI::Action::create("Clear &Cache", { Mod_Ctrl | Mod_Shift, Key_C }, g_icon_bag.clear_cache, [this](auto&) {
402 active_tab().view().debug_request("clear-cache");
403 }));
404
405 m_user_agent_spoof_actions.set_exclusive(true);
406 auto& spoof_user_agent_menu = debug_menu.add_submenu("Spoof &User Agent");
407 m_disable_user_agent_spoofing = GUI::Action::create_checkable("Disabled", [this](auto&) {
408 active_tab().view().debug_request("spoof-user-agent", Web::default_user_agent);
409 });
410 m_disable_user_agent_spoofing->set_status_tip(Web::default_user_agent);
411 spoof_user_agent_menu.add_action(*m_disable_user_agent_spoofing);
412 spoof_user_agent_menu.set_icon(g_icon_bag.spoof);
413 m_user_agent_spoof_actions.add_action(*m_disable_user_agent_spoofing);
414 m_disable_user_agent_spoofing->set_checked(true);
415
416 auto add_user_agent = [this, &spoof_user_agent_menu](auto& name, auto& user_agent) {
417 auto action = GUI::Action::create_checkable(name, [this, user_agent](auto&) {
418 active_tab().view().debug_request("spoof-user-agent", user_agent);
419 });
420 action->set_status_tip(user_agent);
421 spoof_user_agent_menu.add_action(action);
422 m_user_agent_spoof_actions.add_action(action);
423 };
424 add_user_agent("Chrome Linux Desktop", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36");
425 add_user_agent("Firefox Linux Desktop", "Mozilla/5.0 (X11; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0");
426 add_user_agent("Safari macOS Desktop", "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15");
427 add_user_agent("Chrome Android Mobile", "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.66 Mobile Safari/537.36");
428 add_user_agent("Firefox Android Mobile", "Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/86.0");
429 add_user_agent("Safari iOS Mobile", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1");
430
431 auto custom_user_agent = GUI::Action::create_checkable("Custom...", [this](auto& action) {
432 DeprecatedString user_agent;
433 if (GUI::InputBox::show(this, user_agent, "Enter User Agent:"sv, "Custom User Agent"sv, GUI::InputType::NonemptyText) != GUI::InputBox::ExecResult::OK) {
434 m_disable_user_agent_spoofing->activate();
435 return;
436 }
437 active_tab().view().debug_request("spoof-user-agent", user_agent);
438 action.set_status_tip(user_agent);
439 });
440 spoof_user_agent_menu.add_action(custom_user_agent);
441 m_user_agent_spoof_actions.add_action(custom_user_agent);
442
443 debug_menu.add_separator();
444 auto scripting_enabled_action = GUI::Action::create_checkable(
445 "Enable Scripting", [this](auto& action) {
446 active_tab().view().debug_request("scripting", action.is_checked() ? "on" : "off");
447 },
448 this);
449 scripting_enabled_action->set_checked(true);
450 debug_menu.add_action(scripting_enabled_action);
451
452 auto block_pop_ups_action = GUI::Action::create_checkable(
453 "Block Pop-ups", [this](auto& action) {
454 active_tab().view().debug_request("block-pop-ups", action.is_checked() ? "on" : "off");
455 },
456 this);
457 block_pop_ups_action->set_checked(true);
458 debug_menu.add_action(block_pop_ups_action);
459
460 auto same_origin_policy_action = GUI::Action::create_checkable(
461 "Enable Same Origin &Policy", [this](auto& action) {
462 active_tab().view().debug_request("same-origin-policy", action.is_checked() ? "on" : "off");
463 },
464 this);
465 same_origin_policy_action->set_checked(false);
466 debug_menu.add_action(same_origin_policy_action);
467
468 auto& help_menu = add_menu("&Help");
469 help_menu.add_action(GUI::CommonActions::make_command_palette_action(this));
470 help_menu.add_action(WindowActions::the().about_action());
471}
472
473ErrorOr<void> BrowserWindow::load_search_engines(GUI::Menu& settings_menu)
474{
475 m_search_engine_actions.set_exclusive(true);
476 auto& search_engine_menu = settings_menu.add_submenu("&Search Engine");
477 search_engine_menu.set_icon(g_icon_bag.find);
478 bool search_engine_set = false;
479
480 m_disable_search_engine_action = GUI::Action::create_checkable(
481 "Disable", [](auto&) {
482 g_search_engine = {};
483 Config::write_string("Browser"sv, "Preferences"sv, "SearchEngine"sv, g_search_engine);
484 },
485 this);
486 search_engine_menu.add_action(*m_disable_search_engine_action);
487 m_search_engine_actions.add_action(*m_disable_search_engine_action);
488 m_disable_search_engine_action->set_checked(true);
489
490 auto search_engines_file = TRY(Core::File::open(Browser::search_engines_file_path(), Core::File::OpenMode::Read));
491 auto file_size = TRY(search_engines_file->size());
492 auto buffer = TRY(ByteBuffer::create_uninitialized(file_size));
493 if (!search_engines_file->read_until_filled(buffer).is_error()) {
494 StringView buffer_contents { buffer.bytes() };
495 if (auto json = TRY(JsonValue::from_string(buffer_contents)); json.is_array()) {
496 auto json_array = json.as_array();
497 for (auto& json_item : json_array.values()) {
498 if (!json_item.is_object())
499 continue;
500 auto search_engine = json_item.as_object();
501 auto name = search_engine.get_deprecated_string("title"sv).value();
502 auto url_format = search_engine.get_deprecated_string("url_format"sv).value();
503
504 auto action = GUI::Action::create_checkable(
505 name, [&, url_format](auto&) {
506 g_search_engine = url_format;
507 Config::write_string("Browser"sv, "Preferences"sv, "SearchEngine"sv, g_search_engine);
508 },
509 this);
510 search_engine_menu.add_action(action);
511 m_search_engine_actions.add_action(action);
512
513 if (g_search_engine == url_format) {
514 action->set_checked(true);
515 search_engine_set = true;
516 }
517 action->set_status_tip(url_format);
518 }
519 }
520 }
521
522 auto custom_search_engine_action = GUI::Action::create_checkable("Custom...", [&](auto& action) {
523 DeprecatedString search_engine;
524 if (GUI::InputBox::show(this, search_engine, "Enter URL template:"sv, "Custom Search Engine"sv, GUI::InputType::NonemptyText, "https://host/search?q={}"sv) != GUI::InputBox::ExecResult::OK) {
525 m_disable_search_engine_action->activate();
526 return;
527 }
528
529 auto argument_count = search_engine.count("{}"sv);
530 if (argument_count != 1) {
531 GUI::MessageBox::show(this, "Invalid format, must contain '{}' once!"sv, "Error"sv, GUI::MessageBox::Type::Error);
532 m_disable_search_engine_action->activate();
533 return;
534 }
535
536 g_search_engine = search_engine;
537 Config::write_string("Browser"sv, "Preferences"sv, "SearchEngine"sv, g_search_engine);
538 action.set_status_tip(search_engine);
539 });
540 search_engine_menu.add_action(custom_search_engine_action);
541 m_search_engine_actions.add_action(custom_search_engine_action);
542
543 if (!search_engine_set && !g_search_engine.is_empty()) {
544 custom_search_engine_action->set_checked(true);
545 custom_search_engine_action->set_status_tip(g_search_engine);
546 }
547
548 return {};
549}
550
551GUI::TabWidget& BrowserWindow::tab_widget()
552{
553 return *m_tab_widget;
554}
555
556Tab& BrowserWindow::active_tab()
557{
558 return verify_cast<Tab>(*tab_widget().active_widget());
559}
560
561void BrowserWindow::set_window_title_for_tab(Tab const& tab)
562{
563 auto& title = tab.title();
564 auto url = tab.url();
565 set_title(DeprecatedString::formatted("{} - Browser", title.is_empty() ? url.to_deprecated_string() : title));
566}
567
568void BrowserWindow::create_new_tab(URL url, bool activate)
569{
570 auto& new_tab = m_tab_widget->add_tab<Browser::Tab>("New tab", *this);
571
572 m_tab_widget->set_bar_visible(!is_fullscreen() && m_tab_widget->children().size() > 1);
573
574 new_tab.on_title_change = [this, &new_tab](auto& title) {
575 m_tab_widget->set_tab_title(new_tab, title);
576 if (m_tab_widget->active_widget() == &new_tab)
577 set_window_title_for_tab(new_tab);
578 };
579
580 new_tab.on_favicon_change = [this, &new_tab](auto& bitmap) {
581 m_tab_widget->set_tab_icon(new_tab, &bitmap);
582 };
583
584 new_tab.on_tab_open_request = [this](auto& url) {
585 create_new_tab(url, true);
586 };
587
588 new_tab.on_tab_close_request = [this](auto& tab) {
589 m_tab_widget->deferred_invoke([this, &tab] {
590 m_tab_widget->remove_tab(tab);
591 m_tab_widget->set_bar_visible(!is_fullscreen() && m_tab_widget->children().size() > 1);
592 if (m_tab_widget->children().is_empty())
593 close();
594 });
595 };
596
597 new_tab.on_tab_close_other_request = [this](auto& tab) {
598 m_tab_widget->deferred_invoke([this, &tab] {
599 m_tab_widget->remove_all_tabs_except(tab);
600 VERIFY(m_tab_widget->children().size() == 1);
601 m_tab_widget->set_bar_visible(false);
602 });
603 };
604
605 new_tab.on_window_open_request = [this](auto& url) {
606 create_new_window(url);
607 };
608
609 new_tab.on_get_all_cookies = [this](auto& url) {
610 return m_cookie_jar.get_all_cookies(url);
611 };
612
613 new_tab.on_get_named_cookie = [this](auto& url, auto& name) {
614 return m_cookie_jar.get_named_cookie(url, name);
615 };
616
617 new_tab.on_get_cookie = [this](auto& url, auto source) -> DeprecatedString {
618 return m_cookie_jar.get_cookie(url, source);
619 };
620
621 new_tab.on_set_cookie = [this](auto& url, auto& cookie, auto source) {
622 m_cookie_jar.set_cookie(url, cookie, source);
623 };
624
625 new_tab.on_dump_cookies = [this]() {
626 m_cookie_jar.dump_cookies();
627 };
628
629 new_tab.on_update_cookie = [this](auto cookie) {
630 m_cookie_jar.update_cookie(move(cookie));
631 };
632
633 new_tab.on_get_cookies_entries = [this]() {
634 return m_cookie_jar.get_all_cookies();
635 };
636
637 new_tab.on_get_local_storage_entries = [this]() {
638 return active_tab().view().get_local_storage_entries();
639 };
640
641 new_tab.on_get_session_storage_entries = [this]() {
642 return active_tab().view().get_session_storage_entries();
643 };
644
645 new_tab.on_take_screenshot = [this]() {
646 return active_tab().view().take_screenshot();
647 };
648
649 new_tab.load(url);
650
651 dbgln_if(SPAM_DEBUG, "Added new tab {:p}, loading {}", &new_tab, url);
652
653 if (activate)
654 m_tab_widget->set_active_widget(&new_tab);
655}
656
657void BrowserWindow::create_new_window(URL url)
658{
659 GUI::Process::spawn_or_show_error(this, "/bin/Browser"sv, Array { url.to_deprecated_string() });
660}
661
662void BrowserWindow::content_filters_changed()
663{
664 tab_widget().for_each_child_of_type<Browser::Tab>([](auto& tab) {
665 tab.content_filters_changed();
666 return IterationDecision::Continue;
667 });
668}
669
670void BrowserWindow::proxy_mappings_changed()
671{
672 tab_widget().for_each_child_of_type<Browser::Tab>([](auto& tab) {
673 tab.proxy_mappings_changed();
674 return IterationDecision::Continue;
675 });
676}
677
678void BrowserWindow::config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value)
679{
680 if (domain != "Browser")
681 return;
682
683 if (group == "Preferences") {
684 if (key == "SearchEngine")
685 Browser::g_search_engine = value;
686 else if (key == "Home")
687 Browser::g_home_url = value;
688 else if (key == "NewTab")
689 Browser::g_new_tab_url = value;
690 } else if (group.starts_with("Proxy:"sv)) {
691 dbgln("Proxy mapping changed: {}/{} = {}", group, key, value);
692 auto proxy_spec = group.substring_view(6);
693 auto existing_proxy = Browser::g_proxies.find(proxy_spec);
694 if (existing_proxy.is_end())
695 Browser::g_proxies.append(proxy_spec);
696
697 Browser::g_proxy_mappings.set(key, existing_proxy.index());
698 proxy_mappings_changed();
699 }
700
701 // TODO: ColorScheme
702}
703
704void BrowserWindow::config_bool_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, bool value)
705{
706 dbgln("{} {} {} {}", domain, group, key, value);
707 if (domain != "Browser" || group != "Preferences")
708 return;
709
710 if (key == "ShowBookmarksBar") {
711 m_window_actions.show_bookmarks_bar_action().set_checked(value);
712 Browser::BookmarksBarWidget::the().set_visible(value);
713 } else if (key == "EnableContentFilters") {
714 Browser::g_content_filters_enabled = value;
715 content_filters_changed();
716 }
717
718 // NOTE: CloseDownloadWidgetOnFinish is read each time in DownloadWindow
719}
720
721void BrowserWindow::broadcast_window_position(Gfx::IntPoint position)
722{
723 tab_widget().for_each_child_of_type<Browser::Tab>([&](auto& tab) {
724 tab.window_position_changed(position);
725 return IterationDecision::Continue;
726 });
727}
728
729void BrowserWindow::broadcast_window_size(Gfx::IntSize size)
730{
731 tab_widget().for_each_child_of_type<Browser::Tab>([&](auto& tab) {
732 tab.window_size_changed(size);
733 return IterationDecision::Continue;
734 });
735}
736
737void BrowserWindow::event(Core::Event& event)
738{
739 switch (event.type()) {
740 case GUI::Event::Move:
741 broadcast_window_position(static_cast<GUI::MoveEvent&>(event).position());
742 break;
743 case GUI::Event::Resize:
744 broadcast_window_size(static_cast<GUI::ResizeEvent&>(event).size());
745 break;
746 default:
747 break;
748 }
749
750 Window::event(event);
751}
752
753ErrorOr<void> BrowserWindow::take_screenshot(ScreenshotType type)
754{
755 if (!active_tab().on_take_screenshot)
756 return {};
757
758 Gfx::ShareableBitmap bitmap;
759
760 switch (type) {
761 case ScreenshotType::Visible:
762 bitmap = active_tab().on_take_screenshot();
763 break;
764 case ScreenshotType::Full:
765 bitmap = active_tab().view().take_document_screenshot();
766 break;
767 }
768
769 if (!bitmap.is_valid())
770 return Error::from_string_view("Failed to take a screenshot of the current tab"sv);
771
772 LexicalPath path { Core::StandardPaths::downloads_directory() };
773 path = path.append(Core::DateTime::now().to_deprecated_string("screenshot-%Y-%m-%d-%H-%M-%S.png"sv));
774
775 auto encoded = TRY(Gfx::PNGWriter::encode(*bitmap.bitmap()));
776
777 auto screenshot_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
778 TRY(screenshot_file->write_until_depleted(encoded));
779
780 return {};
781}
782
783}