From fdf06600643a7bce1f00182ef7fe31310ea4e6b2 Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Tue, 25 Aug 2020 00:53:22 +0430 Subject: [PATCH] Spreadsheet: Implement state-preserving saves and loads --- Applications/Spreadsheet/Spreadsheet.cpp | 75 +++++++++++- Applications/Spreadsheet/Spreadsheet.h | 17 ++- .../Spreadsheet/SpreadsheetWidget.cpp | 109 +++++++++++++++--- Applications/Spreadsheet/SpreadsheetWidget.h | 7 +- Applications/Spreadsheet/main.cpp | 29 ++++- 5 files changed, 215 insertions(+), 22 deletions(-) diff --git a/Applications/Spreadsheet/Spreadsheet.cpp b/Applications/Spreadsheet/Spreadsheet.cpp index 2e3e0c71dd..e255f3a6dc 100644 --- a/Applications/Spreadsheet/Spreadsheet.cpp +++ b/Applications/Spreadsheet/Spreadsheet.cpp @@ -111,15 +111,20 @@ private: }; Sheet::Sheet(const StringView& name) - : m_name(name) - , m_interpreter(JS::Interpreter::create(*this)) + : Sheet(EmptyConstruct::EmptyConstructTag) { + m_name = name; + for (size_t i = 0; i < 20; ++i) add_row(); for (size_t i = 0; i < 16; ++i) add_column(); +} +Sheet::Sheet(EmptyConstruct) + : m_interpreter(JS::Interpreter::create(*this)) +{ 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(); @@ -301,6 +306,61 @@ void Cell::reference_from(Cell* other) referencing_cells.append(other->make_weak_ptr()); } +RefPtr Sheet::from_json(const JsonObject& object) +{ + auto sheet = adopt(*new Sheet(EmptyConstruct::EmptyConstructTag)); + auto rows = object.get("rows").to_u32(20); + auto columns = object.get("columns"); + if (!columns.is_array()) + return nullptr; + auto name = object.get("name").as_string_or("Sheet"); + + sheet->set_name(name); + + for (size_t i = 0; i < rows; ++i) + sheet->add_row(); + + // FIXME: Better error checking. + columns.as_array().for_each([&](auto& value) { + sheet->m_columns.append(value.as_string()); + return IterationDecision::Continue; + }); + + auto cells = object.get("cells").as_object(); + auto json = sheet->interpreter().global_object().get("JSON"); + auto& parse_function = json.as_object().get("parse").as_function(); + + cells.for_each_member([&](auto& name, JsonValue& value) { + auto position_option = parse_cell_name(name); + if (!position_option.has_value()) + return IterationDecision::Continue; + + auto position = position_option.value(); + auto& obj = value.as_object(); + auto kind = obj.get("kind").as_string_or("LiteralString") == "LiteralString" ? Cell::LiteralString : Cell::Formula; + + OwnPtr cell; + switch (kind) { + case Cell::LiteralString: + cell = make(obj.get("value").to_string(), sheet->make_weak_ptr()); + break; + case Cell::Formula: { + auto& interpreter = sheet->interpreter(); + JS::MarkedValueList args { interpreter.heap() }; + args.append(JS::js_string(interpreter, obj.get("value").as_string())); + auto value = interpreter.call(parse_function, json, move(args)); + cell = make(obj.get("source").to_string(), move(value), sheet->make_weak_ptr()); + break; + } + } + + sheet->m_cells.set(position, cell.release_nonnull()); + return IterationDecision::Continue; + }); + + return sheet; +} + JsonObject Sheet::to_json() const { JsonObject object; @@ -322,7 +382,16 @@ JsonObject Sheet::to_json() const JsonObject data; data.set("kind", it.value->kind == Cell::Kind::Formula ? "Formula" : "LiteralString"); - data.set("value", it.value->data); + if (it.value->kind == Cell::Formula) { + data.set("source", it.value->data); + auto json = m_interpreter->global_object().get("JSON"); + JS::MarkedValueList args(m_interpreter->heap()); + args.append(it.value->evaluated_data); + auto stringified = m_interpreter->call(json.as_object().get("stringify").as_function(), json, move(args)); + data.set("value", stringified.to_string_without_side_effects()); + } else { + data.set("value", it.value->data); + } cells.set(key, move(data)); } diff --git a/Applications/Spreadsheet/Spreadsheet.h b/Applications/Spreadsheet/Spreadsheet.h index 41585b54ca..46c5ab040e 100644 --- a/Applications/Spreadsheet/Spreadsheet.h +++ b/Applications/Spreadsheet/Spreadsheet.h @@ -65,6 +65,15 @@ struct Cell : public Weakable { { } + Cell(String source, JS::Value&& cell_value, WeakPtr sheet) + : dirty(false) + , data(move(source)) + , evaluated_data(move(cell_value)) + , kind(Formula) + , sheet(sheet) + { + } + bool dirty { false }; bool evaluated_externally { false }; String data; @@ -131,6 +140,7 @@ public: static Optional parse_cell_name(const StringView&); JsonObject to_json() const; + static RefPtr from_json(const JsonObject&); const String& name() const { return m_name; } void set_name(const StringView& name) { m_name = name; } @@ -179,7 +189,10 @@ public: bool has_been_visited(Cell* cell) const { return m_visited_cells_in_update.contains(cell); } private: - Sheet(const StringView& name); + enum class EmptyConstruct { EmptyConstructTag }; + + explicit Sheet(EmptyConstruct); + explicit Sheet(const StringView& name); String m_name; Vector m_columns; @@ -191,7 +204,7 @@ private: size_t m_current_column_name_length { 0 }; - NonnullOwnPtr m_interpreter; + mutable NonnullOwnPtr m_interpreter; HashTable m_visited_cells_in_update; }; diff --git a/Applications/Spreadsheet/SpreadsheetWidget.cpp b/Applications/Spreadsheet/SpreadsheetWidget.cpp index c747fb2e63..4294462cfb 100644 --- a/Applications/Spreadsheet/SpreadsheetWidget.cpp +++ b/Applications/Spreadsheet/SpreadsheetWidget.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,7 +43,8 @@ namespace Spreadsheet { -SpreadsheetWidget::SpreadsheetWidget() +SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector&& sheets, bool should_add_sheet_if_empty) + : m_sheets(move(sheets)) { set_fill_with_background_color(true); set_layout().set_margins({ 2, 2, 2, 2 }); @@ -76,8 +78,23 @@ SpreadsheetWidget::SpreadsheetWidget() m_tab_widget = container.add(); m_tab_widget->set_tab_position(GUI::TabWidget::TabPosition::Bottom); - m_sheets.append(Sheet::construct("Sheet 1")); - auto& tab = m_tab_widget->add_tab(m_sheets.first().name(), m_sheets.first()); + m_cell_value_editor = cell_value_editor; + m_current_cell_label = current_cell_label; + + if (m_sheets.is_empty() && should_add_sheet_if_empty) + m_sheets.append(Sheet::construct("Sheet 1")); + + setup_tabs(); +} + +void SpreadsheetWidget::setup_tabs() +{ + RefPtr first_tab_widget; + for (auto& sheet : m_sheets) { + auto& tab = m_tab_widget->add_tab(sheet.name(), sheet); + if (!first_tab_widget) + first_tab_widget = &tab; + } auto change = [&](auto& selected_widget) { if (m_selected_view) { @@ -89,26 +106,28 @@ SpreadsheetWidget::SpreadsheetWidget() StringBuilder builder; builder.append(position.column); builder.appendf("%zu", position.row); - current_cell_label.set_enabled(true); - current_cell_label.set_text(builder.string_view()); + m_current_cell_label->set_enabled(true); + m_current_cell_label->set_text(builder.string_view()); - cell_value_editor.on_change = nullptr; - cell_value_editor.set_text(cell.source()); - cell_value_editor.on_change = [&] { - cell.set_data(cell_value_editor.text()); + m_cell_value_editor->on_change = nullptr; + m_cell_value_editor->set_text(cell.source()); + m_cell_value_editor->on_change = [&] { + cell.set_data(m_cell_value_editor->text()); m_selected_view->sheet().update(); }; - cell_value_editor.set_enabled(true); + m_cell_value_editor->set_enabled(true); }; m_selected_view->on_selection_dropped = [&]() { - cell_value_editor.set_enabled(false); - cell_value_editor.set_text(""); - current_cell_label.set_enabled(false); - current_cell_label.set_text(""); + m_cell_value_editor->set_enabled(false); + m_cell_value_editor->set_text(""); + m_current_cell_label->set_enabled(false); + m_current_cell_label->set_text(""); }; }; - change(tab); + if (first_tab_widget) + change(*first_tab_widget); + m_tab_widget->on_change = [change = move(change)](auto& selected_widget) { change(selected_widget); }; @@ -118,6 +137,66 @@ SpreadsheetWidget::~SpreadsheetWidget() { } +void SpreadsheetWidget::load(const StringView& filename) +{ + auto file_or_error = Core::File::open(filename, Core::IODevice::OpenMode::ReadOnly); + if (file_or_error.is_error()) { + StringBuilder sb; + sb.append("Failed to open "); + sb.append(filename); + sb.append(" for reading. Error: "); + sb.append(file_or_error.error()); + + GUI::MessageBox::show(window(), sb.to_string(), "Error", GUI::MessageBox::Type::Error); + return; + } + + auto json_value_option = JsonParser(file_or_error.value()->read_all()).parse(); + if (!json_value_option.has_value()) { + StringBuilder sb; + sb.append("Failed to parse "); + sb.append(filename); + + GUI::MessageBox::show(window(), sb.to_string(), "Error", GUI::MessageBox::Type::Error); + return; + } + + auto& json_value = json_value_option.value(); + if (!json_value.is_array()) { + StringBuilder sb; + sb.append("Did not find a spreadsheet in "); + sb.append(filename); + + GUI::MessageBox::show(window(), sb.to_string(), "Error", GUI::MessageBox::Type::Error); + return; + } + + NonnullRefPtrVector sheets; + + auto& json_array = json_value.as_array(); + json_array.for_each([&](auto& sheet_json) { + if (!sheet_json.is_object()) + return IterationDecision::Continue; + + auto sheet = Sheet::from_json(sheet_json.as_object()); + if (!sheet) + return IterationDecision::Continue; + + sheets.append(sheet.release_nonnull()); + + return IterationDecision::Continue; + }); + + m_sheets.clear(); + m_sheets = move(sheets); + + while (auto* widget = m_tab_widget->active_widget()) { + m_tab_widget->remove_tab(*widget); + } + + setup_tabs(); +} + void SpreadsheetWidget::save(const StringView& filename) { JsonArray array; diff --git a/Applications/Spreadsheet/SpreadsheetWidget.h b/Applications/Spreadsheet/SpreadsheetWidget.h index 22c2730883..0f4eaa19bf 100644 --- a/Applications/Spreadsheet/SpreadsheetWidget.h +++ b/Applications/Spreadsheet/SpreadsheetWidget.h @@ -39,13 +39,18 @@ public: ~SpreadsheetWidget(); void save(const StringView& filename); + void load(const StringView& filename); private: - SpreadsheetWidget(); + explicit SpreadsheetWidget(NonnullRefPtrVector&& sheets = {}, bool should_add_sheet_if_empty = true); + + void setup_tabs(); NonnullRefPtrVector m_sheets; SpreadsheetView* m_selected_view { nullptr }; RefPtr m_tab_widget; + RefPtr m_current_cell_label; + RefPtr m_cell_value_editor; }; } diff --git a/Applications/Spreadsheet/main.cpp b/Applications/Spreadsheet/main.cpp index dbfb18317a..b0c7eaa393 100644 --- a/Applications/Spreadsheet/main.cpp +++ b/Applications/Spreadsheet/main.cpp @@ -27,6 +27,7 @@ #include "Spreadsheet.h" #include "SpreadsheetWidget.h" #include +#include #include #include #include @@ -36,9 +37,20 @@ int main(int argc, char* argv[]) { + const char* filename = nullptr; + Core::ArgsParser args_parser; + args_parser.add_positional_argument(filename, "File to read from", "file", Core::ArgsParser::Required::No); + args_parser.parse(argc, argv); + if (filename) { + if (!Core::File::exists(filename) || Core::File::is_directory(filename)) { + fprintf(stderr, "File does not exist or is a directory: %s\n", filename); + return 1; + } + } + auto app = GUI::Application::construct(argc, argv); if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix", nullptr) < 0) { @@ -56,6 +68,11 @@ int main(int argc, char* argv[]) return 1; } + if (unveil(Core::StandardPaths::home_directory().characters(), "rwc") < 0) { + perror("unveil"); + return 1; + } + if (unveil("/res", "r") < 0) { perror("unveil"); return 1; @@ -70,7 +87,10 @@ int main(int argc, char* argv[]) window->set_title("Spreadsheet"); window->resize(640, 480); - auto& spreadsheet_widget = window->set_main_widget(); + auto& spreadsheet_widget = window->set_main_widget(NonnullRefPtrVector {}, filename == nullptr); + + if (filename) + spreadsheet_widget.load(filename); auto menubar = GUI::MenuBar::construct(); auto& app_menu = menubar->add_menu("Spreadsheet"); @@ -80,6 +100,13 @@ int main(int argc, char* argv[]) })); auto& file_menu = menubar->add_menu("File"); + file_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) { + Optional load_path = GUI::FilePicker::get_open_filepath(window); + if (!load_path.has_value()) + return; + + spreadsheet_widget.load(load_path.value()); + })); file_menu.add_action(GUI::CommonActions::make_save_action([&](auto&) { String name = "sheet"; Optional save_path = GUI::FilePicker::get_save_filepath(window, name, "json");