SpreadsheetWidget.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. /*
  2. * Copyright (c) 2020-2022, the SerenityOS developers.
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "SpreadsheetWidget.h"
  7. #include "CellSyntaxHighlighter.h"
  8. #include "HelpWindow.h"
  9. #include <LibFileSystemAccessClient/Client.h>
  10. #include <LibGUI/Application.h>
  11. #include <LibGUI/BoxLayout.h>
  12. #include <LibGUI/Button.h>
  13. #include <LibGUI/ColorPicker.h>
  14. #include <LibGUI/EmojiInputDialog.h>
  15. #include <LibGUI/InputBox.h>
  16. #include <LibGUI/Label.h>
  17. #include <LibGUI/Menu.h>
  18. #include <LibGUI/MessageBox.h>
  19. #include <LibGUI/Splitter.h>
  20. #include <LibGUI/TabWidget.h>
  21. #include <LibGUI/TextEditor.h>
  22. #include <LibGUI/Toolbar.h>
  23. #include <LibGUI/ToolbarContainer.h>
  24. #include <LibGfx/Font/FontDatabase.h>
  25. #include <string.h>
  26. namespace Spreadsheet {
  27. SpreadsheetWidget::SpreadsheetWidget(GUI::Window& parent_window, NonnullRefPtrVector<Sheet>&& sheets, bool should_add_sheet_if_empty)
  28. : m_workbook(make<Workbook>(move(sheets), parent_window))
  29. {
  30. set_fill_with_background_color(true);
  31. set_layout<GUI::VerticalBoxLayout>(2);
  32. auto& toolbar_container = add<GUI::ToolbarContainer>();
  33. auto& toolbar = toolbar_container.add<GUI::Toolbar>();
  34. auto& container = add<GUI::VerticalSplitter>();
  35. auto& top_bar = container.add<GUI::Frame>();
  36. top_bar.set_layout<GUI::HorizontalBoxLayout>(GUI::Margins {}, 1);
  37. top_bar.set_preferred_height(26);
  38. auto& current_cell_label = top_bar.add<GUI::Label>("");
  39. current_cell_label.set_fixed_width(50);
  40. auto& help_button = top_bar.add<GUI::Button>();
  41. help_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-help.png"sv).release_value_but_fixme_should_propagate_errors());
  42. help_button.set_tooltip("Functions Help");
  43. help_button.set_fixed_size(20, 20);
  44. help_button.on_click = [&](auto) {
  45. if (!current_view()) {
  46. GUI::MessageBox::show_error(window(), "Can only show function documentation/help when a worksheet exists and is open"sv);
  47. } else if (auto* sheet_ptr = current_worksheet_if_available()) {
  48. auto docs = sheet_ptr->gather_documentation();
  49. auto help_window = HelpWindow::the(window());
  50. help_window->set_docs(move(docs));
  51. help_window->set_window_mode(GUI::WindowMode::Modeless);
  52. help_window->show();
  53. }
  54. };
  55. auto& cell_value_editor = top_bar.add<GUI::TextEditor>(GUI::TextEditor::Type::SingleLine);
  56. cell_value_editor.set_font(Gfx::FontDatabase::default_fixed_width_font());
  57. cell_value_editor.set_scrollbars_enabled(false);
  58. cell_value_editor.on_return_pressed = [this]() {
  59. current_view()->move_cursor(GUI::AbstractView::CursorMovement::Down);
  60. };
  61. cell_value_editor.set_syntax_highlighter(make<CellSyntaxHighlighter>());
  62. cell_value_editor.set_enabled(false);
  63. current_cell_label.set_enabled(false);
  64. m_tab_widget = container.add<GUI::TabWidget>();
  65. m_tab_widget->set_tab_position(GUI::TabWidget::TabPosition::Bottom);
  66. m_cell_value_editor = cell_value_editor;
  67. m_current_cell_label = current_cell_label;
  68. m_inline_documentation_window = GUI::Window::construct(window());
  69. m_inline_documentation_window->set_rect(m_cell_value_editor->rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
  70. m_inline_documentation_window->set_window_type(GUI::WindowType::Tooltip);
  71. m_inline_documentation_window->set_resizable(false);
  72. auto inline_widget = m_inline_documentation_window->set_main_widget<GUI::Frame>().release_value_but_fixme_should_propagate_errors();
  73. inline_widget->set_fill_with_background_color(true);
  74. inline_widget->set_layout<GUI::VerticalBoxLayout>(4);
  75. inline_widget->set_frame_shape(Gfx::FrameShape::Box);
  76. m_inline_documentation_label = inline_widget->add<GUI::Label>();
  77. m_inline_documentation_label->set_fill_with_background_color(true);
  78. m_inline_documentation_label->set_autosize(false);
  79. m_inline_documentation_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
  80. if (!m_workbook->has_sheets() && should_add_sheet_if_empty)
  81. m_workbook->add_sheet("Sheet 1"sv);
  82. m_tab_context_menu = GUI::Menu::construct();
  83. m_rename_action = GUI::CommonActions::make_rename_action([this](auto&) {
  84. VERIFY(m_tab_context_menu_sheet_view);
  85. auto* sheet_ptr = m_tab_context_menu_sheet_view->sheet_if_available();
  86. VERIFY(sheet_ptr); // How did we get here without a sheet?
  87. auto& sheet = *sheet_ptr;
  88. DeprecatedString new_name;
  89. if (GUI::InputBox::show(window(), new_name, DeprecatedString::formatted("New name for '{}'", sheet.name()), "Rename sheet"sv) == GUI::Dialog::ExecResult::OK) {
  90. sheet.set_name(new_name);
  91. sheet.update();
  92. m_tab_widget->set_tab_title(static_cast<GUI::Widget&>(*m_tab_context_menu_sheet_view), new_name);
  93. }
  94. });
  95. m_tab_context_menu->add_action(*m_rename_action);
  96. m_tab_context_menu->add_action(GUI::Action::create("Add new sheet...", Gfx::Bitmap::load_from_file("/res/icons/16x16/new-tab.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) {
  97. DeprecatedString name;
  98. if (GUI::InputBox::show(window(), name, "Name for new sheet"sv, "Create sheet"sv) == GUI::Dialog::ExecResult::OK) {
  99. NonnullRefPtrVector<Sheet> new_sheets;
  100. new_sheets.append(m_workbook->add_sheet(name));
  101. setup_tabs(move(new_sheets));
  102. }
  103. }));
  104. setup_tabs(m_workbook->sheets());
  105. m_new_action = GUI::Action::create("Add New Sheet", Gfx::Bitmap::load_from_file("/res/icons/16x16/new-tab.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  106. add_sheet();
  107. });
  108. m_open_action = GUI::CommonActions::make_open_action([&](auto&) {
  109. if (!request_close())
  110. return;
  111. auto response = FileSystemAccessClient::Client::the().open_file(window());
  112. if (response.is_error())
  113. return;
  114. load_file(response.value().filename(), response.value().stream());
  115. });
  116. m_import_action = GUI::Action::create("Import sheets...", [&](auto&) {
  117. auto response = FileSystemAccessClient::Client::the().open_file(window());
  118. if (response.is_error())
  119. return;
  120. import_sheets(response.value().filename(), response.value().stream());
  121. });
  122. m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
  123. if (current_filename().is_empty()) {
  124. m_save_as_action->activate();
  125. return;
  126. }
  127. auto response = FileSystemAccessClient::Client::the().request_file(window(), current_filename(), Core::File::OpenMode::Write);
  128. if (response.is_error())
  129. return;
  130. save(response.value().filename(), response.value().stream());
  131. });
  132. m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
  133. DeprecatedString name = "workbook";
  134. auto response = FileSystemAccessClient::Client::the().save_file(window(), name, "sheets");
  135. if (response.is_error())
  136. return;
  137. save(response.value().filename(), response.value().stream());
  138. update_window_title();
  139. });
  140. m_quit_action = GUI::CommonActions::make_quit_action([&](auto&) {
  141. if (!request_close())
  142. return;
  143. GUI::Application::the()->quit(0);
  144. });
  145. m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) { clipboard_action(true); }, window());
  146. m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) { clipboard_action(false); }, window());
  147. m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) {
  148. ScopeGuard update_after_paste { [&] { update(); } };
  149. auto* worksheet_ptr = current_worksheet_if_available();
  150. if (!worksheet_ptr) {
  151. GUI::MessageBox::show_error(window(), "There are no active worksheets"sv);
  152. return;
  153. }
  154. auto& sheet = *worksheet_ptr;
  155. auto& cells = sheet.selected_cells();
  156. VERIFY(!cells.is_empty());
  157. const auto& data = GUI::Clipboard::the().fetch_data_and_type();
  158. if (auto spreadsheet_data = data.metadata.get("text/x-spreadsheet-data"); spreadsheet_data.has_value()) {
  159. Vector<Spreadsheet::Position> source_positions, target_positions;
  160. auto lines = spreadsheet_data.value().split_view('\n');
  161. auto action = lines.take_first();
  162. for (auto& line : lines) {
  163. dbgln("Paste line '{}'", line);
  164. auto position = sheet.position_from_url(line);
  165. if (position.has_value())
  166. source_positions.append(position.release_value());
  167. }
  168. for (auto& position : sheet.selected_cells())
  169. target_positions.append(position);
  170. if (source_positions.is_empty())
  171. return;
  172. auto first_position = source_positions.take_first();
  173. auto cell_changes = sheet.copy_cells(move(source_positions), move(target_positions), first_position, action == "cut" ? Spreadsheet::Sheet::CopyOperation::Cut : Spreadsheet::Sheet::CopyOperation::Copy);
  174. undo_stack().push(make<CellsUndoCommand>(cell_changes));
  175. } else {
  176. for (auto& cell : sheet.selected_cells())
  177. sheet.ensure(cell).set_data(StringView { data.data.data(), data.data.size() });
  178. update();
  179. }
  180. },
  181. window());
  182. m_insert_emoji_action = GUI::CommonActions::make_insert_emoji_action([&](auto&) {
  183. auto emoji_input_dialog = GUI::EmojiInputDialog::construct(window());
  184. if (emoji_input_dialog->exec() != GUI::EmojiInputDialog::ExecResult::OK)
  185. return;
  186. auto emoji_code_point = emoji_input_dialog->selected_emoji_text();
  187. if (m_cell_value_editor->has_focus_within()) {
  188. m_cell_value_editor->insert_at_cursor_or_replace_selection(emoji_code_point);
  189. }
  190. auto* worksheet_ptr = current_worksheet_if_available();
  191. if (!worksheet_ptr) {
  192. GUI::MessageBox::show_error(window(), "There are no active worksheets"sv);
  193. return;
  194. }
  195. auto& sheet = *worksheet_ptr;
  196. for (auto& cell : sheet.selected_cells())
  197. sheet.ensure(cell).set_data(emoji_code_point);
  198. update();
  199. },
  200. window());
  201. m_undo_action = GUI::CommonActions::make_undo_action([&](auto&) {
  202. undo();
  203. });
  204. m_redo_action = GUI::CommonActions::make_redo_action([&](auto&) {
  205. redo();
  206. });
  207. m_undo_stack.on_state_change = [this] {
  208. m_undo_action->set_enabled(m_undo_stack.can_undo());
  209. m_redo_action->set_enabled(m_undo_stack.can_redo());
  210. };
  211. m_undo_action->set_enabled(false);
  212. m_redo_action->set_enabled(false);
  213. m_change_background_color_action = GUI::Action::create(
  214. "&Change Background Color", { Mod_Ctrl, Key_B }, Gfx::Bitmap::load_from_file("/res/icons/pixelpaint/bucket.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  215. change_cell_static_color_format(Spreadsheet::FormatType::Background);
  216. },
  217. window());
  218. m_change_foreground_color_action = GUI::Action::create(
  219. "&Change Foreground Color", { Mod_Ctrl, Key_T }, Gfx::Bitmap::load_from_file("/res/icons/16x16/text-color.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  220. change_cell_static_color_format(Spreadsheet::FormatType::Foreground);
  221. },
  222. window());
  223. m_change_background_color_action->set_enabled(false);
  224. m_change_foreground_color_action->set_enabled(false);
  225. m_functions_help_action = GUI::Action::create(
  226. "&Functions Help", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-help.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  227. if (auto* worksheet_ptr = current_worksheet_if_available()) {
  228. auto docs = worksheet_ptr->gather_documentation();
  229. auto help_window = Spreadsheet::HelpWindow::the(window());
  230. help_window->set_docs(move(docs));
  231. help_window->show();
  232. } else {
  233. GUI::MessageBox::show_error(window(), "Cannot prepare documentation/help without an active worksheet"sv);
  234. }
  235. },
  236. window());
  237. m_search_action = GUI::CommonActions::make_command_palette_action(&parent_window);
  238. m_about_action = GUI::CommonActions::make_about_action("Spreadsheet", GUI::Icon::default_icon("app-spreadsheet"sv), &parent_window);
  239. toolbar.add_action(*m_new_action);
  240. toolbar.add_action(*m_open_action);
  241. toolbar.add_action(*m_save_action);
  242. toolbar.add_separator();
  243. toolbar.add_action(*m_cut_action);
  244. toolbar.add_action(*m_copy_action);
  245. toolbar.add_action(*m_paste_action);
  246. toolbar.add_action(*m_undo_action);
  247. toolbar.add_action(*m_redo_action);
  248. toolbar.add_separator();
  249. toolbar.add_action(*m_change_background_color_action);
  250. toolbar.add_action(*m_change_foreground_color_action);
  251. m_cut_action->set_enabled(false);
  252. m_copy_action->set_enabled(false);
  253. m_paste_action->set_enabled(false);
  254. m_insert_emoji_action->set_enabled(false);
  255. m_tab_widget->on_change = [this](auto& selected_widget) {
  256. // for keyboard shortcuts and command palette
  257. m_tab_context_menu_sheet_view = static_cast<SpreadsheetView&>(selected_widget);
  258. };
  259. m_tab_widget->on_context_menu_request = [&](auto& widget, auto& event) {
  260. m_tab_context_menu_sheet_view = static_cast<SpreadsheetView&>(widget);
  261. m_tab_context_menu->popup(event.screen_position());
  262. };
  263. m_tab_widget->on_double_click = [&](auto& widget) {
  264. m_tab_context_menu_sheet_view = static_cast<SpreadsheetView&>(widget);
  265. m_rename_action->activate();
  266. };
  267. }
  268. void SpreadsheetWidget::resize_event(GUI::ResizeEvent& event)
  269. {
  270. GUI::Widget::resize_event(event);
  271. if (m_inline_documentation_window && m_cell_value_editor && window())
  272. m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
  273. }
  274. void SpreadsheetWidget::clipboard_content_did_change(DeprecatedString const& mime_type)
  275. {
  276. if (auto* sheet = current_worksheet_if_available())
  277. m_paste_action->set_enabled(!sheet->selected_cells().is_empty() && mime_type.starts_with("text/"sv));
  278. }
  279. void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector<Sheet> new_sheets)
  280. {
  281. for (auto& sheet : new_sheets) {
  282. auto& new_view = m_tab_widget->add_tab<SpreadsheetView>(sheet.name(), sheet);
  283. new_view.model()->on_cell_data_change = [&](auto& cell, auto& previous_data) {
  284. undo_stack().push(make<CellsUndoCommand>(cell, previous_data));
  285. window()->set_modified(true);
  286. };
  287. new_view.model()->on_cells_data_change = [&](Vector<CellChange> cell_changes) {
  288. undo_stack().push(make<CellsUndoCommand>(cell_changes));
  289. window()->set_modified(true);
  290. };
  291. new_view.on_selection_changed = [&](Vector<Position>&& selection) {
  292. auto* sheet_ptr = current_worksheet_if_available();
  293. // How did this even happen?
  294. VERIFY(sheet_ptr);
  295. auto& sheet = *sheet_ptr;
  296. VERIFY(!selection.is_empty());
  297. m_cut_action->set_enabled(true);
  298. m_copy_action->set_enabled(true);
  299. m_paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type().starts_with("text/"sv));
  300. m_insert_emoji_action->set_enabled(true);
  301. m_current_cell_label->set_enabled(true);
  302. m_cell_value_editor->set_enabled(true);
  303. m_change_background_color_action->set_enabled(true);
  304. m_change_foreground_color_action->set_enabled(true);
  305. if (selection.size() == 1) {
  306. auto& position = selection.first();
  307. m_current_cell_label->set_text(position.to_cell_identifier(sheet));
  308. auto& cell = sheet.ensure(position);
  309. m_cell_value_editor->on_change = nullptr;
  310. m_cell_value_editor->set_text(cell.source());
  311. m_cell_value_editor->on_change = [&] {
  312. auto text = m_cell_value_editor->text();
  313. // FIXME: Lines?
  314. auto offset = m_cell_value_editor->cursor().column();
  315. try_generate_tip_for_input_expression(text, offset);
  316. cell.set_data(move(text));
  317. sheet.update();
  318. update();
  319. };
  320. static_cast<CellSyntaxHighlighter*>(const_cast<Syntax::Highlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(&cell);
  321. return;
  322. }
  323. // There are many cells selected, change all of them.
  324. StringBuilder builder;
  325. builder.appendff("<{}>", selection.size());
  326. m_current_cell_label->set_text(builder.string_view());
  327. Vector<Cell&> cells;
  328. for (auto& position : selection)
  329. cells.append(sheet.ensure(position));
  330. auto& first_cell = cells.first();
  331. m_cell_value_editor->on_change = nullptr;
  332. m_cell_value_editor->set_text(""sv);
  333. m_should_change_selected_cells = false;
  334. m_cell_value_editor->on_focusin = [this] { m_should_change_selected_cells = true; };
  335. m_cell_value_editor->on_focusout = [this] { m_should_change_selected_cells = false; };
  336. m_cell_value_editor->on_change = [cells = move(cells), this]() mutable {
  337. if (m_should_change_selected_cells) {
  338. auto* sheet_ptr = current_worksheet_if_available();
  339. if (!sheet_ptr)
  340. return;
  341. auto& sheet = *sheet_ptr;
  342. auto text = m_cell_value_editor->text();
  343. // FIXME: Lines?
  344. auto offset = m_cell_value_editor->cursor().column();
  345. try_generate_tip_for_input_expression(text, offset);
  346. for (auto& cell : cells)
  347. cell.set_data(text);
  348. sheet.update();
  349. update();
  350. }
  351. };
  352. static_cast<CellSyntaxHighlighter*>(const_cast<Syntax::Highlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(&first_cell);
  353. };
  354. new_view.on_selection_dropped = [&]() {
  355. m_current_cell_label->set_enabled(false);
  356. m_current_cell_label->set_text({});
  357. m_cell_value_editor->on_change = nullptr;
  358. m_cell_value_editor->on_focusin = nullptr;
  359. m_cell_value_editor->on_focusout = nullptr;
  360. m_cell_value_editor->set_text({});
  361. m_cell_value_editor->set_enabled(false);
  362. m_cut_action->set_enabled(false);
  363. m_copy_action->set_enabled(false);
  364. m_paste_action->set_enabled(false);
  365. m_insert_emoji_action->set_enabled(false);
  366. static_cast<CellSyntaxHighlighter*>(const_cast<Syntax::Highlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(nullptr);
  367. };
  368. }
  369. }
  370. void SpreadsheetWidget::try_generate_tip_for_input_expression(StringView source, size_t cursor_offset)
  371. {
  372. auto* sheet_ptr = current_view()->sheet_if_available();
  373. if (!sheet_ptr)
  374. return;
  375. auto& sheet = *sheet_ptr;
  376. m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
  377. if (!current_view() || !source.starts_with('=')) {
  378. m_inline_documentation_window->hide();
  379. return;
  380. }
  381. cursor_offset = min(cursor_offset, source.length());
  382. auto maybe_function_and_argument = get_function_and_argument_index(source.substring_view(0, cursor_offset));
  383. if (!maybe_function_and_argument.has_value()) {
  384. m_inline_documentation_window->hide();
  385. return;
  386. }
  387. auto& [name, index] = maybe_function_and_argument.value();
  388. auto text = sheet.generate_inline_documentation_for(name, index);
  389. if (text.is_empty()) {
  390. m_inline_documentation_window->hide();
  391. } else {
  392. m_inline_documentation_label->set_text(move(text));
  393. m_inline_documentation_window->show();
  394. }
  395. }
  396. void SpreadsheetWidget::undo()
  397. {
  398. if (!m_undo_stack.can_undo())
  399. return;
  400. m_undo_stack.undo();
  401. update();
  402. }
  403. void SpreadsheetWidget::redo()
  404. {
  405. if (!m_undo_stack.can_redo())
  406. return;
  407. m_undo_stack.redo();
  408. update();
  409. }
  410. void SpreadsheetWidget::change_cell_static_color_format(Spreadsheet::FormatType format_type)
  411. {
  412. VERIFY(current_worksheet_if_available());
  413. auto dialog = GUI::ColorPicker::construct(Color::White, window(), "Select Color");
  414. if (dialog->exec() == GUI::Dialog::ExecResult::OK) {
  415. for (auto& position : current_worksheet_if_available()->selected_cells()) {
  416. if (format_type == Spreadsheet::FormatType::Background)
  417. current_worksheet_if_available()->at(position)->type_metadata().static_format.background_color = dialog->color();
  418. else
  419. current_worksheet_if_available()->at(position)->type_metadata().static_format.foreground_color = dialog->color();
  420. }
  421. }
  422. }
  423. void SpreadsheetWidget::save(String const& filename, Core::File& file)
  424. {
  425. auto result = m_workbook->write_to_file(filename, file);
  426. if (result.is_error()) {
  427. GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Cannot save file: {}", result.error()));
  428. return;
  429. }
  430. undo_stack().set_current_unmodified();
  431. window()->set_modified(false);
  432. }
  433. void SpreadsheetWidget::load_file(String const& filename, Core::File& file)
  434. {
  435. auto result = m_workbook->open_file(filename, file);
  436. if (result.is_error()) {
  437. GUI::MessageBox::show_error(window(), result.error());
  438. return;
  439. }
  440. m_cell_value_editor->on_change = nullptr;
  441. m_current_cell_label->set_text("");
  442. m_should_change_selected_cells = false;
  443. while (auto* widget = m_tab_widget->active_widget()) {
  444. m_tab_widget->remove_tab(*widget);
  445. }
  446. setup_tabs(m_workbook->sheets());
  447. update_window_title();
  448. }
  449. void SpreadsheetWidget::import_sheets(String const& filename, Core::File& file)
  450. {
  451. auto result = m_workbook->import_file(filename, file);
  452. if (result.is_error()) {
  453. GUI::MessageBox::show_error(window(), result.error());
  454. return;
  455. }
  456. if (!result.value())
  457. return;
  458. window()->set_modified(true);
  459. m_cell_value_editor->on_change = nullptr;
  460. m_current_cell_label->set_text("");
  461. m_should_change_selected_cells = false;
  462. while (auto* widget = m_tab_widget->active_widget()) {
  463. m_tab_widget->remove_tab(*widget);
  464. }
  465. setup_tabs(m_workbook->sheets());
  466. update_window_title();
  467. }
  468. bool SpreadsheetWidget::request_close()
  469. {
  470. if (!undo_stack().is_current_modified())
  471. return true;
  472. auto result = GUI::MessageBox::ask_about_unsaved_changes(window(), current_filename());
  473. if (result == GUI::MessageBox::ExecResult::Yes) {
  474. m_save_action->activate();
  475. return !m_workbook->dirty();
  476. }
  477. if (result == GUI::MessageBox::ExecResult::No)
  478. return true;
  479. return false;
  480. }
  481. void SpreadsheetWidget::add_sheet()
  482. {
  483. StringBuilder name;
  484. name.append("Sheet"sv);
  485. name.appendff(" {}", m_workbook->sheets().size() + 1);
  486. NonnullRefPtrVector<Sheet> new_sheets;
  487. new_sheets.append(m_workbook->add_sheet(name.string_view()));
  488. setup_tabs(move(new_sheets));
  489. }
  490. void SpreadsheetWidget::add_sheet(NonnullRefPtr<Sheet>&& sheet)
  491. {
  492. VERIFY(m_workbook == &sheet->workbook());
  493. NonnullRefPtrVector<Sheet> new_sheets;
  494. new_sheets.append(move(sheet));
  495. m_workbook->sheets().extend(new_sheets);
  496. setup_tabs(new_sheets);
  497. }
  498. void SpreadsheetWidget::update_window_title()
  499. {
  500. StringBuilder builder;
  501. if (current_filename().is_empty())
  502. builder.append("Untitled"sv);
  503. else
  504. builder.append(current_filename());
  505. builder.append("[*] - Spreadsheet"sv);
  506. window()->set_title(builder.to_deprecated_string());
  507. }
  508. void SpreadsheetWidget::clipboard_action(bool is_cut)
  509. {
  510. /// text/x-spreadsheet-data:
  511. /// - action: copy/cut
  512. /// - currently selected cell
  513. /// - selected cell+
  514. auto* worksheet_ptr = current_worksheet_if_available();
  515. if (!worksheet_ptr) {
  516. GUI::MessageBox::show_error(window(), "There are no active worksheets"sv);
  517. return;
  518. }
  519. auto& worksheet = *worksheet_ptr;
  520. auto& cells = worksheet.selected_cells();
  521. VERIFY(!cells.is_empty());
  522. StringBuilder text_builder, url_builder;
  523. url_builder.append(is_cut ? "cut\n"sv : "copy\n"sv);
  524. bool first = true;
  525. auto cursor = current_selection_cursor();
  526. if (cursor) {
  527. Spreadsheet::Position position { (size_t)cursor->column(), (size_t)cursor->row() };
  528. url_builder.append(position.to_url(worksheet).to_deprecated_string());
  529. url_builder.append('\n');
  530. }
  531. for (auto& cell : cells) {
  532. if (first && !cursor) {
  533. url_builder.append(cell.to_url(worksheet).to_deprecated_string());
  534. url_builder.append('\n');
  535. }
  536. url_builder.append(cell.to_url(worksheet).to_deprecated_string());
  537. url_builder.append('\n');
  538. auto cell_data = worksheet.at(cell);
  539. if (!first)
  540. text_builder.append('\t');
  541. if (cell_data)
  542. text_builder.append(cell_data->data());
  543. first = false;
  544. }
  545. HashMap<DeprecatedString, DeprecatedString> metadata;
  546. metadata.set("text/x-spreadsheet-data", url_builder.to_deprecated_string());
  547. dbgln(url_builder.to_deprecated_string());
  548. GUI::Clipboard::the().set_data(text_builder.string_view().bytes(), "text/plain", move(metadata));
  549. }
  550. void SpreadsheetWidget::initialize_menubar(GUI::Window& window)
  551. {
  552. auto& file_menu = window.add_menu("&File");
  553. file_menu.add_action(*m_new_action);
  554. file_menu.add_action(*m_open_action);
  555. file_menu.add_action(*m_save_action);
  556. file_menu.add_action(*m_save_as_action);
  557. file_menu.add_separator();
  558. file_menu.add_action(*m_import_action);
  559. file_menu.add_separator();
  560. file_menu.add_action(*m_quit_action);
  561. auto& edit_menu = window.add_menu("&Edit");
  562. edit_menu.add_action(*m_undo_action);
  563. edit_menu.add_action(*m_redo_action);
  564. edit_menu.add_separator();
  565. edit_menu.add_action(*m_cut_action);
  566. edit_menu.add_action(*m_copy_action);
  567. edit_menu.add_action(*m_paste_action);
  568. edit_menu.add_action(*m_insert_emoji_action);
  569. auto& help_menu = window.add_menu("&Help");
  570. help_menu.add_action(*m_search_action);
  571. help_menu.add_action(*m_functions_help_action);
  572. help_menu.add_action(*m_about_action);
  573. }
  574. }