MinecraftConsoles/Minecraft.Client/Common/UI/UIScene_AnvilMenu.cpp
MrTheShy 8b23c71b93 Fix TextInput bugs and refactor direct edit handling into UIScene base class
The fake cursor character (_) used for SWFs without m_mcCaret was leaking
into saved sign and anvil text. This happened because setLabel() with
instant=false only updates the C++-side cache, deferring the Flash write
to the next control tick. Any getLabel() call before that tick reads the
old Flash value still containing the underscore. Fixed by passing
instant=true in confirmDirectEdit, cancelDirectEdit, and the Enter key
path inside tickDirectEdit, so the cleaned text hits Flash immediately.

Mouse hover over TextInput controls (world name, anvil name, seed field)
was not showing the yellow highlight border. The hover code used
IggyPlayerSetFocusRS which sets Iggy's internal dispatch focus but does
not trigger Flash's ChangeState callback, so no visual feedback appeared.
Buttons worked fine because Iggy draws its own focus ring on them, but
TextInput relies entirely on ChangeState(0) for the yellow border.
Switched to SetFocusToElement which goes through the Flash-side SetFocus
path, then immediately call setCaretVisible(false) to suppress the
blinking caret that comes with focus. No visual flicker since rendering
happens after both tickInput and scene tick complete.

While direct editing, mouse hover was able to move focus away to other
TextInputs on the same scene (most noticeably on the sign editor, where
hovering a different line would steal focus from the line being typed).
Added an isDirectEditBlocking() check in the hover path to skip focus
changes when any input on the scene is actively being edited.

The Done button in SignEntryMenu was unresponsive to mouse clicks during
direct editing. The root cause is execution order: handleMouseClick runs
before handleInput in the frame. The base handleMouseClick found the Done
button and called handlePress, but handlePress bailed out because of the
isDirectEditing guard. The click was marked consumed, so handleInput
never saw it. Fixed by overriding handleMouseClick in SignEntryMenu to
detect the Done button hit while editing and confirm + close directly.

Added click-outside-to-deselect for anvil and world name text inputs.
Both scenes previously required Enter to confirm the edit, which felt
wrong. Now clicking anywhere outside the text field bounds confirms the
current text, matching standard UI behavior.

The anvil menu now updates the item name in real time while typing, like
Java edition. Previously the name was only applied on Enter, so the
repair cost display was stale until confirmation.

The biggest change is structural: every scene that used direct editing
(AnvilMenu, CreateWorldMenu, SignEntryMenu, LaunchMoreOptionsMenu,
DebugCreateSchematic, DebugSetCamera) had its own copy of the same
boilerplate -- tickDirectEdit loops in tick(), click-outside hit testing
in handleMouseClick(), cooldown guard checks in handleInput/handlePress,
and result dispatch with switch/if chains. This was around 200 lines of
near-identical code scattered across 6 files, each with its own slight
variations and its own bugs waiting to happen.

Pulled all of it into UIScene with two virtual methods: getDirectEditInputs()
where scenes register their text inputs, and onDirectEditFinished() where
they handle confirmed/cancelled results. The base class tick() drives
tickDirectEdit on all registered inputs, handleMouseClick() does the
click-outside-to-deselect hit test generically using panel offsets, and
isDirectEditBlocking() replaces all the inline cooldown checks. Scenes
now just override those two methods and get everything for free.

Also removed the m_activeDirectEditControl enum tracking from the debug
scenes (DebugCreateSchematic, DebugSetCamera) since the base class
handles lifecycle tracking through the controls themselves.
2026-03-06 16:15:24 +01:00

459 lines
11 KiB
C++

