diff options
author | Andreas Kling <awesomekling@gmail.com> | 2019-12-13 20:54:40 +0100 |
---|---|---|
committer | Andreas Kling <awesomekling@gmail.com> | 2019-12-13 20:54:40 +0100 |
commit | 2d39bce3f6244fa270c6c909023475a481436681 (patch) | |
tree | 24f70f2a22cfc50ead32ddab0c2436e0b75abe0f /Libraries | |
parent | 69d05fbf44607dd16351dde6ad868d577ca6e69a (diff) | |
download | serenity-2d39bce3f6244fa270c6c909023475a481436681.zip |
LibGUI: Add a GAbstractColumnView base class for GTableView
Almost everything in GTableView moves up to GAbstractColumnView.
This is in preparation for sharing a base class between GTableView
and GTreeView :^)
Diffstat (limited to 'Libraries')
-rw-r--r-- | Libraries/LibGUI/GAbstractColumnView.cpp | 515 | ||||
-rw-r--r-- | Libraries/LibGUI/GAbstractColumnView.h | 98 | ||||
-rw-r--r-- | Libraries/LibGUI/GAbstractView.h | 6 | ||||
-rw-r--r-- | Libraries/LibGUI/GTableView.cpp | 501 | ||||
-rw-r--r-- | Libraries/LibGUI/GTableView.h | 89 | ||||
-rw-r--r-- | Libraries/LibGUI/Makefile | 1 |
6 files changed, 620 insertions, 590 deletions
diff --git a/Libraries/LibGUI/GAbstractColumnView.cpp b/Libraries/LibGUI/GAbstractColumnView.cpp new file mode 100644 index 0000000000..0c158ff3be --- /dev/null +++ b/Libraries/LibGUI/GAbstractColumnView.cpp @@ -0,0 +1,515 @@ +#include <AK/StringBuilder.h> +#include <LibGUI/GAbstractColumnView.h> +#include <LibGUI/GAction.h> +#include <LibGUI/GMenu.h> +#include <LibGUI/GPainter.h> +#include <LibGUI/GScrollBar.h> + +static const int minimum_column_width = 2; + +GAbstractColumnView::GAbstractColumnView(GWidget* parent) + : GAbstractView(parent) +{ + set_frame_shape(FrameShape::Container); + set_frame_shadow(FrameShadow::Sunken); + set_frame_thickness(2); + + set_should_hide_unnecessary_scrollbars(true); +} + +GAbstractColumnView::~GAbstractColumnView() +{ +} + +void GAbstractColumnView::update_column_sizes() +{ + if (!m_size_columns_to_fit_content) + return; + + if (!model()) + return; + + auto& model = *this->model(); + int column_count = model.column_count(); + int row_count = model.row_count(); + + for (int column = 0; column < column_count; ++column) { + if (is_column_hidden(column)) + continue; + int header_width = header_font().width(model.column_name(column)); + int column_width = header_width; + for (int row = 0; row < row_count; ++row) { + auto cell_data = model.data(model.index(row, column)); + int cell_width = 0; + if (cell_data.is_bitmap()) { + cell_width = cell_data.as_bitmap().width(); + } else { + cell_width = font().width(cell_data.to_string()); + } + 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; + } +} + +void GAbstractColumnView::update_content_size() +{ + if (!model()) + return set_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)) + 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() }); +} + +Rect GAbstractColumnView::header_rect(int column_index) 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() }; +} + +void GAbstractColumnView::set_hovered_header_index(int index) +{ + if (m_hovered_column_header_index == index) + return; + m_hovered_column_header_index = index; + update_headers(); +} + +void GAbstractColumnView::paint_headers(GPainter& painter) +{ + int exposed_width = max(content_size().width(), width()); + painter.fill_rect({ 0, 0, exposed_width, header_height() }, Color::WarmGray); + painter.draw_line({ 0, 0 }, { exposed_width - 1, 0 }, Color::White); + painter.draw_line({ 0, header_height() - 1 }, { exposed_width - 1, header_height() - 1 }, Color::MidGray); + 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 = model()->key_column() == column_index; + Rect 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()->column_metadata(column_index).sortable == GModel::ColumnMetadata::Sortable::True; + StylePainter::paint_button(painter, cell_rect, ButtonStyle::Normal, pressed, hovered); + String text; + if (is_key_column) { + StringBuilder builder; + builder.append(model()->column_name(column_index)); + auto sort_order = model()->sort_order(); + if (sort_order == GSortOrder::Ascending) + builder.append(" \xc3\xb6"); + else if (sort_order == GSortOrder::Descending) + builder.append(" \xc3\xb7"); + text = builder.to_string(); + } else { + text = model()->column_name(column_index); + } + auto text_rect = cell_rect.translated(horizontal_padding(), 0); + if (pressed) + text_rect.move_by(1, 1); + painter.draw_text(text_rect, text, header_font(), TextAlignment::CenterLeft, Color::Black); + x_offset += column_width + horizontal_padding() * 2; + } +} + +bool GAbstractColumnView::is_column_hidden(int column) const +{ + return !column_data(column).visibility; +} + +void GAbstractColumnView::set_column_hidden(int column, bool hidden) +{ + auto& column_data = this->column_data(column); + if (column_data.visibility == !hidden) + return; + column_data.visibility = !hidden; + update_content_size(); + update(); +} + +GMenu& GAbstractColumnView::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 = GMenu::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 = GAction::create(name, [this, column](GAction& action) { + action.set_checked(!action.is_checked()); + set_column_hidden(column, !action.is_checked()); + }); + column_data.visibility_action->set_checkable(true); + column_data.visibility_action->set_checked(true); + + m_header_context_menu->add_action(*column_data.visibility_action); + } + } + return *m_header_context_menu; +} + +const Font& GAbstractColumnView::header_font() +{ + return Font::default_bold_font(); +} + +void GAbstractColumnView::set_cell_painting_delegate(int column, OwnPtr<GTableCellPaintingDelegate>&& delegate) +{ + column_data(column).cell_painting_delegate = move(delegate); +} + +void GAbstractColumnView::update_headers() +{ + Rect rect { 0, 0, frame_inner_rect().width(), header_height() }; + rect.move_by(frame_thickness(), frame_thickness()); + update(rect); +} + +GAbstractColumnView::ColumnData& GAbstractColumnView::column_data(int column) const +{ + if (column >= m_column_data.size()) + m_column_data.resize(column + 1); + return m_column_data.at(column); +} + +Rect GAbstractColumnView::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() }; +} + +int GAbstractColumnView::column_width(int column_index) const +{ + if (!model()) + return 0; + auto& column_data = this->column_data(column_index); + if (!column_data.has_initialized_width) { + ASSERT(!m_size_columns_to_fit_content); + column_data.has_initialized_width = true; + column_data.width = model()->column_metadata(column_index).preferred_width; + } + return column_data.width; +} + +void GAbstractColumnView::mousemove_event(GMouseEvent& event) +{ + if (!model()) + return; + + if (m_in_column_resize) { + auto delta = event.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; + dbg() << "New column 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(event.position())) { + if (!m_pressed_column_header_is_pressed) + update_headers(); + m_pressed_column_header_is_pressed = true; + } else { + 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(event.position())) { + window()->set_override_cursor(GStandardCursor::ResizeHorizontal); + set_hovered_header_index(-1); + return; + } + if (header_rect(i).contains(event.position())) { + set_hovered_header_index(i); + found_hovered_header = true; + } + } + if (!found_hovered_header) + set_hovered_header_index(-1); + } + window()->set_override_cursor(GStandardCursor::None); +} + +void GAbstractColumnView::mouseup_event(GMouseEvent& event) +{ + auto adjusted_position = this->adjusted_position(event.position()); + if (event.button() == GMouseButton::Left) { + if (m_in_column_resize) { + if (!column_resize_grabbable_rect(m_resizing_column).contains(adjusted_position)) + window()->set_override_cursor(GStandardCursor::None); + m_in_column_resize = false; + } + if (m_pressed_column_header_index != -1) { + auto header_rect = this->header_rect(m_pressed_column_header_index); + if (header_rect.contains(event.position())) { + auto new_sort_order = GSortOrder::Ascending; + if (model()->key_column() == m_pressed_column_header_index) + new_sort_order = model()->sort_order() == GSortOrder::Ascending + ? GSortOrder::Descending + : GSortOrder::Ascending; + model()->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(); + } + } +} + +void GAbstractColumnView::mousedown_event(GMouseEvent& event) +{ + if (!model()) + return; + + if (event.button() != GMouseButton::Left) + return; + + 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(event.position())) { + m_resizing_column = i; + m_in_column_resize = true; + m_column_resize_original_width = column_width(i); + m_column_resize_origin = event.position(); + return; + } + auto header_rect = this->header_rect(i); + auto column_metadata = model()->column_metadata(i); + if (header_rect.contains(event.position()) && column_metadata.sortable == GModel::ColumnMetadata::Sortable::True) { + m_pressed_column_header_index = i; + m_pressed_column_header_is_pressed = true; + update_headers(); + return; + } + } + return; + } + + auto index = index_at_event_position(event.position()); + if (!index.is_valid()) { + selection().clear(); + return; + } + if (event.modifiers() & Mod_Ctrl) + selection().toggle(index); + else + selection().set(index); +} + +GModelIndex GAbstractColumnView::index_at_event_position(const Point& position) const +{ + if (!model()) + return {}; + + auto adjusted_position = this->adjusted_position(position); + for (int row = 0, row_count = model()->row_count(); row < row_count; ++row) { + if (!row_rect(row).contains(adjusted_position)) + continue; + for (int column = 0, column_count = model()->column_count(); column < column_count; ++column) { + if (!content_rect(row, column).contains(adjusted_position)) + continue; + return model()->index(row, column); + } + return model()->index(row, 0); + } + return {}; +} + +int GAbstractColumnView::item_count() const +{ + if (!model()) + return 0; + return model()->row_count(); +} + +void GAbstractColumnView::keydown_event(GKeyEvent& event) +{ + if (!model()) + return; + auto& model = *this->model(); + if (event.key() == KeyCode::Key_Return) { + selection().for_each_index([this](auto& index) { + activate(index); + }); + return; + } + if (event.key() == KeyCode::Key_Up) { + GModelIndex new_index; + if (!selection().is_empty()) { + auto old_index = selection().first(); + new_index = model.index(old_index.row() - 1, old_index.column()); + } else { + new_index = model.index(0, 0); + } + if (model.is_valid(new_index)) { + selection().set(new_index); + scroll_into_view(new_index, Orientation::Vertical); + update(); + } + return; + } + if (event.key() == KeyCode::Key_Down) { + GModelIndex new_index; + if (!selection().is_empty()) { + auto old_index = selection().first(); + new_index = model.index(old_index.row() + 1, old_index.column()); + } else { + new_index = model.index(0, 0); + } + if (model.is_valid(new_index)) { + selection().set(new_index); + scroll_into_view(new_index, Orientation::Vertical); + update(); + } + return; + } + if (event.key() == KeyCode::Key_PageUp) { + int items_per_page = visible_content_rect().height() / item_height(); + auto old_index = selection().first(); + auto new_index = model.index(max(0, old_index.row() - items_per_page), old_index.column()); + if (model.is_valid(new_index)) { + selection().set(new_index); + scroll_into_view(new_index, Orientation::Vertical); + update(); + } + return; + } + if (event.key() == KeyCode::Key_PageDown) { + int items_per_page = visible_content_rect().height() / item_height(); + auto old_index = selection().first(); + auto new_index = model.index(min(model.row_count() - 1, old_index.row() + items_per_page), old_index.column()); + if (model.is_valid(new_index)) { + selection().set(new_index); + scroll_into_view(new_index, Orientation::Vertical); + update(); + } + return; + } + return GWidget::keydown_event(event); +} + +void GAbstractColumnView::scroll_into_view(const GModelIndex& index, Orientation orientation) +{ + auto rect = row_rect(index.row()).translated(0, -header_height()); + GScrollableWidget::scroll_into_view(rect, orientation); +} + +void GAbstractColumnView::doubleclick_event(GMouseEvent& event) +{ + if (!model()) + return; + if (event.button() == GMouseButton::Left) { + if (event.y() < header_height()) + return; + if (!selection().is_empty()) { + if (is_editable()) { + begin_editing(selection().first()); + } else { + selection().for_each_index([this](auto& index) { + activate(index); + }); + } + } + } +} + +void GAbstractColumnView::context_menu_event(GContextMenuEvent& event) +{ + if (!model()) + return; + if (event.position().y() < header_height()) { + ensure_header_context_menu().popup(event.screen_position()); + return; + } + + auto index = index_at_event_position(event.position()); + if (index.is_valid()) { + if (!selection().contains(index)) + selection().set(index); + } else { + selection().clear(); + } + if (on_context_menu_request) + on_context_menu_request(index, event); +} + +void GAbstractColumnView::leave_event(CEvent&) +{ + window()->set_override_cursor(GStandardCursor::None); + set_hovered_header_index(-1); +} + +Rect GAbstractColumnView::content_rect(int row, int column) const +{ + auto row_rect = this->row_rect(row); + int x = 0; + for (int i = 0; i < column; ++i) + x += column_width(i) + horizontal_padding() * 2; + + return { row_rect.x() + x, row_rect.y(), column_width(column) + horizontal_padding() * 2, item_height() }; +} + +Rect GAbstractColumnView::content_rect(const GModelIndex& index) const +{ + return content_rect(index.row(), index.column()); +} + +Rect GAbstractColumnView::row_rect(int item_index) const +{ + return { 0, header_height() + (item_index * item_height()), max(content_size().width(), width()), item_height() }; +} + +Point GAbstractColumnView::adjusted_position(const Point& position) const +{ + return position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness()); +} + +void GAbstractColumnView::did_update_model() +{ + GAbstractView::did_update_model(); + update_column_sizes(); + update_content_size(); + update(); +} diff --git a/Libraries/LibGUI/GAbstractColumnView.h b/Libraries/LibGUI/GAbstractColumnView.h new file mode 100644 index 0000000000..d4641b9398 --- /dev/null +++ b/Libraries/LibGUI/GAbstractColumnView.h @@ -0,0 +1,98 @@ +#pragma once + +#include <LibGUI/GAbstractView.h> + +class GPainter; + +// FIXME: Rename this to something without "table cell" in the name. +class GTableCellPaintingDelegate { +public: + virtual ~GTableCellPaintingDelegate() {} + + virtual void paint(GPainter&, const Rect&, const GModel&, const GModelIndex&) = 0; +}; + +class GAbstractColumnView : public GAbstractView { +public: + int item_height() const { return 16; } + + bool alternating_row_colors() const { return m_alternating_row_colors; } + void set_alternating_row_colors(bool b) { m_alternating_row_colors = b; } + + int header_height() const { return m_headers_visible ? 16 : 0; } + + bool headers_visible() const { return m_headers_visible; } + void set_headers_visible(bool headers_visible) { m_headers_visible = headers_visible; } + + bool is_column_hidden(int) const; + void set_column_hidden(int, bool); + + void set_size_columns_to_fit_content(bool b) { m_size_columns_to_fit_content = b; } + bool size_columns_to_fit_content() const { return m_size_columns_to_fit_content; } + + void set_cell_painting_delegate(int column, OwnPtr<GTableCellPaintingDelegate>&&); + + int horizontal_padding() const { return m_horizontal_padding; } + + Point adjusted_position(const Point&) const; + GModelIndex index_at_event_position(const Point&) const; + + virtual Rect content_rect(const GModelIndex&) const override; + Rect content_rect(int row, int column) const; + Rect row_rect(int item_index) const; + + void scroll_into_view(const GModelIndex&, Orientation); + +protected: + virtual ~GAbstractColumnView() override; + explicit GAbstractColumnView(GWidget* parent); + + virtual void did_update_model() override; + virtual void mouseup_event(GMouseEvent&) override; + virtual void mousedown_event(GMouseEvent&) override; + virtual void mousemove_event(GMouseEvent&) override; + virtual void doubleclick_event(GMouseEvent&) override; + virtual void keydown_event(GKeyEvent&) override; + virtual void leave_event(CEvent&) override; + virtual void context_menu_event(GContextMenuEvent&) override; + + void paint_headers(GPainter&); + Rect header_rect(int column) const; + + static const Font& header_font(); + void update_headers(); + void set_hovered_header_index(int); + + struct ColumnData { + int width { 0 }; + bool has_initialized_width { false }; + bool visibility { true }; + RefPtr<GAction> visibility_action; + OwnPtr<GTableCellPaintingDelegate> cell_painting_delegate; + }; + ColumnData& column_data(int column) const; + + mutable Vector<ColumnData> m_column_data; + + GMenu& ensure_header_context_menu(); + RefPtr<GMenu> m_header_context_menu; + + Rect column_resize_grabbable_rect(int) const; + int column_width(int) const; + void update_content_size(); + void update_column_sizes(); + int item_count() const; + +private: + bool m_headers_visible { true }; + bool m_size_columns_to_fit_content { false }; + bool m_in_column_resize { false }; + bool m_alternating_row_colors { true }; + int m_horizontal_padding { 5 }; + Point m_column_resize_origin; + int m_column_resize_original_width { 0 }; + int m_resizing_column { -1 }; + int m_pressed_column_header_index { -1 }; + bool m_pressed_column_header_is_pressed { false }; + int m_hovered_column_header_index { -1 }; +}; diff --git a/Libraries/LibGUI/GAbstractView.h b/Libraries/LibGUI/GAbstractView.h index 218d305bb7..c4293642bd 100644 --- a/Libraries/LibGUI/GAbstractView.h +++ b/Libraries/LibGUI/GAbstractView.h @@ -12,9 +12,6 @@ class GAbstractView : public GScrollableWidget { friend class GModel; public: - explicit GAbstractView(GWidget* parent); - virtual ~GAbstractView() override; - void set_model(RefPtr<GModel>&&); GModel* model() { return m_model.ptr(); } const GModel* model() const { return m_model.ptr(); } @@ -48,6 +45,9 @@ public: NonnullRefPtr<Font> font_for_index(const GModelIndex&) const; protected: + explicit GAbstractView(GWidget* parent); + virtual ~GAbstractView() override; + virtual void did_scroll() override; void activate(const GModelIndex&); void update_edit_widget_position(); diff --git a/Libraries/LibGUI/GTableView.cpp b/Libraries/LibGUI/GTableView.cpp index a37d62025a..c663ae2319 100644 --- a/Libraries/LibGUI/GTableView.cpp +++ b/Libraries/LibGUI/GTableView.cpp @@ -9,289 +9,15 @@ #include <LibGUI/GTextBox.h> #include <LibGUI/GWindow.h> -static const int minimum_column_width = 2; - GTableView::GTableView(GWidget* parent) - : GAbstractView(parent) + : GAbstractColumnView(parent) { - set_frame_shape(FrameShape::Container); - set_frame_shadow(FrameShadow::Sunken); - set_frame_thickness(2); - - set_should_hide_unnecessary_scrollbars(true); } GTableView::~GTableView() { } -void GTableView::update_column_sizes() -{ - if (!m_size_columns_to_fit_content) - return; - - if (!model()) - return; - - auto& model = *this->model(); - int column_count = model.column_count(); - int row_count = model.row_count(); - - for (int column = 0; column < column_count; ++column) { - if (is_column_hidden(column)) - continue; - int header_width = header_font().width(model.column_name(column)); - int column_width = header_width; - for (int row = 0; row < row_count; ++row) { - auto cell_data = model.data(model.index(row, column)); - int cell_width = 0; - if (cell_data.is_bitmap()) { - cell_width = cell_data.as_bitmap().width(); - } else { - cell_width = font().width(cell_data.to_string()); - } - 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; - } -} - -void GTableView::update_content_size() -{ - if (!model()) - return set_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)) - 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() }); -} - -void GTableView::did_update_model() -{ - GAbstractView::did_update_model(); - update_column_sizes(); - update_content_size(); - update(); -} - -Rect GTableView::content_rect(int row, int column) const -{ - auto row_rect = this->row_rect(row); - int x = 0; - for (int i = 0; i < column; ++i) - x += column_width(i) + horizontal_padding() * 2; - - return { row_rect.x() + x, row_rect.y(), column_width(column) + horizontal_padding() * 2, item_height() }; -} - -Rect GTableView::content_rect(const GModelIndex& index) const -{ - return content_rect(index.row(), index.column()); -} - -Rect GTableView::row_rect(int item_index) const -{ - return { 0, header_height() + (item_index * item_height()), max(content_size().width(), width()), item_height() }; -} - -int GTableView::column_width(int column_index) const -{ - if (!model()) - return 0; - auto& column_data = this->column_data(column_index); - if (!column_data.has_initialized_width) { - ASSERT(!m_size_columns_to_fit_content); - column_data.has_initialized_width = true; - column_data.width = model()->column_metadata(column_index).preferred_width; - } - return column_data.width; -} - -Rect GTableView::header_rect(int column_index) 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() }; -} - -Point GTableView::adjusted_position(const Point& position) const -{ - return position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness()); -} - -Rect GTableView::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() }; -} - -void GTableView::mousedown_event(GMouseEvent& event) -{ - if (!model()) - return; - - if (event.button() != GMouseButton::Left) - return; - - 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(event.position())) { - m_resizing_column = i; - m_in_column_resize = true; - m_column_resize_original_width = column_width(i); - m_column_resize_origin = event.position(); - return; - } - auto header_rect = this->header_rect(i); - auto column_metadata = model()->column_metadata(i); - if (header_rect.contains(event.position()) && column_metadata.sortable == GModel::ColumnMetadata::Sortable::True) { - m_pressed_column_header_index = i; - m_pressed_column_header_is_pressed = true; - update_headers(); - return; - } - } - return; - } - - auto index = index_at_event_position(event.position()); - if (!index.is_valid()) { - selection().clear(); - return; - } - if (event.modifiers() & Mod_Ctrl) - selection().toggle(index); - else - selection().set(index); -} - -GModelIndex GTableView::index_at_event_position(const Point& position) const -{ - if (!model()) - return {}; - - auto adjusted_position = this->adjusted_position(position); - for (int row = 0, row_count = model()->row_count(); row < row_count; ++row) { - if (!row_rect(row).contains(adjusted_position)) - continue; - for (int column = 0, column_count = model()->column_count(); column < column_count; ++column) { - if (!content_rect(row, column).contains(adjusted_position)) - continue; - return model()->index(row, column); - } - return model()->index(row, 0); - } - return {}; -} -void GTableView::set_hovered_header_index(int index) -{ - if (m_hovered_column_header_index == index) - return; - m_hovered_column_header_index = index; - update_headers(); -} - -void GTableView::mousemove_event(GMouseEvent& event) -{ - if (!model()) - return; - - if (m_in_column_resize) { - auto delta = event.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; - dbg() << "New column 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(event.position())) { - if (!m_pressed_column_header_is_pressed) - update_headers(); - m_pressed_column_header_is_pressed = true; - } else { - 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(event.position())) { - window()->set_override_cursor(GStandardCursor::ResizeHorizontal); - set_hovered_header_index(-1); - return; - } - if (header_rect(i).contains(event.position())) { - set_hovered_header_index(i); - found_hovered_header = true; - } - } - if (!found_hovered_header) - set_hovered_header_index(-1); - } - window()->set_override_cursor(GStandardCursor::None); -} - -void GTableView::mouseup_event(GMouseEvent& event) -{ - auto adjusted_position = this->adjusted_position(event.position()); - if (event.button() == GMouseButton::Left) { - if (m_in_column_resize) { - if (!column_resize_grabbable_rect(m_resizing_column).contains(adjusted_position)) - window()->set_override_cursor(GStandardCursor::None); - m_in_column_resize = false; - } - if (m_pressed_column_header_index != -1) { - auto header_rect = this->header_rect(m_pressed_column_header_index); - if (header_rect.contains(event.position())) { - auto new_sort_order = GSortOrder::Ascending; - if (model()->key_column() == m_pressed_column_header_index) - new_sort_order = model()->sort_order() == GSortOrder::Ascending - ? GSortOrder::Descending - : GSortOrder::Ascending; - model()->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(); - } - } -} - void GTableView::paint_event(GPaintEvent& event) { GFrame::paint_event(event); @@ -385,228 +111,3 @@ void GTableView::paint_event(GPaintEvent& event) if (headers_visible()) paint_headers(painter); } - -void GTableView::paint_headers(Painter& painter) -{ - int exposed_width = max(content_size().width(), width()); - painter.fill_rect({ 0, 0, exposed_width, header_height() }, Color::WarmGray); - painter.draw_line({ 0, 0 }, { exposed_width - 1, 0 }, Color::White); - painter.draw_line({ 0, header_height() - 1 }, { exposed_width - 1, header_height() - 1 }, Color::MidGray); - 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 = model()->key_column() == column_index; - Rect 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()->column_metadata(column_index).sortable == GModel::ColumnMetadata::Sortable::True; - StylePainter::paint_button(painter, cell_rect, ButtonStyle::Normal, pressed, hovered); - String text; - if (is_key_column) { - StringBuilder builder; - builder.append(model()->column_name(column_index)); - auto sort_order = model()->sort_order(); - if (sort_order == GSortOrder::Ascending) - builder.append(" \xc3\xb6"); - else if (sort_order == GSortOrder::Descending) - builder.append(" \xc3\xb7"); - text = builder.to_string(); - } else { - text = model()->column_name(column_index); - } - auto text_rect = cell_rect.translated(horizontal_padding(), 0); - if (pressed) - text_rect.move_by(1, 1); - painter.draw_text(text_rect, text, header_font(), TextAlignment::CenterLeft, Color::Black); - x_offset += column_width + horizontal_padding() * 2; - } -} - -int GTableView::item_count() const -{ - if (!model()) - return 0; - return model()->row_count(); -} - -void GTableView::keydown_event(GKeyEvent& event) -{ - if (!model()) - return; - auto& model = *this->model(); - if (event.key() == KeyCode::Key_Return) { - selection().for_each_index([this](auto& index) { - activate(index); - }); - return; - } - if (event.key() == KeyCode::Key_Up) { - GModelIndex new_index; - if (!selection().is_empty()) { - auto old_index = selection().first(); - new_index = model.index(old_index.row() - 1, old_index.column()); - } else { - new_index = model.index(0, 0); - } - if (model.is_valid(new_index)) { - selection().set(new_index); - scroll_into_view(new_index, Orientation::Vertical); - update(); - } - return; - } - if (event.key() == KeyCode::Key_Down) { - GModelIndex new_index; - if (!selection().is_empty()) { - auto old_index = selection().first(); - new_index = model.index(old_index.row() + 1, old_index.column()); - } else { - new_index = model.index(0, 0); - } - if (model.is_valid(new_index)) { - selection().set(new_index); - scroll_into_view(new_index, Orientation::Vertical); - update(); - } - return; - } - if (event.key() == KeyCode::Key_PageUp) { - int items_per_page = visible_content_rect().height() / item_height(); - auto old_index = selection().first(); - auto new_index = model.index(max(0, old_index.row() - items_per_page), old_index.column()); - if (model.is_valid(new_index)) { - selection().set(new_index); - scroll_into_view(new_index, Orientation::Vertical); - update(); - } - return; - } - if (event.key() == KeyCode::Key_PageDown) { - int items_per_page = visible_content_rect().height() / item_height(); - auto old_index = selection().first(); - auto new_index = model.index(min(model.row_count() - 1, old_index.row() + items_per_page), old_index.column()); - if (model.is_valid(new_index)) { - selection().set(new_index); - scroll_into_view(new_index, Orientation::Vertical); - update(); - } - return; - } - return GWidget::keydown_event(event); -} - -void GTableView::scroll_into_view(const GModelIndex& index, Orientation orientation) -{ - auto rect = row_rect(index.row()).translated(0, -header_height()); - GScrollableWidget::scroll_into_view(rect, orientation); -} - -GTableView::ColumnData& GTableView::column_data(int column) const -{ - if (column >= m_column_data.size()) - m_column_data.resize(column + 1); - return m_column_data.at(column); -} - -bool GTableView::is_column_hidden(int column) const -{ - return !column_data(column).visibility; -} - -void GTableView::set_column_hidden(int column, bool hidden) -{ - auto& column_data = this->column_data(column); - if (column_data.visibility == !hidden) - return; - column_data.visibility = !hidden; - update_content_size(); - update(); -} - -void GTableView::doubleclick_event(GMouseEvent& event) -{ - if (!model()) - return; - if (event.button() == GMouseButton::Left) { - if (event.y() < header_height()) - return; - if (!selection().is_empty()) { - if (is_editable()) { - begin_editing(selection().first()); - } else { - selection().for_each_index([this](auto& index) { - activate(index); - }); - } - } - } -} - -GMenu& GTableView::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 = GMenu::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 = GAction::create(name, [this, column](GAction& action) { - action.set_checked(!action.is_checked()); - set_column_hidden(column, !action.is_checked()); - }); - column_data.visibility_action->set_checkable(true); - column_data.visibility_action->set_checked(true); - - m_header_context_menu->add_action(*column_data.visibility_action); - } - } - return *m_header_context_menu; -} - -void GTableView::context_menu_event(GContextMenuEvent& event) -{ - if (!model()) - return; - if (event.position().y() < header_height()) { - ensure_header_context_menu().popup(event.screen_position()); - return; - } - - auto index = index_at_event_position(event.position()); - if (index.is_valid()) { - if (!selection().contains(index)) - selection().set(index); - } else { - selection().clear(); - } - if (on_context_menu_request) - on_context_menu_request(index, event); -} - -void GTableView::leave_event(CEvent&) -{ - window()->set_override_cursor(GStandardCursor::None); - set_hovered_header_index(-1); -} - -const Font& GTableView::header_font() -{ - return Font::default_bold_font(); -} - -void GTableView::set_cell_painting_delegate(int column, OwnPtr<GTableCellPaintingDelegate>&& delegate) -{ - column_data(column).cell_painting_delegate = move(delegate); -} - -void GTableView::update_headers() -{ - Rect rect { 0, 0, frame_inner_rect().width(), header_height() }; - rect.move_by(frame_thickness(), frame_thickness()); - update(rect); -} diff --git a/Libraries/LibGUI/GTableView.h b/Libraries/LibGUI/GTableView.h index 89d9a62e7a..6ec5bda424 100644 --- a/Libraries/LibGUI/GTableView.h +++ b/Libraries/LibGUI/GTableView.h @@ -2,101 +2,16 @@ #include <AK/Function.h> #include <AK/HashMap.h> -#include <LibGUI/GAbstractView.h> +#include <LibGUI/GAbstractColumnView.h> #include <LibGUI/GModel.h> -class GPainter; -class GScrollBar; -class Painter; - -class GTableCellPaintingDelegate { -public: - virtual ~GTableCellPaintingDelegate() {} - - virtual void paint(GPainter&, const Rect&, const GModel&, const GModelIndex&) = 0; -}; - -class GTableView : public GAbstractView { +class GTableView : public GAbstractColumnView { C_OBJECT(GTableView) public: virtual ~GTableView() override; - int header_height() const { return m_headers_visible ? 16 : 0; } - int item_height() const { return 16; } - - bool headers_visible() const { return m_headers_visible; } - void set_headers_visible(bool headers_visible) { m_headers_visible = headers_visible; } - - bool alternating_row_colors() const { return m_alternating_row_colors; } - void set_alternating_row_colors(bool b) { m_alternating_row_colors = b; } - - int content_width() const; - int horizontal_padding() const { return m_horizontal_padding; } - - void scroll_into_view(const GModelIndex&, Orientation); - - bool is_column_hidden(int) const; - void set_column_hidden(int, bool); - - void set_size_columns_to_fit_content(bool b) { m_size_columns_to_fit_content = b; } - bool size_columns_to_fit_content() const { return m_size_columns_to_fit_content; } - - Point adjusted_position(const Point&) const; - - virtual Rect content_rect(const GModelIndex&) const override; - - void set_cell_painting_delegate(int column, OwnPtr<GTableCellPaintingDelegate>&&); - protected: explicit GTableView(GWidget* parent); - virtual void did_update_model() override; virtual void paint_event(GPaintEvent&) override; - virtual void mousedown_event(GMouseEvent&) override; - virtual void mousemove_event(GMouseEvent&) override; - virtual void mouseup_event(GMouseEvent&) override; - virtual void doubleclick_event(GMouseEvent&) override; - virtual void keydown_event(GKeyEvent&) override; - virtual void leave_event(CEvent&) override; - virtual void context_menu_event(GContextMenuEvent&) override; - - GModelIndex index_at_event_position(const Point&) const; - - Rect content_rect(int row, int column) const; - void paint_headers(Painter&); - int item_count() const; - Rect row_rect(int item_index) const; - Rect header_rect(int) const; - Rect column_resize_grabbable_rect(int) const; - int column_width(int) const; - void update_content_size(); - void update_column_sizes(); - static const Font& header_font(); - void update_headers(); - void set_hovered_header_index(int); - - struct ColumnData { - int width { 0 }; - bool has_initialized_width { false }; - bool visibility { true }; - RefPtr<GAction> visibility_action; - OwnPtr<GTableCellPaintingDelegate> cell_painting_delegate; - }; - ColumnData& column_data(int column) const; - - mutable Vector<ColumnData> m_column_data; - int m_horizontal_padding { 5 }; - bool m_headers_visible { true }; - bool m_alternating_row_colors { true }; - bool m_size_columns_to_fit_content { false }; - bool m_in_column_resize { false }; - Point m_column_resize_origin; - int m_column_resize_original_width { 0 }; - int m_resizing_column { -1 }; - int m_pressed_column_header_index { -1 }; - bool m_pressed_column_header_is_pressed { false }; - int m_hovered_column_header_index { -1 }; - - GMenu& ensure_header_context_menu(); - RefPtr<GMenu> m_header_context_menu; }; diff --git a/Libraries/LibGUI/Makefile b/Libraries/LibGUI/Makefile index 0d87255745..5c72b0c005 100644 --- a/Libraries/LibGUI/Makefile +++ b/Libraries/LibGUI/Makefile @@ -36,6 +36,7 @@ OBJS = \ GDesktop.o \ GProgressBar.o \ GAbstractView.o \ + GAbstractColumnView.o \ GItemView.o \ GIcon.o \ GFrame.o \ |