ladybird/Libraries/LibGUI/ItemView.cpp
Andreas Kling 8e4751a963 LibGUI: Add a way for models to update without invalidating indexes
This is really just a workaround to keep SystemMonitor's process table
working right wrt selection retention during resorts (while also doing
full index invalidation on things like ProfileViewer inversion.)

It's starting to feel like the model abstraction is not super great
and we'll need a better approach if we want to actually build some more
dynamic functionality into our views.
2020-04-12 12:03:33 +02:00

451 lines
16 KiB
C++

/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <AK/StringBuilder.h>
#include <Kernel/KeyCode.h>
#include <LibGUI/DragOperation.h>
#include <LibGUI/ItemView.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGfx/Palette.h>
//#define DRAGDROP_DEBUG
namespace GUI {
ItemView::ItemView()
{
set_background_role(ColorRole::Base);
set_foreground_role(ColorRole::BaseText);
horizontal_scrollbar().set_visible(false);
}
ItemView::~ItemView()
{
}
void ItemView::select_all()
{
selection().clear();
for (int item_index = 0; item_index < item_count(); ++item_index) {
auto index = model()->index(item_index, model_column());
selection().add(index);
}
}
void ItemView::scroll_into_view(const ModelIndex& index, Orientation orientation)
{
ScrollableWidget::scroll_into_view(item_rect(index.row()), orientation);
}
void ItemView::resize_event(ResizeEvent& event)
{
AbstractView::resize_event(event);
update_content_size();
}
void ItemView::did_update_model(unsigned flags)
{
AbstractView::did_update_model(flags);
update_content_size();
update();
}
void ItemView::update_content_size()
{
if (!model())
return set_content_size({});
m_visual_column_count = available_size().width() / effective_item_size().width();
if (m_visual_column_count)
m_visual_row_count = ceil_div(model()->row_count(), m_visual_column_count);
else
m_visual_row_count = 0;
int content_width = available_size().width();
int content_height = m_visual_row_count * effective_item_size().height();
set_content_size({ content_width, content_height });
}
Gfx::Rect ItemView::item_rect(int item_index) const
{
if (!m_visual_row_count || !m_visual_column_count)
return {};
int visual_row_index = item_index / m_visual_column_count;
int visual_column_index = item_index % m_visual_column_count;
return {
visual_column_index * effective_item_size().width(),
visual_row_index * effective_item_size().height(),
effective_item_size().width(),
effective_item_size().height()
};
}
Vector<int> ItemView::items_intersecting_rect(const Gfx::Rect& rect) const
{
ASSERT(model());
const auto& column_metadata = model()->column_metadata(model_column());
const auto& font = column_metadata.font ? *column_metadata.font : this->font();
Vector<int> item_indexes;
for (int item_index = 0; item_index < item_count(); ++item_index) {
Gfx::Rect item_rect;
Gfx::Rect icon_rect;
Gfx::Rect text_rect;
auto item_text = model()->data(model()->index(item_index, model_column()));
get_item_rects(item_index, font, item_text, item_rect, icon_rect, text_rect);
if (icon_rect.intersects(rect) || text_rect.intersects(rect))
item_indexes.append(item_index);
}
return item_indexes;
}
ModelIndex ItemView::index_at_event_position(const Gfx::Point& position) const
{
ASSERT(model());
// FIXME: Since all items are the same size, just compute the clicked item index
// instead of iterating over everything.
auto adjusted_position = this->adjusted_position(position);
const auto& column_metadata = model()->column_metadata(model_column());
const auto& font = column_metadata.font ? *column_metadata.font : this->font();
for (int item_index = 0; item_index < item_count(); ++item_index) {
Gfx::Rect item_rect;
Gfx::Rect icon_rect;
Gfx::Rect text_rect;
auto index = model()->index(item_index, model_column());
auto item_text = model()->data(index);
get_item_rects(item_index, font, item_text, item_rect, icon_rect, text_rect);
if (icon_rect.contains(adjusted_position) || text_rect.contains(adjusted_position))
return index;
}
return {};
}
Gfx::Point ItemView::adjusted_position(const Gfx::Point& position) const
{
return position.translated(0, vertical_scrollbar().value());
}
void ItemView::mousedown_event(MouseEvent& event)
{
if (!model())
return AbstractView::mousedown_event(event);
if (event.button() != MouseButton::Left)
return AbstractView::mousedown_event(event);
auto index = index_at_event_position(event.position());
if (index.is_valid()) {
// We might start dragging this item, but not rubber-banding.
return AbstractView::mousedown_event(event);
}
ASSERT(m_rubber_band_remembered_selection.is_empty());
if (event.modifiers() & Mod_Ctrl) {
selection().for_each_index([&](auto& index) {
m_rubber_band_remembered_selection.append(index);
});
} else {
selection().clear();
}
auto adjusted_position = this->adjusted_position(event.position());
m_might_drag = false;
m_rubber_banding = true;
m_rubber_band_origin = adjusted_position;
m_rubber_band_current = adjusted_position;
}
void ItemView::mouseup_event(MouseEvent& event)
{
if (m_rubber_banding && event.button() == MouseButton::Left) {
m_rubber_banding = false;
m_rubber_band_remembered_selection.clear();
update();
}
AbstractView::mouseup_event(event);
}
void ItemView::drag_move_event(DragEvent& event)
{
auto index = index_at_event_position(event.position());
ModelIndex new_drop_candidate_index;
if (index.is_valid()) {
bool acceptable = model()->accepts_drag(index, event.data_type());
#ifdef DRAGDROP_DEBUG
dbg() << "Drag of type '" << event.data_type() << "' moving over " << index << ", acceptable: " << acceptable;
#endif
if (acceptable)
new_drop_candidate_index = index;
}
if (m_drop_candidate_index != new_drop_candidate_index) {
m_drop_candidate_index = new_drop_candidate_index;
update();
}
event.accept();
}
void ItemView::mousemove_event(MouseEvent& event)
{
if (!model())
return AbstractView::mousemove_event(event);
if (m_rubber_banding) {
auto adjusted_position = this->adjusted_position(event.position());
if (m_rubber_band_current != adjusted_position) {
m_rubber_band_current = adjusted_position;
auto rubber_band_rect = Gfx::Rect::from_two_points(m_rubber_band_origin, m_rubber_band_current);
selection().clear();
for (auto item_index : items_intersecting_rect(rubber_band_rect)) {
selection().add(model()->index(item_index, model_column()));
}
if (event.modifiers() & Mod_Ctrl) {
for (auto stored_item : m_rubber_band_remembered_selection) {
selection().add(stored_item);
}
}
update();
return;
}
}
AbstractView::mousemove_event(event);
}
void ItemView::get_item_rects(int item_index, const Gfx::Font& font, const Variant& item_text, Gfx::Rect& item_rect, Gfx::Rect& icon_rect, Gfx::Rect& text_rect) const
{
item_rect = this->item_rect(item_index);
icon_rect = { 0, 0, 32, 32 };
icon_rect.center_within(item_rect);
icon_rect.move_by(0, -font.glyph_height() - 6);
text_rect = { 0, icon_rect.bottom() + 6 + 1, font.width(item_text.to_string()), font.glyph_height() };
text_rect.center_horizontally_within(item_rect);
text_rect.inflate(6, 4);
text_rect.intersect(item_rect);
}
void ItemView::second_paint_event(PaintEvent& event)
{
if (!m_rubber_banding)
return;
Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
auto rubber_band_rect = Gfx::Rect::from_two_points(m_rubber_band_origin, m_rubber_band_current);
painter.fill_rect(rubber_band_rect, parent_widget()->palette().rubber_band_fill());
painter.draw_rect(rubber_band_rect, parent_widget()->palette().rubber_band_border());
}
void ItemView::paint_event(PaintEvent& event)
{
Color widget_background_color = palette().color(background_role());
Frame::paint_event(event);
Painter painter(*this);
painter.add_clip_rect(widget_inner_rect());
painter.add_clip_rect(event.rect());
painter.fill_rect(event.rect(), widget_background_color);
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
auto column_metadata = model()->column_metadata(m_model_column);
const Gfx::Font& font = column_metadata.font ? *column_metadata.font : this->font();
for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
auto model_index = model()->index(item_index, m_model_column);
bool is_selected_item = selection().contains(model_index);
Color background_color;
if (is_selected_item) {
background_color = is_focused() ? palette().selection() : palette().inactive_selection();
} else {
background_color = widget_background_color;
}
auto icon = model()->data(model_index, Model::Role::Icon);
auto item_text = model()->data(model_index, Model::Role::Display);
Gfx::Rect item_rect;
Gfx::Rect icon_rect;
Gfx::Rect text_rect;
get_item_rects(item_index, font, item_text, item_rect, icon_rect, text_rect);
if (icon.is_icon()) {
if (auto bitmap = icon.as_icon().bitmap_for_size(icon_rect.width())) {
Gfx::Rect destination = bitmap->rect();
destination.center_within(icon_rect);
if (m_hovered_index.is_valid() && m_hovered_index == model_index) {
painter.blit_brightened(destination.location(), *bitmap, bitmap->rect());
} else {
painter.blit(destination.location(), *bitmap, bitmap->rect());
}
}
}
Color text_color;
if (is_selected_item)
text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text();
else
text_color = model()->data(model_index, Model::Role::ForegroundColor).to_color(palette().color(foreground_role()));
painter.fill_rect(text_rect, background_color);
painter.draw_text(text_rect, item_text.to_string(), font, Gfx::TextAlignment::Center, text_color, Gfx::TextElision::Right);
if (model_index == m_drop_candidate_index) {
// FIXME: This visualization is not great, as it's also possible to drop things on the text label..
painter.draw_rect(icon_rect.inflated(8, 8), palette().selection(), true);
}
};
}
int ItemView::item_count() const
{
if (!model())
return 0;
return model()->row_count();
}
void ItemView::keydown_event(KeyEvent& event)
{
if (!model())
return;
if (!m_visual_row_count || !m_visual_column_count)
return;
auto& model = *this->model();
if (event.key() == KeyCode::Key_Return) {
activate_selected();
return;
}
if (event.key() == KeyCode::Key_Home) {
auto 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_End) {
auto new_index = model.index(model.row_count() - 1, 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_Up) {
ModelIndex new_index;
if (!selection().is_empty()) {
auto old_index = selection().first();
new_index = model.index(old_index.row() - m_visual_column_count, 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) {
ModelIndex new_index;
if (!selection().is_empty()) {
auto old_index = selection().first();
new_index = model.index(old_index.row() + m_visual_column_count, 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_Left) {
ModelIndex 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_Right) {
ModelIndex 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() / effective_item_size().height()) * m_visual_column_count;
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() / effective_item_size().height()) * m_visual_column_count;
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 Widget::keydown_event(event);
}
}