1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-30 21:48:11 +00:00
serenity/Userland/Applications/Spreadsheet/SpreadsheetView.cpp
martinfalisse 1287238430 Spreadsheet: Select the correct cell on click
Due to the margin that is given to be able to select cells for
cutting or extending, have to override the mouse click function
so that the targetted cell is chosen and not the one that may be
beneath the cursor.
2022-03-19 09:31:29 +03:30

403 lines
16 KiB
C++

/*
* Copyright (c) 2020-2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "SpreadsheetView.h"
#include "CellTypeDialog.h"
#include <AK/ScopeGuard.h>
#include <AK/URL.h>
#include <LibCore/MimeData.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/HeaderView.h>
#include <LibGUI/Menu.h>
#include <LibGUI/ModelEditingDelegate.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Scrollbar.h>
#include <LibGUI/TableView.h>
#include <LibGfx/Palette.h>
namespace Spreadsheet {
void SpreadsheetView::EditingDelegate::set_value(GUI::Variant const& value, GUI::ModelEditingDelegate::SelectionBehavior selection_behavior)
{
if (value.as_string().is_null()) {
StringModelEditingDelegate::set_value("", selection_behavior);
commit();
return;
}
if (m_has_set_initial_value)
return StringModelEditingDelegate::set_value(value, selection_behavior);
m_has_set_initial_value = true;
const auto option = m_sheet.at({ (size_t)index().column(), (size_t)index().row() });
if (option)
return StringModelEditingDelegate::set_value(option->source(), selection_behavior);
StringModelEditingDelegate::set_value("", selection_behavior);
}
void InfinitelyScrollableTableView::did_scroll()
{
TableView::did_scroll();
auto& vscrollbar = vertical_scrollbar();
auto& hscrollbar = horizontal_scrollbar();
if (!m_vertical_scroll_end_timer->is_active()) {
if (vscrollbar.is_visible() && vscrollbar.value() == vscrollbar.max()) {
m_vertical_scroll_end_timer->on_timeout = [&] {
if (on_reaching_vertical_end)
on_reaching_vertical_end();
m_vertical_scroll_end_timer->stop();
};
m_vertical_scroll_end_timer->start(50);
}
}
if (!m_horizontal_scroll_end_timer->is_active()) {
if (hscrollbar.is_visible() && hscrollbar.value() == hscrollbar.max()) {
m_horizontal_scroll_end_timer->on_timeout = [&] {
if (on_reaching_horizontal_end)
on_reaching_horizontal_end();
m_horizontal_scroll_end_timer->stop();
};
m_horizontal_scroll_end_timer->start(50);
}
}
}
void InfinitelyScrollableTableView::mousemove_event(GUI::MouseEvent& event)
{
if (auto model = this->model()) {
auto index = index_at_event_position(event.position());
if (!index.is_valid())
return TableView::mousemove_event(event);
auto& sheet = static_cast<SheetModel&>(*model).sheet();
sheet.disable_updates();
ScopeGuard sheet_update_enabler { [&] { sheet.enable_updates(); } };
m_is_hovering_cut_zone = false;
m_is_hovering_extend_zone = false;
if (selection().size() > 0 && !m_should_intercept_drag) {
// Get top-left and bottom-right most cells of selection
auto bottom_right_most_index = selection().first();
auto top_left_most_index = selection().first();
selection().for_each_index([&](auto& index) {
if (index.row() > bottom_right_most_index.row())
bottom_right_most_index = index;
else if (index.column() > bottom_right_most_index.column())
bottom_right_most_index = index;
if (index.row() < top_left_most_index.row())
top_left_most_index = index;
else if (index.column() < top_left_most_index.column())
top_left_most_index = index;
});
auto top_left_rect = content_rect(top_left_most_index);
auto bottom_right_rect = content_rect(bottom_right_most_index);
auto distance_tl = top_left_rect.center() - event.position();
auto distance_br = bottom_right_rect.center() - event.position();
auto is_over_top_line = false;
auto is_over_bottom_line = false;
auto is_over_left_line = false;
auto is_over_right_line = false;
// If cursor is within the bounds of the selection
auto select_padding = 2;
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))) {
if (distance_tl.y() >= (top_left_rect.height() / 2 - select_padding))
is_over_top_line = true;
else if (distance_br.y() <= -(bottom_right_rect.height() / 2 - select_padding))
is_over_bottom_line = true;
if (distance_tl.x() >= (top_left_rect.width() / 2 - select_padding))
is_over_left_line = true;
else if (distance_br.x() <= -(bottom_right_rect.width() / 2 - select_padding))
is_over_right_line = true;
}
if (is_over_bottom_line && is_over_right_line)
m_is_hovering_extend_zone = true;
else if (is_over_top_line || is_over_bottom_line || is_over_left_line || is_over_right_line) {
m_target_cell = top_left_most_index;
m_is_hovering_cut_zone = true;
}
}
if (m_is_hovering_cut_zone || m_is_dragging_for_copy)
set_override_cursor(Gfx::StandardCursor::Drag);
else if (m_is_hovering_extend_zone)
set_override_cursor(Gfx::StandardCursor::Crosshair);
else
set_override_cursor(Gfx::StandardCursor::Arrow);
auto holding_left_button = !!(event.buttons() & GUI::MouseButton::Primary);
if (m_is_dragging_for_copy) {
m_should_intercept_drag = false;
if (holding_left_button) {
m_has_committed_to_dragging = true;
}
} else if (!m_should_intercept_drag) {
if (!holding_left_button) {
m_starting_selection_index = index;
} else {
m_should_intercept_drag = true;
m_might_drag = false;
}
}
if (holding_left_button && m_should_intercept_drag && !m_has_committed_to_dragging) {
if (!m_starting_selection_index.is_valid())
m_starting_selection_index = index;
Vector<GUI::ModelIndex> new_selection;
for (auto i = min(m_starting_selection_index.row(), index.row()), imax = max(m_starting_selection_index.row(), index.row()); i <= imax; ++i) {
for (auto j = min(m_starting_selection_index.column(), index.column()), jmax = max(m_starting_selection_index.column(), index.column()); j <= jmax; ++j) {
auto index = model->index(i, j);
if (index.is_valid())
new_selection.append(move(index));
}
}
if (!event.ctrl())
selection().clear();
selection().add_all(new_selection);
}
}
TableView::mousemove_event(event);
}
void InfinitelyScrollableTableView::mousedown_event(GUI::MouseEvent& event)
{
// Override the mouse event so that the the cell that is 'clicked' is not
// the one right beneath the cursor but instead the one that is referred to
// when m_is_hovering_cut_zone as it can be the case that the user is targetting
// a cell yet be outside of its bounding box due to the select_padding.
if (m_is_hovering_cut_zone) {
m_is_dragging_for_copy = true;
auto rect = content_rect(m_target_cell);
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() };
AbstractTableView::mousedown_event(adjusted_event);
} else {
AbstractTableView::mousedown_event(event);
}
}
void InfinitelyScrollableTableView::mouseup_event(GUI::MouseEvent& event)
{
m_should_intercept_drag = false;
m_has_committed_to_dragging = false;
m_is_dragging_for_copy = false;
if (m_is_hovering_cut_zone) {
auto rect = content_rect(m_target_cell);
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() };
TableView::mouseup_event(adjusted_event);
} else {
TableView::mouseup_event(event);
}
}
void SpreadsheetView::update_with_model()
{
m_sheet_model->update();
m_table_view->update();
}
SpreadsheetView::SpreadsheetView(Sheet& sheet)
: m_sheet(sheet)
, m_sheet_model(SheetModel::create(*m_sheet))
{
set_layout<GUI::VerticalBoxLayout>().set_margins(2);
m_table_view = add<InfinitelyScrollableTableView>();
m_table_view->set_grid_style(GUI::TableView::GridStyle::Both);
m_table_view->set_selection_behavior(GUI::AbstractView::SelectionBehavior::SelectItems);
m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed | GUI::AbstractView::AnyKeyPressed | GUI::AbstractView::DoubleClicked);
m_table_view->set_tab_key_navigation_enabled(true);
m_table_view->row_header().set_visible(true);
m_table_view->set_model(m_sheet_model);
m_table_view->on_reaching_vertical_end = [&]() {
for (size_t i = 0; i < 100; ++i) {
auto index = m_sheet->add_row();
m_table_view->set_column_painting_delegate(index, make<TableCellPainter>(*m_table_view));
};
update_with_model();
};
m_table_view->on_reaching_horizontal_end = [&]() {
for (size_t i = 0; i < 10; ++i) {
m_sheet->add_column();
auto last_column_index = m_sheet->column_count() - 1;
m_table_view->set_column_width(last_column_index, 50);
m_table_view->set_default_column_width(last_column_index, 50);
m_table_view->set_column_header_alignment(last_column_index, Gfx::TextAlignment::Center);
m_table_view->set_column_painting_delegate(last_column_index, make<TableCellPainter>(*m_table_view));
}
update_with_model();
};
set_focus_proxy(m_table_view);
// FIXME: This is dumb.
for (size_t i = 0; i < m_sheet->column_count(); ++i) {
m_table_view->set_column_painting_delegate(i, make<TableCellPainter>(*m_table_view));
m_table_view->set_column_width(i, 50);
m_table_view->set_default_column_width(i, 50);
m_table_view->set_column_header_alignment(i, Gfx::TextAlignment::Center);
}
m_table_view->set_alternating_row_colors(false);
m_table_view->set_highlight_selected_rows(false);
m_table_view->set_editable(true);
m_table_view->aid_create_editing_delegate = [this](auto&) {
auto delegate = make<EditingDelegate>(*m_sheet);
delegate->on_cursor_key_pressed = [this](auto& event) {
m_table_view->stop_editing();
m_table_view->dispatch_event(event);
};
delegate->on_cell_focusout = [this](auto& index, auto& value) {
m_table_view->model()->set_data(index, value);
};
return delegate;
};
m_table_view->on_selection_change = [&] {
m_sheet->selected_cells().clear();
for (auto& index : m_table_view->selection().indices()) {
Position position { (size_t)index.column(), (size_t)index.row() };
m_sheet->selected_cells().set(position);
}
if (m_table_view->selection().is_empty() && on_selection_dropped)
return on_selection_dropped();
Vector<Position> selected_positions;
selected_positions.ensure_capacity(m_table_view->selection().size());
for (auto& selection : m_table_view->selection().indices())
selected_positions.empend((size_t)selection.column(), (size_t)selection.row());
if (on_selection_changed) {
on_selection_changed(move(selected_positions));
update_with_model();
};
};
m_table_view->on_activation = [this](auto&) {
m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set);
};
m_table_view->on_context_menu_request = [&](const GUI::ModelIndex&, const GUI::ContextMenuEvent& event) {
// NOTE: We ignore the specific cell for now.
m_cell_range_context_menu->popup(event.screen_position());
};
m_cell_range_context_menu = GUI::Menu::construct();
m_cell_range_context_menu->add_action(GUI::Action::create("Type and Formatting...", [this](auto&) {
Vector<Position> positions;
for (auto& index : m_table_view->selection().indices()) {
Position position { (size_t)index.column(), (size_t)index.row() };
positions.append(move(position));
}
if (positions.is_empty()) {
auto& index = m_table_view->cursor_index();
Position position { (size_t)index.column(), (size_t)index.row() };
positions.append(move(position));
}
auto dialog = CellTypeDialog::construct(positions, *m_sheet, window());
if (dialog->exec() == GUI::Dialog::ExecOK) {
for (auto& position : positions) {
auto& cell = m_sheet->ensure(position);
cell.set_type(dialog->type());
cell.set_type_metadata(dialog->metadata());
cell.set_conditional_formats(dialog->conditional_formats());
}
m_table_view->update();
}
}));
m_table_view->on_drop = [&](const GUI::ModelIndex& index, const GUI::DropEvent& event) {
if (!index.is_valid())
return;
ScopeGuard update_after_drop { [this] { update(); } };
if (event.mime_data().has_format("text/x-spreadsheet-data")) {
auto const& data = event.mime_data().data("text/x-spreadsheet-data");
StringView urls { data.data(), data.size() };
Vector<Position> source_positions, target_positions;
for (auto& line : urls.lines(false)) {
auto position = m_sheet->position_from_url(line);
if (position.has_value())
source_positions.append(position.release_value());
}
// Drop always has a single target.
Position target { (size_t)index.column(), (size_t)index.row() };
target_positions.append(move(target));
if (source_positions.is_empty())
return;
auto first_position = source_positions.take_first();
m_sheet->copy_cells(move(source_positions), move(target_positions), first_position);
return;
}
if (event.mime_data().has_text()) {
auto& target_cell = m_sheet->ensure({ (size_t)index.column(), (size_t)index.row() });
target_cell.set_data(event.text());
return;
}
};
}
void SpreadsheetView::hide_event(GUI::HideEvent&)
{
if (on_selection_dropped)
on_selection_dropped();
}
void SpreadsheetView::show_event(GUI::ShowEvent&)
{
if (on_selection_changed && !m_table_view->selection().is_empty()) {
Vector<Position> selected_positions;
selected_positions.ensure_capacity(m_table_view->selection().size());
for (auto& selection : m_table_view->selection().indices())
selected_positions.empend((size_t)selection.column(), (size_t)selection.row());
on_selection_changed(move(selected_positions));
}
}
void SpreadsheetView::move_cursor(GUI::AbstractView::CursorMovement direction)
{
m_table_view->move_cursor(direction, GUI::AbstractView::SelectionUpdate::Set);
}
void SpreadsheetView::TableCellPainter::paint(GUI::Painter& painter, const Gfx::IntRect& rect, const Gfx::Palette& palette, const GUI::ModelIndex& index)
{
// Draw a border.
// Undo the horizontal padding done by the table view...
auto cell_rect = rect.inflated(m_table_view.horizontal_padding() * 2, 0);
if (auto bg = index.data(GUI::ModelRole::BackgroundColor); bg.is_color())
painter.fill_rect(cell_rect, bg.as_color());
if (m_table_view.selection().contains(index)) {
Color fill_color = palette.selection();
fill_color.set_alpha(80);
painter.fill_rect(cell_rect, fill_color);
}
auto text_color = index.data(GUI::ModelRole::ForegroundColor).to_color(palette.color(m_table_view.foreground_role()));
auto data = index.data();
auto text_alignment = index.data(GUI::ModelRole::TextAlignment).to_text_alignment(Gfx::TextAlignment::CenterRight);
painter.draw_text(rect, data.to_string(), m_table_view.font_for_index(index), text_alignment, text_color, Gfx::TextElision::Right);
}
}