Serenity Operating System
at master 203 lines 7.5 kB view raw
1/* 2 * Copyright (c) 2020-2022, the SerenityOS developers. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "HelpWindow.h" 8#include "SpreadsheetWidget.h" 9#include <AK/LexicalPath.h> 10#include <AK/QuickSort.h> 11#include <LibGUI/BoxLayout.h> 12#include <LibGUI/Frame.h> 13#include <LibGUI/ListView.h> 14#include <LibGUI/MessageBox.h> 15#include <LibGUI/Model.h> 16#include <LibGUI/Splitter.h> 17#include <LibMarkdown/Document.h> 18#include <LibWeb/Layout/Node.h> 19#include <LibWebView/OutOfProcessWebView.h> 20 21namespace Spreadsheet { 22 23class HelpListModel final : public GUI::Model { 24public: 25 static NonnullRefPtr<HelpListModel> create() { return adopt_ref(*new HelpListModel); } 26 27 virtual ~HelpListModel() override = default; 28 29 virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_keys.size(); } 30 virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; } 31 32 virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role = GUI::ModelRole::Display) const override 33 { 34 if (role == GUI::ModelRole::Display) { 35 return key(index); 36 } 37 38 return {}; 39 } 40 41 DeprecatedString key(const GUI::ModelIndex& index) const { return m_keys[index.row()]; } 42 43 void set_from(JsonObject const& object) 44 { 45 m_keys.clear(); 46 object.for_each_member([this](auto& name, auto&) { 47 m_keys.append(name); 48 }); 49 AK::quick_sort(m_keys); 50 invalidate(); 51 } 52 53private: 54 HelpListModel() 55 { 56 } 57 58 Vector<DeprecatedString> m_keys; 59}; 60 61RefPtr<HelpWindow> HelpWindow::s_the { nullptr }; 62 63HelpWindow::HelpWindow(GUI::Window* parent) 64 : GUI::Window(parent) 65{ 66 resize(530, 365); 67 set_title("Spreadsheet Functions Help"); 68 set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-help.png"sv).release_value_but_fixme_should_propagate_errors()); 69 70 auto widget = set_main_widget<GUI::Widget>().release_value_but_fixme_should_propagate_errors(); 71 widget->set_layout<GUI::VerticalBoxLayout>(); 72 widget->set_fill_with_background_color(true); 73 74 auto& splitter = widget->add<GUI::HorizontalSplitter>(); 75 auto& left_frame = splitter.add<GUI::Frame>(); 76 left_frame.set_layout<GUI::VerticalBoxLayout>(); 77 // FIXME: Get rid of the magic number, dynamically calculate initial size based on left frame contents 78 left_frame.set_preferred_width(100); 79 m_listview = left_frame.add<GUI::ListView>(); 80 m_listview->set_activates_on_selection(true); 81 m_listview->set_model(HelpListModel::create()); 82 83 m_webview = splitter.add<WebView::OutOfProcessWebView>(); 84 m_webview->on_link_click = [this](auto& url, auto&, auto&&) { 85 VERIFY(url.scheme() == "spreadsheet"); 86 if (url.host() == "example") { 87 auto entry = LexicalPath::basename(url.path()); 88 auto doc_option = m_docs.get_object(entry); 89 if (!doc_option.has_value()) { 90 GUI::MessageBox::show_error(this, DeprecatedString::formatted("No documentation entry found for '{}'", url.path())); 91 return; 92 } 93 auto& doc = doc_option.value(); 94 const auto& name = url.fragment(); 95 96 auto maybe_example_data = doc.get_object("example_data"sv); 97 if (!maybe_example_data.has_value()) { 98 GUI::MessageBox::show_error(this, DeprecatedString::formatted("No example data found for '{}'", url.path())); 99 return; 100 } 101 auto& example_data = maybe_example_data.value(); 102 103 if (!example_data.has_object(name)) { 104 GUI::MessageBox::show_error(this, DeprecatedString::formatted("Example '{}' not found for '{}'", name, url.path())); 105 return; 106 } 107 auto& value = example_data.get_object(name).value(); 108 109 auto window = GUI::Window::construct(this); 110 window->resize(size()); 111 window->set_icon(icon()); 112 window->set_title(DeprecatedString::formatted("Spreadsheet Help - Example {} for {}", name, entry)); 113 window->on_close = [window = window.ptr()] { window->remove_from_parent(); }; 114 115 auto widget = window->set_main_widget<SpreadsheetWidget>(window, Vector<NonnullRefPtr<Sheet>> {}, false).release_value_but_fixme_should_propagate_errors(); 116 auto sheet = Sheet::from_json(value, widget->workbook()); 117 if (!sheet) { 118 GUI::MessageBox::show_error(this, DeprecatedString::formatted("Corrupted example '{}' in '{}'", name, url.path())); 119 return; 120 } 121 122 widget->add_sheet(sheet.release_nonnull()); 123 window->show(); 124 } else if (url.host() == "doc") { 125 auto entry = LexicalPath::basename(url.path()); 126 m_webview->load(URL::create_with_data("text/html", render(entry))); 127 } else { 128 dbgln("Invalid spreadsheet action domain '{}'", url.host()); 129 } 130 }; 131 132 m_listview->on_activation = [this](auto& index) { 133 if (!m_webview) 134 return; 135 136 auto key = static_cast<HelpListModel*>(m_listview->model())->key(index); 137 m_webview->load(URL::create_with_data("text/html", render(key))); 138 }; 139} 140 141DeprecatedString HelpWindow::render(StringView key) 142{ 143 VERIFY(m_docs.has_object(key)); 144 auto& doc = m_docs.get_object(key).value(); 145 146 auto name = doc.get_deprecated_string("name"sv).value_or({}); 147 auto argc = doc.get_u32("argc"sv).value_or(0); 148 VERIFY(doc.has_array("argnames"sv)); 149 auto& argnames = doc.get_array("argnames"sv).value(); 150 151 auto docstring = doc.get_deprecated_string("doc"sv).value_or({}); 152 153 StringBuilder markdown_builder; 154 155 markdown_builder.append("# NAME\n`"sv); 156 markdown_builder.append(name); 157 markdown_builder.append("`\n\n"sv); 158 159 markdown_builder.append("# ARGUMENTS\n"sv); 160 if (argc > 0) 161 markdown_builder.appendff("{} required argument(s):\n", argc); 162 else 163 markdown_builder.append("No required arguments.\n"sv); 164 165 for (size_t i = 0; i < argc; ++i) 166 markdown_builder.appendff("- `{}`\n", argnames.at(i).to_deprecated_string()); 167 168 if (argc > 0) 169 markdown_builder.append("\n"sv); 170 171 if ((size_t)argnames.size() > argc) { 172 auto opt_count = argnames.size() - argc; 173 markdown_builder.appendff("{} optional argument(s):\n", opt_count); 174 for (size_t i = argc; i < (size_t)argnames.size(); ++i) 175 markdown_builder.appendff("- `{}`\n", argnames.at(i).to_deprecated_string()); 176 markdown_builder.append("\n"sv); 177 } 178 179 markdown_builder.append("# DESCRIPTION\n"sv); 180 markdown_builder.append(docstring); 181 markdown_builder.append("\n\n"sv); 182 183 if (doc.has("examples"sv)) { 184 auto examples = doc.get_object("examples"sv); 185 VERIFY(examples.has_value()); 186 markdown_builder.append("# EXAMPLES\n"sv); 187 examples->for_each_member([&](auto& text, auto& description_value) { 188 dbgln("```js\n{}\n```\n\n- {}\n", text, description_value.to_deprecated_string()); 189 markdown_builder.appendff("```js\n{}\n```\n\n- {}\n", text, description_value.to_deprecated_string()); 190 }); 191 } 192 193 auto document = Markdown::Document::parse(markdown_builder.string_view()); 194 return document->render_to_html(); 195} 196 197void HelpWindow::set_docs(JsonObject&& docs) 198{ 199 m_docs = move(docs); 200 static_cast<HelpListModel*>(m_listview->model())->set_from(m_docs); 201 m_listview->update(); 202} 203}