diff --git a/Applications/Spreadsheet/CMakeLists.txt b/Applications/Spreadsheet/CMakeLists.txt index 23503e4ead5..880abfb5827 100644 --- a/Applications/Spreadsheet/CMakeLists.txt +++ b/Applications/Spreadsheet/CMakeLists.txt @@ -1,4 +1,5 @@ set(SOURCES + HelpWindow.cpp Spreadsheet.cpp SpreadsheetModel.cpp SpreadsheetView.cpp @@ -7,4 +8,4 @@ set(SOURCES ) serenity_bin(Spreadsheet) -target_link_libraries(Spreadsheet LibGUI LibJS) +target_link_libraries(Spreadsheet LibGUI LibJS LibWeb) diff --git a/Applications/Spreadsheet/HelpWindow.cpp b/Applications/Spreadsheet/HelpWindow.cpp new file mode 100644 index 00000000000..f016abffa8e --- /dev/null +++ b/Applications/Spreadsheet/HelpWindow.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "HelpWindow.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Spreadsheet { + +class HelpListModel final : public GUI::Model { +public: + static NonnullRefPtr create() { return adopt(*new HelpListModel); } + + virtual ~HelpListModel() override { } + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_keys.size(); } + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; } + virtual void update() override { } + + virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role = GUI::ModelRole::Display) const override + { + if (role == GUI::ModelRole::Display) { + return key(index); + } + + return {}; + } + + String key(const GUI::ModelIndex& index) const { return m_keys[index.row()]; } + + void set_from(const JsonObject& object) + { + m_keys.clear(); + object.for_each_member([this](auto& name, auto&) { + m_keys.append(name); + }); + did_update(); + } + +private: + HelpListModel() + { + } + + Vector m_keys; +}; + +RefPtr HelpWindow::s_the { nullptr }; + +HelpWindow::HelpWindow(GUI::Window* parent) + : GUI::Window(parent) +{ + resize(530, 365); + set_title("Spreadsheet Functions Help"); + + auto& widget = set_main_widget(); + widget.set_layout().set_margins({ 4, 4, 4, 4 }); + widget.set_fill_with_background_color(true); + + auto& splitter = widget.add(); + auto& left_frame = splitter.add(); + left_frame.set_layout().set_margins({ 0, 0, 0, 0 }); + left_frame.set_preferred_size(100, 0); + left_frame.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); + auto& list_view = left_frame.add(); + m_listview = &list_view; + list_view.set_model(HelpListModel::create()); + + // FIXME: This should be in the Web namespace! + m_webview = &splitter.add(); + + list_view.on_activation = [this](auto& index) { + if (!m_webview) + return; + + m_webview->load(URL::create_with_data("text/html", render(index))); + }; +} + +String HelpWindow::render(const GUI::ModelIndex& index) +{ + auto key = static_cast(m_listview->model())->key(index); + auto doc_option = m_docs.get(key); + ASSERT(doc_option.is_object()); + + auto& doc = doc_option.as_object(); + + auto name = doc.get("name").to_string(); + auto argc = doc.get("argc").to_u32(0); + auto argnames_value = doc.get("argnames"); + ASSERT(argnames_value.is_array()); + auto& argnames = argnames_value.as_array(); + + auto docstring = doc.get("doc").to_string(); + auto examples_value = doc.get_or("examples", JsonObject {}); + ASSERT(examples_value.is_object()); + auto& examples = examples_value.as_object(); + + StringBuilder markdown_builder; + + markdown_builder.append("# NAME\n`"); + markdown_builder.append(name); + markdown_builder.append("`\n\n"); + + markdown_builder.append("# ARGUMENTS\n"); + if (argc > 0) + markdown_builder.appendf("%d required argument%s: \n", argc, argc > 1 ? "s" : ""); + else + markdown_builder.appendf("No required arguments.\n"); + + for (size_t i = 0; i < argc; ++i) + markdown_builder.appendf("- `%s`\n", argnames.at(i).to_string().characters()); + + if (argc > 0) + markdown_builder.append("\n"); + + if ((size_t)argnames.size() > argc) { + auto opt_count = argnames.size() - argc; + markdown_builder.appendf("%d optional argument%s: \n", opt_count, opt_count > 1 ? "s" : ""); + for (size_t i = argc; i < (size_t)argnames.size(); ++i) + markdown_builder.appendf("- `%s`\n", argnames.at(i).to_string().characters()); + markdown_builder.append("\n"); + } + + markdown_builder.append("# DESCRIPTION\n"); + markdown_builder.append(docstring); + markdown_builder.append("\n\n"); + + if (!examples.is_empty()) { + markdown_builder.append("# EXAMPLES\n"); + examples.for_each_member([&](auto& text, auto& description_value) { + markdown_builder.appendf("- %s\n\n```js\n%s\n```\n", description_value.to_string().characters(), text.characters()); + }); + } + + auto document = Markdown::Document::parse(markdown_builder.string_view()); + return document->render_to_html(); +} + +void HelpWindow::set_docs(JsonObject&& docs) +{ + m_docs = move(docs); + static_cast(m_listview->model())->set_from(m_docs); + m_listview->update(); +} + +HelpWindow::~HelpWindow() +{ +} + +} diff --git a/Applications/Spreadsheet/HelpWindow.h b/Applications/Spreadsheet/HelpWindow.h new file mode 100644 index 00000000000..5160e44f958 --- /dev/null +++ b/Applications/Spreadsheet/HelpWindow.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include +#include + +namespace Spreadsheet { + +class HelpWindow : public GUI::Window { + C_OBJECT(HelpWindow); + +public: + static NonnullRefPtr the() + { + if (s_the) + return *s_the; + + return *(s_the = adopt(*new HelpWindow)); + } + + virtual ~HelpWindow() override; + + void set_docs(JsonObject&& docs); + +private: + static RefPtr s_the; + String render(const GUI::ModelIndex&); + HelpWindow(GUI::Window* parent = nullptr); + + JsonObject m_docs; + OutOfProcessWebView* m_webview { nullptr }; + GUI::ListView* m_listview { nullptr }; +}; + +} diff --git a/Applications/Spreadsheet/Spreadsheet.cpp b/Applications/Spreadsheet/Spreadsheet.cpp index e38c34564bb..75119651a31 100644 --- a/Applications/Spreadsheet/Spreadsheet.cpp +++ b/Applications/Spreadsheet/Spreadsheet.cpp @@ -28,9 +28,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -208,7 +210,6 @@ JS::Value Sheet::evaluate(const StringView& source, Cell* on_behalf_of) void Cell::update_data() { - dbg() << "Update cell " << this << ", dirty=" << dirty; TemporaryChange cell_change { sheet->current_evaluated_cell(), this }; if (!dirty) return; @@ -325,4 +326,35 @@ JsonObject Sheet::to_json() const return object; } +JsonObject Sheet::gather_documentation() const +{ + JsonObject object; + const JS::PropertyName doc_name { "__documentation" }; + + auto& global_object = m_interpreter->global_object(); + for (auto& it : global_object.shape().property_table()) { + auto value = global_object.get(it.key); + if (!value.is_function()) + continue; + + auto& fn = value.as_function(); + if (!fn.has_own_property(doc_name)) + continue; + + auto doc = fn.get(doc_name); + if (!doc.is_string()) + continue; + + JsonParser parser(doc.to_string_without_side_effects()); + auto doc_object = parser.parse(); + + if (doc_object.has_value()) + object.set(it.key.to_display_string(), doc_object.value()); + else + dbg() << "Sheet::gather_documentation(): Failed to parse the documentation for '" << it.key.to_display_string() << "'!"; + } + + return object; +} + } diff --git a/Applications/Spreadsheet/Spreadsheet.h b/Applications/Spreadsheet/Spreadsheet.h index 57292f42275..41585b54ca6 100644 --- a/Applications/Spreadsheet/Spreadsheet.h +++ b/Applications/Spreadsheet/Spreadsheet.h @@ -135,6 +135,8 @@ public: const String& name() const { return m_name; } void set_name(const StringView& name) { m_name = name; } + JsonObject gather_documentation() const; + Optional selected_cell() const { return m_selected_cell; } const HashMap>& cells() const { return m_cells; } HashMap>& cells() { return m_cells; } diff --git a/Applications/Spreadsheet/SpreadsheetWidget.cpp b/Applications/Spreadsheet/SpreadsheetWidget.cpp index 64759054e05..8698db4134f 100644 --- a/Applications/Spreadsheet/SpreadsheetWidget.cpp +++ b/Applications/Spreadsheet/SpreadsheetWidget.cpp @@ -25,11 +25,13 @@ */ #include "SpreadsheetWidget.h" +#include "HelpWindow.h" #include #include #include #include #include +#include #include #include #include @@ -52,6 +54,17 @@ SpreadsheetWidget::SpreadsheetWidget() auto& current_cell_label = top_bar.add(""); current_cell_label.set_preferred_size(50, 0); current_cell_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); + + auto& help_button = top_bar.add("🛈"); + help_button.set_preferred_size(20, 20); + help_button.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + help_button.on_click = [&](auto) { + auto docs = m_selected_view->sheet().gather_documentation(); + auto help_window = HelpWindow::the(); + help_window->set_docs(move(docs)); + help_window->show(); + }; + auto& cell_value_editor = top_bar.add(GUI::TextEditor::Type::SingleLine); cell_value_editor.set_scrollbars_enabled(false); diff --git a/Applications/Spreadsheet/main.cpp b/Applications/Spreadsheet/main.cpp index c94fdfd2817..cd4fd4ff179 100644 --- a/Applications/Spreadsheet/main.cpp +++ b/Applications/Spreadsheet/main.cpp @@ -43,6 +43,16 @@ int main(int argc, char* argv[]) return 1; } + if (unveil("/tmp/portal/webcontent", "rw") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/etc", "r") < 0) { + perror("unveil"); + return 1; + } + if (unveil("/res", "r") < 0) { perror("unveil"); return 1; diff --git a/Base/res/js/Spreadsheet/runtime.js b/Base/res/js/Spreadsheet/runtime.js index 082ab027b0d..e9e19ffd025 100644 --- a/Base/res/js/Spreadsheet/runtime.js +++ b/Base/res/js/Spreadsheet/runtime.js @@ -91,3 +91,53 @@ function integer(value) { return value | 0 } +// Cheat the system and add documentation +range.__documentation = JSON.stringify({ + name: "range", + argc: 2, + argnames: ["start", "end", "column step", "row step"], + doc: + "Generates a list of cell names in a rectangle defined by two " + + "_top left_ and _bottom right_ cells `start` and `end`, spaced" + + " `column step` columns, and `row step` rows apart.", + examples: { + 'range("A1", "C4")': "Generate a range A1:C4", + 'range("A1", "C4", 2)': "Generate a range A1:C4, skipping every other column", + }, +}); + +select.__documentation = JSON.stringify({ + name: "select", + argc: 3, + argnames: ["criteria", "true value", "false value"], + doc: "Selects between the two `true` and `false` values based on the value of `criteria`", + examples: { + "select(A1, A2, A3)": "Evaluates to A2 if A1 is true, A3 otherwise", + }, +}); + +sumIf.__documentation = JSON.stringify({ + name: "sumIf", + argc: 2, + argnames: ["condition", "cell names"], + doc: + "Calculates the sum of cells the value of which evaluates to true when passed to `condition`", + examples: { + 'sumIf(x => x instanceof Number, range("A1", "C4"))': + "Calculates the sum of all numbers within A1:C4", + }, +}); + +countIf.__documentation = JSON.stringify({ + name: "countIf", + argc: 2, + argnames: ["condition", "cell names"], + doc: "Counts cells the value of which evaluates to true when passed to `condition`", + examples: { + 'countIf(x => x instanceof Number, range("A1", "C4"))': + "Counts the number of cells which have numbers within A1:C4", + }, +}); + +now.__documentation = JSON.stringify({ + name: "now",