Spreadsheet: Add conditional formatting

Currently only supports setting the foregound and the background colours.
This patch also unifies `foreground_color' and `background_color' used
throughout to a `Format' struct, in hopes of getting more formatting
options one day :P
This commit is contained in:
AnotherTest 2020-09-25 22:31:39 +03:30 committed by Andreas Kling
parent 6902a09e47
commit 395df7b27d
Notes: sideshowbarker 2024-07-19 02:14:06 +09:00
13 changed files with 401 additions and 28 deletions

View File

@ -1,3 +1,6 @@
compile_json_gui(CondFormatting.json CondFormattingUI.h cond_fmt_ui_json)
compile_json_gui(CondView.json CondFormattingViewUI.h cond_fmt_view_ui_json)
set(SOURCES set(SOURCES
Cell.cpp Cell.cpp
CellSyntaxHighlighter.cpp CellSyntaxHighlighter.cpp
@ -7,6 +10,8 @@ set(SOURCES
CellType/String.cpp CellType/String.cpp
CellType/Type.cpp CellType/Type.cpp
CellTypeDialog.cpp CellTypeDialog.cpp
CondFormattingUI.h
CondFormattingViewUI.h
HelpWindow.cpp HelpWindow.cpp
JSIntegration.cpp JSIntegration.cpp
Spreadsheet.cpp Spreadsheet.cpp

View File

@ -109,16 +109,37 @@ void Cell::update_data()
if (!dirty) if (!dirty)
return; return;
dirty = false; if (dirty) {
if (kind == Formula) { dirty = false;
if (!evaluated_externally) if (kind == Formula) {
evaluated_data = sheet->evaluate(data, this); if (!evaluated_externally)
evaluated_data = sheet->evaluate(data, this);
}
for (auto& ref : referencing_cells) {
if (ref) {
ref->dirty = true;
ref->update();
}
}
} }
for (auto& ref : referencing_cells) { m_evaluated_formats.background_color.clear();
if (ref) { m_evaluated_formats.foreground_color.clear();
ref->dirty = true; StringBuilder builder;
ref->update(); for (auto& fmt : m_conditional_formats) {
if (!fmt.condition.is_empty()) {
builder.clear();
builder.append("return (");
builder.append(fmt.condition);
builder.append(')');
auto value = sheet->evaluate(builder.string_view(), this);
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;
}
} }
} }
} }

View File

@ -27,6 +27,7 @@
#pragma once #pragma once
#include "CellType/Type.h" #include "CellType/Type.h"
#include "ConditionalFormatting.h"
#include "Forward.h" #include "Forward.h"
#include "JSIntegration.h" #include "JSIntegration.h"
#include <AK/String.h> #include <AK/String.h>
@ -62,6 +63,14 @@ struct Cell : public Weakable<Cell> {
void set_type(const CellType*); void set_type(const CellType*);
void set_type_metadata(CellTypeMetadata&&); void set_type_metadata(CellTypeMetadata&&);
const Format& evaluated_formats() const { return m_evaluated_formats; }
const Vector<ConditionalFormat>& conditional_formats() const { return m_conditional_formats; }
void set_conditional_formats(Vector<ConditionalFormat>&& fmts)
{
dirty = true;
m_conditional_formats = move(fmts);
}
String typed_display() const; String typed_display() const;
JS::Value typed_js_data() const; JS::Value typed_js_data() const;
@ -91,6 +100,9 @@ struct Cell : public Weakable<Cell> {
const CellType* m_type { nullptr }; const CellType* m_type { nullptr };
CellTypeMetadata m_type_metadata; CellTypeMetadata m_type_metadata;
Vector<ConditionalFormat> m_conditional_formats;
Format m_evaluated_formats;
private: private:
void update_data(); void update_data();
}; };

View File

@ -27,6 +27,7 @@
#pragma once #pragma once
#include "../Forward.h" #include "../Forward.h"
#include "../ConditionalFormatting.h"
#include <AK/Forward.h> #include <AK/Forward.h>
#include <AK/String.h> #include <AK/String.h>
#include <LibGfx/Color.h> #include <LibGfx/Color.h>
@ -39,8 +40,7 @@ struct CellTypeMetadata {
int length { -1 }; int length { -1 };
String format; String format;
Gfx::TextAlignment alignment { Gfx::TextAlignment::CenterRight }; Gfx::TextAlignment alignment { Gfx::TextAlignment::CenterRight };
Optional<Gfx::Color> static_foreground_color; Format static_format;
Optional<Gfx::Color> static_background_color;
}; };
class CellType { class CellType {

View File

@ -28,12 +28,15 @@
#include "Cell.h" #include "Cell.h"
#include "Spreadsheet.h" #include "Spreadsheet.h"
#include <AK/StringBuilder.h> #include <AK/StringBuilder.h>
#include <Applications/Spreadsheet/CondFormattingUI.h>
#include <Applications/Spreadsheet/CondFormattingViewUI.h>
#include <LibGUI/BoxLayout.h> #include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h> #include <LibGUI/Button.h>
#include <LibGUI/CheckBox.h> #include <LibGUI/CheckBox.h>
#include <LibGUI/ColorInput.h> #include <LibGUI/ColorInput.h>
#include <LibGUI/ComboBox.h> #include <LibGUI/ComboBox.h>
#include <LibGUI/ItemListModel.h> #include <LibGUI/ItemListModel.h>
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/Label.h> #include <LibGUI/Label.h>
#include <LibGUI/ListView.h> #include <LibGUI/ListView.h>
#include <LibGUI/SpinBox.h> #include <LibGUI/SpinBox.h>
@ -41,6 +44,8 @@
#include <LibGUI/TextEditor.h> #include <LibGUI/TextEditor.h>
#include <LibGUI/Widget.h> #include <LibGUI/Widget.h>
REGISTER_WIDGET(Spreadsheet, ConditionsView);
namespace Spreadsheet { namespace Spreadsheet {
CellTypeDialog::CellTypeDialog(const Vector<Position>& positions, Sheet& sheet, GUI::Window* parent) CellTypeDialog::CellTypeDialog(const Vector<Position>& positions, Sheet& sheet, GUI::Window* parent)
@ -56,7 +61,7 @@ CellTypeDialog::CellTypeDialog(const Vector<Position>& positions, Sheet& sheet,
builder.appendf("Format %zu Cells", positions.size()); builder.appendf("Format %zu Cells", positions.size());
set_title(builder.string_view()); set_title(builder.string_view());
resize(270, 360); resize(285, 360);
auto& main_widget = set_main_widget<GUI::Widget>(); auto& main_widget = set_main_widget<GUI::Widget>();
main_widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 }); main_widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
@ -138,8 +143,8 @@ void CellTypeDialog::setup_tabs(GUI::TabWidget& tabs, const Vector<Position>& po
m_type = &cell.type(); m_type = &cell.type();
m_vertical_alignment = vertical_alignment_from(cell.type_metadata().alignment); m_vertical_alignment = vertical_alignment_from(cell.type_metadata().alignment);
m_horizontal_alignment = horizontal_alignment_from(cell.type_metadata().alignment); m_horizontal_alignment = horizontal_alignment_from(cell.type_metadata().alignment);
m_static_background_color = cell.type_metadata().static_background_color; m_static_format = cell.type_metadata().static_format;
m_static_foreground_color = cell.type_metadata().static_foreground_color; m_conditional_formats = cell.conditional_formats();
} }
auto& type_tab = tabs.add_tab<GUI::Widget>("Type"); auto& type_tab = tabs.add_tab<GUI::Widget>("Type");
@ -317,10 +322,10 @@ void CellTypeDialog::setup_tabs(GUI::TabWidget& tabs, const Vector<Position>& po
auto& foreground_selector = foreground_container.add<GUI::ColorInput>(); auto& foreground_selector = foreground_container.add<GUI::ColorInput>();
foreground_selector.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); foreground_selector.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed);
foreground_selector.set_preferred_size(0, 22); foreground_selector.set_preferred_size(0, 22);
if (m_static_foreground_color.has_value()) if (m_static_format.foreground_color.has_value())
foreground_selector.set_color(m_static_foreground_color.value()); foreground_selector.set_color(m_static_format.foreground_color.value());
foreground_selector.on_change = [&]() { foreground_selector.on_change = [&]() {
m_static_foreground_color = foreground_selector.color(); m_static_format.foreground_color = foreground_selector.color();
}; };
} }
@ -340,14 +345,32 @@ void CellTypeDialog::setup_tabs(GUI::TabWidget& tabs, const Vector<Position>& po
auto& background_selector = background_container.add<GUI::ColorInput>(); auto& background_selector = background_container.add<GUI::ColorInput>();
background_selector.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); background_selector.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed);
background_selector.set_preferred_size(0, 22); background_selector.set_preferred_size(0, 22);
if (m_static_background_color.has_value()) if (m_static_format.background_color.has_value())
background_selector.set_color(m_static_background_color.value()); background_selector.set_color(m_static_format.background_color.value());
background_selector.on_change = [&]() { background_selector.on_change = [&]() {
m_static_background_color = background_selector.color(); m_static_format.background_color = background_selector.color();
}; };
} }
} }
} }
auto& conditional_fmt_tab = tabs.add_tab<GUI::Widget>("Conditional Format");
conditional_fmt_tab.load_from_json(cond_fmt_ui_json);
{
auto& view = static_cast<Spreadsheet::ConditionsView&>(*conditional_fmt_tab.find_descendant_by_name("conditions_view"));
view.set_formats(&m_conditional_formats);
auto& add_button = static_cast<GUI::Button&>(*conditional_fmt_tab.find_descendant_by_name("add_button"));
add_button.on_click = [&](auto) {
view.add_format();
};
// FIXME: Disable this when empty.
auto& remove_button = static_cast<GUI::Button&>(*conditional_fmt_tab.find_descendant_by_name("remove_button"));
remove_button.on_click = [&](auto) {
view.remove_top();
};
}
} }
CellTypeMetadata CellTypeDialog::metadata() const CellTypeMetadata CellTypeDialog::metadata() const
@ -355,8 +378,7 @@ CellTypeMetadata CellTypeDialog::metadata() const
CellTypeMetadata metadata; CellTypeMetadata metadata;
metadata.format = m_format; metadata.format = m_format;
metadata.length = m_length; metadata.length = m_length;
metadata.static_foreground_color = m_static_foreground_color; metadata.static_format = m_static_format;
metadata.static_background_color = m_static_background_color;
switch (m_vertical_alignment) { switch (m_vertical_alignment) {
case VerticalAlignment::Top: case VerticalAlignment::Top:
@ -403,4 +425,85 @@ CellTypeMetadata CellTypeDialog::metadata() const
return metadata; return metadata;
} }
ConditionView::ConditionView(ConditionalFormat& fmt)
: m_format(fmt)
{
load_from_json(cond_fmt_view_ui_json);
auto& fg_input = *static_cast<GUI::ColorInput*>(find_descendant_by_name("foreground_input"));
auto& bg_input = *static_cast<GUI::ColorInput*>(find_descendant_by_name("background_input"));
auto& formula_editor = *static_cast<GUI::TextEditor*>(find_descendant_by_name("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::Font::default_fixed_width_font());
formula_editor.on_change = [&] {
m_format.condition = formula_editor.text();
};
}
ConditionView::~ConditionView()
{
}
ConditionsView::ConditionsView()
{
set_layout<GUI::VerticalBoxLayout>().set_spacing(2);
}
void ConditionsView::set_formats(Vector<ConditionalFormat>* formats)
{
ASSERT(!m_formats);
m_formats = formats;
for (auto& entry : *m_formats)
m_widgets.append(add<ConditionView>(entry));
}
void ConditionsView::add_format()
{
ASSERT(m_formats);
m_formats->empend();
auto& last = m_formats->last();
m_widgets.append(add<ConditionView>(last));
update();
}
void ConditionsView::remove_top()
{
ASSERT(m_formats);
if (m_formats->is_empty())
return;
m_formats->take_last();
m_widgets.take_last()->remove_from_parent();
update();
}
ConditionsView::~ConditionsView()
{
}
} }

View File

@ -27,6 +27,7 @@
#pragma once #pragma once
#include "CellType/Type.h" #include "CellType/Type.h"
#include "ConditionalFormatting.h"
#include "Forward.h" #include "Forward.h"
#include <LibGUI/Dialog.h> #include <LibGUI/Dialog.h>
@ -38,6 +39,7 @@ class CellTypeDialog : public GUI::Dialog {
public: public:
CellTypeMetadata metadata() const; CellTypeMetadata metadata() const;
const CellType* type() const { return m_type; } const CellType* type() const { return m_type; }
Vector<ConditionalFormat> conditional_formats() { return m_conditional_formats; }
enum class HorizontalAlignment : int { enum class HorizontalAlignment : int {
Left = 0, Left = 0,
@ -60,8 +62,8 @@ private:
String m_format; String m_format;
HorizontalAlignment m_horizontal_alignment { HorizontalAlignment::Right }; HorizontalAlignment m_horizontal_alignment { HorizontalAlignment::Right };
VerticalAlignment m_vertical_alignment { VerticalAlignment::Center }; VerticalAlignment m_vertical_alignment { VerticalAlignment::Center };
Optional<Color> m_static_foreground_color; Format m_static_format;
Optional<Color> m_static_background_color; Vector<ConditionalFormat> m_conditional_formats;
}; };
} }

View File

@ -0,0 +1,47 @@
{
"name": "main",
"fill_with_background_color": true,
"layout": {
"class": "GUI::VerticalBoxLayout",
"spacing": 4
},
"children": [
{
"class": "Spreadsheet::ConditionsView",
"name": "conditions_view"
},
{
"class": "GUI::Widget",
"vertical_size_policy": "Fixed",
"horizontal_size_policy": "Fill",
"preferred_width": 0,
"preferred_height": 20,
"layout": {
"class": "GUI::HorizontalBoxLayout",
"spacing": 10
},
"children": [
{
"class": "GUI::Button",
"name": "add_button",
"text": "Add",
"horizontal_size_policy": "Fixed",
"vertical_size_policy": "Fixed",
"preferred_width": 100,
"preferred_height": 20
},
{
"class": "GUI::Button",
"name": "remove_button",
"text": "Remove",
"horizontal_size_policy": "Fixed",
"vertical_size_policy": "Fixed",
"preferred_width": 100,
"preferred_height": 20
}
]
}
]
}

View File

@ -0,0 +1,93 @@
{
"class": "GUI::Widget",
"layout": {
"class": "GUI::VerticalBoxLayout",
"spacing": 2
},
"children": [
{
"class": "GUI::Widget",
"layout": {
"class": "GUI::HorizontalBoxLayout",
"spacing": 10
},
"vertical_size_policy": "Fixed",
"preferred_height": 25,
"children": [
{
"class": "GUI::Label",
"name": "if_label",
"horizontal_size_policy": "Fixed",
"vertical_size_policy": "Fixed",
"text": "if...",
"preferred_width": 40,
"preferred_height": 25
},
{
"class": "GUI::TextEditor",
"name": "formula_editor",
"horizontal_size_policy": "Fill",
"vertical_size_policy": "Fixed",
"tooltip": "Use 'value' to refer to the current cell's value",
"preferred_height": 25
}
]
},
{
"class": "GUI::Widget",
"layout": {
"class": "GUI::HorizontalBoxLayout",
"spacing": 10
},
"vertical_size_policy": "Fixed",
"preferred_height": 25,
"children": [
{
"class": "GUI::Label",
"name": "fg_color_label",
"horizontal_size_policy": "Fixed",
"vertical_size_policy": "Fixed",
"text": "Foreground...",
"preferred_width": 150,
"preferred_height": 25
},
{
"class": "GUI::ColorInput",
"name": "foreground_input",
"horizontal_size_policy": "Fill",
"vertical_size_policy": "Fixed",
"preferred_height": 25,
"preferred_width": 25
}
]
},
{
"class": "GUI::Widget",
"layout": {
"class": "GUI::HorizontalBoxLayout",
"spacing": 10
},
"vertical_size_policy": "Fixed",
"preferred_height": 25,
"children": [
{
"class": "GUI::Label",
"name": "bg_color_label",
"horizontal_size_policy": "Fixed",
"vertical_size_policy": "Fixed",
"text": "Background...",
"preferred_width": 150,
"preferred_height": 25
},
{
"class": "GUI::ColorInput",
"name": "background_input",
"horizontal_size_policy": "Fill",
"vertical_size_policy": "Fixed",
"preferred_height": 25,
"preferred_width": 25
}
]
}
]
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Forward.h"
#include <AK/String.h>
#include <LibGUI/ScrollableWidget.h>
#include <LibGfx/Color.h>
namespace Spreadsheet {
struct Format {
Optional<Color> foreground_color;
Optional<Color> background_color;
};
struct ConditionalFormat : public Format {
String condition;
};
class ConditionView : public GUI::Widget {
C_OBJECT(ConditionView)
public:
virtual ~ConditionView() override;
private:
ConditionView(ConditionalFormat&);
ConditionalFormat& m_format;
};
class ConditionsView : public GUI::Widget {
C_OBJECT(ConditionsView)
public:
virtual ~ConditionsView() override;
void set_formats(Vector<ConditionalFormat>*);
void add_format();
void remove_top();
private:
ConditionsView();
Vector<ConditionalFormat>* m_formats { nullptr };
NonnullRefPtrVector<GUI::Widget> m_widgets;
};
}

View File

@ -28,11 +28,14 @@
namespace Spreadsheet { namespace Spreadsheet {
struct Cell; class ConditionView;
class Sheet; class Sheet;
struct Position; class SheetGlobalObject;
class Workbook; class Workbook;
class WorkbookObject; class WorkbookObject;
class SheetGlobalObject; struct Cell;
struct ConditionalFormat;
struct Format;
struct Position;
} }

View File

@ -46,6 +46,12 @@ SheetGlobalObject::~SheetGlobalObject()
JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receiver) const JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receiver) const
{ {
if (name.is_string()) { 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()) { if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
auto& cell = m_sheet.ensure(pos.value()); auto& cell = m_sheet.ensure(pos.value());
cell.reference_from(m_sheet.current_evaluated_cell()); cell.reference_from(m_sheet.current_evaluated_cell());

View File

@ -25,6 +25,7 @@
*/ */
#include "SpreadsheetModel.h" #include "SpreadsheetModel.h"
#include "ConditionalFormatting.h"
#include <LibJS/Runtime/Error.h> #include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/Object.h>
@ -85,7 +86,10 @@ GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role)
return Color(Color::Red); return Color(Color::Red);
} }
if (auto color = cell->type_metadata().static_foreground_color; color.has_value()) 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 color.value();
return {}; return {};
@ -96,7 +100,10 @@ GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role)
if (!cell) if (!cell)
return {}; return {};
if (auto color = cell->type_metadata().static_background_color; color.has_value()) 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 color.value();
return {}; return {};

View File

@ -140,6 +140,7 @@ SpreadsheetView::SpreadsheetView(Sheet& sheet)
auto& cell = m_sheet->ensure(position); auto& cell = m_sheet->ensure(position);
cell.set_type(dialog->type()); cell.set_type(dialog->type());
cell.set_type_metadata(dialog->metadata()); cell.set_type_metadata(dialog->metadata());
cell.set_conditional_formats(dialog->conditional_formats());
} }
m_table_view->update(); m_table_view->update();