Serenity Operating System
1/*
2 * Copyright (c) 2020, Sergey Bugaev <bugaevc@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <LibGUI/ColumnsView.h>
28#include <LibGUI/Model.h>
29#include <LibGUI/Painter.h>
30#include <LibGUI/ScrollBar.h>
31#include <LibGfx/CharacterBitmap.h>
32#include <LibGfx/Palette.h>
33
34namespace GUI {
35
36static const char* s_arrow_bitmap_data = {
37 " "
38 " # "
39 " ## "
40 " ### "
41 " #### "
42 " ### "
43 " ## "
44 " # "
45 " "
46};
47static const int s_arrow_bitmap_width = 9;
48static const int s_arrow_bitmap_height = 9;
49
50ColumnsView::ColumnsView()
51{
52 set_fill_with_background_color(true);
53 set_background_role(ColorRole::Base);
54 set_foreground_role(ColorRole::BaseText);
55 m_columns.append({ {}, 0 });
56}
57
58ColumnsView::~ColumnsView()
59{
60}
61
62void ColumnsView::select_all()
63{
64 Vector<Column> columns_for_selection;
65 selection().for_each_index([&](auto& index) {
66 for (auto& column : m_columns) {
67 if (column.parent_index == index.parent()) {
68 columns_for_selection.append(column);
69 return;
70 }
71 }
72 ASSERT_NOT_REACHED();
73 });
74
75 for (Column& column : columns_for_selection) {
76 int row_count = model()->row_count(column.parent_index);
77 for (int row = 0; row < row_count; row++) {
78 ModelIndex index = model()->index(row, m_model_column, column.parent_index);
79 selection().add(index);
80 }
81 }
82}
83
84void ColumnsView::paint_event(PaintEvent& event)
85{
86 AbstractView::paint_event(event);
87
88 if (!model())
89 return;
90
91 Painter painter(*this);
92 painter.add_clip_rect(frame_inner_rect());
93 painter.add_clip_rect(event.rect());
94 painter.translate(frame_thickness(), frame_thickness());
95 painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
96
97 int column_x = 0;
98
99 for (size_t i = 0; i < m_columns.size(); i++) {
100 auto& column = m_columns[i];
101 auto* next_column = i + 1 == m_columns.size() ? nullptr : &m_columns[i + 1];
102
103 ASSERT(column.width > 0);
104
105 int row_count = model()->row_count(column.parent_index);
106 for (int row = 0; row < row_count; row++) {
107 ModelIndex index = model()->index(row, m_model_column, column.parent_index);
108 ASSERT(index.is_valid());
109
110 bool is_selected_row = selection().contains(index);
111
112 Color background_color = palette().color(background_role());
113 Color text_color = palette().color(foreground_role());
114
115 if (next_column != nullptr && next_column->parent_index == index) {
116 background_color = palette().inactive_selection();
117 text_color = palette().inactive_selection_text();
118 }
119
120 if (is_selected_row) {
121 background_color = palette().selection();
122 text_color = palette().selection_text();
123 }
124
125 Gfx::Rect row_rect { column_x, row * item_height(), column.width, item_height() };
126 painter.fill_rect(row_rect, background_color);
127
128 auto icon = model()->data(index, Model::Role::Icon);
129 Gfx::Rect icon_rect = { column_x + icon_spacing(), 0, icon_size(), icon_size() };
130 icon_rect.center_vertically_within(row_rect);
131 if (icon.is_icon()) {
132 if (auto* bitmap = icon.as_icon().bitmap_for_size(icon_size())) {
133 if (m_hovered_index.is_valid() && m_hovered_index.parent() == index.parent() && m_hovered_index.row() == index.row())
134 painter.blit_brightened(icon_rect.location(), *bitmap, bitmap->rect());
135 else
136 painter.blit(icon_rect.location(), *bitmap, bitmap->rect());
137 }
138 }
139
140 Gfx::Rect text_rect = {
141 icon_rect.right() + 1 + icon_spacing(), row * item_height(),
142 column.width - icon_spacing() - icon_size() - icon_spacing() - icon_spacing() - s_arrow_bitmap_width - icon_spacing(), item_height()
143 };
144 auto text = model()->data(index).to_string();
145 painter.draw_text(text_rect, text, Gfx::TextAlignment::CenterLeft, text_color);
146
147 bool expandable = model()->row_count(index) > 0;
148 if (expandable) {
149 Gfx::Rect arrow_rect = {
150 text_rect.right() + 1 + icon_spacing(), 0,
151 s_arrow_bitmap_width, s_arrow_bitmap_height
152 };
153 arrow_rect.center_vertically_within(row_rect);
154 static auto& arrow_bitmap = Gfx::CharacterBitmap::create_from_ascii(s_arrow_bitmap_data, s_arrow_bitmap_width, s_arrow_bitmap_height).leak_ref();
155 painter.draw_bitmap(arrow_rect.location(), arrow_bitmap, text_color);
156 }
157 }
158
159 int separator_height = content_size().height();
160 if (height() > separator_height)
161 separator_height = height();
162 painter.draw_line({ column_x + column.width, 0 }, { column_x + column.width, separator_height }, palette().button());
163 column_x += column.width + 1;
164 }
165}
166
167void ColumnsView::push_column(ModelIndex& parent_index)
168{
169 ASSERT(model());
170
171 // Drop columns at the end.
172 ModelIndex grandparent = model()->parent_index(parent_index);
173 for (int i = m_columns.size() - 1; i > 0; i--) {
174 if (m_columns[i].parent_index == grandparent)
175 break;
176 m_columns.shrink(i);
177 dbg() << "Dropping column " << i;
178 }
179
180 // Add the new column.
181 dbg() << "Adding a new column";
182 m_columns.append({ parent_index, 0 });
183 update_column_sizes();
184 update();
185}
186
187void ColumnsView::update_column_sizes()
188{
189 if (!model())
190 return;
191
192 int total_width = 0;
193 int total_height = 0;
194
195 for (auto& column : m_columns) {
196 int row_count = model()->row_count(column.parent_index);
197
198 int column_height = row_count * item_height();
199 if (column_height > total_height)
200 total_height = column_height;
201
202 column.width = 10;
203 for (int row = 0; row < row_count; row++) {
204 ModelIndex index = model()->index(row, m_model_column, column.parent_index);
205 ASSERT(index.is_valid());
206 auto text = model()->data(index).to_string();
207 int row_width = icon_spacing() + icon_size() + icon_spacing() + font().width(text) + icon_spacing() + s_arrow_bitmap_width + icon_spacing();
208 if (row_width > column.width)
209 column.width = row_width;
210 }
211 total_width += column.width + 1;
212 }
213
214 set_content_size({ total_width, total_height });
215}
216
217ModelIndex ColumnsView::index_at_event_position(const Gfx::Point& a_position) const
218{
219 if (!model())
220 return {};
221
222 auto position = a_position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness());
223
224 int column_x = 0;
225
226 for (auto& column : m_columns) {
227 if (position.x() < column_x)
228 break;
229 if (position.x() > column_x + column.width) {
230 column_x += column.width;
231 continue;
232 }
233
234 int row = position.y() / item_height();
235 int row_count = model()->row_count(column.parent_index);
236 if (row >= row_count)
237 return {};
238
239 return model()->index(row, m_model_column, column.parent_index);
240 }
241
242 return {};
243}
244
245void ColumnsView::mousedown_event(MouseEvent& event)
246{
247 AbstractView::mousedown_event(event);
248
249 if (!model())
250 return;
251
252 if (event.button() != MouseButton::Left)
253 return;
254
255 auto index = index_at_event_position(event.position());
256 if (index.is_valid() && !(event.modifiers() & Mod_Ctrl)) {
257 if (model()->row_count(index))
258 push_column(index);
259 }
260}
261
262void ColumnsView::did_update_model()
263{
264 AbstractView::did_update_model();
265
266 // FIXME: Don't drop the columns on minor updates.
267 dbg() << "Model was updated; dropping columns :(";
268 m_columns.clear();
269 m_columns.append({ {}, 0 });
270
271 update_column_sizes();
272 update();
273}
274
275void ColumnsView::keydown_event(KeyEvent& event)
276{
277 if (!model())
278 return;
279 auto& model = *this->model();
280
281 if (event.key() == KeyCode::Key_Return) {
282 activate_selected();
283 return;
284 }
285
286 if (event.key() == KeyCode::Key_Up) {
287 ModelIndex new_index;
288 if (!selection().is_empty()) {
289 auto old_index = selection().first();
290 auto parent_index = model.parent_index(old_index);
291 int row = old_index.row() > 0 ? old_index.row() - 1 : 0;
292 new_index = model.sibling(row, old_index.column(), parent_index);
293 } else {
294 new_index = model.index(0, m_model_column, {});
295 }
296 if (model.is_valid(new_index)) {
297 selection().set(new_index);
298 update();
299 }
300 return;
301 }
302
303 if (event.key() == KeyCode::Key_Down) {
304 ModelIndex new_index;
305 if (!selection().is_empty()) {
306 auto old_index = selection().first();
307 auto parent_index = model.parent_index(old_index);
308 int row = old_index.row() + 1;
309 new_index = model.sibling(row, old_index.column(), parent_index);
310 } else {
311 new_index = model.index(0, m_model_column, {});
312 }
313 if (model.is_valid(new_index)) {
314 selection().set(new_index);
315 update();
316 }
317 return;
318 }
319
320 if (event.key() == KeyCode::Key_Left) {
321 ModelIndex new_index;
322 if (!selection().is_empty()) {
323 auto old_index = selection().first();
324 new_index = model.parent_index(old_index);
325 } else {
326 new_index = model.index(0, m_model_column, {});
327 }
328 if (model.is_valid(new_index)) {
329 selection().set(new_index);
330 update();
331 }
332 return;
333 }
334
335 if (event.key() == KeyCode::Key_Right) {
336 ModelIndex old_index, new_index;
337 if (!selection().is_empty()) {
338 old_index = selection().first();
339 new_index = model.index(0, m_model_column, old_index);
340 } else {
341 new_index = model.index(0, m_model_column, {});
342 }
343 if (model.is_valid(new_index)) {
344 selection().set(new_index);
345 if (model.is_valid(old_index))
346 push_column(old_index);
347 update();
348 }
349 return;
350 }
351}
352
353}