mirror of
https://github.com/RGBCube/serenity
synced 2025-05-22 02:25:07 +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
|
set(SOURCES
|
||||||
CellSyntaxHighlighter.cpp
|
CellSyntaxHighlighter.cpp
|
||||||
HelpWindow.cpp
|
HelpWindow.cpp
|
||||||
|
JSIntegration.cpp
|
||||||
Spreadsheet.cpp
|
Spreadsheet.cpp
|
||||||
SpreadsheetModel.cpp
|
SpreadsheetModel.cpp
|
||||||
SpreadsheetView.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 "Spreadsheet.h"
|
||||||
|
#include "JSIntegration.h"
|
||||||
|
#include "Workbook.h"
|
||||||
#include <AK/GenericLexer.h>
|
#include <AK/GenericLexer.h>
|
||||||
#include <AK/JsonArray.h>
|
#include <AK/JsonArray.h>
|
||||||
#include <AK/JsonObject.h>
|
#include <AK/JsonObject.h>
|
||||||
#include <AK/JsonParser.h>
|
#include <AK/JsonParser.h>
|
||||||
#include <LibCore/File.h>
|
#include <LibCore/File.h>
|
||||||
#include <LibJS/Parser.h>
|
#include <LibJS/Parser.h>
|
||||||
#include <LibJS/Runtime/Error.h>
|
|
||||||
#include <LibJS/Runtime/Function.h>
|
#include <LibJS/Runtime/Function.h>
|
||||||
#include <LibJS/Runtime/GlobalObject.h>
|
|
||||||
#include <LibJS/Runtime/Object.h>
|
|
||||||
#include <LibJS/Runtime/Value.h>
|
|
||||||
|
|
||||||
namespace Spreadsheet {
|
namespace Spreadsheet {
|
||||||
|
|
||||||
class SheetGlobalObject : public JS::GlobalObject {
|
Sheet::Sheet(const StringView& name, Workbook& workbook)
|
||||||
JS_OBJECT(SheetGlobalObject, JS::GlobalObject);
|
: Sheet(workbook)
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
m_name = name;
|
m_name = name;
|
||||||
|
|
||||||
|
@ -125,14 +49,40 @@ Sheet::Sheet(const StringView& name)
|
||||||
add_column();
|
add_column();
|
||||||
}
|
}
|
||||||
|
|
||||||
Sheet::Sheet(EmptyConstruct)
|
Sheet::Sheet(Workbook& workbook)
|
||||||
: m_interpreter(JS::Interpreter::create<SheetGlobalObject>(*this))
|
: 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);
|
auto file_or_error = Core::File::open("/res/js/Spreadsheet/runtime.js", Core::IODevice::OpenMode::ReadOnly);
|
||||||
if (!file_or_error.is_error()) {
|
if (!file_or_error.is_error()) {
|
||||||
auto buffer = file_or_error.value()->read_all();
|
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()
|
Sheet::~Sheet()
|
||||||
|
@ -208,14 +158,14 @@ JS::Value Sheet::evaluate(const StringView& source, Cell* on_behalf_of)
|
||||||
return JS::js_undefined();
|
return JS::js_undefined();
|
||||||
|
|
||||||
auto program = parser.parse_program();
|
auto program = parser.parse_program();
|
||||||
m_interpreter->run(m_interpreter->global_object(), program);
|
interpreter().run(global_object(), program);
|
||||||
if (m_interpreter->exception()) {
|
if (interpreter().exception()) {
|
||||||
auto exc = m_interpreter->exception()->value();
|
auto exc = interpreter().exception()->value();
|
||||||
m_interpreter->clear_exception();
|
interpreter().clear_exception();
|
||||||
return exc;
|
return exc;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto value = m_interpreter->last_value();
|
auto value = interpreter().last_value();
|
||||||
if (value.is_empty())
|
if (value.is_empty())
|
||||||
return JS::js_undefined();
|
return JS::js_undefined();
|
||||||
return value;
|
return value;
|
||||||
|
@ -309,9 +259,9 @@ void Cell::reference_from(Cell* other)
|
||||||
referencing_cells.append(other->make_weak_ptr());
|
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 rows = object.get("rows").to_u32(20);
|
||||||
auto columns = object.get("columns");
|
auto columns = object.get("columns");
|
||||||
if (!columns.is_array())
|
if (!columns.is_array())
|
||||||
|
@ -385,8 +335,8 @@ JsonObject Sheet::to_json() const
|
||||||
data.set("kind", it.value->kind == Cell::Kind::Formula ? "Formula" : "LiteralString");
|
data.set("kind", it.value->kind == Cell::Kind::Formula ? "Formula" : "LiteralString");
|
||||||
if (it.value->kind == Cell::Formula) {
|
if (it.value->kind == Cell::Formula) {
|
||||||
data.set("source", it.value->data);
|
data.set("source", it.value->data);
|
||||||
auto json = m_interpreter->global_object().get("JSON");
|
auto json = interpreter().global_object().get("JSON");
|
||||||
auto stringified = m_interpreter->call(json.as_object().get("stringify").as_function(), json, it.value->evaluated_data);
|
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());
|
data.set("value", stringified.to_string_without_side_effects());
|
||||||
} else {
|
} else {
|
||||||
data.set("value", it.value->data);
|
data.set("value", it.value->data);
|
||||||
|
@ -404,19 +354,19 @@ JsonObject Sheet::gather_documentation() const
|
||||||
JsonObject object;
|
JsonObject object;
|
||||||
const JS::PropertyName doc_name { "__documentation" };
|
const JS::PropertyName doc_name { "__documentation" };
|
||||||
|
|
||||||
auto& global_object = m_interpreter->global_object();
|
auto add_docs_from = [&](auto& it, auto& global_object) {
|
||||||
for (auto& it : global_object.shape().property_table()) {
|
|
||||||
auto value = global_object.get(it.key);
|
auto value = global_object.get(it.key);
|
||||||
if (!value.is_function())
|
if (!value.is_function() && !value.is_object())
|
||||||
continue;
|
return;
|
||||||
|
|
||||||
auto& fn = value.as_function();
|
auto& value_object = value.is_object() ? value.as_object() : value.as_function();
|
||||||
if (!fn.has_own_property(doc_name))
|
if (!value_object.has_own_property(doc_name))
|
||||||
continue;
|
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())
|
if (!doc.is_string())
|
||||||
continue;
|
return;
|
||||||
|
|
||||||
JsonParser parser(doc.to_string_without_side_effects());
|
JsonParser parser(doc.to_string_without_side_effects());
|
||||||
auto doc_object = parser.parse();
|
auto doc_object = parser.parse();
|
||||||
|
@ -425,7 +375,13 @@ JsonObject Sheet::gather_documentation() const
|
||||||
object.set(it.key.to_display_string(), doc_object.value());
|
object.set(it.key.to_display_string(), doc_object.value());
|
||||||
else
|
else
|
||||||
dbg() << "Sheet::gather_documentation(): Failed to parse the documentation for '" << it.key.to_display_string() << "'!";
|
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;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@
|
||||||
|
|
||||||
namespace Spreadsheet {
|
namespace Spreadsheet {
|
||||||
|
|
||||||
|
class Workbook;
|
||||||
|
class SheetGlobalObject;
|
||||||
|
|
||||||
struct Position {
|
struct Position {
|
||||||
String column;
|
String column;
|
||||||
size_t row { 0 };
|
size_t row { 0 };
|
||||||
|
@ -140,7 +143,7 @@ public:
|
||||||
static Optional<Position> parse_cell_name(const StringView&);
|
static Optional<Position> parse_cell_name(const StringView&);
|
||||||
|
|
||||||
JsonObject to_json() const;
|
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; }
|
const String& name() const { return m_name; }
|
||||||
void set_name(const StringView& name) { m_name = name; }
|
void set_name(const StringView& name) { m_name = name; }
|
||||||
|
@ -183,16 +186,15 @@ public:
|
||||||
void update(Cell&);
|
void update(Cell&);
|
||||||
|
|
||||||
JS::Value evaluate(const StringView&, Cell* = nullptr);
|
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; }
|
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); }
|
bool has_been_visited(Cell* cell) const { return m_visited_cells_in_update.contains(cell); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class EmptyConstruct { EmptyConstructTag };
|
explicit Sheet(Workbook&);
|
||||||
|
explicit Sheet(const StringView& name, Workbook&);
|
||||||
explicit Sheet(EmptyConstruct);
|
|
||||||
explicit Sheet(const StringView& name);
|
|
||||||
|
|
||||||
String m_name;
|
String m_name;
|
||||||
Vector<String> m_columns;
|
Vector<String> m_columns;
|
||||||
|
@ -200,11 +202,13 @@ private:
|
||||||
HashMap<Position, NonnullOwnPtr<Cell>> m_cells;
|
HashMap<Position, NonnullOwnPtr<Cell>> m_cells;
|
||||||
Optional<Position> m_selected_cell; // FIXME: Make this a collection.
|
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 };
|
Cell* m_current_cell_being_evaluated { nullptr };
|
||||||
|
|
||||||
size_t m_current_column_name_length { 0 };
|
size_t m_current_column_name_length { 0 };
|
||||||
|
|
||||||
mutable NonnullOwnPtr<JS::Interpreter> m_interpreter;
|
|
||||||
HashTable<Cell*> m_visited_cells_in_update;
|
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)
|
if (!m_workbook->has_sheets() && should_add_sheet_if_empty)
|
||||||
m_workbook->add_sheet("Sheet 1");
|
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;
|
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);
|
auto& tab = m_tab_widget->add_tab<SpreadsheetView>(sheet.name(), sheet);
|
||||||
if (!first_tab_widget)
|
if (!first_tab_widget)
|
||||||
first_tab_widget = &tab;
|
first_tab_widget = &tab;
|
||||||
|
@ -148,7 +148,20 @@ void SpreadsheetWidget::load(const StringView& filename)
|
||||||
m_tab_widget->remove_tab(*widget);
|
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)
|
void SpreadsheetWidget::set_filename(const String& filename)
|
||||||
|
|
|
@ -41,6 +41,7 @@ public:
|
||||||
|
|
||||||
void save(const StringView& filename);
|
void save(const StringView& filename);
|
||||||
void load(const StringView& filename);
|
void load(const StringView& filename);
|
||||||
|
void add_sheet();
|
||||||
|
|
||||||
const String& current_filename() const { return m_workbook->current_filename(); }
|
const String& current_filename() const { return m_workbook->current_filename(); }
|
||||||
void set_filename(const String& filename);
|
void set_filename(const String& filename);
|
||||||
|
@ -48,7 +49,7 @@ public:
|
||||||
private:
|
private:
|
||||||
explicit SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets = {}, bool should_add_sheet_if_empty = true);
|
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 };
|
SpreadsheetView* m_selected_view { nullptr };
|
||||||
RefPtr<GUI::Label> m_current_cell_label;
|
RefPtr<GUI::Label> m_current_cell_label;
|
||||||
|
|
|
@ -25,15 +25,26 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "Workbook.h"
|
#include "Workbook.h"
|
||||||
|
#include "JSIntegration.h"
|
||||||
#include <AK/JsonArray.h>
|
#include <AK/JsonArray.h>
|
||||||
#include <AK/JsonObject.h>
|
#include <AK/JsonObject.h>
|
||||||
#include <AK/JsonObjectSerializer.h>
|
#include <AK/JsonObjectSerializer.h>
|
||||||
#include <AK/JsonParser.h>
|
#include <AK/JsonParser.h>
|
||||||
#include <LibCore/File.h>
|
#include <LibCore/File.h>
|
||||||
|
#include <LibJS/Parser.h>
|
||||||
|
#include <LibJS/Runtime/GlobalObject.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
namespace Spreadsheet {
|
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)
|
bool Workbook::set_filename(const String& filename)
|
||||||
{
|
{
|
||||||
if (m_current_filename == filename)
|
if (m_current_filename == filename)
|
||||||
|
@ -81,7 +92,7 @@ Result<bool, String> Workbook::load(const StringView& filename)
|
||||||
if (!sheet_json.is_object())
|
if (!sheet_json.is_object())
|
||||||
return IterationDecision::Continue;
|
return IterationDecision::Continue;
|
||||||
|
|
||||||
auto sheet = Sheet::from_json(sheet_json.as_object());
|
auto sheet = Sheet::from_json(sheet_json.as_object(), *this);
|
||||||
if (!sheet)
|
if (!sheet)
|
||||||
return IterationDecision::Continue;
|
return IterationDecision::Continue;
|
||||||
|
|
||||||
|
|
|
@ -32,12 +32,11 @@
|
||||||
|
|
||||||
namespace Spreadsheet {
|
namespace Spreadsheet {
|
||||||
|
|
||||||
|
class WorkbookObject;
|
||||||
|
|
||||||
class Workbook {
|
class Workbook {
|
||||||
public:
|
public:
|
||||||
Workbook(NonnullRefPtrVector<Sheet>&& sheets)
|
Workbook(NonnullRefPtrVector<Sheet>&& sheets);
|
||||||
: m_sheets(move(sheets))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<bool, String> save(const StringView& filename);
|
Result<bool, String> save(const StringView& filename);
|
||||||
Result<bool, String> load(const StringView& filename);
|
Result<bool, String> load(const StringView& filename);
|
||||||
|
@ -52,13 +51,23 @@ public:
|
||||||
|
|
||||||
Sheet& add_sheet(const StringView& name)
|
Sheet& add_sheet(const StringView& name)
|
||||||
{
|
{
|
||||||
auto sheet = Sheet::construct(name);
|
auto sheet = Sheet::construct(name, *this);
|
||||||
m_sheets.append(sheet);
|
m_sheets.append(sheet);
|
||||||
return *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:
|
private:
|
||||||
NonnullRefPtrVector<Sheet> m_sheets;
|
NonnullRefPtrVector<Sheet> m_sheets;
|
||||||
|
NonnullOwnPtr<JS::Interpreter> m_interpreter;
|
||||||
|
WorkbookObject* m_workbook_object { nullptr };
|
||||||
|
|
||||||
String m_current_filename;
|
String m_current_filename;
|
||||||
};
|
};
|
||||||
|
|
|
@ -95,6 +95,9 @@ int main(int argc, char* argv[])
|
||||||
auto menubar = GUI::MenuBar::construct();
|
auto menubar = GUI::MenuBar::construct();
|
||||||
auto& app_menu = menubar->add_menu("Spreadsheet");
|
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_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
|
||||||
app->quit(0);
|
app->quit(0);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
const sheet = this;
|
|
||||||
|
|
||||||
function range(start, end, columnStep, rowStep) {
|
function range(start, end, columnStep, rowStep) {
|
||||||
columnStep = integer(columnStep ?? 1);
|
columnStep = integer(columnStep ?? 1);
|
||||||
rowStep = integer(rowStep ?? 1);
|
rowStep = integer(rowStep ?? 1);
|
||||||
start = sheet.parse_cell_name(start) ?? { column: "A", row: 0 };
|
start = parse_cell_name(start) ?? { column: "A", row: 0 };
|
||||||
end = sheet.parse_cell_name(end) ?? start;
|
end = parse_cell_name(end) ?? start;
|
||||||
|
|
||||||
if (end.column.length > 1 || start.column.length > 1)
|
if (end.column.length > 1 || start.column.length > 1)
|
||||||
throw new TypeError("Only single-letter column names are allowed (TODO)");
|
throw new TypeError("Only single-letter column names are allowed (TODO)");
|
||||||
|
@ -58,7 +56,7 @@ function select(criteria, t, f) {
|
||||||
function sumIf(condition, cells) {
|
function sumIf(condition, cells) {
|
||||||
let sum = null;
|
let sum = null;
|
||||||
for (let name of cells) {
|
for (let name of cells) {
|
||||||
let cell = sheet[name];
|
let cell = thisSheet[name];
|
||||||
if (condition(cell)) sum = sum === null ? cell : sum + cell;
|
if (condition(cell)) sum = sum === null ? cell : sum + cell;
|
||||||
}
|
}
|
||||||
return sum;
|
return sum;
|
||||||
|
@ -67,7 +65,7 @@ function sumIf(condition, cells) {
|
||||||
function countIf(condition, cells) {
|
function countIf(condition, cells) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (let name of cells) {
|
for (let name of cells) {
|
||||||
let cell = sheet[name];
|
let cell = thisSheet[name];
|
||||||
if (condition(cell)) count++;
|
if (condition(cell)) count++;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
|
@ -89,6 +87,10 @@ function integer(value) {
|
||||||
return value | 0;
|
return value | 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sheet(name) {
|
||||||
|
return workbook.sheet(name);
|
||||||
|
}
|
||||||
|
|
||||||
// Cheat the system and add documentation
|
// Cheat the system and add documentation
|
||||||
range.__documentation = JSON.stringify({
|
range.__documentation = JSON.stringify({
|
||||||
name: "range",
|
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",
|
"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