From 8825abe7ed397be21047bdcd7e241e7aac863272 Mon Sep 17 00:00:00 2001 From: thankyouverycool <66646555+thankyouverycool@users.noreply.github.com> Date: Mon, 28 Feb 2022 11:05:35 -0500 Subject: [PATCH] Help: Convert to GML and propagate more errors Converts Help's layout to GML, propagates model and icon creation errors, and switches to the MainWidget-namespace organization pattern seen in more up-to-date apps like TextEditor to make things easier to maintain going forward. --- Userland/Applications/Help/CMakeLists.txt | 4 + Userland/Applications/Help/HelpWindow.gml | 50 ++++ Userland/Applications/Help/MainWidget.cpp | 324 +++++++++++++++++++++ Userland/Applications/Help/MainWidget.h | 52 ++++ Userland/Applications/Help/ManualModel.h | 4 +- Userland/Applications/Help/main.cpp | 325 +--------------------- 6 files changed, 439 insertions(+), 320 deletions(-) create mode 100644 Userland/Applications/Help/HelpWindow.gml create mode 100644 Userland/Applications/Help/MainWidget.cpp create mode 100644 Userland/Applications/Help/MainWidget.h diff --git a/Userland/Applications/Help/CMakeLists.txt b/Userland/Applications/Help/CMakeLists.txt index dad69f3f57..ab89c645c8 100644 --- a/Userland/Applications/Help/CMakeLists.txt +++ b/Userland/Applications/Help/CMakeLists.txt @@ -4,9 +4,13 @@ serenity_component( TARGETS Help ) +compile_gml(HelpWindow.gml HelpWindowGML.h help_window_gml) + set(SOURCES + HelpWindowGML.h History.cpp main.cpp + MainWidget.cpp ManualModel.cpp ManualPageNode.cpp ManualSectionNode.cpp diff --git a/Userland/Applications/Help/HelpWindow.gml b/Userland/Applications/Help/HelpWindow.gml new file mode 100644 index 0000000000..5c1dfa8e53 --- /dev/null +++ b/Userland/Applications/Help/HelpWindow.gml @@ -0,0 +1,50 @@ +@GUI::Widget { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + spacing: 2 + } + + @GUI::ToolbarContainer { + @GUI::Toolbar { + name: "toolbar" + } + } + + @GUI::HorizontalSplitter { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + + @GUI::TabWidget { + name: "tab_widget" + fixed_width: 200 + container_margins: [6] + + @GUI::TreeView { + name: "browse_view" + } + + @GUI::Widget { + name: "search_container" + layout: @GUI::VerticalBoxLayout {} + + @GUI::TextBox { + name: "search_box" + placeholder: "Search" + } + + @GUI::ListView { + name: "search_view" + } + } + } + + @Web::OutOfProcessWebView { + name: "web_view" + } + } + + @GUI::Statusbar { + name: "statusbar" + } +} diff --git a/Userland/Applications/Help/MainWidget.cpp b/Userland/Applications/Help/MainWidget.cpp new file mode 100644 index 0000000000..d23f872111 --- /dev/null +++ b/Userland/Applications/Help/MainWidget.cpp @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2019-2020, Sergey Bugaev + * Copyright (c) 2021, Andreas Kling + * Copyright (c) 2021, Sam Atkins + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "MainWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Help { + +MainWidget::MainWidget() +{ + load_from_gml(help_window_gml); + m_toolbar = find_descendant_of_type_named("toolbar"); + m_tab_widget = find_descendant_of_type_named("tab_widget"); + m_search_container = find_descendant_of_type_named("search_container"); + + m_search_box = find_descendant_of_type_named("search_box"); + m_search_box->on_change = [this] { + if (auto* model = m_search_view->model()) { + auto& search_model = *static_cast(model); + search_model.set_filter_term(m_search_box->text()); + search_model.invalidate(); + } + }; + + m_search_view = find_descendant_of_type_named("search_view"); + m_search_view->set_should_hide_unnecessary_scrollbars(true); + m_search_view->on_selection_change = [this] { + auto const& index = m_search_view->selection().first(); + if (!index.is_valid()) + return; + + auto* view_model = m_search_view->model(); + if (!view_model) { + m_web_view->load_empty_document(); + return; + } + auto& search_model = *static_cast(view_model); + auto const& mapped_index = search_model.map(index); + String path = m_manual_model->page_path(mapped_index); + if (path.is_null()) { + m_web_view->load_empty_document(); + return; + } + m_browse_view->selection().clear(); + m_browse_view->selection().add(mapped_index); + m_history.push(path); + open_page(path); + }; + + m_browse_view = find_descendant_of_type_named("browse_view"); + m_browse_view->on_selection_change = [this] { + String path = m_manual_model->page_path(m_browse_view->selection().first()); + if (path.is_null()) + return; + + m_history.push(path); + open_page(path); + }; + m_browse_view->on_toggle = [this](GUI::ModelIndex const& index, bool open) { + m_manual_model->update_section_node_on_toggle(index, open); + }; + + m_web_view = find_descendant_of_type_named("web_view"); + m_web_view->on_link_click = [this](auto& url, auto&, unsigned) { + if (url.protocol() == "file") { + auto path = url.path(); + if (!path.starts_with("/usr/share/man/")) { + open_external(url); + return; + } + auto browse_view_index = m_manual_model->index_from_path(path); + if (browse_view_index.has_value()) { + dbgln("Found path _{}_ in m_manual_model at index {}", path, browse_view_index.value()); + m_browse_view->selection().set(browse_view_index.value()); + return; + } + m_history.push(path); + open_page(path); + } else if (url.protocol() == "help") { + if (url.host() == "man") { + if (url.paths().size() != 2) { + dbgln("Bad help page URL '{}'", url); + return; + } + auto const section = url.paths()[0]; + auto const page = url.paths()[1]; + open_url(URL::create_with_file_scheme(String::formatted("/usr/share/man/man{}/{}.md", section, page), url.fragment())); + } else { + dbgln("Bad help operation '{}' in URL '{}'", url.host(), url); + } + } else { + open_external(url); + } + }; + m_web_view->on_context_menu_request = [this](auto& screen_position) { + m_copy_action->set_enabled(!m_web_view->selected_text().is_empty()); + m_context_menu->popup(screen_position); + }; + m_web_view->on_link_hover = [this](URL const& url) { + if (url.is_valid()) + m_statusbar->set_text(url.to_string()); + else + m_statusbar->set_text({}); + }; + + m_go_back_action = GUI::CommonActions::make_go_back_action([this](auto&) { + m_history.go_back(); + open_page(m_history.current()); + }); + + m_go_forward_action = GUI::CommonActions::make_go_forward_action([this](auto&) { + m_history.go_forward(); + open_page(m_history.current()); + }); + + m_go_back_action->set_enabled(false); + m_go_forward_action->set_enabled(false); + + m_go_home_action = GUI::CommonActions::make_go_home_action([this](auto&) { + String path = "/usr/share/man/man7/Help-index.md"; + m_history.push(path); + open_page(path); + }); + + m_copy_action = GUI::CommonActions::make_copy_action([this](auto&) { + auto selected_text = m_web_view->selected_text(); + if (!selected_text.is_empty()) + GUI::Clipboard::the().set_plain_text(selected_text); + }); + + m_select_all_action = GUI::CommonActions::make_select_all_action([this](auto&) { + m_web_view->select_all(); + }); + + m_statusbar = find_descendant_of_type_named("statusbar"); + GUI::Application::the()->on_action_enter = [this](GUI::Action const& action) { + m_statusbar->set_override_text(action.status_tip()); + }; + GUI::Application::the()->on_action_leave = [this](GUI::Action const&) { + m_statusbar->set_override_text({}); + }; +} + +void MainWidget::set_start_page(String const& start_page, int section) +{ + bool set_start_page = false; + if (!start_page.is_null()) { + if (section != 0) { + // > Help [section] [name] + String path = String::formatted("/usr/share/man/man{}/{}.md", section, start_page); + m_history.push(path); + open_page(path); + set_start_page = true; + } else if (URL url = URL::create_with_url_or_path(start_page); url.is_valid() && url.path().ends_with(".md")) { + // > Help [/path/to/documentation/file.md] + m_history.push(url.path()); + open_page(url.path()); + set_start_page = true; + } else { + // > Help [query] + + // First, see if we can find the page by name + char const* sections[] = { + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + }; + for (auto s : sections) { + String path = String::formatted("/usr/share/man/man{}/{}.md", s, start_page); + if (Core::File::exists(path)) { + m_history.push(path); + open_page(path); + set_start_page = true; + break; + } + } + + // No match, so treat the input as a search query + if (!set_start_page) { + m_tab_widget->set_active_widget(m_search_container); + m_search_box->set_text(start_page); + if (auto* model = m_search_view->model(); model) { + auto& search_model = *static_cast(model); + search_model.set_filter_term(m_search_box->text()); + } + } + } + } + if (!set_start_page) + m_go_home_action->activate(); +} + +ErrorOr MainWidget::initialize_fallibles(GUI::Window& window) +{ + (void)TRY(m_toolbar->try_add_action(*m_go_back_action)); + (void)TRY(m_toolbar->try_add_action(*m_go_forward_action)); + (void)TRY(m_toolbar->try_add_action(*m_go_home_action)); + + auto file_menu = TRY(window.try_add_menu("&File")); + TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) { + GUI::Application::the()->quit(); + }))); + + auto go_menu = TRY(window.try_add_menu("&Go")); + TRY(go_menu->try_add_action(*m_go_back_action)); + TRY(go_menu->try_add_action(*m_go_forward_action)); + TRY(go_menu->try_add_action(*m_go_home_action)); + + auto help_menu = TRY(window.try_add_menu("&Help")); + TRY(help_menu->try_add_action(GUI::Action::create("&Contents", { Key_F1 }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/filetype-unknown.png")), [&](auto&) { + String path = "/usr/share/man/man1/Help.md"; + open_page(path); + }))); + TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Help", TRY(GUI::Icon::try_create_default_icon("app-help")), &window))); + + m_context_menu = TRY(GUI::Menu::try_create()); + TRY(m_context_menu->try_add_action(*m_go_back_action)); + TRY(m_context_menu->try_add_action(*m_go_forward_action)); + TRY(m_context_menu->try_add_action(*m_go_home_action)); + TRY(m_context_menu->try_add_separator()); + TRY(m_context_menu->try_add_action(*m_copy_action)); + TRY(m_context_menu->try_add_action(*m_select_all_action)); + + TRY(m_tab_widget->try_add_widget("Browse", *m_browse_view)); + TRY(m_tab_widget->try_add_widget("Search", *m_search_container)); + + m_manual_model = TRY(ManualModel::create()); + m_browse_view->set_model(*m_manual_model); + m_search_view->set_model(TRY(GUI::FilteringProxyModel::create(*m_manual_model))); + m_search_view->model()->invalidate(); + + return {}; +} + +void MainWidget::open_url(URL const& url) +{ + if (url.protocol() == "file") { + auto path = url.path(); + auto source_result = m_manual_model->page_view(path); + if (source_result.is_error()) { + GUI::MessageBox::show(window(), String::formatted("{}", source_result.error()), "Failed to open man page", GUI::MessageBox::Type::Error); + return; + } + + auto source = source_result.value(); + String html; + { + auto md_document = Markdown::Document::parse(source); + VERIFY(md_document); + html = md_document->render_to_html(); + } + + m_web_view->load_html(html, url); + m_web_view->scroll_to_top(); + + GUI::Application::the()->deferred_invoke([&, path = url.path()] { + auto browse_view_index = m_manual_model->index_from_path(path); + if (browse_view_index.has_value()) { + m_browse_view->expand_tree(browse_view_index.value().parent()); + m_browse_view->selection().set(browse_view_index.value()); + + String page_and_section = m_manual_model->page_and_section(browse_view_index.value()); + window()->set_title(String::formatted("{} - Help", page_and_section)); + } else { + window()->set_title("Help"); + } + }); + } +} + +void MainWidget::open_external(URL const& url) +{ + if (!Desktop::Launcher::open(url)) + GUI::MessageBox::show(window(), String::formatted("The link to '{}' could not be opened.", url), "Failed to open link", GUI::MessageBox::Type::Error); +} + +void MainWidget::open_page(String const& path) +{ + m_go_back_action->set_enabled(m_history.can_go_back()); + m_go_forward_action->set_enabled(m_history.can_go_forward()); + + if (path.is_null()) { + window()->set_title("Help"); + m_web_view->load_empty_document(); + return; + } + open_url(URL::create_with_url_or_path(path)); +} + +} diff --git a/Userland/Applications/Help/MainWidget.h b/Userland/Applications/Help/MainWidget.h new file mode 100644 index 0000000000..bab7bc6fbb --- /dev/null +++ b/Userland/Applications/Help/MainWidget.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "History.h" +#include "ManualModel.h" +#include + +namespace Help { + +class MainWidget final : public GUI::Widget { + C_OBJECT(MainWidget); + +public: + virtual ~MainWidget() override = default; + + ErrorOr initialize_fallibles(GUI::Window&); + void set_start_page(String const& page, int section); + +private: + MainWidget(); + + void open_url(URL const&); + void open_page(String const& path); + void open_external(URL const&); + + History m_history; + RefPtr m_context_menu; + RefPtr m_manual_model; + + RefPtr m_go_back_action; + RefPtr m_go_forward_action; + RefPtr m_go_home_action; + RefPtr m_copy_action; + RefPtr m_select_all_action; + + RefPtr m_tab_widget; + RefPtr m_search_container; + RefPtr m_search_box; + RefPtr m_search_view; + RefPtr m_browse_view; + RefPtr m_web_view; + + RefPtr m_toolbar; + RefPtr m_statusbar; +}; + +} diff --git a/Userland/Applications/Help/ManualModel.h b/Userland/Applications/Help/ManualModel.h index 4b4d606991..262464ee8a 100644 --- a/Userland/Applications/Help/ManualModel.h +++ b/Userland/Applications/Help/ManualModel.h @@ -14,9 +14,9 @@ class ManualModel final : public GUI::Model { public: - static NonnullRefPtr create() + static ErrorOr> create() { - return adopt_ref(*new ManualModel); + return adopt_nonnull_ref_or_enomem(new (nothrow) ManualModel); } virtual ~ManualModel() override {}; diff --git a/Userland/Applications/Help/main.cpp b/Userland/Applications/Help/main.cpp index 5956e29237..542c67338d 100644 --- a/Userland/Applications/Help/main.cpp +++ b/Userland/Applications/Help/main.cpp @@ -6,35 +6,15 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include "History.h" -#include "ManualModel.h" -#include +#include "MainWidget.h" #include -#include #include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include #include -#include -#include + +using namespace Help; ErrorOr serenity_main(Main::Arguments arguments) { @@ -91,301 +71,10 @@ ErrorOr serenity_main(Main::Arguments arguments) window->set_title("Help"); window->resize(570, 500); - auto widget = TRY(window->try_set_main_widget()); - (void)TRY(widget->try_set_layout()); - widget->set_fill_with_background_color(true); - widget->layout()->set_spacing(2); + auto main_widget = TRY(window->try_set_main_widget()); + TRY(main_widget->initialize_fallibles(window)); + main_widget->set_start_page(start_page, section); - auto toolbar_container = TRY(widget->try_add()); - auto toolbar = TRY(toolbar_container->try_add()); - - auto splitter = TRY(widget->try_add()); - splitter->layout()->set_spacing(4); - - auto manual_model = ManualModel::create(); - - auto left_tab_bar = TRY(splitter->try_add()); - auto tree_view_container = TRY(left_tab_bar->try_add_tab("Browse")); - (void)TRY(tree_view_container->try_set_layout()); - tree_view_container->layout()->set_margins(4); - auto tree_view = TRY(tree_view_container->try_add()); - auto search_view = TRY(left_tab_bar->try_add_tab("Search")); - (void)TRY(search_view->try_set_layout()); - search_view->layout()->set_margins(4); - auto search_box = TRY(search_view->try_add()); - auto search_list_view = TRY(search_view->try_add()); - search_box->set_fixed_height(20); - search_box->set_placeholder("Search..."); - search_box->on_change = [&] { - if (auto* model = search_list_view->model()) { - auto& search_model = *static_cast(model); - search_model.set_filter_term(search_box->text()); - search_model.invalidate(); - } - }; - search_list_view->set_model(TRY(GUI::FilteringProxyModel::create(manual_model))); - search_list_view->model()->invalidate(); - - tree_view->set_model(manual_model); - left_tab_bar->set_fixed_width(200); - - auto page_view = TRY(splitter->try_add()); - - History history; - - RefPtr go_back_action; - RefPtr go_forward_action; - - auto open_url = [&](auto const& url) { - if (url.protocol() == "file") { - auto path = url.path(); - auto source_result = manual_model->page_view(path); - if (source_result.is_error()) { - GUI::MessageBox::show(window, String::formatted("{}", source_result.error()), "Failed to open man page", GUI::MessageBox::Type::Error); - return; - } - - auto source = source_result.value(); - String html; - { - auto md_document = Markdown::Document::parse(source); - VERIFY(md_document); - html = md_document->render_to_html(); - } - - page_view->load_html(html, url); - page_view->scroll_to_top(); - - app->deferred_invoke([&, path = url.path()] { - auto tree_view_index = manual_model->index_from_path(path); - if (tree_view_index.has_value()) { - tree_view->expand_tree(tree_view_index.value().parent()); - tree_view->selection().set(tree_view_index.value()); - - String page_and_section = manual_model->page_and_section(tree_view_index.value()); - window->set_title(String::formatted("{} - Help", page_and_section)); - } else { - window->set_title("Help"); - } - }); - } - }; - - auto open_page = [&](String const& path) { - go_back_action->set_enabled(history.can_go_back()); - go_forward_action->set_enabled(history.can_go_forward()); - - if (path.is_null()) { - window->set_title("Help"); - page_view->load_empty_document(); - return; - } - - open_url(URL::create_with_url_or_path(path)); - }; - - tree_view->on_selection_change = [&] { - String path = manual_model->page_path(tree_view->selection().first()); - if (path.is_null()) - return; - - history.push(path); - open_page(path); - }; - - tree_view->on_toggle = [&](GUI::ModelIndex const& index, bool open) { - manual_model->update_section_node_on_toggle(index, open); - }; - - auto open_external = [&](auto& url) { - if (!Desktop::Launcher::open(url)) { - GUI::MessageBox::show(window, - String::formatted("The link to '{}' could not be opened.", url), - "Failed to open link", - GUI::MessageBox::Type::Error); - } - }; - search_list_view->on_selection_change = [&] { - auto const& index = search_list_view->selection().first(); - if (!index.is_valid()) - return; - - auto* view_model = search_list_view->model(); - if (!view_model) { - page_view->load_empty_document(); - return; - } - auto& search_model = *static_cast(view_model); - auto const& mapped_index = search_model.map(index); - String path = manual_model->page_path(mapped_index); - if (path.is_null()) { - page_view->load_empty_document(); - return; - } - tree_view->selection().clear(); - tree_view->selection().add(mapped_index); - history.push(path); - open_page(path); - }; - - page_view->on_link_click = [&](auto& url, auto&, unsigned) { - if (url.protocol() == "file") { - auto path = url.path(); - if (!path.starts_with("/usr/share/man/")) { - open_external(url); - return; - } - auto tree_view_index = manual_model->index_from_path(path); - if (tree_view_index.has_value()) { - dbgln("Found path _{}_ in manual_model at index {}", path, tree_view_index.value()); - tree_view->selection().set(tree_view_index.value()); - return; - } - history.push(path); - open_page(path); - } else if (url.protocol() == "help") { - if (url.host() == "man") { - if (url.paths().size() != 2) { - dbgln("Bad help page URL '{}'", url); - return; - } - auto const section = url.paths()[0]; - auto const page = url.paths()[1]; - open_url(URL::create_with_file_scheme(String::formatted("/usr/share/man/man{}/{}.md", section, page), url.fragment())); - } else { - dbgln("Bad help operation '{}' in URL '{}'", url.host(), url); - } - } else { - open_external(url); - } - }; - - go_back_action = GUI::CommonActions::make_go_back_action([&](auto&) { - history.go_back(); - open_page(history.current()); - }); - - go_forward_action = GUI::CommonActions::make_go_forward_action([&](auto&) { - history.go_forward(); - open_page(history.current()); - }); - - go_back_action->set_enabled(false); - go_forward_action->set_enabled(false); - - auto go_home_action = GUI::CommonActions::make_go_home_action([&](auto&) { - String path = "/usr/share/man/man7/Help-index.md"; - history.push(path); - open_page(path); - }); - - (void)TRY(toolbar->try_add_action(*go_back_action)); - (void)TRY(toolbar->try_add_action(*go_forward_action)); - (void)TRY(toolbar->try_add_action(*go_home_action)); - - auto file_menu = TRY(window->try_add_menu("&File")); - TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) { - GUI::Application::the()->quit(); - }))); - - auto go_menu = TRY(window->try_add_menu("&Go")); - TRY(go_menu->try_add_action(*go_back_action)); - TRY(go_menu->try_add_action(*go_forward_action)); - TRY(go_menu->try_add_action(*go_home_action)); - - auto help_menu = TRY(window->try_add_menu("&Help")); - TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Help", app_icon, window))); - - auto context_menu = TRY(GUI::Menu::try_create()); - TRY(context_menu->try_add_action(*go_back_action)); - TRY(context_menu->try_add_action(*go_forward_action)); - TRY(context_menu->try_add_action(*go_home_action)); - TRY(context_menu->try_add_separator()); - - RefPtr copy_action = GUI::CommonActions::make_copy_action([&](auto&) { - auto selected_text = page_view->selected_text(); - if (!selected_text.is_empty()) - GUI::Clipboard::the().set_plain_text(selected_text); - }); - TRY(context_menu->try_add_action(*copy_action)); - - RefPtr select_all_function = GUI::CommonActions::make_select_all_action([&](auto&) { - page_view->select_all(); - }); - TRY(context_menu->try_add_action(*select_all_function)); - - page_view->on_context_menu_request = [&](auto& screen_position) { - copy_action->set_enabled(!page_view->selected_text().is_empty()); - context_menu->popup(screen_position); - }; - - bool set_start_page = false; - if (start_page) { - if (section != 0) { - // > Help [section] [name] - String path = String::formatted("/usr/share/man/man{}/{}.md", section, start_page); - history.push(path); - open_page(path); - set_start_page = true; - } else if (URL url = URL::create_with_url_or_path(start_page); url.is_valid() && url.path().ends_with(".md")) { - // > Help [/path/to/documentation/file.md] - history.push(url.path()); - open_page(url.path()); - set_start_page = true; - } else { - // > Help [query] - - // First, see if we can find the page by name - char const* sections[] = { - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8" - }; - for (auto s : sections) { - String path = String::formatted("/usr/share/man/man{}/{}.md", s, start_page); - if (Core::File::exists(path)) { - history.push(path); - open_page(path); - set_start_page = true; - break; - } - } - - // No match, so treat the input as a search query - if (!set_start_page) { - left_tab_bar->set_active_widget(search_view); - search_box->set_text(start_page); - if (auto* model = search_list_view->model()) { - auto& search_model = *static_cast(model); - search_model.set_filter_term(search_box->text()); - } - } - } - } - if (!set_start_page) - go_home_action->activate(); - - auto statusbar = TRY(widget->try_add()); - app->on_action_enter = [&statusbar](GUI::Action const& action) { - statusbar->set_override_text(action.status_tip()); - }; - app->on_action_leave = [&statusbar](GUI::Action const&) { - statusbar->set_override_text({}); - }; - - page_view->on_link_hover = [&](URL const& url) { - if (url.is_valid()) - statusbar->set_text(url.to_string()); - else - statusbar->set_text({}); - }; - - window->set_focused_widget(left_tab_bar); window->show(); return app->exec();