1// Aseprite UI Library
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 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/combobox.h"
13
14#include "gfx/size.h"
15#include "os/font.h"
16#include "ui/button.h"
17#include "ui/entry.h"
18#include "ui/fit_bounds.h"
19#include "ui/listbox.h"
20#include "ui/listitem.h"
21#include "ui/manager.h"
22#include "ui/message.h"
23#include "ui/resize_event.h"
24#include "ui/scale.h"
25#include "ui/size_hint_event.h"
26#include "ui/system.h"
27#include "ui/theme.h"
28#include "ui/view.h"
29#include "ui/window.h"
30
31#include <algorithm>
32
33namespace ui {
34
35using namespace gfx;
36
37class ComboBoxButton : public Button {
38public:
39 ComboBoxButton() : Button("") {
40 setFocusStop(false);
41 }
42};
43
44class ComboBoxEntry : public Entry {
45public:
46 ComboBoxEntry(ComboBox* comboBox)
47 : Entry(256, ""),
48 m_comboBox(comboBox) {
49 }
50
51protected:
52 bool onProcessMessage(Message* msg) override;
53 void onPaint(PaintEvent& ev) override;
54 void onChange() override;
55
56private:
57 ComboBox* m_comboBox;
58};
59
60class ComboBoxListBox : public ListBox {
61public:
62 ComboBoxListBox(ComboBox* comboBox)
63 : m_comboBox(comboBox) {
64 for (auto item : *comboBox) {
65 if (item->parent())
66 item->parent()->removeChild(item);
67 addChild(item);
68 }
69 }
70
71 void clean() {
72 // Remove all added items so ~Widget() don't delete them.
73 removeAllChildren();
74 selectChild(nullptr);
75 }
76
77protected:
78 bool onProcessMessage(Message* msg) override;
79 void onChange() override;
80
81private:
82 bool isValidItem(int index) const {
83 return (index >= 0 && index < m_comboBox->getItemCount());
84 }
85
86 ComboBox* m_comboBox;
87};
88
89ComboBox::ComboBox()
90 : Widget(kComboBoxWidget)
91 , m_entry(new ComboBoxEntry(this))
92 , m_button(new ComboBoxButton())
93 , m_window(nullptr)
94 , m_listbox(nullptr)
95 , m_selected(-1)
96 , m_editable(false)
97 , m_clickopen(true)
98 , m_casesensitive(true)
99 , m_filtering(false)
100 , m_useCustomWidget(false)
101{
102 m_entry->setExpansive(true);
103
104 // When the "m_button" is clicked ("Click" signal) call onButtonClick() method
105 m_button->Click.connect(&ComboBox::onButtonClick, this);
106
107 addChild(m_entry);
108 addChild(m_button);
109
110 setFocusStop(true);
111 setEditable(m_editable);
112
113 initTheme();
114}
115
116ComboBox::~ComboBox()
117{
118 removeMessageFilters();
119 deleteAllItems();
120}
121
122void ComboBox::setEditable(bool state)
123{
124 m_editable = state;
125
126 if (state) {
127 m_entry->setReadOnly(false);
128 m_entry->showCaret();
129 }
130 else {
131 m_entry->setReadOnly(true);
132 m_entry->hideCaret();
133 }
134}
135
136void ComboBox::setClickOpen(bool state)
137{
138 m_clickopen = state;
139}
140
141void ComboBox::setCaseSensitive(bool state)
142{
143 m_casesensitive = state;
144}
145
146void ComboBox::setUseCustomWidget(bool state)
147{
148 m_useCustomWidget = state;
149}
150
151int ComboBox::addItem(Widget* item)
152{
153 bool sel_first = m_items.empty();
154
155 m_items.push_back(item);
156
157 if (sel_first && !isEditable())
158 setSelectedItemIndex(0);
159
160 return m_items.size()-1;
161}
162
163int ComboBox::addItem(const std::string& text)
164{
165 return addItem(new ListItem(text));
166}
167
168void ComboBox::insertItem(int itemIndex, Widget* item)
169{
170 bool sel_first = m_items.empty();
171
172 m_items.insert(m_items.begin() + itemIndex, item);
173
174 if (sel_first)
175 setSelectedItemIndex(0);
176}
177
178void ComboBox::insertItem(int itemIndex, const std::string& text)
179{
180 insertItem(itemIndex, new ListItem(text));
181}
182
183void ComboBox::removeItem(Widget* item)
184{
185 auto it = std::find(m_items.begin(), m_items.end(), item);
186 ASSERT(it != m_items.end());
187 if (it != m_items.end())
188 m_items.erase(it);
189
190 // Do not delete the given "item"
191}
192
193void ComboBox::deleteItem(int itemIndex)
194{
195 ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());
196
197 Widget* item = m_items[itemIndex];
198
199 m_items.erase(m_items.begin() + itemIndex);
200 delete item;
201}
202
203void ComboBox::deleteAllItems()
204{
205 // Delete all items back to front, in this way Widget::removeChild()
206 // doesn't have to use linear search to update m_parentIndex of all
207 // other children.
208 auto end = m_items.rend();
209 for (auto it=m_items.rbegin(); it != end; ++it)
210 delete *it; // widget
211
212 m_items.clear();
213 m_selected = -1;
214}
215
216int ComboBox::getItemCount() const
217{
218 return m_items.size();
219}
220
221Widget* ComboBox::getItem(const int itemIndex) const
222{
223 if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size()) {
224 return m_items[itemIndex];
225 }
226 else
227 return nullptr;
228}
229
230const std::string& ComboBox::getItemText(int itemIndex) const
231{
232 if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size()) {
233 Widget* item = m_items[itemIndex];
234 return item->text();
235 }
236 else {
237 // Returns the text of the combo-box (it should be empty).
238 ASSERT(text().empty());
239 return text();
240 }
241}
242
243void ComboBox::setItemText(int itemIndex, const std::string& text)
244{
245 ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());
246
247 Widget* item = m_items[itemIndex];
248 item->setText(text);
249}
250
251int ComboBox::findItemIndex(const std::string& text) const
252{
253 int i = 0;
254 for (const Widget* item : m_items) {
255 if ((m_casesensitive && item->text() == text) ||
256 (!m_casesensitive && item->text() == text)) {
257 return i;
258 }
259 i++;
260 }
261 return -1;
262}
263
264int ComboBox::findItemIndexByValue(const std::string& value) const
265{
266 int i = 0;
267 for (const Widget* item : m_items) {
268 if (auto listItem = dynamic_cast<const ListItem*>(item)) {
269 if (listItem->getValue() == value)
270 return i;
271 }
272 ++i;
273 }
274 return -1;
275}
276
277Widget* ComboBox::getSelectedItem() const
278{
279 return getItem(m_selected);
280}
281
282void ComboBox::setSelectedItem(Widget* item)
283{
284 auto it = std::find(m_items.begin(), m_items.end(), item);
285 if (it != m_items.end())
286 setSelectedItemIndex(std::distance(m_items.begin(), it));
287 else if (m_selected >= 0) {
288 m_selected = -1;
289 onChange();
290 }
291}
292
293int ComboBox::getSelectedItemIndex() const
294{
295 return (!m_items.empty() ? m_selected: -1);
296}
297
298void ComboBox::setSelectedItemIndex(int itemIndex)
299{
300 if (itemIndex >= 0 &&
301 (std::size_t)itemIndex < m_items.size() &&
302 m_selected != itemIndex) {
303 m_selected = itemIndex;
304
305 auto it = m_items.begin() + itemIndex;
306 Widget* item = *it;
307 m_entry->setText(item->text());
308 if (isEditable())
309 m_entry->setCaretToEnd();
310
311 onChange();
312 }
313}
314
315std::string ComboBox::getValue() const
316{
317 if (isEditable())
318 return m_entry->text();
319 int index = getSelectedItemIndex();
320 if (index >= 0) {
321 if (auto listItem = dynamic_cast<ListItem*>(m_items[index]))
322 return listItem->getValue();
323 }
324 return std::string();
325}
326
327void ComboBox::setValue(const std::string& value)
328{
329 if (isEditable()) {
330 m_entry->setText(value);
331 if (hasFocus())
332 m_entry->selectAllText();
333 }
334 else {
335 int index = findItemIndexByValue(value);
336 if (index >= 0)
337 setSelectedItemIndex(index);
338 }
339}
340
341Entry* ComboBox::getEntryWidget()
342{
343 return m_entry;
344}
345
346Button* ComboBox::getButtonWidget()
347{
348 return m_button;
349}
350
351bool ComboBox::onProcessMessage(Message* msg)
352{
353 switch (msg->type()) {
354
355 case kCloseMessage:
356 closeListBox();
357 break;
358
359 case kWinMoveMessage:
360 // If we mouse the parent window, we close the list box popup.
361 closeListBox();
362 break;
363
364 case kKeyDownMessage:
365 if (m_window) {
366 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
367 KeyScancode scancode = keymsg->scancode();
368
369 // If the popup is opened
370 if (scancode == kKeyEsc) {
371 closeListBox();
372 return true;
373 }
374 }
375 break;
376
377 case kMouseDownMessage:
378 if (m_window) {
379 if (View::getView(m_listbox)->hasMouse()) {
380 // As we are filtering the kMouseDownMessage, and the
381 // ListBox has the mouse, we "return false" here to say "we
382 // are not interested in this mouse message, it will be
383 // processed by the ListBox itself". In other case, if we
384 // "break" and call Widget::onProcessMessage(), the message
385 // will be propagated to the parent window and could be used
386 // to move the parent window (instead of clicking a listbox
387 // item of the popup m_window).
388 return false;
389 }
390 else {
391 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
392
393 // Use the nativeWindow() from the mouseMsg before we close
394 // the listbox because the mouseMsg->display() could be from
395 // the same popup.
396 const gfx::Point screenPos =
397 mouseMsg->display()->nativeWindow()->pointToScreen(mouseMsg->position());
398
399 closeListBox();
400
401 Widget* pick = manager()->pickFromScreenPos(screenPos);
402 if (pick && pick->hasAncestor(this))
403 return true;
404 }
405 }
406 break;
407
408 case kFocusEnterMessage:
409 // Here we focus the entry field only if the combobox is
410 // editable and receives the focus in a direct way (e.g. when
411 // the window was just opened and the combobox is the first
412 // child or has the "focus magnet" flag enabled.)
413 if ((isEditable()) &&
414 (manager()->getFocus() == this)) {
415 m_entry->requestFocus();
416 }
417 break;
418
419 }
420
421 return Widget::onProcessMessage(msg);
422}
423
424void ComboBox::onInitTheme(InitThemeEvent& ev)
425{
426 Widget::onInitTheme(ev);
427 if (m_window) {
428 m_window->initTheme();
429 m_window->noBorderNoChildSpacing();
430 }
431}
432
433void ComboBox::onResize(ResizeEvent& ev)
434{
435 gfx::Rect bounds = ev.bounds();
436 setBoundsQuietly(bounds);
437
438 // Button
439 Size buttonSize = m_button->sizeHint();
440 m_button->setBounds(Rect(bounds.x2() - buttonSize.w, bounds.y,
441 buttonSize.w, bounds.h));
442
443 // Entry
444 m_entry->setBounds(Rect(bounds.x, bounds.y,
445 bounds.w - buttonSize.w, bounds.h));
446
447 putSelectedItemAsCustomWidget();
448}
449
450void ComboBox::onSizeHint(SizeHintEvent& ev)
451{
452 Size reqSize(0, 0);
453
454 // Calculate the max required width depending on the text-length of
455 // each item.
456 for (const auto& item : m_items)
457 reqSize |= Entry::sizeHintWithText(m_entry, item->text());
458
459 Size buttonSize = m_button->sizeHint();
460 reqSize.w += buttonSize.w;
461 reqSize.h = std::max(reqSize.h, buttonSize.h);
462
463 ev.setSizeHint(reqSize);
464}
465
466bool ComboBoxEntry::onProcessMessage(Message* msg)
467{
468 switch (msg->type()) {
469
470 case kKeyDownMessage:
471 if (hasFocus()) {
472 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
473 KeyScancode scancode = keymsg->scancode();
474
475 // In a non-editable ComboBox
476 if (!m_comboBox->isEditable()) {
477 if (scancode == kKeySpace ||
478 scancode == kKeyEnter ||
479 scancode == kKeyEnterPad) {
480 m_comboBox->switchListBox();
481 return true;
482 }
483 }
484 // In a editable ComboBox
485 else {
486 if (scancode == kKeyUp ||
487 scancode == kKeyDown ||
488 scancode == kKeyPageUp ||
489 scancode == kKeyPageDown) {
490 if (m_comboBox->m_listbox &&
491 m_comboBox->m_listbox->isVisible()) {
492 m_comboBox->m_listbox->requestFocus();
493 m_comboBox->m_listbox->sendMessage(msg);
494 return true;
495 }
496 }
497 else if (scancode == kKeyEnter ||
498 scancode == kKeyEnterPad) {
499 m_comboBox->onEnterOnEditableEntry();
500 }
501 }
502 }
503 break;
504
505 case kMouseDownMessage:
506 if (m_comboBox->isClickOpen() &&
507 (!m_comboBox->isEditable() ||
508 !m_comboBox->m_items.empty())) {
509 m_comboBox->switchListBox();
510 }
511
512 if (m_comboBox->isEditable()) {
513 requestFocus();
514 }
515 else {
516 captureMouse();
517 return true;
518 }
519 break;
520
521 case kMouseUpMessage:
522 if (hasCapture())
523 releaseMouse();
524 break;
525
526 case kMouseMoveMessage:
527 if (hasCapture()) {
528 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
529 gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(mouseMsg->position());
530 Widget* pick = manager()->pickFromScreenPos(screenPos);
531 Widget* listbox = m_comboBox->m_listbox;
532
533 if (pick != nullptr &&
534 (pick == listbox || pick->hasAncestor(listbox))) {
535 releaseMouse();
536
537 MouseMessage mouseMsg2(
538 kMouseDownMessage,
539 *mouseMsg,
540 mouseMsg->positionForDisplay(pick->display()));
541 mouseMsg2.setDisplay(pick->display());
542 pick->sendMessage(&mouseMsg2);
543 return true;
544 }
545 }
546 break;
547
548 case kFocusEnterMessage: {
549 bool result = Entry::onProcessMessage(msg);
550 if (m_comboBox &&
551 m_comboBox->isEditable() &&
552 m_comboBox->m_listbox &&
553 m_comboBox->m_listbox->isVisible()) {
554 // In case that the ListBox is visible and the focus is
555 // obtained by the Entry field, we set the carret at the end
556 // of the text. We don't select the whole text so the user can
557 // delete the last caracters using backspace and complete the
558 // item name.
559 setCaretToEnd();
560 }
561 return result;
562 }
563
564 case kFocusLeaveMessage:
565 if (m_comboBox->isEditable() &&
566 m_comboBox->m_window &&
567 !View::getView(m_comboBox->m_listbox)->hasMouse()) {
568 m_comboBox->closeListBox();
569 }
570 break;
571
572 }
573
574 return Entry::onProcessMessage(msg);
575}
576
577void ComboBoxEntry::onPaint(PaintEvent& ev)
578{
579 theme()->paintComboBoxEntry(ev);
580}
581
582void ComboBoxEntry::onChange()
583{
584 Entry::onChange();
585 if (m_comboBox &&
586 m_comboBox->isEditable()) {
587 m_comboBox->onEntryChange();
588 }
589}
590
591bool ComboBoxListBox::onProcessMessage(Message* msg)
592{
593 switch (msg->type()) {
594
595 case kMouseUpMessage:
596 m_comboBox->closeListBox();
597 return true;
598
599 case kKeyDownMessage:
600 if (hasFocus()) {
601 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
602 KeyScancode scancode = keymsg->scancode();
603
604 if (scancode == kKeySpace ||
605 scancode == kKeyEnter ||
606 scancode == kKeyEnterPad) {
607 m_comboBox->closeListBox();
608 return true;
609 }
610 }
611 break;
612
613 case kFocusEnterMessage:
614 // If the ComboBox is editable, we prefer the focus in the Entry
615 // field (so the user can continue editing it).
616 if (m_comboBox->isEditable())
617 m_comboBox->getEntryWidget()->requestFocus();
618 break;
619 }
620
621 return ListBox::onProcessMessage(msg);
622}
623
624void ComboBoxListBox::onChange()
625{
626 ListBox::onChange();
627
628 int index = getSelectedIndex();
629 if (isValidItem(index))
630 m_comboBox->setSelectedItemIndex(index);
631}
632
633// When the mouse is clicked we switch the visibility-status of the list-box
634void ComboBox::onButtonClick(Event& ev)
635{
636 switchListBox();
637}
638
639void ComboBox::openListBox()
640{
641 if (!isEnabled() || m_window)
642 return;
643
644 onBeforeOpenListBox();
645
646 m_window = new Window(Window::WithoutTitleBar);
647 View* view = new View();
648 m_listbox = new ComboBoxListBox(this);
649 m_window->setAutoRemap(false);
650 m_window->setOnTop(true);
651 m_window->setWantFocus(false);
652 m_window->setSizeable(false);
653 m_window->setMoveable(false);
654
655 Widget* viewport = view->viewport();
656 {
657 gfx::Rect entryBounds = m_entry->bounds();
658 gfx::Size size;
659 size.w = m_button->bounds().x2() - entryBounds.x - view->border().width();
660 size.h = viewport->border().height();
661 for (Widget* item : m_items)
662 if (!item->hasFlags(HIDDEN))
663 size.h += item->sizeHint().h;
664
665 if (!get_multiple_displays()) {
666 const int maxVal =
667 std::max(entryBounds.y, display()->size().h - entryBounds.y2())
668 - 8*guiscale();
669 size.h = std::clamp(size.h, textHeight(), maxVal);
670 }
671
672 viewport->setMinSize(size);
673 }
674
675 m_window->addChild(view);
676 view->attachToView(m_listbox);
677
678 m_listbox->selectIndex(m_selected);
679
680 initTheme();
681 m_window->remapWindow();
682
683 updateListBoxPos();
684 m_window->openWindow();
685
686 filterMessages();
687
688 if (isEditable())
689 m_entry->requestFocus();
690 else
691 m_listbox->requestFocus();
692
693 onOpenListBox();
694}
695
696void ComboBox::closeListBox()
697{
698 if (m_window) {
699 removeMessageFilters();
700
701 m_listbox->clean();
702
703 m_window->closeWindow(this);
704 delete m_window; // window, frame
705
706 m_window = nullptr;
707 m_listbox = nullptr;
708
709 putSelectedItemAsCustomWidget();
710 m_entry->requestFocus();
711
712 onCloseListBox();
713 }
714}
715
716void ComboBox::switchListBox()
717{
718 if (!m_window)
719 openListBox();
720 else
721 closeListBox();
722}
723
724void ComboBox::updateListBoxPos()
725{
726 gfx::Rect entryBounds = m_entry->bounds();
727 gfx::Rect rc(gfx::Point(entryBounds.x,
728 entryBounds.y2()),
729 gfx::Point(m_button->bounds().x2(),
730 entryBounds.y2() + m_window->bounds().h));
731
732 fit_bounds(
733 display(),
734 m_window,
735 rc,
736 [this](const gfx::Rect& workarea,
737 gfx::Rect& bounds,
738 std::function<gfx::Rect(Widget*)> getWidgetBounds) {
739 if (bounds.y2() > workarea.y2())
740 bounds.offset(0, -(bounds.h + getWidgetBounds(m_entry).h));
741 });
742}
743
744void ComboBox::onChange()
745{
746 Change();
747}
748
749void ComboBox::onEntryChange()
750{
751 // Do nothing
752}
753
754void ComboBox::onBeforeOpenListBox()
755{
756 // Do nothing
757}
758
759void ComboBox::onOpenListBox()
760{
761 OpenListBox();
762}
763
764void ComboBox::onCloseListBox()
765{
766 CloseListBox();
767}
768
769void ComboBox::onEnterOnEditableEntry()
770{
771 // Do nothing
772}
773
774void ComboBox::filterMessages()
775{
776 if (!m_filtering) {
777 manager()->addMessageFilter(kMouseDownMessage, this);
778 manager()->addMessageFilter(kKeyDownMessage, this);
779 m_filtering = true;
780 }
781}
782
783void ComboBox::removeMessageFilters()
784{
785 if (m_filtering) {
786 manager()->removeMessageFilter(kMouseDownMessage, this);
787 manager()->removeMessageFilter(kKeyDownMessage, this);
788 m_filtering = false;
789 }
790}
791
792void ComboBox::putSelectedItemAsCustomWidget()
793{
794 if (!useCustomWidget())
795 return;
796
797 Widget* item = getSelectedItem();
798 if (item && item->parent() == nullptr) {
799 if (!m_listbox) {
800 item->setBounds(m_entry->childrenBounds());
801 m_entry->addChild(item);
802 }
803 else {
804 m_entry->removeChild(item);
805 }
806 }
807}
808
809} // namespace ui
810