mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-27 23:09:08 +00:00
LibGUI: Move table view headers into their own widget
This patch introduces the HeaderView class, which is a widget that implements the column headers of TableView and TreeView. This greatly simplifies event management in the view implementations and also makes it much easier to eventually implement row headers.
This commit is contained in:
parent
eca6ff353e
commit
44e371635e
Notes:
sideshowbarker
2024-07-19 03:09:24 +09:00
Author: https://github.com/awesomekling
Commit: 44e371635e
16 changed files with 555 additions and 351 deletions
|
@ -28,6 +28,7 @@
|
|||
#include <AK/Vector.h>
|
||||
#include <LibGUI/AbstractTableView.h>
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/HeaderView.h>
|
||||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
|
@ -37,10 +38,10 @@
|
|||
|
||||
namespace GUI {
|
||||
|
||||
static const int minimum_column_width = 2;
|
||||
|
||||
AbstractTableView::AbstractTableView()
|
||||
{
|
||||
m_column_header = add<HeaderView>(*this, Gfx::Orientation::Horizontal);
|
||||
m_column_header->move_to_back();
|
||||
set_should_hide_unnecessary_scrollbars(true);
|
||||
}
|
||||
|
||||
|
@ -67,9 +68,9 @@ void AbstractTableView::update_column_sizes()
|
|||
int row_count = model.row_count();
|
||||
|
||||
for (int column = 0; column < column_count; ++column) {
|
||||
if (is_column_hidden(column))
|
||||
if (!column_header().is_section_visible(column))
|
||||
continue;
|
||||
int header_width = header_font().width(model.column_name(column));
|
||||
int header_width = m_column_header->font().width(model.column_name(column));
|
||||
if (column == m_key_column && model.is_column_sortable(column))
|
||||
header_width += font().width(" \xE2\xAC\x86"); // UPWARDS BLACK ARROW
|
||||
int column_width = header_width;
|
||||
|
@ -85,9 +86,7 @@ void AbstractTableView::update_column_sizes()
|
|||
}
|
||||
column_width = max(column_width, cell_width);
|
||||
}
|
||||
auto& column_data = this->column_data(column);
|
||||
column_data.width = max(column_data.width, column_width);
|
||||
column_data.has_initialized_width = true;
|
||||
column_header().set_section_size(column, max(m_column_header->section_size(column), column_width));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,264 +97,54 @@ void AbstractTableView::update_content_size()
|
|||
|
||||
int content_width = 0;
|
||||
int column_count = model()->column_count();
|
||||
|
||||
for (int i = 0; i < column_count; ++i) {
|
||||
if (!is_column_hidden(i))
|
||||
if (column_header().is_section_visible(i))
|
||||
content_width += column_width(i) + horizontal_padding() * 2;
|
||||
}
|
||||
int content_height = item_count() * item_height();
|
||||
|
||||
set_content_size({ content_width, content_height });
|
||||
set_size_occupied_by_fixed_elements({ 0, header_height() });
|
||||
column_header().set_width(content_width);
|
||||
set_size_occupied_by_fixed_elements({ 0, m_column_header->height() });
|
||||
}
|
||||
|
||||
Gfx::IntRect AbstractTableView::header_rect(int column_index) const
|
||||
TableCellPaintingDelegate* AbstractTableView::column_painting_delegate(int column) const
|
||||
{
|
||||
if (!model())
|
||||
return {};
|
||||
if (is_column_hidden(column_index))
|
||||
return {};
|
||||
int x_offset = 0;
|
||||
for (int i = 0; i < column_index; ++i) {
|
||||
if (is_column_hidden(i))
|
||||
continue;
|
||||
x_offset += column_width(i) + horizontal_padding() * 2;
|
||||
}
|
||||
return { x_offset, 0, column_width(column_index) + horizontal_padding() * 2, header_height() };
|
||||
// FIXME: This should return a const pointer I think..
|
||||
return const_cast<TableCellPaintingDelegate*>(m_column_painting_delegate.get(column).value_or(nullptr));
|
||||
}
|
||||
|
||||
void AbstractTableView::set_hovered_header_index(int index)
|
||||
void AbstractTableView::set_cell_painting_delegate(int column, OwnPtr<TableCellPaintingDelegate> delegate)
|
||||
{
|
||||
if (m_hovered_column_header_index == index)
|
||||
return;
|
||||
m_hovered_column_header_index = index;
|
||||
update_headers();
|
||||
}
|
||||
|
||||
void AbstractTableView::paint_headers(Painter& painter)
|
||||
{
|
||||
if (!headers_visible())
|
||||
return;
|
||||
int exposed_width = max(content_size().width(), width());
|
||||
painter.fill_rect({ 0, 0, exposed_width, header_height() }, palette().button());
|
||||
painter.draw_line({ 0, 0 }, { exposed_width - 1, 0 }, palette().threed_highlight());
|
||||
painter.draw_line({ 0, header_height() - 1 }, { exposed_width - 1, header_height() - 1 }, palette().threed_shadow1());
|
||||
int x_offset = 0;
|
||||
int column_count = model()->column_count();
|
||||
for (int column_index = 0; column_index < column_count; ++column_index) {
|
||||
if (is_column_hidden(column_index))
|
||||
continue;
|
||||
int column_width = this->column_width(column_index);
|
||||
bool is_key_column = m_key_column == column_index;
|
||||
Gfx::IntRect cell_rect(x_offset, 0, column_width + horizontal_padding() * 2, header_height());
|
||||
bool pressed = column_index == m_pressed_column_header_index && m_pressed_column_header_is_pressed;
|
||||
bool hovered = column_index == m_hovered_column_header_index && model()->is_column_sortable(column_index);
|
||||
Gfx::StylePainter::paint_button(painter, cell_rect, palette(), Gfx::ButtonStyle::Normal, pressed, hovered);
|
||||
String text;
|
||||
if (is_key_column) {
|
||||
StringBuilder builder;
|
||||
builder.append(model()->column_name(column_index));
|
||||
if (m_sort_order == SortOrder::Ascending)
|
||||
builder.append(" \xE2\xAC\x86"); // UPWARDS BLACK ARROW
|
||||
else if (m_sort_order == SortOrder::Descending)
|
||||
builder.append(" \xE2\xAC\x87"); // DOWNWARDS BLACK ARROW
|
||||
text = builder.to_string();
|
||||
} else {
|
||||
text = model()->column_name(column_index);
|
||||
}
|
||||
auto text_rect = cell_rect.shrunken(horizontal_padding() * 2, 0);
|
||||
if (pressed)
|
||||
text_rect.move_by(1, 1);
|
||||
painter.draw_text(text_rect, text, header_font(), column_header_alignment(column_index), palette().button_text());
|
||||
x_offset += column_width + horizontal_padding() * 2;
|
||||
}
|
||||
}
|
||||
|
||||
bool AbstractTableView::is_column_hidden(int column) const
|
||||
{
|
||||
return !column_data(column).visibility;
|
||||
}
|
||||
|
||||
void AbstractTableView::set_column_hidden(int column, bool hidden)
|
||||
{
|
||||
auto& column_data = this->column_data(column);
|
||||
if (column_data.visibility == !hidden)
|
||||
return;
|
||||
column_data.visibility = !hidden;
|
||||
if (column_data.visibility_action) {
|
||||
column_data.visibility_action->set_checked(!hidden);
|
||||
}
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
|
||||
Menu& AbstractTableView::ensure_header_context_menu()
|
||||
{
|
||||
// FIXME: This menu needs to be rebuilt if the model is swapped out,
|
||||
// or if the column count/names change.
|
||||
if (!m_header_context_menu) {
|
||||
ASSERT(model());
|
||||
m_header_context_menu = Menu::construct();
|
||||
|
||||
for (int column = 0; column < model()->column_count(); ++column) {
|
||||
auto& column_data = this->column_data(column);
|
||||
auto name = model()->column_name(column);
|
||||
column_data.visibility_action = Action::create_checkable(name, [this, column](auto& action) {
|
||||
set_column_hidden(column, !action.is_checked());
|
||||
});
|
||||
column_data.visibility_action->set_checked(column_data.visibility);
|
||||
|
||||
m_header_context_menu->add_action(*column_data.visibility_action);
|
||||
}
|
||||
}
|
||||
return *m_header_context_menu;
|
||||
}
|
||||
|
||||
const Gfx::Font& AbstractTableView::header_font()
|
||||
{
|
||||
return Gfx::Font::default_bold_font();
|
||||
}
|
||||
|
||||
void AbstractTableView::set_cell_painting_delegate(int column, OwnPtr<TableCellPaintingDelegate>&& delegate)
|
||||
{
|
||||
column_data(column).cell_painting_delegate = move(delegate);
|
||||
}
|
||||
|
||||
void AbstractTableView::update_headers()
|
||||
{
|
||||
Gfx::IntRect rect { 0, 0, frame_inner_rect().width(), header_height() };
|
||||
rect.move_by(frame_thickness(), frame_thickness());
|
||||
update(rect);
|
||||
}
|
||||
|
||||
AbstractTableView::ColumnData& AbstractTableView::column_data(int column) const
|
||||
{
|
||||
if (static_cast<size_t>(column) >= m_column_data.size())
|
||||
m_column_data.resize(column + 1);
|
||||
return m_column_data.at(column);
|
||||
}
|
||||
|
||||
Gfx::IntRect AbstractTableView::column_resize_grabbable_rect(int column) const
|
||||
{
|
||||
if (!model())
|
||||
return {};
|
||||
auto header_rect = this->header_rect(column);
|
||||
return { header_rect.right() - 1, header_rect.top(), 4, header_rect.height() };
|
||||
if (!delegate)
|
||||
m_column_painting_delegate.remove(column);
|
||||
else
|
||||
m_column_painting_delegate.set(column, move(delegate));
|
||||
}
|
||||
|
||||
int AbstractTableView::column_width(int column_index) const
|
||||
{
|
||||
if (!model())
|
||||
return 0;
|
||||
return column_data(column_index).width;
|
||||
return m_column_header->section_size(column_index);
|
||||
}
|
||||
|
||||
void AbstractTableView::set_column_width(int column, int width)
|
||||
{
|
||||
column_data(column).width = width;
|
||||
column_header().set_section_size(column, width);
|
||||
}
|
||||
|
||||
Gfx::TextAlignment AbstractTableView::column_header_alignment(int column_index) const
|
||||
{
|
||||
if (!model())
|
||||
return Gfx::TextAlignment::CenterLeft;
|
||||
return column_data(column_index).header_alignment;
|
||||
return m_column_header->section_alignment(column_index);
|
||||
}
|
||||
|
||||
void AbstractTableView::set_column_header_alignment(int column, Gfx::TextAlignment alignment)
|
||||
{
|
||||
column_data(column).header_alignment = alignment;
|
||||
}
|
||||
|
||||
void AbstractTableView::mousemove_event(MouseEvent& event)
|
||||
{
|
||||
if (!model())
|
||||
return AbstractView::mousemove_event(event);
|
||||
|
||||
auto adjusted_position = this->adjusted_position(event.position());
|
||||
Gfx::IntPoint horizontally_adjusted_position(adjusted_position.x(), event.position().y());
|
||||
|
||||
if (m_in_column_resize) {
|
||||
auto delta = adjusted_position - m_column_resize_origin;
|
||||
int new_width = m_column_resize_original_width + delta.x();
|
||||
if (new_width <= minimum_column_width)
|
||||
new_width = minimum_column_width;
|
||||
ASSERT(m_resizing_column >= 0 && m_resizing_column < model()->column_count());
|
||||
auto& column_data = this->column_data(m_resizing_column);
|
||||
if (column_data.width != new_width) {
|
||||
column_data.width = new_width;
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pressed_column_header_index != -1) {
|
||||
auto header_rect = this->header_rect(m_pressed_column_header_index);
|
||||
if (header_rect.contains(horizontally_adjusted_position)) {
|
||||
set_hovered_header_index(m_pressed_column_header_index);
|
||||
if (!m_pressed_column_header_is_pressed)
|
||||
update_headers();
|
||||
m_pressed_column_header_is_pressed = true;
|
||||
} else {
|
||||
set_hovered_header_index(-1);
|
||||
if (m_pressed_column_header_is_pressed)
|
||||
update_headers();
|
||||
m_pressed_column_header_is_pressed = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.buttons() == 0) {
|
||||
int column_count = model()->column_count();
|
||||
bool found_hovered_header = false;
|
||||
for (int i = 0; i < column_count; ++i) {
|
||||
if (column_resize_grabbable_rect(i).contains(horizontally_adjusted_position)) {
|
||||
window()->set_override_cursor(StandardCursor::ResizeColumn);
|
||||
set_hovered_header_index(-1);
|
||||
return;
|
||||
}
|
||||
if (header_rect(i).contains(horizontally_adjusted_position)) {
|
||||
set_hovered_header_index(i);
|
||||
found_hovered_header = true;
|
||||
}
|
||||
}
|
||||
if (!found_hovered_header)
|
||||
set_hovered_header_index(-1);
|
||||
}
|
||||
window()->set_override_cursor(StandardCursor::None);
|
||||
|
||||
AbstractView::mousemove_event(event);
|
||||
}
|
||||
|
||||
void AbstractTableView::mouseup_event(MouseEvent& event)
|
||||
{
|
||||
auto adjusted_position = this->adjusted_position(event.position());
|
||||
Gfx::IntPoint horizontally_adjusted_position(adjusted_position.x(), event.position().y());
|
||||
if (event.button() == MouseButton::Left) {
|
||||
if (m_in_column_resize) {
|
||||
if (!column_resize_grabbable_rect(m_resizing_column).contains(horizontally_adjusted_position))
|
||||
window()->set_override_cursor(StandardCursor::None);
|
||||
m_in_column_resize = false;
|
||||
return;
|
||||
}
|
||||
if (m_pressed_column_header_index != -1) {
|
||||
auto header_rect = this->header_rect(m_pressed_column_header_index);
|
||||
if (header_rect.contains(horizontally_adjusted_position)) {
|
||||
auto new_sort_order = SortOrder::Ascending;
|
||||
if (m_key_column == m_pressed_column_header_index)
|
||||
new_sort_order = m_sort_order == SortOrder::Ascending
|
||||
? SortOrder::Descending
|
||||
: SortOrder::Ascending;
|
||||
set_key_column_and_sort_order(m_pressed_column_header_index, new_sort_order);
|
||||
}
|
||||
m_pressed_column_header_index = -1;
|
||||
m_pressed_column_header_is_pressed = false;
|
||||
update_headers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AbstractView::mouseup_event(event);
|
||||
column_header().set_section_alignment(column, alignment);
|
||||
}
|
||||
|
||||
void AbstractTableView::mousedown_event(MouseEvent& event)
|
||||
|
@ -369,27 +158,6 @@ void AbstractTableView::mousedown_event(MouseEvent& event)
|
|||
auto adjusted_position = this->adjusted_position(event.position());
|
||||
Gfx::IntPoint horizontally_adjusted_position(adjusted_position.x(), event.position().y());
|
||||
|
||||
if (event.y() < header_height()) {
|
||||
int column_count = model()->column_count();
|
||||
for (int i = 0; i < column_count; ++i) {
|
||||
if (column_resize_grabbable_rect(i).contains(horizontally_adjusted_position)) {
|
||||
m_resizing_column = i;
|
||||
m_in_column_resize = true;
|
||||
m_column_resize_original_width = column_width(i);
|
||||
m_column_resize_origin = adjusted_position;
|
||||
return;
|
||||
}
|
||||
auto header_rect = this->header_rect(i);
|
||||
if (header_rect.contains(horizontally_adjusted_position) && model()->is_column_sortable(i)) {
|
||||
m_pressed_column_header_index = i;
|
||||
m_pressed_column_header_is_pressed = true;
|
||||
update_headers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_toggle;
|
||||
auto index = index_at_event_position(event.position(), is_toggle);
|
||||
|
||||
|
@ -456,13 +224,13 @@ void AbstractTableView::move_selection(int vertical_steps, int horizontal_steps)
|
|||
|
||||
void AbstractTableView::scroll_into_view(const ModelIndex& index, Orientation orientation)
|
||||
{
|
||||
auto rect = row_rect(index.row()).translated(0, -header_height());
|
||||
auto rect = row_rect(index.row()).translated(0, -m_column_header->height());
|
||||
ScrollableWidget::scroll_into_view(rect, orientation);
|
||||
}
|
||||
|
||||
void AbstractTableView::scroll_into_view(const ModelIndex& index, bool scroll_horizontally, bool scroll_vertically)
|
||||
{
|
||||
auto rect = row_rect(index.row()).translated(0, -header_height());
|
||||
auto rect = row_rect(index.row()).translated(0, -m_column_header->height());
|
||||
ScrollableWidget::scroll_into_view(rect, scroll_horizontally, scroll_vertically);
|
||||
}
|
||||
|
||||
|
@ -471,11 +239,8 @@ void AbstractTableView::doubleclick_event(MouseEvent& event)
|
|||
if (!model())
|
||||
return;
|
||||
if (event.button() == MouseButton::Left) {
|
||||
if (event.y() < header_height())
|
||||
return;
|
||||
if (!selection().is_empty()) {
|
||||
if (!selection().is_empty())
|
||||
activate_or_edit_selected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -483,10 +248,6 @@ void AbstractTableView::context_menu_event(ContextMenuEvent& event)
|
|||
{
|
||||
if (!model())
|
||||
return;
|
||||
if (event.position().y() < header_height()) {
|
||||
ensure_header_context_menu().popup(event.screen_position());
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_toggle;
|
||||
auto index = index_at_event_position(event.position(), is_toggle);
|
||||
|
@ -500,13 +261,6 @@ void AbstractTableView::context_menu_event(ContextMenuEvent& event)
|
|||
on_context_menu_request(index, event);
|
||||
}
|
||||
|
||||
void AbstractTableView::leave_event(Core::Event& event)
|
||||
{
|
||||
AbstractView::leave_event(event);
|
||||
window()->set_override_cursor(StandardCursor::None);
|
||||
set_hovered_header_index(-1);
|
||||
}
|
||||
|
||||
Gfx::IntRect AbstractTableView::content_rect(int row, int column) const
|
||||
{
|
||||
auto row_rect = this->row_rect(row);
|
||||
|
@ -524,7 +278,7 @@ Gfx::IntRect AbstractTableView::content_rect(const ModelIndex& index) const
|
|||
|
||||
Gfx::IntRect AbstractTableView::row_rect(int item_index) const
|
||||
{
|
||||
return { 0, header_height() + (item_index * item_height()), max(content_size().width(), width()), item_height() };
|
||||
return { 0, m_column_header->height() + (item_index * item_height()), max(content_size().width(), width()), item_height() };
|
||||
}
|
||||
|
||||
Gfx::IntPoint AbstractTableView::adjusted_position(const Gfx::IntPoint& position) const
|
||||
|
@ -540,4 +294,40 @@ void AbstractTableView::did_update_model(unsigned flags)
|
|||
update();
|
||||
}
|
||||
|
||||
void AbstractTableView::resize_event(ResizeEvent& event)
|
||||
{
|
||||
AbstractView::resize_event(event);
|
||||
|
||||
if (column_header().is_visible())
|
||||
column_header().set_relative_rect(frame_thickness(), frame_thickness(), content_width(), column_header().preferred_size().height());
|
||||
}
|
||||
|
||||
void AbstractTableView::header_did_change_section_size(Badge<HeaderView>, Gfx::Orientation, int, int)
|
||||
{
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
|
||||
void AbstractTableView::header_did_change_section_visibility(Badge<HeaderView>, Gfx::Orientation, int, bool)
|
||||
{
|
||||
update_content_size();
|
||||
update();
|
||||
}
|
||||
|
||||
void AbstractTableView::set_column_hidden(int column, bool hidden)
|
||||
{
|
||||
column_header().set_section_visible(column, !hidden);
|
||||
}
|
||||
|
||||
void AbstractTableView::set_column_headers_visible(bool visible)
|
||||
{
|
||||
column_header().set_visible(visible);
|
||||
}
|
||||
|
||||
void AbstractTableView::did_scroll()
|
||||
{
|
||||
AbstractView::did_scroll();
|
||||
column_header().set_x(frame_thickness() + -horizontal_scrollbar().value());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue