diff --git a/Applications/CMakeLists.txt b/Applications/CMakeLists.txt index 8c9877e73b..8cfdf4bda9 100644 --- a/Applications/CMakeLists.txt +++ b/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) diff --git a/Applications/Spreadsheet/CMakeLists.txt b/Applications/Spreadsheet/CMakeLists.txt new file mode 100644 index 0000000000..23503e4ead --- /dev/null +++ b/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) diff --git a/Applications/Spreadsheet/Spreadsheet.cpp b/Applications/Spreadsheet/Spreadsheet.cpp new file mode 100644 index 0000000000..40c92ac341 --- /dev/null +++ b/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 +#include +#include +#include +#include +#include +#include +#include + +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("Expected exactly one argument to parse_cell_name()"); + + auto name_value = interpreter.argument(0); + if (!name_value.is_string()) + return interpreter.throw_exception("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(*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 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; +} + +} diff --git a/Applications/Spreadsheet/Spreadsheet.h b/Applications/Spreadsheet/Spreadsheet.h new file mode 100644 index 0000000000..57292f4227 --- /dev/null +++ b/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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(String data, WeakPtr 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; + Vector> 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) { update_data(); } + void update(); + +private: + void update_data(); +}; + +class Sheet : public Core::Object { + C_OBJECT(Sheet); + +public: + ~Sheet(); + + static Optional 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 selected_cell() const { return m_selected_cell; } + const HashMap>& cells() const { return m_cells; } + HashMap>& cells() { return m_cells; } + + Cell* at(const Position& position); + const Cell* at(const Position& position) const { return const_cast(this)->at(position); } + + const Cell* at(const StringView& name) const { return const_cast(this)->at(name); } + Cell* at(const StringView&); + + const Cell& ensure(const Position& position) const { return const_cast(this)->ensure(position); } + Cell& ensure(const Position& position) + { + if (auto cell = at(position)) + return *cell; + + m_cells.set(position, make(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& 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 m_columns; + size_t m_rows { 0 }; + HashMap> m_cells; + Optional m_selected_cell; // FIXME: Make this a collection. + + Cell* m_current_cell_being_evaluated { nullptr }; + + size_t m_current_column_name_length { 0 }; + + NonnullOwnPtr m_interpreter; + HashTable m_visited_cells_in_update; +}; + +} + +namespace AK { + +template<> +struct Traits : public GenericTraits { + 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)); + } +}; + +} diff --git a/Applications/Spreadsheet/SpreadsheetModel.cpp b/Applications/Spreadsheet/SpreadsheetModel.cpp new file mode 100644 index 0000000000..0f66eac289 --- /dev/null +++ b/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(); +} + +} diff --git a/Applications/Spreadsheet/SpreadsheetModel.h b/Applications/Spreadsheet/SpreadsheetModel.h new file mode 100644 index 0000000000..b098b8cc8e --- /dev/null +++ b/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 + +namespace Spreadsheet { + +class SheetModel final : public GUI::Model { +public: + static NonnullRefPtr 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 m_sheet; +}; + +} diff --git a/Applications/Spreadsheet/SpreadsheetView.cpp b/Applications/Spreadsheet/SpreadsheetView.cpp new file mode 100644 index 0000000000..42ac1422a7 --- /dev/null +++ b/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 +#include +#include +#include +#include + +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().set_margins({ 2, 2, 2, 2 }); + m_table_view = add(); + 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(*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(*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); +} + +} diff --git a/Applications/Spreadsheet/SpreadsheetView.h b/Applications/Spreadsheet/SpreadsheetView.h new file mode 100644 index 0000000000..eecfe67bb3 --- /dev/null +++ b/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 +#include +#include +#include + +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 m_sheet; + RefPtr 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() diff --git a/Applications/Spreadsheet/SpreadsheetWidget.cpp b/Applications/Spreadsheet/SpreadsheetWidget.cpp new file mode 100644 index 0000000000..da1cc3bae1 --- /dev/null +++ b/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 +#include +#include +#include +#include +#include +#include +#include + +namespace Spreadsheet { + +SpreadsheetWidget::SpreadsheetWidget() +{ + set_fill_with_background_color(true); + set_layout().set_margins({ 2, 2, 2, 2 }); + m_tab_widget = add(); + m_tab_widget->set_tab_position(GUI::TabWidget::TabPosition::Bottom); + + m_sheets.append(Sheet::construct("Sheet 1")); + m_tab_widget->add_tab(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([&](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; + } +} + +} diff --git a/Applications/Spreadsheet/SpreadsheetWidget.h b/Applications/Spreadsheet/SpreadsheetWidget.h new file mode 100644 index 0000000000..aa14c0a8e2 --- /dev/null +++ b/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 +#include + +namespace Spreadsheet { + +class SpreadsheetWidget final : public GUI::Widget { + C_OBJECT(SpreadsheetWidget); + +public: + ~SpreadsheetWidget(); + + void save(const StringView& filename); + +private: + SpreadsheetWidget(); + + NonnullRefPtrVector m_sheets; + RefPtr m_tab_widget; +}; + +} diff --git a/Applications/Spreadsheet/main.cpp b/Applications/Spreadsheet/main.cpp new file mode 100644 index 0000000000..c94fdfd281 --- /dev/null +++ b/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 +#include +#include +#include + +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(); + + window->show(); + + return app->exec(); +} diff --git a/Base/res/js/Spreadsheet/runtime.js b/Base/res/js/Spreadsheet/runtime.js new file mode 100644 index 0000000000..082ab027b0 --- /dev/null +++ b/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 +} + diff --git a/Libraries/LibGUI/ModelEditingDelegate.h b/Libraries/LibGUI/ModelEditingDelegate.h index f13b5cc979..4d270f64a9 100644 --- a/Libraries/LibGUI/ModelEditingDelegate.h +++ b/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 create_widget() = 0; void commit() @@ -65,6 +65,8 @@ protected: on_commit(); } + const ModelIndex& index() const { return m_index; } + private: RefPtr 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 create_widget() override {