Serenity Operating System
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};