浏览代码

Spreadsheet: Start making a spreadsheet application

AnotherTest 4 年之前
父节点
当前提交
a6ebd29aa5

+ 1 - 0
Applications/CMakeLists.txt

@@ -16,6 +16,7 @@ add_subdirectory(Piano)
 add_subdirectory(PixelPaint)
 add_subdirectory(QuickShow)
 add_subdirectory(SoundPlayer)
+add_subdirectory(Spreadsheet)
 add_subdirectory(SystemMonitor)
 add_subdirectory(ThemeEditor)
 add_subdirectory(Terminal)

+ 10 - 0
Applications/Spreadsheet/CMakeLists.txt

@@ -0,0 +1,10 @@
+set(SOURCES
+    Spreadsheet.cpp
+    SpreadsheetModel.cpp
+    SpreadsheetView.cpp
+    SpreadsheetWidget.cpp
+    main.cpp
+)
+
+serenity_bin(Spreadsheet)
+target_link_libraries(Spreadsheet LibGUI LibJS)

+ 327 - 0
Applications/Spreadsheet/Spreadsheet.cpp

@@ -0,0 +1,327 @@
+/*
+ * 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 "Spreadsheet.h"
+#include <AK/GenericLexer.h>
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <LibCore/File.h>
+#include <LibJS/Parser.h>
+#include <LibJS/Runtime/Error.h>
+#include <LibJS/Runtime/GlobalObject.h>
+#include <LibJS/Runtime/Object.h>
+
+namespace Spreadsheet {
+
+class SheetGlobalObject : public JS::GlobalObject {
+    JS_OBJECT(SheetGlobalObject, JS::GlobalObject);
+
+public:
+    SheetGlobalObject(Sheet& sheet)
+        : m_sheet(sheet)
+    {
+    }
+
+    virtual ~SheetGlobalObject() override
+    {
+    }
+
+    virtual JS::Value get(const JS::PropertyName& name, JS::Value receiver = {}) const override
+    {
+        if (name.is_string()) {
+            if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
+                auto& cell = m_sheet.ensure(pos.value());
+                cell.reference_from(m_sheet.current_evaluated_cell());
+                return cell.js_data();
+            }
+        }
+
+        return GlobalObject::get(name, receiver);
+    }
+
+    virtual bool put(const JS::PropertyName& name, JS::Value value, JS::Value receiver = {}) override
+    {
+        if (name.is_string()) {
+            if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
+                auto& cell = m_sheet.ensure(pos.value());
+                if (auto current = m_sheet.current_evaluated_cell())
+                    current->reference_from(&cell);
+
+                cell.set_data(value); // FIXME: This produces un-savable state!
+                return true;
+            }
+        }
+
+        return GlobalObject::put(name, value, receiver);
+    }
+
+    virtual void initialize() override
+    {
+        GlobalObject::initialize();
+        define_native_function("parse_cell_name", parse_cell_name, 1);
+    }
+
+    static JS_DEFINE_NATIVE_FUNCTION(parse_cell_name)
+    {
+        if (interpreter.argument_count() != 1)
+            return interpreter.throw_exception<JS::TypeError>("Expected exactly one argument to parse_cell_name()");
+
+        auto name_value = interpreter.argument(0);
+        if (!name_value.is_string())
+            return interpreter.throw_exception<JS::TypeError>("Expected a String argument to parse_cell_name()");
+
+        auto position = Sheet::parse_cell_name(name_value.as_string().string());
+        if (!position.has_value())
+            return JS::js_undefined();
+
+        auto object = JS::Object::create_empty(interpreter.global_object());
+        object->put("column", JS::js_string(interpreter, position.value().column));
+        object->put("row", JS::Value((unsigned)position.value().row));
+
+        return object;
+    }
+
+private:
+    Sheet& m_sheet;
+};
+
+Sheet::Sheet(const StringView& name)
+    : m_name(name)
+    , m_interpreter(JS::Interpreter::create<SheetGlobalObject>(*this))
+{
+    for (size_t i = 0; i < 20; ++i)
+        add_row();
+
+    for (size_t i = 0; i < 16; ++i)
+        add_column();
+
+    auto file_or_error = Core::File::open("/res/js/Spreadsheet/runtime.js", Core::IODevice::OpenMode::ReadOnly);
+    if (!file_or_error.is_error()) {
+        auto buffer = file_or_error.value()->read_all();
+        evaluate(buffer);
+    }
+}
+
+Sheet::~Sheet()
+{
+}
+
+size_t Sheet::add_row()
+{
+    return m_rows++;
+}
+
+String Sheet::add_column()
+{
+    if (m_current_column_name_length == 0) {
+        m_current_column_name_length = 1;
+        m_columns.append("A");
+        return "A";
+    }
+
+    if (m_current_column_name_length == 1) {
+        auto last_char = m_columns.last()[0];
+        if (last_char == 'Z') {
+            m_current_column_name_length = 2;
+            m_columns.append("AA");
+            return "AA";
+        }
+
+        last_char++;
+        m_columns.append({ &last_char, 1 });
+        return m_columns.last();
+    }
+
+    TODO();
+}
+
+void Sheet::update()
+{
+    m_visited_cells_in_update.clear();
+    for (auto& it : m_cells) {
+        auto& cell = *it.value;
+        if (has_been_visited(&cell))
+            continue;
+        m_visited_cells_in_update.set(&cell);
+        if (cell.dirty) {
+            // Re-evaluate the cell value, if any.
+            cell.update({});
+        }
+    }
+
+    m_visited_cells_in_update.clear();
+}
+
+void Sheet::update(Cell& cell)
+{
+    if (has_been_visited(&cell))
+        return;
+
+    m_visited_cells_in_update.set(&cell);
+    cell.update({});
+}
+
+JS::Value Sheet::evaluate(const StringView& source, Cell* on_behalf_of)
+{
+    TemporaryChange cell_change { m_current_cell_being_evaluated, on_behalf_of };
+
+    auto parser = JS::Parser(JS::Lexer(source));
+    if (parser.has_errors())
+        return JS::js_undefined();
+
+    auto program = parser.parse_program();
+    m_interpreter->run(m_interpreter->global_object(), program);
+    if (m_interpreter->exception()) {
+        auto exc = m_interpreter->exception()->value();
+        m_interpreter->clear_exception();
+        return exc;
+    }
+
+    auto value = m_interpreter->last_value();
+    if (value.is_empty())
+        return JS::js_undefined();
+    return value;
+}
+
+void Cell::update_data()
+{
+    TemporaryChange cell_change { sheet->current_evaluated_cell(), this };
+    if (!dirty)
+        return;
+
+    dirty = false;
+    if (kind == Formula) {
+        if (!evaluated_externally)
+            evaluated_data = sheet->evaluate(data, this);
+    }
+
+    for (auto& ref : referencing_cells) {
+        if (ref) {
+            ref->dirty = true;
+            ref->update();
+        }
+    }
+}
+
+void Cell::update()
+{
+    sheet->update(*this);
+}
+
+JS::Value Cell::js_data()
+{
+    if (dirty)
+        update();
+
+    if (kind == Formula)
+        return evaluated_data;
+
+    return JS::js_string(sheet->interpreter(), data);
+}
+
+Cell* Sheet::at(const StringView& name)
+{
+    auto pos = parse_cell_name(name);
+    if (pos.has_value())
+        return at(pos.value());
+
+    return nullptr;
+}
+
+Cell* Sheet::at(const Position& position)
+{
+    auto it = m_cells.find(position);
+
+    if (it == m_cells.end())
+        return nullptr;
+
+    return it->value;
+}
+
+Optional<Position> Sheet::parse_cell_name(const StringView& name)
+{
+    GenericLexer lexer(name);
+    auto col = lexer.consume_while([](auto c) { return is_alpha(c); });
+    auto row = lexer.consume_while([](auto c) { return is_alphanum(c) && !is_alpha(c); });
+
+    if (!lexer.is_eof() || row.is_empty() || col.is_empty())
+        return {};
+
+    return Position { col, row.to_uint().value() };
+}
+
+String Cell::source() const
+{
+    StringBuilder builder;
+    if (kind == Formula)
+        builder.append('=');
+    builder.append(data);
+    return builder.to_string();
+}
+
+// FIXME: Find a better way to figure out dependencies
+void Cell::reference_from(Cell* other)
+{
+    if (!other || other == this)
+        return;
+
+    if (!referencing_cells.find([other](auto& ptr) { return ptr.ptr() == other; }).is_end())
+        return;
+
+    referencing_cells.append(other->make_weak_ptr());
+}
+
+JsonObject Sheet::to_json() const
+{
+    JsonObject object;
+    object.set("name", m_name);
+
+    auto columns = JsonArray();
+    for (auto& column : m_columns)
+        columns.append(column);
+    object.set("columns", move(columns));
+
+    object.set("rows", m_rows);
+
+    JsonObject cells;
+    for (auto& it : m_cells) {
+        StringBuilder builder;
+        builder.append(it.key.column);
+        builder.appendf("%zu", it.key.row);
+        auto key = builder.to_string();
+
+        JsonObject data;
+        data.set("kind", it.value->kind == Cell::Kind::Formula ? "Formula" : "LiteralString");
+        data.set("value", it.value->data);
+
+        cells.set(key, move(data));
+    }
+    object.set("cells", move(cells));
+
+    return object;
+}
+
+}

+ 211 - 0
Applications/Spreadsheet/Spreadsheet.h

@@ -0,0 +1,211 @@
+/*
+ * 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/HashMap.h>
+#include <AK/HashTable.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/Traits.h>
+#include <AK/Types.h>
+#include <AK/WeakPtr.h>
+#include <AK/Weakable.h>
+#include <LibCore/Object.h>
+#include <LibJS/Interpreter.h>
+
+namespace Spreadsheet {
+
+struct Position {
+    String column;
+    size_t row { 0 };
+
+    bool operator==(const Position& other) const
+    {
+        return row == other.row && column == other.column;
+    }
+
+    bool operator!=(const Position& other) const
+    {
+        return !(other == *this);
+    }
+};
+
+class Sheet;
+
+struct Cell : public Weakable<Cell> {
+    Cell(String data, WeakPtr<Sheet> sheet)
+        : dirty(false)
+        , data(move(data))
+        , kind(LiteralString)
+        , sheet(sheet)
+    {
+    }
+
+    bool dirty { false };
+    bool evaluated_externally { false };
+    String data;
+    JS::Value evaluated_data;
+
+    enum Kind {
+        LiteralString,
+        Formula,
+    } kind { LiteralString };
+
+    WeakPtr<Sheet> sheet;
+    Vector<WeakPtr<Cell>> referencing_cells;
+
+    void reference_from(Cell*);
+
+    void set_data(String new_data)
+    {
+        if (data == new_data)
+            return;
+
+        if (new_data.starts_with("=")) {
+            new_data = new_data.substring(1, new_data.length() - 1);
+            kind = Formula;
+        } else {
+            kind = LiteralString;
+        }
+
+        data = move(new_data);
+        dirty = true;
+        evaluated_externally = false;
+    }
+
+    void set_data(JS::Value new_data)
+    {
+        dirty = true;
+        evaluated_externally = true;
+
+        StringBuilder builder;
+
+        builder.append("=");
+        builder.append(new_data.to_string_without_side_effects());
+        data = builder.build();
+
+        evaluated_data = move(new_data);
+    }
+
+    String source() const;
+
+    JS::Value js_data();
+
+    void update(Badge<Sheet>) { update_data(); }
+    void update();
+
+private:
+    void update_data();
+};
+
+class Sheet : public Core::Object {
+    C_OBJECT(Sheet);
+
+public:
+    ~Sheet();
+
+    static Optional<Position> parse_cell_name(const StringView&);
+
+    JsonObject to_json() const;
+
+    const String& name() const { return m_name; }
+    void set_name(const StringView& name) { m_name = name; }
+
+    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; }
+
+    Cell* at(const Position& position);
+    const Cell* at(const Position& position) const { return const_cast<Sheet*>(this)->at(position); }
+
+    const Cell* at(const StringView& name) const { return const_cast<Sheet*>(this)->at(name); }
+    Cell* at(const StringView&);
+
+    const Cell& ensure(const Position& position) const { return const_cast<Sheet*>(this)->ensure(position); }
+    Cell& ensure(const Position& position)
+    {
+        if (auto cell = at(position))
+            return *cell;
+
+        m_cells.set(position, make<Cell>(String::empty(), make_weak_ptr()));
+        return *at(position);
+    }
+
+    size_t add_row();
+    String add_column();
+
+    size_t row_count() const { return m_rows; }
+    size_t column_count() const { return m_columns.size(); }
+    const Vector<String>& columns() const { return m_columns; }
+    const String& column(size_t index) const
+    {
+        ASSERT(column_count() > index);
+        return m_columns[index];
+    }
+
+    void update();
+    void update(Cell&);
+
+    JS::Value evaluate(const StringView&, Cell* = nullptr);
+    JS::Interpreter& interpreter() { return *m_interpreter; }
+
+    Cell*& current_evaluated_cell() { return m_current_cell_being_evaluated; }
+    bool has_been_visited(Cell* cell) const { return m_visited_cells_in_update.contains(cell); }
+
+private:
+    Sheet(const StringView& name);
+
+    String m_name;
+    Vector<String> m_columns;
+    size_t m_rows { 0 };
+    HashMap<Position, NonnullOwnPtr<Cell>> m_cells;
+    Optional<Position> m_selected_cell; // FIXME: Make this a collection.
+
+    Cell* m_current_cell_being_evaluated { nullptr };
+
+    size_t m_current_column_name_length { 0 };
+
+    NonnullOwnPtr<JS::Interpreter> m_interpreter;
+    HashTable<Cell*> m_visited_cells_in_update;
+};
+
+}
+
+namespace AK {
+
+template<>
+struct Traits<Spreadsheet::Position> : public GenericTraits<Spreadsheet::Position> {
+    static constexpr bool is_trivial() { return false; }
+    static unsigned hash(const Spreadsheet::Position& p)
+    {
+        return pair_int_hash(
+            string_hash(p.column.characters(), p.column.length()),
+            u64_hash(p.row));
+    }
+};
+
+}

+ 104 - 0
Applications/Spreadsheet/SpreadsheetModel.cpp

@@ -0,0 +1,104 @@
+/*
+ * 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 "SpreadsheetModel.h"
+
+namespace Spreadsheet {
+
+SheetModel::~SheetModel()
+{
+}
+
+GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+    if (!index.is_valid())
+        return {};
+
+    if (role == GUI::ModelRole::Display) {
+        if (index.column() == 0)
+            return String::number(index.row());
+
+        const auto* value = m_sheet->at({ m_sheet->column(index.column() - 1), (size_t)index.row() });
+        if (!value)
+            return String::empty();
+
+        if (value->kind == Spreadsheet::Cell::Formula)
+            return value->evaluated_data.is_empty() ? "" : value->evaluated_data.to_string_without_side_effects();
+
+        return value->data;
+    }
+
+    if (role == GUI::ModelRole::TextAlignment) {
+        if (index.column() == 0)
+            return {};
+
+        return {};
+    }
+
+    return {};
+}
+
+String SheetModel::column_name(int index) const
+{
+    if (index < 0)
+        return {};
+
+    if (index == 0)
+        return "";
+
+    return m_sheet->column(index - 1);
+}
+
+bool SheetModel::is_editable(const GUI::ModelIndex& index) const
+{
+    if (!index.is_valid())
+        return false;
+
+    if (index.column() == 0)
+        return false;
+
+    return true;
+}
+
+void SheetModel::set_data(const GUI::ModelIndex& index, const GUI::Variant& value)
+{
+    if (!index.is_valid())
+        return;
+
+    if (index.column() == 0)
+        return;
+
+    auto& cell = m_sheet->ensure({ m_sheet->column(index.column() - 1), (size_t)index.row() });
+    cell.set_data(value.to_string());
+    update();
+}
+
+void SheetModel::update()
+{
+    m_sheet->update();
+}
+
+}

+ 56 - 0
Applications/Spreadsheet/SpreadsheetModel.h

@@ -0,0 +1,56 @@
+/*
+ * 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 "Spreadsheet.h"
+#include <LibGUI/Model.h>
+
+namespace Spreadsheet {
+
+class SheetModel final : public GUI::Model {
+public:
+    static NonnullRefPtr<SheetModel> create(Sheet& sheet) { return adopt(*new SheetModel(sheet)); }
+    virtual ~SheetModel() override;
+
+    virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_sheet->row_count(); }
+    virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_sheet->column_count() + 1; }
+    virtual String column_name(int) const override;
+    virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+    virtual bool is_editable(const GUI::ModelIndex&) const override;
+    virtual void set_data(const GUI::ModelIndex&, const GUI::Variant&) override;
+    virtual void update() override;
+
+private:
+    explicit SheetModel(Sheet& sheet)
+        : m_sheet(sheet)
+    {
+    }
+
+    NonnullRefPtr<Sheet> m_sheet;
+};
+
+}

+ 89 - 0
Applications/Spreadsheet/SpreadsheetView.cpp

@@ -0,0 +1,89 @@
+/*
+ * 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 "SpreadsheetView.h"
+#include "SpreadsheetModel.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/ModelEditingDelegate.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/TableView.h>
+#include <LibGfx/Palette.h>
+
+namespace Spreadsheet {
+
+SpreadsheetView::~SpreadsheetView()
+{
+}
+
+void SpreadsheetView::EditingDelegate::set_value(const GUI::Variant& value)
+{
+    if (m_has_set_initial_value)
+        return StringModelEditingDelegate::set_value(value);
+
+    m_has_set_initial_value = true;
+    const auto option = m_sheet.at({ m_sheet.column(index().column() - 1), (size_t)index().row() });
+    if (option)
+        return StringModelEditingDelegate::set_value(option->source());
+
+    StringModelEditingDelegate::set_value("");
+}
+
+SpreadsheetView::SpreadsheetView(Sheet& sheet)
+    : m_sheet(sheet)
+{
+    set_layout<GUI::VerticalBoxLayout>().set_margins({ 2, 2, 2, 2 });
+    m_table_view = add<GUI::TableView>();
+    m_table_view->set_model(SheetModel::create(*m_sheet));
+
+    // FIXME: This is dumb.
+    for (size_t i = 0; i < m_sheet->column_count(); ++i) {
+        m_table_view->set_cell_painting_delegate(i + 1, make<TableCellPainter>(*m_table_view));
+        m_table_view->set_column_width(i + 1, 50);
+        m_table_view->set_column_header_alignment(i + 1, Gfx::TextAlignment::Center);
+    }
+
+    m_table_view->set_alternating_row_colors(false);
+    m_table_view->set_highlight_selected_rows(false);
+    m_table_view->set_editable(true);
+    m_table_view->aid_create_editing_delegate = [&](auto&) {
+        return make<EditingDelegate>(*m_sheet);
+    };
+}
+
+void SpreadsheetView::TableCellPainter::paint(GUI::Painter& painter, const Gfx::IntRect& rect, const Gfx::Palette& palette, const GUI::ModelIndex& index)
+{
+    // Draw a border.
+    // Undo the horizontal padding done by the table view...
+    painter.draw_rect(rect.inflated(m_table_view.horizontal_padding() * 2, 0), palette.ruler());
+    if (m_table_view.selection().contains(index))
+        painter.draw_rect(rect.inflated(m_table_view.horizontal_padding() * 2 + 1, 1), palette.ruler_border());
+
+    auto data = index.data();
+    auto text_alignment = index.data(GUI::ModelRole::TextAlignment).to_text_alignment(Gfx::TextAlignment::CenterLeft);
+    painter.draw_text(rect, data.to_string(), m_table_view.font_for_index(index), text_alignment, palette.color(m_table_view.foreground_role()), Gfx::TextElision::Right);
+}
+
+}

+ 81 - 0
Applications/Spreadsheet/SpreadsheetView.h

@@ -0,0 +1,81 @@
+/*
+ * 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 "Spreadsheet.h"
+#include <LibGUI/AbstractTableView.h>
+#include <LibGUI/ModelEditingDelegate.h>
+#include <LibGUI/Widget.h>
+#include <string.h>
+
+namespace Spreadsheet {
+
+class SpreadsheetView final : public GUI::Widget {
+    C_OBJECT(SpreadsheetView);
+
+public:
+    ~SpreadsheetView();
+
+    const Sheet& sheet() const { return *m_sheet; }
+
+private:
+    SpreadsheetView(Sheet&);
+
+    class EditingDelegate : public GUI::StringModelEditingDelegate {
+    public:
+        EditingDelegate(const Sheet& sheet)
+            : m_sheet(sheet)
+        {
+        }
+        virtual void set_value(const GUI::Variant& value) override;
+
+    private:
+        bool m_has_set_initial_value { false };
+        const Sheet& m_sheet;
+    };
+
+    class TableCellPainter final : public GUI::TableCellPaintingDelegate {
+    public:
+        TableCellPainter(const GUI::TableView& view)
+            : m_table_view(view)
+        {
+        }
+        void paint(GUI::Painter&, const Gfx::IntRect&, const Gfx::Palette&, const GUI::ModelIndex&) override;
+
+    private:
+        const GUI::TableView& m_table_view;
+    };
+
+    NonnullRefPtr<Sheet> m_sheet;
+    RefPtr<GUI::TableView> m_table_view;
+};
+
+}
+
+AK_BEGIN_TYPE_TRAITS(Spreadsheet::SpreadsheetView)
+static bool is_type(const Core::Object& object) { return !strcmp(object.class_name(), "SpreadsheetView"); }
+AK_END_TYPE_TRAITS()

+ 89 - 0
Applications/Spreadsheet/SpreadsheetWidget.cpp

@@ -0,0 +1,89 @@
+/*
+ * 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 "SpreadsheetWidget.h"
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonObjectSerializer.h>
+#include <LibCore/File.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/TabWidget.h>
+#include <string.h>
+
+namespace Spreadsheet {
+
+SpreadsheetWidget::SpreadsheetWidget()
+{
+    set_fill_with_background_color(true);
+    set_layout<GUI::VerticalBoxLayout>().set_margins({ 2, 2, 2, 2 });
+    m_tab_widget = add<GUI::TabWidget>();
+    m_tab_widget->set_tab_position(GUI::TabWidget::TabPosition::Bottom);
+
+    m_sheets.append(Sheet::construct("Sheet 1"));
+    m_tab_widget->add_tab<SpreadsheetView>(m_sheets.first().name(), m_sheets.first());
+}
+
+SpreadsheetWidget::~SpreadsheetWidget()
+{
+}
+
+void SpreadsheetWidget::save(const StringView& filename)
+{
+    JsonArray array;
+    m_tab_widget->for_each_child_of_type<SpreadsheetView>([&](auto& view) {
+        array.append(view.sheet().to_json());
+        return IterationDecision::Continue;
+    });
+
+    auto file_content = array.to_string();
+
+    auto file = Core::File::construct(filename);
+    file->open(Core::IODevice::WriteOnly);
+    if (!file->is_open()) {
+        StringBuilder sb;
+        sb.append("Failed to open ");
+        sb.append(filename);
+        sb.append(" for write. Error: ");
+        sb.append(file->error_string());
+
+        GUI::MessageBox::show(window(), sb.to_string(), "Error", GUI::MessageBox::Type::Error);
+        return;
+    }
+
+    bool result = file->write(file_content);
+    if (!result) {
+        int error_number = errno;
+        StringBuilder sb;
+        sb.append("Unable to save file. Error: ");
+        sb.append(strerror(error_number));
+
+        GUI::MessageBox::show(window(), sb.to_string(), "Error", GUI::MessageBox::Type::Error);
+        return;
+    }
+}
+
+}

+ 50 - 0
Applications/Spreadsheet/SpreadsheetWidget.h

@@ -0,0 +1,50 @@
+/*
+ * 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 "SpreadsheetView.h"
+#include <AK/NonnullRefPtrVector.h>
+#include <LibGUI/Widget.h>
+
+namespace Spreadsheet {
+
+class SpreadsheetWidget final : public GUI::Widget {
+    C_OBJECT(SpreadsheetWidget);
+
+public:
+    ~SpreadsheetWidget();
+
+    void save(const StringView& filename);
+
+private:
+    SpreadsheetWidget();
+
+    NonnullRefPtrVector<Sheet> m_sheets;
+    RefPtr<GUI::TabWidget> m_tab_widget;
+};
+
+}

+ 65 - 0
Applications/Spreadsheet/main.cpp

@@ -0,0 +1,65 @@
+/*
+ * 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 "Spreadsheet.h"
+#include "SpreadsheetWidget.h"
+#include <LibCore/ArgsParser.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Forward.h>
+#include <LibGUI/Window.h>
+
+int main(int argc, char* argv[])
+{
+    Core::ArgsParser args_parser;
+    args_parser.parse(argc, argv);
+
+    auto app = GUI::Application::construct(argc, argv);
+
+    if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix", nullptr) < 0) {
+        perror("pledge");
+        return 1;
+    }
+
+    if (unveil("/res", "r") < 0) {
+        perror("unveil");
+        return 1;
+    }
+
+    if (unveil(nullptr, nullptr) < 0) {
+        perror("unveil");
+        return 1;
+    }
+
+    auto window = GUI::Window::construct();
+    window->set_title("Spreadsheet");
+    window->resize(640, 480);
+
+    window->set_main_widget<Spreadsheet::SpreadsheetWidget>();
+
+    window->show();
+
+    return app->exec();
+}

+ 93 - 0
Base/res/js/Spreadsheet/runtime.js

@@ -0,0 +1,93 @@
+const sheet = this
+
+function range(start, end, column_step, row_step) {
+    column_step = integer(column_step ?? 1)
+    row_step = integer(row_step ?? 1)
+    start = sheet.parse_cell_name(start) ?? {column: 'A', row: 0}
+    end = sheet.parse_cell_name(end) ?? start
+
+    if (end.column.length > 1 || start.column.length > 1)
+        throw new TypeError("Only single-letter column names are allowed (TODO)");
+
+    const cells = []
+
+    for (let col = Math.min(start.column.charCodeAt(0), end.column.charCodeAt(0));
+        col <= Math.max(start.column.charCodeAt(0), end.column.charCodeAt(0));
+        ++col) {
+        for (let row = Math.min(start.row, end.row);
+            row <= Math.max(start.row, end.row);
+            ++row) {
+
+            cells.push(String.fromCharCode(col) + row)
+        }
+    }
+
+    return cells
+}
+
+// FIXME: Remove this and use String.split() eventually
+function split(str, sep) {
+    const parts = []
+    let split_index = -1
+    for(;;) {
+        split_index = str.indexOf(sep)
+        if (split_index == -1) {
+            if (str.length)
+                parts.push(str)
+            return parts
+        }
+        parts.push(str.substring(0, split_index))
+        str = str.slice(split_index + sep.length)
+    }
+}
+
+function R(fmt, ...args) {
+    if (args.length !== 0)
+        throw new TypeError("R`` format must be literal")
+
+    fmt = fmt[0]
+    return range(...split(fmt, ':'))
+}
+
+function select(criteria, t, f) {
+    if (criteria)
+        return t;
+    return f;
+}
+
+function sumif(condition, cells) {
+    let sum = null
+    for (let name of cells) {
+        let cell = sheet[name]
+        if (condition(cell))
+            sum = sum === null ? cell : sum + cell
+    }
+    return sum
+}
+
+function countif(condition, cells) {
+    let count = 0
+    for (let name of cells) {
+        let cell = sheet[name]
+        if (condition(cell))
+            count++
+    }
+    return count
+}
+
+function now() {
+    return new Date()
+}
+
+function repeat(count, str) {
+    return Array(count + 1).join(str)
+}
+
+function randrange(min, max) {
+    return Math.random() * (max - min) + min
+}
+
+function integer(value) {
+    return value | 0
+}
+

+ 7 - 5
Libraries/LibGUI/ModelEditingDelegate.h

@@ -34,7 +34,7 @@ namespace GUI {
 
 class ModelEditingDelegate {
 public:
-    virtual ~ModelEditingDelegate() {}
+    virtual ~ModelEditingDelegate() { }
 
     void bind(Model& model, const ModelIndex& index)
     {
@@ -53,10 +53,10 @@ public:
     virtual Variant value() const = 0;
     virtual void set_value(const Variant&) = 0;
 
-    virtual void will_begin_editing() {}
+    virtual void will_begin_editing() { }
 
 protected:
-    ModelEditingDelegate() {}
+    ModelEditingDelegate() { }
 
     virtual RefPtr<Widget> create_widget() = 0;
     void commit()
@@ -65,6 +65,8 @@ protected:
             on_commit();
     }
 
+    const ModelIndex& index() const { return m_index; }
+
 private:
     RefPtr<Model> m_model;
     ModelIndex m_index;
@@ -73,8 +75,8 @@ private:
 
 class StringModelEditingDelegate : public ModelEditingDelegate {
 public:
-    StringModelEditingDelegate() {}
-    virtual ~StringModelEditingDelegate() override {}
+    StringModelEditingDelegate() { }
+    virtual ~StringModelEditingDelegate() override { }
 
     virtual RefPtr<Widget> create_widget() override
     {