1// Aseprite UI Library
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This file is released under the terms of the MIT license.
6// Read LICENSE.txt for more information.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "ui/listbox.h"
13
14#include "base/fs.h"
15#include "ui/display.h"
16#include "ui/listitem.h"
17#include "ui/message.h"
18#include "ui/resize_event.h"
19#include "ui/separator.h"
20#include "ui/size_hint_event.h"
21#include "ui/system.h"
22#include "ui/theme.h"
23#include "ui/view.h"
24
25#include <algorithm>
26
27namespace ui {
28
29static inline bool sort_by_text(Widget* a, Widget* b) {
30 return (base::compare_filenames(a->text(), b->text()) < 0);
31}
32
33using namespace gfx;
34
35ListBox::ListBox()
36 : Widget(kListBoxWidget)
37 , m_multiselect(false)
38 , m_firstSelectedIndex(-1)
39 , m_lastSelectedIndex(-1)
40{
41 setFocusStop(true);
42 initTheme();
43}
44
45void ListBox::setMultiselect(const bool multiselect)
46{
47 m_multiselect = multiselect;
48}
49
50Widget* ListBox::getSelectedChild()
51{
52 for (auto child : children())
53 if (child->isSelected())
54 return child;
55
56 return nullptr;
57}
58
59int ListBox::getSelectedIndex()
60{
61 int i = 0;
62
63 for (auto child : children()) {
64 if (child->isSelected())
65 return i;
66
67 i++;
68 }
69
70 return -1;
71}
72
73void ListBox::selectChild(Widget* item, Message* msg)
74{
75 bool didChange = false;
76
77 int itemIndex = getChildIndex(item);
78 m_lastSelectedIndex = itemIndex;
79
80 if (m_multiselect) {
81 // Save current state of all children when we start selecting
82 if (msg == nullptr ||
83 msg->type() == kMouseDownMessage ||
84 msg->type() == kKeyDownMessage) {
85 m_firstSelectedIndex = itemIndex;
86 m_states.resize(children().size());
87
88 int i = 0;
89 for (auto child : children()) {
90 bool state = child->isSelected();
91 if (msg && !msg->ctrlPressed() && !msg->cmdPressed())
92 state = false;
93 if (m_states[i] != state) {
94 didChange = true;
95 m_states[i] = state;
96 }
97 ++i;
98 }
99 }
100 }
101
102 int i = 0;
103 for (auto child : children()) {
104 bool newState;
105
106 if (m_multiselect) {
107 ASSERT(i >= 0 && i < int(m_states.size()));
108 newState = (i >= 0 && i < int(m_states.size()) ? m_states[i]: false);
109
110 if (i >= std::min(itemIndex, m_firstSelectedIndex) &&
111 i <= std::max(itemIndex, m_firstSelectedIndex)) {
112 newState = !newState;
113 }
114 }
115 else {
116 newState = (child == item);
117 }
118
119 if (child->isSelected() != newState) {
120 didChange = true;
121 child->setSelected(newState);
122 }
123
124 ++i;
125 }
126
127 if (item)
128 makeChildVisible(item);
129
130 if (didChange)
131 onChange();
132}
133
134void ListBox::selectIndex(int index, Message* msg)
135{
136 if (index < 0 || index >= int(children().size()))
137 return;
138
139 Widget* child = at(index);
140 if (child)
141 selectChild(child, msg);
142}
143
144int ListBox::getItemsCount() const
145{
146 return int(children().size());
147}
148
149void ListBox::makeChildVisible(Widget* child)
150{
151 View* view = View::getView(this);
152 if (!view)
153 return;
154
155 gfx::Point scroll = view->viewScroll();
156 gfx::Rect vp = view->viewportBounds();
157
158 if (child->bounds().y < vp.y)
159 scroll.y = child->bounds().y - bounds().y;
160 else if (child->bounds().y > vp.y + vp.h - child->bounds().h)
161 scroll.y = (child->bounds().y - bounds().y
162 - vp.h + child->bounds().h);
163
164 view->setViewScroll(scroll);
165}
166
167// Setup the scroll to center the selected item in the viewport
168void ListBox::centerScroll()
169{
170 View* view = View::getView(this);
171 Widget* item = getSelectedChild();
172
173 if (view && item) {
174 gfx::Rect vp = view->viewportBounds();
175 gfx::Point scroll = view->viewScroll();
176
177 scroll.y = ((item->bounds().y - bounds().y)
178 - vp.h/2 + item->bounds().h/2);
179
180 view->setViewScroll(scroll);
181 }
182}
183
184void ListBox::sortItems()
185{
186 sortItems(&sort_by_text);
187}
188
189void ListBox::sortItems(bool (*cmp)(Widget* a, Widget* b))
190{
191 WidgetsList widgets = children();
192 std::sort(widgets.begin(), widgets.end(), cmp);
193
194 // Remove all children and add then again.
195 removeAllChildren();
196 for (Widget* child : widgets)
197 addChild(child);
198}
199
200bool ListBox::onProcessMessage(Message* msg)
201{
202 switch (msg->type()) {
203
204 case kOpenMessage:
205 centerScroll();
206 break;
207
208 case kMouseDownMessage:
209 captureMouse();
210 [[fallthrough]];
211
212 case kMouseMoveMessage:
213 if (hasCapture()) {
214 gfx::Point screenPos = msg->display()->nativeWindow()->pointToScreen(static_cast<MouseMessage*>(msg)->position());
215 gfx::Point mousePos = display()->nativeWindow()->pointFromScreen(screenPos);
216 View* view = View::getView(this);
217 bool pick_item = true;
218
219 if (view && m_lastSelectedIndex >= 0) {
220 gfx::Rect vp = view->viewportBounds();
221
222 if (mousePos.y < vp.y) {
223 const int num = std::max(1, (vp.y - mousePos.y) / 8);
224 const int select =
225 advanceIndexThroughVisibleItems(m_lastSelectedIndex, -num, false);
226 selectIndex(select, msg);
227 pick_item = false;
228 }
229 else if (mousePos.y >= vp.y + vp.h) {
230 const int num = std::max(1, (mousePos.y - (vp.y+vp.h-1)) / 8);
231 const int select =
232 advanceIndexThroughVisibleItems(m_lastSelectedIndex, +num, false);
233 selectIndex(select, msg);
234 pick_item = false;
235 }
236 }
237
238 if (pick_item) {
239 Widget* picked = nullptr;
240
241 if (view) {
242 picked = view->viewport()->pick(mousePos);
243 }
244 else {
245 picked = pick(mousePos);
246 }
247
248 if (dynamic_cast<ui::Separator*>(picked))
249 picked = nullptr;
250
251 // If the picked widget is a child of the list, select it
252 if (picked && hasChild(picked))
253 selectChild(picked, msg);
254 }
255 }
256 return true;
257
258 case kMouseUpMessage:
259 if (hasCapture())
260 releaseMouse();
261 return true;
262
263 case kMouseWheelMessage: {
264 View* view = View::getView(this);
265 if (view) {
266 auto mouseMsg = static_cast<MouseMessage*>(msg);
267 gfx::Point scroll = view->viewScroll();
268
269 if (mouseMsg->preciseWheel())
270 scroll += mouseMsg->wheelDelta();
271 else
272 scroll += mouseMsg->wheelDelta() * textHeight()*3;
273
274 view->setViewScroll(scroll);
275 }
276 break;
277 }
278
279 case kKeyDownMessage:
280 if (hasFocus() && !children().empty()) {
281 int select = getSelectedIndex();
282 int bottom = std::max(0, int(children().size()-1));
283 View* view = View::getView(this);
284 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
285 KeyScancode scancode = keymsg->scancode();
286
287 if (keymsg->onlyCmdPressed()) {
288 if (scancode == kKeyUp) scancode = kKeyHome;
289 if (scancode == kKeyDown) scancode = kKeyEnd;
290 }
291
292 switch (scancode) {
293 case kKeyUp:
294 // Select previous element.
295 if (select >= 0) {
296 select = advanceIndexThroughVisibleItems(select, -1, true);
297 }
298 // Or select the bottom of the list if there is no
299 // selected item.
300 else {
301 select = advanceIndexThroughVisibleItems(bottom+1, -1, true);
302 }
303 break;
304 case kKeyDown:
305 select = advanceIndexThroughVisibleItems(select, +1, true);
306 break;
307 case kKeyHome:
308 select = advanceIndexThroughVisibleItems(-1, +1, false);
309 break;
310 case kKeyEnd:
311 select = advanceIndexThroughVisibleItems(bottom+1, -1, false);
312 break;
313 case kKeyPageUp:
314 if (view) {
315 gfx::Rect vp = view->viewportBounds();
316 select = advanceIndexThroughVisibleItems(
317 select, -vp.h / textHeight(), false);
318 }
319 else
320 select = 0;
321 break;
322 case kKeyPageDown:
323 if (view) {
324 gfx::Rect vp = view->viewportBounds();
325 select = advanceIndexThroughVisibleItems(
326 select, +vp.h / textHeight(), false);
327 }
328 else {
329 select = bottom;
330 }
331 break;
332 case kKeyLeft:
333 case kKeyRight:
334 if (view) {
335 gfx::Rect vp = view->viewportBounds();
336 gfx::Point scroll = view->viewScroll();
337 int sgn = (keymsg->scancode() == kKeyLeft) ? -1: 1;
338
339 scroll.x += vp.w/2*sgn;
340
341 view->setViewScroll(scroll);
342 }
343 break;
344 default:
345 return Widget::onProcessMessage(msg);
346 }
347
348 selectIndex(std::clamp(select, 0, bottom), msg);
349 return true;
350 }
351 break;
352
353 case kDoubleClickMessage:
354 onDoubleClickItem();
355 return true;
356 }
357
358 return Widget::onProcessMessage(msg);
359}
360
361void ListBox::onPaint(PaintEvent& ev)
362{
363 theme()->paintListBox(ev);
364}
365
366void ListBox::onResize(ResizeEvent& ev)
367{
368 setBoundsQuietly(ev.bounds());
369
370 Rect cpos = childrenBounds();
371
372 for (auto child : children()) {
373 if (child->hasFlags(HIDDEN))
374 continue;
375
376 cpos.h = child->sizeHint().h;
377 child->setBounds(cpos);
378
379 cpos.y += child->bounds().h + childSpacing();
380 }
381}
382
383void ListBox::onSizeHint(SizeHintEvent& ev)
384{
385 int w = 0, h = 0;
386 int visibles = 0;
387
388 for (auto child : children()) {
389 if (child->hasFlags(HIDDEN))
390 continue;
391
392 Size reqSize = child->sizeHint();
393
394 w = std::max(w, reqSize.w);
395 h += reqSize.h;
396 ++visibles;
397 }
398 if (visibles > 1)
399 h += childSpacing() * (visibles-1);
400
401 w += border().width();
402 h += border().height();
403
404 ev.setSizeHint(Size(w, h));
405}
406
407void ListBox::onChange()
408{
409 Change();
410}
411
412void ListBox::onDoubleClickItem()
413{
414 DoubleClickItem();
415}
416
417int ListBox::advanceIndexThroughVisibleItems(
418 int startIndex, int delta, const bool loop)
419{
420 const int bottom = std::max(0, int(children().size()-1));
421 const int sgn = SGN(delta);
422 int index = startIndex;
423
424 startIndex = std::clamp(startIndex, 0, bottom);
425 int lastVisibleIndex = startIndex;
426
427 bool cycle = false;
428
429 while (delta) {
430 index += sgn;
431
432 if (cycle && index == startIndex) {
433 break;
434 }
435 else if (index < 0) {
436 if (!loop)
437 break;
438 index = bottom-sgn;
439 cycle = true;
440 }
441 else if (index > bottom) {
442 if (!loop)
443 break;
444 index = 0-sgn;
445 cycle = true;
446 }
447 else if (index >= 0 && index < children().size()) {
448 Widget* item = at(index);
449 if (item &&
450 !item->hasFlags(HIDDEN) &&
451 // We can completely ignore separators from navigation
452 // keys.
453 !dynamic_cast<Separator*>(item)) {
454 lastVisibleIndex = index;
455 delta -= sgn;
456 }
457 }
458 }
459 return lastVisibleIndex;
460}
461
462} // namespace ui
463