Serenity Operating System
at master 484 lines 16 kB view raw
1/* 2 * Copyright (c) 2020, Sergey Bugaev <bugaevc@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * Copyright (c) 2022, networkException <networkexception@serenityos.org> 5 * 6 * SPDX-License-Identifier: BSD-2-Clause 7 */ 8 9#include <LibGUI/ColumnsView.h> 10#include <LibGUI/Model.h> 11#include <LibGUI/Painter.h> 12#include <LibGUI/Scrollbar.h> 13#include <LibGfx/CharacterBitmap.h> 14#include <LibGfx/Palette.h> 15 16namespace GUI { 17 18static constexpr Gfx::CharacterBitmap s_arrow_bitmap { 19 " " 20 " # " 21 " ## " 22 " ### " 23 " #### " 24 " ### " 25 " ## " 26 " # " 27 " "sv, 28 9, 9 29}; 30 31ColumnsView::ColumnsView() 32{ 33 set_fill_with_background_color(true); 34 set_background_role(ColorRole::Base); 35 set_foreground_role(ColorRole::BaseText); 36 m_columns.append({ {}, 0 }); 37} 38 39void ColumnsView::select_all() 40{ 41 Vector<Column> columns_for_selection; 42 selection().for_each_index([&](auto& index) { 43 for (auto& column : m_columns) { 44 if (column.parent_index == index.parent()) { 45 columns_for_selection.append(column); 46 return; 47 } 48 } 49 VERIFY_NOT_REACHED(); 50 }); 51 52 for (Column& column : columns_for_selection) { 53 int row_count = model()->row_count(column.parent_index); 54 for (int row = 0; row < row_count; row++) { 55 ModelIndex index = model()->index(row, m_model_column, column.parent_index); 56 selection().add(index); 57 } 58 } 59} 60 61void ColumnsView::second_paint_event(PaintEvent& event) 62{ 63 if (!m_rubber_banding) 64 return; 65 66 Painter painter(*this); 67 painter.add_clip_rect(event.rect()); 68 painter.add_clip_rect(widget_inner_rect()); 69 70 // Columns start rendering relative to the widget inner rect. We also account for horizontal scroll here. 71 int column_x = widget_inner_rect().left() - horizontal_scrollbar().value(); 72 for (auto const& column : m_columns) { 73 if (m_rubber_band_origin_column.parent_index == column.parent_index) 74 break; 75 column_x += column.width + 1; 76 } 77 78 // After walking all columns to the current one we get its bounds relative to the widget inner rect and scroll position. 79 auto column_left = column_x; 80 auto column_right = column_x + m_rubber_band_origin_column.width; 81 82 // The rubber band rect always stays inside the widget inner rect, the vertical component is handled by mousemove 83 auto rubber_band_left = clamp(column_left, widget_inner_rect().left(), widget_inner_rect().right() + 1); 84 auto rubber_band_right = clamp(column_right, widget_inner_rect().left(), widget_inner_rect().right() + 1); 85 86 auto rubber_band_rect = Gfx::IntRect::from_two_points({ rubber_band_left, m_rubber_band_origin }, { rubber_band_right, m_rubber_band_current }); 87 88 painter.fill_rect(rubber_band_rect, palette().rubber_band_fill()); 89 painter.draw_rect(rubber_band_rect, palette().rubber_band_border()); 90} 91 92void ColumnsView::paint_event(PaintEvent& event) 93{ 94 AbstractView::paint_event(event); 95 96 if (!model()) 97 return; 98 99 Painter painter(*this); 100 painter.add_clip_rect(frame_inner_rect()); 101 painter.add_clip_rect(event.rect()); 102 painter.translate(frame_thickness(), frame_thickness()); 103 painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value()); 104 105 int column_x = 0; 106 107 auto selection_color = is_focused() ? palette().selection() : palette().inactive_selection(); 108 109 for (size_t i = 0; i < m_columns.size(); i++) { 110 auto& column = m_columns[i]; 111 auto* next_column = i + 1 == m_columns.size() ? nullptr : &m_columns[i + 1]; 112 113 VERIFY(column.width > 0); 114 115 int row_count = model()->row_count(column.parent_index); 116 for (int row = 0; row < row_count; row++) { 117 ModelIndex index = model()->index(row, m_model_column, column.parent_index); 118 VERIFY(index.is_valid()); 119 120 bool is_selected_row = selection().contains(index); 121 122 Color background_color = palette().color(background_role()); 123 Color text_color = palette().color(foreground_role()); 124 125 if (next_column != nullptr && next_column->parent_index == index) { 126 background_color = palette().inactive_selection(); 127 text_color = palette().inactive_selection_text(); 128 } 129 130 if (is_selected_row) { 131 background_color = selection_color; 132 text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text(); 133 } 134 135 Gfx::IntRect row_rect { column_x, row * item_height(), column.width, item_height() }; 136 137 if (m_edit_index.row() != row) 138 painter.fill_rect(row_rect, background_color); 139 140 auto icon = index.data(ModelRole::Icon); 141 Gfx::IntRect icon_rect = { column_x + icon_spacing(), 0, icon_size(), icon_size() }; 142 icon_rect.center_vertically_within(row_rect); 143 if (icon.is_icon()) { 144 if (auto* bitmap = icon.as_icon().bitmap_for_size(icon_size())) { 145 if (is_selected_row) { 146 auto tint = selection_color.with_alpha(100); 147 painter.blit_filtered(icon_rect.location(), *bitmap, bitmap->rect(), [&](auto src) { return src.blend(tint); }); 148 } else if (m_hovered_index.is_valid() && m_hovered_index.parent() == index.parent() && m_hovered_index.row() == index.row()) { 149 painter.blit_brightened(icon_rect.location(), *bitmap, bitmap->rect()); 150 } else { 151 auto opacity = index.data(ModelRole::IconOpacity).as_float_or(1.0f); 152 painter.blit(icon_rect.location(), *bitmap, bitmap->rect(), opacity); 153 } 154 } 155 } 156 157 Gfx::IntRect text_rect = { 158 icon_rect.right() + 1 + icon_spacing(), row * item_height(), 159 column.width - icon_spacing() - icon_size() - icon_spacing() - icon_spacing() - static_cast<int>(s_arrow_bitmap.width()) - icon_spacing(), item_height() 160 }; 161 draw_item_text(painter, index, is_selected_row, text_rect, index.data().to_deprecated_string(), font_for_index(index), Gfx::TextAlignment::CenterLeft, Gfx::TextElision::None); 162 163 if (is_focused() && index == cursor_index()) { 164 painter.draw_rect(row_rect, palette().color(background_role())); 165 painter.draw_focus_rect(row_rect, palette().focus_outline()); 166 } 167 168 if (has_pending_drop() && index == drop_candidate_index()) { 169 painter.draw_rect(row_rect, palette().selection(), true); 170 } 171 172 bool expandable = model()->row_count(index) > 0; 173 if (expandable) { 174 Gfx::IntRect arrow_rect = { 175 text_rect.right() + 1 + icon_spacing(), 0, 176 s_arrow_bitmap.width(), s_arrow_bitmap.height() 177 }; 178 arrow_rect.center_vertically_within(row_rect); 179 painter.draw_bitmap(arrow_rect.location(), s_arrow_bitmap, text_color); 180 } 181 } 182 183 int separator_height = content_size().height(); 184 if (height() > separator_height) 185 separator_height = height(); 186 painter.draw_line({ column_x + column.width, 0 }, { column_x + column.width, separator_height }, palette().button()); 187 column_x += column.width + column_separator_width(); 188 } 189} 190 191void ColumnsView::push_column(ModelIndex const& parent_index) 192{ 193 VERIFY(model()); 194 195 // Drop columns at the end. 196 ModelIndex grandparent = model()->parent_index(parent_index); 197 for (int i = m_columns.size() - 1; i > 0; i--) { 198 if (m_columns[i].parent_index == grandparent) 199 break; 200 m_columns.shrink(i); 201 dbgln("Dropping column {}", i); 202 } 203 204 // Add the new column. 205 dbgln("Adding a new column"); 206 m_columns.append({ parent_index, 0 }); 207 update_column_sizes(); 208 209 // FIXME: Find a way not to jump the view so much when changing folders within the same directory. 210 scroll_to_right(); 211 212 update(); 213} 214 215void ColumnsView::update_column_sizes() 216{ 217 if (!model()) 218 return; 219 220 int total_width = 0; 221 int total_height = 0; 222 223 for (auto& column : m_columns) { 224 int row_count = model()->row_count(column.parent_index); 225 226 int column_height = row_count * item_height(); 227 if (column_height > total_height) 228 total_height = column_height; 229 230 column.width = 10; 231 for (int row = 0; row < row_count; row++) { 232 ModelIndex index = model()->index(row, m_model_column, column.parent_index); 233 VERIFY(index.is_valid()); 234 auto text = index.data().to_deprecated_string(); 235 int row_width = icon_spacing() + icon_size() + icon_spacing() + font().width(text) + icon_spacing() + s_arrow_bitmap.width() + icon_spacing(); 236 if (row_width > column.width) 237 column.width = row_width; 238 } 239 total_width += column.width + column_separator_width(); 240 } 241 242 // "Hide" last separator behind a window frame. 243 total_width -= column_separator_width(); 244 245 set_content_size({ total_width, total_height }); 246} 247 248Optional<ColumnsView::Column> ColumnsView::column_at_event_position(Gfx::IntPoint position) const 249{ 250 if (!model()) 251 return {}; 252 253 int column_x = 0; 254 255 for (auto const& column : m_columns) { 256 if (position.x() < column_x) 257 break; 258 if (position.x() > column_x + column.width) { 259 column_x += column.width + column_separator_width(); 260 continue; 261 } 262 263 return column; 264 } 265 266 return {}; 267} 268 269void ColumnsView::select_range(ModelIndex const& index) 270{ 271 auto min_row = min(selection_start_index().row(), index.row()); 272 auto max_row = max(selection_start_index().row(), index.row()); 273 auto parent = index.parent(); 274 275 clear_selection(); 276 for (auto row = min_row; row <= max_row; ++row) { 277 auto new_index = model()->index(row, m_model_column, parent); 278 if (new_index.is_valid()) 279 toggle_selection(new_index); 280 } 281} 282 283ModelIndex ColumnsView::index_at_event_position_in_column(Gfx::IntPoint position, Column const& column) const 284{ 285 int row = position.y() / item_height(); 286 int row_count = model()->row_count(column.parent_index); 287 if (row >= row_count) 288 return {}; 289 290 return model()->index(row, m_model_column, column.parent_index); 291} 292 293ModelIndex ColumnsView::index_at_event_position(Gfx::IntPoint widget_position) const 294{ 295 auto position = to_content_position(widget_position); 296 auto const& column = column_at_event_position(position); 297 if (!column.has_value()) 298 return {}; 299 300 return index_at_event_position_in_column(position, *column); 301} 302 303void ColumnsView::mousedown_event(MouseEvent& event) 304{ 305 AbstractView::mousedown_event(event); 306 307 if (!model()) 308 return; 309 310 if (event.button() != MouseButton::Primary) 311 return; 312 313 auto position = to_content_position(event.position()); 314 auto column = column_at_event_position(position); 315 if (!column.has_value()) 316 return; 317 318 auto index = index_at_event_position_in_column(position, *column); 319 if (index.is_valid() && !(event.modifiers() & Mod_Ctrl)) { 320 if (model()->row_count(index)) { 321 auto is_index_already_open = m_columns.first_matching([&](auto& column) { return column.parent_index == index; }).has_value(); 322 if (is_index_already_open) { 323 set_cursor(index, SelectionUpdate::Set); 324 } else { 325 push_column(index); 326 } 327 } 328 return; 329 } 330 331 if (selection_mode() == SelectionMode::MultiSelection) { 332 m_rubber_banding = true; 333 m_rubber_band_origin_column = *column; 334 m_rubber_band_origin = position.y(); 335 m_rubber_band_current = position.y(); 336 } 337} 338 339void ColumnsView::mousemove_event(MouseEvent& event) 340{ 341 if (m_rubber_banding) { 342 m_rubber_band_current = clamp(event.position().y(), widget_inner_rect().top(), widget_inner_rect().bottom() + 1); 343 344 auto parent = m_rubber_band_origin_column.parent_index; 345 int row_count = model()->row_count(parent); 346 347 clear_selection(); 348 349 set_suppress_update_on_selection_change(true); 350 351 for (int row = 0; row < row_count; row++) { 352 auto index = model()->index(row, m_model_column, parent); 353 VERIFY(index.is_valid()); 354 355 int row_top = row * item_height(); 356 int row_bottom = row * item_height() + item_height(); 357 358 if ((m_rubber_band_origin > row_top && m_rubber_band_current < row_top) || (m_rubber_band_origin > row_bottom && m_rubber_band_current < row_bottom)) { 359 add_selection(index); 360 } 361 } 362 363 set_suppress_update_on_selection_change(false); 364 365 update(); 366 } 367 368 AbstractView::mousemove_event(event); 369} 370 371void ColumnsView::mouseup_event(MouseEvent& event) 372{ 373 if (m_rubber_banding && event.button() == MouseButton::Primary) { 374 m_rubber_banding = false; 375 update(); 376 } 377} 378 379void ColumnsView::model_did_update(unsigned flags) 380{ 381 AbstractView::model_did_update(flags); 382 383 // FIXME: Don't drop the columns on minor updates. 384 m_columns.clear(); 385 m_columns.append({ {}, 0 }); 386 387 update_column_sizes(); 388 update(); 389} 390 391void ColumnsView::move_cursor(CursorMovement movement, SelectionUpdate selection_update) 392{ 393 if (!model()) 394 return; 395 auto& model = *this->model(); 396 if (!cursor_index().is_valid()) { 397 set_cursor(model.index(0, m_model_column, {}), SelectionUpdate::Set); 398 return; 399 } 400 401 ModelIndex new_index; 402 auto cursor_parent = model.parent_index(cursor_index()); 403 404 switch (movement) { 405 case CursorMovement::Up: { 406 int row = cursor_index().row() > 0 ? cursor_index().row() - 1 : 0; 407 new_index = model.index(row, cursor_index().column(), cursor_parent); 408 break; 409 } 410 case CursorMovement::Down: { 411 int row = cursor_index().row() + 1; 412 new_index = model.index(row, cursor_index().column(), cursor_parent); 413 break; 414 } 415 case CursorMovement::Left: 416 new_index = cursor_parent; 417 break; 418 case CursorMovement::Right: { 419 // Don't reset columns if one already exists. 420 auto maybe_column = m_columns.first_matching([&](auto& column) { return model.parent_index(column.parent_index) == cursor_index(); }); 421 if (maybe_column.has_value()) { 422 new_index = maybe_column->parent_index; 423 break; 424 } 425 426 new_index = model.index(0, m_model_column, cursor_index()); 427 if (model.is_within_range(new_index)) { 428 if (model.is_within_range(cursor_index())) 429 push_column(cursor_index()); 430 update(); 431 } 432 break; 433 } 434 default: 435 break; 436 } 437 438 if (new_index.is_valid()) 439 set_cursor(new_index, selection_update); 440} 441 442Gfx::IntRect ColumnsView::index_content_rect(ModelIndex const& index) 443{ 444 int column_x = 0; 445 for (auto const& column : m_columns) { 446 if (column.parent_index == index.parent()) 447 return { column_x, index.row() * item_height(), column.width, item_height() }; 448 449 column_x += column.width + column_separator_width(); 450 } 451 return {}; 452} 453 454void ColumnsView::scroll_into_view(ModelIndex const& index, bool scroll_horizontally, bool scroll_vertically) 455{ 456 if (!model()) 457 return; 458 AbstractScrollableWidget::scroll_into_view(index_content_rect(index), scroll_horizontally, scroll_vertically); 459} 460 461Gfx::IntRect ColumnsView::content_rect(ModelIndex const& index) const 462{ 463 if (!index.is_valid()) 464 return {}; 465 466 int column_x = 0; 467 for (auto& column : m_columns) { 468 if (column.parent_index == index.parent()) 469 return { column_x + icon_size(), index.row() * item_height(), column.width - icon_size(), item_height() }; 470 column_x += column.width + 1; 471 } 472 473 return {}; 474} 475 476Gfx::IntRect ColumnsView::paint_invalidation_rect(ModelIndex const& index) const 477{ 478 auto rect = content_rect(index); 479 rect.translate_by(-icon_size(), 0); 480 rect.set_width(rect.width() + icon_size()); 481 return rect; 482} 483 484}