//----------------------------------------------------------------------------- // Copyright (c) 2013 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "gui/guiListBoxCtrl.h" #include "gui/guiCanvas.h" #include #include "guiListBoxCtrl_ScriptBinding.h" IMPLEMENT_CONOBJECT(GuiListBoxCtrl); GuiListBoxCtrl::GuiListBoxCtrl() { mItems.clear(); mSelectedItems.clear(); mMultipleSelections = true; mFitParentWidth = true; mItemSize = Point2I(10,20); mLastClickItem = NULL; mRendersChildren = false; mIsContainer = false; mActive = true; caller = this; setField("profile", "GuiListBoxProfile"); } GuiListBoxCtrl::~GuiListBoxCtrl() { clearItems(); } void GuiListBoxCtrl::initPersistFields() { Parent::initPersistFields(); addField( "AllowMultipleSelections", TypeBool, Offset( mMultipleSelections, GuiListBoxCtrl) ); addField( "FitParentWidth", TypeBool, Offset( mFitParentWidth, GuiListBoxCtrl) ); } bool GuiListBoxCtrl::onWake() { if( !Parent::onWake() ) return false; updateSize(); return true; } void GuiListBoxCtrl::addObject(SimObject *obj) { AssertWarn(0, "GuiListBoxCtrl::addObject: cannot add children to the GuiListBoxCtrl. It is not a container. Child was not added."); removeObject(obj); } #pragma region ItemAccessors void GuiListBoxCtrl::clearItems() { // Free item list allocated memory while( mItems.size() ) deleteItem( 0 ); // Free our vector lists mItems.clear(); mSelectedItems.clear(); } void GuiListBoxCtrl::clearSelection() { if( !mSelectedItems.size() ) return; VectorPtr::iterator i = mSelectedItems.begin(); for( ; i != mSelectedItems.end(); i++ ) (*i)->isSelected = false; mSelectedItems.clear(); if (caller->isMethod("onUnselectAll")) { Con::executef(caller, 1, "onUnselectAll"); } } void GuiListBoxCtrl::removeSelection( S32 index ) { // Range Check if( index >= mItems.size() || index < 0 ) { Con::warnf("GuiListBoxCtrl::removeSelection - index out of range!" ); return; } removeSelection( mItems[index], index ); } void GuiListBoxCtrl::removeSelection( LBItem *item, S32 index ) { if( !mSelectedItems.size() ) return; if( !item ) return; for( S32 i = 0 ; i < mSelectedItems.size(); i++ ) { if( mSelectedItems[i] == item ) { mSelectedItems.erase( &mSelectedItems[i] ); item->isSelected = false; if (caller->isMethod("onUnselect")) { Con::executef(caller, 4, "onUnselect", Con::getIntArg( index ), item->itemText, Con::getIntArg(item->ID)); } return; } } } void GuiListBoxCtrl::addSelection( S32 index ) { // Range Check if( index >= mItems.size() || index < 0 ) { Con::warnf("GuiListBoxCtrl::addSelection- index out of range!" ); return; } addSelection( mItems[index], index ); } void GuiListBoxCtrl::addSelection( LBItem *item, S32 index ) { if( !mMultipleSelections ) { if( !mSelectedItems.empty() ) { LBItem* selItem = mSelectedItems.front(); if( selItem != item ) clearSelection(); else return; } } else { if( !mSelectedItems.empty() ) { for( S32 i = 0; i < mSelectedItems.size(); i++ ) { if( mSelectedItems[ i ] == item ) return; } } } item->isSelected = true; mSelectedItems.push_front( item ); ScrollToIndex(index); if(caller->isMethod("onSelect")) Con::executef(caller, 4, "onSelect", Con::getIntArg( index ), item->itemText, Con::getIntArg(item->ID)); } S32 GuiListBoxCtrl::getItemIndex( LBItem *item ) { if( mItems.empty() ) return -1; // Lookup the index of an item in our list, by the pointer to the item for( S32 i = 0; i < mItems.size(); i++ ) if( mItems[i] == item ) return i; return -1; } S32 GuiListBoxCtrl::getItemCount() { return mItems.size(); } S32 GuiListBoxCtrl::getSelCount() { return mSelectedItems.size(); } S32 GuiListBoxCtrl::getSelectedItem() { if( mSelectedItems.empty() || mItems.empty() ) return -1; for( S32 i = 0 ; i < mItems.size(); i++ ) if( mItems[i]->isSelected ) return i; return -1; } void GuiListBoxCtrl::getSelectedItems( Vector &Items ) { // Clear our return vector Items.clear(); // If there are no selected items, return an empty vector if( mSelectedItems.empty() ) return; for( S32 i = 0; i < mItems.size(); i++ ) if( mItems[i]->isSelected ) Items.push_back( i ); } S32 GuiListBoxCtrl::findItemText( StringTableEntry text, bool caseSensitive ) { // Check Proper Arguments if( !text || !text[0] || text == StringTable->EmptyString ) { Con::warnf("GuiListBoxCtrl::findItemText - No Text Specified!"); return -1; } // Check Items Exist. if( mItems.empty() ) return -1; // Lookup the index of an item in our list, by the pointer to the item for( S32 i = 0; i < mItems.size(); i++ ) { // Case Sensitive Compare? if( caseSensitive && ( dStrcmp( mItems[i]->itemText, text ) == 0 ) ) return i; else if (!caseSensitive && ( dStricmp( mItems[i]->itemText, text ) == 0 )) return i; } // Not Found! return -1; } void GuiListBoxCtrl::setSelectionInternal(StringTableEntry text) { if(text != StringTable->EmptyString) { S32 index = findItemText(text); if (index != -1) { mSelectedItems.clear(); LBItem *item = mItems[index]; item->isSelected = true; mSelectedItems.push_front(item); } } } void GuiListBoxCtrl::setCurSel( S32 index ) { // Range Check if( index >= mItems.size() ) { Con::warnf("GuiListBoxCtrl::setCurSel - index out of range!" ); return; } // If index -1 is specified, we clear the selection if( index == -1 ) { for (auto item : mItems) { item->isSelected = false; } mSelectedItems.clear(); return; } // Add the selection addSelection( mItems[ index ], index ); } void GuiListBoxCtrl::setCurSelRange( S32 start, S32 stop ) { // Verify Selection Range if( start < 0 ) start = 0; else if( start > mItems.size() ) start = mItems.size(); if( stop < 0 ) stop = 0; else if( stop > mItems.size() ) stop = mItems.size(); S32 iterStart = ( start < stop ) ? start : stop; S32 iterStop = ( start < stop ) ? stop : start; for( ; iterStart <= iterStop; iterStart++ ) addSelection( mItems[iterStart], iterStart ); } S32 GuiListBoxCtrl::addItem( StringTableEntry text, void *itemData ) { // This just calls insert item at the end of the list return insertItem( mItems.size(), text, itemData ); } S32 GuiListBoxCtrl::addItemWithColor( StringTableEntry text, ColorF color, void *itemData ) { // This just calls insert item at the end of the list return insertItemWithColor( mItems.size(), text, color, itemData ); } S32 GuiListBoxCtrl::addItemWithID(StringTableEntry text, S32 ID, void *itemData) { S32 index = insertItem( mItems.size(), text, itemData); setItemID(index, ID); return index; } void GuiListBoxCtrl::setItemColor( S32 index, ColorF color ) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::setItemColor - invalid index"); return; } LBItem* item = mItems[index]; item->hasColor = true; item->color = color; } void GuiListBoxCtrl::clearItemColor(S32 index) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::setItemColor - invalid index"); return; } LBItem* item = mItems[index]; item->hasColor = false; } void GuiListBoxCtrl::clearAllColors() { if (!mSelectedItems.size()) return; VectorPtr::iterator i = mSelectedItems.begin(); for (; i != mSelectedItems.end(); i++) (*i)->hasColor = false; } ColorF GuiListBoxCtrl::getItemColor(S32 index) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::getItemColor - invalid index"); return ColorF(0,0,0,0); } LBItem* item = mItems[index]; return item->color; } bool GuiListBoxCtrl::getItemHasColor(S32 index) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::getItemHasColor - invalid index"); return false; } LBItem* item = mItems[index]; return item->hasColor; } void GuiListBoxCtrl::setItemID(S32 index, S32 ID) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::setItemID - invalid index"); return; } LBItem* item = mItems[index]; item->ID = ID; } S32 GuiListBoxCtrl::getItemID(S32 index) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::setItemID - invalid index"); return 0; } LBItem* item = mItems[index]; return item->ID; } S32 GuiListBoxCtrl::findItemID(S32 ID) { // Check Items Exist. if (mItems.empty()) return -1; // Lookup the index of an item in our list, by the pointer to the item for (S32 i = 0; i < mItems.size(); i++) { if (mItems[i]->ID == ID) { return i; } } // Not Found! return -1; } void GuiListBoxCtrl::setItemActive(S32 index) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::setItemActive - invalid index"); return; } LBItem* item = mItems[index]; item->isActive = true; } void GuiListBoxCtrl::setItemInactive(S32 index) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::setItemInactive - invalid index"); return; } LBItem* item = mItems[index]; item->isActive = false; } bool GuiListBoxCtrl::getItemActive(S32 index) { if ((index >= mItems.size()) || index < 0) { Con::warnf("GuiListBoxCtrl::getItemActive - invalid index"); return true; } LBItem* item = mItems[index]; return item->isActive; } S32 GuiListBoxCtrl::insertItem( S32 index, StringTableEntry text, void *itemData ) { // If the index is greater than our list size, insert it at the end if( index >= mItems.size() ) index = mItems.size(); // Sanity checking if( !text ) { Con::warnf("GuiListBoxCtrl::insertItem - cannot add NULL string" ); return -1; } LBItem *newItem = createItem(); if( !newItem ) { Con::warnf("GuiListBoxCtrl::insertItem - error allocating item memory!" ); return -1; } // Assign item data newItem->itemText = StringTable->insert(text, true); newItem->isSelected = false; newItem->isActive = true; newItem->ID = 0; newItem->itemData = itemData; newItem->hasColor = false; // Add to list mItems.insert(index); mItems[index] = newItem; // Resize our list to fit our items updateSize(); // Return our index in list (last) return index; } GuiListBoxCtrl::LBItem* GuiListBoxCtrl::createItem() { LBItem* newItem = new LBItem; if (!newItem) { return nullptr; } return newItem; } S32 GuiListBoxCtrl::insertItemWithColor( S32 index, StringTableEntry text, ColorF color, void *itemData ) { if( color == ColorF(-1, -1, -1) ) { Con::warnf("GuiListBoxCtrl::insertItem - cannot add NULL color" ); return -1; } index = insertItem(index, text, itemData); if(index != -1) { setItemColor(index, color); } // Return our index in list (last) return index; } void GuiListBoxCtrl::deleteItem( S32 index ) { // Range Check if( index >= mItems.size() || index < 0 ) { Con::warnf("GuiListBoxCtrl::deleteItem - index out of range!" ); return; } // Grab our item LBItem* item = mItems[ index ]; if( !item ) { Con::warnf("GuiListBoxCtrl::deleteItem - Bad Item Data!" ); return; } // Remove it from the selected list. if( item->isSelected ) { for( VectorPtr::iterator i = mSelectedItems.begin(); i != mSelectedItems.end(); i++ ) { if( item == *i ) { mSelectedItems.erase_fast( i ); break; } } } // Remove it from the list mItems.erase( &mItems[ index ] ); // Free the memory associated with it delete item; } StringTableEntry GuiListBoxCtrl::getItemText( S32 index ) { // Range Checking if( index > mItems.size() || index < 0 ) { Con::warnf( "GuiListBoxCtrl::getItemText - index out of range!" ); return StringTable->EmptyString; } return mItems[ index ]->itemText; } void GuiListBoxCtrl::setItemText( S32 index, StringTableEntry text ) { // Sanity Checking if( !text ) { Con::warnf("GuiListBoxCtrl::setItemText - Invalid Text Specified!" ); return; } // Range Checking if( index > mItems.size() || index < 0 ) { Con::warnf( "GuiListBoxCtrl::getItemText - index out of range!" ); return; } mItems[ index ]->itemText = StringTable->insert( text ); } #pragma endregion #pragma region Sizing void GuiListBoxCtrl::updateSize() { if( !mProfile || !mProfile->getFont(mFontSizeAdjust)) return; GFont *font = mProfile->getFont(mFontSizeAdjust); Point2I contentSize = Point2I(10, font->getHeight() + 2); if (!mFitParentWidth) { // Find the maximum width cell: S32 maxWidth = 1; for ( U32 i = 0; i < (U32)mItems.size(); i++ ) { S32 width = font->getStrWidth( mItems[i]->itemText ); if( width > maxWidth ) maxWidth = width; } contentSize.x = maxWidth + 6; } mItemSize = this->getOuterExtent(contentSize, NormalState, mProfile); Point2I newExtent = Point2I(mItemSize.x, mItemSize.y * mItems.size()); //Don't update the extent.x if we are matching our parent's size. We will handle it during rendering. if (mFitParentWidth) { newExtent.x = mBounds.extent.x; } resize( mBounds.point, newExtent ); } void GuiListBoxCtrl::parentResized(const Point2I &oldParentExtent, const Point2I &newParentExtent) { Parent::parentResized( oldParentExtent, newParentExtent ); updateSize(); } #pragma endregion #pragma region Rendering void GuiListBoxCtrl::onRender( Point2I offset, const RectI &updateRect ) { RectI clip = dglGetClipRect(); if (mFitParentWidth && ( mBounds.extent.x != clip.extent.x || mItemSize.x != clip.extent.x)) { mBounds.extent.x = clip.extent.x; mItemSize.x = clip.extent.x; } for ( S32 i = 0; i < mItems.size(); i++) { // Only render visible items if ((i + 1) * mItemSize.y + offset.y < updateRect.point.y) continue; // Break out once we're no longer in visible item range if( i * mItemSize.y + offset.y >= updateRect.point.y + updateRect.extent.y) break; RectI itemRect = RectI( offset.x, offset.y + ( i * mItemSize.y ), mItemSize.x, mItemSize.y ); // Render our item onRenderItem( itemRect, mItems[i] ); } } void GuiListBoxCtrl::onRenderItem( RectI &itemRect, LBItem *item ) { Point2I cursorPt = Point2I(0,0); GuiCanvas *root = getRoot(); if (root) { cursorPt = root->getCursorPos(); } GuiControlState currentState = GuiControlState::NormalState; if (!mActive || !item->isActive) currentState = GuiControlState::DisabledState; else if (item->isSelected) currentState = GuiControlState::SelectedState; else if (itemRect.pointInRect(cursorPt)) currentState = GuiControlState::HighlightState; RectI ctrlRect = applyMargins(itemRect.point, itemRect.extent, currentState, mProfile); if (!ctrlRect.isValidRect()) { return; } renderUniversalRect(ctrlRect, mProfile, currentState); //Render Text dglSetBitmapModulation(getFontColor(mProfile, currentState)); RectI fillRect = applyBorders(ctrlRect.point, ctrlRect.extent, currentState, mProfile); RectI contentRect = applyPadding(fillRect.point, fillRect.extent, currentState, mProfile); // Render color bullet if needed if (item->hasColor) { RectI drawArea = RectI(contentRect.point.x, contentRect.point.y, contentRect.extent.y, contentRect.extent.y); ColorI color = item->color; renderColorBullet(drawArea, color, 5); contentRect.point.x += contentRect.extent.y; contentRect.extent.x -= contentRect.extent.y; } renderText(contentRect.point, contentRect.extent, item->itemText, mProfile); } void GuiListBoxCtrl::ScrollToIndex(const S32 targetIndex) { GuiScrollCtrl* parent = dynamic_cast(getParent()); if (parent) parent->scrollRectVisible(RectI(0, mItemSize.y * targetIndex, mItemSize.x, mItemSize.y)); } #pragma endregion #pragma region InputEvents void GuiListBoxCtrl::onTouchDragged(const GuiEvent &event) { if (!mActive || !mVisible) { return; } S32 hitIndex = getHitIndex(event); if (hitIndex >= mItems.size() || hitIndex == -1) return; LBItem *hitItem = mItems[hitIndex]; if (hitItem == NULL || !hitItem->isActive) return; if(caller->isMethod("onTouchDragged")) { Con::executef(caller, 4, "onTouchDragged", Con::getIntArg(hitIndex), hitItem->itemText, Con::getIntArg(hitItem->ID)); } else { GuiControl* parent = getParent(); if (parent) parent->onTouchDragged(event); } } void GuiListBoxCtrl::onTouchDown( const GuiEvent &event ) { if (!mActive || !mVisible) { return; } setFirstResponder(); S32 hitIndex = getHitIndex(event); if ( hitIndex >= mItems.size() || hitIndex == -1 ) return; LBItem *hitItem = mItems[ hitIndex ]; if ( hitItem == NULL || !hitItem->isActive) return; handleItemClick(hitItem, hitIndex, event); } S32 GuiListBoxCtrl::getHitIndex(const GuiEvent& event) { Point2I localPoint = globalToLocalCoord(event.mousePoint); return (localPoint.y < 0) ? -1 : (S32)mFloor((F32)localPoint.y / (F32)mItemSize.y); } void GuiListBoxCtrl::handleItemClick(LBItem* hitItem, S32 hitIndex, const GuiEvent& event) { if( !mMultipleSelections ) { handleItemClick_SingleSelection(hitItem, hitIndex, event); } else { if (!handleItemClick_MultiSelection(hitItem, hitIndex, event)) { return; } } handleItemClick_ClickCallbacks(hitItem, hitIndex, event); mLastClickItem = hitItem; } void GuiListBoxCtrl::handleItemClick_SingleSelection(LBItem* hitItem, S32 hitIndex, const GuiEvent& event) { // No current selection? Just select the cell and move on S32 selItem = getSelectedItem(); if (selItem != hitIndex && selItem != -1) clearSelection(); // Set the current selection setCurSel(hitIndex); } bool GuiListBoxCtrl::handleItemClick_MultiSelection(LBItem* hitItem, S32 hitIndex, const GuiEvent& event) { // Deal with multiple selections if (event.modifier & SI_CTRL) { // Ctrl-Click toggles selection if (hitItem->isSelected) { removeSelection(hitItem, hitIndex); // We return here when we deselect an item because we don't store last clicked when we deselect return false; } else addSelection(hitItem, hitIndex); } else if (event.modifier & SI_SHIFT) { if (!mLastClickItem) addSelection(hitItem, hitIndex); else setCurSelRange(getItemIndex(mLastClickItem), hitIndex); } else { if (getSelCount() != 0) { S32 selItem = getSelectedItem(); if (selItem != -1 && mItems[selItem] != hitItem) clearSelection(); } addSelection(hitItem, hitIndex); } return true; } void GuiListBoxCtrl::handleItemClick_ClickCallbacks(LBItem* hitItem, S32 hitIndex, const GuiEvent& event) { if (hitItem == mLastClickItem && event.mouseClickCount == 2) { if (caller->isMethod("onDoubleClick")) Con::executef(caller, 4, "onDoubleClick", Con::getIntArg(hitIndex), hitItem->itemText, Con::getIntArg(hitItem->ID)); } else if (caller->isMethod("onClick")) { Con::executef(caller, 4, "onClick", Con::getIntArg(hitIndex), hitItem->itemText, Con::getIntArg(hitItem->ID)); } } bool GuiListBoxCtrl::onKeyDown(const GuiEvent &event) { //if this control is a dead end, make sure the event stops here if (!mVisible || !mActive || !mAwake || mItems.size() == 0) return true; S32 index = getSelectedItem(); switch (event.keyCode) { case KEY_RETURN: if (mAltConsoleCommand[0]) Con::evaluate(mAltConsoleCommand, false); return true; case KEY_LEFT: case KEY_UP: if (index == -1) { //Select the bottom item addSelection(mItems.size() - 1); return true; } else if(index != 0) { addSelection(index - 1); return true; } break; case KEY_DOWN: case KEY_RIGHT: if (index == -1) { //Select the top item addSelection(0); return true; } else if (index != (mItems.size() - 1)) { addSelection(index + 1); return true; } break; case KEY_HOME: addSelection(0); return true; case KEY_END: addSelection(mItems.size() - 1); return true; case KEY_DELETE: if (index != -1 && isMethod("onDeleteKey")) Con::executef(caller, 4, "onDeleteKey", Con::getIntArg(index), getItemText(index), Con::getIntArg(getItemID(index))); return true; default: return Parent::onKeyDown(event); }; return false; } #pragma endregion #pragma region sorting bool GuiListBoxCtrl::LBItem::sIncreasing = true; void GuiListBoxCtrl::sortByText(bool increasing) { if (mItems.size() < 2) return; LBItem::sIncreasing = increasing; std::sort(mItems.begin(), mItems.end(), LBItem::compByText); } void GuiListBoxCtrl::sortByID(bool increasing) { if (mItems.size() < 2) return; LBItem::sIncreasing = increasing; std::sort(mItems.begin(), mItems.end(), LBItem::compByID); } #pragma endregion