mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 22:02:44 +00:00 
			
		
		
		
	Spreadsheet: Add support for multiple sheets
This also refactors the js integration stuff to allow sheets to reference each other safely.
This commit is contained in:
		
							parent
							
								
									e1f5f709ee
								
							
						
					
					
						commit
						cb7fe4fe7c
					
				
					 11 changed files with 365 additions and 126 deletions
				
			
		|  | @ -1,6 +1,7 @@ | |||
| set(SOURCES | ||||
|     CellSyntaxHighlighter.cpp | ||||
|     HelpWindow.cpp | ||||
|     JSIntegration.cpp | ||||
|     Spreadsheet.cpp | ||||
|     SpreadsheetModel.cpp | ||||
|     SpreadsheetView.cpp | ||||
|  |  | |||
							
								
								
									
										157
									
								
								Applications/Spreadsheet/JSIntegration.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								Applications/Spreadsheet/JSIntegration.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,157 @@ | |||
| /*
 | ||||
|  * 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 "JSIntegration.h" | ||||
| #include "Spreadsheet.h" | ||||
| #include "Workbook.h" | ||||
| #include <LibJS/Runtime/Error.h> | ||||
| #include <LibJS/Runtime/GlobalObject.h> | ||||
| #include <LibJS/Runtime/Object.h> | ||||
| #include <LibJS/Runtime/Value.h> | ||||
| 
 | ||||
| namespace Spreadsheet { | ||||
| 
 | ||||
| SheetGlobalObject::SheetGlobalObject(Sheet& sheet) | ||||
|     : m_sheet(sheet) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| SheetGlobalObject::~SheetGlobalObject() | ||||
| { | ||||
| } | ||||
| 
 | ||||
| JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receiver) const | ||||
| { | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| bool SheetGlobalObject::put(const JS::PropertyName& name, JS::Value value, JS::Value receiver) | ||||
| { | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| void SheetGlobalObject::initialize() | ||||
| { | ||||
|     GlobalObject::initialize(); | ||||
|     define_native_function("parse_cell_name", parse_cell_name, 1); | ||||
| } | ||||
| 
 | ||||
| JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::parse_cell_name) | ||||
| { | ||||
|     if (interpreter.argument_count() != 1) { | ||||
|         interpreter.throw_exception<JS::TypeError>("Expected exactly one argument to parse_cell_name()"); | ||||
|         return {}; | ||||
|     } | ||||
|     auto name_value = interpreter.argument(0); | ||||
|     if (!name_value.is_string()) { | ||||
|         interpreter.throw_exception<JS::TypeError>("Expected a String argument to parse_cell_name()"); | ||||
|         return {}; | ||||
|     } | ||||
|     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; | ||||
| } | ||||
| 
 | ||||
| WorkbookObject::WorkbookObject(Workbook& workbook) | ||||
|     : JS::Object(*JS::Object::create_empty(workbook.global_object())) | ||||
|     , m_workbook(workbook) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| WorkbookObject::~WorkbookObject() | ||||
| { | ||||
| } | ||||
| 
 | ||||
| void WorkbookObject::initialize(JS::GlobalObject& global_object) | ||||
| { | ||||
|     Object::initialize(global_object); | ||||
|     define_native_function("sheet", sheet, 1); | ||||
| } | ||||
| 
 | ||||
| JS_DEFINE_NATIVE_FUNCTION(WorkbookObject::sheet) | ||||
| { | ||||
|     if (interpreter.argument_count() != 1) { | ||||
|         interpreter.throw_exception<JS::TypeError>("Expected exactly one argument to sheet()"); | ||||
|         return {}; | ||||
|     } | ||||
|     auto name_value = interpreter.argument(0); | ||||
|     if (!name_value.is_string() && !name_value.is_number()) { | ||||
|         interpreter.throw_exception<JS::TypeError>("Expected a String or Number argument to sheet()"); | ||||
|         return {}; | ||||
|     } | ||||
| 
 | ||||
|     auto* this_object = interpreter.this_value(global_object).to_object(interpreter, global_object); | ||||
|     if (!this_object) | ||||
|         return {}; | ||||
| 
 | ||||
|     if (!this_object->inherits("WorkbookObject")) { | ||||
|         interpreter.throw_exception<JS::TypeError>(JS::ErrorType::NotA, "WorkbookObject"); | ||||
|         return {}; | ||||
|     } | ||||
| 
 | ||||
|     auto& workbook = static_cast<WorkbookObject*>(this_object)->m_workbook; | ||||
| 
 | ||||
|     if (name_value.is_string()) { | ||||
|         auto& name = name_value.as_string().string(); | ||||
|         for (auto& sheet : workbook.sheets()) { | ||||
|             if (sheet.name() == name) | ||||
|                 return JS::Value(&sheet.global_object()); | ||||
|         } | ||||
|     } else { | ||||
|         auto index = name_value.as_size_t(); | ||||
|         if (index < workbook.sheets().size()) | ||||
|             return JS::Value(&workbook.sheets()[index].global_object()); | ||||
|     } | ||||
| 
 | ||||
|     return JS::js_undefined(); | ||||
| } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										71
									
								
								Applications/Spreadsheet/JSIntegration.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								Applications/Spreadsheet/JSIntegration.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| /*
 | ||||
|  * 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 <LibJS/Forward.h> | ||||
| #include <LibJS/Runtime/GlobalObject.h> | ||||
| 
 | ||||
| namespace Spreadsheet { | ||||
| 
 | ||||
| class Sheet; | ||||
| class Workbook; | ||||
| 
 | ||||
| class SheetGlobalObject : public JS::GlobalObject { | ||||
|     JS_OBJECT(SheetGlobalObject, JS::GlobalObject); | ||||
| 
 | ||||
| public: | ||||
|     SheetGlobalObject(Sheet& sheet); | ||||
| 
 | ||||
|     virtual ~SheetGlobalObject() override; | ||||
| 
 | ||||
|     virtual JS::Value get(const JS::PropertyName& name, JS::Value receiver = {}) const override; | ||||
|     virtual bool put(const JS::PropertyName& name, JS::Value value, JS::Value receiver = {}) override; | ||||
|     virtual void initialize() override; | ||||
| 
 | ||||
|     JS_DECLARE_NATIVE_FUNCTION(parse_cell_name); | ||||
| 
 | ||||
| private: | ||||
|     Sheet& m_sheet; | ||||
| }; | ||||
| 
 | ||||
| class WorkbookObject : public JS::Object { | ||||
|     JS_OBJECT(WorkbookObject, JS::Object); | ||||
| 
 | ||||
| public: | ||||
|     WorkbookObject(Workbook& workbook); | ||||
| 
 | ||||
|     virtual ~WorkbookObject() override; | ||||
| 
 | ||||
|     virtual void initialize(JS::GlobalObject&) override; | ||||
| 
 | ||||
|     JS_DECLARE_NATIVE_FUNCTION(sheet); | ||||
| 
 | ||||
| private: | ||||
|     Workbook& m_workbook; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
|  | @ -25,96 +25,20 @@ | |||
|  */ | ||||
| 
 | ||||
| #include "Spreadsheet.h" | ||||
| #include "JSIntegration.h" | ||||
| #include "Workbook.h" | ||||
| #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> | ||||
| #include <LibJS/Runtime/Value.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) { | ||||
|             interpreter.throw_exception<JS::TypeError>("Expected exactly one argument to parse_cell_name()"); | ||||
|             return {}; | ||||
|         } | ||||
|         auto name_value = interpreter.argument(0); | ||||
|         if (!name_value.is_string()) { | ||||
|             interpreter.throw_exception<JS::TypeError>("Expected a String argument to parse_cell_name()"); | ||||
|             return {}; | ||||
|         } | ||||
|         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) | ||||
|     : Sheet(EmptyConstruct::EmptyConstructTag) | ||||
| Sheet::Sheet(const StringView& name, Workbook& workbook) | ||||
|     : Sheet(workbook) | ||||
| { | ||||
|     m_name = name; | ||||
| 
 | ||||
|  | @ -125,14 +49,40 @@ Sheet::Sheet(const StringView& name) | |||
|         add_column(); | ||||
| } | ||||
| 
 | ||||
