Serenity Operating System
at master 288 lines 12 kB view raw
1/* 2 * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> 3 * Copyright (c) 2021, Andreas Kling <kling@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 "MainWidget.h" 11#include <AK/LexicalPath.h> 12#include <AK/String.h> 13#include <AK/StringView.h> 14#include <AK/URL.h> 15#include <Applications/Help/HelpWindowGML.h> 16#include <LibCore/ArgsParser.h> 17#include <LibCore/System.h> 18#include <LibDesktop/Launcher.h> 19#include <LibGUI/Action.h> 20#include <LibGUI/Application.h> 21#include <LibGUI/Clipboard.h> 22#include <LibGUI/ListView.h> 23#include <LibGUI/Menu.h> 24#include <LibGUI/Menubar.h> 25#include <LibGUI/MessageBox.h> 26#include <LibGUI/Statusbar.h> 27#include <LibGUI/TabWidget.h> 28#include <LibGUI/TextBox.h> 29#include <LibGUI/Toolbar.h> 30#include <LibGUI/TreeView.h> 31#include <LibGUI/Window.h> 32#include <LibGfx/Bitmap.h> 33#include <LibMain/Main.h> 34#include <LibManual/Node.h> 35#include <LibManual/PageNode.h> 36#include <LibManual/Path.h> 37#include <LibManual/SectionNode.h> 38#include <LibMarkdown/Document.h> 39 40namespace Help { 41 42MainWidget::MainWidget() 43{ 44 load_from_gml(help_window_gml).release_value_but_fixme_should_propagate_errors(); 45 m_toolbar = find_descendant_of_type_named<GUI::Toolbar>("toolbar"); 46 m_tab_widget = find_descendant_of_type_named<GUI::TabWidget>("tab_widget"); 47 m_search_container = find_descendant_of_type_named<GUI::Widget>("search_container"); 48 49 m_search_box = find_descendant_of_type_named<GUI::TextBox>("search_box"); 50 m_search_box->on_change = [this] { 51 m_filter_model->set_filter_term(m_search_box->text()); 52 }; 53 m_search_box->on_down_pressed = [this] { 54 m_search_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set); 55 }; 56 m_search_box->on_up_pressed = [this] { 57 m_search_view->move_cursor(GUI::AbstractView::CursorMovement::Up, GUI::AbstractView::SelectionUpdate::Set); 58 }; 59 60 m_search_view = find_descendant_of_type_named<GUI::ListView>("search_view"); 61 m_search_view->set_should_hide_unnecessary_scrollbars(true); 62 m_search_view->on_selection_change = [this] { 63 auto const& index = m_search_view->selection().first(); 64 if (!index.is_valid()) 65 return; 66 67 auto* view_model = m_search_view->model(); 68 if (!view_model) { 69 m_web_view->load_empty_document(); 70 return; 71 } 72 auto& search_model = *static_cast<GUI::FilteringProxyModel*>(view_model); 73 auto const& mapped_index = search_model.map(index); 74 auto path = m_manual_model->page_path(mapped_index); 75 if (!path.has_value()) { 76 m_web_view->load_empty_document(); 77 return; 78 } 79 m_browse_view->selection().clear(); 80 m_browse_view->selection().add(mapped_index); 81 m_history.push(path.value()); 82 open_page(path.value()); 83 }; 84 85 m_browse_view = find_descendant_of_type_named<GUI::TreeView>("browse_view"); 86 m_browse_view->on_selection_change = [this] { 87 auto path = m_manual_model->page_path(m_browse_view->selection().first()); 88 if (!path.has_value()) 89 return; 90 91 m_history.push(path.value()); 92 open_page(path.value()); 93 }; 94 m_browse_view->on_toggle = [this](GUI::ModelIndex const& index, bool open) { 95 m_manual_model->update_section_node_on_toggle(index, open); 96 }; 97 98 m_web_view = find_descendant_of_type_named<WebView::OutOfProcessWebView>("web_view"); 99 m_web_view->on_link_click = [this](auto& url, auto&, unsigned) { 100 if (url.scheme() == "file") { 101 auto path = LexicalPath { url.path() }; 102 if (!path.is_child_of(Manual::manual_base_path)) { 103 open_external(url); 104 return; 105 } 106 auto browse_view_index = m_manual_model->index_from_path(path.string()); 107 if (browse_view_index.has_value()) { 108 dbgln("Found path _{}_ in m_manual_model at index {}", path, browse_view_index.value()); 109 m_browse_view->selection().set(browse_view_index.value()); 110 return; 111 } 112 m_history.push(path.string()); 113 auto string_path = String::from_utf8(path.string()); 114 if (string_path.is_error()) 115 return; 116 open_page(string_path.value()); 117 } else if (url.scheme() == "help") { 118 auto maybe_page = Manual::Node::try_find_from_help_url(url); 119 if (maybe_page.is_error()) { 120 dbgln("Error opening page: {}", maybe_page.error()); 121 return; 122 } 123 auto maybe_path = maybe_page.value()->path(); 124 if (!maybe_path.is_error()) 125 return; 126 open_page(maybe_path.release_value()); 127 } else { 128 open_external(url); 129 } 130 }; 131 m_web_view->on_context_menu_request = [this](auto screen_position) { 132 m_copy_action->set_enabled(!m_web_view->selected_text().is_empty()); 133 m_context_menu->popup(screen_position); 134 }; 135 m_web_view->on_link_hover = [this](URL const& url) { 136 if (url.is_valid()) 137 m_statusbar->set_text(url.to_deprecated_string()); 138 else 139 m_statusbar->set_text({}); 140 }; 141 142 m_go_back_action = GUI::CommonActions::make_go_back_action([this](auto&) { 143 m_history.go_back(); 144 open_page(MUST(String::from_deprecated_string(m_history.current()))); 145 }); 146 147 m_go_forward_action = GUI::CommonActions::make_go_forward_action([this](auto&) { 148 m_history.go_forward(); 149 open_page(MUST(String::from_deprecated_string(m_history.current()))); 150 }); 151 152 m_go_back_action->set_enabled(false); 153 m_go_forward_action->set_enabled(false); 154 155 m_copy_action = GUI::CommonActions::make_copy_action([this](auto&) { 156 auto selected_text = m_web_view->selected_text(); 157 if (!selected_text.is_empty()) 158 GUI::Clipboard::the().set_plain_text(selected_text); 159 }); 160 161 m_select_all_action = GUI::CommonActions::make_select_all_action([this](auto&) { 162 m_web_view->select_all(); 163 }); 164 165 m_statusbar = find_descendant_of_type_named<GUI::Statusbar>("statusbar"); 166 GUI::Application::the()->on_action_enter = [this](GUI::Action const& action) { 167 m_statusbar->set_override_text(action.status_tip()); 168 }; 169 GUI::Application::the()->on_action_leave = [this](GUI::Action const&) { 170 m_statusbar->set_override_text({}); 171 }; 172} 173 174ErrorOr<void> MainWidget::set_start_page(Vector<StringView, 2> query_parameters) 175{ 176 auto result = Manual::Node::try_create_from_query(query_parameters); 177 if (result.is_error()) { 178 // No match, so treat the input as a search query 179 m_tab_widget->set_active_widget(m_search_container); 180 m_search_box->set_focus(true); 181 m_search_box->set_text(query_parameters.first_matching([](auto&) { return true; }).value_or(""sv)); 182 m_search_box->select_all(); 183 m_filter_model->set_filter_term(m_search_box->text()); 184 m_go_home_action->activate(); 185 } else { 186 auto const page = TRY(result.value()->path()); 187 m_history.push(page); 188 open_page(page); 189 } 190 return {}; 191} 192 193ErrorOr<void> MainWidget::initialize_fallibles(GUI::Window& window) 194{ 195 static String const help_index_path = TRY(TRY(Manual::PageNode::help_index_page())->path()); 196 m_go_home_action = GUI::CommonActions::make_go_home_action([this](auto&) { 197 m_history.push(help_index_path); 198 open_page(help_index_path); 199 }); 200 201 (void)TRY(m_toolbar->try_add_action(*m_go_back_action)); 202 (void)TRY(m_toolbar->try_add_action(*m_go_forward_action)); 203 (void)TRY(m_toolbar->try_add_action(*m_go_home_action)); 204 205 auto file_menu = TRY(window.try_add_menu("&File")); 206 TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) { 207 GUI::Application::the()->quit(); 208 }))); 209 210 auto go_menu = TRY(window.try_add_menu("&Go")); 211 TRY(go_menu->try_add_action(*m_go_back_action)); 212 TRY(go_menu->try_add_action(*m_go_forward_action)); 213 TRY(go_menu->try_add_action(*m_go_home_action)); 214 215 auto help_menu = TRY(window.try_add_menu("&Help")); 216 String help_page_path = TRY(TRY(try_make_ref_counted<Manual::PageNode>(Manual::sections[1 - 1], TRY("Help"_string)))->path()); 217 TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(&window))); 218 TRY(help_menu->try_add_action(GUI::Action::create("&Contents", { Key_F1 }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-unknown.png"sv)), [this, help_page_path = move(help_page_path)](auto&) { 219 open_page(help_page_path); 220 }))); 221 TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Help", TRY(GUI::Icon::try_create_default_icon("app-help"sv)), &window))); 222 223 m_context_menu = TRY(GUI::Menu::try_create()); 224 TRY(m_context_menu->try_add_action(*m_go_back_action)); 225 TRY(m_context_menu->try_add_action(*m_go_forward_action)); 226 TRY(m_context_menu->try_add_action(*m_go_home_action)); 227 TRY(m_context_menu->try_add_separator()); 228 TRY(m_context_menu->try_add_action(*m_copy_action)); 229 TRY(m_context_menu->try_add_action(*m_select_all_action)); 230 231 m_manual_model = TRY(ManualModel::create()); 232 m_browse_view->set_model(*m_manual_model); 233 m_filter_model = TRY(GUI::FilteringProxyModel::create(*m_manual_model)); 234 m_search_view->set_model(*m_filter_model); 235 m_filter_model->set_filter_term(""sv); 236 237 return {}; 238} 239 240void MainWidget::open_url(URL const& url) 241{ 242 m_go_back_action->set_enabled(m_history.can_go_back()); 243 m_go_forward_action->set_enabled(m_history.can_go_forward()); 244 245 if (url.scheme() == "file") { 246 m_web_view->load(url); 247 m_web_view->scroll_to_top(); 248 249 auto browse_view_index = m_manual_model->index_from_path(url.path()); 250 if (browse_view_index.has_value()) { 251 if (browse_view_index.value() != m_browse_view->selection_start_index()) { 252 m_browse_view->expand_all_parents_of(browse_view_index.value()); 253 m_browse_view->set_cursor(browse_view_index.value(), GUI::AbstractView::SelectionUpdate::Set); 254 } 255 256 auto page_and_section = m_manual_model->page_and_section(browse_view_index.value()); 257 if (!page_and_section.has_value()) 258 return; 259 auto title = String::formatted("{} - Help", page_and_section.value()); 260 if (!title.is_error()) 261 window()->set_title(title.release_value().to_deprecated_string()); 262 } else { 263 window()->set_title("Help"); 264 } 265 } 266} 267 268void MainWidget::open_external(URL const& url) 269{ 270 if (!Desktop::Launcher::open(url)) 271 GUI::MessageBox::show(window(), DeprecatedString::formatted("The link to '{}' could not be opened.", url), "Failed to open link"sv, GUI::MessageBox::Type::Error); 272} 273 274void MainWidget::open_page(Optional<String> const& path) 275{ 276 m_go_back_action->set_enabled(m_history.can_go_back()); 277 m_go_forward_action->set_enabled(m_history.can_go_forward()); 278 279 if (!path.has_value()) { 280 window()->set_title("Help"); 281 m_web_view->load_empty_document(); 282 return; 283 } 284 dbgln("open page: {}", path.value()); 285 open_url(URL::create_with_url_or_path(path.value().to_deprecated_string())); 286} 287 288}