summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Applications/Spreadsheet/CMakeLists.txt3
-rw-r--r--Applications/Spreadsheet/HelpWindow.cpp180
-rw-r--r--Applications/Spreadsheet/HelpWindow.h62
-rw-r--r--Applications/Spreadsheet/Spreadsheet.cpp34
-rw-r--r--Applications/Spreadsheet/Spreadsheet.h2
-rw-r--r--Applications/Spreadsheet/SpreadsheetWidget.cpp13
-rw-r--r--Applications/Spreadsheet/main.cpp10
-rw-r--r--Base/res/js/Spreadsheet/runtime.js50
8 files changed, 352 insertions, 2 deletions
diff --git a/Applications/Spreadsheet/CMakeLists.txt b/Applications/Spreadsheet/CMakeLists.txt
index 23503e4ead..880abfb582 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 0000000000..f016abffa8
--- /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 <LibGUI/BoxLayout.h>
+#include <LibGUI/Frame.h>
+#include <LibGUI/ListView.h>
+#include <LibGUI/Model.h>
+#include <LibGUI/Splitter.h>
+#include <LibMarkdown/Document.h>
+#include <LibWeb/Layout/LayoutNode.h>
+#include <LibWeb/OutOfProcessWebView.h>
+
+namespace Spreadsheet {
+
+class HelpListModel final : public GUI::Model {
+public:
+ static NonnullRefPtr<HelpListModel> 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<String> m_keys;
+};
+
+RefPtr<HelpWindow> 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<GUI::Widget>();
+ widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
+ widget.set_fill_with_background_color(true);
+
+ auto& splitter = widget.add<GUI::HorizontalSplitter>();
+ auto& left_frame = splitter.add<GUI::Frame>();
+ left_frame.set_layout<GUI::VerticalBoxLayout>().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<GUI::ListView>();
+ m_listview = &list_view;
+ list_view.set_model(HelpListModel::create());
+
+ // FIXME: This should be in the Web namespace!
+ m_webview = &splitter.add<OutOfProcessWebView>();
+
+ 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<HelpListModel*>(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<HelpListModel*>(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 0000000000..5160e44f95
--- /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 <AK/JsonObject.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibWeb/OutOfProcessWebView.h>
+
+namespace Spreadsheet {
+
+class HelpWindow : public GUI::Window {
+ C_OBJECT(HelpWindow);
+
+public:
+ static NonnullRefPtr<HelpWindow> the()
+ {
+ if (s_the)
+ return *s_the;
+
+ return *(s_the = adopt(*new HelpWindow));
+ }
+
+ virtual ~HelpWindow() override;
+
+ void set_docs(JsonObject&& docs);
+
+private:
+ static RefPtr<HelpWindow> 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 e38c34564b..75119651a3 100644
--- a/Applications/Spreadsheet/Spreadsheet.cpp
+++ b/Applications/Spreadsheet/Spreadsheet.cpp
@@ -28,9 +28,11 @@
#include <AK/GenericLexer.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
+#include <AK/JsonParser.h>
#include <LibCore/File.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/Error.h>
+#include <LibJS/Runtime/Function.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Object.h>
@@ -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 57292f4227..41585b54ca 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<Position> selected_cell() const { return m_selected_cell; }
const HashMap<Position, NonnullOwnPtr<Cell>>& cells() const { return m_cells; }
HashMap<Position, NonnullOwnPtr<Cell>>& cells() { return m_cells; }
diff --git a/Applications/Spreadsheet/SpreadsheetWidget.cpp b/Applications/Spreadsheet/SpreadsheetWidget.cpp
index 64759054e0..8698db4134 100644
--- a/Applications/Spreadsheet/SpreadsheetWidget.cpp
+++ b/Applications/Spreadsheet/SpreadsheetWidget.cpp
@@ -25,11 +25,13 @@
*/
#include "SpreadsheetWidget.h"
+#include "HelpWindow.h"
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonObjectSerializer.h>
#include <LibCore/File.h>
#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Splitter.h>
@@ -52,6 +54,17 @@ SpreadsheetWidget::SpreadsheetWidget()
auto& current_cell_label = top_bar.add<GUI::Label>("");
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<GUI::Button>("🛈");
+ 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>(GUI::TextEditor::Type::SingleLine);
cell_value_editor.set_scrollbars_enabled(false);
diff --git a/Applications/Spreadsheet/main.cpp b/Applications/Spreadsheet/main.cpp
index c94fdfd281..cd4fd4ff17 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 082ab027b0..e9e19ffd02 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",