| Sheet::Sheet(EmptyConstruct) | ||||
|     : m_interpreter(JS::Interpreter::create<SheetGlobalObject>(*this)) | ||||
| Sheet::Sheet(Workbook& workbook) | ||||
|     : m_workbook(workbook) | ||||
| { | ||||
|     m_global_object = m_workbook.interpreter().heap().allocate_without_global_object<SheetGlobalObject>(*this); | ||||
|     m_global_object->set_prototype(&m_workbook.global_object()); | ||||
|     m_global_object->initialize(); | ||||
|     m_global_object->put("thisSheet", m_global_object); // Self-reference is unfortunate, but required.
 | ||||
| 
 | ||||
|     // Sadly, these have to be evaluated once per sheet.
 | ||||
|     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); | ||||
|         JS::Parser parser { JS::Lexer(buffer) }; | ||||
|         if (parser.has_errors()) { | ||||
|             dbg() << "Spreadsheet: Failed to parse runtime code"; | ||||
|             for (auto& error : parser.errors()) | ||||
|                 dbg() << "Error: " << error.to_string() << "\n" | ||||
|                       << error.source_location_hint(buffer); | ||||
|         } else { | ||||
|             interpreter().run(global_object(), parser.parse_program()); | ||||
|             if (auto exc = interpreter().exception()) { | ||||
|                 dbg() << "Spreadsheet: Failed to run runtime code: "; | ||||
|                 for (auto& t : exc->trace()) | ||||
|                     dbg() << t; | ||||
|                 interpreter().clear_exception(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| JS::Interpreter& Sheet::interpreter() const | ||||
| { | ||||
|     return m_workbook.interpreter(); | ||||
| } | ||||
| 
 | ||||
| Sheet::~Sheet() | ||||
|  | @ -208,14 +158,14 @@ JS::Value Sheet::evaluate(const StringView& source, Cell* on_behalf_of) | |||
|         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(); | ||||
|     interpreter().run(global_object(), program); | ||||
|     if (interpreter().exception()) { | ||||
|         auto exc = interpreter().exception()->value(); | ||||
|         interpreter().clear_exception(); | ||||
|         return exc; | ||||
|     } | ||||
| 
 | ||||
|     auto value = m_interpreter->last_value(); | ||||
|     auto value = interpreter().last_value(); | ||||
|     if (value.is_empty()) | ||||
|         return JS::js_undefined(); | ||||
|     return value; | ||||
|  | @ -309,9 +259,9 @@ void Cell::reference_from(Cell* other) | |||
|     referencing_cells.append(other->make_weak_ptr()); | ||||
| } | ||||
| 
 | ||||
| RefPtr<Sheet> Sheet::from_json(const JsonObject& object) | ||||
| RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook) | ||||
| { | ||||
|     auto sheet = adopt(*new Sheet(EmptyConstruct::EmptyConstructTag)); | ||||
|     auto sheet = adopt(*new Sheet(workbook)); | ||||
|     auto rows = object.get("rows").to_u32(20); | ||||
|     auto columns = object.get("columns"); | ||||
|     if (!columns.is_array()) | ||||
|  | @ -385,8 +335,8 @@ JsonObject Sheet::to_json() const | |||
|         data.set("kind", it.value->kind == Cell::Kind::Formula ? "Formula" : "LiteralString"); | ||||
|         if (it.value->kind == Cell::Formula) { | ||||
|             data.set("source", it.value->data); | ||||
|             auto json = m_interpreter->global_object().get("JSON"); | ||||
|             auto stringified = m_interpreter->call(json.as_object().get("stringify").as_function(), json, it.value->evaluated_data); | ||||
|             auto json = interpreter().global_object().get("JSON"); | ||||
|             auto stringified = interpreter().call(json.as_object().get("stringify").as_function(), json, it.value->evaluated_data); | ||||
|             data.set("value", stringified.to_string_without_side_effects()); | ||||
|         } else { | ||||
|             data.set("value", it.value->data); | ||||
|  | @ -404,19 +354,19 @@ 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 add_docs_from = [&](auto& it, auto& global_object) { | ||||
|         auto value = global_object.get(it.key); | ||||
|         if (!value.is_function()) | ||||
|             continue; | ||||
|         if (!value.is_function() && !value.is_object()) | ||||
|             return; | ||||
| 
 | ||||
|         auto& fn = value.as_function(); | ||||
|         if (!fn.has_own_property(doc_name)) | ||||
|             continue; | ||||
|         auto& value_object = value.is_object() ? value.as_object() : value.as_function(); | ||||
|         if (!value_object.has_own_property(doc_name)) | ||||
|             return; | ||||
| 
 | ||||
|         auto doc = fn.get(doc_name); | ||||
|         dbg() << "Found '" << it.key.to_display_string() << "'"; | ||||
|         auto doc = value_object.get(doc_name); | ||||
|         if (!doc.is_string()) | ||||
|             continue; | ||||
|             return; | ||||
| 
 | ||||
|         JsonParser parser(doc.to_string_without_side_effects()); | ||||
|         auto doc_object = parser.parse(); | ||||
|  | @ -425,7 +375,13 @@ JsonObject Sheet::gather_documentation() const | |||
|             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() << "'!"; | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     for (auto& it : interpreter().global_object().shape().property_table()) | ||||
|         add_docs_from(it, interpreter().global_object()); | ||||
| 
 | ||||
|     for (auto& it : global_object().shape().property_table()) | ||||
|         add_docs_from(it, global_object()); | ||||
| 
 | ||||
|     return object; | ||||
| } | ||||
|  |  | |||
|  | @ -39,6 +39,9 @@ | |||
| 
 | ||||
| namespace Spreadsheet { | ||||
| 
 | ||||
| class Workbook; | ||||
| class SheetGlobalObject; | ||||
| 
 | ||||
| struct Position { | ||||
|     String column; | ||||
|     size_t row { 0 }; | ||||
|  | @ -140,7 +143,7 @@ public: | |||
|     static Optional<Position> parse_cell_name(const StringView&); | ||||
| 
 | ||||
|     JsonObject to_json() const; | ||||
|     static RefPtr<Sheet> from_json(const JsonObject&); | ||||
|     static RefPtr<Sheet> from_json(const JsonObject&, Workbook&); | ||||
| 
 | ||||
|     const String& name() const { return m_name; } | ||||
|     void set_name(const StringView& name) { m_name = name; } | ||||
|  | @ -183,16 +186,15 @@ public: | |||
|     void update(Cell&); | ||||
| 
 | ||||
|     JS::Value evaluate(const StringView&, Cell* = nullptr); | ||||
|     JS::Interpreter& interpreter() { return *m_interpreter; } | ||||
|     JS::Interpreter& interpreter() const; | ||||
|     SheetGlobalObject& global_object() const { return *m_global_object; } | ||||
| 
 | ||||
|     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: | ||||
|     enum class EmptyConstruct { EmptyConstructTag }; | ||||
| 
 | ||||
|     explicit Sheet(EmptyConstruct); | ||||
|     explicit Sheet(const StringView& name); | ||||
|     explicit Sheet(Workbook&); | ||||
|     explicit Sheet(const StringView& name, Workbook&); | ||||
| 
 | ||||
|     String m_name; | ||||
|     Vector<String> m_columns; | ||||
|  | @ -200,11 +202,13 @@ private: | |||
|     HashMap<Position, NonnullOwnPtr<Cell>> m_cells; | ||||
|     Optional<Position> m_selected_cell; // FIXME: Make this a collection.
 | ||||
| 
 | ||||
|     Workbook& m_workbook; | ||||
|     mutable SheetGlobalObject* m_global_object; | ||||
| 
 | ||||
|     Cell* m_current_cell_being_evaluated { nullptr }; | ||||
| 
 | ||||
|     size_t m_current_column_name_length { 0 }; | ||||
| 
 | ||||
|     mutable NonnullOwnPtr<JS::Interpreter> m_interpreter; | ||||
|     HashTable<Cell*> m_visited_cells_in_update; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -81,13 +81,13 @@ SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets, bool s | |||
|     if (!m_workbook->has_sheets() && should_add_sheet_if_empty) | ||||
|         m_workbook->add_sheet("Sheet 1"); | ||||
| 
 | ||||
|     setup_tabs(); | ||||
|     setup_tabs(m_workbook->sheets()); | ||||
| } | ||||
| 
 | ||||
| void SpreadsheetWidget::setup_tabs() | ||||
| void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector<Sheet> new_sheets) | ||||
| { | ||||
|     RefPtr<GUI::Widget> first_tab_widget; | ||||
|     for (auto& sheet : m_workbook->sheets()) { | ||||
|     for (auto& sheet : new_sheets) { | ||||
|         auto& tab = m_tab_widget->add_tab<SpreadsheetView>(sheet.name(), sheet); | ||||
|         if (!first_tab_widget) | ||||
|             first_tab_widget = &tab; | ||||
|  | @ -148,7 +148,20 @@ void SpreadsheetWidget::load(const StringView& filename) | |||
|         m_tab_widget->remove_tab(*widget); | ||||
|     } | ||||
| 
 | ||||
|     setup_tabs(); | ||||
|     setup_tabs(m_workbook->sheets()); | ||||
| } | ||||
| 
 | ||||
| void SpreadsheetWidget::add_sheet() | ||||
| { | ||||
|     StringBuilder name; | ||||
|     name.append("Sheet"); | ||||
|     name.appendf(" %d", m_workbook->sheets().size() + 1); | ||||
| 
 | ||||
|     auto& sheet = m_workbook->add_sheet(name.string_view()); | ||||
| 
 | ||||
|     NonnullRefPtrVector<Sheet> new_sheets; | ||||
|     new_sheets.append(sheet); | ||||
|     setup_tabs(new_sheets); | ||||
| } | ||||
| 
 | ||||
| void SpreadsheetWidget::set_filename(const String& filename) | ||||
|  |  | |||
|  | @ -41,6 +41,7 @@ public: | |||
| 
 | ||||
|     void save(const StringView& filename); | ||||
|     void load(const StringView& filename); | ||||
|     void add_sheet(); | ||||
| 
 | ||||
|     const String& current_filename() const { return m_workbook->current_filename(); } | ||||
|     void set_filename(const String& filename); | ||||
|  | @ -48,7 +49,7 @@ public: | |||
| private: | ||||
|     explicit SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets = {}, bool should_add_sheet_if_empty = true); | ||||
| 
 | ||||
|     void setup_tabs(); | ||||
|     void setup_tabs(NonnullRefPtrVector<Sheet> new_sheets); | ||||
| 
 | ||||
|     SpreadsheetView* m_selected_view { nullptr }; | ||||
|     RefPtr<GUI::Label> m_current_cell_label; | ||||
|  |  | |||
|  | @ -25,15 +25,26 @@ | |||
|  */ | ||||
| 
 | ||||
| #include "Workbook.h" | ||||
| #include "JSIntegration.h" | ||||
| #include <AK/JsonArray.h> | ||||
| #include <AK/JsonObject.h> | ||||
| #include <AK/JsonObjectSerializer.h> | ||||
| #include <AK/JsonParser.h> | ||||
| #include <LibCore/File.h> | ||||
| #include <LibJS/Parser.h> | ||||
| #include <LibJS/Runtime/GlobalObject.h> | ||||
| #include <string.h> | ||||
| 
 | ||||
| namespace Spreadsheet { | ||||
| 
 | ||||
| Workbook::Workbook(NonnullRefPtrVector<Sheet>&& sheets) | ||||
|     : m_sheets(move(sheets)) | ||||
|     , m_interpreter(JS::Interpreter::create<JS::GlobalObject>()) | ||||
| { | ||||
|     m_workbook_object = interpreter().heap().allocate<WorkbookObject>(global_object(), *this); | ||||
|     global_object().put("workbook", workbook_object()); | ||||
| } | ||||
| 
 | ||||
| bool Workbook::set_filename(const String& filename) | ||||
| { | ||||
|     if (m_current_filename == filename) | ||||
|  | @ -81,7 +92,7 @@ Result<bool, String> Workbook::load(const StringView& filename) | |||
|         if (!sheet_json.is_object()) | ||||
|             return IterationDecision::Continue; | ||||
| 
 | ||||
|         auto sheet = Sheet::from_json(sheet_json.as_object()); | ||||
|         auto sheet = Sheet::from_json(sheet_json.as_object(), *this); | ||||
|         if (!sheet) | ||||
|             return IterationDecision::Continue; | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,12 +32,11 @@ | |||
| 
 | ||||
| namespace Spreadsheet { | ||||
| 
 | ||||
| class WorkbookObject; | ||||
| 
 | ||||
| class Workbook { | ||||
| public: | ||||
|     Workbook(NonnullRefPtrVector<Sheet>&& sheets) | ||||
|         : m_sheets(move(sheets)) | ||||
|     { | ||||
|     } | ||||
|     Workbook(NonnullRefPtrVector<Sheet>&& sheets); | ||||
| 
 | ||||
|     Result<bool, String> save(const StringView& filename); | ||||
|     Result<bool, String> load(const StringView& filename); | ||||
|  | @ -52,13 +51,23 @@ public: | |||
| 
 | ||||
|     Sheet& add_sheet(const StringView& name) | ||||
|     { | ||||
|         auto sheet = Sheet::construct(name); | ||||
|         auto sheet = Sheet::construct(name, *this); | ||||
|         m_sheets.append(sheet); | ||||
|         return *sheet; | ||||
|     } | ||||
| 
 | ||||
|     JS::Interpreter& interpreter() { return *m_interpreter; } | ||||
|     const JS::Interpreter& interpreter() const { return *m_interpreter; } | ||||
| 
 | ||||
|     JS::GlobalObject& global_object() { return m_interpreter->global_object(); } | ||||
|     const JS::GlobalObject& global_object() const { return m_interpreter->global_object(); } | ||||
| 
 | ||||
|     WorkbookObject* workbook_object() { return m_workbook_object; } | ||||
| 
 | ||||
| private: | ||||
|     NonnullRefPtrVector<Sheet> m_sheets; | ||||
|     NonnullOwnPtr<JS::Interpreter> m_interpreter; | ||||
|     WorkbookObject* m_workbook_object { nullptr }; | ||||
| 
 | ||||
|     String m_current_filename; | ||||
| }; | ||||
|  |  | |||
|  | @ -95,6 +95,9 @@ int main(int argc, char* argv[]) | |||
|     auto menubar = GUI::MenuBar::construct(); | ||||
|     auto& app_menu = menubar->add_menu("Spreadsheet"); | ||||
| 
 | ||||
|     app_menu.add_action(GUI::Action::create("Add New Sheet", Gfx::Bitmap::load_from_file("/res/icons/16x16/new-tab.png"), [&](auto&) { | ||||
|         spreadsheet_widget.add_sheet(); | ||||
|     })); | ||||
|     app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { | ||||
|         app->quit(0); | ||||
|     })); | ||||
|  |  | |||
|  | @ -1,10 +1,8 @@ | |||
| const sheet = this; | ||||
| 
 | ||||
| function range(start, end, columnStep, rowStep) { | ||||
|     columnStep = integer(columnStep ?? 1); | ||||
|     rowStep = integer(rowStep ?? 1); | ||||
|     start = sheet.parse_cell_name(start) ?? { column: "A", row: 0 }; | ||||
|     end = sheet.parse_cell_name(end) ?? start; | ||||
|     start = parse_cell_name(start) ?? { column: "A", row: 0 }; | ||||
|     end = 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)"); | ||||
|  | @ -58,7 +56,7 @@ function select(criteria, t, f) { | |||
| function sumIf(condition, cells) { | ||||
|     let sum = null; | ||||
|     for (let name of cells) { | ||||
|         let cell = sheet[name]; | ||||
|         let cell = thisSheet[name]; | ||||
|         if (condition(cell)) sum = sum === null ? cell : sum + cell; | ||||
|     } | ||||
|     return sum; | ||||
|  | @ -67,7 +65,7 @@ function sumIf(condition, cells) { | |||
| function countIf(condition, cells) { | ||||
|     let count = 0; | ||||
|     for (let name of cells) { | ||||
|         let cell = sheet[name]; | ||||
|         let cell = thisSheet[name]; | ||||
|         if (condition(cell)) count++; | ||||
|     } | ||||
|     return count; | ||||
|  | @ -89,6 +87,10 @@ function integer(value) { | |||
|     return value | 0; | ||||
| } | ||||
| 
 | ||||
| function sheet(name) { | ||||
|     return workbook.sheet(name); | ||||
| } | ||||
| 
 | ||||
| // Cheat the system and add documentation
 | ||||
| range.__documentation = JSON.stringify({ | ||||
|     name: "range", | ||||
|  | @ -172,3 +174,14 @@ integer.__documentation = JSON.stringify({ | |||
|         "A1 = integer(A0)": "Sets the value of the cell A1 to the integer value of the cell A0", | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| sheet.__documentation = JSON.stringify({ | ||||
|     name: "sheet", | ||||
|     argc: 1, | ||||
|     argnames: ["name or index"], | ||||
|     doc: "Returns a reference to another sheet, identified by _name_ or _index_", | ||||
|     examples: { | ||||
|         "sheet('Sheet 1').A4": "Read the value of the cell A4 in a sheet named 'Sheet 1'", | ||||
|         "sheet(0).A0 = 123": "Set the value of the cell A0 in the first sheet to 123", | ||||
|     }, | ||||
| }); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 AnotherTest
						AnotherTest