Serenity Operating System
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}