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 | |
27 | namespace ui { |
28 | |
29 | static inline bool sort_by_text(Widget* a, Widget* b) { |
30 | return (base::compare_filenames(a->text(), b->text()) < 0); |
31 | } |
32 | |
33 | using namespace gfx; |
34 | |
35 | ListBox::ListBox() |
36 | : Widget(kListBoxWidget) |
37 | , m_multiselect(false) |
38 | , m_firstSelectedIndex(-1) |
39 | , m_lastSelectedIndex(-1) |
40 | { |
41 | setFocusStop(true); |
42 | initTheme(); |
43 | } |
44 | |
45 | void ListBox::setMultiselect(const bool multiselect) |
46 | { |
47 | m_multiselect = multiselect; |
48 | } |
49 | |
50 | Widget* ListBox::getSelectedChild() |
51 | { |
52 | for (auto child : children()) |
53 | if (child->isSelected()) |
54 | return child; |
55 | |
56 | return nullptr; |
57 | } |
58 | |
59 | int 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 | |
73 | void 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 | |
134 | void 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 | |
144 | int ListBox::getItemsCount() const |
145 | { |
146 | return int(children().size()); |
147 | } |
148 | |
149 | void 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 |
168 | void 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 | |
184 | void ListBox::sortItems() |
185 | { |
186 | sortItems(&sort_by_text); |
187 | } |
188 | |
189 | void 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 | |
200 | bool 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 | |
361 | void ListBox::onPaint(PaintEvent& ev) |
362 | { |
363 | theme()->paintListBox(ev); |
364 | } |
365 | |
366 | void 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 | |
383 | void 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 | |
407 | void ListBox::onChange() |
408 | { |
409 | Change(); |
410 | } |
411 | |
412 | void ListBox::onDoubleClickItem() |
413 | { |
414 | DoubleClickItem(); |
415 | } |
416 | |
417 | int 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 | |