From e99c2261e348510edf5c549fc75a9fac4372cedf Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Sat, 7 Nov 2020 23:18:41 +0330 Subject: [PATCH] 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. --- Applications/Spreadsheet/Spreadsheet.cpp | 87 ++++++++++++++++++- Applications/Spreadsheet/Spreadsheet.h | 15 ++++ Applications/Spreadsheet/SpreadsheetModel.cpp | 30 ++++++- Applications/Spreadsheet/SpreadsheetModel.h | 1 + Applications/Spreadsheet/SpreadsheetView.cpp | 36 ++++---- Applications/Spreadsheet/SpreadsheetView.h | 6 ++ Applications/Spreadsheet/SpreadsheetWidget.h | 8 ++ Applications/Spreadsheet/main.cpp | 57 ++++++------ 8 files changed, 187 insertions(+), 53 deletions(-) diff --git a/Applications/Spreadsheet/Spreadsheet.cpp b/Applications/Spreadsheet/Spreadsheet.cpp index 309b9c10c8..07ed58ba3d 100644 --- a/Applications/Spreadsheet/Spreadsheet.cpp +++ b/Applications/Spreadsheet/Spreadsheet.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,8 @@ #include #include +//#define COPY_DEBUG + namespace Spreadsheet { Sheet::Sheet(const StringView& name, Workbook& workbook) @@ -204,21 +207,99 @@ Optional 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 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 from, Vector to, Optional 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::from_json(const JsonObject& object, Workbook& workbook) diff --git a/Applications/Spreadsheet/Spreadsheet.h b/Applications/Spreadsheet/Spreadsheet.h index 4f1c3f2110..4748e79879 100644 --- a/Applications/Spreadsheet/Spreadsheet.h +++ b/Applications/Spreadsheet/Spreadsheet.h @@ -51,6 +51,11 @@ public: Cell* from_url(const URL&); const Cell* from_url(const URL& url) const { return const_cast(this)->from_url(url); } + Optional 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 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& 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 from, Vector to, Optional resolve_relative_to = {}); + private: explicit Sheet(Workbook&); explicit Sheet(const StringView& name, Workbook&); diff --git a/Applications/Spreadsheet/SpreadsheetModel.cpp b/Applications/Spreadsheet/SpreadsheetModel.cpp index cf07b0c6af..58ac126095 100644 --- a/Applications/Spreadsheet/SpreadsheetModel.cpp +++ b/Applications/Spreadsheet/SpreadsheetModel.cpp @@ -27,6 +27,7 @@ #include "SpreadsheetModel.h" #include "ConditionalFormatting.h" #include +#include #include #include @@ -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 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(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) diff --git a/Applications/Spreadsheet/SpreadsheetModel.h b/Applications/Spreadsheet/SpreadsheetModel.h index 8edf46ab1e..8926f6104b 100644 --- a/Applications/Spreadsheet/SpreadsheetModel.h +++ b/Applications/Spreadsheet/SpreadsheetModel.h @@ -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 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; diff --git a/Applications/Spreadsheet/SpreadsheetView.cpp b/Applications/Spreadsheet/SpreadsheetView.cpp index 681d99736f..dfb718182d 100644 --- a/Applications/Spreadsheet/SpreadsheetView.cpp +++ b/Applications/Spreadsheet/SpreadsheetView.cpp @@ -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 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; } diff --git a/Applications/Spreadsheet/SpreadsheetView.h b/Applications/Spreadsheet/SpreadsheetView.h index 8d175df37e..93a3c83acc 100644 --- a/Applications/Spreadsheet/SpreadsheetView.h +++ b/Applications/Spreadsheet/SpreadsheetView.h @@ -29,6 +29,7 @@ #include "Spreadsheet.h" #include #include +#include #include #include @@ -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&&)> on_selection_changed; Function on_selection_dropped; diff --git a/Applications/Spreadsheet/SpreadsheetWidget.h b/Applications/Spreadsheet/SpreadsheetWidget.h index ae1121a120..27060b2843 100644 --- a/Applications/Spreadsheet/SpreadsheetWidget.h +++ b/Applications/Spreadsheet/SpreadsheetWidget.h @@ -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&& sheets = {}, bool should_add_sheet_if_empty = true); diff --git a/Applications/Spreadsheet/main.cpp b/Applications/Spreadsheet/main.cpp index 0cf7e8a748..8233f356aa 100644 --- a/Applications/Spreadsheet/main.cpp +++ b/Applications/Spreadsheet/main.cpp @@ -27,6 +27,7 @@ #include "HelpWindow.h" #include "Spreadsheet.h" #include "SpreadsheetWidget.h" +#include #include #include #include @@ -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 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 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() });