Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@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 <AK/StringBuilder.h>
28#include <AK/Vector.h>
29#include <LibGUI/AbstractTableView.h>
30#include <LibGUI/Action.h>
31#include <LibGUI/Menu.h>
32#include <LibGUI/Model.h>
33#include <LibGUI/Painter.h>
34#include <LibGUI/ScrollBar.h>
35#include <LibGUI/Window.h>
36#include <LibGfx/Palette.h>
37
38namespace GUI {
39
40static const int minimum_column_width = 2;
41
42AbstractTableView::AbstractTableView()
43{
44 set_should_hide_unnecessary_scrollbars(true);
45}
46
47AbstractTableView::~AbstractTableView()
48{
49}
50
51void AbstractTableView::select_all()
52{
53 selection().clear();
54 for (int item_index = 0; item_index < item_count(); ++item_index) {
55 auto index = model()->index(item_index);
56 selection().add(index);
57 }
58}
59
60void AbstractTableView::update_column_sizes()
61{
62 if (!m_size_columns_to_fit_content)
63 return;
64
65 if (!model())
66 return;
67
68 auto& model = *this->model();
69 int column_count = model.column_count();
70 int row_count = model.row_count();
71 int key_column = model.key_column();
72
73 for (int column = 0; column < column_count; ++column) {
74 if (is_column_hidden(column))
75 continue;
76 int header_width = header_font().width(model.column_name(column));
77 if (column == key_column)
78 header_width += font().width(" \xc3\xb6");
79 int column_width = header_width;
80 for (int row = 0; row < row_count; ++row) {
81 auto cell_data = model.data(model.index(row, column));
82 int cell_width = 0;
83 if (cell_data.is_bitmap()) {
84 cell_width = cell_data.as_bitmap().width();
85 } else {
86 cell_width = font().width(cell_data.to_string());
87 }
88 column_width = max(column_width, cell_width);
89 }
90 auto& column_data = this->column_data(column);
91 column_data.width = max(column_data.width, column_width);
92 column_data.has_initialized_width = true;
93 }
94}
95
96void AbstractTableView::update_content_size()
97{
98 if (!model())
99 return set_content_size({});
100
101 int content_width = 0;
102 int column_count = model()->column_count();
103 for (int i = 0; i < column_count; ++i) {
104 if (!is_column_hidden(i))
105 content_width += column_width(i) + horizontal_padding() * 2;
106 }
107 int content_height = item_count() * item_height();
108
109 set_content_size({ content_width, content_height });
110 set_size_occupied_by_fixed_elements({ 0, header_height() });
111}
112
113Gfx::Rect AbstractTableView::header_rect(int column_index) const
114{
115 if (!model())
116 return {};
117 if (is_column_hidden(column_index))
118 return {};
119 int x_offset = 0;
120 for (int i = 0; i < column_index; ++i) {
121 if (is_column_hidden(i))
122 continue;
123 x_offset += column_width(i) + horizontal_padding() * 2;
124 }
125 return { x_offset, 0, column_width(column_index) + horizontal_padding() * 2, header_height() };
126}
127
128void AbstractTableView::set_hovered_header_index(int index)
129{
130 if (m_hovered_column_header_index == index)
131 return;
132 m_hovered_column_header_index = index;
133 update_headers();
134}
135
136void AbstractTableView::paint_headers(Painter& painter)
137{
138 if (!headers_visible())
139 return;
140 int exposed_width = max(content_size().width(), width());
141 painter.fill_rect({ 0, 0, exposed_width, header_height() }, palette().button());
142 painter.draw_line({ 0, 0 }, { exposed_width - 1, 0 }, palette().threed_highlight());
143 painter.draw_line({ 0, header_height() - 1 }, { exposed_width - 1, header_height() - 1 }, palette().threed_shadow1());
144 int x_offset = 0;
145 int column_count = model()->column_count();
146 for (int column_index = 0; column_index < column_count; ++column_index) {
147 if (is_column_hidden(column_index))
148 continue;
149 int column_width = this->column_width(column_index);
150 bool is_key_column = model()->key_column() == column_index;
151 Gfx::Rect cell_rect(x_offset, 0, column_width + horizontal_padding() * 2, header_height());
152 bool pressed = column_index == m_pressed_column_header_index && m_pressed_column_header_is_pressed;
153 bool hovered = column_index == m_hovered_column_header_index && model()->column_metadata(column_index).sortable == Model::ColumnMetadata::Sortable::True;
154 Gfx::StylePainter::paint_button(painter, cell_rect, palette(), Gfx::ButtonStyle::Normal, pressed, hovered);
155 String text;
156 if (is_key_column) {
157 StringBuilder builder;
158 builder.append(model()->column_name(column_index));
159 auto sort_order = model()->sort_order();
160 if (sort_order == SortOrder::Ascending)
161 builder.append(" \xc3\xb6");
162 else if (sort_order == SortOrder::Descending)
163 builder.append(" \xc3\xb7");
164 text = builder.to_string();
165 } else {
166 text = model()->column_name(column_index);
167 }
168 auto text_rect = cell_rect.translated(horizontal_padding(), 0);
169 if (pressed)
170 text_rect.move_by(1, 1);
171 painter.draw_text(text_rect, text, header_font(), Gfx::TextAlignment::CenterLeft, palette().button_text());
172 x_offset += column_width + horizontal_padding() * 2;
173 }
174}
175
176bool AbstractTableView::is_column_hidden(int column) const
177{
178 return !column_data(column).visibility;
179}
180
181void AbstractTableView::set_column_hidden(int column, bool hidden)
182{
183 auto& column_data = this->column_data(column);
184 if (column_data.visibility == !hidden)
185 return;
186 column_data.visibility = !hidden;
187 update_content_size();
188 update();
189}
190
191Menu& AbstractTableView::ensure_header_context_menu()
192{
193 // FIXME: This menu needs to be rebuilt if the model is swapped out,
194 // or if the column count/names change.
195 if (!m_header_context_menu) {
196 ASSERT(model());
197 m_header_context_menu = Menu::construct();
198
199 for (int column = 0; column < model()->column_count(); ++column) {
200 auto& column_data = this->column_data(column);
201 auto name = model()->column_name(column);
202 column_data.visibility_action = Action::create(name, [this, column](Action& action) {
203 action.set_checked(!action.is_checked());
204 set_column_hidden(column, !action.is_checked());
205 });
206 column_data.visibility_action->set_checkable(true);
207 column_data.visibility_action->set_checked(true);
208
209 m_header_context_menu->add_action(*column_data.visibility_action);
210 }
211 }
212 return *m_header_context_menu;
213}
214
215const Gfx::Font& AbstractTableView::header_font()
216{
217 return Gfx::Font::default_bold_font();
218}
219
220void AbstractTableView::set_cell_painting_delegate(int column, OwnPtr<TableCellPaintingDelegate>&& delegate)
221{
222 column_data(column).cell_painting_delegate = move(delegate);
223}
224
225void AbstractTableView::update_headers()
226{
227 Gfx::Rect rect { 0, 0, frame_inner_rect().width(), header_height() };
228 rect.move_by(frame_thickness(), frame_thickness());
229 update(rect);
230}
231
232AbstractTableView::ColumnData& AbstractTableView::column_data(int column) const
233{
234 if (static_cast<size_t>(column) >= m_column_data.size())
235 m_column_data.resize(column + 1);
236 return m_column_data.at(column);
237}
238
239Gfx::Rect AbstractTableView::column_resize_grabbable_rect(int column) const
240{
241 if (!model())
242 return {};
243 auto header_rect = this->header_rect(column);
244 return { header_rect.right() - 1, header_rect.top(), 4, header_rect.height() };
245}
246
247int AbstractTableView::column_width(int column_index) const
248{
249 if (!model())
250 return 0;
251 auto& column_data = this->column_data(column_index);
252 if (!column_data.has_initialized_width) {
253 ASSERT(!m_size_columns_to_fit_content);
254 column_data.has_initialized_width = true;
255 column_data.width = model()->column_metadata(column_index).preferred_width;
256 }
257 return column_data.width;
258}
259
260void AbstractTableView::mousemove_event(MouseEvent& event)
261{
262 if (!model())
263 return AbstractView::mousemove_event(event);
264
265 auto adjusted_position = this->adjusted_position(event.position());
266
267 if (m_in_column_resize) {
268 auto delta = adjusted_position - m_column_resize_origin;
269 int new_width = m_column_resize_original_width + delta.x();
270 if (new_width <= minimum_column_width)
271 new_width = minimum_column_width;
272 ASSERT(m_resizing_column >= 0 && m_resizing_column < model()->column_count());
273 auto& column_data = this->column_data(m_resizing_column);
274 if (column_data.width != new_width) {
275 column_data.width = new_width;
276 update_content_size();
277 update();
278 }
279 return;
280 }
281
282 if (m_pressed_column_header_index != -1) {
283 auto header_rect = this->header_rect(m_pressed_column_header_index);
284 if (header_rect.contains(adjusted_position)) {
285 set_hovered_header_index(m_pressed_column_header_index);
286 if (!m_pressed_column_header_is_pressed)
287 update_headers();
288 m_pressed_column_header_is_pressed = true;
289 } else {
290 set_hovered_header_index(-1);
291 if (m_pressed_column_header_is_pressed)
292 update_headers();
293 m_pressed_column_header_is_pressed = false;
294 }
295 return;
296 }
297
298 if (event.buttons() == 0) {
299 int column_count = model()->column_count();
300 bool found_hovered_header = false;
301 for (int i = 0; i < column_count; ++i) {
302 if (column_resize_grabbable_rect(i).contains(adjusted_position)) {
303 window()->set_override_cursor(StandardCursor::ResizeHorizontal);
304 set_hovered_header_index(-1);
305 return;
306 }
307 if (header_rect(i).contains(adjusted_position)) {
308 set_hovered_header_index(i);
309 found_hovered_header = true;
310 }
311 }
312 if (!found_hovered_header)
313 set_hovered_header_index(-1);
314 }
315 window()->set_override_cursor(StandardCursor::None);
316
317 AbstractView::mousemove_event(event);
318}
319
320void AbstractTableView::mouseup_event(MouseEvent& event)
321{
322 auto adjusted_position = this->adjusted_position(event.position());
323 if (event.button() == MouseButton::Left) {
324 if (m_in_column_resize) {
325 if (!column_resize_grabbable_rect(m_resizing_column).contains(adjusted_position))
326 window()->set_override_cursor(StandardCursor::None);
327 m_in_column_resize = false;
328 return;
329 }
330 if (m_pressed_column_header_index != -1) {
331 auto header_rect = this->header_rect(m_pressed_column_header_index);
332 if (header_rect.contains(adjusted_position)) {
333 auto new_sort_order = SortOrder::Ascending;
334 if (model()->key_column() == m_pressed_column_header_index)
335 new_sort_order = model()->sort_order() == SortOrder::Ascending
336 ? SortOrder::Descending
337 : SortOrder::Ascending;
338 model()->set_key_column_and_sort_order(m_pressed_column_header_index, new_sort_order);
339 }
340 m_pressed_column_header_index = -1;
341 m_pressed_column_header_is_pressed = false;
342 update_headers();
343 return;
344 }
345 }
346
347 AbstractView::mouseup_event(event);
348}
349
350void AbstractTableView::mousedown_event(MouseEvent& event)
351{
352 if (!model())
353 return AbstractView::mousedown_event(event);
354
355 if (event.button() != MouseButton::Left)
356 return AbstractView::mousedown_event(event);
357
358 auto adjusted_position = this->adjusted_position(event.position());
359
360 if (event.y() < header_height()) {
361 int column_count = model()->column_count();
362 for (int i = 0; i < column_count; ++i) {
363 if (column_resize_grabbable_rect(i).contains(adjusted_position)) {
364 m_resizing_column = i;
365 m_in_column_resize = true;
366 m_column_resize_original_width = column_width(i);
367 m_column_resize_origin = adjusted_position;
368 return;
369 }
370 auto header_rect = this->header_rect(i);
371 auto column_metadata = model()->column_metadata(i);
372 if (header_rect.contains(adjusted_position) && column_metadata.sortable == Model::ColumnMetadata::Sortable::True) {
373 m_pressed_column_header_index = i;
374 m_pressed_column_header_is_pressed = true;
375 update_headers();
376 return;
377 }
378 }
379 return;
380 }
381
382 bool is_toggle;
383 auto index = index_at_event_position(event.position(), is_toggle);
384
385 if (index.is_valid() && is_toggle && model()->row_count(index)) {
386 toggle_index(index);
387 return;
388 }
389
390 AbstractView::mousedown_event(event);
391}
392
393ModelIndex AbstractTableView::index_at_event_position(const Gfx::Point& position, bool& is_toggle) const
394{
395 is_toggle = false;
396 if (!model())
397 return {};
398
399 auto adjusted_position = this->adjusted_position(position);
400 for (int row = 0, row_count = model()->row_count(); row < row_count; ++row) {
401 if (!row_rect(row).contains(adjusted_position))
402 continue;
403 for (int column = 0, column_count = model()->column_count(); column < column_count; ++column) {
404 if (!content_rect(row, column).contains(adjusted_position))
405 continue;
406 return model()->index(row, column);
407 }
408 return model()->index(row, 0);
409 }
410 return {};
411}
412
413ModelIndex AbstractTableView::index_at_event_position(const Gfx::Point& position) const
414{
415 bool is_toggle;
416 auto index = index_at_event_position(position, is_toggle);
417 return is_toggle ? ModelIndex() : index;
418}
419
420int AbstractTableView::item_count() const
421{
422 if (!model())
423 return 0;
424 return model()->row_count();
425}
426
427void AbstractTableView::keydown_event(KeyEvent& event)
428{
429 if (!model())
430 return;
431 auto& model = *this->model();
432 if (event.key() == KeyCode::Key_Return) {
433 activate_selected();
434 return;
435 }
436 if (event.key() == KeyCode::Key_Up) {
437 ModelIndex new_index;
438 if (!selection().is_empty()) {
439 auto old_index = selection().first();
440 new_index = model.index(old_index.row() - 1, old_index.column());
441 } else {
442 new_index = model.index(0, 0);
443 }
444 if (model.is_valid(new_index)) {
445 selection().set(new_index);
446 scroll_into_view(new_index, Orientation::Vertical);
447 update();
448 }
449 return;
450 }
451 if (event.key() == KeyCode::Key_Down) {
452 ModelIndex new_index;
453 if (!selection().is_empty()) {
454 auto old_index = selection().first();
455 new_index = model.index(old_index.row() + 1, old_index.column());
456 } else {
457 new_index = model.index(0, 0);
458 }
459 if (model.is_valid(new_index)) {
460 selection().set(new_index);
461 scroll_into_view(new_index, Orientation::Vertical);
462 update();
463 }
464 return;
465 }
466 if (event.key() == KeyCode::Key_PageUp) {
467 int items_per_page = visible_content_rect().height() / item_height();
468 auto old_index = selection().first();
469 auto new_index = model.index(max(0, old_index.row() - items_per_page), old_index.column());
470 if (model.is_valid(new_index)) {
471 selection().set(new_index);
472 scroll_into_view(new_index, Orientation::Vertical);
473 update();
474 }
475 return;
476 }
477 if (event.key() == KeyCode::Key_PageDown) {
478 int items_per_page = visible_content_rect().height() / item_height();
479 auto old_index = selection().first();
480 auto new_index = model.index(min(model.row_count() - 1, old_index.row() + items_per_page), old_index.column());
481 if (model.is_valid(new_index)) {
482 selection().set(new_index);
483 scroll_into_view(new_index, Orientation::Vertical);
484 update();
485 }
486 return;
487 }
488 return Widget::keydown_event(event);
489}
490
491void AbstractTableView::scroll_into_view(const ModelIndex& index, Orientation orientation)
492{
493 auto rect = row_rect(index.row()).translated(0, -header_height());
494 ScrollableWidget::scroll_into_view(rect, orientation);
495}
496
497void AbstractTableView::doubleclick_event(MouseEvent& event)
498{
499 if (!model())
500 return;
501 if (event.button() == MouseButton::Left) {
502 if (event.y() < header_height())
503 return;
504 if (!selection().is_empty()) {
505 if (is_editable())
506 begin_editing(selection().first());
507 else
508 activate_selected();
509 }
510 }
511}
512
513void AbstractTableView::context_menu_event(ContextMenuEvent& event)
514{
515 if (!model())
516 return;
517 if (event.position().y() < header_height()) {
518 ensure_header_context_menu().popup(event.screen_position());
519 return;
520 }
521
522 bool is_toggle;
523 auto index = index_at_event_position(event.position(), is_toggle);
524 if (index.is_valid()) {
525 if (!selection().contains(index))
526 selection().set(index);
527 } else {
528 selection().clear();
529 }
530 if (on_context_menu_request)
531 on_context_menu_request(index, event);
532}
533
534void AbstractTableView::leave_event(Core::Event&)
535{
536 window()->set_override_cursor(StandardCursor::None);
537 set_hovered_header_index(-1);
538}
539
540Gfx::Rect AbstractTableView::content_rect(int row, int column) const
541{
542 auto row_rect = this->row_rect(row);
543 int x = 0;
544 for (int i = 0; i < column; ++i)
545 x += column_width(i) + horizontal_padding() * 2;
546
547 return { row_rect.x() + x, row_rect.y(), column_width(column) + horizontal_padding() * 2, item_height() };
548}
549
550Gfx::Rect AbstractTableView::content_rect(const ModelIndex& index) const
551{
552 return content_rect(index.row(), index.column());
553}
554
555Gfx::Rect AbstractTableView::row_rect(int item_index) const
556{
557 return { 0, header_height() + (item_index * item_height()), max(content_size().width(), width()), item_height() };
558}
559
560Gfx::Point AbstractTableView::adjusted_position(const Gfx::Point& position) const
561{
562 return position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness());
563}
564
565void AbstractTableView::did_update_model()
566{
567 AbstractView::did_update_model();
568 update_column_sizes();
569 update_content_size();
570 update();
571}
572
573}