Serenity Operating System
at master 783 lines 32 kB view raw
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}