diff options
author | AnotherTest <ali.mpfard@gmail.com> | 2020-11-07 23:18:41 +0330 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-11-08 21:46:13 +0100 |
commit | e99c2261e348510edf5c549fc75a9fac4372cedf (patch) | |
tree | 73eafeaf2b2874c0dc9956848b1807f00c0831ae /Applications | |
parent | 7878596532f32b65be06dd032035b5ecbf35c896 (diff) | |
download | serenity-e99c2261e348510edf5c549fc75a9fac4372cedf.zip |
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.
Diffstat (limited to 'Applications')
-rw-r--r-- | Applications/Spreadsheet/Spreadsheet.cpp | 87 | ||||
-rw-r--r-- | Applications/Spreadsheet/Spreadsheet.h | 15 | ||||
-rw-r--r-- | Applications/Spreadsheet/SpreadsheetModel.cpp | 30 | ||||
-rw-r--r-- | Applications/Spreadsheet/SpreadsheetModel.h | 1 | ||||
-rw-r--r-- | Applications/Spreadsheet/SpreadsheetView.cpp | 38 | ||||
-rw-r--r-- | Applications/Spreadsheet/SpreadsheetView.h | 6 | ||||
-rw-r--r-- | Applications/Spreadsheet/SpreadsheetWidget.h | 8 | ||||
-rw-r--r-- | Applications/Spreadsheet/main.cpp | 57 |
8 files changed, 188 insertions, 54 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 <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) @@ -205,20 +208,98 @@ 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) 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<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&); 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 <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) 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<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; 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; - } - - 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); + Vector<Position> source_positions, target_positions; + + 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 <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; 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<Sheet>&& 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 <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() }); |