etqw-sdk/source/game/guis/UIRadialMenu.cpp

1781 lines
54 KiB
C++

// 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<unsigned short>& 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();
}