1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-14 12:05:00 +00:00
serenity/Userland/Applications/Spreadsheet/SpreadsheetWidget.cpp
creator1creeper1 19d9d5bfe1 Everywhere: Mark Vector of mutable references as mutable
The point of a reference type is to behave just like the referred-to
type. So, a Foo& should behave just like a Foo.

In these cases, we had a const Vector. If it was a const Vector of Foo,
iterating over the Vector would only permit taking const references to
the individual Foos.

However, we had a const Vector of Foo&. The behavior should not
change. We should still only be permitted to take const references to
the individual Foos. Otherwise, we would be allowed to mutate the
individual Foos, which would mutate the elements of the const Vector.
This wouldn't modify the stored pointers, but it would modify the
objects that the references refer to. Since references should be
transparent, this should not be legal.

So it should be impossible to get mutable references into a const
Vector. Since we need mutable references in these cases to call the
mutating member functions, we need to mark the Vector as mutable as
well.
2022-01-16 00:38:21 +03:30

552 lines
21 KiB
C++

/*
* Copyright (c) 2020, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "SpreadsheetWidget.h"
#include "CellSyntaxHighlighter.h"
#include "HelpWindow.h"
#include "LibFileSystemAccessClient/Client.h"
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/InputBox.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 <LibGUI/Toolbar.h>
#include <LibGUI/ToolbarContainer.h>
#include <LibGfx/FontDatabase.h>
#include <string.h>
namespace Spreadsheet {
SpreadsheetWidget::SpreadsheetWidget(GUI::Window& parent_window, NonnullRefPtrVector<Sheet>&& sheets, bool should_add_sheet_if_empty)
: m_workbook(make<Workbook>(move(sheets), parent_window))
{
set_fill_with_background_color(true);
set_layout<GUI::VerticalBoxLayout>().set_margins(2);
auto& toolbar_container = add<GUI::ToolbarContainer>();
auto& toolbar = toolbar_container.add<GUI::Toolbar>();
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>("i");
help_button.set_fixed_size(20, 20);
help_button.on_click = [&](auto) {
if (!m_selected_view) {
GUI::MessageBox::show_error(window(), "Can only show function documentation/help when a worksheet exists and is open");
} else if (auto* sheet_ptr = m_selected_view->sheet_if_available()) {
auto docs = sheet_ptr->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.on_return_pressed = [this]() {
m_selected_view->move_cursor(GUI::AbstractView::CursorMovement::Down);
};
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);
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();
m_rename_action = GUI::Action::create("Rename...", [this](auto&) {
VERIFY(m_tab_context_menu_sheet_view);
auto* sheet_ptr = m_tab_context_menu_sheet_view->sheet_if_available();
VERIFY(sheet_ptr); // How did we get here without a sheet?
auto& sheet = *sheet_ptr;
String new_name;
if (GUI::InputBox::show(window(), new_name, 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(*m_rename_action);
m_tab_context_menu->add_action(GUI::Action::create("Add new sheet...", [this](auto&) {
String name;
if (GUI::InputBox::show(window(), name, "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());
m_new_action = GUI::Action::create("Add New Sheet", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/new-tab.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
add_sheet();
});
m_open_action = GUI::CommonActions::make_open_action([&](auto&) {
Optional<String> load_path = GUI::FilePicker::get_open_filepath(window());
if (!load_path.has_value())
return;
auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(window()->window_id(), *load_path);
if (response.error != 0) {
if (response.error != -1)
GUI::MessageBox::show_error(window(), String::formatted("Opening \"{}\" failed: {}", *response.chosen_file, strerror(response.error)));
return;
}
load_file(*response.fd, *load_path);
});
m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
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;
save(save_path.value());
} else {
save(current_filename());
}
});
m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
String name = "workbook";
Optional<String> save_path = GUI::FilePicker::get_save_filepath(window(), name, "sheets");
if (!save_path.has_value())
return;
save(save_path.value());
if (!current_filename().is_empty())
set_filename(current_filename());
});
m_quit_action = GUI::CommonActions::make_quit_action([&](auto&) {
if (!request_close())
return;
GUI::Application::the()->quit(0);
});
m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) { clipboard_action(true); }, window());
m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) { clipboard_action(false); }, window());
m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) {
ScopeGuard update_after_paste { [&] { update(); } };
auto* worksheet_ptr = current_worksheet_if_available();
if (!worksheet_ptr) {
GUI::MessageBox::show_error(window(), "There are no active worksheets");
return;
}
auto& sheet = *worksheet_ptr;
auto& cells = sheet.selected_cells();
VERIFY(!cells.is_empty());
const auto& data = GUI::Clipboard::the().fetch_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 lines = spreadsheet_data.value().split_view('\n');
auto action = lines.take_first();
for (auto& line : lines) {
dbgln("Paste line '{}'", line);
auto position = sheet.position_from_url(line);
if (position.has_value())
source_positions.append(position.release_value());
}
for (auto& position : sheet.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, action == "cut" ? Spreadsheet::Sheet::CopyOperation::Cut : Spreadsheet::Sheet::CopyOperation::Copy);
} else {
for (auto& cell : sheet.selected_cells())
sheet.ensure(cell).set_data(StringView { data.data.data(), data.data.size() });
update();
}
},
window());
m_functions_help_action = GUI::Action::create(
"&Functions Help", [&](auto&) {
if (auto* worksheet_ptr = current_worksheet_if_available()) {
auto docs = worksheet_ptr->gather_documentation();
auto help_window = Spreadsheet::HelpWindow::the(window());
help_window->set_docs(move(docs));
help_window->show();
} else {
GUI::MessageBox::show_error(window(), "Cannot prepare documentation/help without an active worksheet");
}
},
window());
m_about_action = GUI::CommonActions::make_about_action("Spreadsheet", GUI::Icon::default_icon("app-spreadsheet"), window());
toolbar.add_action(*m_new_action);
toolbar.add_action(*m_open_action);
toolbar.add_action(*m_save_action);
toolbar.add_separator();
toolbar.add_action(*m_cut_action);
toolbar.add_action(*m_copy_action);
toolbar.add_action(*m_paste_action);
}
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) {
auto* sheet_ptr = m_selected_view->sheet_if_available();
// How did this even happen?
VERIFY(sheet_ptr);
auto& sheet = *sheet_ptr;
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();
m_current_cell_label->set_enabled(true);
m_current_cell_label->set_text(position.to_cell_identifier(sheet));
auto& cell = 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));
sheet.update();
update();
};
m_cell_value_editor->set_enabled(true);
static_cast<CellSyntaxHighlighter*>(const_cast<Syntax::Highlighter*>(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(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]() mutable {
if (m_should_change_selected_cells) {
auto* sheet_ptr = m_selected_view->sheet_if_available();
if (!sheet_ptr)
return;
auto& sheet = *sheet_ptr;
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);
sheet.update();
update();
}
};
m_cell_value_editor->set_enabled(true);
static_cast<CellSyntaxHighlighter*>(const_cast<Syntax::Highlighter*>(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<Syntax::Highlighter*>(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 = static_cast<SpreadsheetView&>(widget);
m_tab_context_menu->popup(event.screen_position());
};
m_tab_widget->on_double_click = [&](auto& widget) {
m_tab_context_menu_sheet_view = static_cast<SpreadsheetView&>(widget);
VERIFY(m_tab_context_menu_sheet_view);
auto* sheet_ptr = m_tab_context_menu_sheet_view->sheet_if_available();
VERIFY(sheet_ptr); // How did we get here without a sheet?
auto& sheet = *sheet_ptr;
String new_name;
if (GUI::InputBox::show(window(), new_name, 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);
}
};
}
void SpreadsheetWidget::try_generate_tip_for_input_expression(StringView source, size_t cursor_offset)
{
auto* sheet_ptr = m_selected_view->sheet_if_available();
if (!sheet_ptr)
return;
auto& sheet = *sheet_ptr;
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 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(StringView filename)
{
auto result = m_workbook->save(filename);
if (result.is_error())
GUI::MessageBox::show_error(window(), result.error());
}
void SpreadsheetWidget::load_file(int fd, StringView filename)
{
auto result = m_workbook->open_file(fd, 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)
{
VERIFY(m_workbook == &sheet->workbook());
NonnullRefPtrVector<Sheet> new_sheets;
new_sheets.append(move(sheet));
m_workbook->sheets().extend(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();
}
}
void SpreadsheetWidget::clipboard_action(bool is_cut)
{
/// text/x-spreadsheet-data:
/// - action: copy/cut
/// - currently selected cell
/// - selected cell+
auto* worksheet_ptr = current_worksheet_if_available();
if (!worksheet_ptr) {
GUI::MessageBox::show_error(window(), "There are no active worksheets");
return;
}
auto& worksheet = *worksheet_ptr;
auto& cells = worksheet.selected_cells();
VERIFY(!cells.is_empty());
StringBuilder text_builder, url_builder;
url_builder.append(is_cut ? "cut\n" : "copy\n");
bool first = true;
auto cursor = current_selection_cursor();
if (cursor) {
Spreadsheet::Position position { (size_t)cursor->column(), (size_t)cursor->row() };
url_builder.append(position.to_url(worksheet).to_string());
url_builder.append('\n');
}
for (auto& cell : cells) {
if (first && !cursor) {
url_builder.append(cell.to_url(worksheet).to_string());
url_builder.append('\n');
}
url_builder.append(cell.to_url(worksheet).to_string());
url_builder.append('\n');
auto cell_data = 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());
dbgln(url_builder.to_string());
GUI::Clipboard::the().set_data(text_builder.string_view().bytes(), "text/plain", move(metadata));
}
void SpreadsheetWidget::initialize_menubar(GUI::Window& window)
{
auto& file_menu = window.add_menu("&File");
file_menu.add_action(*m_new_action);
file_menu.add_action(*m_open_action);
file_menu.add_action(*m_save_action);
file_menu.add_action(*m_save_as_action);
file_menu.add_separator();
file_menu.add_action(*m_quit_action);
auto& edit_menu = window.add_menu("&Edit");
edit_menu.add_action(*m_cut_action);
edit_menu.add_action(*m_copy_action);
edit_menu.add_action(*m_paste_action);
auto& help_menu = window.add_menu("&Help");
help_menu.add_action(*m_functions_help_action);
help_menu.add_action(*m_about_action);
}
SpreadsheetWidget::~SpreadsheetWidget()
{
}
}