2021-03-20 10:30:49 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2020-2021, the SerenityOS developers.
|
|
|
|
*
|
2021-04-22 08:24:48 +00:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2021-03-20 10:30:49 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "ExportDialog.h"
|
|
|
|
#include "Spreadsheet.h"
|
|
|
|
#include "Workbook.h"
|
2023-12-16 14:19:34 +00:00
|
|
|
#include <AK/ByteString.h>
|
2021-03-20 10:30:49 +00:00
|
|
|
#include <AK/JsonArray.h>
|
|
|
|
#include <AK/LexicalPath.h>
|
2023-01-25 19:19:05 +00:00
|
|
|
#include <AK/MemoryStream.h>
|
2021-03-20 10:30:49 +00:00
|
|
|
#include <Applications/Spreadsheet/CSVExportGML.h>
|
|
|
|
#include <LibCore/StandardPaths.h>
|
|
|
|
#include <LibGUI/Application.h>
|
|
|
|
#include <LibGUI/CheckBox.h>
|
|
|
|
#include <LibGUI/ComboBox.h>
|
|
|
|
#include <LibGUI/ItemListModel.h>
|
|
|
|
#include <LibGUI/RadioButton.h>
|
|
|
|
#include <LibGUI/TextBox.h>
|
|
|
|
#include <LibGUI/Wizards/WizardDialog.h>
|
|
|
|
#include <LibGUI/Wizards/WizardPage.h>
|
2021-11-07 01:15:10 +00:00
|
|
|
#include <string.h>
|
2021-03-20 10:30:49 +00:00
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
// This is defined in ImportDialog.cpp, we can't include it twice, since the generated symbol is exported.
|
2022-07-11 17:32:29 +00:00
|
|
|
extern StringView select_format_page_gml;
|
2021-03-20 10:30:49 +00:00
|
|
|
|
|
|
|
namespace Spreadsheet {
|
|
|
|
|
2022-04-01 17:58:27 +00:00
|
|
|
CSVExportDialogPage::CSVExportDialogPage(Sheet const& sheet)
|
2021-03-20 10:30:49 +00:00
|
|
|
: m_data(sheet.to_xsv())
|
|
|
|
{
|
2021-06-12 11:24:45 +00:00
|
|
|
m_headers.extend(m_data.take_first());
|
2021-03-20 10:30:49 +00:00
|
|
|
|
2023-06-08 11:46:11 +00:00
|
|
|
m_page = GUI::WizardPage::create(
|
|
|
|
"CSV Export Options"sv,
|
|
|
|
"Please select the options for the csv file you wish to export to"sv)
|
|
|
|
.release_value_but_fixme_should_propagate_errors();
|
2021-03-20 10:30:49 +00:00
|
|
|
|
2023-01-07 12:38:23 +00:00
|
|
|
m_page->body_widget().load_from_gml(csv_export_gml).release_value_but_fixme_should_propagate_errors();
|
2021-03-20 10:30:49 +00: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_export_header_check_box = m_page->body_widget().find_descendant_of_type_named<GUI::CheckBox>("export_header_check_box");
|
|
|
|
m_quote_all_fields_check_box = m_page->body_widget().find_descendant_of_type_named<GUI::CheckBox>("quote_all_fields_check_box");
|
|
|
|
m_data_preview_text_editor = m_page->body_widget().find_descendant_of_type_named<GUI::TextEditor>("data_preview_text_editor");
|
|
|
|
|
|
|
|
m_data_preview_text_editor->set_should_hide_unnecessary_scrollbars(true);
|
|
|
|
|
2023-12-16 14:19:34 +00:00
|
|
|
m_quote_escape_combo_box->set_model(GUI::ItemListModel<ByteString>::create(m_quote_escape_items));
|
2021-03-20 10:30:49 +00:00
|
|
|
|
|
|
|
// By default, use commas, double quotes with repeat, disable headers, and quote only the fields that require quoting.
|
|
|
|
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 18:34:31 +00:00
|
|
|
m_delimiter_other_text_box->on_change = [&] {
|
2021-03-20 10:30:49 +00: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 18:34:31 +00:00
|
|
|
m_quote_other_text_box->on_change = [&] {
|
2021-03-20 10:30:49 +00: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 18:34:31 +00:00
|
|
|
m_quote_escape_combo_box->on_change = [&](auto&, auto&) { update_preview(); };
|
2021-03-20 10:30:49 +00:00
|
|
|
m_export_header_check_box->on_checked = [&](auto) { update_preview(); };
|
|
|
|
m_quote_all_fields_check_box->on_checked = [&](auto) { update_preview(); };
|
|
|
|
|
|
|
|
update_preview();
|
|
|
|
}
|
|
|
|
|
2023-02-10 00:00:18 +00:00
|
|
|
auto CSVExportDialogPage::generate(Stream& stream, GenerationType type) -> ErrorOr<void>
|
2021-03-20 10:30:49 +00:00
|
|
|
{
|
2023-12-16 14:19:34 +00:00
|
|
|
auto delimiter = TRY([this]() -> ErrorOr<ByteString> {
|
2022-12-31 14:38:12 +00:00
|
|
|
if (m_delimiter_other_radio->is_checked()) {
|
|
|
|
if (m_delimiter_other_text_box->text().is_empty())
|
|
|
|
return Error::from_string_literal("Delimiter unset");
|
|
|
|
return m_delimiter_other_text_box->text();
|
|
|
|
}
|
|
|
|
if (m_delimiter_comma_radio->is_checked())
|
|
|
|
return ",";
|
|
|
|
if (m_delimiter_semicolon_radio->is_checked())
|
|
|
|
return ";";
|
|
|
|
if (m_delimiter_tab_radio->is_checked())
|
|
|
|
return "\t";
|
|
|
|
if (m_delimiter_space_radio->is_checked())
|
|
|
|
return " ";
|
|
|
|
return Error::from_string_literal("Delimiter unset");
|
|
|
|
}());
|
|
|
|
|
2023-12-16 14:19:34 +00:00
|
|
|
auto quote = TRY([this]() -> ErrorOr<ByteString> {
|
2022-12-31 14:38:12 +00:00
|
|
|
if (m_quote_other_radio->is_checked()) {
|
|
|
|
if (m_quote_other_text_box->text().is_empty())
|
|
|
|
return Error::from_string_literal("Quote separator unset");
|
|
|
|
return m_quote_other_text_box->text();
|
|
|
|
}
|
|
|
|
if (m_quote_single_radio->is_checked())
|
|
|
|
return "'";
|
|
|
|
if (m_quote_double_radio->is_checked())
|
|
|
|
return "\"";
|
|
|
|
return Error::from_string_literal("Quote separator unset");
|
|
|
|
}());
|
|
|
|
|
|
|
|
auto quote_escape = [this]() {
|
|
|
|
auto index = m_quote_escape_combo_box->selected_index();
|
|
|
|
if (index == 0)
|
|
|
|
return Writer::WriterTraits::Repeat;
|
|
|
|
if (index == 1)
|
|
|
|
return Writer::WriterTraits::Backslash;
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
}();
|
2021-03-20 10:30:49 +00:00
|
|
|
|
|
|
|
auto should_export_headers = m_export_header_check_box->is_checked();
|
|
|
|
auto should_quote_all_fields = m_quote_all_fields_check_box->is_checked();
|
|
|
|
|
|
|
|
Writer::WriterTraits traits {
|
|
|
|
move(delimiter),
|
|
|
|
move(quote),
|
|
|
|
quote_escape,
|
|
|
|
};
|
|
|
|
|
2021-09-07 10:56:50 +00:00
|
|
|
auto behaviors = Writer::default_behaviors();
|
2023-12-16 14:19:34 +00:00
|
|
|
Vector<ByteString> empty_headers;
|
2021-03-20 10:30:49 +00:00
|
|
|
auto* headers = &empty_headers;
|
|
|
|
|
|
|
|
if (should_export_headers) {
|
2021-09-07 10:56:50 +00:00
|
|
|
behaviors = behaviors | Writer::WriterBehavior::WriteHeaders;
|
2021-03-20 10:30:49 +00:00
|
|
|
headers = &m_headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (should_quote_all_fields)
|
2021-09-07 10:56:50 +00:00
|
|
|
behaviors = behaviors | Writer::WriterBehavior::QuoteAll;
|
2021-03-20 10:30:49 +00:00
|
|
|
|
2023-01-14 17:11:29 +00:00
|
|
|
switch (type) {
|
|
|
|
case GenerationType::Normal:
|
2023-12-16 14:19:34 +00:00
|
|
|
TRY((Writer::XSV<decltype(m_data), Vector<ByteString>>::generate(stream, m_data, move(traits), *headers, behaviors)));
|
2023-01-14 17:11:29 +00:00
|
|
|
break;
|
|
|
|
case GenerationType::Preview:
|
|
|
|
TRY((Writer::XSV<decltype(m_data), decltype(*headers)>::generate_preview(stream, m_data, move(traits), *headers, behaviors)));
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
2021-03-20 10:30:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CSVExportDialogPage::update_preview()
|
|
|
|
{
|
2023-01-02 10:05:52 +00:00
|
|
|
auto maybe_error = [this]() -> ErrorOr<void> {
|
2023-01-25 19:19:05 +00:00
|
|
|
AllocatingMemoryStream memory_stream;
|
2023-01-14 17:11:29 +00:00
|
|
|
TRY(generate(memory_stream, GenerationType::Preview));
|
2023-01-02 10:05:52 +00:00
|
|
|
auto buffer = TRY(memory_stream.read_until_eof());
|
|
|
|
m_data_preview_text_editor->set_text(StringView(buffer));
|
|
|
|
m_data_preview_text_editor->update();
|
|
|
|
return {};
|
|
|
|
}();
|
|
|
|
if (maybe_error.is_error())
|
2023-12-16 14:19:34 +00:00
|
|
|
m_data_preview_text_editor->set_text(ByteString::formatted("Cannot update preview: {}", maybe_error.error()));
|
2021-03-20 10:30:49 +00:00
|
|
|
}
|
|
|
|
|
2023-12-16 14:19:34 +00:00
|
|
|
ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file, ByteString filename, Workbook& workbook)
|
2021-03-20 10:30:49 +00:00
|
|
|
{
|
2023-06-08 11:46:11 +00:00
|
|
|
auto wizard = TRY(GUI::WizardDialog::create(GUI::Application::the()->active_window()));
|
2021-03-20 10:30:49 +00:00
|
|
|
wizard->set_title("File Export Wizard");
|
2022-07-11 17:32:29 +00:00
|
|
|
wizard->set_icon(GUI::Icon::default_icon("app-spreadsheet"sv).bitmap_for_size(16));
|
2021-03-20 10:30:49 +00:00
|
|
|
|
2022-12-31 14:38:12 +00:00
|
|
|
auto export_xsv = [&]() -> ErrorOr<void> {
|
2021-03-20 10:30:49 +00:00
|
|
|
// FIXME: Prompt for the user to select a specific sheet to export
|
|
|
|
// For now, export the first sheet (if available)
|
|
|
|
if (!workbook.has_sheets())
|
2022-12-31 14:38:12 +00:00
|
|
|
return Error::from_string_literal("The workbook has no sheets to export!");
|
2021-03-20 10:30:49 +00:00
|
|
|
|
|
|
|
CSVExportDialogPage page { workbook.sheets().first() };
|
|
|
|
wizard->replace_page(page.page());
|
2023-01-01 19:10:06 +00:00
|
|
|
if (wizard->exec() != GUI::Dialog::ExecResult::OK)
|
2022-12-31 14:38:12 +00:00
|
|
|
return Error::from_string_literal("CSV Export was cancelled");
|
2023-01-01 19:10:06 +00:00
|
|
|
|
2023-01-14 19:28:24 +00:00
|
|
|
TRY(page.generate(file, CSVExportDialogPage::GenerationType::Normal));
|
2023-01-14 17:11:29 +00:00
|
|
|
return {};
|
2021-03-20 10:30:49 +00:00
|
|
|
};
|
|
|
|
|
2022-12-31 14:38:12 +00:00
|
|
|
auto export_worksheet = [&]() -> ErrorOr<void> {
|
2021-03-20 10:30:49 +00:00
|
|
|
JsonArray array;
|
|
|
|
for (auto& sheet : workbook.sheets())
|
2023-04-17 05:38:27 +00:00
|
|
|
array.must_append(sheet->to_json());
|
2021-03-20 10:30:49 +00:00
|
|
|
|
2023-12-16 14:19:34 +00:00
|
|
|
auto file_content = array.to_byte_string();
|
2023-03-01 14:37:45 +00:00
|
|
|
return file.write_until_depleted(file_content.bytes());
|
2021-03-20 10:30:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if (mime == "text/csv") {
|
|
|
|
return export_xsv();
|
2022-05-21 17:36:28 +00:00
|
|
|
} else if (mime == "application/x-sheets+json") {
|
2021-03-20 10:30:49 +00:00
|
|
|
return export_worksheet();
|
|
|
|
} else {
|
2023-06-08 11:46:11 +00:00
|
|
|
auto page = TRY(GUI::WizardPage::create(
|
|
|
|
"Export File Format"sv,
|
|
|
|
TRY(String::formatted("Select the format you wish to export to '{}' as", LexicalPath::basename(filename)))));
|
2021-03-20 10:30:49 +00:00
|
|
|
|
|
|
|
page->on_next_page = [] { return nullptr; };
|
|
|
|
|
2023-01-07 12:38:23 +00:00
|
|
|
TRY(page->body_widget().load_from_gml(select_format_page_gml));
|
2021-03-20 10:30:49 +00:00
|
|
|
auto format_combo_box = page->body_widget().find_descendant_of_type_named<GUI::ComboBox>("select_format_page_format_combo_box");
|
|
|
|
|
2023-12-16 14:19:34 +00:00
|
|
|
Vector<ByteString> supported_formats {
|
2021-03-20 10:30:49 +00:00
|
|
|
"CSV (text/csv)",
|
|
|
|
"Spreadsheet Worksheet",
|
|
|
|
};
|
2023-12-16 14:19:34 +00:00
|
|
|
format_combo_box->set_model(GUI::ItemListModel<ByteString>::create(supported_formats));
|
2021-03-20 10:30:49 +00:00
|
|
|
|
|
|
|
wizard->push_page(page);
|
|
|
|
|
2022-05-13 12:10:27 +00:00
|
|
|
if (wizard->exec() != GUI::Dialog::ExecResult::OK)
|
2022-12-31 14:38:12 +00:00
|
|
|
return Error::from_string_literal("Export was cancelled");
|
2021-03-20 10:30:49 +00:00
|
|
|
|
|
|
|
if (format_combo_box->selected_index() == 0)
|
|
|
|
return export_xsv();
|
|
|
|
|
|
|
|
if (format_combo_box->selected_index() == 1)
|
|
|
|
return export_worksheet();
|
|
|
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|