aseprite/src/ui/combobox.cpp

772 lines
18 KiB
C++
Raw Normal View History

// Aseprite UI Library
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
2007-09-19 07:57:02 +08:00
#ifdef HAVE_CONFIG_H
2024-12-16 21:10:34 +08:00
#include "config.h"
#endif
2009-07-13 04:29:16 +08:00
#include "ui/combobox.h"
#include "gfx/size.h"
2018-08-09 23:58:43 +08:00
#include "os/font.h"
#include "ui/button.h"
#include "ui/entry.h"
#include "ui/fit_bounds.h"
#include "ui/listbox.h"
#include "ui/listitem.h"
#include "ui/manager.h"
#include "ui/message.h"
#include "ui/resize_event.h"
#include "ui/scale.h"
#include "ui/size_hint_event.h"
#include "ui/system.h"
#include "ui/theme.h"
#include "ui/view.h"
#include "ui/window.h"
2007-09-19 07:57:02 +08:00
#include <algorithm>
namespace ui {
using namespace gfx;
class ComboBoxButton : public Button {
public:
2024-12-16 21:10:34 +08:00
ComboBoxButton() : Button("") { setFocusStop(false); }
};
class ComboBoxEntry : public Entry {
public:
2024-12-16 21:10:34 +08:00
ComboBoxEntry(ComboBox* comboBox) : Entry(256, ""), m_comboBox(comboBox) {}
protected:
bool onProcessMessage(Message* msg) override;
void onPaint(PaintEvent& ev) override;
void onChange() override;
private:
ComboBox* m_comboBox;
};
class ComboBoxListBox : public ListBox {
public:
2024-12-16 21:10:34 +08:00
ComboBoxListBox(ComboBox* comboBox) : m_comboBox(comboBox)
{
for (auto item : *comboBox) {
if (item->parent())
item->parent()->removeChild(item);
addChild(item);
}
}
2024-12-16 21:10:34 +08:00
void clean()
{
// Remove all added items so ~Widget() don't delete them.
removeAllChildren();
selectChild(nullptr);
}
protected:
bool onProcessMessage(Message* msg) override;
void onChange() override;
private:
2024-12-16 21:10:34 +08:00
bool isValidItem(int index) const { return (index >= 0 && index < m_comboBox->getItemCount()); }
ComboBox* m_comboBox;
};
ComboBox::ComboBox()
: Widget(kComboBoxWidget)
, m_entry(new ComboBoxEntry(this))
, m_button(new ComboBoxButton())
, m_window(nullptr)
, m_listbox(nullptr)
, m_selected(-1)
, m_editable(false)
, m_clickopen(true)
, m_casesensitive(true)
, m_filtering(false)
, m_useCustomWidget(false)
2007-09-19 07:57:02 +08:00
{
m_entry->setExpansive(true);
// When the "m_button" is clicked ("Click" signal) call onButtonClick() method
m_button->Click.connect(&ComboBox::onButtonClick, this);
2007-09-19 07:57:02 +08:00
addChild(m_entry);
addChild(m_button);
2007-09-19 07:57:02 +08:00
setFocusStop(true);
setEditable(m_editable);
initTheme();
2007-09-19 07:57:02 +08:00
}
ComboBox::~ComboBox()
2007-09-19 07:57:02 +08:00
{
removeMessageFilters();
deleteAllItems();
}
2007-09-19 07:57:02 +08:00
void ComboBox::setEditable(bool state)
{
m_editable = state;
2007-09-19 07:57:02 +08:00
if (state) {
2010-12-09 01:28:13 +08:00
m_entry->setReadOnly(false);
m_entry->showCaret();
2007-09-19 07:57:02 +08:00
}
else {
2010-12-09 01:28:13 +08:00
m_entry->setReadOnly(true);
m_entry->hideCaret();
2007-09-19 07:57:02 +08:00
}
}
void ComboBox::setClickOpen(bool state)
2007-09-19 07:57:02 +08:00
{
m_clickopen = state;
2007-09-19 07:57:02 +08:00
}
void ComboBox::setCaseSensitive(bool state)
2007-09-19 07:57:02 +08:00
{
m_casesensitive = state;
2007-09-19 07:57:02 +08:00
}
void ComboBox::setUseCustomWidget(bool state)
{
m_useCustomWidget = state;
}
int ComboBox::addItem(Widget* item)
2007-09-19 07:57:02 +08:00
{
bool sel_first = m_items.empty();
2007-09-19 07:57:02 +08:00
m_items.push_back(item);
2007-09-19 07:57:02 +08:00
if (sel_first && !isEditable())
setSelectedItemIndex(0);
2024-12-16 21:10:34 +08:00
return m_items.size() - 1;
2007-09-19 07:57:02 +08:00
}
int ComboBox::addItem(const std::string& text)
{
return addItem(new ListItem(text));
}
void ComboBox::insertItem(int itemIndex, Widget* item)
{
bool sel_first = m_items.empty();
m_items.insert(m_items.begin() + itemIndex, item);
if (sel_first)
setSelectedItemIndex(0);
}
void ComboBox::insertItem(int itemIndex, const std::string& text)
{
insertItem(itemIndex, new ListItem(text));
}
void ComboBox::removeItem(Widget* item)
{
auto it = std::find(m_items.begin(), m_items.end(), item);
ASSERT(it != m_items.end());
if (it != m_items.end())
m_items.erase(it);
// Do not delete the given "item"
}
void ComboBox::deleteItem(int itemIndex)
2007-09-19 07:57:02 +08:00
{
ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());
2007-09-19 07:57:02 +08:00
Widget* item = m_items[itemIndex];
2007-09-19 07:57:02 +08:00
m_items.erase(m_items.begin() + itemIndex);
delete item;
}
void ComboBox::deleteAllItems()
{
// Delete all items back to front, in this way Widget::removeChild()
// doesn't have to use linear search to update m_parentIndex of all
// other children.
auto end = m_items.rend();
2024-12-16 21:10:34 +08:00
for (auto it = m_items.rbegin(); it != end; ++it)
delete *it; // widget
m_items.clear();
m_selected = -1;
2007-09-19 07:57:02 +08:00
}
int ComboBox::getItemCount() const
2007-09-19 07:57:02 +08:00
{
return m_items.size();
}
Widget* ComboBox::getItem(const int itemIndex) const
{
if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size()) {
return m_items[itemIndex];
}
else
2020-04-09 04:48:06 +08:00
return nullptr;
}
const std::string& ComboBox::getItemText(int itemIndex) const
{
if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size()) {
Widget* item = m_items[itemIndex];
return item->text();
2007-09-19 07:57:02 +08:00
}
else {
// Returns the text of the combo-box (it should be empty).
ASSERT(text().empty());
return text();
}
2007-09-19 07:57:02 +08:00
}
void ComboBox::setItemText(int itemIndex, const std::string& text)
2007-09-19 07:57:02 +08:00
{
ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());
Widget* item = m_items[itemIndex];
item->setText(text);
2007-09-19 07:57:02 +08:00
}
2015-04-09 18:46:55 +08:00
int ComboBox::findItemIndex(const std::string& text) const
2007-09-19 07:57:02 +08:00
{
2015-04-09 18:46:55 +08:00
int i = 0;
for (const Widget* item : m_items) {
2024-12-16 21:10:34 +08:00
if ((m_casesensitive && item->text() == text) || (!m_casesensitive && item->text() == text)) {
2015-04-09 18:46:55 +08:00
return i;
}
2015-04-09 18:46:55 +08:00
i++;
}
2015-04-09 18:46:55 +08:00
return -1;
}
2015-04-09 18:46:55 +08:00
int ComboBox::findItemIndexByValue(const std::string& value) const
{
int i = 0;
for (const Widget* item : m_items) {
if (auto listItem = dynamic_cast<const ListItem*>(item)) {
if (listItem->getValue() == value)
return i;
}
++i;
2015-04-09 18:46:55 +08:00
}
return -1;
2007-09-19 07:57:02 +08:00
}
Widget* ComboBox::getSelectedItem() const
2007-09-19 07:57:02 +08:00
{
return getItem(m_selected);
2007-09-19 07:57:02 +08:00
}
void ComboBox::setSelectedItem(Widget* item)
2007-09-19 07:57:02 +08:00
{
auto it = std::find(m_items.begin(), m_items.end(), item);
if (it != m_items.end())
setSelectedItemIndex(std::distance(m_items.begin(), it));
else if (m_selected >= 0) {
m_selected = -1;
onChange();
}
}
int ComboBox::getSelectedItemIndex() const
{
2024-12-16 21:10:34 +08:00
return (!m_items.empty() ? m_selected : -1);
2007-09-19 07:57:02 +08:00
}
void ComboBox::setSelectedItemIndex(int itemIndex)
2007-09-19 07:57:02 +08:00
{
2024-12-16 21:10:34 +08:00
if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size() && m_selected != itemIndex) {
m_selected = itemIndex;
2007-09-19 07:57:02 +08:00
auto it = m_items.begin() + itemIndex;
Widget* item = *it;
m_entry->setText(item->text());
if (isEditable())
m_entry->setCaretToEnd();
onChange();
}
2007-09-19 07:57:02 +08:00
}
2015-04-09 18:46:55 +08:00
std::string ComboBox::getValue() const
{
if (isEditable())
return m_entry->text();
int index = getSelectedItemIndex();
if (index >= 0) {
if (auto listItem = dynamic_cast<ListItem*>(m_items[index]))
return listItem->getValue();
}
return std::string();
2015-04-09 18:46:55 +08:00
}
void ComboBox::setValue(const std::string& value)
{
if (isEditable()) {
m_entry->setText(value);
if (hasFocus())
m_entry->selectAllText();
}
else {
int index = findItemIndexByValue(value);
if (index >= 0)
setSelectedItemIndex(index);
}
2015-04-09 18:46:55 +08:00
}
2010-12-09 01:28:13 +08:00
Entry* ComboBox::getEntryWidget()
2007-09-19 07:57:02 +08:00
{
return m_entry;
2007-09-19 07:57:02 +08:00
}
Button* ComboBox::getButtonWidget()
2007-09-19 07:57:02 +08:00
{
return m_button;
2007-09-19 07:57:02 +08:00
}
bool ComboBox::onProcessMessage(Message* msg)
{
switch (msg->type()) {
2024-12-16 21:10:34 +08:00
case kCloseMessage: closeListBox(); break;
case kWinMoveMessage:
// If we mouse the parent window, we close the list box popup.
closeListBox();
2007-09-19 07:57:02 +08:00
break;
2015-05-07 03:27:45 +08:00
case kKeyDownMessage:
if (m_window) {
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
KeyScancode scancode = keymsg->scancode();
// If the popup is opened
if (scancode == kKeyEsc) {
closeListBox();
return true;
}
}
break;
case kMouseDownMessage:
if (m_window) {
if (View::getView(m_listbox)->hasMouse()) {
// As we are filtering the kMouseDownMessage, and the
// ListBox has the mouse, we "return false" here to say "we
// are not interested in this mouse message, it will be
// processed by the ListBox itself". In other case, if we
// "break" and call Widget::onProcessMessage(), the message
// will be propagated to the parent window and could be used
// to move the parent window (instead of clicking a listbox
// item of the popup m_window).
return false;
}
else {
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
// Use the nativeWindow() from the mouseMsg before we close
// the listbox because the mouseMsg->display() could be from
// the same popup.
2024-12-16 21:10:34 +08:00
const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(
mouseMsg->position());
closeListBox();
Widget* pick = manager()->pickFromScreenPos(screenPos);
if (pick && pick->hasAncestor(this))
return true;
}
2007-09-19 07:57:02 +08:00
}
break;
case kFocusEnterMessage:
// Here we focus the entry field only if the combobox is
// editable and receives the focus in a direct way (e.g. when
// the window was just opened and the combobox is the first
// child or has the "focus magnet" flag enabled.)
2024-12-16 21:10:34 +08:00
if ((isEditable()) && (manager()->getFocus() == this)) {
m_entry->requestFocus();
}
break;
2007-09-19 07:57:02 +08:00
}
return Widget::onProcessMessage(msg);
2007-09-19 07:57:02 +08:00
}
void ComboBox::onInitTheme(InitThemeEvent& ev)
{
Widget::onInitTheme(ev);
if (m_window) {
m_window->initTheme();
m_window->noBorderNoChildSpacing();
}
}
void ComboBox::onResize(ResizeEvent& ev)
{
gfx::Rect bounds = ev.bounds();
setBoundsQuietly(bounds);
// Button
2015-12-04 08:50:05 +08:00
Size buttonSize = m_button->sizeHint();
2024-12-16 21:10:34 +08:00
m_button->setBounds(Rect(bounds.x2() - buttonSize.w, bounds.y, buttonSize.w, bounds.h));
// Entry
2024-12-16 21:10:34 +08:00
m_entry->setBounds(Rect(bounds.x, bounds.y, bounds.w - buttonSize.w, bounds.h));
putSelectedItemAsCustomWidget();
}
2015-12-04 08:50:05 +08:00
void ComboBox::onSizeHint(SizeHintEvent& ev)
{
Size reqSize(0, 0);
// Calculate the max required width depending on the text-length of
// each item.
for (const auto& item : m_items)
reqSize |= Entry::sizeHintWithText(m_entry, item->text());
2015-12-04 08:50:05 +08:00
Size buttonSize = m_button->sizeHint();
reqSize.w += buttonSize.w;
reqSize.h = std::max(reqSize.h, buttonSize.h);
2015-12-04 08:50:05 +08:00
ev.setSizeHint(reqSize);
}
bool ComboBoxEntry::onProcessMessage(Message* msg)
2007-09-19 07:57:02 +08:00
{
switch (msg->type()) {
case kKeyDownMessage:
if (hasFocus()) {
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
KeyScancode scancode = keymsg->scancode();
// In a non-editable ComboBox
if (!m_comboBox->isEditable()) {
2024-12-16 21:10:34 +08:00
if (scancode == kKeySpace || scancode == kKeyEnter || scancode == kKeyEnterPad) {
m_comboBox->switchListBox();
return true;
}
}
// In a editable ComboBox
else {
2024-12-16 21:10:34 +08:00
if (scancode == kKeyUp || scancode == kKeyDown || scancode == kKeyPageUp ||
scancode == kKeyPageDown) {
2024-12-16 21:10:34 +08:00
if (m_comboBox->m_listbox && m_comboBox->m_listbox->isVisible()) {
m_comboBox->m_listbox->requestFocus();
m_comboBox->m_listbox->sendMessage(msg);
return true;
}
}
2024-12-16 21:10:34 +08:00
else if (scancode == kKeyEnter || scancode == kKeyEnterPad) {
m_comboBox->onEnterOnEditableEntry();
}
}
2007-09-19 07:57:02 +08:00
}
break;
case kMouseDownMessage:
if (m_comboBox->isClickOpen() &&
2024-12-16 21:10:34 +08:00
(!m_comboBox->isEditable() || !m_comboBox->m_items.empty())) {
m_comboBox->switchListBox();
2007-12-05 05:50:31 +08:00
}
2007-09-19 07:57:02 +08:00
if (m_comboBox->isEditable()) {
requestFocus();
2007-09-19 07:57:02 +08:00
}
else {
captureMouse();
return true;
}
break;
case kMouseUpMessage:
if (hasCapture())
releaseMouse();
break;
case kMouseMoveMessage:
if (hasCapture()) {
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
2024-12-16 21:10:34 +08:00
gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(
mouseMsg->position());
Widget* pick = manager()->pickFromScreenPos(screenPos);
Widget* listbox = m_comboBox->m_listbox;
2024-12-16 21:10:34 +08:00
if (pick != nullptr && (pick == listbox || pick->hasAncestor(listbox))) {
releaseMouse();
2024-12-16 21:10:34 +08:00
MouseMessage mouseMsg2(kMouseDownMessage,
*mouseMsg,
mouseMsg->positionForDisplay(pick->display()));
mouseMsg2.setRecipient(pick);
mouseMsg2.setDisplay(pick->display());
2014-02-24 19:30:43 +08:00
pick->sendMessage(&mouseMsg2);
return true;
}
}
2007-09-19 07:57:02 +08:00
break;
2009-11-22 08:26:58 +08:00
case kFocusEnterMessage: {
bool result = Entry::onProcessMessage(msg);
2024-12-16 21:10:34 +08:00
if (m_comboBox && m_comboBox->isEditable() && m_comboBox->m_listbox &&
m_comboBox->m_listbox->isVisible()) {
// In case that the ListBox is visible and the focus is
// obtained by the Entry field, we set the carret at the end
// of the text. We don't select the whole text so the user can
// delete the last caracters using backspace and complete the
// item name.
setCaretToEnd();
}
return result;
}
case kFocusLeaveMessage:
2024-12-16 21:10:34 +08:00
if (m_comboBox->isEditable() && m_comboBox->m_window &&
!View::getView(m_comboBox->m_listbox)->hasMouse()) {
m_comboBox->closeListBox();
}
break;
2007-09-19 07:57:02 +08:00
}
return Entry::onProcessMessage(msg);
2007-09-19 07:57:02 +08:00
}
void ComboBoxEntry::onPaint(PaintEvent& ev)
{
theme()->paintComboBoxEntry(ev);
}
void ComboBoxEntry::onChange()
{
Entry::onChange();
2024-12-16 21:10:34 +08:00
if (m_comboBox && m_comboBox->isEditable()) {
m_comboBox->onEntryChange();
}
}
bool ComboBoxListBox::onProcessMessage(Message* msg)
2007-09-19 07:57:02 +08:00
{
switch (msg->type()) {
2024-12-16 21:10:34 +08:00
case kMouseUpMessage: m_comboBox->closeListBox(); return true;
2007-09-19 07:57:02 +08:00
case kKeyDownMessage:
if (hasFocus()) {
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
KeyScancode scancode = keymsg->scancode();
2024-12-16 21:10:34 +08:00
if (scancode == kKeySpace || scancode == kKeyEnter || scancode == kKeyEnterPad) {
m_comboBox->closeListBox();
return true;
}
2007-09-19 07:57:02 +08:00
}
break;
case kFocusEnterMessage:
// If the ComboBox is editable, we prefer the focus in the Entry
// field (so the user can continue editing it).
if (m_comboBox->isEditable())
m_comboBox->getEntryWidget()->requestFocus();
break;
2007-09-19 07:57:02 +08:00
}
return ListBox::onProcessMessage(msg);
}
void ComboBoxListBox::onChange()
{
ListBox::onChange();
int index = getSelectedIndex();
if (isValidItem(index))
m_comboBox->setSelectedItemIndex(index);
2007-09-19 07:57:02 +08:00
}
// When the mouse is clicked we switch the visibility-status of the list-box
void ComboBox::onButtonClick()
2007-09-19 07:57:02 +08:00
{
switchListBox();
2007-09-19 07:57:02 +08:00
}
void ComboBox::openListBox()
2007-09-19 07:57:02 +08:00
{
if (!isEnabled() || m_window)
return;
2007-09-19 07:57:02 +08:00
onBeforeOpenListBox();
m_window = new Window(Window::WithoutTitleBar);
View* view = new View();
m_listbox = new ComboBoxListBox(this);
m_window->setAutoRemap(false);
m_window->setOnTop(true);
m_window->setWantFocus(false);
m_window->setSizeable(false);
m_window->setMoveable(false);
2007-09-19 07:57:02 +08:00
Widget* viewport = view->viewport();
{
gfx::Rect entryBounds = m_entry->bounds();
gfx::Size size;
size.w = m_button->bounds().x2() - entryBounds.x - view->border().width();
size.h = viewport->border().height();
for (Widget* item : m_items)
if (!item->hasFlags(HIDDEN))
size.h += item->sizeHint().h;
if (!get_multiple_displays()) {
2024-12-16 21:10:34 +08:00
const int maxVal = std::max(entryBounds.y, display()->size().h - entryBounds.y2()) -
8 * guiscale();
2022-06-10 21:31:13 +08:00
size.h = std::clamp(size.h, textHeight(), maxVal);
}
viewport->setMinSize(size);
}
2007-09-19 07:57:02 +08:00
m_window->addChild(view);
view->attachToView(m_listbox);
2007-09-19 07:57:02 +08:00
m_listbox->selectIndex(m_selected);
2007-09-19 07:57:02 +08:00
initTheme();
m_window->remapWindow();
2007-09-19 07:57:02 +08:00
updateListBoxPos();
2015-05-07 03:27:45 +08:00
m_window->openWindow();
filterMessages();
if (isEditable())
m_entry->requestFocus();
else
m_listbox->requestFocus();
onOpenListBox();
2007-09-19 07:57:02 +08:00
}
void ComboBox::closeListBox()
2007-09-19 07:57:02 +08:00
{
if (m_window) {
removeMessageFilters();
m_listbox->clean();
m_window->closeWindow(this);
2024-12-16 21:10:34 +08:00
delete m_window; // window, frame
m_window = nullptr;
m_listbox = nullptr;
2007-09-19 07:57:02 +08:00
putSelectedItemAsCustomWidget();
m_entry->requestFocus();
onCloseListBox();
2007-09-19 07:57:02 +08:00
}
}
void ComboBox::switchListBox()
2007-09-19 07:57:02 +08:00
{
if (!m_window)
openListBox();
2007-09-19 07:57:02 +08:00
else
closeListBox();
2007-09-19 07:57:02 +08:00
}
void ComboBox::updateListBoxPos()
2007-09-19 07:57:02 +08:00
{
gfx::Rect entryBounds = m_entry->bounds();
2024-12-16 21:10:34 +08:00
gfx::Rect rc(gfx::Point(entryBounds.x, entryBounds.y2()),
gfx::Point(m_button->bounds().x2(), entryBounds.y2() + m_window->bounds().h));
fit_bounds(display(),
m_window,
rc,
[this](const gfx::Rect& workarea,
gfx::Rect& bounds,
std::function<gfx::Rect(Widget*)> getWidgetBounds) {
if (bounds.y2() > workarea.y2())
bounds.offset(0, -(bounds.h + getWidgetBounds(m_entry).h));
});
2007-09-19 07:57:02 +08:00
}
void ComboBox::onChange()
{
Change();
}
void ComboBox::onEntryChange()
{
// Do nothing
}
void ComboBox::onBeforeOpenListBox()
{
// Do nothing
}
void ComboBox::onOpenListBox()
{
OpenListBox();
}
void ComboBox::onCloseListBox()
{
CloseListBox();
}
void ComboBox::onEnterOnEditableEntry()
{
// Do nothing
}
void ComboBox::filterMessages()
{
if (!m_filtering) {
manager()->addMessageFilter(kMouseDownMessage, this);
manager()->addMessageFilter(kKeyDownMessage, this);
m_filtering = true;
}
}
void ComboBox::removeMessageFilters()
{
if (m_filtering) {
manager()->removeMessageFilter(kMouseDownMessage, this);
manager()->removeMessageFilter(kKeyDownMessage, this);
m_filtering = false;
}
}
void ComboBox::putSelectedItemAsCustomWidget()
{
if (!useCustomWidget())
return;
Widget* item = getSelectedItem();
if (item && item->parent() == nullptr) {
if (!m_listbox) {
item->setBounds(m_entry->childrenBounds());
m_entry->addChild(item);
}
else {
m_entry->removeChild(item);
}
}
}
} // namespace ui