Serenity Operating System
at master 246 lines 10 kB view raw
1/* 2 * Copyright (c) 2020-2021, the SerenityOS developers. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "ExportDialog.h" 8#include "Spreadsheet.h" 9#include "Workbook.h" 10#include <AK/DeprecatedString.h> 11#include <AK/JsonArray.h> 12#include <AK/LexicalPath.h> 13#include <AK/MemoryStream.h> 14#include <Applications/Spreadsheet/CSVExportGML.h> 15#include <LibCore/StandardPaths.h> 16#include <LibGUI/Application.h> 17#include <LibGUI/CheckBox.h> 18#include <LibGUI/ComboBox.h> 19#include <LibGUI/ItemListModel.h> 20#include <LibGUI/RadioButton.h> 21#include <LibGUI/TextBox.h> 22#include <LibGUI/Wizards/WizardDialog.h> 23#include <LibGUI/Wizards/WizardPage.h> 24#include <string.h> 25#include <unistd.h> 26 27// This is defined in ImportDialog.cpp, we can't include it twice, since the generated symbol is exported. 28extern StringView select_format_page_gml; 29 30namespace Spreadsheet { 31 32CSVExportDialogPage::CSVExportDialogPage(Sheet const& sheet) 33 : m_data(sheet.to_xsv()) 34{ 35 m_headers.extend(m_data.take_first()); 36 37 m_page = GUI::WizardPage::construct( 38 "CSV Export Options", 39 "Please select the options for the csv file you wish to export to"); 40 41 m_page->body_widget().load_from_gml(csv_export_gml).release_value_but_fixme_should_propagate_errors(); 42 m_page->set_is_final_page(true); 43 44 m_delimiter_comma_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_comma_radio"); 45 m_delimiter_semicolon_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_semicolon_radio"); 46 m_delimiter_tab_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_tab_radio"); 47 m_delimiter_space_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_space_radio"); 48 m_delimiter_other_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("delimiter_other_radio"); 49 m_delimiter_other_text_box = m_page->body_widget().find_descendant_of_type_named<GUI::TextBox>("delimiter_other_text_box"); 50 m_quote_single_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("quote_single_radio"); 51 m_quote_double_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("quote_double_radio"); 52 m_quote_other_radio = m_page->body_widget().find_descendant_of_type_named<GUI::RadioButton>("quote_other_radio"); 53 m_quote_other_text_box = m_page->body_widget().find_descendant_of_type_named<GUI::TextBox>("quote_other_text_box"); 54 m_quote_escape_combo_box = m_page->body_widget().find_descendant_of_type_named<GUI::ComboBox>("quote_escape_combo_box"); 55 m_export_header_check_box = m_page->body_widget().find_descendant_of_type_named<GUI::CheckBox>("export_header_check_box"); 56 m_quote_all_fields_check_box = m_page->body_widget().find_descendant_of_type_named<GUI::CheckBox>("quote_all_fields_check_box"); 57 m_data_preview_text_editor = m_page->body_widget().find_descendant_of_type_named<GUI::TextEditor>("data_preview_text_editor"); 58 59 m_data_preview_text_editor->set_should_hide_unnecessary_scrollbars(true); 60 61 m_quote_escape_combo_box->set_model(GUI::ItemListModel<DeprecatedString>::create(m_quote_escape_items)); 62 63 // By default, use commas, double quotes with repeat, disable headers, and quote only the fields that require quoting. 64 m_delimiter_comma_radio->set_checked(true); 65 m_quote_double_radio->set_checked(true); 66 m_quote_escape_combo_box->set_selected_index(0); // Repeat 67 68 m_delimiter_comma_radio->on_checked = [&](auto) { update_preview(); }; 69 m_delimiter_semicolon_radio->on_checked = [&](auto) { update_preview(); }; 70 m_delimiter_tab_radio->on_checked = [&](auto) { update_preview(); }; 71 m_delimiter_space_radio->on_checked = [&](auto) { update_preview(); }; 72 m_delimiter_other_radio->on_checked = [&](auto) { update_preview(); }; 73 m_delimiter_other_text_box->on_change = [&] { 74 if (m_delimiter_other_radio->is_checked()) 75 update_preview(); 76 }; 77 m_quote_single_radio->on_checked = [&](auto) { update_preview(); }; 78 m_quote_double_radio->on_checked = [&](auto) { update_preview(); }; 79 m_quote_other_radio->on_checked = [&](auto) { update_preview(); }; 80 m_quote_other_text_box->on_change = [&] { 81 if (m_quote_other_radio->is_checked()) 82 update_preview(); 83 }; 84 m_quote_escape_combo_box->on_change = [&](auto&, auto&) { update_preview(); }; 85 m_export_header_check_box->on_checked = [&](auto) { update_preview(); }; 86 m_quote_all_fields_check_box->on_checked = [&](auto) { update_preview(); }; 87 88 update_preview(); 89} 90 91auto CSVExportDialogPage::generate(Stream& stream, GenerationType type) -> ErrorOr<void> 92{ 93 auto delimiter = TRY([this]() -> ErrorOr<DeprecatedString> { 94 if (m_delimiter_other_radio->is_checked()) { 95 if (m_delimiter_other_text_box->text().is_empty()) 96 return Error::from_string_literal("Delimiter unset"); 97 return m_delimiter_other_text_box->text(); 98 } 99 if (m_delimiter_comma_radio->is_checked()) 100 return ","; 101 if (m_delimiter_semicolon_radio->is_checked()) 102 return ";"; 103 if (m_delimiter_tab_radio->is_checked()) 104 return "\t"; 105 if (m_delimiter_space_radio->is_checked()) 106 return " "; 107 return Error::from_string_literal("Delimiter unset"); 108 }()); 109 110 auto quote = TRY([this]() -> ErrorOr<DeprecatedString> { 111 if (m_quote_other_radio->is_checked()) { 112 if (m_quote_other_text_box->text().is_empty()) 113 return Error::from_string_literal("Quote separator unset"); 114 return m_quote_other_text_box->text(); 115 } 116 if (m_quote_single_radio->is_checked()) 117 return "'"; 118 if (m_quote_double_radio->is_checked()) 119 return "\""; 120 return Error::from_string_literal("Quote separator unset"); 121 }()); 122 123 auto quote_escape = [this]() { 124 auto index = m_quote_escape_combo_box->selected_index(); 125 if (index == 0) 126 return Writer::WriterTraits::Repeat; 127 if (index == 1) 128 return Writer::WriterTraits::Backslash; 129 VERIFY_NOT_REACHED(); 130 }(); 131 132 auto should_export_headers = m_export_header_check_box->is_checked(); 133 auto should_quote_all_fields = m_quote_all_fields_check_box->is_checked(); 134 135 Writer::WriterTraits traits { 136 move(delimiter), 137 move(quote), 138 quote_escape, 139 }; 140 141 auto behaviors = Writer::default_behaviors(); 142 Vector<DeprecatedString> empty_headers; 143 auto* headers = &empty_headers; 144 145 if (should_export_headers) { 146 behaviors = behaviors | Writer::WriterBehavior::WriteHeaders; 147 headers = &m_headers; 148 } 149 150 if (should_quote_all_fields) 151 behaviors = behaviors | Writer::WriterBehavior::QuoteAll; 152 153 switch (type) { 154 case GenerationType::Normal: 155 TRY((Writer::XSV<decltype(m_data), Vector<DeprecatedString>>::generate(stream, m_data, move(traits), *headers, behaviors))); 156 break; 157 case GenerationType::Preview: 158 TRY((Writer::XSV<decltype(m_data), decltype(*headers)>::generate_preview(stream, m_data, move(traits), *headers, behaviors))); 159 break; 160 default: 161 VERIFY_NOT_REACHED(); 162 } 163 164 return {}; 165} 166 167void CSVExportDialogPage::update_preview() 168{ 169 auto maybe_error = [this]() -> ErrorOr<void> { 170 AllocatingMemoryStream memory_stream; 171 TRY(generate(memory_stream, GenerationType::Preview)); 172 auto buffer = TRY(memory_stream.read_until_eof()); 173 m_data_preview_text_editor->set_text(StringView(buffer)); 174 m_data_preview_text_editor->update(); 175 return {}; 176 }(); 177 if (maybe_error.is_error()) 178 m_data_preview_text_editor->set_text(DeprecatedString::formatted("Cannot update preview: {}", maybe_error.error())); 179} 180 181ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file, DeprecatedString filename, Workbook& workbook) 182{ 183 auto wizard = GUI::WizardDialog::construct(GUI::Application::the()->active_window()); 184 wizard->set_title("File Export Wizard"); 185 wizard->set_icon(GUI::Icon::default_icon("app-spreadsheet"sv).bitmap_for_size(16)); 186 187 auto export_xsv = [&]() -> ErrorOr<void> { 188 // FIXME: Prompt for the user to select a specific sheet to export 189 // For now, export the first sheet (if available) 190 if (!workbook.has_sheets()) 191 return Error::from_string_literal("The workbook has no sheets to export!"); 192 193 CSVExportDialogPage page { workbook.sheets().first() }; 194 wizard->replace_page(page.page()); 195 if (wizard->exec() != GUI::Dialog::ExecResult::OK) 196 return Error::from_string_literal("CSV Export was cancelled"); 197 198 TRY(page.generate(file, CSVExportDialogPage::GenerationType::Normal)); 199 return {}; 200 }; 201 202 auto export_worksheet = [&]() -> ErrorOr<void> { 203 JsonArray array; 204 for (auto& sheet : workbook.sheets()) 205 array.append(sheet->to_json()); 206 207 auto file_content = array.to_deprecated_string(); 208 return file.write_until_depleted(file_content.bytes()); 209 }; 210 211 if (mime == "text/csv") { 212 return export_xsv(); 213 } else if (mime == "application/x-sheets+json") { 214 return export_worksheet(); 215 } else { 216 auto page = GUI::WizardPage::construct( 217 "Export File Format", 218 DeprecatedString::formatted("Select the format you wish to export to '{}' as", LexicalPath::basename(filename))); 219 220 page->on_next_page = [] { return nullptr; }; 221 222 TRY(page->body_widget().load_from_gml(select_format_page_gml)); 223 auto format_combo_box = page->body_widget().find_descendant_of_type_named<GUI::ComboBox>("select_format_page_format_combo_box"); 224 225 Vector<DeprecatedString> supported_formats { 226 "CSV (text/csv)", 227 "Spreadsheet Worksheet", 228 }; 229 format_combo_box->set_model(GUI::ItemListModel<DeprecatedString>::create(supported_formats)); 230 231 wizard->push_page(page); 232 233 if (wizard->exec() != GUI::Dialog::ExecResult::OK) 234 return Error::from_string_literal("Export was cancelled"); 235 236 if (format_combo_box->selected_index() == 0) 237 return export_xsv(); 238 239 if (format_combo_box->selected_index() == 1) 240 return export_worksheet(); 241 242 VERIFY_NOT_REACHED(); 243 } 244} 245 246};