Spreadsheet: Add support for copying ranges of cells to other cells

Now the entire range is copied to the area around the target cell,
translating the current cursor to the target.
This commit is contained in:
AnotherTest 2020-11-07 23:18:41 +03:30 committed by Andreas Kling
parent 7878596532
commit e99c2261e3
Notes: sideshowbarker 2024-07-19 01:29:52 +09:00
8 changed files with 187 additions and 53 deletions

View file

@ -31,6 +31,7 @@
#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>
@ -38,6 +39,8 @@
#include <LibJS/Runtime/Function.h>
#include <ctype.h>
//#define COPY_DEBUG
namespace Spreadsheet {
Sheet::Sheet(const StringView& name, Workbook& workbook)
@ -204,21 +207,99 @@ Optional<Position> Sheet::parse_cell_name(const StringView& name)
}
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 nullptr;
return {};
}
if (url.protocol() != "spreadsheet" || url.host() != "cell") {
dbgln("Bad url: {}", url.to_string());
return nullptr;
return {};
}
// FIXME: Figure out a way to do this cross-process.
ASSERT(url.path() == String::formatted("/{}", getpid()));
return at(url.fragment());
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()) {
dbg() << "Column '" << offset.column << "' does not exist!";
return base;
}
if (offset_base_column_it.is_end()) {
dbg() << "Column '" << offset_base.column << "' does not exist!";
return base;
}
if (base_column_it.is_end()) {
dbg() << "Column '" << base.column << "' does not exist!";
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;
}
auto ref_cells = target_cell.referencing_cells;
target_cell = *source_cell;
target_cell.dirty = true;
target_cell.referencing_cells = move(ref_cells);
};
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;
}
// Just disallow misaligned copies.
dbg() << "Cannot copy " << from.size() << " cells to " << to.size() << " cells";
}
RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook)

View file

@ -51,6 +51,11 @@ public:
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&);
@ -87,6 +92,14 @@ public:
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);
@ -105,6 +118,8 @@ public:
const Workbook& workbook() const { return m_workbook; }
void copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to = {});
private:
explicit Sheet(Workbook&);
explicit Sheet(const StringView& name, Workbook&);

View file

@ -27,6 +27,7 @@
#include "SpreadsheetModel.h"
#include "ConditionalFormatting.h"
#include <AK/URL.h>
#include <LibGUI/AbstractView.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Object.h>
@ -69,11 +70,8 @@ GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role)
return cell->typed_display();
}
if (role == GUI::ModelRole::DragData) {
// FIXME: It would be really nice if we could send out a URL *and* some extra data,
// The Event already has support for this, but the user-facing API does not.
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() });
@ -119,6 +117,30 @@ GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role)
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)

View file

@ -40,6 +40,7 @@ public:
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;

View file

@ -157,28 +157,24 @@ SpreadsheetView::SpreadsheetView(Sheet& sheet)
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() };
bool first = true;
for (auto url : urls.lines(false)) {
if (!first) { // FIXME: Allow d&d from many cells to many cells, somehow.
dbg() << "Ignored '" << url << "'";
continue;
}
Vector<Position> source_positions, target_positions;
first = false;
auto& target_cell = m_sheet->ensure({ m_sheet->column(index.column()), (size_t)index.row() });
auto* source_cell = m_sheet->from_url(url);
if (!source_cell) {
target_cell.set_data("");
return;
}
auto ref_cells = target_cell.referencing_cells;
target_cell = *source_cell;
target_cell.dirty = true;
target_cell.referencing_cells = move(ref_cells);
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;
}

View file

@ -29,6 +29,7 @@
#include "Spreadsheet.h"
#include <LibGUI/AbstractTableView.h>
#include <LibGUI/ModelEditingDelegate.h>
#include <LibGUI/TableView.h>
#include <LibGUI/Widget.h>
#include <string.h>
@ -87,6 +88,11 @@ public:
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;

View file

@ -52,6 +52,14 @@ public:
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:
explicit SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets = {}, bool should_add_sheet_if_empty = true);

View file

@ -27,6 +27,7 @@
#include "HelpWindow.h"
#include "Spreadsheet.h"
#include "SpreadsheetWidget.h"
#include <AK/ScopeGuard.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibGUI/AboutDialog.h>
@ -153,11 +154,26 @@ int main(int argc, char* argv[])
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');
@ -175,41 +191,30 @@ int main(int argc, char* argv[])
},
window));
edit_menu.add_action(GUI::CommonActions::make_paste_action([&](auto&) {
ScopeGuard update_after_paste { [&] { spreadsheet_widget.current_worksheet().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<URL> urls;
for (auto line : spreadsheet_data.value().split_view('\n')) {
if (line.is_empty())
continue;
URL url { line };
if (!url.is_valid())
continue;
urls.append(move(url));
Vector<Spreadsheet::Position> source_positions, target_positions;
auto& sheet = spreadsheet_widget.current_worksheet();
for (auto& line : spreadsheet_data.value().split_view('\n')) {
dbg() << "Paste line '" << line << "'";
auto position = sheet.position_from_url(line);
if (position.has_value())
source_positions.append(position.release_value());
}
if (urls.size() == 1 && cells.size() == 1) {
auto& cell = *cells.begin();
auto& url = urls.first();
auto* source_cell = spreadsheet_widget.current_worksheet().from_url(url);
if (source_cell) {
auto& target_cell = spreadsheet_widget.current_worksheet().ensure(cell);
auto references = target_cell.referencing_cells;
target_cell = *source_cell;
target_cell.referencing_cells = move(references);
target_cell.dirty = true;
spreadsheet_widget.update();
}
for (auto& position : spreadsheet_widget.current_worksheet().selected_cells())
target_positions.append(position);
if (source_positions.is_empty())
return;
}
if (urls.size() != cells.size()) {
// FIXME: Somehow copy a bunch of cells into another bunch of cells.
TODO();
}
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() });