aseprite/src/ui/combobox.cpp
2020-04-08 17:48:06 -03:00

763 lines
16 KiB
C++

// Aseprite UI Library
// Copyright (C) 2018-2020 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.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "ui/combobox.h"
#include "base/clamp.h"
#include "gfx/size.h"
#include "os/font.h"
#include "ui/button.h"
#include "ui/entry.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"
#include <algorithm>
namespace ui {
using namespace gfx;
class ComboBoxButton : public Button {
public:
ComboBoxButton() : Button("") {
setFocusStop(false);
}
};
class ComboBoxEntry : public Entry {
public:
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:
ComboBoxListBox(ComboBox* comboBox)
: m_comboBox(comboBox) {
for (auto item : *comboBox) {
if (item->parent())
item->parent()->removeChild(item);
addChild(item);
}
}
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:
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)
{
m_entry->setExpansive(true);
// When the "m_button" is clicked ("Click" signal) call onButtonClick() method
m_button->Click.connect(&ComboBox::onButtonClick, this);
addChild(m_entry);
addChild(m_button);
setFocusStop(true);
setEditable(m_editable);
initTheme();
}
ComboBox::~ComboBox()
{
removeMessageFilters();
deleteAllItems();
}
void ComboBox::setEditable(bool state)
{
m_editable = state;
if (state) {
m_entry->setReadOnly(false);
m_entry->showCaret();
}
else {
m_entry->setReadOnly(true);
m_entry->hideCaret();
}
}
void ComboBox::setClickOpen(bool state)
{
m_clickopen = state;
}
void ComboBox::setCaseSensitive(bool state)
{
m_casesensitive = state;
}
void ComboBox::setUseCustomWidget(bool state)
{
m_useCustomWidget = state;
}
int ComboBox::addItem(Widget* item)
{
bool sel_first = m_items.empty();
m_items.push_back(item);
if (sel_first && !isEditable())
setSelectedItemIndex(0);
return m_items.size()-1;
}
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)
{
ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());
Widget* item = m_items[itemIndex];
m_items.erase(m_items.begin() + itemIndex);
delete item;
}
void ComboBox::deleteAllItems()
{
for (Widget* item : m_items)
delete item; // widget
m_items.clear();
m_selected = -1;
}
int ComboBox::getItemCount() const
{
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
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();
}
else {
// Returns the text of the combo-box (it should be empty).
ASSERT(text().empty());
return text();
}
}
void ComboBox::setItemText(int itemIndex, const std::string& text)
{
ASSERT(itemIndex >= 0 && (std::size_t)itemIndex < m_items.size());
Widget* item = m_items[itemIndex];
item->setText(text);
}
int ComboBox::findItemIndex(const std::string& text) const
{
int i = 0;
for (const Widget* item : m_items) {
if ((m_casesensitive && item->text() == text) ||
(!m_casesensitive && item->text() == text)) {
return i;
}
i++;
}
return -1;
}
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;
}
return -1;
}
Widget* ComboBox::getSelectedItem() const
{
return getItem(m_selected);
}
void ComboBox::setSelectedItem(Widget* item)
{
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
{
return (!m_items.empty() ? m_selected: -1);
}
void ComboBox::setSelectedItemIndex(int itemIndex)
{
if (itemIndex >= 0 &&
(std::size_t)itemIndex < m_items.size() &&
m_selected != itemIndex) {
m_selected = itemIndex;
auto it = m_items.begin() + itemIndex;
Widget* item = *it;
m_entry->setText(item->text());
if (isEditable())
m_entry->setCaretToEnd();
onChange();
}
}
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();
}
void ComboBox::setValue(const std::string& value)
{
if (isEditable()) {
m_entry->setText(value);
m_entry->selectAllText();
}
else {
int index = findItemIndexByValue(value);
if (index >= 0)
setSelectedItemIndex(index);
}
}
Entry* ComboBox::getEntryWidget()
{
return m_entry;
}
Button* ComboBox::getButtonWidget()
{
return m_button;
}
bool ComboBox::onProcessMessage(Message* msg)
{
switch (msg->type()) {
case kCloseMessage:
closeListBox();
break;
case kWinMoveMessage:
if (m_window)
m_window->moveWindow(getListBoxPos());
break;
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()) {
closeListBox();
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
Widget* pick = manager()->pick(mouseMsg->position());
if (pick && pick->hasAncestor(this))
return true;
}
}
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.)
if ((isEditable()) &&
(manager()->getFocus() == this)) {
m_entry->requestFocus();
}
break;
}
return Widget::onProcessMessage(msg);
}
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
Size buttonSize = m_button->sizeHint();
m_button->setBounds(Rect(bounds.x2() - buttonSize.w, bounds.y,
buttonSize.w, bounds.h));
// Entry
m_entry->setBounds(Rect(bounds.x, bounds.y,
bounds.w - buttonSize.w, bounds.h));
putSelectedItemAsCustomWidget();
}
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());
Size buttonSize = m_button->sizeHint();
reqSize.w += buttonSize.w;
reqSize.h = std::max(reqSize.h, buttonSize.h);
ev.setSizeHint(reqSize);
}
bool ComboBoxEntry::onProcessMessage(Message* msg)
{
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()) {
if (scancode == kKeySpace ||
scancode == kKeyEnter ||
scancode == kKeyEnterPad) {
m_comboBox->switchListBox();
return true;
}
}
// In a editable ComboBox
else {
if (scancode == kKeyUp ||
scancode == kKeyDown ||
scancode == kKeyPageUp ||
scancode == kKeyPageDown) {
if (m_comboBox->m_listbox &&
m_comboBox->m_listbox->isVisible()) {
m_comboBox->m_listbox->requestFocus();
m_comboBox->m_listbox->sendMessage(msg);
return true;
}
}
else if (scancode == kKeyEnter ||
scancode == kKeyEnterPad) {
m_comboBox->onEnterOnEditableEntry();
}
}
}
break;
case kMouseDownMessage:
if (m_comboBox->isClickOpen() &&
(!m_comboBox->isEditable() ||
!m_comboBox->m_items.empty())) {
m_comboBox->switchListBox();
}
if (m_comboBox->isEditable()) {
requestFocus();
}
else {
captureMouse();
return true;
}
break;
case kMouseUpMessage:
if (hasCapture())
releaseMouse();
break;
case kMouseMoveMessage:
if (hasCapture()) {
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
Widget* pick = manager()->pick(mouseMsg->position());
Widget* listbox = m_comboBox->m_listbox;
if (pick != nullptr &&
(pick == listbox || pick->hasAncestor(listbox))) {
releaseMouse();
MouseMessage mouseMsg2(kMouseDownMessage,
mouseMsg->pointerType(),
mouseMsg->button(),
mouseMsg->modifiers(),
mouseMsg->position());
pick->sendMessage(&mouseMsg2);
return true;
}
}
break;
case kFocusEnterMessage: {
bool result = Entry::onProcessMessage(msg);
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;
}
}
return Entry::onProcessMessage(msg);
}
void ComboBoxEntry::onPaint(PaintEvent& ev)
{
theme()->paintComboBoxEntry(ev);
}
void ComboBoxEntry::onChange()
{
Entry::onChange();
if (m_comboBox &&
m_comboBox->isEditable()) {
m_comboBox->onEntryChange();
}
}
bool ComboBoxListBox::onProcessMessage(Message* msg)
{
switch (msg->type()) {
case kMouseUpMessage:
m_comboBox->closeListBox();
return true;
case kKeyDownMessage:
if (hasFocus()) {
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
KeyScancode scancode = keymsg->scancode();
if (scancode == kKeySpace ||
scancode == kKeyEnter ||
scancode == kKeyEnterPad) {
m_comboBox->closeListBox();
return true;
}
}
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;
}
return ListBox::onProcessMessage(msg);
}
void ComboBoxListBox::onChange()
{
ListBox::onChange();
int index = getSelectedIndex();
if (isValidItem(index))
m_comboBox->setSelectedItemIndex(index);
}
// When the mouse is clicked we switch the visibility-status of the list-box
void ComboBox::onButtonClick(Event& ev)
{
switchListBox();
}
void ComboBox::openListBox()
{
if (!isEnabled() || m_window)
return;
onBeforeOpenListBox();
m_window = new Window(Window::WithoutTitleBar);
View* view = new View();
m_listbox = new ComboBoxListBox(this);
m_window->setOnTop(true);
m_window->setWantFocus(false);
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;
int max = std::max(entryBounds.y, ui::display_h() - entryBounds.y2()) - 8*guiscale();
size.h = base::clamp(size.h, textHeight(), max);
viewport->setMinSize(size);
}
m_window->addChild(view);
view->attachToView(m_listbox);
m_listbox->selectIndex(m_selected);
initTheme();
m_window->remapWindow();
gfx::Rect rc = getListBoxPos();
m_window->positionWindow(rc.x, rc.y);
m_window->openWindow();
filterMessages();
if (isEditable())
m_entry->requestFocus();
else
m_listbox->requestFocus();
onOpenListBox();
}
void ComboBox::closeListBox()
{
if (m_window) {
m_listbox->clean();
m_window->closeWindow(this);
delete m_window; // window, frame
m_window = nullptr;
m_listbox = nullptr;
removeMessageFilters();
putSelectedItemAsCustomWidget();
m_entry->requestFocus();
onCloseListBox();
}
}
void ComboBox::switchListBox()
{
if (!m_window)
openListBox();
else
closeListBox();
}
gfx::Rect ComboBox::getListBoxPos() const
{
gfx::Rect entryBounds = m_entry->bounds();
gfx::Rect rc(gfx::Point(entryBounds.x,
entryBounds.y2()),
gfx::Point(m_button->bounds().x2(),
entryBounds.y2() + m_window->bounds().h));
if (rc.y2() > ui::display_h())
rc.offset(0, -(rc.h + entryBounds.h));
return rc;
}
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