diff --git a/Applications/Spreadsheet/CMakeLists.txt b/Applications/Spreadsheet/CMakeLists.txt index e3c4a0766d..b9d854143f 100644 --- a/Applications/Spreadsheet/CMakeLists.txt +++ b/Applications/Spreadsheet/CMakeLists.txt @@ -5,6 +5,7 @@ set(SOURCES CellType/Numeric.cpp CellType/String.cpp CellType/Type.cpp + CellTypeDialog.cpp HelpWindow.cpp JSIntegration.cpp Spreadsheet.cpp diff --git a/Applications/Spreadsheet/Cell.cpp b/Applications/Spreadsheet/Cell.cpp index d7beec0bb3..3c01eaaace 100644 --- a/Applications/Spreadsheet/Cell.cpp +++ b/Applications/Spreadsheet/Cell.cpp @@ -60,12 +60,16 @@ void Cell::set_data(JS::Value new_data) 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) { - m_type = cell_type; - return; + return set_type(cell_type); } ASSERT_NOT_REACHED(); diff --git a/Applications/Spreadsheet/Cell.h b/Applications/Spreadsheet/Cell.h index f7dd3c71dd..532cb5c6af 100644 --- a/Applications/Spreadsheet/Cell.h +++ b/Applications/Spreadsheet/Cell.h @@ -59,6 +59,7 @@ struct Cell : public Weakable { void set_data(JS::Value new_data); void set_type(const StringView& name); + void set_type(const CellType*); void set_type_metadata(CellTypeMetadata&&); String typed_display() const; diff --git a/Applications/Spreadsheet/CellType/Type.cpp b/Applications/Spreadsheet/CellType/Type.cpp index 80933c1a3d..63d7cd7b4c 100644 --- a/Applications/Spreadsheet/CellType/Type.cpp +++ b/Applications/Spreadsheet/CellType/Type.cpp @@ -43,6 +43,14 @@ const CellType* CellType::get_by_name(const StringView& name) return s_cell_types.get(name).value_or(nullptr); } +Vector CellType::names() +{ + Vector names; + for (auto& it : s_cell_types) + names.append(it.key); + return names; +} + CellType::CellType(const StringView& name) { ASSERT(!s_cell_types.contains(name)); diff --git a/Applications/Spreadsheet/CellType/Type.h b/Applications/Spreadsheet/CellType/Type.h index 7ed9dbe763..cb07a8a262 100644 --- a/Applications/Spreadsheet/CellType/Type.h +++ b/Applications/Spreadsheet/CellType/Type.h @@ -29,6 +29,7 @@ #include "../Forward.h" #include #include +#include #include namespace Spreadsheet { @@ -36,11 +37,13 @@ namespace Spreadsheet { struct CellTypeMetadata { int length { -1 }; String format; + Gfx::TextAlignment alignment { Gfx::TextAlignment::CenterRight }; }; class CellType { public: static const CellType* get_by_name(const StringView&); + static Vector names(); virtual String display(Cell&, const CellTypeMetadata&) const = 0; virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const = 0; diff --git a/Applications/Spreadsheet/CellTypeDialog.cpp b/Applications/Spreadsheet/CellTypeDialog.cpp new file mode 100644 index 0000000000..95b7e47cb4 --- /dev/null +++ b/Applications/Spreadsheet/CellTypeDialog.cpp @@ -0,0 +1,341 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Spreadsheet { + +CellTypeDialog::CellTypeDialog(const Vector& positions, Sheet& sheet, GUI::Window* parent) + : GUI::Dialog(parent) +{ + ASSERT(!positions.is_empty()); + + StringBuilder builder; + + if (positions.size() == 1) + builder.appendf("Format Cell %s%zu", positions.first().column.characters(), positions.first().row); + else + builder.appendf("Format %zu Cells", positions.size()); + + set_title(builder.string_view()); + resize(270, 360); + + auto& main_widget = set_main_widget(); + main_widget.set_layout().set_margins({ 4, 4, 4, 4 }); + main_widget.set_fill_with_background_color(true); + + auto& tab_widget = main_widget.add(); + setup_tabs(tab_widget, positions, sheet); + + auto& buttonbox = main_widget.add(); + buttonbox.set_preferred_size({ 0, 20 }); + buttonbox.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + auto& button_layout = buttonbox.set_layout(); + button_layout.set_spacing(10); + button_layout.add_spacer(); + auto& ok_button = buttonbox.add("OK"); + ok_button.set_preferred_size({ 80, 0 }); + ok_button.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); + ok_button.on_click = [&](auto) { done(ExecOK); }; +} + +const Vector g_horizontal_alignments { "Left", "Center", "Right" }; +const Vector g_vertical_alignments { "Top", "Center", "Bottom" }; +Vector 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& positions, Sheet& sheet) +{ + g_types.clear(); + for (auto& type_name : CellType::names()) + g_types.append(type_name); + + Vector 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); + } + + auto& type_tab = tabs.add_tab("Type"); + type_tab.set_layout().set_margins({ 2, 2, 2, 2 }); + { + auto& left_side = type_tab.add(); + left_side.set_layout(); + auto& right_side = type_tab.add(); + right_side.set_layout(); + right_side.set_preferred_size(170, 0); + right_side.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); + + auto& type_list = left_side.add(); + type_list.set_model(*GUI::ItemListModel::create(g_types)); + type_list.set_multi_select(false); + 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("Override max length"); + auto& spinbox = right_side.add(); + 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.set_preferred_size(0, 20); + spinbox.set_preferred_size(0, 20); + checkbox.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + spinbox.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + + 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("Override display format"); + auto& editor = right_side.add(); + checkbox.set_checked(!m_format.is_null()); + editor.set_should_hide_unnecessary_scrollbars(true); + editor.set_enabled(!m_format.is_null()); + editor.set_text(m_format); + + checkbox.set_preferred_size(0, 20); + editor.set_preferred_size(0, 20); + checkbox.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + editor.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + + 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("Alignment"); + alignment_tab.set_layout().set_margins({ 2, 2, 2, 2 }); + + // FIXME: Frame? + // Horizontal alignment + { + auto& horizontal_alignment_selection_container = alignment_tab.add(); + horizontal_alignment_selection_container.set_layout(); + horizontal_alignment_selection_container.layout()->set_margins({ 0, 4, 0, 0 }); + horizontal_alignment_selection_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + horizontal_alignment_selection_container.set_preferred_size(0, 22); + + auto& horizontal_alignment_label = horizontal_alignment_selection_container.add(); + horizontal_alignment_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + horizontal_alignment_label.set_text("Horizontal Text Alignment"); + + auto& horizontal_combobox = alignment_tab.add(); + horizontal_combobox.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + horizontal_combobox.set_preferred_size(0, 22); + horizontal_combobox.set_only_allow_values_from_model(true); + horizontal_combobox.set_model(*GUI::ItemListModel::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(); + vertical_alignment_container.set_layout(); + vertical_alignment_container.layout()->set_margins({ 0, 4, 0, 0 }); + vertical_alignment_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + vertical_alignment_container.set_preferred_size(0, 22); + + auto& vertical_alignment_label = vertical_alignment_container.add(); + vertical_alignment_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + vertical_alignment_label.set_text("Vertical Text Alignment"); + + auto& vertical_combobox = alignment_tab.add(); + vertical_combobox.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + vertical_combobox.set_preferred_size(0, 22); + vertical_combobox.set_only_allow_values_from_model(true); + vertical_combobox.set_model(*GUI::ItemListModel::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(); + } + }; + } +} + +CellTypeMetadata CellTypeDialog::metadata() const +{ + CellTypeMetadata metadata; + metadata.format = m_format; + metadata.length = m_length; + + 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; +} + +} diff --git a/Applications/Spreadsheet/CellTypeDialog.h b/Applications/Spreadsheet/CellTypeDialog.h new file mode 100644 index 0000000000..84ccbc8613 --- /dev/null +++ b/Applications/Spreadsheet/CellTypeDialog.h @@ -0,0 +1,65 @@ +/* + * 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 "Forward.h" +#include + +namespace Spreadsheet { + +class CellTypeDialog : public GUI::Dialog { + C_OBJECT(CellTypeDialog); + +public: + CellTypeMetadata metadata() const; + const CellType* type() const { return m_type; } + + enum class HorizontalAlignment : int { + Left = 0, + Center, + Right, + }; + enum class VerticalAlignment : int { + Top = 0, + Center, + Bottom, + }; + +private: + CellTypeDialog(const Vector&, Sheet&, GUI::Window* parent = nullptr); + void setup_tabs(GUI::TabWidget&, const Vector&, 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 }; +}; + +} diff --git a/Applications/Spreadsheet/SpreadsheetModel.cpp b/Applications/Spreadsheet/SpreadsheetModel.cpp index c4c5c286a6..84eb2b7198 100644 --- a/Applications/Spreadsheet/SpreadsheetModel.cpp +++ b/Applications/Spreadsheet/SpreadsheetModel.cpp @@ -67,8 +67,13 @@ GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) return cell->typed_display(); } - if (role == GUI::ModelRole::TextAlignment) - return {}; + 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() }); diff --git a/Applications/Spreadsheet/SpreadsheetView.cpp b/Applications/Spreadsheet/SpreadsheetView.cpp index 9229993a5e..c1ae829046 100644 --- a/Applications/Spreadsheet/SpreadsheetView.cpp +++ b/Applications/Spreadsheet/SpreadsheetView.cpp @@ -25,9 +25,11 @@ */ #include "SpreadsheetView.h" +#include "CellTypeDialog.h" #include "SpreadsheetModel.h" #include #include +#include #include #include #include @@ -112,6 +114,37 @@ SpreadsheetView::SpreadsheetView(Sheet& sheet) 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 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()); + } + + m_table_view->update(); + } + })); } void SpreadsheetView::hide_event(GUI::HideEvent&) diff --git a/Applications/Spreadsheet/SpreadsheetView.h b/Applications/Spreadsheet/SpreadsheetView.h index 608493a4dd..8d175df37e 100644 --- a/Applications/Spreadsheet/SpreadsheetView.h +++ b/Applications/Spreadsheet/SpreadsheetView.h @@ -138,6 +138,7 @@ private: NonnullRefPtr m_sheet; RefPtr m_table_view; + RefPtr m_cell_range_context_menu; }; }