#include "stdafx.h"
#include "UI.h"
#include "..\..\..\Minecraft.World\net.minecraft.world.inventory.h"
#include "..\..\..\Minecraft.World\net.minecraft.world.level.tile.entity.h"
#include "MultiPlayerLocalPlayer.h"
#include "..\..\Minecraft.h"
#include "UIScene_AnvilMenu.h"
UIScene_AnvilMenu::UIScene_AnvilMenu(int iPad, void *_initData, UILayer *parentLayer) : UIScene_AbstractContainerMenu(iPad, parentLayer)
{
// Setup all the Iggy references we need for this scene
initialiseMovie();
m_showingCross = false;
m_textInputAnvil.init(m_itemName,eControl_TextInput);
m_labelAnvil.init( app.GetString(IDS_REPAIR_AND_NAME) );
AnvilScreenInput *initData = (AnvilScreenInput *)_initData;
m_inventory = initData->inventory;
Minecraft *pMinecraft = Minecraft::GetInstance();
if( pMinecraft->localgameModes[iPad] != NULL )
{
TutorialMode *gameMode = (TutorialMode *)pMinecraft->localgameModes[iPad];
m_previousTutorialState = gameMode->getTutorial()->getCurrentState();
gameMode->getTutorial()->changeTutorialState(e_Tutorial_State_Anvil_Menu, this);
}
m_repairMenu = new AnvilMenu( initData->inventory, initData->level, initData->x, initData->y, initData->z, pMinecraft->localplayers[iPad] );
m_repairMenu->addSlotListener(this);
Initialize( iPad, m_repairMenu, true, AnvilMenu::INV_SLOT_START, eSectionAnvilUsing, eSectionAnvilMax );
m_slotListItem1.addSlots(AnvilMenu::INPUT_SLOT, 1);
m_slotListItem2.addSlots(AnvilMenu::ADDITIONAL_SLOT, 1);
m_slotListResult.addSlots(AnvilMenu::RESULT_SLOT, 1);
bool expensive = false;
wstring m_costString = L"";
if(m_repairMenu->cost > 0)
{
if(m_repairMenu->cost >= 40 && !pMinecraft->localplayers[iPad]->abilities.instabuild)
{
m_costString = app.GetString(IDS_REPAIR_EXPENSIVE);
expensive = true;
}
else if(!m_repairMenu->getSlot(AnvilMenu::RESULT_SLOT)->hasItem())
{
// Do nothing
}
else
{
LPCWSTR costString = app.GetString(IDS_REPAIR_COST);
wchar_t temp[256];
swprintf(temp, 256, costString, m_repairMenu->cost);
m_costString = temp;
if(!m_repairMenu->getSlot(AnvilMenu::RESULT_SLOT)->mayPickup(dynamic_pointer_cast<Player>(m_inventory->player->shared_from_this())))
{
expensive = true;
}
}
}
setCostLabel(m_costString, expensive);
if(initData) delete initData;
setIgnoreInput(false);
app.SetRichPresenceContext(iPad, CONTEXT_GAME_STATE_ANVIL);
}
wstring UIScene_AnvilMenu::getMoviePath()
{
if(app.GetLocalPlayerCount() > 1)
{
return L"AnvilMenuSplit";
}
else
{
return L"AnvilMenu";
}
}
void UIScene_AnvilMenu::handleReload()
{
Initialize( m_iPad, m_menu, true, AnvilMenu::INV_SLOT_START, eSectionAnvilUsing, eSectionAnvilMax );
m_slotListItem1.addSlots(AnvilMenu::INPUT_SLOT, 1);
m_slotListItem2.addSlots(AnvilMenu::ADDITIONAL_SLOT, 1);
m_slotListResult.addSlots(AnvilMenu::RESULT_SLOT, 1);
}
void UIScene_AnvilMenu::tick()
{
UIScene_AbstractContainerMenu::tick();
#ifdef _WINDOWS64
// Live update: sync item name per-keystroke while editing (like Java edition)
if (m_textInputAnvil.isDirectEditing())
{
const wstring& buf = m_textInputAnvil.getEditBuffer();
if (buf != m_itemName)
{
m_itemName = buf;
updateItemName();
}
}
#endif
handleTick();
}
int UIScene_AnvilMenu::getSectionColumns(ESceneSection eSection)
{
int cols = 0;
switch( eSection )
{
case eSectionAnvilItem1:
cols = 1;
break;
case eSectionAnvilItem2:
cols = 1;
break;
case eSectionAnvilResult:
cols = 1;
break;
case eSectionAnvilInventory:
cols = 9;
break;
case eSectionAnvilUsing:
cols = 9;
break;
default:
assert( false );
break;
}
return cols;
}
int UIScene_AnvilMenu::getSectionRows(ESceneSection eSection)
{
int rows = 0;
switch( eSection )
{
case eSectionAnvilItem1:
rows = 1;
break;
case eSectionAnvilItem2:
rows = 1;
break;
case eSectionAnvilResult:
rows = 1;
break;
case eSectionAnvilInventory:
rows = 3;
break;
case eSectionAnvilUsing:
rows = 1;
break;
default:
assert( false );
break;
}
return rows;
}
void UIScene_AnvilMenu::GetPositionOfSection( ESceneSection eSection, UIVec2D* pPosition )
{
switch( eSection )
{
case eSectionAnvilItem1:
pPosition->x = m_slotListItem1.getXPos();
pPosition->y = m_slotListItem1.getYPos();
break;
case eSectionAnvilItem2:
pPosition->x = m_slotListItem2.getXPos();
pPosition->y = m_slotListItem2.getYPos();
break;
case eSectionAnvilResult:
pPosition->x = m_slotListResult.getXPos();
pPosition->y = m_slotListResult.getYPos();
break;
case eSectionAnvilName:
pPosition->x = m_textInputAnvil.getXPos();
pPosition->y = m_textInputAnvil.getYPos();
break;
case eSectionAnvilInventory:
pPosition->x = m_slotListInventory.getXPos();
pPosition->y = m_slotListInventory.getYPos();
break;
case eSectionAnvilUsing:
pPosition->x = m_slotListHotbar.getXPos();
pPosition->y = m_slotListHotbar.getYPos();
break;
default:
assert( false );
break;
}
}
void UIScene_AnvilMenu::GetItemScreenData( ESceneSection eSection, int iItemIndex, UIVec2D* pPosition, UIVec2D* pSize )
{
UIVec2D sectionSize;
switch( eSection )
{
case eSectionAnvilItem1:
sectionSize.x = m_slotListItem1.getWidth();
sectionSize.y = m_slotListItem1.getHeight();
break;
case eSectionAnvilItem2:
sectionSize.x = m_slotListItem2.getWidth();
sectionSize.y = m_slotListItem2.getHeight();
break;
case eSectionAnvilResult:
sectionSize.x = m_slotListResult.getWidth();
sectionSize.y = m_slotListResult.getHeight();
break;
case eSectionAnvilName:
sectionSize.x = m_textInputAnvil.getWidth();
sectionSize.y = m_textInputAnvil.getHeight();
break;
case eSectionAnvilInventory:
sectionSize.x = m_slotListInventory.getWidth();
sectionSize.y = m_slotListInventory.getHeight();
break;
case eSectionAnvilUsing:
sectionSize.x = m_slotListHotbar.getWidth();
sectionSize.y = m_slotListHotbar.getHeight();
break;
default:
assert( false );
break;
}
if(IsSectionSlotList(eSection))
{
int rows = getSectionRows(eSection);
int cols = getSectionColumns(eSection);
pSize->x = sectionSize.x/cols;
pSize->y = sectionSize.y/rows;
int itemCol = iItemIndex % cols;
int itemRow = iItemIndex/cols;
pPosition->x = itemCol * pSize->x;
pPosition->y = itemRow * pSize->y;
}
else
{
GetPositionOfSection(eSection, pPosition);
pSize->x = sectionSize.x;
pSize->y = sectionSize.y;
}
}
void UIScene_AnvilMenu::setSectionSelectedSlot(ESceneSection eSection, int x, int y)
{
int cols = getSectionColumns(eSection);
int index = (y * cols) + x;
UIControl_SlotList *slotList = NULL;
switch( eSection )
{
case eSectionAnvilItem1:
slotList = &m_slotListItem1;
break;
case eSectionAnvilItem2:
slotList = &m_slotListItem2;
break;
case eSectionAnvilResult:
slotList = &m_slotListResult;
break;
case eSectionAnvilInventory:
slotList = &m_slotListInventory;
break;
case eSectionAnvilUsing:
slotList = &m_slotListHotbar;
break;
default:
assert( false );
break;
}
slotList->setHighlightSlot(index);
}
UIControl *UIScene_AnvilMenu::getSection(ESceneSection eSection)
{
UIControl *control = NULL;
switch( eSection )
{
case eSectionAnvilItem1:
control = &m_slotListItem1;
break;
case eSectionAnvilItem2:
control = &m_slotListItem2;
break;
case eSectionAnvilResult:
control = &m_slotListResult;
break;
case eSectionAnvilName:
control = &m_textInputAnvil;
break;
case eSectionAnvilInventory:
control = &m_slotListInventory;
break;
case eSectionAnvilUsing:
control = &m_slotListHotbar;
break;
default:
assert( false );
break;
}
return control;
}
#ifdef _WINDOWS64
void UIScene_AnvilMenu::getDirectEditInputs(vector<UIControl_TextInput*> &inputs)
{
inputs.push_back(&m_textInputAnvil);
}
void UIScene_AnvilMenu::onDirectEditFinished(UIControl_TextInput *input, UIControl_TextInput::EDirectEditResult result)
{
m_itemName = input->getEditBuffer();
updateItemName();
}
#endif
int UIScene_AnvilMenu::KeyboardCompleteCallback(LPVOID lpParam,bool bRes)
{
UIScene_AnvilMenu *pClass=(UIScene_AnvilMenu *)lpParam;
pClass->setIgnoreInput(false);
if (bRes)
{
#ifdef _WINDOWS64
uint16_t pchText[128];
ZeroMemory(pchText, 128 * sizeof(uint16_t));
Win64_GetKeyboardText(pchText, 128);
pClass->setEditNameValue((wchar_t *)pchText);
pClass->m_itemName = (wchar_t *)pchText;
pClass->updateItemName();
#else
uint16_t pchText[128];
ZeroMemory(pchText, 128 * sizeof(uint16_t) );
InputManager.GetText(pchText);
pClass->setEditNameValue((wchar_t *)pchText);
pClass->m_itemName = (wchar_t *)pchText;
pClass->updateItemName();
#endif
}
return 0;
}
void UIScene_AnvilMenu::handleEditNamePressed()
{
#ifdef _WINDOWS64
if (isDirectEditBlocking())
return;
if (g_KBMInput.IsKBMActive())
{
m_textInputAnvil.beginDirectEdit(30);
}
else
{
setIgnoreInput(true);
UIKeyboardInitData kbData;
kbData.title = app.GetString(IDS_TITLE_RENAME);
kbData.defaultText = m_textInputAnvil.getLabel();
kbData.maxChars = 30;
kbData.callback = &UIScene_AnvilMenu::KeyboardCompleteCallback;
kbData.lpParam = this;
ui.NavigateToScene(m_iPad, eUIScene_Keyboard, &kbData, eUILayer_Fullscreen, eUIGroup_Fullscreen);
}
#else
setIgnoreInput(true);
#if defined(__PS3__) || defined(__ORBIS__) || defined __PSVITA__
int language = XGetLanguage();
switch(language)
{
case XC_LANGUAGE_JAPANESE:
case XC_LANGUAGE_KOREAN:
case XC_LANGUAGE_TCHINESE:
InputManager.RequestKeyboard(app.GetString(IDS_TITLE_RENAME),m_textInputAnvil.getLabel(),(DWORD)m_iPad,30,&UIScene_AnvilMenu::KeyboardCompleteCallback,this,C_4JInput::EKeyboardMode_Default);
break;
default:
InputManager.RequestKeyboard(app.GetString(IDS_TITLE_RENAME),m_textInputAnvil.getLabel(),(DWORD)m_iPad,30,&UIScene_AnvilMenu::KeyboardCompleteCallback,this,C_4JInput::EKeyboardMode_Alphabet_Extended);
break;
}
#else
InputManager.RequestKeyboard(app.GetString(IDS_TITLE_RENAME),m_textInputAnvil.getLabel(),(DWORD)m_iPad,30,&UIScene_AnvilMenu::KeyboardCompleteCallback,this,C_4JInput::EKeyboardMode_Default);
#endif
#endif
}
void UIScene_AnvilMenu::setEditNameValue(const wstring &name)
{
m_textInputAnvil.setLabel(name);
}
void UIScene_AnvilMenu::setEditNameEditable(bool enabled)
{
}
void UIScene_AnvilMenu::setCostLabel(const wstring &label, bool canAfford)
{
if (!getMovie()) return;
IggyDataValue result;
IggyDataValue value[2];
IggyStringUTF16 stringVal;
stringVal.string = (IggyUTF16*)label.c_str();
stringVal.length = label.length();
value[0].type = IGGY_DATATYPE_string_UTF16;
value[0].string16 = stringVal;
value[1].type = IGGY_DATATYPE_boolean;
value[1].boolval = canAfford;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSetCostLabel , 2 , value );
}
void UIScene_AnvilMenu::showCross(bool show)
{
if(m_showingCross != show)
{
if (!getMovie()) return;
IggyDataValue result;
IggyDataValue value[1];
value[0].type = IGGY_DATATYPE_boolean;
value[0].boolval = show;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcShowRedCross , 1 , value );
m_showingCross = show;
}
}
void UIScene_AnvilMenu::handleDestroy()
{
#ifdef __PSVITA__
app.DebugPrintf("missing InputManager.DestroyKeyboard on Vita !!!!!!\n");
#endif
// another player destroyed the anvil, so shut down the keyboard if it is displayed
#if ( defined __PS3__ || defined __ORBIS__ || defined _DURANGO)
InputManager.DestroyKeyboard();
#endif
UIScene_AbstractContainerMenu::handleDestroy();
}