//----------------------------------------------------------------------------- // Torque Game Engine // Copyright (C) GarageGames.com, Inc. //----------------------------------------------------------------------------- #include "console/console.h" #include "console/consoleTypes.h" #include "dgl/dgl.h" #include "console/simBase.h" #include "gui/core/guiCanvas.h" #include "gui/containers/guiTabBookCtrl.h" #include "platform/event.h" #include "core/fileStream.h" #include "gui/containers/guiScrollCtrl.h" #include "gui/editor/guiEditCtrl.h" #include "gui/controls/guiPopUpCtrl.h" #include "gui/core/guiDefaultControlRender.h" // So we can set tab alignment via gui editor static EnumTable::Enums tabAlignEnums[] = { { GuiTabBookCtrl::AlignTop, "Top" }, { GuiTabBookCtrl::AlignLeft, "Left" }, { GuiTabBookCtrl::AlignBottom,"Bottom" }, { GuiTabBookCtrl::AlignRight, "Right" } }; static EnumTable gTabAlignEnums(4,&tabAlignEnums[0]); IMPLEMENT_CONOBJECT(GuiTabBookCtrl); GuiTabBookCtrl::GuiTabBookCtrl() { VECTOR_SET_ASSOCIATION(mPages); mTabHeight = 24; mLastTabHeight = mTabHeight; mTabPosition = GuiTabBookCtrl::AlignTop; mLastTabPosition = mTabPosition; mActivePage = NULL; mHoverTab = NULL; mHasTexture = false; mBitmapBounds = NULL; mBounds.extent.set( 400, 300 ); mPageRect = RectI(0,0,0,0); mTabRect = RectI(0,0,0,0); mPages.reserve(12); mTabMargin = 7; mMinTabWidth = 64; } void GuiTabBookCtrl::initPersistFields() { Parent::initPersistFields(); addField("TabPosition", TypeEnum, Offset(mTabPosition,GuiTabBookCtrl), 1, &gTabAlignEnums ); addField("TabHeight", TypeS32, Offset(mTabHeight,GuiTabBookCtrl) ); addField("TabMargin", TypeS32, Offset(mTabMargin,GuiTabBookCtrl)); addField("MinTabWidth", TypeS32, Offset(mMinTabWidth,GuiTabBookCtrl)); } // Empty for now, will implement for handling design time context menu for manipulating pages ConsoleMethod( GuiTabBookCtrl, addPage, void, 2, 2, "(no arguments expected)") { object->addNewPage(); } //ConsoleMethod( GuiTabBookCtrl, removePage, void, 2, 2, "()") //{ //} bool GuiTabBookCtrl::onAdd() { Parent::onAdd(); return true; } void GuiTabBookCtrl::onRemove() { Parent::onRemove(); } void GuiTabBookCtrl::onChildRemoved( GuiControl* child ) { for (S32 i = 0; i < mPages.size(); i++ ) { GuiTabPageCtrl* tab = mPages[i].Page; if( tab == child ) { if( tab == mActivePage ) mActivePage = NULL; mPages.erase( i ); break; } } if( mPages.empty() ) mActivePage = NULL; else if (mActivePage == NULL ) mActivePage = static_cast(mPages[0].Page); } void GuiTabBookCtrl::onChildAdded( GuiControl *child ) { GuiTabPageCtrl *page = dynamic_cast(child); if( !page ) { Con::warnf("GuiTabBookCtrl::onChildAdded - attempting to add NON GuiTabPageCtrl as child page"); SimObject *simObj = reinterpret_cast(child); removeObject( simObj ); if( mActivePage ) { mActivePage->addObject( simObj ); } else { Con::warnf("GuiTabBookCtrl::onChildAdded - unable to find active page to reassign ownership of new child control to, placing on parent"); GuiControl *rent = getParent(); if( rent ) rent->addObject( simObj ); } return; } TabHeaderInfo newPage; newPage.Page = page; newPage.TabRow = -1; newPage.TabColumn = -1; mPages.push_back( newPage ); // Calculate Page Information calculatePageTabs(); child->resize( mPageRect.point, mPageRect.extent ); // Select this Page selectPage( page ); } bool GuiTabBookCtrl::onWake() { if (! Parent::onWake()) return false; mHasTexture = mProfile->constructBitmapArray(); if( mHasTexture ) mBitmapBounds = mProfile->mBitmapArrayRects.address(); return true; } void GuiTabBookCtrl::onSleep() { Parent::onSleep(); } void GuiTabBookCtrl::addNewPage() { char textbuf[1024]; GuiTabPageCtrl * page = new GuiTabPageCtrl(); page->setField("profile", "GuiTabPageProfile"); dSprintf(textbuf, sizeof(textbuf), "TabBookPage%d_%d", getId(), page->getId()); page->registerObject(textbuf); this->addObject( page ); } void GuiTabBookCtrl::resize(const Point2I &newPosition, const Point2I &newExtent) { Parent::resize( newPosition, newExtent ); calculatePageTabs(); // Resize Children SimSet::iterator i; for(i = begin(); i != end(); i++) { GuiControl *ctrl = static_cast(*i); ctrl->resize( mPageRect.point, mPageRect.extent ); } } void GuiTabBookCtrl::childResized(GuiControl *child) { //Parent::childResized( child ); child->resize( mPageRect.point, mPageRect.extent ); } void GuiTabBookCtrl::onMouseDown(const GuiEvent &event) { Point2I localMouse = globalToLocalCoord( event.mousePoint ); if( mTabRect.pointInRect( localMouse ) ) { GuiTabPageCtrl *tab = findHitTab( localMouse ); if( tab != NULL ) selectPage( tab ); } } void GuiTabBookCtrl::onMouseMove(const GuiEvent &event) { Point2I localMouse = globalToLocalCoord( event.mousePoint ); if( mTabRect.pointInRect( localMouse ) ) { GuiTabPageCtrl *tab = findHitTab( localMouse ); if( tab != NULL && mHoverTab != tab ) mHoverTab = tab; else if ( !tab ) mHoverTab = NULL; } Parent::onMouseMove( event ); } void GuiTabBookCtrl::onMouseLeave( const GuiEvent &event ) { mHoverTab = NULL; } bool GuiTabBookCtrl::onMouseDownEditor(const GuiEvent &event, Point2I offset) { bool handled = false; Point2I localMouse = globalToLocalCoord( event.mousePoint ); if( mTabRect.pointInRect( localMouse ) ) { GuiTabPageCtrl *tab = findHitTab( localMouse ); if( tab != NULL ) { selectPage( tab ); handled = true; } } // This shouldn't be called if it's not design time, but check just incase if ( GuiControl::smDesignTime ) { // If we clicked in the editor and our addset is the tab book // ctrl, select the child ctrl so we can edit it's properties GuiEditCtrl* edit = GuiControl::smEditorHandle; if( edit && ( edit->getAddSet() == this ) && mActivePage != NULL ) edit->select( mActivePage ); } // Return whether we handled this or not. return handled; } void GuiTabBookCtrl::onPreRender() { // sometimes we need to resize because of a changed persistent field // that's what this does solveDirty(); } void GuiTabBookCtrl::onRender(Point2I offset, const RectI &updateRect) { RectI tabRect = mTabRect; tabRect.point += offset; RectI pageRect = mPageRect; pageRect.point += offset; // We're so nice we'll store the old modulation before we clear it for our rendering! :) ColorI oldModulation; dglGetBitmapModulation( &oldModulation ); // Wipe it out dglClearBitmapModulation(); // Render background renderBackground( offset, updateRect ); // Render our tabs renderTabs( offset ); // Render Children renderChildControls( offset, updateRect ); // Restore old modulation dglSetBitmapModulation( oldModulation ); } void GuiTabBookCtrl::renderBackground( Point2I offset, const RectI& updateRect ) { RectI winRect; winRect.point = offset; winRect.extent = mBounds.extent; if( mHasTexture && mProfile->mBitmapArrayRects.size() >= NumBitmaps) renderSizableBitmapBordersFilledIndex( winRect, TabBackground, mProfile ); else dglDrawRectFill(winRect, mProfile->mFillColor); } void GuiTabBookCtrl::renderTabs( const Point2I &offset ) { // If the tab size is zero, don't render tabs, // and assume it's a tab-less tab-book - JDD if( mPages.empty() || mTabHeight <= 0 ) return; for( S32 i = 0; i < mPages.size(); i++ ) { RectI tabBounds = mPages[i].TabRect; tabBounds.point += offset; GuiTabPageCtrl *tab = mPages[i].Page; if( tab != NULL ) renderTab( tabBounds, tab ); } } void GuiTabBookCtrl::renderTab( RectI tabRect, GuiTabPageCtrl *tab ) { StringTableEntry text = tab->getText(); ColorI oldColor; dglGetBitmapModulation( &oldColor ); // Is this a skinned control? if( mHasTexture && mProfile->mBitmapArrayRects.size() >= NumBitmaps) { S32 indexMultiplier = 1; switch( mTabPosition ) { case AlignTop: case AlignBottom: if ( mActivePage == tab ) indexMultiplier += TabSelected; else if( mHoverTab == tab ) indexMultiplier += TabHover; else indexMultiplier += TabNormal; //dglDrawBitmapStretchSR(mProfile->mTextureHandle,tabRect,stretchRect, ( mTabPosition == AlignBottom ) ? GFlip_Y : 0 ); break; case AlignLeft: case AlignRight: if ( mActivePage == tab ) indexMultiplier += TabSelectedVertical; else if( mHoverTab == tab ) indexMultiplier += TabHoverVertical; else indexMultiplier += TabNormalVertical; //dglDrawBitmapStretchSR(mProfile->mTextureHandle,tabRect,stretchRect, ( mTabPosition == AlignRight ) ? GFlip_X : 0 ); break; } renderFixedBitmapBordersFilled( tabRect, indexMultiplier, mProfile ); } else { // If this isn't a skinned control or the bitmap is simply missing, handle it WELL if ( mActivePage == tab ) dglDrawRectFill(tabRect, mProfile->mFillColor); else if( mHoverTab == tab ) dglDrawRectFill(tabRect, mProfile->mFillColorHL); else dglDrawRectFill(tabRect, mProfile->mFillColorNA); } dglSetBitmapModulation(mProfile->mFontColor); switch( mTabPosition ) { case AlignTop: case AlignBottom: renderJustifiedTextRot( tabRect.point, tabRect.extent, text, 0); break; case AlignLeft: renderJustifiedTextRot( tabRect.point, tabRect.extent, text, -90 ); break; case AlignRight: renderJustifiedTextRot( tabRect.point, tabRect.extent, text, -90 ); break; } dglSetBitmapModulation( oldColor); } void GuiTabBookCtrl::renderJustifiedTextRot(Point2I offset, Point2I extent, const char *text, F32 rot ) { GFont *font = mProfile->mFont; S32 textWidth = font->getStrWidth(text); Point2I start; offset += mProfile->mTextOffset; if( mTabPosition == AlignLeft || mTabPosition == AlignRight ) { switch( mProfile->mAlignment ) { case GuiControlProfile::RightJustify: start.set( 0, extent.y - textWidth); break; case GuiControlProfile::CenterJustify: start.set( 0, ( extent.y - textWidth) / 2); break; default: // GuiControlProfile::LeftJustify start.set( 0, 0 ); break; } if( textWidth > extent.y ) start.set( 0, 0 ); start.x = ( ( extent.x - font->getHeight() ) / 2 ) + font->getHeight(); } else { // align the horizontal switch( mProfile->mAlignment ) { case GuiControlProfile::RightJustify: start.set( extent.x - textWidth, 0 ); break; case GuiControlProfile::CenterJustify: start.set( ( extent.x - textWidth) / 2, 0 ); break; default: // GuiControlProfile::LeftJustify start.set( 0, 0 ); break; } if( textWidth > extent.x ) start.set( 0, 0 ); start.y = ( extent.y - font->getHeight() ) / 2; } dglDrawText( font, start + offset, text, mProfile->mFontColors,9,rot ); } // This is nothing but a clever hack to allow the tab page children // to cast this to a GuiControl* so that the file doesn't need to have circular // includes. generic method overriding for the win! void GuiTabBookCtrl::setUpdate() { Parent::setUpdate(); setUpdateRegion(Point2I(0,0), mBounds.extent); calculatePageTabs(); } void GuiTabBookCtrl::solveDirty() { bool dirty = false; if( mTabPosition != mLastTabPosition ) { mLastTabPosition = mTabPosition; dirty = true; } if( mTabHeight != mLastTabHeight ) { mLastTabHeight = mTabHeight; dirty = true; } if( mTabWidth != mLastTabWidth ) { mLastTabWidth = mTabWidth; dirty = true; } if( dirty ) { calculatePageTabs(); resize( mBounds.point, mBounds.extent ); } } S32 GuiTabBookCtrl::calculatePageTabWidth( GuiTabPageCtrl *page ) { if( !page ) return mTabWidth; StringTableEntry text = page->getText(); if( !text || dStrlen(text) == 0 || mProfile->mFont == NULL ) return mTabWidth; GFont *font = mProfile->mFont; return font->getStrNWidth( text, dStrlen(text) ); } void GuiTabBookCtrl::calculatePageTabs() { // Short Circuit. // // If the tab size is zero, don't render tabs, // and assume it's a tab-less tab-book - JDD if( mPages.empty() || mTabHeight <= 0 ) return; S32 currRow = 0; S32 currColumn = 0; S32 maxRow = 0; S32 maxColumn = 0; S32 currX = 0; S32 currY = 0; S32 maxWidth = 0; S32 maxHeight = 0; for( S32 i = 0; i < mPages.size(); i++ ) { // Fetch Tab Width S32 tabWidth = calculatePageTabWidth( mPages[i].Page ) + ( mTabMargin * 2 ); tabWidth = getMax( tabWidth, mMinTabWidth ); TabHeaderInfo &info = mPages[i]; switch( mTabPosition ) { case AlignTop: case AlignBottom: // If we're going to go outside our bounds // with this tab move it down a row if( currX + tabWidth > mBounds.extent.x ) { // Calculate and Advance State. maxWidth = getMax( tabWidth, maxWidth ); balanceRow( currRow, currX ); info.TabRow = ++currRow; // Reset Necessaries info.TabColumn = currColumn = maxWidth = currX = 0; } else { info.TabRow = currRow; info.TabColumn = currColumn++; } // Calculate Tabs Bounding Rect info.TabRect.point.x = currX; info.TabRect.extent.x = tabWidth; info.TabRect.extent.y = mTabHeight; // Adjust Y Point based on alignment if( mTabPosition == AlignTop ) info.TabRect.point.y = ( info.TabRow * mTabHeight ); else info.TabRect.point.y = mBounds.extent.y - ( ( 1 + info.TabRow ) * mTabHeight ); currX += tabWidth; break; case AlignLeft: case AlignRight: // If we're going to go outside our bounds // with this tab move it down a row if( currY + tabWidth > mBounds.extent.y ) { // Balance Tab Column. balanceColumn( currColumn, currY ); // Calculate and Advance State. info.TabColumn = ++currColumn; info.TabRow = currRow = currY = 0; } else { info.TabColumn = currColumn; info.TabRow = currRow++; } // Calculate Tabs Bounding Rect info.TabRect.point.y = currY; info.TabRect.extent.y = tabWidth; info.TabRect.extent.x = mTabHeight; // Adjust Y Point based on alignment if( mTabPosition == AlignLeft ) info.TabRect.point.x = ( info.TabColumn * mTabHeight ); else info.TabRect.point.x = mBounds.extent.x - ( (1 + info.TabColumn) * mTabHeight ); currY += tabWidth; break; }; } currRow++; currColumn++; Point2I localPoint = mBounds.extent; // Calculate switch( mTabPosition ) { case AlignTop: localPoint.y -= mBounds.point.y; mTabRect.point.x = 0; mTabRect.extent.x = localPoint.x; mTabRect.point.y = 0; mTabRect.extent.y = currRow * mTabHeight; mPageRect.point.x = 0; mPageRect.point.y = mTabRect.extent.y; mPageRect.extent.x = mTabRect.extent.x; mPageRect.extent.y = mBounds.extent.y - mTabRect.extent.y; break; case AlignBottom: mTabRect.point.x = 0; mTabRect.extent.x = localPoint.x; mTabRect.extent.y = currRow * mTabHeight; mTabRect.point.y = mBounds.extent.y - mTabRect.extent.y; mPageRect.point.x = 0; mPageRect.point.y = 0; mPageRect.extent.x = mTabRect.extent.x; mPageRect.extent.y = localPoint.y - mTabRect.extent.y; break; case AlignLeft: mTabRect.point.y = 0; mTabRect.extent.y = mBounds.extent.y; mTabRect.point.x = 0; mTabRect.extent.x = currColumn * mTabHeight; mPageRect.point.y = 0; mPageRect.point.x = mTabRect.extent.x; mPageRect.extent.y = localPoint.y; mPageRect.extent.x = localPoint.x - mTabRect.extent.x; break; case AlignRight: mTabRect.extent.x = currColumn * mTabHeight; mTabRect.point.y = 0; mTabRect.extent.y = localPoint.y; mTabRect.point.x = localPoint.x - mTabRect.extent.x; mPageRect.point.y = 0; mPageRect.point.x = 0; mPageRect.extent.y = localPoint.y; mPageRect.extent.x = localPoint.x - mTabRect.extent.x; break; }; } void GuiTabBookCtrl::balanceColumn( S32 column , S32 totalTabWidth ) { // Short Circuit. // // If the tab size is zero, don't render tabs, // and assume it's a tab-less tab-book - JDD if( mPages.empty() || mTabHeight <= 0 ) return; Vector rowTemp; rowTemp.clear(); for( S32 i = 0; i < mPages.size(); i++ ) { TabHeaderInfo &info = mPages[i]; if(info.TabColumn == column ) rowTemp.push_back( &mPages[i] ); } if( rowTemp.empty() ) return; // Balance the tabs across the remaining space S32 spaceToDivide = mBounds.extent.y - totalTabWidth; S32 pointDelta = 0; for( S32 i = 0; i < rowTemp.size(); i++ ) { TabHeaderInfo &info = *rowTemp[i]; S32 extraSpace = (S32)( spaceToDivide / rowTemp.size() ); info.TabRect.extent.y += extraSpace; info.TabRect.point.y += pointDelta; pointDelta += extraSpace; } } void GuiTabBookCtrl::balanceRow( S32 row, S32 totalTabWidth ) { // Short Circuit. // // If the tab size is zero, don't render tabs, // and assume it's a tab-less tab-book - JDD if( mPages.empty() || mTabHeight <= 0 ) return; Vector rowTemp; rowTemp.clear(); for( S32 i = 0; i < mPages.size(); i++ ) { TabHeaderInfo &info = mPages[i]; if(info.TabRow == row ) rowTemp.push_back( &mPages[i] ); } if( rowTemp.empty() ) return; // Balance the tabs across the remaining space S32 spaceToDivide = mBounds.extent.x - totalTabWidth; S32 pointDelta = 0; for( S32 i = 0; i < rowTemp.size(); i++ ) { TabHeaderInfo &info = *rowTemp[i]; S32 extraSpace = (S32)spaceToDivide / ( rowTemp.size() ); info.TabRect.extent.x += extraSpace; info.TabRect.point.x += pointDelta; pointDelta += extraSpace; } } GuiTabPageCtrl *GuiTabBookCtrl::findHitTab( const GuiEvent &event ) { return findHitTab( event.mousePoint ); } GuiTabPageCtrl *GuiTabBookCtrl::findHitTab( Point2I hitPoint ) { // Short Circuit. // // If the tab size is zero, don't render tabs, // and assume it's a tab-less tab-book - JDD if( mPages.empty() || mTabHeight <= 0 ) return NULL; for( S32 i = 0; i < mPages.size(); i++ ) { if( mPages[i].TabRect.pointInRect( hitPoint ) ) return mPages[i].Page; } return NULL; } ConsoleMethod( GuiTabBookCtrl, selectPage, void, 3, 3, "(int pageIndex)") { S32 pageIndex = dAtoi(argv[2]); object->selectPage(pageIndex); } void GuiTabBookCtrl::selectPage( S32 index ) { if( mPages.size() < index ) return; // Select the page selectPage( mPages[ index ].Page ); } void GuiTabBookCtrl::selectPage( GuiTabPageCtrl *page ) { Vector::iterator i = mPages.begin(); for( ; i != mPages.end() ; i++ ) { GuiTabPageCtrl *tab = reinterpret_cast((*i).Page); if( page == tab ) { mActivePage = tab; tab->setVisible( true ); // Notify User char *retBuffer = Con::getReturnBuffer( 512 ); dStrcpy( retBuffer, tab->getText() ); Con::executef( this, 2, "onTabSelected", retBuffer ); } else tab->setVisible( false ); } } bool GuiTabBookCtrl::onKeyDown(const GuiEvent &event) { // Tab = Next Page // Ctrl-Tab = Previous Page if( 0 && event.keyCode == KEY_TAB ) { if( event.modifier & SI_CTRL ) selectPrevPage(); else selectNextPage(); return true; } return Parent::onKeyDown( event ); } void GuiTabBookCtrl::selectNextPage() { if( mPages.empty() ) return; if( mActivePage == NULL ) mActivePage = mPages[0].Page; S32 nI = 0; for( ; nI < mPages.size(); nI++ ) { GuiTabPageCtrl *tab = mPages[ nI ].Page; if( tab == mActivePage ) { if( nI == ( mPages.size() - 1 ) ) selectPage( 0 ); else if ( nI + 1 <= ( mPages.size() - 1 ) ) selectPage( nI + 1 ); else selectPage( 0 ); // Notify User if( isMethod( "onTabSelected" ) ) { char *retBuffer = Con::getReturnBuffer( 512 ); dStrcpy( retBuffer, tab->getText() ); Con::executef( this, 2, "onTabSelected", retBuffer ); } return; } } } void GuiTabBookCtrl::selectPrevPage() { if( mPages.empty() ) return; if( mActivePage == NULL ) mActivePage = mPages[0].Page; S32 nI = 0; for( ; nI < mPages.size(); nI++ ) { GuiTabPageCtrl *tab = mPages[ nI ].Page; if( tab == mActivePage ) { if( nI == 0 ) selectPage( mPages.size() - 1 ); else selectPage( nI - 1 ); // Notify User if( isMethod( "onTabSelected" ) ) { char *retBuffer = Con::getReturnBuffer( 512 ); dStrcpy( retBuffer, tab->getText() ); Con::executef( this, 2, "onTabSelected", retBuffer ); } return; } } }