2021-03-21 20:08:08 +03:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2020-2021, the SerenityOS developers.
|
|
|
|
*
|
2021-04-22 11:24:48 +03:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2021-03-21 20:08:08 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "ImportDialog.h"
|
|
|
|
#include "Spreadsheet.h"
|
|
|
|
#include <AK/JsonParser.h>
|
|
|
|
#include <AK/LexicalPath.h>
|
|
|
|
#include <Applications/Spreadsheet/CSVImportGML.h>
|
|
|
|
#include <Applications/Spreadsheet/FormatSelectionPageGML.h>
|
|
|
|
#include <LibGUI/Application.h>
|
|
|
|
#include <LibGUI/CheckBox.h>
|
|
|
|
#include <LibGUI/ComboBox.h>
|
|
|
|
#include <LibGUI/ItemListModel.h>
|
|
|
|
#include <LibGUI/RadioButton.h>
|
2021-03-25 08:55:24 +03:00
|
|
|
#include <LibGUI/StackWidget.h>
|
2021-03-21 20:08:08 +03:00
|
|
|
#include <LibGUI/TableView.h>
|
|
|
|
#include <LibGUI/TextBox.h>
|
2022-01-09 22:31:51 +03:00
|
|
|
#include <LibGUI/Window.h>
|
2021-03-21 20:08:08 +03:00
|
|
|
#include <LibGUI/Wizards/WizardDialog.h>
|
|
|
|
#include <LibGUI/Wizards/WizardPage.h>
|
|
|
|
|
|
|
|
namespace Spreadsheet {
|
|
|
|
|
|
|
|
CSVImportDialogPage::CSVImportDialogPage(StringView csv)
|
|
|
|
: m_csv(csv)
|
|
|
|
{
|
2023-06-08 14:46:11 +03:00
|
|
|
m_page = GUI::WizardPage::create(
|
|
|
|
"CSV Import Options"sv,
|
|
|
|
"Please select the options for the csv file you wish to import"sv)
|
|
|
|
.release_value_but_fixme_should_propagate_errors();
|
2021-03-21 20:08:08 +03:00
|
|
|
|
2023-01-07 15:38:23 +03:00
|
|
|
m_page->body_widget().load_from_gml(csv_import_gml).release_value_but_fixme_should_propagate_errors();
|
2021-03-21 20:08:08 +03:00
|
|
|
m_page->set_is_final_page(true);
|
|
|
|
|
|
|
|
m_delimiter_comma_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_comma_radio");
|
|
|
|
m_delimiter_semicolon_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_semicolon_radio");
|
|
|
|
m_delimiter_tab_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_tab_radio");
|
|
|
|
m_delimiter_space_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_space_radio");
|
|
|
|
m_delimiter_other_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_other_radio");
|
|
|
|
m_delimiter_other_text_box = m_page->body_widget().find_descendant_of_type_named<GUI::TextBox>("delimiter_other_text_box");
|
|
|
|
m_quote_single_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("quote_single_radio");
|
|
|
|
m_quote_double_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("quote_double_radio");
|
|
|
|
m_quote_other_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("quote_other_radio");
|
|
|
|
m_quote_other_text_box = m_page->body_widget().find_descendant_of_type_named<GUI::TextBox>("quote_other_text_box");
|
|
|
|
m_quote_escape_combo_box = m_page->body_widget().find_descendant_of_type_named<GUI::ComboBox>("quote_escape_combo_box");
|
|
|
|
m_read_header_check_box = m_page->body_widget().find_descendant_of_type_named<GUI::CheckBox>("read_header_check_box");
|
|
|
|
m_trim_leading_field_spaces_check_box = m_page->body_widget().find_descendant_of_type_named<GUI::CheckBox>("trim_leading_field_spaces_check_box");
|
|
|
|
m_trim_trailing_field_spaces_check_box = m_page->body_widget().find_descendant_of_type_named<GUI::CheckBox>("trim_trailing_field_spaces_check_box");
|
|
|
|
m_data_preview_table_view = m_page->body_widget().find_descendant_of_type_named<GUI::TableView>("data_preview_table_view");
|
2021-03-25 08:55:24 +03:00
|
|
|
m_data_preview_error_label = m_page->body_widget().find_descendant_of_type_named<GUI::Label>("data_preview_error_label");
|
|
|
|
m_data_preview_widget = m_page->body_widget().find_descendant_of_type_named<GUI::StackWidget>("data_preview_widget");
|
2021-03-21 20:08:08 +03:00
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
m_quote_escape_combo_box->set_model(GUI::ItemListModel<ByteString>::create(m_quote_escape_items));
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
// By default, use commas, double quotes with repeat, and disable headers.
|
|
|
|
m_delimiter_comma_radio->set_checked(true);
|
|
|
|
m_quote_double_radio->set_checked(true);
|
|
|
|
m_quote_escape_combo_box->set_selected_index(0); // Repeat
|
|
|
|
|
|
|
|
m_delimiter_comma_radio->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_delimiter_semicolon_radio->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_delimiter_tab_radio->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_delimiter_space_radio->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_delimiter_other_radio->on_checked = [&](auto) { update_preview(); };
|
AK+Everywhere: Disallow constructing Functions from incompatible types
Previously, AK::Function would accept _any_ callable type, and try to
call it when called, first with the given set of arguments, then with
zero arguments, and if all of those failed, it would simply not call the
function and **return a value-constructed Out type**.
This lead to many, many, many hard to debug situations when someone
forgot a `const` in their lambda argument types, and many cases of
people taking zero arguments in their lambdas to ignore them.
This commit reworks the Function interface to not include any such
surprising behaviour, if your function instance is not callable with
the declared argument set of the Function, it can simply not be
assigned to that Function instance, end of story.
2021-06-05 21:34:31 +03:00
|
|
|
m_delimiter_other_text_box->on_change = [&] {
|
2021-03-21 20:08:08 +03:00
|
|
|
if (m_delimiter_other_radio->is_checked())
|
|
|
|
update_preview();
|
|
|
|
};
|
|
|
|
m_quote_single_radio->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_quote_double_radio->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_quote_other_radio->on_checked = [&](auto) { update_preview(); };
|
AK+Everywhere: Disallow constructing Functions from incompatible types
Previously, AK::Function would accept _any_ callable type, and try to
call it when called, first with the given set of arguments, then with
zero arguments, and if all of those failed, it would simply not call the
function and **return a value-constructed Out type**.
This lead to many, many, many hard to debug situations when someone
forgot a `const` in their lambda argument types, and many cases of
people taking zero arguments in their lambdas to ignore them.
This commit reworks the Function interface to not include any such
surprising behaviour, if your function instance is not callable with
the declared argument set of the Function, it can simply not be
assigned to that Function instance, end of story.
2021-06-05 21:34:31 +03:00
|
|
|
m_quote_other_text_box->on_change = [&] {
|
2021-03-21 20:08:08 +03:00
|
|
|
if (m_quote_other_radio->is_checked())
|
|
|
|
update_preview();
|
|
|
|
};
|
AK+Everywhere: Disallow constructing Functions from incompatible types
Previously, AK::Function would accept _any_ callable type, and try to
call it when called, first with the given set of arguments, then with
zero arguments, and if all of those failed, it would simply not call the
function and **return a value-constructed Out type**.
This lead to many, many, many hard to debug situations when someone
forgot a `const` in their lambda argument types, and many cases of
people taking zero arguments in their lambdas to ignore them.
This commit reworks the Function interface to not include any such
surprising behaviour, if your function instance is not callable with
the declared argument set of the Function, it can simply not be
assigned to that Function instance, end of story.
2021-06-05 21:34:31 +03:00
|
|
|
m_quote_escape_combo_box->on_change = [&](auto&, auto&) { update_preview(); };
|
2021-03-21 20:08:08 +03:00
|
|
|
m_read_header_check_box->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_trim_leading_field_spaces_check_box->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_trim_trailing_field_spaces_check_box->on_checked = [&](auto) { update_preview(); };
|
|
|
|
|
|
|
|
update_preview();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto CSVImportDialogPage::make_reader() -> Optional<Reader::XSV>
|
|
|
|
{
|
2023-12-16 17:19:34 +03:00
|
|
|
ByteString delimiter;
|
|
|
|
ByteString quote;
|
2021-03-21 20:08:08 +03:00
|
|
|
Reader::ParserTraits::QuoteEscape quote_escape;
|
|
|
|
|
|
|
|
// Delimiter
|
|
|
|
if (m_delimiter_other_radio->is_checked())
|
|
|
|
delimiter = m_delimiter_other_text_box->text();
|
|
|
|
else if (m_delimiter_comma_radio->is_checked())
|
|
|
|
delimiter = ",";
|
|
|
|
else if (m_delimiter_semicolon_radio->is_checked())
|
|
|
|
delimiter = ";";
|
|
|
|
else if (m_delimiter_tab_radio->is_checked())
|
|
|
|
delimiter = "\t";
|
|
|
|
else if (m_delimiter_space_radio->is_checked())
|
|
|
|
delimiter = " ";
|
|
|
|
else
|
|
|
|
return {};
|
|
|
|
|
|
|
|
// Quote separator
|
|
|
|
if (m_quote_other_radio->is_checked())
|
|
|
|
quote = m_quote_other_text_box->text();
|
|
|
|
else if (m_quote_single_radio->is_checked())
|
|
|
|
quote = "'";
|
|
|
|
else if (m_quote_double_radio->is_checked())
|
|
|
|
quote = "\"";
|
|
|
|
else
|
|
|
|
return {};
|
|
|
|
|
|
|
|
// Quote escape
|
|
|
|
auto index = m_quote_escape_combo_box->selected_index();
|
|
|
|
if (index == 0)
|
|
|
|
quote_escape = Reader::ParserTraits::Repeat;
|
|
|
|
else if (index == 1)
|
|
|
|
quote_escape = Reader::ParserTraits::Backslash;
|
|
|
|
else
|
|
|
|
return {};
|
|
|
|
|
|
|
|
auto should_read_headers = m_read_header_check_box->is_checked();
|
|
|
|
auto should_trim_leading = m_trim_leading_field_spaces_check_box->is_checked();
|
|
|
|
auto should_trim_trailing = m_trim_trailing_field_spaces_check_box->is_checked();
|
|
|
|
|
|
|
|
if (quote.is_empty() || delimiter.is_empty())
|
|
|
|
return {};
|
|
|
|
|
|
|
|
Reader::ParserTraits traits {
|
|
|
|
move(delimiter),
|
|
|
|
move(quote),
|
|
|
|
quote_escape,
|
|
|
|
};
|
|
|
|
|
2021-09-07 13:56:50 +03:00
|
|
|
auto behaviors = Reader::default_behaviors() | Reader::ParserBehavior::Lenient;
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
if (should_read_headers)
|
2021-09-07 13:56:50 +03:00
|
|
|
behaviors = behaviors | Reader::ParserBehavior::ReadHeaders;
|
2021-03-21 20:08:08 +03:00
|
|
|
if (should_trim_leading)
|
2021-09-07 13:56:50 +03:00
|
|
|
behaviors = behaviors | Reader::ParserBehavior::TrimLeadingFieldSpaces;
|
2021-03-21 20:08:08 +03:00
|
|
|
if (should_trim_trailing)
|
2021-09-07 13:56:50 +03:00
|
|
|
behaviors = behaviors | Reader::ParserBehavior::TrimTrailingFieldSpaces;
|
2021-03-21 20:08:08 +03:00
|
|
|
|
2021-09-07 13:56:50 +03:00
|
|
|
return Reader::XSV(m_csv, move(traits), behaviors);
|
2023-07-08 05:48:11 +03:00
|
|
|
}
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
void CSVImportDialogPage::update_preview()
|
|
|
|
|
|
|
|
{
|
|
|
|
m_previously_made_reader = make_reader();
|
|
|
|
if (!m_previously_made_reader.has_value()) {
|
|
|
|
m_data_preview_table_view->set_model(nullptr);
|
2023-08-07 12:12:38 +03:00
|
|
|
m_data_preview_error_label->set_text("Could not read the given file"_string);
|
2021-03-25 08:55:24 +03:00
|
|
|
m_data_preview_widget->set_active_widget(m_data_preview_error_label);
|
2021-03-21 20:08:08 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto& reader = *m_previously_made_reader;
|
2021-03-25 08:55:24 +03:00
|
|
|
if (reader.has_error()) {
|
|
|
|
m_data_preview_table_view->set_model(nullptr);
|
2023-04-29 17:41:48 +03:00
|
|
|
m_data_preview_error_label->set_text(String::formatted("XSV parse error:\n{}", reader.error_string()).release_value_but_fixme_should_propagate_errors());
|
2021-03-25 08:55:24 +03:00
|
|
|
m_data_preview_widget->set_active_widget(m_data_preview_error_label);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-05-14 20:02:08 +03:00
|
|
|
Vector<String> headers;
|
|
|
|
for (auto const& header : reader.headers())
|
2023-12-16 17:19:34 +03:00
|
|
|
headers.append(String::from_byte_string(header).release_value_but_fixme_should_propagate_errors());
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
m_data_preview_table_view->set_model(
|
2023-05-14 20:02:08 +03:00
|
|
|
GUI::ItemListModel<Reader::XSV::Row, Reader::XSV, Vector<String>>::create(reader, headers, min(8ul, reader.size())));
|
2021-03-25 08:55:24 +03:00
|
|
|
m_data_preview_widget->set_active_widget(m_data_preview_table_view);
|
2021-03-21 20:08:08 +03:00
|
|
|
m_data_preview_table_view->update();
|
|
|
|
}
|
|
|
|
|
2024-01-23 19:26:43 +03:00
|
|
|
ErrorOr<Vector<NonnullRefPtr<Sheet>>, ByteString> ImportDialog::make_and_run_for(GUI::Window& parent, StringView mime, ByteString const& filename, Core::File& file, Workbook& workbook)
|
2021-03-21 20:08:08 +03:00
|
|
|
{
|
2023-06-08 14:46:11 +03:00
|
|
|
auto wizard = GUI::WizardDialog::create(&parent).release_value_but_fixme_should_propagate_errors();
|
2021-03-21 20:08:08 +03:00
|
|
|
wizard->set_title("File Import Wizard");
|
2022-07-11 20:32:29 +03:00
|
|
|
wizard->set_icon(GUI::Icon::default_icon("app-spreadsheet"sv).bitmap_for_size(16));
|
2021-03-21 20:08:08 +03:00
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
auto import_xsv = [&]() -> ErrorOr<Vector<NonnullRefPtr<Sheet>>, ByteString> {
|
2023-01-14 22:28:24 +03:00
|
|
|
auto contents_or_error = file.read_until_eof();
|
|
|
|
if (contents_or_error.is_error())
|
2023-12-16 17:19:34 +03:00
|
|
|
return ByteString::formatted("{}", contents_or_error.release_error());
|
2023-05-14 21:24:54 +03:00
|
|
|
CSVImportDialogPage page { contents_or_error.value() };
|
2021-03-21 20:08:08 +03:00
|
|
|
wizard->replace_page(page.page());
|
|
|
|
auto result = wizard->exec();
|
|
|
|
|
2022-05-13 15:10:27 +03:00
|
|
|
if (result == GUI::Dialog::ExecResult::OK) {
|
2021-03-21 20:08:08 +03:00
|
|
|
auto& reader = page.reader();
|
|
|
|
|
2023-03-06 16:17:01 +03:00
|
|
|
Vector<NonnullRefPtr<Sheet>> sheets;
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
if (reader.has_value()) {
|
2021-06-16 07:04:19 +03:00
|
|
|
reader->parse();
|
2021-03-25 08:55:24 +03:00
|
|
|
if (reader.value().has_error())
|
2023-12-16 17:19:34 +03:00
|
|
|
return ByteString::formatted("CSV Import failed: {}", reader.value().error_string());
|
2021-03-25 08:55:24 +03:00
|
|
|
|
2021-03-21 20:08:08 +03:00
|
|
|
auto sheet = Sheet::from_xsv(reader.value(), workbook);
|
|
|
|
if (sheet)
|
|
|
|
sheets.append(sheet.release_nonnull());
|
|
|
|
}
|
|
|
|
|
|
|
|
return sheets;
|
|
|
|
}
|
2023-01-15 02:25:51 +03:00
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
return ByteString { "CSV Import was cancelled" };
|
2021-03-21 20:08:08 +03:00
|
|
|
};
|
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
auto import_worksheet = [&]() -> ErrorOr<Vector<NonnullRefPtr<Sheet>>, ByteString> {
|
2023-01-14 22:28:24 +03:00
|
|
|
auto contents_or_error = file.read_until_eof();
|
|
|
|
if (contents_or_error.is_error())
|
2023-12-16 17:19:34 +03:00
|
|
|
return ByteString::formatted("{}", contents_or_error.release_error());
|
2023-01-14 22:28:24 +03:00
|
|
|
auto json_value_option = JsonParser(contents_or_error.release_value()).parse();
|
2023-01-15 02:25:51 +03:00
|
|
|
if (json_value_option.is_error())
|
2023-12-16 17:19:34 +03:00
|
|
|
return ByteString::formatted("Failed to parse {}", filename);
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
auto& json_value = json_value_option.value();
|
2023-01-15 02:25:51 +03:00
|
|
|
if (!json_value.is_array())
|
2023-12-16 17:19:34 +03:00
|
|
|
return ByteString::formatted("Did not find a spreadsheet in {}", filename);
|
2021-03-21 20:08:08 +03:00
|
|
|
|
2023-03-06 16:17:01 +03:00
|
|
|
Vector<NonnullRefPtr<Sheet>> sheets;
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
auto& json_array = json_value.as_array();
|
|
|
|
json_array.for_each([&](auto& sheet_json) {
|
|
|
|
if (!sheet_json.is_object())
|
|
|
|
return IterationDecision::Continue;
|
|
|
|
|
2021-03-22 15:33:22 +03:00
|
|
|
if (auto sheet = Sheet::from_json(sheet_json.as_object(), workbook))
|
|
|
|
sheets.append(sheet.release_nonnull());
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
return IterationDecision::Continue;
|
|
|
|
});
|
|
|
|
|
|
|
|
return sheets;
|
|
|
|
};
|
|
|
|
|
|
|
|
if (mime == "text/csv") {
|
|
|
|
return import_xsv();
|
2022-05-21 20:36:28 +03:00
|
|
|
} else if (mime == "application/x-sheets+json") {
|
2021-03-21 20:08:08 +03:00
|
|
|
return import_worksheet();
|
|
|
|
} else {
|
2023-06-08 14:46:11 +03:00
|
|
|
auto page = GUI::WizardPage::create(
|
|
|
|
"Import File Format"sv,
|
2024-01-23 19:26:43 +03:00
|
|
|
ByteString::formatted("Select the format you wish to import '{}' as", LexicalPath::basename(filename)))
|
2023-06-08 14:46:11 +03:00
|
|
|
.release_value_but_fixme_should_propagate_errors();
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
page->on_next_page = [] { return nullptr; };
|
|
|
|
|
2023-01-07 15:38:23 +03:00
|
|
|
page->body_widget().load_from_gml(select_format_page_gml).release_value_but_fixme_should_propagate_errors();
|
2021-03-21 20:08:08 +03:00
|
|
|
auto format_combo_box = page->body_widget().find_descendant_of_type_named<GUI::ComboBox>("select_format_page_format_combo_box");
|
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
Vector<ByteString> supported_formats {
|
2021-03-21 20:08:08 +03:00
|
|
|
"CSV (text/csv)",
|
|
|
|
"Spreadsheet Worksheet",
|
|
|
|
};
|
2023-12-16 17:19:34 +03:00
|
|
|
format_combo_box->set_model(GUI::ItemListModel<ByteString>::create(supported_formats));
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
wizard->push_page(page);
|
|
|
|
|
2022-05-13 15:10:27 +03:00
|
|
|
if (wizard->exec() != GUI::Dialog::ExecResult::OK)
|
2023-12-16 17:19:34 +03:00
|
|
|
return ByteString { "Import was cancelled" };
|
2021-03-21 20:08:08 +03:00
|
|
|
|
|
|
|
if (format_combo_box->selected_index() == 0)
|
|
|
|
return import_xsv();
|
|
|
|
|
|
|
|
if (format_combo_box->selected_index() == 1)
|
|
|
|
return import_worksheet();
|
|
|
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|