diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 85aba4228be..2742d666403 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -34,6 +34,7 @@ #include "WW3D2/texturefilter.h" #include "Common/UserPreferences.h" +#include "GameClient/Color.h" typedef UnsignedInt CursorCaptureMode; typedef UnsignedInt ScreenEdgeScrollMode; @@ -76,6 +77,10 @@ class OptionPreferences : public UserPreferences Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); + Bool getKeyboardOverlayEnabled() const; + Color getKeyboardOverlayColor() const; + Bool getKeyboardOverlayBackdropEnabled() const; + Color getKeyboardOverlayBackdropColor() const; Bool getCursorCaptureEnabledInWindowedGame() const; Bool getCursorCaptureEnabledInWindowedMenu() const; Bool getCursorCaptureEnabledInFullscreenGame() const; @@ -131,4 +136,8 @@ class OptionPreferences : public UserPreferences Bool getShowMoneyPerMinute() const; Real getGameWindowTransitionSpeedMultiplier() const; + +private: + // TheSuperHackers @feature Read one 0-255 colour channel, clamped, with a fallback. + UnsignedByte getColorChannel(const char *keyName, UnsignedByte defaultValue) const; }; diff --git a/Core/GameEngine/Include/GameClient/HotKey.h b/Core/GameEngine/Include/GameClient/HotKey.h index 70206f24cf6..cbd75d5a948 100644 --- a/Core/GameEngine/Include/GameClient/HotKey.h +++ b/Core/GameEngine/Include/GameClient/HotKey.h @@ -98,6 +98,11 @@ class HotKeyManager : public SubsystemInterface AsciiString searchHotKey( const AsciiString& label); AsciiString searchHotKey( const UnicodeString& uStr ); + // TheSuperHackers @feature Which hotkey actually got registered to this window, if any. + // Deliberately not derived from the window's label -- addHotKey drops keys that collide + // with an earlier button, so only this reports the key that will really work. + AsciiString getHotKeyForWindow( const GameWindow *win ) const; + private: typedef std::map HotKeyMap; HotKeyMap m_hotKeyMap; diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index e681ef8b192..7def0daac2e 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -216,6 +216,71 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const return FALSE; } +// TheSuperHackers @feature Draw each command bar cameo's keyboard hotkey over the cameo. +// Options.ini: KeyboardOverlay = Yes +Bool OptionPreferences::getKeyboardOverlayEnabled() const +{ + OptionPreferences::const_iterator it = find("KeyboardOverlay"); + if (it == end()) + return FALSE; + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + +UnsignedByte OptionPreferences::getColorChannel(const char *keyName, UnsignedByte defaultValue) const +{ + OptionPreferences::const_iterator it = find(AsciiString(keyName)); + if (it == end()) + return defaultValue; + + Int value = atoi(it->second.str()); + if (value < 0) + value = 0; + else if (value > 255) + value = 255; + + return (UnsignedByte)value; +} + +// TheSuperHackers @feature Colour of the command bar hotkey overlay letters. +// Options.ini: KeyboardOverlayRed / KeyboardOverlayGreen / KeyboardOverlayBlue, +// each 0-255, defaulting to white. A missing or out of range channel falls +// back to full brightness rather than silently drawing an invisible letter. +Color OptionPreferences::getKeyboardOverlayColor() const +{ + return GameMakeColor( + getColorChannel("KeyboardOverlayRed", 255), + getColorChannel("KeyboardOverlayGreen", 255), + getColorChannel("KeyboardOverlayBlue", 255), + 255); +} + +// TheSuperHackers @feature Translucent plate drawn behind the hotkey letter. +Bool OptionPreferences::getKeyboardOverlayBackdropEnabled() const +{ + OptionPreferences::const_iterator it = find("KeyboardOverlayBackdrop"); + if (it == end()) + return TRUE; // on by default -- it is what makes the letter readable + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + +Color OptionPreferences::getKeyboardOverlayBackdropColor() const +{ + // black at 50% opacity, matching the darkened plate look + return GameMakeColor( + getColorChannel("KeyboardOverlayBackdropRed", 0), + getColorChannel("KeyboardOverlayBackdropGreen", 0), + getColorChannel("KeyboardOverlayBackdropBlue", 0), + getColorChannel("KeyboardOverlayBackdropOpacity", 128)); +} + Bool OptionPreferences::getRetaliationModeEnabled() { OptionPreferences::const_iterator it = find("Retaliation"); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp index 0b7ecf22d1e..91e1f133199 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp @@ -157,6 +157,24 @@ void HotKeyManager::addHotKey( GameWindow *win, const AsciiString& keyIn) m_hotKeyMap[key] = newHK; } +//----------------------------------------------------------------------------- +// TheSuperHackers @feature Reverse lookup for the hotkey overlay on command bar cameos. +//----------------------------------------------------------------------------- +AsciiString HotKeyManager::getHotKeyForWindow( const GameWindow *win ) const +{ + if( win == nullptr ) + return AsciiString::TheEmptyString; + + // the map only ever holds the currently displayed commands, so this stays tiny + for( HotKeyMap::const_iterator it = m_hotKeyMap.begin(); it != m_hotKeyMap.end(); ++it ) + { + if( it->second.m_win == win ) + return it->second.m_key; + } + + return AsciiString::TheEmptyString; +} + //----------------------------------------------------------------------------- Bool HotKeyManager::executeHotKey( const AsciiString& keyIn ) { diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h index 5979a8a5f4f..218893bee48 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h @@ -65,6 +65,9 @@ /////////////////////////////////////////////////////////////////////////////// extern void W3DGadgetPushButtonDraw( GameWindow *window, WinInstanceData *instData ); +// TheSuperHackers @feature Free the hotkey overlay's display strings; must run before +// TheDisplayStringManager is destroyed. +extern void W3DGadgetPushButtonFreeHotKeyStrings( void ); extern void W3DGadgetPushButtonImageDraw( GameWindow *window, WinInstanceData *instData ); extern void W3DGadgetCheckBoxDraw( GameWindow *window, WinInstanceData *instData ); extern void W3DGadgetCheckBoxImageDraw( GameWindow *window, WinInstanceData *instData ); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp index 2b89eb1e564..5c03845ba0a 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp @@ -53,6 +53,12 @@ #include "GameClient/GameWindowManager.h" #include "GameClient/GadgetPushButton.h" #include "GameClient/Display.h" +// TheSuperHackers @feature for the command bar hotkey overlay +#include "Common/GlobalData.h" +#include "GameClient/DisplayStringManager.h" +#include "GameClient/GameFont.h" +#include "GameClient/GlobalLanguage.h" +#include "GameClient/HotKey.h" #include "W3DDevice/GameClient/W3DGameWindow.h" #include "W3DDevice/GameClient/W3DDisplay.h" #include "W3DDevice/GameClient/W3DGadget.h" @@ -78,6 +84,106 @@ void W3DGadgetPushButtonImageDrawOne(GameWindow *window, WinInstanceData *instDa // drawButtonText ============================================================= /** Draw button text to the screen */ //============================================================================= +// TheSuperHackers @feature Command bar hotkey overlay (Options.ini: KeyboardOverlay). +// One display string per letter, so that drawing many cameos in a row does not +// rebuild sentence geometry over and over. There are only ever a handful of +// distinct hotkeys on screen, so this stays small. Returned to the manager by +// W3DGadgetPushButtonFreeHotKeyStrings before the manager is torn down. +static DisplayString *s_hotKeyStrings[ 256 ] = { nullptr }; + +//============================================================================= +/** Return the overlay's display strings to the manager. Must run before + * TheDisplayStringManager is destroyed, which asserts on any string still + * registered -- the W3DDisplayStringManager destructor calls this. */ +//============================================================================= +void W3DGadgetPushButtonFreeHotKeyStrings( void ) +{ + for( Int i = 0; i < 256; ++i ) + { + if( s_hotKeyStrings[ i ] != nullptr && TheDisplayStringManager != nullptr ) + TheDisplayStringManager->freeDisplayString( s_hotKeyStrings[ i ] ); + + s_hotKeyStrings[ i ] = nullptr; + } +} + +// drawButtonHotKeyOverlay ==================================================== +/** Draw the keyboard hotkey letter over a command bar cameo, so the player can + * learn the shortcuts without hunting through tooltips. + * + * The letter comes from what actually got registered in the hotkey manager, not + * from the button's label -- colliding hotkeys are dropped at registration, and + * drawing those would advertise a key that does nothing. */ +//============================================================================= +static void drawButtonHotKeyOverlay( GameWindow *window ) +{ + if( !TheGlobalData || !TheGlobalData->m_keyboardOverlayEnabled ) + return; + + if( TheHotKeyManager == nullptr || TheDisplayStringManager == nullptr ) + return; + + // only cameo style buttons opt into overlay states, so this leaves menu buttons alone + if( !BitIsSet( window->winGetStatus(), WIN_STATUS_USE_OVERLAY_STATES ) ) + return; + + AsciiString hotKey = TheHotKeyManager->getHotKeyForWindow( window ); + if( hotKey.isEmpty() ) + return; + + // Only a single byte printable key can be shown faithfully. A localized mnemonic outside + // ASCII arrives as a multi byte sequence here, and drawing its first byte would show a + // wrong or garbled shortcut - better to draw nothing for those. + if( hotKey.getLength() != 1 || !isprint( (unsigned char)hotKey.getCharAt( 0 ) ) ) + return; + + const UnsignedByte index = (UnsignedByte)hotKey.getCharAt( 0 ); + DisplayString *hotKeyString = s_hotKeyStrings[ index ]; + + if( hotKeyString == nullptr ) + { + hotKeyString = TheDisplayStringManager->newDisplayString(); + if( hotKeyString == nullptr ) + return; + + Int pointSize = 10; + if( TheGlobalLanguageData ) + pointSize = TheGlobalLanguageData->adjustFontSize( pointSize ); + hotKeyString->setFont( TheFontLibrary->getFont( AsciiString( "Arial" ), pointSize, TRUE ) ); + + // the manager stores keys lowercased, but shortcuts read better as capitals + UnicodeString text; + WideChar upper = (WideChar)toupper( (Int)index ); + text.concat( upper ); + hotKeyString->setText( text ); + + s_hotKeyStrings[ index ] = hotKeyString; + } + + ICoord2D origin; + window->winGetScreenPosition( &origin.x, &origin.y ); + + // tuck it into the top left of the cameo, where no existing decoration lives + const Int inset = 2; + const Int textX = origin.x + inset; + const Int textY = origin.y + inset; + + // Optional plate behind the letter, so it stays readable over busy cameo art. + if( TheGlobalData->m_keyboardOverlayBackdrop ) + { + Int width, height; + hotKeyString->getSize( &width, &height ); + + const Int pad = 1; + TheDisplay->drawFillRect( textX - pad, textY - pad, + width + pad * 2, height + pad * 2, + TheGlobalData->m_keyboardOverlayBackdropColor ); + } + + hotKeyString->draw( textX, textY, + TheGlobalData->m_keyboardOverlayColor, GameMakeColor( 0, 0, 0, 255 ) ); +} + static void drawButtonText( GameWindow *window, WinInstanceData *instData ) { ICoord2D origin, size, textPos; @@ -481,6 +587,10 @@ void W3DGadgetPushButtonImageDrawOne( GameWindow *window, } } } + + // TheSuperHackers @feature Draw the hotkey letter last, so it stays readable on top + // of the hilite and pushed overlays. + drawButtonHotKeyOverlay( window ); } diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 0c8e8820660..011469dd5eb 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -141,6 +141,11 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Show each command bar cameo's hotkey letter on the cameo. + Bool m_keyboardOverlayEnabled; + Color m_keyboardOverlayColor; + Bool m_keyboardOverlayBackdrop; + Color m_keyboardOverlayBackdropColor; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index f7720c351a2..598447d3f3f 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1047,6 +1047,10 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_keyboardOverlayEnabled = FALSE; + m_keyboardOverlayColor = GameMakeColor( 255, 255, 255, 255 ); + m_keyboardOverlayBackdrop = TRUE; + m_keyboardOverlayBackdropColor = GameMakeColor( 0, 0, 0, 128 ); m_doubleClickAttackMove = FALSE; } @@ -1199,6 +1203,10 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_keyboardOverlayEnabled = optionPref.getKeyboardOverlayEnabled(); + TheWritableGlobalData->m_keyboardOverlayColor = optionPref.getKeyboardOverlayColor(); + TheWritableGlobalData->m_keyboardOverlayBackdrop = optionPref.getKeyboardOverlayBackdropEnabled(); + TheWritableGlobalData->m_keyboardOverlayBackdropColor = optionPref.getKeyboardOverlayBackdropColor(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp index d67d6282d30..549c00afcfa 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp @@ -36,6 +36,8 @@ #include "GameClient/DrawGroupInfo.h" #include "GameClient/GlobalLanguage.h" #include "W3DDevice/GameClient/W3DDisplayStringManager.h" +// TheSuperHackers @feature for the hotkey overlay string teardown +#include "W3DDevice/GameClient/W3DGadget.h" /////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS @@ -56,6 +58,11 @@ W3DDisplayStringManager::W3DDisplayStringManager() //------------------------------------------------------------------------------------------------- W3DDisplayStringManager::~W3DDisplayStringManager() { + // TheSuperHackers @feature The hotkey overlay's shared strings must come back to the + // manager while it is still alive; the base class destructor asserts on any string + // left registered. + W3DGadgetPushButtonFreeHotKeyStrings(); + for (Int i = 0; i < MAX_GROUPS; ++i) { if (m_groupNumeralStrings[i]) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 89a5fa08f9d..0d2bea5da0b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,11 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Show each command bar cameo's hotkey letter on the cameo. + Bool m_keyboardOverlayEnabled; + Color m_keyboardOverlayColor; + Bool m_keyboardOverlayBackdrop; + Color m_keyboardOverlayBackdropColor; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..ca8158d35e1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1062,6 +1062,10 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_keyboardOverlayEnabled = FALSE; + m_keyboardOverlayColor = GameMakeColor( 255, 255, 255, 255 ); + m_keyboardOverlayBackdrop = TRUE; + m_keyboardOverlayBackdropColor = GameMakeColor( 0, 0, 0, 128 ); m_doubleClickAttackMove = FALSE; } @@ -1206,6 +1210,10 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_keyboardOverlayEnabled = optionPref.getKeyboardOverlayEnabled(); + TheWritableGlobalData->m_keyboardOverlayColor = optionPref.getKeyboardOverlayColor(); + TheWritableGlobalData->m_keyboardOverlayBackdrop = optionPref.getKeyboardOverlayBackdropEnabled(); + TheWritableGlobalData->m_keyboardOverlayBackdropColor = optionPref.getKeyboardOverlayBackdropColor(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp index 34d1c0e0046..681c9ed074b 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp @@ -36,6 +36,8 @@ #include "GameClient/DrawGroupInfo.h" #include "GameClient/GlobalLanguage.h" #include "W3DDevice/GameClient/W3DDisplayStringManager.h" +// TheSuperHackers @feature for the hotkey overlay string teardown +#include "W3DDevice/GameClient/W3DGadget.h" /////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS @@ -56,6 +58,11 @@ W3DDisplayStringManager::W3DDisplayStringManager() //------------------------------------------------------------------------------------------------- W3DDisplayStringManager::~W3DDisplayStringManager() { + // TheSuperHackers @feature The hotkey overlay's shared strings must come back to the + // manager while it is still alive; the base class destructor asserts on any string + // left registered. + W3DGadgetPushButtonFreeHotKeyStrings(); + for (Int i = 0; i < MAX_GROUPS; ++i) { if (m_groupNumeralStrings[i])