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:
parent
aa939c4b4b
commit
dc28c07fa5
287 changed files with 1 additions and 1 deletions
28
Userland/Applications/Spreadsheet/CMakeLists.txt
Normal file
28
Userland/Applications/Spreadsheet/CMakeLists.txt
Normal 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)
|
211
Userland/Applications/Spreadsheet/Cell.cpp
Normal file
211
Userland/Applications/Spreadsheet/Cell.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
138
Userland/Applications/Spreadsheet/Cell.h
Normal file
138
Userland/Applications/Spreadsheet/Cell.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
81
Userland/Applications/Spreadsheet/CellSyntaxHighlighter.cpp
Normal file
81
Userland/Applications/Spreadsheet/CellSyntaxHighlighter.cpp
Normal 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()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
47
Userland/Applications/Spreadsheet/CellSyntaxHighlighter.h
Normal file
47
Userland/Applications/Spreadsheet/CellSyntaxHighlighter.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
68
Userland/Applications/Spreadsheet/CellType/Date.cpp
Normal file
68
Userland/Applications/Spreadsheet/CellType/Date.cpp
Normal 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
|
||||
}
|
||||
|
||||
}
|
42
Userland/Applications/Spreadsheet/CellType/Date.h
Normal file
42
Userland/Applications/Spreadsheet/CellType/Date.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
69
Userland/Applications/Spreadsheet/CellType/Format.cpp
Normal file
69
Userland/Applications/Spreadsheet/CellType/Format.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
35
Userland/Applications/Spreadsheet/CellType/Format.h
Normal file
35
Userland/Applications/Spreadsheet/CellType/Format.h
Normal 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);
|
||||
|
||||
}
|
52
Userland/Applications/Spreadsheet/CellType/Identity.cpp
Normal file
52
Userland/Applications/Spreadsheet/CellType/Identity.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
42
Userland/Applications/Spreadsheet/CellType/Identity.h
Normal file
42
Userland/Applications/Spreadsheet/CellType/Identity.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
76
Userland/Applications/Spreadsheet/CellType/Numeric.cpp
Normal file
76
Userland/Applications/Spreadsheet/CellType/Numeric.cpp
Normal 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());
|
||||
}
|
||||
|
||||
}
|
42
Userland/Applications/Spreadsheet/CellType/Numeric.h
Normal file
42
Userland/Applications/Spreadsheet/CellType/Numeric.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
57
Userland/Applications/Spreadsheet/CellType/String.cpp
Normal file
57
Userland/Applications/Spreadsheet/CellType/String.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
42
Userland/Applications/Spreadsheet/CellType/String.h
Normal file
42
Userland/Applications/Spreadsheet/CellType/String.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
63
Userland/Applications/Spreadsheet/CellType/Type.cpp
Normal file
63
Userland/Applications/Spreadsheet/CellType/Type.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
64
Userland/Applications/Spreadsheet/CellType/Type.h
Normal file
64
Userland/Applications/Spreadsheet/CellType/Type.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
483
Userland/Applications/Spreadsheet/CellTypeDialog.cpp
Normal file
483
Userland/Applications/Spreadsheet/CellTypeDialog.cpp
Normal 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()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
69
Userland/Applications/Spreadsheet/CellTypeDialog.h
Normal file
69
Userland/Applications/Spreadsheet/CellTypeDialog.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
40
Userland/Applications/Spreadsheet/CondFormatting.gml
Normal file
40
Userland/Applications/Spreadsheet/CondFormatting.gml
Normal 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 {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
54
Userland/Applications/Spreadsheet/CondView.gml
Normal file
54
Userland/Applications/Spreadsheet/CondView.gml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
73
Userland/Applications/Spreadsheet/ConditionalFormatting.h
Normal file
73
Userland/Applications/Spreadsheet/ConditionalFormatting.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
41
Userland/Applications/Spreadsheet/Forward.h
Normal file
41
Userland/Applications/Spreadsheet/Forward.h
Normal 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;
|
||||
|
||||
}
|
229
Userland/Applications/Spreadsheet/HelpWindow.cpp
Normal file
229
Userland/Applications/Spreadsheet/HelpWindow.cpp
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
63
Userland/Applications/Spreadsheet/HelpWindow.h
Normal file
63
Userland/Applications/Spreadsheet/HelpWindow.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
444
Userland/Applications/Spreadsheet/JSIntegration.cpp
Normal file
444
Userland/Applications/Spreadsheet/JSIntegration.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
82
Userland/Applications/Spreadsheet/JSIntegration.h
Normal file
82
Userland/Applications/Spreadsheet/JSIntegration.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
60
Userland/Applications/Spreadsheet/Position.h
Normal file
60
Userland/Applications/Spreadsheet/Position.h
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
43
Userland/Applications/Spreadsheet/Readers/CSV.h
Normal file
43
Userland/Applications/Spreadsheet/Readers/CSV.h
Normal 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)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
}
|
110
Userland/Applications/Spreadsheet/Readers/Test/TestXSV.cpp
Normal file
110
Userland/Applications/Spreadsheet/Readers/Test/TestXSV.cpp
Normal 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)
|
272
Userland/Applications/Spreadsheet/Readers/XSV.cpp
Normal file
272
Userland/Applications/Spreadsheet/Readers/XSV.cpp
Normal 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 };
|
||||
}
|
||||
|
||||
}
|
208
Userland/Applications/Spreadsheet/Readers/XSV.h
Normal file
208
Userland/Applications/Spreadsheet/Readers/XSV.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
734
Userland/Applications/Spreadsheet/Spreadsheet.cpp
Normal file
734
Userland/Applications/Spreadsheet/Spreadsheet.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
188
Userland/Applications/Spreadsheet/Spreadsheet.h
Normal file
188
Userland/Applications/Spreadsheet/Spreadsheet.h
Normal 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));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
178
Userland/Applications/Spreadsheet/SpreadsheetModel.cpp
Normal file
178
Userland/Applications/Spreadsheet/SpreadsheetModel.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
60
Userland/Applications/Spreadsheet/SpreadsheetModel.h
Normal file
60
Userland/Applications/Spreadsheet/SpreadsheetModel.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
334
Userland/Applications/Spreadsheet/SpreadsheetView.cpp
Normal file
334
Userland/Applications/Spreadsheet/SpreadsheetView.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
168
Userland/Applications/Spreadsheet/SpreadsheetView.h
Normal file
168
Userland/Applications/Spreadsheet/SpreadsheetView.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
347
Userland/Applications/Spreadsheet/SpreadsheetWidget.cpp
Normal file
347
Userland/Applications/Spreadsheet/SpreadsheetWidget.cpp
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
86
Userland/Applications/Spreadsheet/SpreadsheetWidget.h
Normal file
86
Userland/Applications/Spreadsheet/SpreadsheetWidget.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
190
Userland/Applications/Spreadsheet/Workbook.cpp
Normal file
190
Userland/Applications/Spreadsheet/Workbook.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
78
Userland/Applications/Spreadsheet/Workbook.h
Normal file
78
Userland/Applications/Spreadsheet/Workbook.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
44
Userland/Applications/Spreadsheet/Writers/CSV.h
Normal file
44
Userland/Applications/Spreadsheet/Writers/CSV.h
Normal 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)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -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)
|
215
Userland/Applications/Spreadsheet/Writers/XSV.h
Normal file
215
Userland/Applications/Spreadsheet/Writers/XSV.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
253
Userland/Applications/Spreadsheet/main.cpp
Normal file
253
Userland/Applications/Spreadsheet/main.cpp
Normal 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();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue