// Copyright (C) 2002-2012 Nikolaus Gebhardt // This file is part of the "Irrlicht Engine". // For conditions of distribution and use, see copyright notice in irrlicht.h #include "CGUIListBox.h" #ifdef _IRR_COMPILE_WITH_GUI_ #include "CGUIListBox.h" #include "IGUISkin.h" #include "IGUIEnvironment.h" #include "IVideoDriver.h" #include "IGUIFont.h" #include "IGUISpriteBank.h" #include "CGUIScrollBar.h" #include "os.h" namespace irr { namespace gui { //! constructor CGUIListBox::CGUIListBox(IGUIEnvironment* environment, IGUIElement* parent, s32 id, core::rect rectangle, bool clip, bool drawBack, bool moveOverSelect) : IGUIListBox(environment, parent, id, rectangle), Selected(-1), ItemHeight(0),ItemHeightOverride(0), TotalItemHeight(0), ItemsIconWidth(0), Font(0), IconBank(0), ScrollBar(0), selectTime(0), LastKeyTime(0), Selecting(false), DrawBack(drawBack), MoveOverSelect(moveOverSelect), AutoScroll(true), HighlightWhenNotFocused(true) { #ifdef _DEBUG setDebugName("CGUIListBox"); #endif IGUISkin* skin = Environment->getSkin(); ScrollBar = new CGUIScrollBar(false, Environment, this, -1, core::recti(0, 0, 1, 1), !clip); ScrollBar->setSubElement(true); ScrollBar->setTabStop(false); ScrollBar->setAlignment(EGUIA_LOWERRIGHT, EGUIA_LOWERRIGHT, EGUIA_UPPERLEFT, EGUIA_LOWERRIGHT); ScrollBar->setVisible(false); ScrollBar->setPos(0); updateScrollBarSize(skin->getSize(EGDS_SCROLLBAR_SIZE)); setNotClipped(!clip); // this element can be tabbed to setTabStop(true); setTabOrder(-1); updateAbsolutePosition(); } //! destructor CGUIListBox::~CGUIListBox() { if (ScrollBar) ScrollBar->drop(); if (Font) Font->drop(); if (IconBank) IconBank->drop(); } //! returns amount of list items u32 CGUIListBox::getItemCount() const { return Items.size(); } //! returns string of a list item. the may be a value from 0 to itemCount-1 const wchar_t* CGUIListBox::getListItem(u32 id) const { if (id>=Items.size()) return 0; return Items[id].Text.c_str(); } //! Returns the icon of an item s32 CGUIListBox::getIcon(u32 id) const { if (id>=Items.size()) return -1; return Items[id].Icon; } //! adds a list item, returns id of item void CGUIListBox::removeItem(u32 id) { if (id >= Items.size()) return; if ((u32)Selected==id) { Selected = -1; } else if ((u32)Selected > id) { Selected -= 1; selectTime = os::Timer::getTime(); } Items.erase(id); recalculateItemHeight(); } s32 CGUIListBox::getItemAt(s32 xpos, s32 ypos) const { if ( xpos < AbsoluteRect.UpperLeftCorner.X || xpos >= AbsoluteRect.LowerRightCorner.X || ypos < AbsoluteRect.UpperLeftCorner.Y || ypos >= AbsoluteRect.LowerRightCorner.Y ) return -1; if ( ItemHeight == 0 ) return -1; s32 item = ((ypos - AbsoluteRect.UpperLeftCorner.Y - 1) + ScrollBar->getPos()) / ItemHeight; if ( item < 0 || item >= (s32)Items.size()) return -1; return item; } //! clears the list void CGUIListBox::clear() { Items.clear(); ItemsIconWidth = 0; Selected = -1; ScrollBar->setPos(0); recalculateItemHeight(); } void CGUIListBox::recalculateItemHeight() { IGUISkin* skin = Environment->getSkin(); if (Font != skin->getFont()) { if (Font) Font->drop(); Font = skin->getFont(); if ( 0 == ItemHeightOverride ) ItemHeight = 0; if (Font) { if ( 0 == ItemHeightOverride ) ItemHeight = Font->getDimension(L"A").Height + 4; Font->grab(); } } TotalItemHeight = ItemHeight * Items.size(); ScrollBar->setMax( core::max_(0, TotalItemHeight - AbsoluteRect.getHeight()) ); s32 minItemHeight = ItemHeight > 0 ? ItemHeight : 1; ScrollBar->setSmallStep ( minItemHeight ); ScrollBar->setLargeStep ( 2*minItemHeight ); if ( TotalItemHeight <= AbsoluteRect.getHeight() ) ScrollBar->setVisible(false); else ScrollBar->setVisible(true); } //! returns id of selected item. returns -1 if no item is selected. s32 CGUIListBox::getSelected() const { return Selected; } //! sets the selected item. Set this to -1 if no item should be selected void CGUIListBox::setSelected(s32 id) { if ((u32)id>=Items.size()) Selected = -1; else Selected = id; selectTime = os::Timer::getTime(); recalculateScrollPos(); } //! sets the selected item. Set this to -1 if no item should be selected void CGUIListBox::setSelected(const wchar_t *item) { s32 index = -1; if ( item ) { for ( index = 0; index < (s32) Items.size(); ++index ) { if ( Items[index].Text == item ) break; } } setSelected ( index ); } //! called if an event happened. bool CGUIListBox::OnEvent(const SEvent& event) { if (isEnabled()) { switch(event.EventType) { case EET_KEY_INPUT_EVENT: if (event.KeyInput.PressedDown && (event.KeyInput.Key == KEY_DOWN || event.KeyInput.Key == KEY_UP || event.KeyInput.Key == KEY_HOME || event.KeyInput.Key == KEY_END || event.KeyInput.Key == KEY_NEXT || event.KeyInput.Key == KEY_PRIOR ) ) { s32 oldSelected = Selected; switch (event.KeyInput.Key) { case KEY_DOWN: Selected += 1; break; case KEY_UP: Selected -= 1; break; case KEY_HOME: Selected = 0; break; case KEY_END: Selected = (s32)Items.size()-1; break; case KEY_NEXT: Selected += AbsoluteRect.getHeight() / ItemHeight; break; case KEY_PRIOR: Selected -= AbsoluteRect.getHeight() / ItemHeight; break; default: break; } if (Selected<0) Selected = 0; if (Selected >= (s32)Items.size()) Selected = Items.size() - 1; // will set Selected to -1 for empty listboxes which is correct recalculateScrollPos(); // post the news if (oldSelected != Selected && Parent && !Selecting && !MoveOverSelect) { SEvent e; e.EventType = EET_GUI_EVENT; e.GUIEvent.Caller = this; e.GUIEvent.Element = 0; e.GUIEvent.EventType = EGET_LISTBOX_CHANGED; Parent->OnEvent(e); } return true; } else if (!event.KeyInput.PressedDown && ( event.KeyInput.Key == KEY_RETURN || event.KeyInput.Key == KEY_SPACE ) ) { if (Parent) { SEvent e; e.EventType = EET_GUI_EVENT; e.GUIEvent.Caller = this; e.GUIEvent.Element = 0; e.GUIEvent.EventType = EGET_LISTBOX_SELECTED_AGAIN; Parent->OnEvent(e); } return true; } else if (event.KeyInput.Key == KEY_TAB ) { return false; } else if (event.KeyInput.PressedDown && event.KeyInput.Char) { // change selection based on text as it is typed. u32 now = os::Timer::getTime(); if (now - LastKeyTime < 500) { // add to key buffer if it isn't a key repeat if (!(KeyBuffer.size() == 1 && KeyBuffer[0] == event.KeyInput.Char)) { KeyBuffer += L" "; KeyBuffer[KeyBuffer.size()-1] = event.KeyInput.Char; } } else { KeyBuffer = L" "; KeyBuffer[0] = event.KeyInput.Char; } LastKeyTime = now; // find the selected item, starting at the current selection s32 start = Selected; // dont change selection if the key buffer matches the current item if (Selected > -1 && KeyBuffer.size() > 1) { if (Items[Selected].Text.size() >= KeyBuffer.size() && KeyBuffer.equals_ignore_case(Items[Selected].Text.subString(0,KeyBuffer.size()))) return true; } s32 current; for (current = start+1; current < (s32)Items.size(); ++current) { if (Items[current].Text.size() >= KeyBuffer.size()) { if (KeyBuffer.equals_ignore_case(Items[current].Text.subString(0,KeyBuffer.size()))) { if (Parent && Selected != current && !Selecting && !MoveOverSelect) { SEvent e; e.EventType = EET_GUI_EVENT; e.GUIEvent.Caller = this; e.GUIEvent.Element = 0; e.GUIEvent.EventType = EGET_LISTBOX_CHANGED; Parent->OnEvent(e); } setSelected(current); return true; } } } for (current = 0; current <= start; ++current) { if (Items[current].Text.size() >= KeyBuffer.size()) { if (KeyBuffer.equals_ignore_case(Items[current].Text.subString(0,KeyBuffer.size()))) { if (Parent && Selected != current && !Selecting && !MoveOverSelect) { Selected = current; SEvent e; e.EventType = EET_GUI_EVENT; e.GUIEvent.Caller = this; e.GUIEvent.Element = 0; e.GUIEvent.EventType = EGET_LISTBOX_CHANGED; Parent->OnEvent(e); } setSelected(current); return true; } } } return true; } break; case EET_GUI_EVENT: switch(event.GUIEvent.EventType) { case gui::EGET_SCROLL_BAR_CHANGED: if (event.GUIEvent.Caller == ScrollBar) return true; break; case gui::EGET_ELEMENT_FOCUS_LOST: { if (event.GUIEvent.Caller == this) Selecting = false; } default: break; } break; case EET_MOUSE_INPUT_EVENT: { core::position2d p(event.MouseInput.X, event.MouseInput.Y); switch(event.MouseInput.Event) { case EMIE_MOUSE_WHEEL: ScrollBar->setPos(ScrollBar->getPos() + (event.MouseInput.Wheel < 0 ? -1 : 1)*-ItemHeight/2); return true; case EMIE_LMOUSE_PRESSED_DOWN: { Selecting = true; return true; } case EMIE_LMOUSE_LEFT_UP: { Selecting = false; if (isPointInside(p)) selectNew(event.MouseInput.Y); return true; } case EMIE_MOUSE_MOVED: if (Selecting || MoveOverSelect) { if (isPointInside(p)) { selectNew(event.MouseInput.Y, true); return true; } } default: break; } } break; default: break; } } return IGUIElement::OnEvent(event); } void CGUIListBox::selectNew(s32 ypos, bool onlyHover) { u32 now = os::Timer::getTime(); s32 oldSelected = Selected; Selected = getItemAt(AbsoluteRect.UpperLeftCorner.X, ypos); if (Selected<0 && !Items.empty()) Selected = 0; recalculateScrollPos(); gui::EGUI_EVENT_TYPE eventType = (Selected == oldSelected && now < selectTime + 500) ? EGET_LISTBOX_SELECTED_AGAIN : EGET_LISTBOX_CHANGED; selectTime = now; // post the news if (Parent && !onlyHover) { SEvent event; event.EventType = EET_GUI_EVENT; event.GUIEvent.Caller = this; event.GUIEvent.Element = 0; event.GUIEvent.EventType = eventType; Parent->OnEvent(event); } } //! Update the position and size of the listbox, and update the scrollbar void CGUIListBox::updateAbsolutePosition() { IGUIElement::updateAbsolutePosition(); recalculateItemHeight(); } //! draws the element and its children void CGUIListBox::draw() { if (!IsVisible) return; recalculateItemHeight(); // if the font changed IGUISkin* skin = Environment->getSkin(); updateScrollBarSize(skin->getSize(EGDS_SCROLLBAR_SIZE)); core::rect* clipRect = 0; // draw background core::rect frameRect(AbsoluteRect); // draw items core::rect clientClip(AbsoluteRect); clientClip.UpperLeftCorner.Y += 1; clientClip.UpperLeftCorner.X += 1; if (ScrollBar->isVisible()) clientClip.LowerRightCorner.X -= ScrollBar->getRelativePosition().getWidth(); clientClip.LowerRightCorner.Y -= 1; clientClip.clipAgainst(AbsoluteClippingRect); skin->draw3DSunkenPane(this, skin->getColor(EGDC_3D_HIGH_LIGHT), true, DrawBack, frameRect, &AbsoluteClippingRect); if (clipRect) clientClip.clipAgainst(*clipRect); frameRect = AbsoluteRect; frameRect.UpperLeftCorner.X += 1; if (ScrollBar->isVisible()) frameRect.LowerRightCorner.X -= ScrollBar->getRelativePosition().getWidth(); frameRect.LowerRightCorner.Y = AbsoluteRect.UpperLeftCorner.Y + ItemHeight; frameRect.UpperLeftCorner.Y -= ScrollBar->getPos(); frameRect.LowerRightCorner.Y -= ScrollBar->getPos(); bool hl = (HighlightWhenNotFocused || Environment->hasFocus(this) || Environment->hasFocus(ScrollBar)); for (s32 i=0; i<(s32)Items.size(); ++i) { if (frameRect.LowerRightCorner.Y >= AbsoluteRect.UpperLeftCorner.Y && frameRect.UpperLeftCorner.Y <= AbsoluteRect.LowerRightCorner.Y) { if (i == Selected && hl) { skin->draw2DRectangle(this, hasItemOverrideColor(i, EGUI_LBC_BACKGROUND_HIGHLIGHT) ? getItemOverrideColor(i, EGUI_LBC_BACKGROUND_HIGHLIGHT) : getItemDefaultColor(EGUI_LBC_BACKGROUND_HIGHLIGHT), frameRect, &clientClip); } else if ( hasItemOverrideColor(i, EGUI_LBC_BACKGROUND ) ) { skin->draw2DRectangle(this, getItemOverrideColor(i, EGUI_LBC_BACKGROUND), frameRect, &clientClip); } core::rect textRect = frameRect; textRect.UpperLeftCorner.X += 3; if (Font) { if (IconBank && (Items[i].Icon > -1)) { core::position2di iconPos = textRect.UpperLeftCorner; iconPos.Y += textRect.getHeight() / 2; iconPos.X += ItemsIconWidth/2; if ( i==Selected && hl ) { IconBank->draw2DSprite( (u32)Items[i].Icon, iconPos, &clientClip, hasItemOverrideColor(i, EGUI_LBC_ICON_HIGHLIGHT) ? getItemOverrideColor(i, EGUI_LBC_ICON_HIGHLIGHT) : getItemDefaultColor(EGUI_LBC_ICON_HIGHLIGHT), selectTime, os::Timer::getTime(), false, true); } else { IconBank->draw2DSprite( (u32)Items[i].Icon, iconPos, &clientClip, hasItemOverrideColor(i, EGUI_LBC_ICON) ? getItemOverrideColor(i, EGUI_LBC_ICON) : getItemDefaultColor(EGUI_LBC_ICON), 0 , (i==Selected) ? os::Timer::getTime() : 0, false, true); } } textRect.UpperLeftCorner.X += ItemsIconWidth+3; if ( i==Selected && hl ) { Font->draw(Items[i].Text.c_str(), textRect, hasItemOverrideColor(i, EGUI_LBC_TEXT_HIGHLIGHT) ? getItemOverrideColor(i, EGUI_LBC_TEXT_HIGHLIGHT) : getItemDefaultColor(EGUI_LBC_TEXT_HIGHLIGHT), false, true, &clientClip); } else { Font->draw(Items[i].Text.c_str(), textRect, hasItemOverrideColor(i, EGUI_LBC_TEXT) ? getItemOverrideColor(i, EGUI_LBC_TEXT) : getItemDefaultColor(EGUI_LBC_TEXT), false, true, &clientClip); } textRect.UpperLeftCorner.X -= ItemsIconWidth+3; } } frameRect.UpperLeftCorner.Y += ItemHeight; frameRect.LowerRightCorner.Y += ItemHeight; } IGUIElement::draw(); } //! adds an list item with an icon u32 CGUIListBox::addItem(const wchar_t* text, s32 icon) { ListItem i; i.Text = text; i.Icon = icon; Items.push_back(i); recalculateItemHeight(); recalculateItemWidth(icon); return Items.size() - 1; } void CGUIListBox::setSpriteBank(IGUISpriteBank* bank) { if ( bank == IconBank ) return; if (IconBank) IconBank->drop(); IconBank = bank; if (IconBank) IconBank->grab(); } void CGUIListBox::recalculateScrollPos() { if (!AutoScroll) return; const s32 selPos = (Selected == -1 ? TotalItemHeight : Selected * ItemHeight) - ScrollBar->getPos(); if (selPos < 0) { ScrollBar->setPos(ScrollBar->getPos() + selPos); } else if (selPos > AbsoluteRect.getHeight() - ItemHeight) { ScrollBar->setPos(ScrollBar->getPos() + selPos - AbsoluteRect.getHeight() + ItemHeight); } } void CGUIListBox::updateScrollBarSize(s32 size) { if ( size != ScrollBar->getRelativePosition().getWidth() ) { core::recti r(RelativeRect.getWidth() - size, 0, RelativeRect.getWidth(), RelativeRect.getHeight()); ScrollBar->setRelativePosition(r); } } void CGUIListBox::setAutoScrollEnabled(bool scroll) { AutoScroll = scroll; } bool CGUIListBox::isAutoScrollEnabled() const { return AutoScroll; } bool CGUIListBox::getSerializationLabels(EGUI_LISTBOX_COLOR colorType, core::stringc & useColorLabel, core::stringc & colorLabel) const { switch ( colorType ) { case EGUI_LBC_TEXT: useColorLabel = "UseColText"; colorLabel = "ColText"; break; case EGUI_LBC_TEXT_HIGHLIGHT: useColorLabel = "UseColTextHl"; colorLabel = "ColTextHl"; break; case EGUI_LBC_ICON: useColorLabel = "UseColIcon"; colorLabel = "ColIcon"; break; case EGUI_LBC_ICON_HIGHLIGHT: useColorLabel = "UseColIconHl"; colorLabel = "ColIconHl"; break; case EGUI_LBC_BACKGROUND: useColorLabel = "UseColBg"; colorLabel = "ColBg"; break; case EGUI_LBC_BACKGROUND_HIGHLIGHT: useColorLabel = "UseColBgHl"; colorLabel = "ColBgHl"; break; default: return false; } return true; } //! Writes attributes of the element. void CGUIListBox::serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options=0) const { IGUIListBox::serializeAttributes(out,options); // todo: out->addString ("IconBank", IconBank->getName?); out->addBool ("DrawBack", DrawBack); out->addBool ("MoveOverSelect", MoveOverSelect); out->addBool ("AutoScroll", AutoScroll); out->addInt("ItemCount", Items.size()); for (u32 i=0;iaddString(label.c_str(), Items[i].Text.c_str() ); for ( s32 c=0; c < (s32)EGUI_LBC_COUNT; ++c ) { core::stringc useColorLabel, colorLabel; if ( !getSerializationLabels((EGUI_LISTBOX_COLOR)c, useColorLabel, colorLabel) ) return; label = useColorLabel; label += i; if ( Items[i].OverrideColors[c].Use ) { out->addBool(label.c_str(), true ); label = colorLabel; label += i; out->addColor(label.c_str(), Items[i].OverrideColors[c].Color); } else { out->addBool(label.c_str(), false ); } } } out->addInt("Selected", Selected); } //! Reads attributes of the element void CGUIListBox::deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options=0) { clear(); DrawBack = in->getAttributeAsBool("DrawBack", DrawBack); MoveOverSelect = in->getAttributeAsBool("MoveOverSelect", MoveOverSelect); AutoScroll = in->getAttributeAsBool("AutoScroll", AutoScroll); IGUIListBox::deserializeAttributes(in,options); const s32 count = in->getAttributeAsInt("ItemCount"); for (s32 i=0; igetAttributeAsStringW(label.c_str()); addItem(item.Text.c_str(), item.Icon); for ( u32 c=0; c < EGUI_LBC_COUNT; ++c ) { core::stringc useColorLabel, colorLabel; if ( !getSerializationLabels((EGUI_LISTBOX_COLOR)c, useColorLabel, colorLabel) ) return; label = useColorLabel; label += i; Items[i].OverrideColors[c].Use = in->getAttributeAsBool(label.c_str()); if ( Items[i].OverrideColors[c].Use ) { label = colorLabel; label += i; Items[i].OverrideColors[c].Color = in->getAttributeAsColor(label.c_str()); } } } Selected = in->getAttributeAsInt("Selected", Selected); recalculateScrollPos(); } void CGUIListBox::recalculateItemWidth(s32 icon) { if (IconBank && icon > -1 && IconBank->getSprites().size() > (u32)icon && IconBank->getSprites()[(u32)icon].Frames.size()) { u32 rno = IconBank->getSprites()[(u32)icon].Frames[0].rectNumber; if (IconBank->getPositions().size() > rno) { const s32 w = IconBank->getPositions()[rno].getWidth(); if (w > ItemsIconWidth) ItemsIconWidth = w; } } } void CGUIListBox::setItem(u32 index, const wchar_t* text, s32 icon) { if ( index >= Items.size() ) return; Items[index].Text = text; Items[index].Icon = icon; recalculateItemHeight(); recalculateItemWidth(icon); } //! Insert the item at the given index //! Return the index on success or -1 on failure. s32 CGUIListBox::insertItem(u32 index, const wchar_t* text, s32 icon) { ListItem i; i.Text = text; i.Icon = icon; Items.insert(i, index); recalculateItemHeight(); recalculateItemWidth(icon); return index; } void CGUIListBox::swapItems(u32 index1, u32 index2) { if ( index1 >= Items.size() || index2 >= Items.size() ) return; ListItem dummmy = Items[index1]; Items[index1] = Items[index2]; Items[index2] = dummmy; } void CGUIListBox::setItemOverrideColor(u32 index, video::SColor color) { for ( u32 c=0; c < EGUI_LBC_COUNT; ++c ) { Items[index].OverrideColors[c].Use = true; Items[index].OverrideColors[c].Color = color; } } void CGUIListBox::setItemOverrideColor(u32 index, EGUI_LISTBOX_COLOR colorType, video::SColor color) { if ( index >= Items.size() || colorType < 0 || colorType >= EGUI_LBC_COUNT ) return; Items[index].OverrideColors[colorType].Use = true; Items[index].OverrideColors[colorType].Color = color; } void CGUIListBox::clearItemOverrideColor(u32 index) { for (u32 c=0; c < (u32)EGUI_LBC_COUNT; ++c ) { Items[index].OverrideColors[c].Use = false; } } void CGUIListBox::clearItemOverrideColor(u32 index, EGUI_LISTBOX_COLOR colorType) { if ( index >= Items.size() || colorType < 0 || colorType >= EGUI_LBC_COUNT ) return; Items[index].OverrideColors[colorType].Use = false; } bool CGUIListBox::hasItemOverrideColor(u32 index, EGUI_LISTBOX_COLOR colorType) const { if ( index >= Items.size() || colorType < 0 || colorType >= EGUI_LBC_COUNT ) return false; return Items[index].OverrideColors[colorType].Use; } video::SColor CGUIListBox::getItemOverrideColor(u32 index, EGUI_LISTBOX_COLOR colorType) const { if ( (u32)index >= Items.size() || colorType < 0 || colorType >= EGUI_LBC_COUNT ) return video::SColor(); return Items[index].OverrideColors[colorType].Color; } video::SColor CGUIListBox::getItemDefaultColor(EGUI_LISTBOX_COLOR colorType) const { IGUISkin* skin = Environment->getSkin(); if ( !skin ) return video::SColor(); switch ( colorType ) { case EGUI_LBC_TEXT: return skin->getColor(EGDC_BUTTON_TEXT); case EGUI_LBC_TEXT_HIGHLIGHT: return skin->getColor(EGDC_HIGH_LIGHT_TEXT); case EGUI_LBC_ICON: return skin->getColor(EGDC_ICON); case EGUI_LBC_ICON_HIGHLIGHT: return skin->getColor(EGDC_ICON_HIGH_LIGHT); case EGUI_LBC_BACKGROUND: return skin->getColor(EGDC_3D_HIGH_LIGHT); case EGUI_LBC_BACKGROUND_HIGHLIGHT: return skin->getColor(EGDC_HIGH_LIGHT); default: return video::SColor(); } } //! set global itemHeight void CGUIListBox::setItemHeight( s32 height ) { ItemHeight = height; ItemHeightOverride = 1; } //! Sets whether to draw the background void CGUIListBox::setDrawBackground(bool draw) { DrawBack = draw; } //! Access the vertical scrollbar IGUIScrollBar* CGUIListBox::getVerticalScrollBar() const { return ScrollBar; } } // end namespace gui } // end namespace irr #endif // _IRR_COMPILE_WITH_GUI_