// Copyright (C) 2007 Id Software, Inc. // #include "../precompiled.h" #pragma hdrstop #if defined( _DEBUG ) && !defined( ID_REDIRECT_NEWDELETE ) #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #include "../misc/WorldToScreen.h" #include "UIWindow.h" #include "UIRadialMenu.h" #include "UserInterfaceLocal.h" #include "UserInterfaceManager.h" #include "../Player.h" #include "../script/Script_Helper.h" #include "../../sys/sys_local.h" // the angles on the circle static const float DEFAULT_WEDGE_SIZE = 45.0f; static const float DEFAULT_WEDGE_START = 180.0f - DEFAULT_WEDGE_SIZE * 0.5f; static const float DEFAULT_WEDGE_END = DEFAULT_WEDGE_START + DEFAULT_WEDGE_SIZE; static const float LEFT_ARC_START = DEFAULT_WEDGE_END; static const float LEFT_ARC_END = 360.0f - 45.0f; static const float LEFT_ARC_RANGE = LEFT_ARC_END - LEFT_ARC_START; const char sdUITemplateFunctionInstance_IdentifierRadialMenu[] = "sdUIRadialFunction"; idBlockAlloc< sdUIRadialMenu::vec4EvaluatorList_t, 32 > sdUIRadialMenu::vec4EvaluatorListAllocator; SD_UI_IMPLEMENT_CLASS( sdUIRadialMenu, sdUIWindow ) idHashMap< sdUITemplateFunction< sdUIRadialMenu >* > sdUIRadialMenu::radialMenuFunctions; SD_UI_PUSH_CLASS_TAG( sdUIRadialMenu ) const char* sdUIRadialMenu::eventNames[ RME_NUM_EVENTS - WE_NUM_EVENTS ] = { SD_UI_EVENT_TAG( "onCommand", "[Command Name]", "Called when the given input command occurs. The game script calls these commands to control navigation within the quick chat." ), SD_UI_EVENT_PARM_TAG( "onMeasureItem", "", "Measure the item size before drawing." ), SD_UI_EVENT_PARM( float, "itemIndex", "Item index" ) SD_UI_EVENT_PARM( float, "itemStyle", "Item style, position of the item if drawing a radial menu." ) SD_UI_EVENT_PARM( vec2, "size", "Size of rectangle" ) SD_UI_EVENT_RETURN_PARM( vec2, "New size of rectangle" ) SD_UI_END_EVENT_TAG SD_UI_EVENT_TAG( "onDrawItem", "", "Draw a quick chat item." ), SD_UI_EVENT_PARM( wstring, "key", "Shortcut key" ) SD_UI_EVENT_PARM( float, "title", "Localized title (convert float to a handle before usage)." ) SD_UI_EVENT_PARM( float, "chevron", "Material handle for chevron to draw." ) SD_UI_EVENT_PARM( float, "enabled", "Player is able to execute the quick chat item." ) SD_UI_EVENT_PARM( rect, "itemRecte", "Item rectangle." ) SD_UI_EVENT_PARM( float, "itemIndex", "Item index." ) SD_UI_EVENT_PARM( float, "itemStyle", "Item style, position of the item if drawing a radial menu." ) SD_UI_EVENT_PARM( string, "drawCallback", "Optional callback when drawing. GUI posts an optional named event with this string." ) SD_UI_END_EVENT_TAG SD_UI_EVENT_TAG( "onDrawContext", "", "Draw the context button (centered in the radial menu)." ), SD_UI_EVENT_PARM( rect, "itemRect", "Context rectangle" ) SD_UI_END_EVENT_TAG SD_UI_EVENT_TAG( "onDrawDeadZone", "", "Not used." ), SD_UI_EVENT_TAG( "onPagePushed", "", "Called when the player enters a new page." ), SD_UI_EVENT_TAG( "onPagePopped", "", "Called when the player exits a page. currentPage will be -1 if there is no valid page." ), }; SD_UI_POP_CLASS_TAG idCVar gui_debugRadialMenus( "gui_debugRadialMenus", "0", CVAR_GAME | CVAR_BOOL, "Show radial menu debugging info" ); idCVar g_radialMenuStyle( "g_radialMenuStyle", "0", CVAR_GAME | CVAR_INTEGER | CVAR_ARCHIVE | CVAR_PROFILE, "Sets the style of the quick chat menu: 0 = radial, 1 = vertical" ); idCVar g_radialMenuUseNumberShortcuts( "g_radialMenuUseNumberShortcuts", "1", CVAR_GAME | CVAR_BOOL | CVAR_ARCHIVE | CVAR_PROFILE, "Use numbers instead of alpha-numeric shortcuts" ); idCVar g_radialMenuMouseSensitivity( "g_radialMenuMouseSensitivity", "0.5", CVAR_GAME | CVAR_FLOAT | CVAR_ARCHIVE | CVAR_PROFILE, "Mouse input scale" ); /* ============ sdUIRadialMenu::radialItem_t::Update ============ */ bool sdUIRadialMenu::radialItem_t::Update() { bool sizeChanged = false; if ( scriptUpdateCallback.Length() > 0 ) { idPlayer* localPlayer = gameLocal.GetLocalPlayer(); if ( localPlayer != NULL ) { const sdProgram::sdFunction* callback = localPlayer->GetScriptFunction( scriptUpdateCallback.c_str() ); if ( callback != NULL ) { bool wasEnabled = flags.enabled; sdScriptHelper h; h.Push( scriptUpdateParm.c_str() ); localPlayer->CallNonBlockingScriptEvent( callback, h ); const char* returnVal = h.GetReturnedString(); idStrList list; idSplitStringIntoList( list, returnVal, "|" ); if ( list.Num() >= 5 ) { flags.enabled = sdTypeFromString< bool >( list[ 4 ] ); } else { flags.enabled = true; } if ( flags.enabled || wasEnabled ) { if ( list.Num() >= 1 ) { title = declHolder.FindLocStr( list[ 0 ] ); sizeChanged = true; /* this can spam the console, only use it for debugging if( title->GetState() == DS_DEFAULTED ) { gameLocal.Warning( "Invalid title %s", title->GetName() ); } */ } if ( list.Num() >= 2 ) { if ( list[ 1 ].Length() == 0 ) { part.mi.Clear(); } else { owner->GetScope().GetUI()->LookupMaterial( list[ 1 ].c_str(), part.mi, &part.width, &part.height ); } } commandID.SetNum( 0 ); commandData.SetNum( 0 ); if ( list.Num() >= 4 ) { commandID.Alloc() = list[ 2 ]; commandData.Alloc() = list[ 3 ]; } if ( list.Num() >= 6 ) { // extra info commandData.Alloc() = list[ 5 ]; } } } } } return sizeChanged; } /* ============ sdUIRadialMenu::InitFunctions ============ */ #pragma inline_depth( 0 ) #pragma optimize( "", off ) SD_UI_PUSH_CLASS_TAG( sdUIRadialMenu ) void sdUIRadialMenu::InitFunctions() { SD_UI_FUNC_TAG( insertItem, "Insert an item into a page." ) SD_UI_FUNC_PARM( float, "page", "Page number." ) SD_UI_FUNC_PARM( string, "text", "Item text. \"title||id,id2,id3||data,data1,data2||key\"." ) SD_UI_FUNC_RETURN_PARM( float, "Number of items." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "insertItem", new sdUITemplateFunction< sdUIRadialMenu >( 'f', "fs", &sdUIRadialMenu::Script_InsertItem ) ); SD_UI_FUNC_TAG( insertPage, "Insert a new page." ) SD_UI_FUNC_PARM( string, "title", "Page title." ) SD_UI_FUNC_RETURN_PARM( float, "Page number for current page." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "insertPage", new sdUITemplateFunction< sdUIRadialMenu >( 'f', "s", &sdUIRadialMenu::Script_InsertPage ) ); SD_UI_FUNC_TAG( getItemData, "Get item data." ) SD_UI_FUNC_PARM( float, "itemPage", "Item page. If -1 then use the current page." ) SD_UI_FUNC_PARM( float, "itemNumber", "Item number. If -1 then use the current page" ) SD_UI_FUNC_PARM( float, "dataIndex", "Command data index." ) SD_UI_FUNC_RETURN_PARM( string, "Commnand data." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "getItemData", new sdUITemplateFunction< sdUIRadialMenu >( 's', "fff", &sdUIRadialMenu::Script_GetItemData ) ); SD_UI_FUNC_TAG( clear, "Clear all pages." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "clear", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "", &sdUIRadialMenu::Script_Clear ) ); SD_UI_FUNC_TAG( clearPage, "Clear all page items." ) SD_UI_FUNC_PARM( float, "page", "Page index." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "clearPage", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "f", &sdUIRadialMenu::Script_ClearPage ) ); SD_UI_FUNC_TAG( pushPage, "Push page on stack, make it the current page and reset mouse position. Calls onPagePushed event." ) SD_UI_FUNC_PARM( float, "page", "Page index." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "pushPage", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "f", &sdUIRadialMenu::Script_PushPage ) ); SD_UI_FUNC_TAG( popPage, "Pop the stack of pages. Current page is the page on the top after popping." ) SD_UI_FUNC_RETURN_PARM( string, "The page popped." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "popPage", new sdUITemplateFunction< sdUIRadialMenu >( 'f', "", &sdUIRadialMenu::Script_PopPage ) ); SD_UI_FUNC_TAG( clearPageStack, "Clear the page stack." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "clearPageStack", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "", &sdUIRadialMenu::Script_ClearPageStack ) ); SD_UI_FUNC_TAG( postCommand, "Post a command with the command data for the item." ) SD_UI_FUNC_PARM( float, "page", "Page number. Current page if -1." ) SD_UI_FUNC_PARM( float, "item", "Item number. Current item if -1." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "postCommand", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "ff", &sdUIRadialMenu::Script_PostCommand ) ); SD_UI_FUNC_TAG( loadFromDef, "Load a radial menu from def files." ) SD_UI_FUNC_PARM( string, "defName", "Def name to load." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "loadFromDef", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "s", &sdUIRadialMenu::Script_LoadFromDef ) ); SD_UI_FUNC_TAG( appendFromDef, "Load a radial menu page from def files." ) SD_UI_FUNC_PARM( string, "defName", "Def name to load." ) SD_UI_FUNC_PARM( float, "page", "Page index." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "appendFromDef", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "sf", &sdUIRadialMenu::Script_AppendFromDef ) ); SD_UI_FUNC_TAG( fillFromEnumerator, "Fill menu from an enumerator." ) SD_UI_FUNC_PARM( string, "enumerator", "Name of enumerator." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "fillFromEnumerator", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "s", &sdUIRadialMenu::Script_FillFromEnumerator ) ); SD_UI_FUNC_TAG( transitionItemVec4, "Transition for an item." ) SD_UI_FUNC_PARM( float, "propertyType", "Property type. Either RTP_FORECOLOR/RTP_BACKCOLOR/RTP_PROPERTY_0/RTP_PROPERTY_1/RTP_PROPERTY_2/RTP_PROPERTY_3." ) SD_UI_FUNC_PARM( vec4, "from", "Transition from." ) SD_UI_FUNC_PARM( vec4, "to", "Transition to." ) SD_UI_FUNC_PARM( float, "time", "Time for transition." ) SD_UI_FUNC_PARM( string, "acceleration", "Non linear transition." ) SD_UI_FUNC_PARM( float, "item", "Item index." ) SD_UI_FUNC_PARM( float, "page", "Page index." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "transitionItemVec4", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "f44fsff", &sdUIRadialMenu::Script_TransitionItemVec4 ) ); SD_UI_FUNC_TAG( getItemTransitionVec4Result, "Get the result from a transition given a property type." ) SD_UI_FUNC_PARM( float, "propertyType", "Where property type is one of:\n* RTP_FORECOLOR\n* RTP_BACKCOLOR\n* RTP_PROPERTY_0\n* RTP_PROPERTY_1\n* RTP_PROPERTY_2\n* RTP_PROPERTY_3." ) SD_UI_FUNC_PARM( vec4, "default", "Default value is returned if item is not found or propertyType is invalid." ) SD_UI_FUNC_PARM( float, "item", "Item index." ) SD_UI_FUNC_PARM( float, "page", "Page index." ) SD_UI_FUNC_RETURN_PARM( vec4, "Transition value." ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "getItemTransitionVec4Result", new sdUITemplateFunction< sdUIRadialMenu >( '4', "f4ff", &sdUIRadialMenu::Script_GetItemTransitionVec4Result ) ); SD_UI_FUNC_TAG( clearTransitions, "Clear transitions." ) SD_UI_FUNC_PARM( float, "item", "Item index. All items if less than 0" ) SD_UI_FUNC_PARM( float, "page", "Page index. All pages if less than 0" ) SD_UI_END_FUNC_TAG radialMenuFunctions.Set( "clearTransitions", new sdUITemplateFunction< sdUIRadialMenu >( 'v', "ff", &sdUIRadialMenu::Script_ClearTransitions ) ); SD_UI_ENUM_TAG( RMF_USE_NUMBER_SHORTCUTS, "Use number shortcuts." ) sdDeclGUI::AddDefine( va( "RMF_USE_NUMBER_SHORTCUTS %i", RMF_USE_NUMBER_SHORTCUTS ) ); SD_UI_PUSH_GROUP_TAG( "Radial Transition Property Flags" ) SD_UI_ENUM_TAG( RTP_FORECOLOR, "Forecolor transition property." ) sdDeclGUI::AddDefine( va( "RTP_FORECOLOR %i", RTP_FORECOLOR ) ); SD_UI_ENUM_TAG( RTP_BACKCOLOR, "Backcolor transition property." ) sdDeclGUI::AddDefine( va( "RTP_BACKCOLOR %i", RTP_BACKCOLOR ) ); SD_UI_ENUM_TAG( RTP_PROPERTY_0, "Transition property 0." ) sdDeclGUI::AddDefine( va( "RTP_PROPERTY_0 %i", RTP_PROPERTY_0 ) ); SD_UI_ENUM_TAG( RTP_PROPERTY_1, "Transition property 1." ) sdDeclGUI::AddDefine( va( "RTP_PROPERTY_1 %i", RTP_PROPERTY_1 ) ); SD_UI_ENUM_TAG( RTP_PROPERTY_2, "Transition property 2." ) sdDeclGUI::AddDefine( va( "RTP_PROPERTY_2 %i", RTP_PROPERTY_2 ) ); SD_UI_ENUM_TAG( RTP_PROPERTY_3, "Transition property 3." ) sdDeclGUI::AddDefine( va( "RTP_PROPERTY_3 %i", RTP_PROPERTY_3 ) ); SD_UI_POP_GROUP_TAG SD_UI_PUSH_GROUP_TAG( "Draw Style Flags" ) SD_UI_ENUM_TAG( DS_ARC, "ARC draw style." ) sdDeclGUI::AddDefine( va( "DS_ARC %i", DS_ARC ) ); SD_UI_ENUM_TAG( DS_VERTICAL, "Vertical draw style." ) sdDeclGUI::AddDefine( va( "DS_VERTICAL %i", DS_VERTICAL ) ); SD_UI_ENUM_TAG( DS_INVALID, "Invalid draw style." ) sdDeclGUI::AddDefine( va( "DS_INVALID %i", DS_INVALID ) ); SD_UI_POP_GROUP_TAG SD_UI_PUSH_GROUP_TAG( "Radial Item Style Flags" ) SD_UI_ENUM_TAG( RIS_LEFT, "Item style left." ) sdDeclGUI::AddDefine( va( "RIS_LEFT %i", RIS_LEFT ) ); SD_UI_ENUM_TAG( RIS_RIGHT, "Item style right." ) sdDeclGUI::AddDefine( va( "RIS_RIGHT %i", RIS_RIGHT ) ); SD_UI_ENUM_TAG( RIS_CENTER, "Item style center." ) sdDeclGUI::AddDefine( va( "RIS_CENTER %i", RIS_CENTER ) ); SD_UI_ENUM_TAG( RIS_TOP, "Item style top." ) sdDeclGUI::AddDefine( va( "RIS_TOP %i", RIS_TOP ) ); SD_UI_ENUM_TAG( RIS_BOTTOM, "Item style bottom." ) sdDeclGUI::AddDefine( va( "RIS_BOTTOM %i", RIS_BOTTOM ) ); SD_UI_POP_GROUP_TAG } SD_UI_POP_CLASS_TAG #pragma optimize( "", on ) #pragma inline_depth() /* ============ sdUIRadialMenu::sdUIRadialMenu ============ */ sdUIRadialMenu::sdUIRadialMenu() : currentPage( -1.0f ), currentItem( -1.0f ), radius( 64.0f ), verticalPadding( 0.0f ), lastActiveEventFrame ( -1 ) { scriptState.GetProperties().RegisterProperty( "currentPage", currentPage ); scriptState.GetProperties().RegisterProperty( "radius", radius ); scriptState.GetProperties().RegisterProperty( "currentItem", currentItem ); scriptState.GetProperties().RegisterProperty( "verticalPadding", verticalPadding ); scriptState.GetProperties().RegisterProperty( "drawStyle", drawStyle ); lastArcMoveInfo.Set( 0.0f, 0.0f ); imaginaryCursorPos.Set( 0.0f, 0.0f ); drawStyle = DS_ARC; UI_ADD_FLOAT_CALLBACK( currentPage, sdUIRadialMenu, OnCurrentPageChanged ); UI_ADD_FLOAT_CALLBACK( currentPage, sdUIRadialMenu, OnDrawStyleChanged ); UI_ADD_FLOAT_VALIDATOR( currentItem, sdUIRadialMenu, OnValidateCurrentItem ); UI_ADD_FLOAT_VALIDATOR( drawStyle, sdUIRadialMenu, OnValidateDrawStyle ); } /* ============ sdUIRadialMenu::~sdUIRadialMenu ============ */ sdUIRadialMenu::~sdUIRadialMenu() { DisconnectGlobalCallbacks(); } /* ============ sdUIRadialMenu::EnumerateEvents ============ */ void sdUIRadialMenu::EnumerateEvents( const char* name, const idList& flags, idList< sdUIEventInfo >& events, const idTokenCache& tokenCache ) { if( !idStr::Icmp( name, "onCommand" )) { int id = -1; for( int i = 0; i < flags.Num(); i++ ) { const idToken& name = tokenCache[ flags[ i ] ]; id = NamedEventHandleForString( name.c_str() ); events.Append( sdUIEventInfo( RME_COMMAND, id ) ); } return; } if( !idStr::Icmp( name, "onMeasureItem" )) { events.Append( sdUIEventInfo( RME_MEASUREITEM, 0 ) ); return; } if( !idStr::Icmp( name, "onDrawItem" )) { events.Append( sdUIEventInfo( RME_DRAWITEM, 0 ) ); return; } if( !idStr::Icmp( name, "onDrawContext" )) { events.Append( sdUIEventInfo( RME_DRAWCONTEXT, 0 ) ); return; } if( !idStr::Icmp( name, "onPagePushed" )) { events.Append( sdUIEventInfo( RME_PAGE_PUSHED, 0 ) ); return; } if( !idStr::Icmp( name, "onPagePopped" )) { events.Append( sdUIEventInfo( RME_PAGE_POPPED, 0 ) ); return; } sdUIWindow::EnumerateEvents( name, flags, events, tokenCache ); } /* ============ sdUIRadialMenu::FindFunction ============ */ const sdUIRadialMenu::uiTemplateFunction_t* sdUIRadialMenu::FindFunction( const char* name ) { sdUITemplateFunction< sdUIRadialMenu >** ptr; return radialMenuFunctions.Get( name, &ptr ) ? *ptr : NULL; } /* ============ sdUIRadialMenu::GetFunction ============ */ sdUIFunctionInstance* sdUIRadialMenu::GetFunction( const char* name ) { const sdUITemplateFunction< sdUIRadialMenu >* function = sdUIRadialMenu::FindFunction( name ); if ( !function ) { return sdUIWindow::GetFunction( name ); } return new sdUITemplateFunctionInstance< sdUIRadialMenu, sdUITemplateFunctionInstance_IdentifierRadialMenu >( this, function ); } /* ============ sdUIRadialMenu::RunNamedMethod ============ */ bool sdUIRadialMenu::RunNamedMethod( const char* name, sdUIFunctionStack& stack ) { const sdUITemplateFunction< sdUIRadialMenu >* func = sdUIRadialMenu::FindFunction( name ); if ( !func ) { return sdUIWindow::RunNamedMethod( name, stack ); } CALL_MEMBER_FN_PTR( this, func->GetFunction() )( stack ); return true; } /* ============ sdUIRadialMenu::DrawLocal ============ */ void sdUIRadialMenu::DrawLocal() { if( !PreDraw() ) { return; } idPlayer* player = gameLocal.GetLocalPlayer(); if ( player != NULL ) { idEntity* entity = gameLocal.localPlayerProperties.GetContextEntity(); if( entity != NULL ) { idVec3 org = entity->GetLastPushedOrigin(); idMat3 axes = entity->GetLastPushedAxis(); idBounds bounds = entity->GetPhysics()->GetBounds(); sdWorldToScreenConverter converter( gameLocal.playerView.GetCurrentView() ); sdBounds2D screenBounds; converter.Transform( bounds, axes, org, screenBounds ); GetUI()->PushScriptVar( screenBounds.ToVec4() ); RunEvent( sdUIEventInfo( RME_DRAWCONTEXT, 0 ) ); GetUI()->ClearScriptStack(); } } if( drawStyle == DS_INVALID ) { return; } int currentPageIndex = idMath::Ftoi( currentPage ); if( currentPageIndex >= 0 && currentPageIndex < pages.Num() ) { radialPage_t& page = pages[ currentPageIndex ]; int currentItem = idMath::Ftoi( this->currentItem ); idVec2 origin; if( drawStyle == DS_ARC ) { origin.Set( cachedClientRect.x + cachedClientRect.z * 0.5f, cachedClientRect.y + cachedClientRect.w * 0.5f ); } else if( drawStyle == DS_VERTICAL ) { origin.Set( cachedClientRect.x + cachedClientRect.z * 0.5f, cachedClientRect.y ); } for ( int i = 0; i < page.items.Num(); i++ ) { page.items[ i ].Update(); } DrawTitle( page, origin ); if( drawStyle == DS_ARC ) { DrawItemCircle( page, origin ); } else if( drawStyle == DS_VERTICAL ) { DrawItemLine( page, origin ); } } } /* ============ sdUIRadialMenu::DrawItemCircle ============ */ void sdUIRadialMenu::DrawItemCircle( radialPage_t& page, const idVec2& center ) { if ( page.items.Num() < 1 ) { return; } // calculate an approximate angle interval to place the items at int numDivisions = page.items.Num(); float angleInterval = 360.0f / ( numDivisions - 2 ); // calculate the radius the circle will need to be float minHeight = page.maxSize.y + verticalPadding; float deltaD = 2.0f * radius * idMath::Sin( DEG2RAD( angleInterval * 0.5f ) ); float deltaW = radius * idMath::Sin( DEG2RAD( angleInterval ) ); float deltaH = idMath::Sqrt( deltaD * deltaD - deltaW * deltaW ); float newRadius = radius; if ( deltaH > idMath::FLT_EPSILON ) { newRadius = ( minHeight / deltaH ) * radius; if ( newRadius < radius + page.maxSize.y * 0.5f ) { newRadius = radius + page.maxSize.y; } } if ( gui_debugRadialMenus.GetBool() ) { deviceContext->DrawCircle( center.x, center.y, idVec2( newRadius, newRadius ), 1.0f, 32, colorGreen ); deviceContext->DrawClippedRect( center.x - 2.0f, center.y - 2.0f, 4.0f, 4.0f, colorYellow ); } // calculate where the first position down can be int numRows = ( page.items.Num() - 1 ) / 2; // draw the odd item out at the bottom, centered to keep the list from looking lopsided bool drawBottomItem = ( ( page.items.Num() - 2 ) % 2 ) != 0 ; if( drawBottomItem ) { numRows--; } float currentY = ( numRows * ( page.maxSize.y ) ) * 0.5f; float xOffset = cachedClientRect.z * 0.5f - page.maxSize.y; // draw the first item right in the center - this is the default DrawItem( page, page.items[ 0 ], 0, center, center, RIS_CENTER ); int itemUpto = 2; if ( itemUpto >= page.items.Num() ) { return; } DrawItem( page, page.items[ 1 ], 1, center, center - idVec2( 0.0f, currentY + page.maxSize.y + verticalPadding ), RIS_CENTER | RIS_TOP ); for ( int i = 0; i < numRows; i++, currentY -= page.maxSize.y + verticalPadding ) { float currentX = -idMath::Sqrt( idMath::Fabs( newRadius * newRadius - currentY * currentY ) ); radialItem_t& item = page.items[ itemUpto ]; idVec2 offset( currentX - xOffset, -currentY ); int flags = 0; if( itemUpto == 1 || itemUpto == 2 ) { flags = RIS_TOP; } else { if( drawBottomItem ) { if( itemUpto == page.items.Num() - 3 || itemUpto == page.items.Num() - 2 ) { flags = RIS_BOTTOM; } } else { if( itemUpto == page.items.Num() - 2 || itemUpto == page.items.Num() - 1 ) { flags = RIS_BOTTOM; } } } DrawItem( page, item, itemUpto, center, center + offset, RIS_LEFT | flags); if ( gui_debugRadialMenus.GetBool() ) { deviceContext->DrawLine( center, center + offset, 1.0f, colorRed ); } itemUpto++; if ( itemUpto < page.items.Num() ) { radialItem_t& item = page.items[ itemUpto ]; offset.x = -offset.x; DrawItem( page, item, itemUpto, center, center + offset, RIS_RIGHT | flags ); if ( gui_debugRadialMenus.GetBool() ) { deviceContext->DrawLine( center, center + offset, 1.0f, colorRed ); } itemUpto++; } } if( drawBottomItem ) { DrawItem( page, page.items[ page.items.Num() - 1 ], page.items.Num() - 1, center, center + idVec2( 0.0f, -currentY + verticalPadding ), RIS_CENTER | RIS_BOTTOM ); } } /* ============ sdUIRadialMenu::DrawItemLine ============ */ void sdUIRadialMenu::DrawItemLine( radialPage_t& page, const idVec2& center ) { float currentX = 0.0f; float currentY = 0.0f; for ( int i = 0; i < page.items.Num(); i++, currentY += page.maxSize.y + verticalPadding ) { radialItem_t& item = page.items[ i ]; idVec2 offset( currentX, currentY ); int flags = RIS_CENTER; if( i == 0 ) { flags |= RIS_TOP; } else if( i == page.items.Num() - 1 ) { flags |= RIS_BOTTOM; } DrawItem( page, item, i, center, center + offset, flags ); if ( gui_debugRadialMenus.GetBool() ) { deviceContext->DrawLine( center, center + offset, 1.0f, colorRed ); } } } /* ============ ToPolar ============ */ static void ToPolar( const idVec2& valueExpr, float& angleOut, float& distanceOut ) { if( idMath::Fabs( valueExpr.x ) < idMath::FLT_EPSILON && idMath::Fabs( valueExpr.y ) < idMath::FLT_EPSILON ) { angleOut = 0.0f; distanceOut = 0.0f; return; } idVec2 conversionValue = valueExpr; if ( idMath::Fabs( conversionValue.x ) > idMath::FLT_EPSILON ) { angleOut = RAD2DEG( idMath::ATan( -conversionValue.y, conversionValue.x ) ); distanceOut = conversionValue.Length(); } else { if ( conversionValue.y > 0.0f ) { distanceOut = conversionValue.y; angleOut = -90.0f; } else { distanceOut = -conversionValue.y; angleOut = 90.0f; } } angleOut = idMath::AngleNormalize360( angleOut + 90.0f ); } /* ============ ToCartesian ============ */ static idVec2 ToCartesian( const float distanceIn, const float angleIn ) { return idVec2( idMath::Cos( DEG2RAD( angleIn - 90.0f ) ) * distanceIn, -idMath::Sin( DEG2RAD( angleIn - 90.0f ) ) * distanceIn ); } /* ============ sdUIRadialMenu::DrawItem ============ */ void sdUIRadialMenu::DrawItem( radialPage_t& page, radialItem_t& item, int index, const idVec2& center, const idVec2& offset, int radialItemStyle ) { sdBounds2D drawRect; idVec2 size( page.maxSize.x, item.size.y ); GetUI()->PushScriptVar( size ); GetUI()->PushScriptVar( static_cast< float >( radialItemStyle ) ); GetUI()->PushScriptVar( static_cast< float >( index ) ); if( RunEvent( sdUIEventInfo( RME_MEASUREITEM, 0 ) ) ) { GetUI()->PopScriptVar( size ); } GetUI()->ClearScriptStack(); drawRect.FromRectangle( 0.0f, 0.0f, size.x, size.y ); drawRect.TranslateSelf( offset ); drawRect.TranslateSelf( 0.5f * ( offset.x - drawRect.GetMins().x ), 0.0f ); if( drawStyle == DS_ARC ) { drawRect.TranslateSelf( 0.0f, -size.y * 0.5f ); } drawRect.TranslateSelf( -0.5f * drawRect.GetWidth(), 0.0f ); idVec2 delta = drawRect.GetCenter() - center; float theta; float distance; ToPolar( delta, theta, distance ); item.lastDrawAngle = theta; if ( item.title != NULL ) { bool useIndex = TestFlag( RMF_USE_NUMBER_SHORTCUTS ); const wchar_t* shortcutKey = L""; if( !item.commandKey.IsEmpty() || useIndex ) { if( useIndex ) { if ( !item.commandNumberKey.IsEmpty() ) { shortcutKey = va( L"%hs", item.commandNumberKey.c_str() ); } else { if( index == 9 ) { shortcutKey = L"0"; } else { shortcutKey = va( L"%i", index + 1 ); } } } else { shortcutKey = va( L"%hs", item.commandKey.c_str() ); } } GetUI()->PushScriptVar( item.drawCallback.c_str() ); GetUI()->PushScriptVar( radialItemStyle ); GetUI()->PushScriptVar( index ); GetUI()->PushScriptVar( drawRect.ToVec4() ); GetUI()->PushScriptVar( item.flags.enabled ? 1.0f : 0.0f ); GetUI()->PushScriptVar( item.flags.drawChevron ? 1.0f : 0.0f ); GetUI()->PushScriptVar( item.title->Index() ); GetUI()->PushScriptVar( shortcutKey ); RunEvent( sdUIEventInfo( RME_DRAWITEM, 0 ) ); GetUI()->ClearScriptStack(); } if ( gui_debugRadialMenus.GetBool() ) { // deviceContext->DrawClippedRect( offset.x - 2.0f, offset.y - 2.0f, 4.0f, 4.0f, idVec4( 1.0f, 0.0f, 1.0f, 0.5f ) ); // deviceContext->DrawClippedRect( drawRect.GetMins().x, drawRect.GetMins().y, drawRect.GetWidth(), drawRect.GetHeight(), idVec4( 1.0f, 0.0f, 0.0f, 0.5f ) ); } } /* ============ sdUIRadialMenu::DrawTitle ============ */ void sdUIRadialMenu::DrawTitle( radialPage_t& page, const idVec2& center ) { if ( gui_debugRadialMenus.GetBool() ) { idVec2 centerPos = GetUI()->screenCenter; idVec2 cursorPos = GetUI()->cursorPos; deviceContext->DrawClippedRect( cursorPos.x - 3, cursorPos.y - 3, 6, 6, colorYellow ); deviceContext->DrawLine( centerPos, cursorPos, 1, colorYellow ); deviceContext->DrawClippedRect( imaginaryCursorPos.x - 3 + centerPos.x, imaginaryCursorPos.y - 3 + centerPos.y, 6, 6, colorGreen ); deviceContext->DrawLine( centerPos, imaginaryCursorPos + centerPos, 1, colorGreen ); } if( drawStyle == DS_ARC ) { if( gui_debugRadialMenus.GetBool() ) { idVec4 yellow( colorYellow ); yellow.w = 0.5f; const idVec2& cursor = GetUI()->cursorPos; deviceContext->DrawLine( center, cursor, 1.0f, yellow ); } } else if( drawStyle == DS_VERTICAL ) { } } /* ============ sdUIRadialMenu::HandleArcMouseMove ============ */ void sdUIRadialMenu::HandleArcMouseMove( const sdSysEvent* event, const idVec2 delta, radialPage_t& page, int& currentItem ) { idVec2 newPosition = delta; float angle, distance; ToPolar( newPosition, angle, distance ); // clamp the distance to the radius if ( idMath::Fabs( distance ) > radius ) { distance = idMath::ClampFloat( -radius, radius, distance ); newPosition = ToCartesian( distance, angle ); GetUI()->cursorPos = newPosition + GetUI()->screenCenter; } // find the delta from the last move idVec2 lastCursorPos = ToCartesian( lastArcMoveInfo.x, lastArcMoveInfo.y ); idVec2 moveDelta = ( newPosition - lastCursorPos ); // if it hasn't moved a few units from the last position that was processed then don't process it float moveDist = moveDelta.Length(); // record the processing lastArcMoveInfo.Set( distance, angle ); // update where the imaginary cursor is imaginaryCursorPos = lastCursorPos + moveDelta; int iCurrentItem = idMath::Ftoi( currentItem ); bool dontSnap = false; bool useDelta = false; if ( event->IsControllerMouseEvent() ) { } else { // check whether the player has made a fast movement (to be handled gesturally) bool isExtremeVerticalItem = ( iCurrentItem != -1 && ( ( idMath::Fabs( page.items[ iCurrentItem ].lastDrawAngle - 180.0f ) < 4.0f ) || idMath::Fabs( page.items[ iCurrentItem ].lastDrawAngle ) < 4.0f ) ); bool isVerticalMove = idMath::Fabs( angle - 180.0f ) < 4.0f || idMath::Fabs( angle ) < 4.0f; if ( moveDist > 5.0f || ( isVerticalMove && moveDist >= 2.0f ) ) { if ( idMath::Fabs( moveDelta.x ) < 2.0f ) { // not as sensitive to movements if they're largely up & down if ( moveDist > 10.0f || isExtremeVerticalItem ) { dontSnap = true; useDelta = true; } } else { dontSnap = true; useDelta = true; } } // gameLocal.Printf( "%.1f %.1f\n", moveDelta.x, moveDelta.y ); if ( useDelta ) { // do a super-fast snappy move // find the closest point of the flick line relative to the center idVec2 dir = moveDelta; dir.Normalize(); idVec2 diff = -lastCursorPos; float t = diff * dir; idVec2 closest = lastCursorPos + t * dir; // find out the angle and length of the vector float alpha, dist; ToPolar( closest - lastCursorPos, alpha, dist ); alpha = idMath::AngleNormalize360( alpha - 90.0f ); float closestDistance = closest.Length(); float distToPoint = idMath::Sqrt( Square( radius ) - Square( closestDistance ) ); idVec2 point = closest + distToPoint * dir; ToPolar( point, angle, distance ); } else if ( distance < radius * 0.25f ) { // move to the dead zone if( idMath::Ftoi( drawStyle ) == DS_ARC ) { currentItem = 0.0f; } return; } } // how many items on each side int numItems = page.items.Num(); if ( numItems == 0 ) { return; } int numOnLeft = numItems / 2; if( ( ( numItems - 2 ) % 2 ) != 0 ) { numOnLeft--; } // figure out the angle divisions for items float angleInterval = LEFT_ARC_RANGE / numOnLeft; float midAngle = angle; float bestAngle = idMath::INFINITY; int newItem = iCurrentItem; int checkedItems = 0; bool found = false; int i = ( iCurrentItem + 1 ) % numItems; while( checkedItems < page.items.Num() ) { float min = page.items[ i ].lastDrawAngle - angleInterval * 0.5f; float max = min + angleInterval; float angleDifference = idMath::AngleNormalize360( idMath::Fabs( angle - page.items[ i ].lastDrawAngle ) ); if ( ( ( angle > min && angle < max ) || ( angle > ( min + 360.0f ) && angle < ( max + 360.0f ) ) ) && angleDifference <= bestAngle ) { if( i != 0 || found == false ) { newItem = i; midAngle = ( min + max ) * 0.5f; distance = radius; bestAngle = angleDifference; found = true; } } i = ( i + 1 ) % numItems; checkedItems++; } // jrad - allow selection of disabled items to improve usability (muscle-memory) // if ( page.items[ newItem ].flags.enabled ) { currentItem = newItem; // } if ( !event->IsControllerMouseEvent() ) { // // OUTPUT // calculate the cursor position idVec2 newCursorPos = ToCartesian( distance, angle ) + GetUI()->screenCenter; if ( !dontSnap ) { imaginaryCursorPos = newCursorPos - GetUI()->screenCenter; } // // Debugging! newCursorPos = ToCartesian( radius, midAngle ) + GetUI()->screenCenter; if ( useDelta ) { GetUI()->cursorPos = newCursorPos; lastArcMoveInfo.Set( radius, midAngle ); imaginaryCursorPos = newCursorPos - GetUI()->screenCenter; } } } /* ============ sdUIRadialMenu::PostEvent ============ */ bool sdUIRadialMenu::PostEvent( const sdSysEvent* event ) { int currentPage = idMath::Ftoi( this->currentPage ); int currentItem = idMath::Ftoi( this->currentItem ); if( currentPage < 0 || currentPage >= pages.Num() ) { return false; } radialPage_t& page = pages[ currentPage ]; bool retVal = false; if ( event->IsMouseEvent() ) { if( ( currentItem < 0 && idMath::Ftoi( drawStyle ) != DS_ARC ) || currentItem > page.items.Num() ) { return false; } idVec2 delta = GetUI()->cursorPos.GetValue() - GetUI()->screenCenter.GetValue(); if( drawStyle == DS_ARC ) { HandleArcMouseMove( event, delta, page, currentItem ); this->currentItem = idMath::ClampInt( -1, page.items.Num() - 1, currentItem ); } else if( drawStyle == DS_VERTICAL ) { float moveThreshhold = Max< float >( radius, 32.0f ); if( idMath::Fabs( delta.y ) > moveThreshhold ) { GetUI()->cursorPos = GetUI()->screenCenter; if( delta.y < 0.0f && currentItem > 0 ) { currentItem--; } else if( delta.y > 0.0f && currentItem < page.items.Num() ) { currentItem++; } } this->currentItem = idMath::ClampInt( 0, page.items.Num() - 1, currentItem ); } } else if ( event->IsMouseButtonEvent() ) { if ( event->IsButtonDown() ) { // handle mouse wheel if ( drawStyle == DS_VERTICAL ) { mouseButton_t mb = event->GetMouseButton(); if ( mb == M_MWHEELUP ) { currentItem--; retVal = true; } else if ( mb == M_MWHEELDOWN ) { currentItem++; retVal = true; } this->currentItem = idMath::ClampInt( 0, page.items.Num() - 1, currentItem ); } } } else if ( event->IsKeyEvent() ) { if ( event->IsKeyDown() && lastActiveEventFrame != gameLocal.framenum ) { bool useIndex = TestFlag( RMF_USE_NUMBER_SHORTCUTS ); keyNum_t keyNum = K_INVALID; for ( int i = 0; i < page.items.Num(); i++ ) { const radialItem_t& item = page.items[ i ]; if ( !item.flags.enabled ) { continue; } if ( useIndex ) { if ( !item.commandNumberKey.IsEmpty() ) { keyNum = sys->Keyboard().ConvertCharToKey( item.commandNumberKey.c_str()[0] ); // FIXME: change storage to just a char } else { if ( i == 9 ) { keyNum = K_0; } else { keyNum = (keyNum_t)( K_1 + i ); } } } else { keyNum = sys->Keyboard().ConvertCharToKey( item.commandKey.c_str()[0] ); // FIXME: change storage to just a char } if ( keyNum == K_INVALID ) { continue; } if ( event->GetKey() == keyNum ) { this->currentItem = i; for ( int cmdIndex = 0; cmdIndex < item.commandID.Num(); cmdIndex++ ) { const char* id = item.commandID[ cmdIndex ]; int eventID = namedEvents.FindIndex( id ); if ( eventID == -1 || !RunEvent( sdUIEventInfo( RME_COMMAND, eventID ) ) ) { gameLocal.Warning( "sdUIRadialMenu::PostEvent: no command handler provided for '%s' (item '%s')", id, item.title == NULL ? "NULL" : item.title->GetName() ); } else { lastActiveEventFrame = gameLocal.framenum; } } retVal = true; break; } } } } if ( !retVal ) { if ( event->IsMouseButtonEvent() || event->IsKeyEvent() ) { windowEvent_t eventID = event->IsKeyDown() ? WE_KEYDOWN : WE_KEYUP; retVal |= HandleBoundKeyInput( event ); if ( !retVal && lastActiveEventFrame != gameLocal.framenum ) { bool down; idKey* key = keyInputManager->GetKeyForEvent( *event, down ); int keyId = key == NULL ? -1 : key->GetId(); retVal |= RunEvent( sdUIEventInfo( eventID, keyId ) ); if ( retVal == true && event->GetMouseButton() == M_MOUSE1 ) { lastActiveEventFrame = gameLocal.framenum; } } } } return retVal; } /* ============ sdUIRadialMenu::Script_InsertItem ============ */ void sdUIRadialMenu::Script_InsertItem( sdUIFunctionStack& stack ) { int pageNum; stack.Pop( pageNum ); if( pageNum < 0 ) { pageNum = idMath::Ftoi( currentPage ); } if( pageNum < 0 || pageNum > pages.Num() ) { stack.Push( -1.0f ); return; } radialPage_t& page = pages[ pageNum ]; radialItem_t& item = page.items.Alloc(); idStr title; stack.Pop( title ); idStrList list; idSplitStringIntoList( list, title.c_str(), "||" ); if( list.Num() >= 1 ) { item.title = declHolder.FindLocStr( list[ 0 ].c_str() ); if( item.title->GetState() == DS_DEFAULTED ) { gameLocal.Warning( "Invalid title %s", item.title->GetName() ); } } if( list.Num() >= 2 ) { idSplitStringIntoList( item.commandID, list[ 1 ].c_str(), "," ); } if( list.Num() >= 3 ) { idSplitStringIntoList( item.commandData, list[ 2 ].c_str(), "," ); } if( list.Num() >= 4 ) { item.commandKey = list[ 3 ]; } item.commandNumberKey = ""; stack.Push( page.items.Num() - 1 ); MakeLayoutDirty(); } /* ============ sdUIRadialMenu::Script_InsertPage ============ */ void sdUIRadialMenu::Script_InsertPage( sdUIFunctionStack& stack ) { radialPage_t& page = pages.Alloc(); idStr title; stack.Pop( title ); page.title = declHolder.FindLocStr( title ); if( page.title->GetState() == DS_DEFAULTED ) { gameLocal.Warning( "Invalid title %s", page.title->GetName() ); } currentPage = pages.Num() - 1; stack.Push( currentPage ); } /* ============ sdUIRadialMenu::Script_PushPage ============ */ void sdUIRadialMenu::Script_PushPage( sdUIFunctionStack& stack ) { int index; stack.Pop( index ); pageStack.Push( index ); currentPage = index; GetUI()->cursorPos = GetUI()->screenCenter; lastArcMoveInfo.x = 0.0f; lastArcMoveInfo.y = 0.0f; imaginaryCursorPos = GetUI()->screenCenter; RunEvent( sdUIEventInfo( RME_PAGE_PUSHED, 0 ) ); } /* ============ sdUIRadialMenu::Script_PopPage ============ */ void sdUIRadialMenu::Script_PopPage( sdUIFunctionStack& stack ) { int index = -1; if( !pageStack.Empty() ) { pageStack.Pop(); } if( !pageStack.Empty() ) { index = pageStack.Top(); } currentPage = index; stack.Push( index ); RunEvent( sdUIEventInfo( RME_PAGE_POPPED, 0 ) ); } /* ============ sdUIRadialMenu::OnCurrentPageChanged ============ */ void sdUIRadialMenu::OnCurrentPageChanged( const float oldValue, const float newValue ) { radialPage_t* oldPage = GetSafePage( idMath::Ftoi( oldValue ) ); radialPage_t* newPage = GetSafePage( idMath::Ftoi( newValue ) ); if( oldPage != NULL ) { oldPage->currentItem = idMath::Ftoi( currentItem ); } if( newPage != NULL ) { if( idMath::Ftoi( drawStyle ) != DS_ARC ) { MoveToFirstEnabledItem( newPage->currentItem ); } else { currentItem = -1.0f; } newPage->maxSize.Zero(); MakeLayoutDirty(); } else { currentItem = 0.0f; } } /* ============ sdUIRadialMenu::Script_GetItemData ============ */ void sdUIRadialMenu::Script_GetItemData( sdUIFunctionStack& stack ) { int pageNum; int itemNum; int dataIndex; stack.Pop( pageNum ); stack.Pop( itemNum ); stack.Pop( dataIndex ); if( dataIndex < 0 ) { dataIndex = 0; } if( pageNum < 0 ) { pageNum = idMath::Ftoi( currentPage ); } if( itemNum < 0 ) { itemNum = idMath::Ftoi( currentItem ); } radialItem_t* item = GetSafeItem( pageNum, itemNum ); if( !item || dataIndex >= item->commandData.Num() ) { stack.Push( "" ); return; } stack.Push( item->commandData[ dataIndex ] ); } /* ============ sdUIRadialMenu::Script_ClearPage ============ */ void sdUIRadialMenu::Script_ClearPage( sdUIFunctionStack& stack ) { int pageNum; stack.Pop( pageNum ); if( pageNum < 0 ) { pageNum = idMath::Ftoi( currentPage ); } if( radialPage_t* page = GetSafePage( pageNum )) { page->items.Clear(); if( pageNum == currentPage ) { page->currentItem = -1; } } MakeLayoutDirty(); } /* ============ sdUIRadialMenu::Script_ClearPageStack ============ */ void sdUIRadialMenu::Script_ClearPageStack( sdUIFunctionStack& stack ) { pageStack.Clear(); } /* ============ sdUIRadialMenu::Script_PostCommand ============ */ void sdUIRadialMenu::Script_PostCommand( sdUIFunctionStack& stack ) { int pageNum; stack.Pop( pageNum ); if( pageNum < 0.0f ) { pageNum = idMath::Ftoi( currentPage ); } int itemNum; stack.Pop( itemNum ); if( itemNum < 0.0f ) { itemNum = idMath::Ftoi( currentItem ); } if( radialItem_t* item = GetSafeItem( pageNum, itemNum )) { if ( item->flags.enabled ) { for( int cmdIndex = 0; cmdIndex < item->commandID.Num(); cmdIndex++ ) { const char* id = item->commandID[ cmdIndex ]; int eventID = namedEvents.FindIndex( id ); if( eventID == -1 || !RunEvent( sdUIEventInfo( RME_COMMAND, eventID ) ) ) { gameLocal.Warning( "sdUIRadialMenu::PostEvent: no command handler provided for '%s' (item '%s')", id, item->title == NULL ? "NULL" : item->title->GetName() ); } } } } } /* ============ sdUIRadialMenu::Script_LoadFromDef ============ */ void sdUIRadialMenu::Script_LoadFromDef( sdUIFunctionStack& stack ) { idStr defName; stack.Pop( defName ); Clear( this ); if( defName.IsEmpty() ) { gameLocal.Warning( "sdUIRadialMenu::Script_LoadFromDef: gui '%s': window '%s', tried to load an empty radialMenuDef", GetUI()->GetName(), name.GetValue().c_str() ); return; } const sdDeclRadialMenu* def = gameLocal.declRadialMenuType.LocalFind( defName, true ); declManager->AddDependency( GetUI()->GetDecl(), def ); for( int i = 0; i < def->GetNumPages(); i++ ) { declManager->AddDependency( GetUI()->GetDecl(), &def->GetPage( i ) ); radialPage_t& page = pages.Alloc(); page.title = def->GetPage( i ).GetTitle(); if( page.title->GetState() == DS_DEFAULTED ) { gameLocal.Warning( "sdUIRadialMenu::Script_LoadFromDef: gui '%s': window '%s', defaulted title for '%s'", GetUI()->GetName(), name.GetValue().c_str(), page.title->GetName() ); } page.popFactor = def->GetPage( i ).GetKeys().GetFloat( "popFactor", "1.5" ); LoadFromDef( def->GetPage( i ), page ); } if( pages.Num() ) { currentPage = 0.0f; if( pages.Front().items.Num() ) { currentItem = 0.0f; } } MakeLayoutDirty(); } /* ============ LoadPrefixFromDict ============ */ void LoadPrefixFromDict( const char* prefix, const idDict& dict, idStrList& list ) { const idKeyValue* kv = dict.FindKey( prefix ); if( kv ) { list.Append( kv->GetValue() ); } int index = 1; kv = dict.FindKey( va( "%s%i", prefix, index ) ); while( kv ) { list.Append( kv->GetValue() ); index++; kv = dict.FindKey( va( "%s%i", prefix, index ) ); } } /* ============ sdUIRadialMenu::LoadFromDef ============ */ void sdUIRadialMenu::LoadFromDef( const sdDeclRadialMenu& def, radialPage_t& page ) { const char* tempStr = NULL; for( int i = 0; i < def.GetNumItems(); i++ ) { if( i >= MAX_ITEMS_PER_PAGE ) { const char* pageTitle = va( "%ls", page.title->GetText() ); gameLocal.Warning( "More than %i items on page '%s'", MAX_ITEMS_PER_PAGE, pageTitle ); return; } radialItem_t& item = page.items.Alloc(); item.owner = this; const idDict& keys = def.GetItemKeys( i ); LoadPrefixFromDict( "data", keys, item.commandData ); LoadPrefixFromDict( "command", keys, item.commandID ); item.commandKey = keys.GetString( "key" ); item.commandNumberKey = keys.GetString( "numberKey" ); item.title = def.GetItemTitle( i ); item.flags.drawChevron = keys.GetBool( "drawChevron", "0" ); item.flags.enabled = keys.GetBool( "enabled", "1" ); item.drawCallback = keys.GetString( "draw", "" ); if( item.title->GetState() == DS_DEFAULTED ) { gameLocal.Warning( "sdUIRadialMenu::Script_LoadFromDef: gui '%s': window '%s', defaulted title for '%s' on page '%s'", GetUI()->GetName(), name.GetValue().c_str(), item.title->GetName(), page.title->GetName() ); } item.scriptUpdateCallback = keys.GetString( "scriptUpdate" ); item.scriptUpdateParm = keys.GetString( "scriptUpdateParm" ); tempStr = keys.GetString( "mtr_icon" ); if( tempStr[ 0 ] != '\0' ) { GetUI()->LookupMaterial( tempStr, item.part.mi, &item.part.width, &item.part.height ); } } } /* ============ sdUIRadialMenu::Script_AppendFromDef ============ */ void sdUIRadialMenu::Script_AppendFromDef( sdUIFunctionStack& stack ) { idStr defName; stack.Pop( defName ); int pageNum; stack.Pop( pageNum ); if( defName.IsEmpty() ) { gameLocal.Warning( "sdUIRadialMenu::Script_LoadFromDef: gui '%s': window '%s', tried to load an empty radialMenuDef", GetUI()->GetName(), name.GetValue().c_str() ); return; } const sdDeclRadialMenu* def = gameLocal.declRadialMenuType.LocalFind( defName, true ); declManager->AddDependency( GetUI()->GetDecl(), def ); if( pageNum < 0.0f ) { pageNum = idMath::Ftoi( currentPage ); } if( radialPage_t* page = GetSafePage( pageNum ) ) { LoadFromDef( *def, *page ); } MakeLayoutDirty(); } /* ============ sdUIRadialMenu::ClearPage ============ */ void sdUIRadialMenu::Clear( sdUIRadialMenu* menu ) { sdUIFunctionStack stack; menu->RunNamedMethod( "clear", stack ); } /* ============ sdUIRadialMenu::InsertPage ============ */ int sdUIRadialMenu::InsertPage( sdUIRadialMenu* menu, int page, const char* text ) { sdUIFunctionStack stack; stack.Push( page ); stack.Push( text ); if( !menu->RunNamedMethod( "insertPage", stack ) ) { return -1; } int retVal; stack.Pop( retVal ); return retVal; } /* ============ sdUIRadialMenu::InsertItem ============ */ int sdUIRadialMenu::InsertItem( sdUIRadialMenu* menu, int page, const char* text ) { sdUIFunctionStack stack; stack.Push( page ); stack.Push( text ); if( !menu->RunNamedMethod( "insertItem", stack ) ) { return -1; } int retVal; stack.Pop( retVal ); return retVal; } /* ============ sdUIRadialMenu::Script_FillFromEnumerator ============ */ void sdUIRadialMenu::Script_FillFromEnumerator( sdUIFunctionStack& stack ) { idStr name; stack.Pop( name ); uiRadialMenuEnumerationCallback_t enumerator = uiManager->GetRadialMenuEnumerationCallback( name ); if ( enumerator != NULL ) { enumerator( this ); } else { gameLocal.Warning( "sdUIRadialMenu::Script_FillFromEnumerator: '%s' Unknown enumerator '%s'", this->name.GetValue().c_str(), name.c_str() ); } } /* ============ sdUIRadialMenu::Script_Clear ============ */ void sdUIRadialMenu::Script_Clear( sdUIFunctionStack& stack ) { pages.Clear(); pageStack.Clear(); currentItem = -1.0f; currentPage = -1.0f; MakeLayoutDirty(); lastActiveEventFrame = -1; } /* ============ sdUIRadialMenu::OnValidateCurrentItem ============ */ bool sdUIRadialMenu::OnValidateCurrentItem( const float newValue ) { radialPage_t* page = GetSafePage( currentPage ); if( page == NULL ) { return true; // allow resetting the current selection when we're empty } int index = idMath::Ftoi( newValue ); // arc style has a dead zone in the center that can be accessed if( ( newValue < 0 && idMath::Ftoi( drawStyle ) != DS_ARC ) || newValue >= page->items.Num() ) { return false; } // jrad - allow selection of disabled items to improve usability (muscle-memory) /* if( page->items[ index ].flags.enabled == false ) { return false; } */ return true; } /* ============ sdUIRadialMenu::Script_TransitionItemVec4 ============ */ void sdUIRadialMenu::Script_TransitionItemVec4( sdUIFunctionStack& stack ) { float property; float page; float item; idVec4 from; idVec4 to; float time; idStr accel; stack.Pop( property ); stack.Pop( from ); stack.Pop( to ); stack.Pop( time ); stack.Pop( accel ); stack.Pop( item ); stack.Pop( page ); if( time < 0.0f ) { gameLocal.Error( "TransitionItemVec4: '%s' duration '%i' out of bounds", name.GetValue().c_str(), idMath::Ftoi( time ) ); return; } // handle all pages if( page < 0.0f ) { for( int i = 0; i < pages.Num(); i++ ) { InitVec4Transition( idMath::Ftoi( property ), from, to, idMath::Ftoi( time ), accel, item, i ); } } else { InitVec4Transition( idMath::Ftoi( property ), from, to, idMath::Ftoi( time ), accel, item, page ); } } /* ============ sdUIRadialMenu::InitVec4Transition ============ */ void sdUIRadialMenu::InitVec4Transition( const int property, const idVec4& from, const idVec4& to, const int time, const idStr& accel, int item, int page ) { radialItem_t* radialItem = GetSafeItem( page, item ); if( radialItem == NULL ) { return; } vec4Evaluator_t* evaluator = radialItem->transition.GetEvaluator( property, true ); assert( evaluator != NULL ); evaluator->InitLerp( GetUI()->GetCurrentTime(), GetUI()->GetCurrentTime() + idMath::Ftoi( time ), from, to ); if( !accel.IsEmpty() ) { idLexer src( accel.c_str(), accel.Length(), "TransitionItemVec4", LEXFL_NOERRORS ); idToken token; if( !src.ReadToken( &token ) ) { gameLocal.Error( "TransitionItemVec4: '%s' invalid acceleration", name.GetValue().c_str() ); return; } bool isTable = idStr::Icmpn( token, "table://", 8 ) == 0; if( !isTable ) { idVec2 accelTimes; accelTimes.x = src.ParseFloat(); src.ExpectTokenString( "," ); accelTimes.y = src.ParseFloat(); evaluator->InitAccelDecelEvaluation( accelTimes.x, accelTimes.y ); } else if( token.Length() > 8 ) { evaluator->InitTableEvaluation( token.c_str() + 8 ); } } } /* ============ sdUIRadialMenu::Script_GetItemTransitionVec4Result ============ */ void sdUIRadialMenu::Script_GetItemTransitionVec4Result( sdUIFunctionStack& stack ) { float property; idVec4 defaultValue; float page; float item; stack.Pop( property ); stack.Pop( defaultValue ); stack.Pop( item ); stack.Pop( page ); if( property < RTP_FORECOLOR || property >= RTP_PROPERTY_MAX ) { gameLocal.Error( "GetItemTransitionVec4Result: '%s' property index '%i' out of bounds", name.GetValue().c_str(), idMath::Ftoi( property ) ); stack.Push( defaultValue ); return; } radialItem_t* radialItem = GetSafeItem( page, item ); if( radialItem == NULL ) { stack.Push( defaultValue ); return; } vec4Evaluator_t* evaluator = radialItem->transition.GetEvaluator( idMath::Ftoi( property ), false ); if( evaluator == NULL || !evaluator->IsInitialized() ) { stack.Push( defaultValue ); return; } idVec4 result = evaluator->Evaluate( GetUI()->GetCurrentTime() ); stack.Push( result ); } /* ============ sdUIRadialMenu::Script_ClearTransitions ============ */ void sdUIRadialMenu::Script_ClearTransitions( sdUIFunctionStack& stack ) { float item; float page; stack.Pop( item ); stack.Pop( page ); if( page >= pages.Num() ) { gameLocal.Warning( "ClearTransitions: '%s' page '%i' out of bounds", name.GetValue().c_str(), idMath::Ftoi( page ) ); return; } int iItem = idMath::Ftoi( item ); if( page < 0.0f ) { // clear all columns for( int i = 0; i < pages.Num(); i++ ) { radialPage_t& page = pages[ i ]; if( iItem < 0 ) { for( int trans = 0; trans < page.items.Num(); trans++ ) { ClearTransition( page.items[ trans ].transition ); } } else { ClearTransition( page.items[ iItem ].transition ); } } } else { radialPage_t& radialPage = pages[ page ]; if( iItem < 0 ) { for( int trans = 0; trans < radialPage.items.Num(); trans++ ) { ClearTransition( radialPage.items[ trans ].transition ); } } else if( iItem < radialPage.items.Num() ) { ClearTransition( radialPage.items[ iItem ].transition ); } } } /* ============ sdUIRadialMenu::ClearTransition ============ */ void sdUIRadialMenu::ClearTransition( sdTransition& transition ) { static const idVec4 colorInvisible( 0.0f, 0.0f, 0.0f, 0.0f ); transition.backColor.InitConstant( colorInvisible ); transition.foreColor.InitConstant( colorWhite ); transition.FreeEvaluators(); } /* ============ sdUIRadialMenu::GetEvaluator ============ */ sdUIRadialMenu::vec4Evaluator_t* sdUIRadialMenu::sdTransition::GetEvaluator( int index, bool allowCreate ) { switch( index ) { case RTP_FORECOLOR: return &foreColor; case RTP_BACKCOLOR: return &backColor; default: if( evaluators == NULL ) { if( !allowCreate ) { return NULL; } evaluators = vec4EvaluatorListAllocator.Alloc(); evaluators->SetNum( MAX_VEC4_EVALUATORS ); } return &(*evaluators)[ index ]; } } /* ============ sdUIRadialMenu::MoveToFirstEnabledItem ============ */ void sdUIRadialMenu::MoveToFirstEnabledItem( int index ) { if( currentPage < 0 || currentPage >= pages.Num() ) { currentItem = -1.0f; return; } currentItem = index; /* jrad - allow selection of disabled items to improve usability (muscle-memory) int item = idMath::Ftoi( currentItem ); radialPage_t& page = pages[ idMath::Ftoi( currentPage ) ]; if( page.items.Num() == 0 ) { return; } if( item < 0 || item >= page.items.Num() ) { item = 0; } if( item >= page.items.Num() ) { return; } // see if the current item is available page.items[ item ].Update(); if( page.items[ item ].flags.enabled ) { currentItem = item; return; } // find the next item that's available for ( int i = 0; i < page.items.Num(); i++ ) { page.items[ i ].Update(); if( page.items[ i ].flags.enabled ) { currentItem = i; break; } } currentItem = -1.0f; */ } /* ============ sdUIRadialMenu::OnValidateDrawStyle ============ */ bool sdUIRadialMenu::OnValidateDrawStyle( const float newValue ) { int iValue = idMath::Ftoi( newValue ); return ( iValue >= DS_INVALID && iValue < DS_MAX ); } /* ============ sdUIRadialMenu::ApplyLayout ============ */ void sdUIRadialMenu::ApplyLayout() { if( windowState.recalculateLayout ) { int textWidth; int textHeight; radialPage_t* newPage = GetSafePage( idMath::Ftoi( currentPage ) ); if( newPage == NULL ) { assert( 0 ); return; } ActivateFont( true ); sdBounds2D screenBounds( 0.0f, 0.0f, SCREEN_WIDTH, SCREEN_HEIGHT ); for( int i = 0; i < newPage->items.Num(); i++ ) { radialItem_t& item = newPage->items[ i ]; deviceContext->GetTextDimensions( item.title->GetText(), screenBounds, GetDrawTextFlags(), cachedFontHandle, fontSize, textWidth, textHeight ); item.size.Set( textWidth, textHeight ); newPage->maxSize.x = Max( idMath::Ftoi( newPage->maxSize.x ), textWidth ); newPage->maxSize.y = Max( idMath::Ftoi( newPage->maxSize.y ), textHeight ); } } sdUIWindow::ApplyLayout(); } /* ============ sdUIRadialMenu::OnDrawStyleChanged ============ */ void sdUIRadialMenu::OnDrawStyleChanged( const float oldValue, const float newValue ) { MakeLayoutDirty(); }