Serenity Operating System
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}