aseprite/src/ui/combobox.cpp

731 lines
16 KiB
C++
Raw Normal View History

// Aseprite UI Library
// 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
2009-07-13 04:29:16 +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/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
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;
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)
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();
removeAllItems();
}
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);
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::removeItem(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::removeAllItems()
{
for (Widget* item : m_items)
delete item; // 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(int itemIndex)
{
if (itemIndex >= 0 && (std::size_t)itemIndex < m_items.size()) {
return m_items[itemIndex];
}
else
return NULL;
}
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) {
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 (!m_items.empty() ? m_items[m_selected]: NULL);
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
{
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
{
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);
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()) {
2007-09-19 07:57:02 +08:00
case kCloseMessage:
closeListBox();
break;
case kWinMoveMessage:
if (m_window)
m_window->moveWindow(getListBoxPos());
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()) {
closeListBox();
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
Widget* pick = manager()->pick(mouseMsg->position());
return (pick && pick->hasAncestor(this) ? true: false);
}
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.)
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();
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();
}
2015-12-04 08:50:05 +08:00
void ComboBox::onSizeHint(SizeHintEvent& ev)
{
2015-12-04 08:50:05 +08:00
Size entrySize = m_entry->sizeHint();
2015-12-23 04:14:26 +08:00
Size reqSize = entrySize;
// Get the text-length of every item
auto end = m_items.end();
for (auto it = m_items.begin(); it != end; ++it) {
int item_w =
2*guiscale()+
font()->textLength((*it)->text().c_str())+
2015-12-23 04:14:26 +08:00
16*guiscale();
reqSize.w = MAX(reqSize.w, item_w);
}
2015-12-04 08:50:05 +08:00
Size buttonSize = m_button->sizeHint();
reqSize.w += buttonSize.w;
reqSize.h = 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()) {
2007-09-19 07:57:02 +08:00
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;
}
}
}
2007-09-19 07:57:02 +08:00
}
break;
case kMouseDownMessage:
if (m_comboBox->isClickOpen() &&
(!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);
Widget* pick = manager()->pick(mouseMsg->position());
Widget* listbox = m_comboBox->m_listbox;
if (pick != NULL && (pick == listbox || pick->hasAncestor(listbox))) {
releaseMouse();
2014-02-24 19:30:43 +08:00
MouseMessage mouseMsg2(kMouseDownMessage,
mouseMsg->pointerType(),
mouseMsg->buttons(),
mouseMsg->modifiers(),
mouseMsg->position());
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);
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;
}
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);
}
bool ComboBoxListBox::onProcessMessage(Message* msg)
2007-09-19 07:57:02 +08:00
{
switch (msg->type()) {
2007-09-19 07:57:02 +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();
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(Event& ev)
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
m_window = new Window(Window::WithoutTitleBar);
View* view = new View();
m_listbox = new ComboBoxListBox(this);
m_window->setOnTop(true);
m_window->setWantFocus(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)
size.h += item->sizeHint().h;
int max = MAX(entryBounds.y, ui::display_h() - entryBounds.y2()) - 8*guiscale();
size.h = MID(textHeight(), size.h, max);
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
gfx::Rect rc = getListBoxPos();
m_window->positionWindow(rc.x, rc.y);
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) {
m_listbox->clean();
m_window->closeWindow(this);
delete m_window; // window, frame
m_window = nullptr;
m_listbox = nullptr;
2007-09-19 07:57:02 +08:00
removeMessageFilters();
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
}
gfx::Rect ComboBox::getListBoxPos() const
2007-09-19 07:57:02 +08:00
{
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));
2007-09-19 07:57:02 +08:00
return rc;
}
void ComboBox::onChange()
{
Change();
}
void ComboBox::onOpenListBox()
{
OpenListBox();
}
void ComboBox::onCloseListBox()
{
CloseListBox();
}
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