1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 19:47:34 +00:00

Applications: Move to Userland/Applications/

This commit is contained in:
Andreas Kling 2021-01-12 12:05:23 +01:00
parent aa939c4b4b
commit dc28c07fa5
287 changed files with 1 additions and 1 deletions

View file

@ -0,0 +1,28 @@
compile_gml(CondFormatting.gml CondFormattingGML.h cond_fmt_gml)
compile_gml(CondView.gml CondFormattingViewGML.h cond_fmt_view_gml)
set(SOURCES
Cell.cpp
CellSyntaxHighlighter.cpp
CellType/Date.cpp
CellType/Format.cpp
CellType/Identity.cpp
CellType/Numeric.cpp
CellType/String.cpp
CellType/Type.cpp
CellTypeDialog.cpp
CondFormattingGML.h
CondFormattingViewGML.h
HelpWindow.cpp
JSIntegration.cpp
Readers/XSV.cpp
Spreadsheet.cpp
SpreadsheetModel.cpp
SpreadsheetView.cpp
SpreadsheetWidget.cpp
Workbook.cpp
main.cpp
)
serenity_app(Spreadsheet ICON app-spreadsheet)
target_link_libraries(Spreadsheet LibGUI LibJS LibWeb)

View file

@ -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.
*/
#include "Cell.h"
#include "Spreadsheet.h"
#include <AK/StringBuilder.h>
#include <AK/TemporaryChange.h>
namespace Spreadsheet {
void Cell::set_data(String new_data)
{
if (m_data == new_data)
return;
if (new_data.starts_with("=")) {
new_data = new_data.substring(1, new_data.length() - 1);
m_kind = Formula;
} else {
m_kind = LiteralString;
}
m_data = move(new_data);
m_dirty = true;
m_evaluated_externally = false;
}
void Cell::set_data(JS::Value new_data)
{
m_dirty = true;
m_evaluated_externally = true;
StringBuilder builder;
builder.append(new_data.to_string_without_side_effects());
m_data = builder.build();
m_evaluated_data = move(new_data);
}
void Cell::set_type(const CellType* type)
{
m_type = type;
}
void Cell::set_type(const StringView& name)
{
auto* cell_type = CellType::get_by_name(name);
if (cell_type) {
return set_type(cell_type);
}
ASSERT_NOT_REACHED();
}
void Cell::set_type_metadata(CellTypeMetadata&& metadata)
{
m_type_metadata = move(metadata);
}
const CellType& Cell::type() const
{
if (m_type)
return *m_type;
if (m_kind == LiteralString) {
if (m_data.to_int().has_value())
return *CellType::get_by_name("Numeric");
}
return *CellType::get_by_name("Identity");
}
String Cell::typed_display() const
{
return type().display(const_cast<Cell&>(*this), m_type_metadata);
}
JS::Value Cell::typed_js_data() const
{
return type().js_value(const_cast<Cell&>(*this), m_type_metadata);
}
void Cell::update_data(Badge<Sheet>)
{
TemporaryChange cell_change { m_sheet->current_evaluated_cell(), this };
if (!m_dirty)
return;
m_js_exception = {};
if (m_dirty) {
m_dirty = false;
if (m_kind == Formula) {
if (!m_evaluated_externally) {
auto [value, exception] = m_sheet->evaluate(m_data, this);
m_evaluated_data = value;
m_js_exception = move(exception);
}
}
for (auto& ref : m_referencing_cells) {
if (ref) {
ref->m_dirty = true;
ref->update();
}
}
}
m_evaluated_formats.background_color.clear();
m_evaluated_formats.foreground_color.clear();
if (!m_js_exception) {
StringBuilder builder;
for (auto& fmt : m_conditional_formats) {
if (!fmt.condition.is_empty()) {
builder.clear();
builder.append("return (");
builder.append(fmt.condition);
builder.append(')');
auto [value, exception] = m_sheet->evaluate(builder.string_view(), this);
if (exception) {
m_js_exception = move(exception);
} else {
if (value.to_boolean()) {
if (fmt.background_color.has_value())
m_evaluated_formats.background_color = fmt.background_color;
if (fmt.foreground_color.has_value())
m_evaluated_formats.foreground_color = fmt.foreground_color;
}
}
}
}
}
}
void Cell::update()
{
m_sheet->update(*this);
}
JS::Value Cell::js_data()
{
if (m_dirty)
update();
if (m_kind == Formula)
return m_evaluated_data;
return JS::js_string(m_sheet->interpreter().heap(), m_data);
}
String Cell::source() const
{
StringBuilder builder;
if (m_kind == Formula)
builder.append('=');
builder.append(m_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 (!m_referencing_cells.find_if([other](const auto& ptr) { return ptr.ptr() == other; }).is_end())
return;
m_referencing_cells.append(other->make_weak_ptr());
}
void Cell::copy_from(const Cell& other)
{
m_dirty = true;
m_evaluated_externally = other.m_evaluated_externally;
m_data = other.m_data;
m_evaluated_data = other.m_evaluated_data;
m_kind = other.m_kind;
m_type = other.m_type;
m_type_metadata = other.m_type_metadata;
m_conditional_formats = other.m_conditional_formats;
m_evaluated_formats = other.m_evaluated_formats;
if (!other.m_js_exception)
m_js_exception = other.m_js_exception;
}
}

View file

@ -0,0 +1,138 @@
/*
* 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 "CellType/Type.h"
#include "ConditionalFormatting.h"
#include "Forward.h"
#include "JSIntegration.h"
#include "Position.h"
#include <AK/String.h>
#include <AK/Types.h>
#include <AK/WeakPtr.h>
namespace Spreadsheet {
struct Cell : public Weakable<Cell> {
enum Kind {
LiteralString,
Formula,
};
Cell(String data, Position position, WeakPtr<Sheet> sheet)
: m_dirty(false)
, m_data(move(data))
, m_kind(LiteralString)
, m_sheet(sheet)
, m_position(move(position))
{
}
Cell(String source, JS::Value&& cell_value, Position position, WeakPtr<Sheet> sheet)
: m_dirty(false)
, m_data(move(source))
, m_evaluated_data(move(cell_value))
, m_kind(Formula)
, m_sheet(sheet)
, m_position(move(position))
{
}
void reference_from(Cell*);
void set_data(String new_data);
void set_data(JS::Value new_data);
bool dirty() const { return m_dirty; }
void clear_dirty() { m_dirty = false; }
void set_exception(JS::Exception* exc) { m_js_exception = exc; }
JS::Exception* exception() const { return m_js_exception; }
const String& data() const { return m_data; }
const JS::Value& evaluated_data() const { return m_evaluated_data; }
Kind kind() const { return m_kind; }
const Vector<WeakPtr<Cell>>& referencing_cells() const { return m_referencing_cells; }
void set_type(const StringView& name);
void set_type(const CellType*);
void set_type_metadata(CellTypeMetadata&&);
const Position& position() const { return m_position; }
void set_position(Position position, Badge<Sheet>)
{
if (position != m_position) {
m_dirty = true;
m_position = move(position);
}
}
const Format& evaluated_formats() const { return m_evaluated_formats; }
Format& evaluated_formats() { return m_evaluated_formats; }
const Vector<ConditionalFormat>& conditional_formats() const { return m_conditional_formats; }
void set_conditional_formats(Vector<ConditionalFormat>&& fmts)
{
m_dirty = true;
m_conditional_formats = move(fmts);
}
String typed_display() const;
JS::Value typed_js_data() const;
const CellType& type() const;
const CellTypeMetadata& type_metadata() const { return m_type_metadata; }
CellTypeMetadata& type_metadata() { return m_type_metadata; }
String source() const;
JS::Value js_data();
void update();
void update_data(Badge<Sheet>);
const Sheet& sheet() const { return *m_sheet; }
Sheet& sheet() { return *m_sheet; }
void copy_from(const Cell&);
private:
bool m_dirty { false };
bool m_evaluated_externally { false };
String m_data;
JS::Value m_evaluated_data;
JS::Exception* m_js_exception { nullptr };
Kind m_kind { LiteralString };
WeakPtr<Sheet> m_sheet;
Vector<WeakPtr<Cell>> m_referencing_cells;
const CellType* m_type { nullptr };
CellTypeMetadata m_type_metadata;
Position m_position;
Vector<ConditionalFormat> m_conditional_formats;
Format m_evaluated_formats;
};
}

View file

@ -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.
*/
#include "CellSyntaxHighlighter.h"
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/Palette.h>
#include <LibJS/Lexer.h>
namespace Spreadsheet {
void CellSyntaxHighlighter::rehighlight(Gfx::Palette palette)
{
ASSERT(m_editor);
auto text = m_editor->text();
m_editor->document().spans().clear();
if (!text.starts_with('=')) {
m_editor->update();
return;
}
JSSyntaxHighlighter::rehighlight(palette);
// Highlight the '='
m_editor->document().spans().empend(
GUI::TextRange { { 0, 0 }, { 0, 1 } },
Gfx::TextAttributes {
palette.syntax_keyword(),
Optional<Color> {},
false,
false,
},
nullptr,
false);
if (m_cell && m_cell->exception()) {
auto range = m_cell->exception()->source_ranges().first();
GUI::TextRange text_range { { range.start.line - 1, range.start.column }, { range.end.line - 1, range.end.column - 1 } };
m_editor->document().spans().prepend(
GUI::TextDocumentSpan {
text_range,
Gfx::TextAttributes {
Color::Black,
Color::Red,
false,
false,
},
nullptr,
false });
}
m_editor->update();
}
CellSyntaxHighlighter::~CellSyntaxHighlighter()
{
}
}

View file

@ -0,0 +1,47 @@
/*
* 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 "Cell.h"
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/SyntaxHighlighter.h>
namespace Spreadsheet {
class CellSyntaxHighlighter final : public GUI::JSSyntaxHighlighter {
public:
CellSyntaxHighlighter() { }
virtual ~CellSyntaxHighlighter() override;
virtual void rehighlight(Gfx::Palette) override;
void set_cell(const Cell* cell) { m_cell = cell; }
private:
const Cell* m_cell { nullptr };
};
}

View file

@ -0,0 +1,68 @@
/*
* 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 "Date.h"
#include "../Cell.h"
#include "../Spreadsheet.h"
#include <AK/ScopeGuard.h>
#include <LibCore/DateTime.h>
namespace Spreadsheet {
DateCell::DateCell()
: CellType("Date")
{
}
DateCell::~DateCell()
{
}
String DateCell::display(Cell& cell, const CellTypeMetadata& metadata) const
{
ScopeGuard propagate_exception { [&cell] {
if (auto exc = cell.sheet().interpreter().exception()) {
cell.sheet().interpreter().vm().clear_exception();
cell.set_exception(exc);
}
} };
auto timestamp = js_value(cell, metadata);
auto string = Core::DateTime::from_timestamp(timestamp.to_i32(cell.sheet().global_object())).to_string(metadata.format.is_empty() ? "%Y-%m-%d %H:%M:%S" : metadata.format.characters());
if (metadata.length >= 0)
return string.substring(0, metadata.length);
return string;
}
JS::Value DateCell::js_value(Cell& cell, const CellTypeMetadata&) const
{
auto js_data = cell.js_data();
auto value = js_data.to_double(cell.sheet().global_object());
return JS::Value(value / 1000); // Turn it to seconds
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 "Type.h"
namespace Spreadsheet {
class DateCell : public CellType {
public:
DateCell();
virtual ~DateCell() override;
virtual String display(Cell&, const CellTypeMetadata&) const override;
virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
};
}

View file

@ -0,0 +1,69 @@
/*
* 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 "Format.h"
#include <AK/PrintfImplementation.h>
#include <AK/String.h>
#include <AK/StringBuilder.h>
namespace Spreadsheet {
template<typename T, typename V>
struct SingleEntryListNext {
ALWAYS_INLINE T operator()(V value) const
{
return (T)value;
}
};
template<typename PutChFunc, typename ArgumentListRefT, template<typename T, typename U = ArgumentListRefT> typename NextArgument>
struct PrintfImpl : public PrintfImplementation::PrintfImpl<PutChFunc, ArgumentListRefT, NextArgument> {
ALWAYS_INLINE PrintfImpl(PutChFunc& putch, char*& bufptr, const int& nwritten)
: PrintfImplementation::PrintfImpl<PutChFunc, ArgumentListRefT, NextArgument>(putch, bufptr, nwritten)
{
}
// Disallow pointer formats.
ALWAYS_INLINE int format_n(const PrintfImplementation::ModifierState&, ArgumentListRefT&) const
{
return 0;
}
ALWAYS_INLINE int format_s(const PrintfImplementation::ModifierState&, ArgumentListRefT&) const
{
return 0;
}
};
String format_double(const char* format, double value)
{
StringBuilder builder;
auto putch = [&](auto, auto ch) { builder.append(ch); };
printf_internal<decltype(putch), PrintfImpl, double, SingleEntryListNext>(putch, nullptr, format, value);
return builder.build();
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Forward.h>
namespace Spreadsheet {
String format_double(const char* format, double value);
}

View file

@ -0,0 +1,52 @@
/*
* 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 "Identity.h"
#include "../Cell.h"
#include "../Spreadsheet.h"
namespace Spreadsheet {
IdentityCell::IdentityCell()
: CellType("Identity")
{
}
IdentityCell::~IdentityCell()
{
}
String IdentityCell::display(Cell& cell, const CellTypeMetadata&) const
{
return cell.js_data().to_string_without_side_effects();
}
JS::Value IdentityCell::js_value(Cell& cell, const CellTypeMetadata&) const
{
return cell.js_data();
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 "Type.h"
namespace Spreadsheet {
class IdentityCell : public CellType {
public:
IdentityCell();
virtual ~IdentityCell() override;
virtual String display(Cell&, const CellTypeMetadata&) const override;
virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
};
}

View file

@ -0,0 +1,76 @@
/*
* 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 "Numeric.h"
#include "../Cell.h"
#include "../Spreadsheet.h"
#include "Format.h"
#include <AK/ScopeGuard.h>
namespace Spreadsheet {
NumericCell::NumericCell()
: CellType("Numeric")
{
}
NumericCell::~NumericCell()
{
}
String NumericCell::display(Cell& cell, const CellTypeMetadata& metadata) const
{
ScopeGuard propagate_exception { [&cell] {
if (auto exc = cell.sheet().interpreter().exception()) {
cell.sheet().interpreter().vm().clear_exception();
cell.set_exception(exc);
}
} };
auto value = js_value(cell, metadata);
String string;
if (metadata.format.is_empty())
string = value.to_string_without_side_effects();
else
string = format_double(metadata.format.characters(), value.to_double(cell.sheet().global_object()));
if (metadata.length >= 0)
return string.substring(0, metadata.length);
return string;
}
JS::Value NumericCell::js_value(Cell& cell, const CellTypeMetadata&) const
{
ScopeGuard propagate_exception { [&cell] {
if (auto exc = cell.sheet().interpreter().exception()) {
cell.sheet().interpreter().vm().clear_exception();
cell.set_exception(exc);
}
} };
return cell.js_data().to_number(cell.sheet().global_object());
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 "Type.h"
namespace Spreadsheet {
class NumericCell : public CellType {
public:
NumericCell();
virtual ~NumericCell() override;
virtual String display(Cell&, const CellTypeMetadata&) const override;
virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
};
}

View file

@ -0,0 +1,57 @@
/*
* 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 "String.h"
#include "../Cell.h"
#include "../Spreadsheet.h"
namespace Spreadsheet {
StringCell::StringCell()
: CellType("String")
{
}
StringCell::~StringCell()
{
}
String StringCell::display(Cell& cell, const CellTypeMetadata& metadata) const
{
auto string = cell.js_data().to_string_without_side_effects();
if (metadata.length >= 0)
return string.substring(0, metadata.length);
return string;
}
JS::Value StringCell::js_value(Cell& cell, const CellTypeMetadata& metadata) const
{
auto string = display(cell, metadata);
return JS::js_string(cell.sheet().interpreter().heap(), string);
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 "Type.h"
namespace Spreadsheet {
class StringCell : public CellType {
public:
StringCell();
virtual ~StringCell() override;
virtual String display(Cell&, const CellTypeMetadata&) const override;
virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
};
}

View file

@ -0,0 +1,63 @@
/*
* 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 "Type.h"
#include "Date.h"
#include "Identity.h"
#include "Numeric.h"
#include "String.h"
#include <AK/HashMap.h>
#include <AK/OwnPtr.h>
static HashMap<String, Spreadsheet::CellType*> s_cell_types;
static Spreadsheet::StringCell s_string_cell;
static Spreadsheet::NumericCell s_numeric_cell;
static Spreadsheet::IdentityCell s_identity_cell;
static Spreadsheet::DateCell s_date_cell;
namespace Spreadsheet {
const CellType* CellType::get_by_name(const StringView& name)
{
return s_cell_types.get(name).value_or(nullptr);
}
Vector<StringView> CellType::names()
{
Vector<StringView> names;
for (auto& it : s_cell_types)
names.append(it.key);
return names;
}
CellType::CellType(const StringView& name)
: m_name(name)
{
ASSERT(!s_cell_types.contains(name));
s_cell_types.set(name, this);
}
}

View file

@ -0,0 +1,64 @@
/*
* 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 "../ConditionalFormatting.h"
#include "../Forward.h"
#include <AK/Forward.h>
#include <AK/String.h>
#include <LibGfx/Color.h>
#include <LibGfx/TextAlignment.h>
#include <LibJS/Forward.h>
namespace Spreadsheet {
struct CellTypeMetadata {
int length { -1 };
String format;
Gfx::TextAlignment alignment { Gfx::TextAlignment::CenterRight };
Format static_format;
};
class CellType {
public:
static const CellType* get_by_name(const StringView&);
static Vector<StringView> names();
virtual String display(Cell&, const CellTypeMetadata&) const = 0;
virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const = 0;
virtual ~CellType() { }
const String& name() const { return m_name; }
protected:
CellType(const StringView& name);
private:
String m_name;
};
}

View file

@ -0,0 +1,483 @@
/*
* 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 "CellTypeDialog.h"
#include "Cell.h"
#include "Spreadsheet.h"
#include <AK/StringBuilder.h>
#include <Applications/Spreadsheet/CondFormattingGML.h>
#include <Applications/Spreadsheet/CondFormattingViewGML.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/ColorInput.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/Label.h>
#include <LibGUI/ListView.h>
#include <LibGUI/SpinBox.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Widget.h>
#include <LibGfx/FontDatabase.h>
REGISTER_WIDGET(Spreadsheet, ConditionsView);
namespace Spreadsheet {
CellTypeDialog::CellTypeDialog(const Vector<Position>& positions, Sheet& sheet, GUI::Window* parent)
: GUI::Dialog(parent)
{
ASSERT(!positions.is_empty());
StringBuilder builder;
if (positions.size() == 1)
builder.appendff("Format cell {}{}", positions.first().column, positions.first().row);
else
builder.appendff("Format {} cells", positions.size());
set_title(builder.string_view());
set_icon(parent->icon());
resize(285, 360);
auto& main_widget = set_main_widget<GUI::Widget>();
main_widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
main_widget.set_fill_with_background_color(true);
auto& tab_widget = main_widget.add<GUI::TabWidget>();
setup_tabs(tab_widget, positions, sheet);
auto& buttonbox = main_widget.add<GUI::Widget>();
buttonbox.set_shrink_to_fit(true);
auto& button_layout = buttonbox.set_layout<GUI::HorizontalBoxLayout>();
button_layout.set_spacing(10);
button_layout.add_spacer();
auto& ok_button = buttonbox.add<GUI::Button>("OK");
ok_button.set_fixed_width(80);
ok_button.on_click = [&](auto) { done(ExecOK); };
}
const Vector<String> g_horizontal_alignments { "Left", "Center", "Right" };
const Vector<String> g_vertical_alignments { "Top", "Center", "Bottom" };
Vector<String> g_types;
constexpr static CellTypeDialog::VerticalAlignment vertical_alignment_from(Gfx::TextAlignment alignment)
{
switch (alignment) {
case Gfx::TextAlignment::CenterRight:
case Gfx::TextAlignment::CenterLeft:
case Gfx::TextAlignment::Center:
return CellTypeDialog::VerticalAlignment::Center;
case Gfx::TextAlignment::TopRight:
case Gfx::TextAlignment::TopLeft:
return CellTypeDialog::VerticalAlignment::Top;
case Gfx::TextAlignment::BottomRight:
return CellTypeDialog::VerticalAlignment::Bottom;
}
return CellTypeDialog::VerticalAlignment::Center;
}
constexpr static CellTypeDialog::HorizontalAlignment horizontal_alignment_from(Gfx::TextAlignment alignment)
{
switch (alignment) {
case Gfx::TextAlignment::Center:
return CellTypeDialog::HorizontalAlignment::Center;
case Gfx::TextAlignment::CenterRight:
case Gfx::TextAlignment::TopRight:
case Gfx::TextAlignment::BottomRight:
return CellTypeDialog::HorizontalAlignment::Right;
case Gfx::TextAlignment::TopLeft:
case Gfx::TextAlignment::CenterLeft:
return CellTypeDialog::HorizontalAlignment::Left;
}
return CellTypeDialog::HorizontalAlignment::Right;
}
void CellTypeDialog::setup_tabs(GUI::TabWidget& tabs, const Vector<Position>& positions, Sheet& sheet)
{
g_types.clear();
for (auto& type_name : CellType::names())
g_types.append(type_name);
Vector<Cell*> cells;
for (auto& position : positions) {
if (auto cell = sheet.at(position))
cells.append(cell);
}
if (cells.size() == 1) {
auto& cell = *cells.first();
m_format = cell.type_metadata().format;
m_length = cell.type_metadata().length;
m_type = &cell.type();
m_vertical_alignment = vertical_alignment_from(cell.type_metadata().alignment);
m_horizontal_alignment = horizontal_alignment_from(cell.type_metadata().alignment);
m_static_format = cell.type_metadata().static_format;
m_conditional_formats = cell.conditional_formats();
}
auto& type_tab = tabs.add_tab<GUI::Widget>("Type");
type_tab.set_layout<GUI::HorizontalBoxLayout>().set_margins({ 4, 4, 4, 4 });
{
auto& left_side = type_tab.add<GUI::Widget>();
left_side.set_layout<GUI::VerticalBoxLayout>();
auto& right_side = type_tab.add<GUI::Widget>();
right_side.set_layout<GUI::VerticalBoxLayout>();
right_side.set_fixed_width(170);
auto& type_list = left_side.add<GUI::ListView>();
type_list.set_model(*GUI::ItemListModel<String>::create(g_types));
type_list.set_should_hide_unnecessary_scrollbars(true);
type_list.on_selection = [&](auto& index) {
if (!index.is_valid()) {
m_type = nullptr;
return;
}
m_type = CellType::get_by_name(g_types.at(index.row()));
};
{
auto& checkbox = right_side.add<GUI::CheckBox>("Override max length");
auto& spinbox = right_side.add<GUI::SpinBox>();
checkbox.set_checked(m_length != -1);
spinbox.set_min(0);
spinbox.set_enabled(m_length != -1);
if (m_length > -1)
spinbox.set_value(m_length);
checkbox.on_checked = [&](auto checked) {
spinbox.set_enabled(checked);
if (!checked) {
m_length = -1;
spinbox.set_value(0);
}
};
spinbox.on_change = [&](auto value) {
m_length = value;
};
}
{
auto& checkbox = right_side.add<GUI::CheckBox>("Override display format");
auto& editor = right_side.add<GUI::TextEditor>();
checkbox.set_checked(!m_format.is_empty());
editor.set_should_hide_unnecessary_scrollbars(true);
editor.set_enabled(!m_format.is_empty());
editor.set_text(m_format);
checkbox.on_checked = [&](auto checked) {
editor.set_enabled(checked);
if (!checked)
m_format = String::empty();
editor.set_text(m_format);
};
editor.on_change = [&] {
m_format = editor.text();
};
}
}
auto& alignment_tab = tabs.add_tab<GUI::Widget>("Alignment");
alignment_tab.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
{
// FIXME: Frame?
// Horizontal alignment
{
auto& horizontal_alignment_selection_container = alignment_tab.add<GUI::Widget>();
horizontal_alignment_selection_container.set_layout<GUI::HorizontalBoxLayout>();
horizontal_alignment_selection_container.layout()->set_margins({ 0, 4, 0, 0 });
horizontal_alignment_selection_container.set_fixed_height(22);
auto& horizontal_alignment_label = horizontal_alignment_selection_container.add<GUI::Label>();
horizontal_alignment_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
horizontal_alignment_label.set_text("Horizontal text alignment");
auto& horizontal_combobox = alignment_tab.add<GUI::ComboBox>();
horizontal_combobox.set_only_allow_values_from_model(true);
horizontal_combobox.set_model(*GUI::ItemListModel<String>::create(g_horizontal_alignments));
horizontal_combobox.set_selected_index((int)m_horizontal_alignment);
horizontal_combobox.on_change = [&](auto&, const GUI::ModelIndex& index) {
switch (index.row()) {
case 0:
m_horizontal_alignment = HorizontalAlignment::Left;
break;
case 1:
m_horizontal_alignment = HorizontalAlignment::Center;
break;
case 2:
m_horizontal_alignment = HorizontalAlignment::Right;
break;
default:
ASSERT_NOT_REACHED();
}
};
}
// Vertical alignment
{
auto& vertical_alignment_container = alignment_tab.add<GUI::Widget>();
vertical_alignment_container.set_layout<GUI::HorizontalBoxLayout>();
vertical_alignment_container.layout()->set_margins({ 0, 4, 0, 0 });
vertical_alignment_container.set_fixed_height(22);
auto& vertical_alignment_label = vertical_alignment_container.add<GUI::Label>();
vertical_alignment_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
vertical_alignment_label.set_text("Vertical text alignment");
auto& vertical_combobox = alignment_tab.add<GUI::ComboBox>();
vertical_combobox.set_only_allow_values_from_model(true);
vertical_combobox.set_model(*GUI::ItemListModel<String>::create(g_vertical_alignments));
vertical_combobox.set_selected_index((int)m_vertical_alignment);
vertical_combobox.on_change = [&](auto&, const GUI::ModelIndex& index) {
switch (index.row()) {
case 0:
m_vertical_alignment = VerticalAlignment::Top;
break;
case 1:
m_vertical_alignment = VerticalAlignment::Center;
break;
case 2:
m_vertical_alignment = VerticalAlignment::Bottom;
break;
default:
ASSERT_NOT_REACHED();
}
};
}
}
auto& colors_tab = tabs.add_tab<GUI::Widget>("Color");
colors_tab.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
{
// Static formatting
{
auto& static_formatting_container = colors_tab.add<GUI::Widget>();
static_formatting_container.set_layout<GUI::VerticalBoxLayout>();
static_formatting_container.set_shrink_to_fit(true);
// Foreground
{
// FIXME: Somehow allow unsetting these.
auto& foreground_container = static_formatting_container.add<GUI::Widget>();
foreground_container.set_layout<GUI::HorizontalBoxLayout>();
foreground_container.layout()->set_margins({ 0, 4, 0, 0 });
foreground_container.set_fixed_height(22);
auto& foreground_label = foreground_container.add<GUI::Label>();
foreground_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
foreground_label.set_text("Static foreground color");
auto& foreground_selector = foreground_container.add<GUI::ColorInput>();
if (m_static_format.foreground_color.has_value())
foreground_selector.set_color(m_static_format.foreground_color.value());
foreground_selector.on_change = [&]() {
m_static_format.foreground_color = foreground_selector.color();
};
}
// Background
{
// FIXME: Somehow allow unsetting these.
auto& background_container = static_formatting_container.add<GUI::Widget>();
background_container.set_layout<GUI::HorizontalBoxLayout>();
background_container.layout()->set_margins({ 0, 4, 0, 0 });
background_container.set_fixed_height(22);
auto& background_label = background_container.add<GUI::Label>();
background_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
background_label.set_text("Static background color");
auto& background_selector = background_container.add<GUI::ColorInput>();
if (m_static_format.background_color.has_value())
background_selector.set_color(m_static_format.background_color.value());
background_selector.on_change = [&]() {
m_static_format.background_color = background_selector.color();
};
}
}
}
auto& conditional_fmt_tab = tabs.add_tab<GUI::Widget>("Conditional format");
conditional_fmt_tab.load_from_gml(cond_fmt_gml);
{
auto& view = *conditional_fmt_tab.find_descendant_of_type_named<Spreadsheet::ConditionsView>("conditions_view");
view.set_formats(&m_conditional_formats);
auto& add_button = *conditional_fmt_tab.find_descendant_of_type_named<GUI::Button>("add_button");
add_button.on_click = [&](auto) {
view.add_format();
};
// FIXME: Disable this when empty.
auto& remove_button = *conditional_fmt_tab.find_descendant_of_type_named<GUI::Button>("remove_button");
remove_button.on_click = [&](auto) {
view.remove_top();
};
}
}
CellTypeMetadata CellTypeDialog::metadata() const
{
CellTypeMetadata metadata;
metadata.format = m_format;
metadata.length = m_length;
metadata.static_format = m_static_format;
switch (m_vertical_alignment) {
case VerticalAlignment::Top:
switch (m_horizontal_alignment) {
case HorizontalAlignment::Left:
metadata.alignment = Gfx::TextAlignment::TopLeft;
break;
case HorizontalAlignment::Center:
metadata.alignment = Gfx::TextAlignment::Center; // TopCenter?
break;
case HorizontalAlignment::Right:
metadata.alignment = Gfx::TextAlignment::TopRight;
break;
}
break;
case VerticalAlignment::Center:
switch (m_horizontal_alignment) {
case HorizontalAlignment::Left:
metadata.alignment = Gfx::TextAlignment::CenterLeft;
break;
case HorizontalAlignment::Center:
metadata.alignment = Gfx::TextAlignment::Center;
break;
case HorizontalAlignment::Right:
metadata.alignment = Gfx::TextAlignment::CenterRight;
break;
}
break;
case VerticalAlignment::Bottom:
switch (m_horizontal_alignment) {
case HorizontalAlignment::Left:
metadata.alignment = Gfx::TextAlignment::CenterLeft; // BottomLeft?
break;
case HorizontalAlignment::Center:
metadata.alignment = Gfx::TextAlignment::Center;
break;
case HorizontalAlignment::Right:
metadata.alignment = Gfx::TextAlignment::BottomRight;
break;
}
break;
}
return metadata;
}
ConditionView::ConditionView(ConditionalFormat& fmt)
: m_format(fmt)
{
load_from_gml(cond_fmt_view_gml);
auto& fg_input = *find_descendant_of_type_named<GUI::ColorInput>("foreground_input");
auto& bg_input = *find_descendant_of_type_named<GUI::ColorInput>("background_input");
auto& formula_editor = *find_descendant_of_type_named<GUI::TextEditor>("formula_editor");
if (m_format.foreground_color.has_value())
fg_input.set_color(m_format.foreground_color.value());
if (m_format.background_color.has_value())
bg_input.set_color(m_format.background_color.value());
formula_editor.set_text(m_format.condition);
// FIXME: Allow unsetting these.
fg_input.on_change = [&] {
m_format.foreground_color = fg_input.color();
};
bg_input.on_change = [&] {
m_format.background_color = bg_input.color();
};
formula_editor.set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>());
formula_editor.set_should_hide_unnecessary_scrollbars(true);
formula_editor.set_font(&Gfx::FontDatabase::default_fixed_width_font());
formula_editor.on_change = [&] {
m_format.condition = formula_editor.text();
};
}
ConditionView::~ConditionView()
{
}
ConditionsView::ConditionsView()
{
set_layout<GUI::VerticalBoxLayout>().set_spacing(2);
}
void ConditionsView::set_formats(Vector<ConditionalFormat>* formats)
{
ASSERT(!m_formats);
m_formats = formats;
for (auto& entry : *m_formats)
m_widgets.append(add<ConditionView>(entry));
}
void ConditionsView::add_format()
{
ASSERT(m_formats);
m_formats->empend();
auto& last = m_formats->last();
m_widgets.append(add<ConditionView>(last));
update();
}
void ConditionsView::remove_top()
{
ASSERT(m_formats);
if (m_formats->is_empty())
return;
m_formats->take_last();
m_widgets.take_last()->remove_from_parent();
update();
}
ConditionsView::~ConditionsView()
{
}
}

View file

@ -0,0 +1,69 @@
/*
* 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 "CellType/Type.h"
#include "ConditionalFormatting.h"
#include "Forward.h"
#include <LibGUI/Dialog.h>
namespace Spreadsheet {
class CellTypeDialog : public GUI::Dialog {
C_OBJECT(CellTypeDialog);
public:
CellTypeMetadata metadata() const;
const CellType* type() const { return m_type; }
Vector<ConditionalFormat> conditional_formats() { return m_conditional_formats; }
enum class HorizontalAlignment : int {
Left = 0,
Center,
Right,
};
enum class VerticalAlignment : int {
Top = 0,
Center,
Bottom,
};
private:
CellTypeDialog(const Vector<Position>&, Sheet&, GUI::Window* parent = nullptr);
void setup_tabs(GUI::TabWidget&, const Vector<Position>&, Sheet&);
const CellType* m_type { nullptr };
int m_length { -1 };
String m_format;
HorizontalAlignment m_horizontal_alignment { HorizontalAlignment::Right };
VerticalAlignment m_vertical_alignment { VerticalAlignment::Center };
Format m_static_format;
Vector<ConditionalFormat> m_conditional_formats;
};
}

View file

@ -0,0 +1,40 @@
@GUI::Widget {
name: "main"
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
margins: [4, 4, 4, 4]
spacing: 4
}
@Spreadsheet::ConditionsView {
name: "conditions_view"
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
spacing: 10
}
@GUI::Widget {
}
@GUI::Button {
name: "add_button"
text: "Add"
fixed_width: 70
}
@GUI::Button {
name: "remove_button"
text: "Remove"
fixed_width: 70
}
@GUI::Widget {
}
}
}

View file

@ -0,0 +1,54 @@
@GUI::Widget {
layout: @GUI::VerticalBoxLayout {
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "if..."
fixed_width: 40
}
@GUI::TextEditor {
name: "formula_editor"
fixed_height: 25
tooltip: "Use 'value' to refer to the current cell's value"
}
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Foreground..."
fixed_width: 150
}
@GUI::ColorInput {
name: "foreground_input"
}
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Background..."
fixed_width: 150
}
@GUI::ColorInput {
name: "background_input"
}
}
}

View file

@ -0,0 +1,73 @@
/*
* 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 "Forward.h"
#include <AK/String.h>
#include <LibGUI/ScrollableWidget.h>
#include <LibGfx/Color.h>
namespace Spreadsheet {
struct Format {
Optional<Color> foreground_color;
Optional<Color> background_color;
};
struct ConditionalFormat : public Format {
String condition;
};
class ConditionView : public GUI::Widget {
C_OBJECT(ConditionView)
public:
virtual ~ConditionView() override;
private:
ConditionView(ConditionalFormat&);
ConditionalFormat& m_format;
};
class ConditionsView : public GUI::Widget {
C_OBJECT(ConditionsView)
public:
virtual ~ConditionsView() override;
void set_formats(Vector<ConditionalFormat>*);
void add_format();
void remove_top();
private:
ConditionsView();
Vector<ConditionalFormat>* m_formats { nullptr };
NonnullRefPtrVector<GUI::Widget> m_widgets;
};
}

View file

@ -0,0 +1,41 @@
/*
* 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
namespace Spreadsheet {
class ConditionView;
class Sheet;
class SheetGlobalObject;
class Workbook;
class WorkbookObject;
struct Cell;
struct ConditionalFormat;
struct Format;
struct Position;
}

View file

@ -0,0 +1,229 @@
/*
* 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 "HelpWindow.h"
#include "SpreadsheetWidget.h"
#include <AK/LexicalPath.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Frame.h>
#include <LibGUI/ListView.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Model.h>
#include <LibGUI/Splitter.h>
#include <LibMarkdown/Document.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/OutOfProcessWebView.h>
namespace Spreadsheet {
class HelpListModel final : public GUI::Model {
public:
static NonnullRefPtr<HelpListModel> create() { return adopt(*new HelpListModel); }
virtual ~HelpListModel() override { }
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_keys.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
virtual void update() override { }
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role = GUI::ModelRole::Display) const override
{
if (role == GUI::ModelRole::Display) {
return key(index);
}
return {};
}
String key(const GUI::ModelIndex& index) const { return m_keys[index.row()]; }
void set_from(const JsonObject& object)
{
m_keys.clear();
object.for_each_member([this](auto& name, auto&) {
m_keys.append(name);
});
did_update();
}
private:
HelpListModel()
{
}
Vector<String> m_keys;
};
RefPtr<HelpWindow> HelpWindow::s_the { nullptr };
HelpWindow::HelpWindow(GUI::Window* parent)
: GUI::Window(parent)
{
resize(530, 365);
set_title("Spreadsheet Functions Help");
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-help.png"));
auto& widget = set_main_widget<GUI::Widget>();
widget.set_layout<GUI::VerticalBoxLayout>();
widget.set_fill_with_background_color(true);
auto& splitter = widget.add<GUI::HorizontalSplitter>();
auto& left_frame = splitter.add<GUI::Frame>();
left_frame.set_layout<GUI::VerticalBoxLayout>();
left_frame.set_fixed_width(100);
m_listview = left_frame.add<GUI::ListView>();
m_listview->set_activates_on_selection(true);
m_listview->set_model(HelpListModel::create());
m_webview = splitter.add<Web::OutOfProcessWebView>();
m_webview->on_link_click = [this](auto& url, auto&, auto&&) {
ASSERT(url.protocol() == "spreadsheet");
if (url.host() == "example") {
auto entry = LexicalPath(url.path()).basename();
auto doc_option = m_docs.get(entry);
if (!doc_option.is_object()) {
GUI::MessageBox::show_error(this, String::formatted("No documentation entry found for '{}'", url.path()));
return;
}
auto& doc = doc_option.as_object();
const auto& name = url.fragment();
auto example_data_value = doc.get_or("example_data", JsonObject {});
if (!example_data_value.is_object()) {
GUI::MessageBox::show_error(this, String::formatted("No example data found for '{}'", url.path()));
return;
}
auto& example_data = example_data_value.as_object();
auto value = example_data.get(name);
if (!value.is_object()) {
GUI::MessageBox::show_error(this, String::formatted("Example '{}' not found for '{}'", name, url.path()));
return;
}
auto window = GUI::Window::construct(this);
window->resize(size());
window->set_icon(icon());
window->set_title(String::formatted("Spreadsheet Help - Example {} for {}", name, entry));
window->on_close = [window = window.ptr()] { window->remove_from_parent(); };
auto& widget = window->set_main_widget<SpreadsheetWidget>(NonnullRefPtrVector<Sheet> {}, false);
auto sheet = Sheet::from_json(value.as_object(), widget.workbook());
if (!sheet) {
GUI::MessageBox::show_error(this, String::formatted("Corrupted example '{}' in '{}'", name, url.path()));
return;
}
widget.add_sheet(sheet.release_nonnull());
window->show();
} else if (url.host() == "doc") {
auto entry = LexicalPath(url.path()).basename();
m_webview->load(URL::create_with_data("text/html", render(entry)));
} else {
dbgln("Invalid spreadsheet action domain '{}'", url.host());
}
};
m_listview->on_activation = [this](auto& index) {
if (!m_webview)
return;
auto key = static_cast<HelpListModel*>(m_listview->model())->key(index);
m_webview->load(URL::create_with_data("text/html", render(key)));
};
}
String HelpWindow::render(const StringView& key)
{
auto doc_option = m_docs.get(key);
ASSERT(doc_option.is_object());
auto& doc = doc_option.as_object();
auto name = doc.get("name").to_string();
auto argc = doc.get("argc").to_u32(0);
auto argnames_value = doc.get("argnames");
ASSERT(argnames_value.is_array());
auto& argnames = argnames_value.as_array();
auto docstring = doc.get("doc").to_string();
auto examples_value = doc.get_or("examples", JsonObject {});
ASSERT(examples_value.is_object());
auto& examples = examples_value.as_object();
StringBuilder markdown_builder;
markdown_builder.append("# NAME\n`");
markdown_builder.append(name);
markdown_builder.append("`\n\n");
markdown_builder.append("# ARGUMENTS\n");
if (argc > 0)
markdown_builder.appendff("{} required argument(s):\n", argc);
else
markdown_builder.appendf("No required arguments.\n");
for (size_t i = 0; i < argc; ++i)
markdown_builder.appendff("- `{}`\n", argnames.at(i).to_string());
if (argc > 0)
markdown_builder.append("\n");
if ((size_t)argnames.size() > argc) {
auto opt_count = argnames.size() - argc;
markdown_builder.appendff("{} optional argument(s):\n", opt_count);
for (size_t i = argc; i < (size_t)argnames.size(); ++i)
markdown_builder.appendff("- `{}`\n", argnames.at(i).to_string());
markdown_builder.append("\n");
}
markdown_builder.append("# DESCRIPTION\n");
markdown_builder.append(docstring);
markdown_builder.append("\n\n");
if (!examples.is_empty()) {
markdown_builder.append("# EXAMPLES\n");
examples.for_each_member([&](auto& text, auto& description_value) {
dbgln("- {}\n\n```js\n{}\n```\n", description_value.to_string(), text);
markdown_builder.appendff("- {}\n\n```js\n{}\n```\n", description_value.to_string(), text);
});
}
auto document = Markdown::Document::parse(markdown_builder.string_view());
return document->render_to_html();
}
void HelpWindow::set_docs(JsonObject&& docs)
{
m_docs = move(docs);
static_cast<HelpListModel*>(m_listview->model())->set_from(m_docs);
m_listview->update();
}
HelpWindow::~HelpWindow()
{
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/JsonObject.h>
#include <LibGUI/Dialog.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibWeb/OutOfProcessWebView.h>
namespace Spreadsheet {
class HelpWindow : public GUI::Window {
C_OBJECT(HelpWindow);
public:
static NonnullRefPtr<HelpWindow> the(GUI::Window* window)
{
if (s_the)
return *s_the;
return *(s_the = adopt(*new HelpWindow(window)));
}
virtual ~HelpWindow() override;
void set_docs(JsonObject&& docs);
private:
static RefPtr<HelpWindow> s_the;
String render(const StringView& key);
HelpWindow(GUI::Window* parent = nullptr);
JsonObject m_docs;
RefPtr<Web::OutOfProcessWebView> m_webview;
RefPtr<GUI::ListView> m_listview;
};
}

View file

@ -0,0 +1,444 @@
/*
* 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/Lexer.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/Value.h>
namespace Spreadsheet {
Optional<FunctionAndArgumentIndex> get_function_and_argument_index(StringView source)
{
JS::Lexer lexer { source };
// Track <identifier> <OpenParen>'s, and how many complete expressions are inside the parenthesised expression.
Vector<size_t> state;
StringView last_name;
Vector<StringView> names;
size_t open_parens_since_last_commit = 0;
size_t open_curlies_and_brackets_since_last_commit = 0;
bool previous_was_identifier = false;
auto token = lexer.next();
while (token.type() != JS::TokenType::Eof) {
switch (token.type()) {
case JS::TokenType::Identifier:
previous_was_identifier = true;
last_name = token.value();
break;
case JS::TokenType::ParenOpen:
if (!previous_was_identifier) {
open_parens_since_last_commit++;
break;
}
previous_was_identifier = false;
state.append(0);
names.append(last_name);
break;
case JS::TokenType::ParenClose:
previous_was_identifier = false;
if (open_parens_since_last_commit == 0) {
state.take_last();
names.take_last();
break;
}
--open_parens_since_last_commit;
break;
case JS::TokenType::Comma:
previous_was_identifier = false;
if (open_parens_since_last_commit == 0 && open_curlies_and_brackets_since_last_commit == 0) {
state.last()++;
break;
}
break;
case JS::TokenType::BracketOpen:
previous_was_identifier = false;
open_curlies_and_brackets_since_last_commit++;
break;
case JS::TokenType::BracketClose:
previous_was_identifier = false;
if (open_curlies_and_brackets_since_last_commit > 0)
open_curlies_and_brackets_since_last_commit--;
break;
case JS::TokenType::CurlyOpen:
previous_was_identifier = false;
open_curlies_and_brackets_since_last_commit++;
break;
case JS::TokenType::CurlyClose:
previous_was_identifier = false;
if (open_curlies_and_brackets_since_last_commit > 0)
open_curlies_and_brackets_since_last_commit--;
break;
default:
previous_was_identifier = false;
break;
}
token = lexer.next();
}
if (!names.is_empty() && !state.is_empty())
return FunctionAndArgumentIndex { names.last(), state.last() };
return {};
}
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 (name.as_string() == "value") {
if (auto cell = m_sheet.current_evaluated_cell())
return cell->js_data();
return JS::js_undefined();
}
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.typed_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("get_real_cell_contents", get_real_cell_contents, 1);
define_native_function("set_real_cell_contents", set_real_cell_contents, 2);
define_native_function("parse_cell_name", parse_cell_name, 1);
define_native_function("current_cell_position", current_cell_position, 0);
define_native_function("column_arithmetic", column_arithmetic, 2);
define_native_function("column_index", column_index, 1);
}
void SheetGlobalObject::visit_edges(Visitor& visitor)
{
GlobalObject::visit_edges(visitor);
for (auto& it : m_sheet.cells()) {
if (it.value->exception())
visitor.visit(it.value->exception());
visitor.visit(it.value->evaluated_data());
}
}
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_real_cell_contents)
{
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return JS::js_null();
if (StringView("SheetGlobalObject") != this_object->class_name()) {
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
return {};
}
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
if (vm.argument_count() != 1) {
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to get_real_cell_contents()");
return {};
}
auto name_value = vm.argument(0);
if (!name_value.is_string()) {
vm.throw_exception<JS::TypeError>(global_object, "Expected a String argument to get_real_cell_contents()");
return {};
}
auto position = Sheet::parse_cell_name(name_value.as_string().string());
if (!position.has_value()) {
vm.throw_exception<JS::TypeError>(global_object, "Invalid cell name");
return {};
}
const auto* cell = sheet_object->m_sheet.at(position.value());
if (!cell)
return JS::js_undefined();
if (cell->kind() == Spreadsheet::Cell::Kind::Formula)
return JS::js_string(vm.heap(), String::formatted("={}", cell->data()));
return JS::js_string(vm.heap(), cell->data());
}
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::set_real_cell_contents)
{
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return JS::js_null();
if (StringView("SheetGlobalObject") != this_object->class_name()) {
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
return {};
}
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
if (vm.argument_count() != 2) {
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly two arguments to set_real_cell_contents()");
return {};
}
auto name_value = vm.argument(0);
if (!name_value.is_string()) {
vm.throw_exception<JS::TypeError>(global_object, "Expected the first argument of set_real_cell_contents() to be a String");
return {};
}
auto position = Sheet::parse_cell_name(name_value.as_string().string());
if (!position.has_value()) {
vm.throw_exception<JS::TypeError>(global_object, "Invalid cell name");
return {};
}
auto new_contents_value = vm.argument(1);
if (!new_contents_value.is_string()) {
vm.throw_exception<JS::TypeError>(global_object, "Expected the second argument of set_real_cell_contents() to be a String");
return {};
}
auto& cell = sheet_object->m_sheet.ensure(position.value());
auto& new_contents = new_contents_value.as_string().string();
cell.set_data(new_contents);
return JS::js_null();
}
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::parse_cell_name)
{
if (vm.argument_count() != 1) {
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to parse_cell_name()");
return {};
}
auto name_value = vm.argument(0);
if (!name_value.is_string()) {
vm.throw_exception<JS::TypeError>(global_object, "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(global_object);
object->put("column", JS::js_string(vm, position.value().column));
object->put("row", JS::Value((unsigned)position.value().row));
return object;
}
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::current_cell_position)
{
if (vm.argument_count() != 0) {
vm.throw_exception<JS::TypeError>(global_object, "Expected no arguments to current_cell_position()");
return {};
}
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return JS::js_null();
if (StringView("SheetGlobalObject") != this_object->class_name()) {
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
return {};
}
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
auto* current_cell = sheet_object->m_sheet.current_evaluated_cell();
if (!current_cell)
return JS::js_null();
auto position = current_cell->position();
auto object = JS::Object::create_empty(global_object);
object->put("column", JS::js_string(vm, position.column));
object->put("row", JS::Value((unsigned)position.row));
return object;
}
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_index)
{
if (vm.argument_count() != 1) {
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to column_index()");
return {};
}
auto column_name = vm.argument(0);
if (!column_name.is_string()) {
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "String");
return {};
}
auto& column_name_str = column_name.as_string().string();
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return JS::js_null();
if (StringView("SheetGlobalObject") != this_object->class_name()) {
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
return {};
}
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
auto& sheet = sheet_object->m_sheet;
auto column_index = sheet.column_index(column_name_str);
if (!column_index.has_value()) {
vm.throw_exception(global_object, JS::TypeError::create(global_object, String::formatted("'{}' is not a valid column", column_name_str)));
return {};
}
return JS::Value((i32)column_index.value());
}
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_arithmetic)
{
if (vm.argument_count() != 2) {
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly two arguments to column_arithmetic()");
return {};
}
auto column_name = vm.argument(0);
if (!column_name.is_string()) {
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "String");
return {};
}
auto& column_name_str = column_name.as_string().string();
auto offset = vm.argument(1).to_number(global_object);
if (!offset.is_number())
return {};
auto offset_number = offset.as_i32();
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return JS::js_null();
if (StringView("SheetGlobalObject") != this_object->class_name()) {
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
return {};
}
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
auto& sheet = sheet_object->m_sheet;
auto new_column = sheet.column_arithmetic(column_name_str, offset_number);
if (!new_column.has_value()) {
vm.throw_exception(global_object, JS::TypeError::create(global_object, String::formatted("'{}' is not a valid column", column_name_str)));
return {};
}
return JS::js_string(vm, new_column.release_value());
}
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);
}
void WorkbookObject::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
for (auto& sheet : m_workbook.sheets())
visitor.visit(&sheet.global_object());
}
JS_DEFINE_NATIVE_FUNCTION(WorkbookObject::sheet)
{
if (vm.argument_count() != 1) {
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to sheet()");
return {};
}
auto name_value = vm.argument(0);
if (!name_value.is_string() && !name_value.is_number()) {
vm.throw_exception<JS::TypeError>(global_object, "Expected a String or Number argument to sheet()");
return {};
}
auto* this_object = vm.this_value(global_object).to_object(global_object);
if (!this_object)
return {};
if (!is<WorkbookObject>(this_object)) {
vm.throw_exception<JS::TypeError>(global_object, 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();
}
}

View file

@ -0,0 +1,82 @@
/*
* 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 "Forward.h"
#include <LibJS/Forward.h>
#include <LibJS/Runtime/GlobalObject.h>
namespace Spreadsheet {
struct FunctionAndArgumentIndex {
String function_name;
size_t argument_index { 0 };
};
Optional<FunctionAndArgumentIndex> get_function_and_argument_index(StringView source);
class SheetGlobalObject final : public JS::GlobalObject {
JS_OBJECT(SheetGlobalObject, JS::GlobalObject);
public:
SheetGlobalObject(Sheet&);
virtual ~SheetGlobalObject() override;
virtual JS::Value get(const JS::PropertyName&, JS::Value receiver = {}) const override;
virtual bool put(const JS::PropertyName&, JS::Value value, JS::Value receiver = {}) override;
virtual void initialize() override;
JS_DECLARE_NATIVE_FUNCTION(get_real_cell_contents);
JS_DECLARE_NATIVE_FUNCTION(set_real_cell_contents);
JS_DECLARE_NATIVE_FUNCTION(parse_cell_name);
JS_DECLARE_NATIVE_FUNCTION(current_cell_position);
JS_DECLARE_NATIVE_FUNCTION(column_index);
JS_DECLARE_NATIVE_FUNCTION(column_arithmetic);
private:
virtual void visit_edges(Visitor&) override;
Sheet& m_sheet;
};
class WorkbookObject final : public JS::Object {
JS_OBJECT(WorkbookObject, JS::Object);
public:
WorkbookObject(Workbook&);
virtual ~WorkbookObject() override;
virtual void initialize(JS::GlobalObject&) override;
JS_DECLARE_NATIVE_FUNCTION(sheet);
private:
virtual void visit_edges(Visitor&) override;
Workbook& m_workbook;
};
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/String.h>
#include <AK/Types.h>
#include <AK/URL.h>
namespace Spreadsheet {
struct Position {
String column;
size_t row { 0 };
bool operator==(const Position& other) const
{
return row == other.row && column == other.column;
}
bool operator!=(const Position& other) const
{
return !(other == *this);
}
URL to_url() const
{
URL url;
url.set_protocol("spreadsheet");
url.set_host("cell");
url.set_path(String::formatted("/{}", getpid()));
url.set_fragment(String::formatted("{}{}", column, row));
return url;
}
};
}

View file

@ -0,0 +1,43 @@
/*
* 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 "XSV.h"
#include <AK/Forward.h>
#include <AK/StringView.h>
namespace Reader {
class CSV : public XSV {
public:
CSV(StringView source, ParserBehaviour behaviours = default_behaviours())
: XSV(source, { ",", "\"", ParserTraits::Repeat }, behaviours)
{
}
};
}

View file

@ -0,0 +1,110 @@
/*
* 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 <AK/TestSuite.h>
#include "../CSV.h"
#include "../XSV.h"
#include <LibCore/File.h>
TEST_CASE(should_parse_valid_data)
{
{
auto data = R"~~~(Foo, Bar, Baz
1, 2, 3
4, 5, 6
"""x", y"z, 9)~~~";
auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
EXPECT(!csv.has_error());
EXPECT_EQ(csv[0]["Foo"], "1");
EXPECT_EQ(csv[2]["Foo"], "\"x");
EXPECT_EQ(csv[2]["Bar"], "y\"z");
}
{
auto data = R"~~~(Foo, Bar, Baz
1 , 2, 3
4, "5 " , 6
"""x", y"z, 9 )~~~";
auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces | Reader::ParserBehaviour::TrimTrailingFieldSpaces };
EXPECT(!csv.has_error());
EXPECT_EQ(csv[0]["Foo"], "1");
EXPECT_EQ(csv[1]["Bar"], "5 ");
EXPECT_EQ(csv[2]["Foo"], "\"x");
EXPECT_EQ(csv[2]["Baz"], "9");
}
}
TEST_CASE(should_fail_nicely)
{
{
auto data = R"~~~(Foo, Bar, Baz
x, y)~~~";
auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
EXPECT(csv.has_error());
EXPECT_EQ(csv.error(), Reader::ReadError::NonConformingColumnCount);
}
{
auto data = R"~~~(Foo, Bar, Baz
x, y, "z)~~~";
auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
EXPECT(csv.has_error());
EXPECT_EQ(csv.error(), Reader::ReadError::QuoteFailure);
}
}
TEST_CASE(should_iterate_rows)
{
auto data = R"~~~(Foo, Bar, Baz
1, 2, 3
4, 5, 6
"""x", y"z, 9)~~~";
auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
EXPECT(!csv.has_error());
bool ran = false;
for (auto row : csv)
ran = !row[0].is_empty();
EXPECT(ran);
}
BENCHMARK_CASE(fairly_big_data)
{
auto file_or_error = Core::File::open(__FILE__ ".data", Core::IODevice::OpenMode::ReadOnly);
EXPECT_EQ_FORCE(file_or_error.is_error(), false);
auto data = file_or_error.value()->read_all();
auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders };
EXPECT(!csv.has_error());
EXPECT_EQ(csv.size(), 100000u);
}
TEST_MAIN(XSV)

View file

@ -0,0 +1,272 @@
/*
* 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 "XSV.h"
#include <AK/StringBuilder.h>
namespace Reader {
ParserBehaviour operator&(ParserBehaviour left, ParserBehaviour right)
{
return static_cast<ParserBehaviour>(static_cast<u32>(left) & static_cast<u32>(right));
}
ParserBehaviour operator|(ParserBehaviour left, ParserBehaviour right)
{
return static_cast<ParserBehaviour>(static_cast<u32>(left) | static_cast<u32>(right));
}
void XSV::set_error(ReadError error)
{
if (m_error == ReadError::None)
m_error = error;
}
Vector<String> XSV::headers() const
{
Vector<String> headers;
for (auto& field : m_names)
headers.append(field.is_string_view ? field.as_string_view : field.as_string.view());
return headers;
}
void XSV::parse()
{
if ((m_behaviours & ParserBehaviour::ReadHeaders) != ParserBehaviour::None)
read_headers();
while (!has_error() && !m_lexer.is_eof())
m_rows.append(read_row());
if (!m_lexer.is_eof())
set_error(ReadError::DataPastLogicalEnd);
}
void XSV::read_headers()
{
if (!m_names.is_empty()) {
set_error(ReadError::InternalError);
m_names.clear();
}
m_names = read_row(true);
}
Vector<XSV::Field> XSV::read_row(bool header_row)
{
Vector<Field> row;
bool first = true;
while (!(m_lexer.is_eof() || m_lexer.next_is('\n') || m_lexer.next_is("\r\n")) && (first || m_lexer.consume_specific(m_traits.separator))) {
first = false;
row.append(read_one_field());
}
if (!m_lexer.is_eof()) {
auto crlf_ok = m_lexer.consume_specific("\r\n");
if (!crlf_ok) {
auto lf_ok = m_lexer.consume_specific('\n');
if (!lf_ok)
set_error(ReadError::DataPastLogicalEnd);
}
}
if (!header_row && (m_behaviours & ParserBehaviour::ReadHeaders) != ParserBehaviour::None && row.size() != m_names.size())
set_error(ReadError::NonConformingColumnCount);
return row;
}
XSV::Field XSV::read_one_field()
{
if ((m_behaviours & ParserBehaviour::TrimLeadingFieldSpaces) != ParserBehaviour::None)
m_lexer.consume_while(is_any_of(" \t\v"));
bool is_quoted = false;
Field field;
if (m_lexer.next_is(m_traits.quote.view())) {
is_quoted = true;
field = read_one_quoted_field();
} else {
field = read_one_unquoted_field();
}
if ((m_behaviours & ParserBehaviour::TrimTrailingFieldSpaces) != ParserBehaviour::None) {
m_lexer.consume_while(is_any_of(" \t\v"));
if (!is_quoted) {
// Also have to trim trailing spaces from unquoted fields.
StringView view;
if (field.is_string_view)
view = field.as_string_view;
else
view = field.as_string;
if (!view.is_empty()) {
ssize_t i = view.length() - 1;
for (; i >= 0; --i) {
if (!view.substring_view(i, 1).is_one_of(" ", "\t", "\v"))
break;
}
view = view.substring_view(0, i + 1);
}
if (field.is_string_view)
field.as_string_view = view;
else
field.as_string = field.as_string.substring(0, view.length());
}
}
return field;
}
XSV::Field XSV::read_one_quoted_field()
{
if (!m_lexer.consume_specific(m_traits.quote))
set_error(ReadError::InternalError);
size_t start = m_lexer.tell(), end = start;
bool is_copy = false;
StringBuilder builder;
auto allow_newlines = (m_behaviours & ParserBehaviour::AllowNewlinesInFields) != ParserBehaviour::None;
for (; !m_lexer.is_eof();) {
char ch;
switch (m_traits.quote_escape) {
case ParserTraits::Backslash:
if (m_lexer.consume_specific('\\') && m_lexer.consume_specific(m_traits.quote)) {
// If there is an escaped quote, we have no choice but to make a copy.
if (!is_copy) {
is_copy = true;
builder.append(m_source.substring_view(start, end - start));
}
builder.append(m_traits.quote);
end = m_lexer.tell();
continue;
}
break;
case ParserTraits::Repeat:
if (m_lexer.consume_specific(m_traits.quote)) {
if (m_lexer.consume_specific(m_traits.quote)) {
// If there is an escaped quote, we have no choice but to make a copy.
if (!is_copy) {
is_copy = true;
builder.append(m_source.substring_view(start, end - start));
}
builder.append(m_traits.quote);
end = m_lexer.tell();
continue;
}
for (size_t i = 0; i < m_traits.quote.length(); ++i)
m_lexer.retreat();
goto end;
}
break;
}
if (m_lexer.next_is(m_traits.quote.view()))
goto end;
if (!allow_newlines) {
if (m_lexer.next_is('\n') || m_lexer.next_is("\r\n"))
goto end;
}
ch = m_lexer.consume();
if (is_copy)
builder.append(ch);
end = m_lexer.tell();
continue;
end:
break;
}
if (!m_lexer.consume_specific(m_traits.quote))
set_error(ReadError::QuoteFailure);
if (is_copy)
return { {}, builder.to_string(), false };
return { m_source.substring_view(start, end - start), {}, true };
}
XSV::Field XSV::read_one_unquoted_field()
{
size_t start = m_lexer.tell(), end = start;
bool allow_quote_in_field = (m_behaviours & ParserBehaviour::QuoteOnlyInFieldStart) != ParserBehaviour::None;
for (; !m_lexer.is_eof();) {
if (m_lexer.next_is(m_traits.separator.view()))
break;
if (m_lexer.next_is("\r\n") || m_lexer.next_is("\n"))
break;
if (m_lexer.consume_specific(m_traits.quote)) {
if (!allow_quote_in_field)
set_error(ReadError::QuoteFailure);
end = m_lexer.tell();
continue;
}
m_lexer.consume();
end = m_lexer.tell();
}
return { m_source.substring_view(start, end - start), {}, true };
}
StringView XSV::Row::operator[](StringView name) const
{
ASSERT(!m_xsv.m_names.is_empty());
auto it = m_xsv.m_names.find_if([&](const auto& entry) { return name == entry; });
ASSERT(!it.is_end());
return (*this)[it.index()];
}
StringView XSV::Row::operator[](size_t column) const
{
auto& field = m_xsv.m_rows[m_index][column];
if (field.is_string_view)
return field.as_string_view;
return field.as_string;
}
const XSV::Row XSV::operator[](size_t index) const
{
return const_cast<XSV&>(*this)[index];
}
XSV::Row XSV::operator[](size_t index)
{
ASSERT(m_rows.size() > index);
return Row { *this, index };
}
}

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/GenericLexer.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <AK/Vector.h>
namespace Reader {
enum class ParserBehaviour : u32 {
None = 0,
ReadHeaders = 1,
AllowNewlinesInFields = ReadHeaders << 1,
TrimLeadingFieldSpaces = ReadHeaders << 2,
TrimTrailingFieldSpaces = ReadHeaders << 3,
QuoteOnlyInFieldStart = ReadHeaders << 4,
};
ParserBehaviour operator&(ParserBehaviour left, ParserBehaviour right);
ParserBehaviour operator|(ParserBehaviour left, ParserBehaviour right);
struct ParserTraits {
String separator;
String quote { "\"" };
enum {
Repeat,
Backslash,
} quote_escape { Repeat };
};
#define ENUMERATE_READ_ERRORS() \
E(None, "No errors") \
E(NonConformingColumnCount, "Header count does not match given column count") \
E(QuoteFailure, "Quoting failure") \
E(InternalError, "Internal error") \
E(DataPastLogicalEnd, "Exrta data past the logical end of the rows")
enum class ReadError {
#define E(name, _) name,
ENUMERATE_READ_ERRORS()
#undef E
};
inline constexpr ParserBehaviour default_behaviours()
{
return ParserBehaviour::QuoteOnlyInFieldStart;
}
class XSV {
public:
XSV(StringView source, const ParserTraits& traits, ParserBehaviour behaviours = default_behaviours())
: m_source(source)
, m_lexer(m_source)
, m_traits(traits)
, m_behaviours(behaviours)
{
parse();
}
virtual ~XSV() { }
bool has_error() const { return m_error != ReadError::None; }
ReadError error() const { return m_error; }
String error_string() const
{
switch (m_error) {
#define E(x, y) \
case ReadError::x: \
return y;
ENUMERATE_READ_ERRORS();
#undef E
}
ASSERT_NOT_REACHED();
}
size_t size() const { return m_rows.size(); }
Vector<String> headers() const;
class Row {
public:
explicit Row(XSV& xsv, size_t index)
: m_xsv(xsv)
, m_index(index)
{
}
StringView operator[](StringView name) const;
StringView operator[](size_t column) const;
size_t index() const { return m_index; }
// FIXME: Implement begin() and end(), keeping `Field' out of the API.
private:
XSV& m_xsv;
size_t m_index { 0 };
};
template<bool const_>
class RowIterator {
public:
explicit RowIterator(const XSV& xsv, size_t init_index = 0) requires(const_)
: m_xsv(const_cast<XSV&>(xsv))
, m_index(init_index)
{
}
explicit RowIterator(XSV& xsv, size_t init_index = 0) requires(!const_)
: m_xsv(xsv)
, m_index(init_index)
{
}
Row operator*() const { return Row { m_xsv, m_index }; }
Row operator*() requires(!const_) { return Row { m_xsv, m_index }; }
RowIterator& operator++()
{
++m_index;
return *this;
}
bool is_end() const { return m_index == m_xsv.m_rows.size(); }
bool operator==(const RowIterator& other) const
{
return m_index == other.m_index && &m_xsv == &other.m_xsv;
}
bool operator==(const RowIterator<!const_>& other) const
{
return m_index == other.m_index && &m_xsv == &other.m_xsv;
}
private:
XSV& m_xsv;
size_t m_index { 0 };
};
const Row operator[](size_t index) const;
Row operator[](size_t index);
auto begin() { return RowIterator<false>(*this); }
auto end() { return RowIterator<false>(*this, m_rows.size()); }
auto begin() const { return RowIterator<true>(*this); }
auto end() const { return RowIterator<true>(*this, m_rows.size()); }
using ConstIterator = RowIterator<true>;
using Iterator = RowIterator<false>;
private:
struct Field {
StringView as_string_view;
String as_string; // This member only used if the parser couldn't use the original source verbatim.
bool is_string_view { true };
bool operator==(StringView other) const
{
if (is_string_view)
return other == as_string_view;
return as_string == other;
}
};
void set_error(ReadError error);
void parse();
void read_headers();
Vector<Field> read_row(bool header_row = false);
Field read_one_field();
Field read_one_quoted_field();
Field read_one_unquoted_field();
StringView m_source;
GenericLexer m_lexer;
const ParserTraits& m_traits;
ParserBehaviour m_behaviours;
Vector<Field> m_names;
Vector<Vector<Field>> m_rows;
ReadError m_error { ReadError::None };
};
}

View file

@ -0,0 +1,734 @@
/*
* 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 "JSIntegration.h"
#include "Workbook.h"
#include <AK/ByteBuffer.h>
#include <AK/GenericLexer.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonParser.h>
#include <AK/ScopeGuard.h>
#include <AK/TemporaryChange.h>
#include <AK/URL.h>
#include <LibCore/File.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/Function.h>
#include <ctype.h>
//#define COPY_DEBUG
namespace Spreadsheet {
Sheet::Sheet(const StringView& name, Workbook& workbook)
: Sheet(workbook)
{
m_name = name;
for (size_t i = 0; i < default_row_count; ++i)
add_row();
for (size_t i = 0; i < default_column_count; ++i)
add_column();
}
Sheet::Sheet(Workbook& workbook)
: m_workbook(workbook)
{
JS::DeferGC defer_gc(m_workbook.interpreter().heap());
m_global_object = m_workbook.interpreter().heap().allocate_without_global_object<SheetGlobalObject>(*this);
global_object().initialize();
global_object().put("workbook", m_workbook.workbook_object());
global_object().put("thisSheet", &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();
JS::Parser parser { JS::Lexer(buffer) };
if (parser.has_errors()) {
warnln("Spreadsheet: Failed to parse runtime code");
parser.print_errors();
} else {
interpreter().run(global_object(), parser.parse_program());
if (auto exc = interpreter().exception()) {
warnln("Spreadsheet: Failed to run runtime code: ");
for (auto& t : exc->trace())
warnln("{}", t);
interpreter().vm().clear_exception();
}
}
}
}
Sheet::~Sheet()
{
}
JS::Interpreter& Sheet::interpreter() const
{
return m_workbook.interpreter();
}
size_t Sheet::add_row()
{
return m_rows++;
}
static String convert_to_string(size_t value, unsigned base = 26, StringView map = {})
{
if (map.is_null())
map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
ASSERT(base >= 2 && base <= map.length());
// The '8 bits per byte' assumption may need to go?
Array<char, round_up_to_power_of_two(sizeof(size_t) * 8 + 1, 2)> buffer;
size_t i = 0;
do {
buffer[i++] = map[value % base];
value /= base;
} while (value > 0);
// NOTE: Weird as this may seem, the thing that comes after 'A' is 'AA', which as a number would be '00'
// to make this work, only the most significant digit has to be in a range of (1..25) as opposed to (0..25),
// but only if it's not the only digit in the string.
if (i > 1)
--buffer[i - 1];
for (size_t j = 0; j < i / 2; ++j)
swap(buffer[j], buffer[i - j - 1]);
return String { ReadonlyBytes(buffer.data(), i) };
}
static size_t convert_from_string(StringView str, unsigned base = 26, StringView map = {})
{
if (map.is_null())
map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
ASSERT(base >= 2 && base <= map.length());
size_t value = 0;
for (size_t i = str.length(); i > 0; --i) {
auto digit_value = map.find_first_of(str[i - 1]).value_or(0);
// NOTE: Refer to the note in `convert_to_string()'.
if (i == str.length() && str.length() > 1)
++digit_value;
value = value * base + digit_value;
}
return value;
}
String Sheet::add_column()
{
auto next_column = convert_to_string(m_columns.size());
m_columns.append(next_column);
return next_column;
}
void Sheet::update()
{
if (m_should_ignore_updates) {
m_update_requested = true;
return;
}
m_visited_cells_in_update.clear();
Vector<Cell*> cells_copy;
// Grab a copy as updates might insert cells into the table.
for (auto& it : m_cells) {
if (it.value->dirty()) {
cells_copy.append(it.value);
m_workbook.set_dirty(true);
}
}
for (auto& cell : cells_copy)
update(*cell);
m_visited_cells_in_update.clear();
}
void Sheet::update(Cell& cell)
{
if (m_should_ignore_updates) {
m_update_requested = true;
return;
}
if (cell.dirty()) {
if (has_been_visited(&cell)) {
// This may be part of an cyclic reference chain,
// so just ignore it.
cell.clear_dirty();
return;
}
m_visited_cells_in_update.set(&cell);
cell.update_data({});
}
}
Sheet::ValueAndException Sheet::evaluate(const StringView& source, Cell* on_behalf_of)
{
TemporaryChange cell_change { m_current_cell_being_evaluated, on_behalf_of };
ScopeGuard clear_exception { [&] { interpreter().vm().clear_exception(); } };
auto parser = JS::Parser(JS::Lexer(source));
if (parser.has_errors() || interpreter().exception())
return { JS::js_undefined(), interpreter().exception() };
auto program = parser.parse_program();
interpreter().run(global_object(), program);
if (interpreter().exception()) {
auto exc = interpreter().exception();
return { JS::js_undefined(), exc };
}
auto value = interpreter().vm().last_value();
if (value.is_empty())
return { JS::js_undefined(), {} };
return { value, {} };
}
Cell* Sheet::at(const StringView& name)
{
auto pos = parse_cell_name(name);
if (pos.has_value())
return at(pos.value());
return nullptr;
}
Cell* Sheet::at(const Position& position)
{
auto it = m_cells.find(position);
if (it == m_cells.end())
return nullptr;
return it->value;
}
Optional<Position> Sheet::parse_cell_name(const StringView& name)
{
GenericLexer lexer(name);
auto col = lexer.consume_while(isalpha);
auto row = lexer.consume_while(isdigit);
if (!lexer.is_eof() || row.is_empty() || col.is_empty())
return {};
return Position { col, row.to_uint().value() };
}
Optional<size_t> Sheet::column_index(const StringView& column_name) const
{
auto index = convert_from_string(column_name);
if (m_columns.size() <= index || m_columns[index] != column_name)
return {};
return index;
}
Optional<String> Sheet::column_arithmetic(const StringView& column_name, int offset)
{
auto maybe_index = column_index(column_name);
if (!maybe_index.has_value())
return {};
if (offset < 0 && maybe_index.value() < (size_t)(0 - offset))
return m_columns.first();
auto index = maybe_index.value() + offset;
if (m_columns.size() > index)
return m_columns[index];
for (size_t i = m_columns.size(); i <= index; ++i)
add_column();
return m_columns.last();
}
Cell* Sheet::from_url(const URL& url)
{
auto maybe_position = position_from_url(url);
if (!maybe_position.has_value())
return nullptr;
return at(maybe_position.value());
}
Optional<Position> Sheet::position_from_url(const URL& url) const
{
if (!url.is_valid()) {
dbgln("Invalid url: {}", url.to_string());
return {};
}
if (url.protocol() != "spreadsheet" || url.host() != "cell") {
dbgln("Bad url: {}", url.to_string());
return {};
}
// FIXME: Figure out a way to do this cross-process.
ASSERT(url.path() == String::formatted("/{}", getpid()));
return parse_cell_name(url.fragment());
}
Position Sheet::offset_relative_to(const Position& base, const Position& offset, const Position& offset_base) const
{
auto offset_column_it = m_columns.find(offset.column);
auto offset_base_column_it = m_columns.find(offset_base.column);
auto base_column_it = m_columns.find(base.column);
if (offset_column_it.is_end()) {
dbgln("Column '{}' does not exist!", offset.column);
return base;
}
if (offset_base_column_it.is_end()) {
dbgln("Column '{}' does not exist!", offset.column);
return base;
}
if (base_column_it.is_end()) {
dbgln("Column '{}' does not exist!", offset.column);
return offset;
}
auto new_column = column(offset_column_it.index() + base_column_it.index() - offset_base_column_it.index());
auto new_row = offset.row + base.row - offset_base.row;
return { move(new_column), new_row };
}
void Sheet::copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to)
{
auto copy_to = [&](auto& source_position, Position target_position) {
auto& target_cell = ensure(target_position);
auto* source_cell = at(source_position);
if (!source_cell) {
target_cell.set_data("");
return;
}
target_cell.copy_from(*source_cell);
};
if (from.size() == to.size()) {
auto from_it = from.begin();
// FIXME: Ordering.
for (auto& position : to)
copy_to(*from_it++, position);
return;
}
if (to.size() == 1) {
// Resolve each index as relative to the first index offset from the selection.
auto& target = to.first();
for (auto& position : from) {
#ifdef COPY_DEBUG
dbg() << "Paste from '" << position.to_url() << "' to '" << target.to_url() << "'";
#endif
copy_to(position, resolve_relative_to.has_value() ? offset_relative_to(target, position, resolve_relative_to.value()) : target);
}
return;
}
if (from.size() == 1) {
// Fill the target selection with the single cell.
auto& source = from.first();
for (auto& position : to) {
#ifdef COPY_DEBUG
dbg() << "Paste from '" << source.to_url() << "' to '" << position.to_url() << "'";
#endif
copy_to(source, resolve_relative_to.has_value() ? offset_relative_to(position, source, resolve_relative_to.value()) : position);
}
return;
}
// Just disallow misaligned copies.
dbgln("Cannot copy {} cells to {} cells", from.size(), to.size());
}
RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook)
{
auto sheet = adopt(*new Sheet(workbook));
auto rows = object.get("rows").to_u32(default_row_count);
auto columns = object.get("columns");
auto name = object.get("name").as_string_or("Sheet");
sheet->set_name(name);
for (size_t i = 0; i < max(rows, (unsigned)Sheet::default_row_count); ++i)
sheet->add_row();
// FIXME: Better error checking.
if (columns.is_array()) {
columns.as_array().for_each([&](auto& value) {
sheet->m_columns.append(value.as_string());
return IterationDecision::Continue;
});
}
if (sheet->m_columns.size() < default_column_count && sheet->columns_are_standard()) {
for (size_t i = sheet->m_columns.size(); i < default_column_count; ++i)
sheet->add_column();
}
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();
auto read_format = [](auto& format, const auto& obj) {
if (auto value = obj.get("foreground_color"); value.is_string())
format.foreground_color = Color::from_string(value.as_string());
if (auto value = obj.get("background_color"); value.is_string())
format.background_color = Color::from_string(value.as_string());
};
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> cell;
switch (kind) {
case Cell::LiteralString:
cell = make<Cell>(obj.get("value").to_string(), position, *sheet);
break;
case Cell::Formula: {
auto& interpreter = sheet->interpreter();
auto value = interpreter.vm().call(parse_function, json, JS::js_string(interpreter.heap(), obj.get("value").as_string()));
cell = make<Cell>(obj.get("source").to_string(), move(value), position, *sheet);
break;
}
}
auto type_name = obj.get_or("type", "Numeric").to_string();
cell->set_type(type_name);
auto type_meta = obj.get("type_metadata");
if (type_meta.is_object()) {
auto& meta_obj = type_meta.as_object();
auto meta = cell->type_metadata();
if (auto value = meta_obj.get("length"); value.is_number())
meta.length = value.to_i32();
if (auto value = meta_obj.get("format"); value.is_string())
meta.format = value.as_string();
read_format(meta.static_format, meta_obj);
cell->set_type_metadata(move(meta));
}
auto conditional_formats = obj.get("conditional_formats");
auto cformats = cell->conditional_formats();
if (conditional_formats.is_array()) {
conditional_formats.as_array().for_each([&](const auto& fmt_val) {
if (!fmt_val.is_object())
return IterationDecision::Continue;
auto& fmt_obj = fmt_val.as_object();
auto fmt_cond = fmt_obj.get("condition").to_string();
if (fmt_cond.is_empty())
return IterationDecision::Continue;
ConditionalFormat fmt;
fmt.condition = move(fmt_cond);
read_format(fmt, fmt_obj);
cformats.append(move(fmt));
return IterationDecision::Continue;
});
cell->set_conditional_formats(move(cformats));
}
auto evaluated_format = obj.get("evaluated_formats");
if (evaluated_format.is_object()) {
auto& evaluated_format_obj = evaluated_format.as_object();
auto& evaluated_fmts = cell->evaluated_formats();
read_format(evaluated_fmts, evaluated_format_obj);
}
sheet->m_cells.set(position, cell.release_nonnull());
return IterationDecision::Continue;
});
return sheet;
}
Position Sheet::written_data_bounds() const
{
Position bound;
for (auto& entry : m_cells) {
if (entry.key.row >= bound.row)
bound.row = entry.key.row;
if (entry.key.column >= bound.column)
bound.column = entry.key.column;
}
return bound;
}
/// The sheet is allowed to have nonstandard column names
/// this checks whether all existing columns are 'standard'
/// (i.e. as generated by 'convert_to_string()'
bool Sheet::columns_are_standard() const
{
for (size_t i = 0; i < m_columns.size(); ++i) {
if (m_columns[i] != convert_to_string(i))
return false;
}
return true;
}
JsonObject Sheet::to_json() const
{
JsonObject object;
object.set("name", m_name);
auto save_format = [](const auto& format, auto& obj) {
if (format.foreground_color.has_value())
obj.set("foreground_color", format.foreground_color.value().to_string());
if (format.background_color.has_value())
obj.set("background_color", format.background_color.value().to_string());
};
auto bottom_right = written_data_bounds();
if (!columns_are_standard()) {
auto columns = JsonArray();
for (auto& column : m_columns)
columns.append(column);
object.set("columns", move(columns));
}
object.set("rows", bottom_right.row + 1);
JsonObject cells;
for (auto& it : m_cells) {
StringBuilder builder;
builder.append(it.key.column);
builder.appendff("{}", it.key.row);
auto key = builder.to_string();
JsonObject data;
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 = interpreter().global_object().get("JSON");
auto stringified = interpreter().vm().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());
}
// Set type & meta
auto& type = it.value->type();
auto& meta = it.value->type_metadata();
data.set("type", type.name());
JsonObject metadata_object;
metadata_object.set("length", meta.length);
metadata_object.set("format", meta.format);
#if 0
metadata_object.set("alignment", alignment_to_string(meta.alignment));
#endif
save_format(meta.static_format, metadata_object);
data.set("type_metadata", move(metadata_object));
// Set conditional formats
JsonArray conditional_formats;
for (auto& fmt : it.value->conditional_formats()) {
JsonObject fmt_object;
fmt_object.set("condition", fmt.condition);
save_format(fmt, fmt_object);
conditional_formats.append(move(fmt_object));
}
data.set("conditional_formats", move(conditional_formats));
auto& evaluated_formats = it.value->evaluated_formats();
JsonObject evaluated_formats_obj;
save_format(evaluated_formats, evaluated_formats_obj);
data.set("evaluated_formats", move(evaluated_formats_obj));
cells.set(key, move(data));
}
object.set("cells", move(cells));
return object;
}
Vector<Vector<String>> Sheet::to_xsv() const
{
Vector<Vector<String>> data;
auto bottom_right = written_data_bounds();
// First row = headers.
size_t column_count = m_columns.size();
if (columns_are_standard()) {
column_count = convert_from_string(bottom_right.column) + 1;
Vector<String> cols;
for (size_t i = 0; i < column_count; ++i)
cols.append(m_columns[i]);
data.append(move(cols));
} else {
data.append(m_columns);
}
for (size_t i = 0; i <= bottom_right.row; ++i) {
Vector<String> row;
row.resize(column_count);
for (size_t j = 0; j < column_count; ++j) {
auto cell = at({ m_columns[j], i });
if (cell)
row[j] = cell->typed_display();
}
data.append(move(row));
}
return data;
}
RefPtr<Sheet> Sheet::from_xsv(const Reader::XSV& xsv, Workbook& workbook)
{
auto cols = xsv.headers();
auto rows = xsv.size();
auto sheet = adopt(*new Sheet(workbook));
sheet->m_columns = cols;
for (size_t i = 0; i < max(rows, Sheet::default_row_count); ++i)
sheet->add_row();
if (sheet->columns_are_standard()) {
for (size_t i = sheet->m_columns.size(); i < Sheet::default_column_count; ++i)
sheet->add_column();
}
for (auto row : xsv) {
for (size_t i = 0; i < cols.size(); ++i) {
auto str = row[i];
if (str.is_empty())
continue;
Position position { cols[i], row.index() };
auto cell = make<Cell>(str, position, *sheet);
sheet->m_cells.set(position, move(cell));
}
}
return sheet;
}
JsonObject Sheet::gather_documentation() const
{
JsonObject object;
const JS::PropertyName doc_name { "__documentation" };
auto add_docs_from = [&](auto& it, auto& global_object) {
auto value = global_object.get(it.key);
if (!value.is_function() && !value.is_object())
return;
auto& value_object = value.is_object() ? value.as_object() : value.as_function();
if (!value_object.has_own_property(doc_name))
return;
dbgln("Found '{}'", it.key.to_display_string());
auto doc = value_object.get(doc_name);
if (!doc.is_string())
return;
JsonParser parser(doc.to_string_without_side_effects());
auto doc_object = parser.parse();
if (doc_object.has_value())
object.set(it.key.to_display_string(), doc_object.value());
else
dbgln("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());
m_cached_documentation = move(object);
return m_cached_documentation.value();
}
String Sheet::generate_inline_documentation_for(StringView function, size_t argument_index)
{
if (!m_cached_documentation.has_value())
gather_documentation();
auto& docs = m_cached_documentation.value();
auto entry = docs.get(function);
if (entry.is_null() || !entry.is_object())
return String::formatted("{}(...???{})", function, argument_index);
auto& entry_object = entry.as_object();
size_t argc = entry_object.get("argc").to_int(0);
auto argnames_value = entry_object.get("argnames");
if (!argnames_value.is_array())
return String::formatted("{}(...{}???{})", function, argc, argument_index);
auto& argnames = argnames_value.as_array();
StringBuilder builder;
builder.appendff("{}(", function);
for (size_t i = 0; i < (size_t)argnames.size(); ++i) {
if (i != 0 && i < (size_t)argnames.size())
builder.append(", ");
if (i == argument_index)
builder.append('<');
else if (i >= argc)
builder.append('[');
builder.append(argnames[i].to_string());
if (i == argument_index)
builder.append('>');
else if (i >= argc)
builder.append(']');
}
builder.append(')');
return builder.build();
}
}

View file

@ -0,0 +1,188 @@
/*
* 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 "Cell.h"
#include "Forward.h"
#include "Readers/XSV.h"
#include <AK/HashMap.h>
#include <AK/HashTable.h>
#include <AK/String.h>
#include <AK/StringBuilder.h>
#include <AK/Traits.h>
#include <AK/Types.h>
#include <AK/WeakPtr.h>
#include <AK/Weakable.h>
#include <LibCore/Object.h>
#include <LibJS/Interpreter.h>
namespace Spreadsheet {
class Sheet : public Core::Object {
C_OBJECT(Sheet);
public:
constexpr static size_t default_row_count = 100;
constexpr static size_t default_column_count = 26;
~Sheet();
static Optional<Position> parse_cell_name(const StringView&);
Optional<size_t> column_index(const StringView& column_name) const;
Optional<String> column_arithmetic(const StringView& column_name, int offset);
Cell* from_url(const URL&);
const Cell* from_url(const URL& url) const { return const_cast<Sheet*>(this)->from_url(url); }
Optional<Position> position_from_url(const URL& url) const;
/// Resolve 'offset' to an absolute position assuming 'base' is at 'offset_base'.
/// Effectively, "Walk the distance between 'offset' and 'offset_base' away from 'base'".
Position offset_relative_to(const Position& base, const Position& offset, const Position& offset_base) const;
JsonObject to_json() const;
static RefPtr<Sheet> from_json(const JsonObject&, Workbook&);
Vector<Vector<String>> to_xsv() const;
static RefPtr<Sheet> from_xsv(const Reader::XSV&, Workbook&);
const String& name() const { return m_name; }
void set_name(const StringView& name) { m_name = name; }
JsonObject gather_documentation() const;
const HashTable<Position>& selected_cells() const { return m_selected_cells; }
HashTable<Position>& selected_cells() { return m_selected_cells; }
const HashMap<Position, NonnullOwnPtr<Cell>>& cells() const { return m_cells; }
HashMap<Position, NonnullOwnPtr<Cell>>& cells() { return m_cells; }
Cell* at(const Position& position);
const Cell* at(const Position& position) const { return const_cast<Sheet*>(this)->at(position); }
const Cell* at(const StringView& name) const { return const_cast<Sheet*>(this)->at(name); }
Cell* at(const StringView&);
const Cell& ensure(const Position& position) const { return const_cast<Sheet*>(this)->ensure(position); }
Cell& ensure(const Position& position)
{
if (auto cell = at(position))
return *cell;
m_cells.set(position, make<Cell>(String::empty(), position, *this));
return *at(position);
}
size_t add_row();
String add_column();
size_t row_count() const { return m_rows; }
size_t column_count() const { return m_columns.size(); }
const Vector<String>& columns() const { return m_columns; }
const String& column(size_t index)
{
for (size_t i = column_count(); i < index; ++i)
add_column();
ASSERT(column_count() > index);
return m_columns[index];
}
const String& column(size_t index) const
{
ASSERT(column_count() > index);
return m_columns[index];
}
void update();
void update(Cell&);
void disable_updates() { m_should_ignore_updates = true; }
void enable_updates()
{
m_should_ignore_updates = false;
if (m_update_requested) {
m_update_requested = false;
update();
}
}
struct ValueAndException {
JS::Value value;
JS::Exception* exception { nullptr };
};
ValueAndException evaluate(const StringView&, Cell* = nullptr);
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); }
const Workbook& workbook() const { return m_workbook; }
void copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to = {});
/// Gives the bottom-right corner of the smallest bounding box containing all the written data.
Position written_data_bounds() const;
bool columns_are_standard() const;
String generate_inline_documentation_for(StringView function, size_t argument_index);
private:
explicit Sheet(Workbook&);
explicit Sheet(const StringView& name, Workbook&);
String m_name;
Vector<String> m_columns;
size_t m_rows { 0 };
HashMap<Position, NonnullOwnPtr<Cell>> m_cells;
HashTable<Position> m_selected_cells;
Workbook& m_workbook;
mutable SheetGlobalObject* m_global_object;
Cell* m_current_cell_being_evaluated { nullptr };
HashTable<Cell*> m_visited_cells_in_update;
bool m_should_ignore_updates { false };
bool m_update_requested { false };
mutable Optional<JsonObject> m_cached_documentation;
};
}
namespace AK {
template<>
struct Traits<Spreadsheet::Position> : public GenericTraits<Spreadsheet::Position> {
static constexpr bool is_trivial() { return false; }
static unsigned hash(const Spreadsheet::Position& p)
{
return pair_int_hash(
string_hash(p.column.characters(), p.column.length()),
u64_hash(p.row));
}
};
}

View file

@ -0,0 +1,178 @@
/*
* 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"
#include "ConditionalFormatting.h"
#include <AK/URL.h>
#include <LibGUI/AbstractView.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Object.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) {
const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
if (!cell)
return String::empty();
if (cell->kind() == Spreadsheet::Cell::Formula) {
if (auto exception = cell->exception()) {
StringBuilder builder;
builder.append("Error: ");
auto value = exception->value();
if (value.is_object()) {
auto& object = value.as_object();
if (is<JS::Error>(object)) {
auto error = object.get("message").to_string_without_side_effects();
builder.append(error);
return builder.to_string();
}
}
auto error = value.to_string(cell->sheet().global_object());
// This is annoying, but whatever.
cell->sheet().interpreter().vm().clear_exception();
builder.append(error);
return builder.to_string();
}
}
return cell->typed_display();
}
if (role == GUI::ModelRole::MimeData)
return Position { m_sheet->column(index.column()), (size_t)index.row() }.to_url().to_string();
if (role == GUI::ModelRole::TextAlignment) {
const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
if (!cell)
return {};
return cell->type_metadata().alignment;
}
if (role == GUI::ModelRole::ForegroundColor) {
const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
if (!cell)
return {};
if (cell->kind() == Spreadsheet::Cell::Formula) {
if (cell->exception())
return Color(Color::Red);
}
if (cell->evaluated_formats().foreground_color.has_value())
return cell->evaluated_formats().foreground_color.value();
if (auto color = cell->type_metadata().static_format.foreground_color; color.has_value())
return color.value();
return {};
}
if (role == GUI::ModelRole::BackgroundColor) {
const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
if (!cell)
return {};
if (cell->evaluated_formats().background_color.has_value())
return cell->evaluated_formats().background_color.value();
if (auto color = cell->type_metadata().static_format.background_color; color.has_value())
return color.value();
return {};
}
return {};
}
RefPtr<Core::MimeData> SheetModel::mime_data(const GUI::ModelSelection& selection) const
{
auto mime_data = GUI::Model::mime_data(selection);
bool first = true;
const GUI::ModelIndex* cursor = nullptr;
const_cast<SheetModel*>(this)->for_each_view([&](const GUI::AbstractView& view) {
if (!first)
return;
cursor = &view.cursor_index();
first = false;
});
ASSERT(cursor);
Position cursor_position { m_sheet->column(cursor->column()), (size_t)cursor->row() };
auto new_data = String::formatted("{}\n{}",
cursor_position.to_url().to_string(),
StringView(mime_data->data("text/x-spreadsheet-data")));
mime_data->set_data("text/x-spreadsheet-data", new_data.to_byte_buffer());
return mime_data;
}
String SheetModel::column_name(int index) const
{
if (index < 0)
return {};
return m_sheet->column(index);
}
bool SheetModel::is_editable(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return false;
return true;
}
void SheetModel::set_data(const GUI::ModelIndex& index, const GUI::Variant& value)
{
if (!index.is_valid())
return;
auto& cell = m_sheet->ensure({ m_sheet->column(index.column()), (size_t)index.row() });
cell.set_data(value.to_string());
update();
}
void SheetModel::update()
{
m_sheet->update();
did_update(UpdateFlag::DontInvalidateIndexes);
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Spreadsheet.h"
#include <LibGUI/Model.h>
namespace Spreadsheet {
class SheetModel final : public GUI::Model {
public:
static NonnullRefPtr<SheetModel> create(Sheet& sheet) { return adopt(*new SheetModel(sheet)); }
virtual ~SheetModel() override;
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_sheet->row_count(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_sheet->column_count(); }
virtual String column_name(int) const override;
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual RefPtr<Core::MimeData> mime_data(const GUI::ModelSelection&) 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;
virtual bool is_column_sortable(int) const override { return false; }
virtual StringView drag_data_type() const override { return "text/x-spreadsheet-data"; }
Sheet& sheet() { return *m_sheet; }
private:
explicit SheetModel(Sheet& sheet)
: m_sheet(sheet)
{
}
NonnullRefPtr<Sheet> m_sheet;
};
}

View file

@ -0,0 +1,334 @@
/*
* 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 "CellTypeDialog.h"
#include "SpreadsheetModel.h"
#include <AK/ScopeGuard.h>
#include <AK/URL.h>
#include <LibCore/MimeData.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/HeaderView.h>
#include <LibGUI/Menu.h>
#include <LibGUI/ModelEditingDelegate.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/TableView.h>
#include <LibGfx/Palette.h>
namespace Spreadsheet {
SpreadsheetView::~SpreadsheetView()
{
}
void SpreadsheetView::EditingDelegate::set_value(const GUI::Variant& value)
{
if (value.as_string().is_null()) {
StringModelEditingDelegate::set_value("");
commit();
return;
}
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()), (size_t)index().row() });
if (option)
return StringModelEditingDelegate::set_value(option->source());
StringModelEditingDelegate::set_value("");
}
void InfinitelyScrollableTableView::did_scroll()
{
TableView::did_scroll();
auto& vscrollbar = vertical_scrollbar();
auto& hscrollbar = horizontal_scrollbar();
if (vscrollbar.is_visible() && vscrollbar.value() == vscrollbar.max()) {
if (on_reaching_vertical_end)
on_reaching_vertical_end();
}
if (hscrollbar.is_visible() && hscrollbar.value() == hscrollbar.max()) {
if (on_reaching_horizontal_end)
on_reaching_horizontal_end();
}
}
void InfinitelyScrollableTableView::mousemove_event(GUI::MouseEvent& event)
{
if (auto model = this->model()) {
auto index = index_at_event_position(event.position());
if (!index.is_valid())
return TableView::mousemove_event(event);
auto& sheet = static_cast<SheetModel&>(*model).sheet();
sheet.disable_updates();
ScopeGuard sheet_update_enabler { [&] { sheet.enable_updates(); } };
auto holding_left_button = !!(event.buttons() & GUI::MouseButton::Left);
auto rect = content_rect(index);
auto distance = rect.center().absolute_relative_distance_to(event.position());
if (distance.x() >= rect.width() / 2 - 5 && distance.y() >= rect.height() / 2 - 5) {
set_override_cursor(Gfx::StandardCursor::Crosshair);
m_should_intercept_drag = false;
if (holding_left_button) {
m_has_committed_to_dragging = true;
// Force a drag to happen by moving the mousedown position to the center of the cell.
m_left_mousedown_position = rect.center();
}
} else if (!m_should_intercept_drag) {
set_override_cursor(Gfx::StandardCursor::Arrow);
if (!holding_left_button) {
m_starting_selection_index = index;
} else {
m_should_intercept_drag = true;
m_might_drag = false;
}
}
if (holding_left_button && m_should_intercept_drag && !m_has_committed_to_dragging) {
if (!m_starting_selection_index.is_valid())
m_starting_selection_index = index;
Vector<GUI::ModelIndex> new_selection;
for (auto i = min(m_starting_selection_index.row(), index.row()), imax = max(m_starting_selection_index.row(), index.row()); i <= imax; ++i) {
for (auto j = min(m_starting_selection_index.column(), index.column()), jmax = max(m_starting_selection_index.column(), index.column()); j <= jmax; ++j) {
auto index = model->index(i, j);
if (index.is_valid())
new_selection.append(move(index));
}
}
if (!event.ctrl())
selection().clear();
selection().add_all(new_selection);
}
}
TableView::mousemove_event(event);
}
void InfinitelyScrollableTableView::mouseup_event(GUI::MouseEvent& event)
{
m_should_intercept_drag = false;
m_has_committed_to_dragging = false;
TableView::mouseup_event(event);
}
void SpreadsheetView::update_with_model()
{
m_table_view->model()->update();
m_table_view->update();
}
SpreadsheetView::SpreadsheetView(Sheet& sheet)
: m_sheet(sheet)
{
set_layout<GUI::VerticalBoxLayout>().set_margins({ 2, 2, 2, 2 });
m_table_view = add<InfinitelyScrollableTableView>();
m_table_view->set_grid_style(GUI::TableView::GridStyle::Both);
m_table_view->set_selection_behavior(GUI::AbstractView::SelectionBehavior::SelectItems);
m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed | GUI::AbstractView::AnyKeyPressed | GUI::AbstractView::DoubleClicked);
m_table_view->set_tab_key_navigation_enabled(true);
m_table_view->row_header().set_visible(true);
m_table_view->set_model(SheetModel::create(*m_sheet));
m_table_view->on_reaching_vertical_end = [&]() {
for (size_t i = 0; i < 100; ++i) {
auto index = m_sheet->add_row();
m_table_view->set_column_painting_delegate(index, make<TableCellPainter>(*m_table_view));
};
update_with_model();
};
m_table_view->on_reaching_horizontal_end = [&]() {
for (size_t i = 0; i < 10; ++i) {
m_sheet->add_column();
auto last_column_index = m_sheet->column_count() - 1;
m_table_view->set_column_width(last_column_index, 50);
m_table_view->set_column_header_alignment(last_column_index, Gfx::TextAlignment::Center);
}
update_with_model();
};
set_focus_proxy(m_table_view);
// FIXME: This is dumb.
for (size_t i = 0; i < m_sheet->column_count(); ++i) {
m_table_view->set_column_painting_delegate(i, make<TableCellPainter>(*m_table_view));
m_table_view->set_column_width(i, 50);
m_table_view->set_column_header_alignment(i, 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 = [this](auto&) {
auto delegate = make<EditingDelegate>(*m_sheet);
delegate->on_cursor_key_pressed = [this](auto& event) {
m_table_view->stop_editing();
m_table_view->event(event);
};
return delegate;
};
m_table_view->on_selection_change = [&] {
m_sheet->selected_cells().clear();
for (auto& index : m_table_view->selection().indexes()) {
Position position { m_sheet->column(index.column()), (size_t)index.row() };
m_sheet->selected_cells().set(position);
}
if (m_table_view->selection().is_empty() && on_selection_dropped)
return on_selection_dropped();
Vector<Position> selected_positions;
selected_positions.ensure_capacity(m_table_view->selection().size());
for (auto& selection : m_table_view->selection().indexes())
selected_positions.empend(m_sheet->column(selection.column()), (size_t)selection.row());
if (on_selection_changed) {
on_selection_changed(move(selected_positions));
update_with_model();
};
};
m_table_view->on_activation = [this](auto&) {
m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set);
};
m_table_view->on_context_menu_request = [&](const GUI::ModelIndex&, const GUI::ContextMenuEvent& event) {
// NOTE: We ignore the specific cell for now.
m_cell_range_context_menu->popup(event.screen_position());
};
m_cell_range_context_menu = GUI::Menu::construct();
m_cell_range_context_menu->add_action(GUI::Action::create("Type and Formatting...", [this](auto&) {
Vector<Position> positions;
for (auto& index : m_table_view->selection().indexes()) {
Position position { m_sheet->column(index.column()), (size_t)index.row() };
positions.append(move(position));
}
if (positions.is_empty()) {
auto& index = m_table_view->cursor_index();
Position position { m_sheet->column(index.column()), (size_t)index.row() };
positions.append(move(position));
}
auto dialog = CellTypeDialog::construct(positions, *m_sheet, window());
if (dialog->exec() == GUI::Dialog::ExecOK) {
for (auto& position : positions) {
auto& cell = m_sheet->ensure(position);
cell.set_type(dialog->type());
cell.set_type_metadata(dialog->metadata());
cell.set_conditional_formats(dialog->conditional_formats());
}
m_table_view->update();
}
}));
m_table_view->on_drop = [&](const GUI::ModelIndex& index, const GUI::DropEvent& event) {
if (!index.is_valid())
return;
ScopeGuard update_after_drop { [this] { update(); } };
if (event.mime_data().has_format("text/x-spreadsheet-data")) {
auto data = event.mime_data().data("text/x-spreadsheet-data");
StringView urls { data.data(), data.size() };
Vector<Position> source_positions, target_positions;
for (auto& line : urls.lines(false)) {
auto position = m_sheet->position_from_url(line);
if (position.has_value())
source_positions.append(position.release_value());
}
// Drop always has a single target.
Position target { m_sheet->column(index.column()), (size_t)index.row() };
target_positions.append(move(target));
if (source_positions.is_empty())
return;
auto first_position = source_positions.take_first();
m_sheet->copy_cells(move(source_positions), move(target_positions), first_position);
return;
}
if (event.mime_data().has_text()) {
auto* target_cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
ASSERT(target_cell);
target_cell->set_data(event.text());
return;
}
};
}
void SpreadsheetView::hide_event(GUI::HideEvent&)
{
if (on_selection_dropped)
on_selection_dropped();
}
void SpreadsheetView::show_event(GUI::ShowEvent&)
{
if (on_selection_changed && !m_table_view->selection().is_empty()) {
Vector<Position> selected_positions;
selected_positions.ensure_capacity(m_table_view->selection().size());
for (auto& selection : m_table_view->selection().indexes())
selected_positions.empend(m_sheet->column(selection.column()), (size_t)selection.row());
on_selection_changed(move(selected_positions));
}
}
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...
auto cell_rect = rect.inflated(m_table_view.horizontal_padding() * 2, 0);
if (auto bg = index.data(GUI::ModelRole::BackgroundColor); bg.is_color())
painter.fill_rect(cell_rect, bg.as_color());
if (m_table_view.selection().contains(index)) {
Color fill_color = palette.selection();
fill_color.set_alpha(80);
painter.fill_rect(cell_rect, fill_color);
}
auto text_color = index.data(GUI::ModelRole::ForegroundColor).to_color(palette.color(m_table_view.foreground_role()));
auto data = index.data();
auto text_alignment = index.data(GUI::ModelRole::TextAlignment).to_text_alignment(Gfx::TextAlignment::CenterRight);
painter.draw_text(rect, data.to_string(), m_table_view.font_for_index(index), text_alignment, text_color, Gfx::TextElision::Right);
}
}

View file

@ -0,0 +1,168 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Spreadsheet.h"
#include <LibGUI/AbstractTableView.h>
#include <LibGUI/ModelEditingDelegate.h>
#include <LibGUI/TableView.h>
#include <LibGUI/Widget.h>
#include <string.h>
namespace Spreadsheet {
class CellEditor final : public GUI::TextEditor {
C_OBJECT(CellEditor);
public:
virtual ~CellEditor() { }
Function<void(GUI::KeyEvent&)> on_cursor_key_pressed;
private:
CellEditor()
: TextEditor(TextEditor::Type::SingleLine)
{
}
static bool is_navigation(const GUI::KeyEvent& event)
{
if (event.modifiers() == KeyModifier::Mod_Shift && event.key() == KeyCode::Key_Tab)
return true;
if (event.modifiers())
return false;
switch (event.key()) {
case KeyCode::Key_Tab:
case KeyCode::Key_Left:
case KeyCode::Key_Right:
case KeyCode::Key_Up:
case KeyCode::Key_Down:
case KeyCode::Key_Return:
return true;
default:
return false;
}
}
virtual void keydown_event(GUI::KeyEvent& event) override
{
if (is_navigation(event))
on_cursor_key_pressed(event);
else
TextEditor::keydown_event(event);
}
};
class InfinitelyScrollableTableView : public GUI::TableView {
C_OBJECT(InfinitelyScrollableTableView)
public:
Function<void()> on_reaching_vertical_end;
Function<void()> on_reaching_horizontal_end;
private:
virtual void did_scroll() override;
virtual void mousemove_event(GUI::MouseEvent&) override;
virtual void mouseup_event(GUI::MouseEvent&) override;
bool m_should_intercept_drag { false };
bool m_has_committed_to_dragging { false };
GUI::ModelIndex m_starting_selection_index;
};
class SpreadsheetView final : public GUI::Widget {
C_OBJECT(SpreadsheetView);
public:
~SpreadsheetView();
const Sheet& sheet() const { return *m_sheet; }
Sheet& sheet() { return *m_sheet; }
const GUI::ModelIndex* cursor() const
{
return &m_table_view->cursor_index();
}
Function<void(Vector<Position>&&)> on_selection_changed;
Function<void()> on_selection_dropped;
private:
virtual void hide_event(GUI::HideEvent&) override;
virtual void show_event(GUI::ShowEvent&) override;
void update_with_model();
SpreadsheetView(Sheet&);
class EditingDelegate final : public GUI::StringModelEditingDelegate {
public:
EditingDelegate(const Sheet& sheet)
: m_sheet(sheet)
{
}
virtual void set_value(const GUI::Variant& value) override;
virtual RefPtr<Widget> create_widget() override
{
auto textbox = CellEditor::construct();
textbox->on_escape_pressed = [this] {
rollback();
};
textbox->on_cursor_key_pressed = [this](auto& event) {
commit();
on_cursor_key_pressed(event);
};
return textbox;
}
Function<void(GUI::KeyEvent&)> on_cursor_key_pressed;
private:
bool m_has_set_initial_value { false };
const Sheet& m_sheet;
};
class TableCellPainter final : public GUI::TableCellPaintingDelegate {
public:
TableCellPainter(const GUI::TableView& view)
: m_table_view(view)
{
}
void paint(GUI::Painter&, const Gfx::IntRect&, const Gfx::Palette&, const GUI::ModelIndex&) override;
private:
const GUI::TableView& m_table_view;
};
NonnullRefPtr<Sheet> m_sheet;
RefPtr<InfinitelyScrollableTableView> m_table_view;
RefPtr<GUI::Menu> m_cell_range_context_menu;
};
}

View file

@ -0,0 +1,347 @@
/*
* 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 "CellSyntaxHighlighter.h"
#include "HelpWindow.h"
#include "LibGUI/InputBox.h"
#include <LibCore/File.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/FontDatabase.h>
#include <string.h>
namespace Spreadsheet {
SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets, bool should_add_sheet_if_empty)
: m_workbook(make<Workbook>(move(sheets)))
{
set_fill_with_background_color(true);
set_layout<GUI::VerticalBoxLayout>().set_margins({ 2, 2, 2, 2 });
auto& container = add<GUI::VerticalSplitter>();
auto& top_bar = container.add<GUI::Frame>();
top_bar.set_layout<GUI::HorizontalBoxLayout>().set_spacing(1);
top_bar.set_fixed_height(26);
auto& current_cell_label = top_bar.add<GUI::Label>("");
current_cell_label.set_fixed_width(50);
auto& help_button = top_bar.add<GUI::Button>("🛈");
help_button.set_fixed_size(20, 20);
help_button.on_click = [&](auto) {
auto docs = m_selected_view->sheet().gather_documentation();
auto help_window = HelpWindow::the(window());
help_window->set_docs(move(docs));
help_window->show();
};
auto& cell_value_editor = top_bar.add<GUI::TextEditor>(GUI::TextEditor::Type::SingleLine);
cell_value_editor.set_font(Gfx::FontDatabase::default_fixed_width_font());
cell_value_editor.set_scrollbars_enabled(false);
cell_value_editor.set_syntax_highlighter(make<CellSyntaxHighlighter>());
cell_value_editor.set_enabled(false);
current_cell_label.set_enabled(false);
m_tab_widget = container.add<GUI::TabWidget>();
m_tab_widget->set_tab_position(GUI::TabWidget::TabPosition::Bottom);
m_cell_value_editor = cell_value_editor;
m_current_cell_label = current_cell_label;
m_inline_documentation_window = GUI::Window::construct(window());
m_inline_documentation_window->set_rect(m_cell_value_editor->rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
m_inline_documentation_window->set_window_type(GUI::WindowType::Tooltip);
m_inline_documentation_window->set_resizable(false);
auto& inline_widget = m_inline_documentation_window->set_main_widget<GUI::Frame>();
inline_widget.set_fill_with_background_color(true);
inline_widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
inline_widget.set_frame_shape(Gfx::FrameShape::Box);
m_inline_documentation_label = inline_widget.add<GUI::Label>();
m_inline_documentation_label->set_fill_with_background_color(true);
m_inline_documentation_label->set_autosize(false);
m_inline_documentation_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
if (!m_workbook->has_sheets() && should_add_sheet_if_empty)
m_workbook->add_sheet("Sheet 1");
m_tab_context_menu = GUI::Menu::construct();
auto rename_action = GUI::Action::create("Rename...", [this](auto&) {
ASSERT(m_tab_context_menu_sheet_view);
auto& sheet = m_tab_context_menu_sheet_view->sheet();
String new_name;
if (GUI::InputBox::show(new_name, window(), String::formatted("New name for '{}'", sheet.name()), "Rename sheet") == GUI::Dialog::ExecOK) {
sheet.set_name(new_name);
sheet.update();
m_tab_widget->set_tab_title(static_cast<GUI::Widget&>(*m_tab_context_menu_sheet_view), new_name);
}
});
m_tab_context_menu->add_action(rename_action);
m_tab_context_menu->add_action(GUI::Action::create("Add new sheet...", [this](auto&) {
String name;
if (GUI::InputBox::show(name, window(), "Name for new sheet", "Create sheet") == GUI::Dialog::ExecOK) {
NonnullRefPtrVector<Sheet> new_sheets;
new_sheets.append(m_workbook->add_sheet(name));
setup_tabs(move(new_sheets));
}
}));
setup_tabs(m_workbook->sheets());
}
void SpreadsheetWidget::resize_event(GUI::ResizeEvent& event)
{
GUI::Widget::resize_event(event);
if (m_inline_documentation_window && m_cell_value_editor && window())
m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
}
void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector<Sheet> new_sheets)
{
RefPtr<GUI::Widget> first_tab_widget;
for (auto& sheet : new_sheets) {
auto& tab = m_tab_widget->add_tab<SpreadsheetView>(sheet.name(), sheet);
if (!first_tab_widget)
first_tab_widget = &tab;
}
auto change = [&](auto& selected_widget) {
if (m_selected_view) {
m_selected_view->on_selection_changed = nullptr;
m_selected_view->on_selection_dropped = nullptr;
};
m_selected_view = &static_cast<SpreadsheetView&>(selected_widget);
m_selected_view->on_selection_changed = [&](Vector<Position>&& selection) {
if (selection.is_empty()) {
m_current_cell_label->set_enabled(false);
m_current_cell_label->set_text({});
m_cell_value_editor->on_change = nullptr;
m_cell_value_editor->on_focusin = nullptr;
m_cell_value_editor->on_focusout = nullptr;
m_cell_value_editor->set_text("");
m_cell_value_editor->set_enabled(false);
return;
}
if (selection.size() == 1) {
auto& position = selection.first();
StringBuilder builder;
builder.append(position.column);
builder.appendff("{}", position.row);
m_current_cell_label->set_enabled(true);
m_current_cell_label->set_text(builder.string_view());
auto& cell = m_selected_view->sheet().ensure(position);
m_cell_value_editor->on_change = nullptr;
m_cell_value_editor->set_text(cell.source());
m_cell_value_editor->on_change = [&] {
auto text = m_cell_value_editor->text();
// FIXME: Lines?
auto offset = m_cell_value_editor->cursor().column();
try_generate_tip_for_input_expression(text, offset);
cell.set_data(move(text));
m_selected_view->sheet().update();
update();
};
m_cell_value_editor->set_enabled(true);
static_cast<CellSyntaxHighlighter*>(const_cast<GUI::SyntaxHighlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(&cell);
return;
}
// There are many cells selected, change all of them.
StringBuilder builder;
builder.appendff("<{}>", selection.size());
m_current_cell_label->set_enabled(true);
m_current_cell_label->set_text(builder.string_view());
Vector<Cell*> cells;
for (auto& position : selection)
cells.append(&m_selected_view->sheet().ensure(position));
auto first_cell = cells.first();
m_cell_value_editor->on_change = nullptr;
m_cell_value_editor->set_text("");
m_should_change_selected_cells = false;
m_cell_value_editor->on_focusin = [this] { m_should_change_selected_cells = true; };
m_cell_value_editor->on_focusout = [this] { m_should_change_selected_cells = false; };
m_cell_value_editor->on_change = [cells = move(cells), this] {
if (m_should_change_selected_cells) {
auto text = m_cell_value_editor->text();
// FIXME: Lines?
auto offset = m_cell_value_editor->cursor().column();
try_generate_tip_for_input_expression(text, offset);
for (auto* cell : cells)
cell->set_data(text);
m_selected_view->sheet().update();
update();
}
};
m_cell_value_editor->set_enabled(true);
static_cast<CellSyntaxHighlighter*>(const_cast<GUI::SyntaxHighlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(first_cell);
};
m_selected_view->on_selection_dropped = [&]() {
m_cell_value_editor->set_enabled(false);
static_cast<CellSyntaxHighlighter*>(const_cast<GUI::SyntaxHighlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(nullptr);
m_cell_value_editor->set_text("");
m_current_cell_label->set_enabled(false);
m_current_cell_label->set_text("");
};
};
if (first_tab_widget)
change(*first_tab_widget);
m_tab_widget->on_change = [change = move(change)](auto& selected_widget) {
change(selected_widget);
};
m_tab_widget->on_context_menu_request = [&](auto& widget, auto& event) {
m_tab_context_menu_sheet_view = widget;
m_tab_context_menu->popup(event.screen_position());
};
}
void SpreadsheetWidget::try_generate_tip_for_input_expression(StringView source, size_t cursor_offset)
{
m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
if (!m_selected_view || !source.starts_with('=')) {
m_inline_documentation_window->hide();
return;
}
auto maybe_function_and_argument = get_function_and_argument_index(source.substring_view(0, cursor_offset));
if (!maybe_function_and_argument.has_value()) {
m_inline_documentation_window->hide();
return;
}
auto& [name, index] = maybe_function_and_argument.value();
auto& sheet = m_selected_view->sheet();
auto text = sheet.generate_inline_documentation_for(name, index);
if (text.is_empty()) {
m_inline_documentation_window->hide();
} else {
m_inline_documentation_label->set_text(move(text));
m_inline_documentation_window->show();
}
}
void SpreadsheetWidget::save(const StringView& filename)
{
auto result = m_workbook->save(filename);
if (result.is_error())
GUI::MessageBox::show_error(window(), result.error());
}
void SpreadsheetWidget::load(const StringView& filename)
{
auto result = m_workbook->load(filename);
if (result.is_error()) {
GUI::MessageBox::show_error(window(), result.error());
return;
}
m_tab_widget->on_change = nullptr;
m_cell_value_editor->on_change = nullptr;
m_current_cell_label->set_text("");
m_should_change_selected_cells = false;
while (auto* widget = m_tab_widget->active_widget()) {
m_tab_widget->remove_tab(*widget);
}
setup_tabs(m_workbook->sheets());
}
bool SpreadsheetWidget::request_close()
{
if (!m_workbook->dirty())
return true;
auto result = GUI::MessageBox::show(window(), "The spreadsheet has been modified. Would you like to save?", "Unsaved changes", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel);
if (result == GUI::MessageBox::ExecYes) {
if (current_filename().is_empty()) {
String name = "workbook";
Optional<String> save_path = GUI::FilePicker::get_save_filepath(window(), name, "sheets");
if (!save_path.has_value())
return false;
save(save_path.value());
} else {
save(current_filename());
}
return true;
}
if (result == GUI::MessageBox::ExecNo)
return true;
return false;
}
void SpreadsheetWidget::add_sheet()
{
StringBuilder name;
name.append("Sheet");
name.appendff(" {}", m_workbook->sheets().size() + 1);
NonnullRefPtrVector<Sheet> new_sheets;
new_sheets.append(m_workbook->add_sheet(name.string_view()));
setup_tabs(move(new_sheets));
}
void SpreadsheetWidget::add_sheet(NonnullRefPtr<Sheet>&& sheet)
{
ASSERT(m_workbook == &sheet->workbook());
NonnullRefPtrVector<Sheet> new_sheets;
new_sheets.append(move(sheet));
m_workbook->sheets().append(new_sheets);
setup_tabs(new_sheets);
}
void SpreadsheetWidget::set_filename(const String& filename)
{
if (m_workbook->set_filename(filename)) {
StringBuilder builder;
builder.append("Spreadsheet - ");
builder.append(current_filename());
window()->set_title(builder.string_view());
window()->update();
}
}
SpreadsheetWidget::~SpreadsheetWidget()
{
}
}

View file

@ -0,0 +1,86 @@
/*
* 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 "Workbook.h"
#include <AK/NonnullRefPtrVector.h>
#include <LibGUI/Widget.h>
namespace Spreadsheet {
class SpreadsheetWidget final : public GUI::Widget {
C_OBJECT(SpreadsheetWidget);
public:
~SpreadsheetWidget();
void save(const StringView& filename);
void load(const StringView& filename);
bool request_close();
void add_sheet();
void add_sheet(NonnullRefPtr<Sheet>&&);
const String& current_filename() const { return m_workbook->current_filename(); }
const Sheet& current_worksheet() const { return m_selected_view->sheet(); }
Sheet& current_worksheet() { return m_selected_view->sheet(); }
void set_filename(const String& filename);
Workbook& workbook() { return *m_workbook; }
const Workbook& workbook() const { return *m_workbook; }
const GUI::ModelIndex* current_selection_cursor() const
{
if (!m_selected_view)
return nullptr;
return m_selected_view->cursor();
}
private:
virtual void resize_event(GUI::ResizeEvent&) override;
explicit SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets = {}, bool should_add_sheet_if_empty = true);
void setup_tabs(NonnullRefPtrVector<Sheet> new_sheets);
void try_generate_tip_for_input_expression(StringView source, size_t offset);
SpreadsheetView* m_selected_view { nullptr };
RefPtr<GUI::Label> m_current_cell_label;
RefPtr<GUI::TextEditor> m_cell_value_editor;
RefPtr<GUI::Window> m_inline_documentation_window;
RefPtr<GUI::Label> m_inline_documentation_label;
RefPtr<GUI::TabWidget> m_tab_widget;
RefPtr<GUI::Menu> m_tab_context_menu;
RefPtr<SpreadsheetView> m_tab_context_menu_sheet_view;
bool m_should_change_selected_cells { false };
OwnPtr<Workbook> m_workbook;
};
}

View file

@ -0,0 +1,190 @@
/*
* 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 "Workbook.h"
#include "JSIntegration.h"
#include "Readers/CSV.h"
#include "Writers/CSV.h"
#include <AK/ByteBuffer.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonObjectSerializer.h>
#include <AK/JsonParser.h>
#include <AK/Stream.h>
#include <LibCore/File.h>
#include <LibCore/FileStream.h>
#include <LibCore/MimeData.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <string.h>
namespace Spreadsheet {
static JS::VM& global_vm()
{
static RefPtr<JS::VM> vm;
if (!vm)
vm = JS::VM::create();
return *vm;
}
Workbook::Workbook(NonnullRefPtrVector<Sheet>&& sheets)
: m_sheets(move(sheets))
, m_interpreter(JS::Interpreter::create<JS::GlobalObject>(global_vm()))
, m_interpreter_scope(JS::VM::InterpreterExecutionScope(interpreter()))
{
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)
return false;
m_current_filename = filename;
return true;
}
Result<bool, String> Workbook::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());
return sb.to_string();
}
auto mime = Core::guess_mime_type_based_on_filename(filename);
if (mime == "text/csv") {
// FIXME: Prompt the user for settings.
NonnullRefPtrVector<Sheet> sheets;
auto sheet = Sheet::from_xsv(Reader::CSV(file_or_error.value()->read_all(), Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders), *this);
if (sheet)
sheets.append(sheet.release_nonnull());
m_sheets.clear();
m_sheets = move(sheets);
} else {
// Assume JSON.
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);
return sb.to_string();
}
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);
return sb.to_string();
}
NonnullRefPtrVector<Sheet> 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(), *this);
if (!sheet)
return IterationDecision::Continue;
sheets.append(sheet.release_nonnull());
return IterationDecision::Continue;
});
m_sheets.clear();
m_sheets = move(sheets);
}
set_filename(filename);
return true;
}
Result<bool, String> Workbook::save(const StringView& filename)
{
auto mime = Core::guess_mime_type_based_on_filename(filename);
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());
return sb.to_string();
}
if (mime == "text/csv") {
// FIXME: Prompt the user for settings and which sheet to export.
Core::OutputFileStream stream { file };
auto data = m_sheets[0].to_xsv();
auto header_string = data.take_first();
Vector<StringView> headers;
for (auto& str : header_string)
headers.append(str);
Writer::CSV csv { stream, data, headers };
if (csv.has_error())
return String::formatted("Unable to save file, CSV writer error: {}", csv.error_string());
} else {
JsonArray array;
for (auto& sheet : m_sheets)
array.append(sheet.to_json());
auto file_content = array.to_string();
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));
return sb.to_string();
}
}
set_filename(filename);
set_dirty(false);
return true;
}
}

View file

@ -0,0 +1,78 @@
/*
* 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 "Forward.h"
#include "Spreadsheet.h"
#include <AK/NonnullOwnPtrVector.h>
#include <AK/Result.h>
namespace Spreadsheet {
class Workbook {
public:
Workbook(NonnullRefPtrVector<Sheet>&& sheets);
Result<bool, String> save(const StringView& filename);
Result<bool, String> load(const StringView& filename);
const String& current_filename() const { return m_current_filename; }
bool set_filename(const String& filename);
bool dirty() { return m_dirty; }
void set_dirty(bool dirty) { m_dirty = dirty; }
bool has_sheets() const { return !m_sheets.is_empty(); }
const NonnullRefPtrVector<Sheet>& sheets() const { return m_sheets; }
NonnullRefPtrVector<Sheet> sheets() { return m_sheets; }
Sheet& add_sheet(const StringView& 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;
JS::VM::InterpreterExecutionScope m_interpreter_scope;
WorkbookObject* m_workbook_object { nullptr };
String m_current_filename;
bool m_dirty { false };
};
}

View file

@ -0,0 +1,44 @@
/*
* 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 "XSV.h"
#include <AK/Forward.h>
#include <AK/StringView.h>
namespace Writer {
template<typename ContainerType>
class CSV : public XSV<ContainerType> {
public:
CSV(OutputStream& output, const ContainerType& data, const Vector<StringView>& headers = {}, WriterBehaviour behaviours = default_behaviours())
: XSV<ContainerType>(output, data, { ",", "\"", WriterTraits::Repeat }, headers, behaviours)
{
}
};
}

View file

@ -0,0 +1,96 @@
/*
* 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 <AK/TestSuite.h>
#include "../CSV.h"
#include "../XSV.h"
#include <AK/MemoryStream.h>
TEST_CASE(can_write)
{
Vector<Vector<int>> data = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 },
};
auto buffer = ByteBuffer::create_uninitialized(1024);
OutputMemoryStream stream { buffer };
Writer::CSV csv(stream, data);
auto expected_output = R"~(1,2,3
4,5,6
7,8,9
)~";
EXPECT_EQ(StringView { stream.bytes() }, expected_output);
}
TEST_CASE(can_write_with_header)
{
Vector<Vector<int>> data = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 },
};
auto buffer = ByteBuffer::create_uninitialized(1024);
OutputMemoryStream stream { buffer };
Writer::CSV csv(stream, data, { "A", "B\"", "C" });
auto expected_output = R"~(A,"B""",C
1,2,3
4,5,6
7,8,9
)~";
EXPECT_EQ(StringView { stream.bytes() }, expected_output);
}
TEST_CASE(can_write_with_different_behaviours)
{
Vector<Vector<String>> data = {
{ "Well", "Hello\"", "Friends" },
{ "We\"ll", "Hello,", " Friends" },
};
auto buffer = ByteBuffer::create_uninitialized(1024);
OutputMemoryStream stream { buffer };
Writer::CSV csv(stream, data, { "A", "B\"", "C" }, Writer::WriterBehaviour::QuoteOnlyInFieldStart | Writer::WriterBehaviour::WriteHeaders);
auto expected_output = R"~(A,B",C
Well,Hello",Friends
We"ll,"Hello,", Friends
)~";
EXPECT_EQ(StringView { stream.bytes() }, expected_output);
}
TEST_MAIN(XSV)

View file

@ -0,0 +1,215 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/GenericLexer.h>
#include <AK/OwnPtr.h>
#include <AK/Stream.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <AK/Vector.h>
namespace Writer {
enum class WriterBehaviour : u32 {
None = 0,
WriteHeaders = 1,
AllowNewlinesInFields = WriteHeaders << 1,
QuoteOnlyInFieldStart = WriteHeaders << 2,
QuoteAll = WriteHeaders << 3,
};
inline WriterBehaviour operator&(WriterBehaviour left, WriterBehaviour right)
{
return static_cast<WriterBehaviour>(static_cast<u32>(left) & static_cast<u32>(right));
}
inline WriterBehaviour operator|(WriterBehaviour left, WriterBehaviour right)
{
return static_cast<WriterBehaviour>(static_cast<u32>(left) | static_cast<u32>(right));
}
struct WriterTraits {
String separator;
String quote { "\"" };
enum {
Repeat,
Backslash,
} quote_escape { Repeat };
};
#define ENUMERATE_WRITE_ERRORS() \
E(None, "No errors") \
E(NonConformingColumnCount, "Header count does not match given column count") \
E(InternalError, "Internal error")
enum class WriteError {
#define E(name, _) name,
ENUMERATE_WRITE_ERRORS()
#undef E
};
inline constexpr WriterBehaviour default_behaviours()
{
return WriterBehaviour::None;
}
template<typename ContainerType>
class XSV {
public:
XSV(OutputStream& output, const ContainerType& data, const WriterTraits& traits, const Vector<StringView>& headers = {}, WriterBehaviour behaviours = default_behaviours())
: m_data(data)
, m_traits(traits)
, m_behaviours(behaviours)
, m_names(headers)
, m_output(output)
{
if (!headers.is_empty())
m_behaviours = m_behaviours | WriterBehaviour::WriteHeaders;
generate();
}
virtual ~XSV() { }
bool has_error() const { return m_error != WriteError::None; }
WriteError error() const { return m_error; }
String error_string() const
{
switch (m_error) {
#define E(x, y) \
case WriteError::x: \
return y;
ENUMERATE_WRITE_ERRORS();
#undef E
}
ASSERT_NOT_REACHED();
}
private:
void set_error(WriteError error)
{
if (m_error == WriteError::None)
m_error = error;
}
void generate()
{
auto with_headers = (m_behaviours & WriterBehaviour::WriteHeaders) != WriterBehaviour::None;
if (with_headers) {
write_row(m_names);
if (m_output.write({ "\n", 1 }) != 1)
set_error(WriteError::InternalError);
}
for (auto&& row : m_data) {
if (with_headers) {
if (row.size() != m_names.size())
set_error(WriteError::NonConformingColumnCount);
}
write_row(row);
if (m_output.write({ "\n", 1 }) != 1)
set_error(WriteError::InternalError);
}
}
template<typename T>
void write_row(T&& row)
{
bool first = true;
for (auto&& entry : row) {
if (!first) {
if (m_output.write(m_traits.separator.bytes()) != m_traits.separator.length())
set_error(WriteError::InternalError);
}
first = false;
write_entry(entry);
}
}
template<typename T>
void write_entry(T&& entry)
{
auto string = String::formatted("{}", FormatIfSupported(entry));
auto safe_to_write_normally = !string.contains("\n") && !string.contains(m_traits.separator);
if (safe_to_write_normally) {
if ((m_behaviours & WriterBehaviour::QuoteOnlyInFieldStart) == WriterBehaviour::None)
safe_to_write_normally = !string.contains(m_traits.quote);
else
safe_to_write_normally = !string.starts_with(m_traits.quote);
}
if (safe_to_write_normally) {
if (m_output.write(string.bytes()) != string.length())
set_error(WriteError::InternalError);
return;
}
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
set_error(WriteError::InternalError);
GenericLexer lexer(string);
while (!lexer.is_eof()) {
if (lexer.consume_specific(m_traits.quote)) {
switch (m_traits.quote_escape) {
case WriterTraits::Repeat:
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
set_error(WriteError::InternalError);
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
set_error(WriteError::InternalError);
break;
case WriterTraits::Backslash:
if (m_output.write({ "\\", 1 }) != 1)
set_error(WriteError::InternalError);
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
set_error(WriteError::InternalError);
break;
}
continue;
}
auto ch = lexer.consume();
if (m_output.write({ &ch, 1 }) != 1)
set_error(WriteError::InternalError);
}
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
set_error(WriteError::InternalError);
}
const ContainerType& m_data;
const WriterTraits& m_traits;
WriterBehaviour m_behaviours;
const Vector<StringView>& m_names;
WriteError m_error { WriteError::None };
OutputStream& m_output;
};
}

View file

@ -0,0 +1,253 @@
/*
* 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 "HelpWindow.h"
#include "Spreadsheet.h"
#include "SpreadsheetWidget.h"
#include <AK/ScopeGuard.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibGUI/Application.h>
#include <LibGUI/Clipboard.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/Forward.h>
#include <LibGUI/Icon.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/Window.h>
int main(int argc, char* argv[])
{
if (pledge("stdio shared_buffer accept rpath unix cpath wpath fattr thread", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix", nullptr) < 0) {
perror("pledge");
return 1;
}
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)) {
warnln("File does not exist or is a directory: {}", filename);
return 1;
}
}
if (unveil("/tmp/portal/webcontent", "rw") < 0) {
perror("unveil");
return 1;
}
if (unveil("/etc", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil(Core::StandardPaths::home_directory().characters(), "rwc") < 0) {
perror("unveil");
return 1;
}
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil(nullptr, nullptr) < 0) {
perror("unveil");
return 1;
}
auto app_icon = GUI::Icon::default_icon("app-spreadsheet");
auto window = GUI::Window::construct();
window->set_title("Spreadsheet");
window->resize(640, 480);
window->set_icon(app_icon.bitmap_for_size(16));
auto& spreadsheet_widget = window->set_main_widget<Spreadsheet::SpreadsheetWidget>(NonnullRefPtrVector<Spreadsheet::Sheet> {}, filename == nullptr);
if (filename)
spreadsheet_widget.load(filename);
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_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
if (!spreadsheet_widget.request_close())
return;
app->quit(0);
}));
window->on_close_request = [&]() -> GUI::Window::CloseRequestDecision {
if (spreadsheet_widget.request_close())
return GUI::Window::CloseRequestDecision::Close;
return GUI::Window::CloseRequestDecision::StayOpen;
};
auto& file_menu = menubar->add_menu("File");
file_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
Optional<String> 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&) {
if (spreadsheet_widget.current_filename().is_empty()) {
String name = "workbook";
Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, name, "sheets");
if (!save_path.has_value())
return;
spreadsheet_widget.save(save_path.value());
} else {
spreadsheet_widget.save(spreadsheet_widget.current_filename());
}
}));
file_menu.add_action(GUI::CommonActions::make_save_as_action([&](auto&) {
auto current_filename = spreadsheet_widget.current_filename();
String name = "workbook";
Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, name, "sheets");
if (!save_path.has_value())
return;
spreadsheet_widget.save(save_path.value());
if (!current_filename.is_empty())
spreadsheet_widget.set_filename(current_filename);
}));
auto& edit_menu = menubar->add_menu("Edit");
edit_menu.add_action(GUI::CommonActions::make_copy_action([&](auto&) {
/// text/x-spreadsheet-data:
/// - currently selected cell
/// - selected cell+
auto& cells = spreadsheet_widget.current_worksheet().selected_cells();
ASSERT(!cells.is_empty());
StringBuilder text_builder, url_builder;
bool first = true;
auto cursor = spreadsheet_widget.current_selection_cursor();
if (cursor) {
Spreadsheet::Position position { spreadsheet_widget.current_worksheet().column(cursor->column()), (size_t)cursor->row() };
url_builder.append(position.to_url().to_string());
url_builder.append('\n');
}
for (auto& cell : cells) {
if (first && !cursor) {
url_builder.append(cell.to_url().to_string());
url_builder.append('\n');
}
url_builder.append(cell.to_url().to_string());
url_builder.append('\n');
auto cell_data = spreadsheet_widget.current_worksheet().at(cell);
if (!first)
text_builder.append('\t');
if (cell_data)
text_builder.append(cell_data->data());
first = false;
}
HashMap<String, String> metadata;
metadata.set("text/x-spreadsheet-data", url_builder.to_string());
GUI::Clipboard::the().set_data(text_builder.string_view().bytes(), "text/plain", move(metadata));
},
window));
edit_menu.add_action(GUI::CommonActions::make_paste_action([&](auto&) {
ScopeGuard update_after_paste { [&] { spreadsheet_widget.update(); } };
auto& cells = spreadsheet_widget.current_worksheet().selected_cells();
ASSERT(!cells.is_empty());
const auto& data = GUI::Clipboard::the().data_and_type();
if (auto spreadsheet_data = data.metadata.get("text/x-spreadsheet-data"); spreadsheet_data.has_value()) {
Vector<Spreadsheet::Position> source_positions, target_positions;
auto& sheet = spreadsheet_widget.current_worksheet();
for (auto& line : spreadsheet_data.value().split_view('\n')) {
dbgln("Paste line '{}'", line);
auto position = sheet.position_from_url(line);
if (position.has_value())
source_positions.append(position.release_value());
}
for (auto& position : spreadsheet_widget.current_worksheet().selected_cells())
target_positions.append(position);
if (source_positions.is_empty())
return;
auto first_position = source_positions.take_first();
sheet.copy_cells(move(source_positions), move(target_positions), first_position);
} else {
for (auto& cell : spreadsheet_widget.current_worksheet().selected_cells())
spreadsheet_widget.current_worksheet().ensure(cell).set_data(StringView { data.data.data(), data.data.size() });
spreadsheet_widget.update();
}
},
window));
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::Action::create(
"Functions Help", [&](auto&) {
auto docs = spreadsheet_widget.current_worksheet().gather_documentation();
auto help_window = Spreadsheet::HelpWindow::the(window);
help_window->set_docs(move(docs));
help_window->show();
},
window));
app_menu.add_separator();
help_menu.add_action(GUI::CommonActions::make_about_action("Spreadsheet", app_icon, window));
app->set_menubar(move(menubar));
window->show();
return app->exec();
}