Serenity Operating System
at master 501 lines 21 kB view raw
1/* 2 * Copyright (c) 2020-2022, the SerenityOS developers. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "SpreadsheetView.h" 8#include "CellTypeDialog.h" 9#include <AK/ScopeGuard.h> 10#include <AK/URL.h> 11#include <LibCore/MimeData.h> 12#include <LibGUI/BoxLayout.h> 13#include <LibGUI/HeaderView.h> 14#include <LibGUI/Menu.h> 15#include <LibGUI/ModelEditingDelegate.h> 16#include <LibGUI/Painter.h> 17#include <LibGUI/Scrollbar.h> 18#include <LibGUI/TableView.h> 19#include <LibGfx/Palette.h> 20 21namespace Spreadsheet { 22 23void SpreadsheetView::EditingDelegate::set_value(GUI::Variant const& value, GUI::ModelEditingDelegate::SelectionBehavior selection_behavior) 24{ 25 if (value.as_string().is_null()) { 26 StringModelEditingDelegate::set_value("", selection_behavior); 27 commit(); 28 return; 29 } 30 31 if (m_has_set_initial_value) 32 return StringModelEditingDelegate::set_value(value, selection_behavior); 33 34 m_has_set_initial_value = true; 35 auto const option = m_sheet.at({ (size_t)index().column(), (size_t)index().row() }); 36 if (option) 37 return StringModelEditingDelegate::set_value(option->source(), selection_behavior); 38 39 StringModelEditingDelegate::set_value("", selection_behavior); 40} 41 42void InfinitelyScrollableTableView::did_scroll() 43{ 44 TableView::did_scroll(); 45 auto& vscrollbar = vertical_scrollbar(); 46 auto& hscrollbar = horizontal_scrollbar(); 47 if (!m_vertical_scroll_end_timer->is_active()) { 48 if (vscrollbar.is_visible() && vscrollbar.value() == vscrollbar.max()) { 49 m_vertical_scroll_end_timer->on_timeout = [&] { 50 if (on_reaching_vertical_end) 51 on_reaching_vertical_end(); 52 m_vertical_scroll_end_timer->stop(); 53 }; 54 m_vertical_scroll_end_timer->start(50); 55 } 56 } 57 if (!m_horizontal_scroll_end_timer->is_active()) { 58 if (hscrollbar.is_visible() && hscrollbar.value() == hscrollbar.max()) { 59 m_horizontal_scroll_end_timer->on_timeout = [&] { 60 if (on_reaching_horizontal_end) 61 on_reaching_horizontal_end(); 62 m_horizontal_scroll_end_timer->stop(); 63 }; 64 m_horizontal_scroll_end_timer->start(50); 65 } 66 } 67} 68 69void InfinitelyScrollableTableView::mousemove_event(GUI::MouseEvent& event) 70{ 71 if (auto model = this->model()) { 72 auto index = index_at_event_position(event.position()); 73 if (!index.is_valid()) 74 return TableView::mousemove_event(event); 75 76 auto& sheet = static_cast<SheetModel&>(*model).sheet(); 77 sheet.disable_updates(); 78 ScopeGuard sheet_update_enabler { [&] { sheet.enable_updates(); } }; 79 80 if (!is_dragging()) { 81 auto tooltip = model->data(index, static_cast<GUI::ModelRole>(SheetModel::Role::Tooltip)); 82 if (tooltip.is_string()) { 83 set_tooltip(tooltip.as_string()); 84 show_or_hide_tooltip(); 85 } else { 86 set_tooltip({}); 87 } 88 } 89 90 m_is_hovering_cut_zone = false; 91 m_is_hovering_extend_zone = false; 92 if (selection().size() > 0 && !m_is_dragging_for_select) { 93 // Get top-left and bottom-right most cells of selection 94 auto bottom_right_most_index = selection().first(); 95 auto top_left_most_index = selection().first(); 96 selection().for_each_index([&](auto& index) { 97 if (index.row() > bottom_right_most_index.row()) 98 bottom_right_most_index = index; 99 else if (index.column() > bottom_right_most_index.column()) 100 bottom_right_most_index = index; 101 if (index.row() < top_left_most_index.row()) 102 top_left_most_index = index; 103 else if (index.column() < top_left_most_index.column()) 104 top_left_most_index = index; 105 }); 106 107 auto top_left_rect = content_rect_minus_scrollbars(top_left_most_index); 108 auto bottom_right_rect = content_rect_minus_scrollbars(bottom_right_most_index); 109 auto distance_tl = top_left_rect.center() - event.position(); 110 auto distance_br = bottom_right_rect.center() - event.position(); 111 auto is_over_top_line = false; 112 auto is_over_bottom_line = false; 113 auto is_over_left_line = false; 114 auto is_over_right_line = false; 115 116 // If cursor is within the bounds of the selection 117 auto select_padding = 2; 118 if ((distance_br.y() >= -(bottom_right_rect.height() / 2 + select_padding)) && (distance_tl.y() <= (top_left_rect.height() / 2 + select_padding)) && (distance_br.x() >= -(bottom_right_rect.width() / 2 + select_padding)) && (distance_tl.x() <= (top_left_rect.width() / 2 + select_padding))) { 119 if (distance_tl.y() >= (top_left_rect.height() / 2 - select_padding)) 120 is_over_top_line = true; 121 else if (distance_br.y() <= -(bottom_right_rect.height() / 2 - select_padding)) 122 is_over_bottom_line = true; 123 124 if (distance_tl.x() >= (top_left_rect.width() / 2 - select_padding)) 125 is_over_left_line = true; 126 else if (distance_br.x() <= -(bottom_right_rect.width() / 2 - select_padding)) 127 is_over_right_line = true; 128 } 129 130 if (is_over_bottom_line && is_over_right_line) { 131 m_target_cell = bottom_right_most_index; 132 m_is_hovering_extend_zone = true; 133 } else if (is_over_top_line || is_over_bottom_line || is_over_left_line || is_over_right_line) { 134 m_target_cell = top_left_most_index; 135 m_is_hovering_cut_zone = true; 136 } 137 } 138 139 if (m_is_hovering_cut_zone || m_is_dragging_for_cut) 140 set_override_cursor(Gfx::StandardCursor::Drag); 141 else if (m_is_hovering_extend_zone || m_is_dragging_for_extend) 142 set_override_cursor(Gfx::StandardCursor::Crosshair); 143 else 144 set_override_cursor(Gfx::StandardCursor::Arrow); 145 146 auto holding_left_button = !!(event.buttons() & GUI::MouseButton::Primary); 147 if (m_is_dragging_for_cut) { 148 m_is_dragging_for_select = false; 149 if (holding_left_button) { 150 m_has_committed_to_cutting = true; 151 } 152 } else if (!m_is_dragging_for_select) { 153 if (!holding_left_button) { 154 m_starting_selection_index = index; 155 } else { 156 m_is_dragging_for_select = true; 157 m_might_drag = false; 158 } 159 } 160 161 if (!m_has_committed_to_extending) { 162 if (m_is_dragging_for_extend && holding_left_button) { 163 m_has_committed_to_extending = true; 164 m_starting_selection_index = m_target_cell; 165 } 166 } 167 168 if (holding_left_button && m_is_dragging_for_select && !m_has_committed_to_cutting) { 169 if (!m_starting_selection_index.is_valid()) 170 m_starting_selection_index = index; 171 172 Vector<GUI::ModelIndex> new_selection; 173 for (auto i = min(m_starting_selection_index.row(), index.row()), imax = max(m_starting_selection_index.row(), index.row()); i <= imax; ++i) { 174 for (auto j = min(m_starting_selection_index.column(), index.column()), jmax = max(m_starting_selection_index.column(), index.column()); j <= jmax; ++j) { 175 auto index = model->index(i, j); 176 if (index.is_valid()) 177 new_selection.append(move(index)); 178 } 179 } 180 181 if (!event.ctrl()) 182 selection().clear(); 183 184 // Since the extend function has similarities to the select, then do 185 // a check within the selection process to see if extending. 186 if (m_has_committed_to_extending) { 187 if (index.row() == m_target_cell.row() || index.column() == m_target_cell.column()) 188 selection().add_all(new_selection); 189 else 190 // Prevent the target cell from being unselected in the cases 191 // when extending in a direction that is not in the same column or 192 // row as the same. (Extension can only be done linearly, not diagonally) 193 selection().add(m_target_cell); 194 } else { 195 selection().add_all(new_selection); 196 } 197 } 198 } 199 200 TableView::mousemove_event(event); 201} 202 203void InfinitelyScrollableTableView::mousedown_event(GUI::MouseEvent& event) 204{ 205 // Override the mouse event so that the cell that is 'clicked' is not 206 // the one right beneath the cursor but instead the one that is referred to 207 // when m_is_hovering_cut_zone as it can be the case that the user is targeting 208 // a cell yet be outside of its bounding box due to the select_padding. 209 if (m_is_hovering_cut_zone || m_is_hovering_extend_zone) { 210 if (m_is_hovering_cut_zone) 211 m_is_dragging_for_cut = true; 212 else if (m_is_hovering_extend_zone) 213 m_is_dragging_for_extend = true; 214 auto rect = content_rect_minus_scrollbars(m_target_cell); 215 GUI::MouseEvent adjusted_event = { (GUI::Event::Type)event.type(), rect.center(), event.buttons(), event.button(), event.modifiers(), event.wheel_delta_x(), event.wheel_delta_y(), event.wheel_raw_delta_x(), event.wheel_raw_delta_y() }; 216 AbstractTableView::mousedown_event(adjusted_event); 217 } else { 218 AbstractTableView::mousedown_event(event); 219 if (event.button() == GUI::Primary) { 220 auto index = index_at_event_position(event.position()); 221 AbstractTableView::set_cursor(index, SelectionUpdate::Set); 222 } 223 } 224} 225 226void InfinitelyScrollableTableView::mouseup_event(GUI::MouseEvent& event) 227{ 228 // If done extending 229 if (m_has_committed_to_extending) { 230 Vector<Position> from; 231 Position position { (size_t)m_target_cell.column(), (size_t)m_target_cell.row() }; 232 from.append(position); 233 Vector<CellChange> cell_changes; 234 selection().for_each_index([&](auto& index) { 235 if (index == m_starting_selection_index) 236 return; 237 auto& sheet = static_cast<SheetModel&>(*this->model()).sheet(); 238 Vector<Position> to; 239 Position position { (size_t)index.column(), (size_t)index.row() }; 240 to.append(position); 241 auto cell_change = sheet.copy_cells(from, to); 242 cell_changes.extend(cell_change); 243 }); 244 if (static_cast<SheetModel&>(*this->model()).on_cells_data_change) 245 static_cast<SheetModel&>(*this->model()).on_cells_data_change(cell_changes); 246 update(); 247 } 248 249 m_is_dragging_for_select = false; 250 m_is_dragging_for_cut = false; 251 m_is_dragging_for_extend = false; 252 m_has_committed_to_cutting = false; 253 m_has_committed_to_extending = false; 254 if (m_is_hovering_cut_zone || m_is_hovering_extend_zone) { 255 auto rect = content_rect_minus_scrollbars(m_target_cell); 256 GUI::MouseEvent adjusted_event = { (GUI::Event::Type)event.type(), rect.center(), event.buttons(), event.button(), event.modifiers(), event.wheel_delta_x(), event.wheel_delta_y(), event.wheel_raw_delta_x(), event.wheel_raw_delta_y() }; 257 TableView::mouseup_event(adjusted_event); 258 } else { 259 TableView::mouseup_event(event); 260 } 261} 262 263void InfinitelyScrollableTableView::drop_event(GUI::DropEvent& event) 264{ 265 m_is_dragging_for_cut = false; 266 set_override_cursor(Gfx::StandardCursor::Arrow); 267 if (!index_at_event_position(event.position()).is_valid()) 268 return; 269 270 TableView::drop_event(event); 271 auto drop_index = index_at_event_position(event.position()); 272 if (selection().size() > 0) { 273 // Get top left index position of previous selection 274 auto top_left_most_index = selection().first(); 275 selection().for_each_index([&](auto& index) { 276 if (index.row() < top_left_most_index.row()) 277 top_left_most_index = index; 278 else if (index.column() < top_left_most_index.column()) 279 top_left_most_index = index; 280 }); 281 282 // Compare with drag location 283 auto x_diff = drop_index.column() - top_left_most_index.column(); 284 auto y_diff = drop_index.row() - top_left_most_index.row(); 285 286 // Set new selection 287 Vector<GUI::ModelIndex> new_selection; 288 for (auto& index : selection().indices()) { 289 auto new_index = model()->index(index.row() + y_diff, index.column() + x_diff); 290 new_selection.append(move(new_index)); 291 } 292 selection().clear(); 293 AbstractTableView::set_cursor(drop_index, SelectionUpdate::Set); 294 selection().add_all(new_selection); 295 } 296} 297 298void SpreadsheetView::update_with_model() 299{ 300 m_sheet_model->update(); 301 m_table_view->update(); 302} 303 304SpreadsheetView::SpreadsheetView(Sheet& sheet) 305 : m_sheet(sheet) 306 , m_sheet_model(SheetModel::create(*m_sheet)) 307{ 308 set_layout<GUI::VerticalBoxLayout>(2); 309 m_table_view = add<InfinitelyScrollableTableView>(); 310 m_table_view->set_grid_style(GUI::TableView::GridStyle::Both); 311 m_table_view->set_selection_behavior(GUI::AbstractView::SelectionBehavior::SelectItems); 312 m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed | GUI::AbstractView::AnyKeyPressed | GUI::AbstractView::DoubleClicked); 313 m_table_view->set_tab_key_navigation_enabled(true); 314 m_table_view->row_header().set_visible(true); 315 m_table_view->set_model(m_sheet_model); 316 m_table_view->on_reaching_vertical_end = [&]() { 317 for (size_t i = 0; i < 100; ++i) { 318 auto index = m_sheet->add_row(); 319 m_table_view->set_column_painting_delegate(index, make<TableCellPainter>(*m_table_view)); 320 }; 321 update_with_model(); 322 }; 323 m_table_view->on_reaching_horizontal_end = [&]() { 324 for (size_t i = 0; i < 10; ++i) { 325 m_sheet->add_column(); 326 auto last_column_index = m_sheet->column_count() - 1; 327 m_table_view->set_column_width(last_column_index, 50); 328 m_table_view->set_default_column_width(last_column_index, 50); 329 m_table_view->set_column_header_alignment(last_column_index, Gfx::TextAlignment::Center); 330 m_table_view->set_column_painting_delegate(last_column_index, make<TableCellPainter>(*m_table_view)); 331 } 332 update_with_model(); 333 }; 334 335 set_focus_proxy(m_table_view); 336 337 // FIXME: This is dumb. 338 for (size_t i = 0; i < m_sheet->column_count(); ++i) { 339 m_table_view->set_column_painting_delegate(i, make<TableCellPainter>(*m_table_view)); 340 m_table_view->set_column_width(i, 50); 341 m_table_view->set_default_column_width(i, 50); 342 m_table_view->set_column_header_alignment(i, Gfx::TextAlignment::Center); 343 } 344 345 m_table_view->set_alternating_row_colors(false); 346 m_table_view->set_highlight_selected_rows(false); 347 m_table_view->set_editable(true); 348 m_table_view->aid_create_editing_delegate = [this](auto&) { 349 auto delegate = make<EditingDelegate>(*m_sheet); 350 delegate->on_cursor_key_pressed = [this](auto& event) { 351 m_table_view->stop_editing(); 352 m_table_view->dispatch_event(event); 353 }; 354 delegate->on_cell_focusout = [this](auto& index, auto& value) { 355 m_table_view->model()->set_data(index, value); 356 }; 357 return delegate; 358 }; 359 360 m_table_view->on_selection_change = [&] { 361 m_sheet->selected_cells().clear(); 362 for (auto& index : m_table_view->selection().indices()) { 363 Position position { (size_t)index.column(), (size_t)index.row() }; 364 m_sheet->selected_cells().set(position); 365 } 366 367 if (m_table_view->selection().is_empty() && on_selection_dropped) 368 return on_selection_dropped(); 369 370 Vector<Position> selected_positions; 371 selected_positions.ensure_capacity(m_table_view->selection().size()); 372 for (auto& selection : m_table_view->selection().indices()) 373 selected_positions.empend((size_t)selection.column(), (size_t)selection.row()); 374 375 if (on_selection_changed) { 376 on_selection_changed(move(selected_positions)); 377 update_with_model(); 378 }; 379 }; 380 381 m_table_view->on_activation = [this](auto&) { 382 m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set); 383 }; 384 385 m_table_view->on_context_menu_request = [&](const GUI::ModelIndex&, const GUI::ContextMenuEvent& event) { 386 // NOTE: We ignore the specific cell for now. 387 m_cell_range_context_menu->popup(event.screen_position()); 388 }; 389 390 m_cell_range_context_menu = GUI::Menu::construct(); 391 m_cell_range_context_menu->add_action(GUI::Action::create("Type and Formatting...", [this](auto&) { 392 Vector<Position> positions; 393 for (auto& index : m_table_view->selection().indices()) { 394 Position position { (size_t)index.column(), (size_t)index.row() }; 395 positions.append(move(position)); 396 } 397 398 if (positions.is_empty()) { 399 auto& index = m_table_view->cursor_index(); 400 Position position { (size_t)index.column(), (size_t)index.row() }; 401 positions.append(move(position)); 402 } 403 404 auto dialog = CellTypeDialog::construct(positions, *m_sheet, window()); 405 if (dialog->exec() == GUI::Dialog::ExecResult::OK) { 406 for (auto& position : positions) { 407 auto& cell = m_sheet->ensure(position); 408 cell.set_type(dialog->type()); 409 cell.set_type_metadata(dialog->metadata()); 410 cell.set_conditional_formats(dialog->conditional_formats()); 411 } 412 413 m_table_view->update(); 414 } 415 })); 416 417 m_table_view->on_drop = [&](const GUI::ModelIndex& index, const GUI::DropEvent& event) { 418 if (!index.is_valid()) 419 return; 420 421 ScopeGuard update_after_drop { [this] { update(); } }; 422 423 if (event.mime_data().has_format("text/x-spreadsheet-data")) { 424 auto const& data = event.mime_data().data("text/x-spreadsheet-data"); 425 StringView urls { data.data(), data.size() }; 426 Vector<Position> source_positions, target_positions; 427 428 for (auto& line : urls.lines(false)) { 429 auto position = m_sheet->position_from_url(line); 430 if (position.has_value()) 431 source_positions.append(position.release_value()); 432 } 433 434 // Drop always has a single target. 435 Position target { (size_t)index.column(), (size_t)index.row() }; 436 target_positions.append(move(target)); 437 438 if (source_positions.is_empty()) 439 return; 440 441 auto first_position = source_positions.take_first(); 442 auto cell_changes = m_sheet->copy_cells(move(source_positions), move(target_positions), first_position, Spreadsheet::Sheet::CopyOperation::Cut); 443 if (model()->on_cells_data_change) 444 model()->on_cells_data_change(cell_changes); 445 446 return; 447 } 448 449 if (event.mime_data().has_text()) { 450 auto& target_cell = m_sheet->ensure({ (size_t)index.column(), (size_t)index.row() }); 451 target_cell.set_data(event.text()); 452 return; 453 } 454 }; 455} 456 457void SpreadsheetView::hide_event(GUI::HideEvent&) 458{ 459 if (on_selection_dropped) 460 on_selection_dropped(); 461} 462 463void SpreadsheetView::show_event(GUI::ShowEvent&) 464{ 465 if (on_selection_changed && !m_table_view->selection().is_empty()) { 466 Vector<Position> selected_positions; 467 selected_positions.ensure_capacity(m_table_view->selection().size()); 468 for (auto& selection : m_table_view->selection().indices()) 469 selected_positions.empend((size_t)selection.column(), (size_t)selection.row()); 470 471 on_selection_changed(move(selected_positions)); 472 } 473} 474 475void SpreadsheetView::move_cursor(GUI::AbstractView::CursorMovement direction) 476{ 477 m_table_view->move_cursor(direction, GUI::AbstractView::SelectionUpdate::Set); 478} 479 480void SpreadsheetView::TableCellPainter::paint(GUI::Painter& painter, Gfx::IntRect const& rect, Gfx::Palette const& palette, const GUI::ModelIndex& index) 481{ 482 // Draw a border. 483 // Undo the horizontal padding done by the table view... 484 auto cell_rect = rect.inflated(m_table_view.horizontal_padding() * 2, 0); 485 486 if (auto bg = index.data(GUI::ModelRole::BackgroundColor); bg.is_color()) 487 painter.fill_rect(cell_rect, bg.as_color()); 488 489 if (m_table_view.selection().contains(index)) { 490 Color fill_color = palette.selection(); 491 fill_color.set_alpha(80); 492 painter.fill_rect(cell_rect, fill_color); 493 } 494 495 auto text_color = index.data(GUI::ModelRole::ForegroundColor).to_color(palette.color(m_table_view.foreground_role())); 496 auto data = index.data(); 497 auto text_alignment = index.data(GUI::ModelRole::TextAlignment).to_text_alignment(Gfx::TextAlignment::CenterRight); 498 painter.draw_text(rect, data.to_deprecated_string(), m_table_view.font_for_index(index), text_alignment, text_color, Gfx::TextElision::Right); 499} 500 501}