Files
FileManager/ContextMenu.cpp
T

241 lines
8.6 KiB
C++
Raw Permalink Normal View History

2025-08-30 01:23:50 +10:00
#include "ContextMenu.h"
2025-08-30 03:06:35 +10:00
#include "GfxPrims.h"
2025-08-30 01:23:50 +10:00
#include <wchar.h>
2025-09-02 14:26:19 +10:00
/*
===============================================================================
ContextMenu
- Small, controller-driven popup menu for the file browser.
- Works on OG Xbox (XDK / VS2003): ANSI strings, no STL allocations here.
- Supports non-selectable separator rows that are skipped by navigation.
Life cycle:
* AddItem() / AddSeparator()
* OpenAt(x,y,width,rowH) -> becomes active and edge-debounced
* Draw(font, device) -> render when m_open = true
* OnPad(pad, outAct) -> handles movement / choose / close
* Close() -> hide
Input:
* D-Pad Up/Down or stick Y to move selection (skips separators/disabled)
* A to choose (CHOSEN result + outAct set)
* B or X to close (CLOSED result)
* Start ignored here (caller handles global Start if needed)
Rendering:
* Simple framed panel with dynamic header height, divider line,
then N rows of fixed height (m_rowH).
===============================================================================
*/
2025-08-30 01:23:50 +10:00
ContextMenu::ContextMenu(){
m_count=0; m_sel=0;
m_open=false; m_waitRelease=false;
m_x=0; m_y=0; m_w=320; m_rowH=28;
m_prevA=m_prevB=m_prevX=m_prevWhite=m_prevBlack=0;
m_prevButtons=0;
}
void ContextMenu::Clear(){ m_count=0; m_sel=0; }
2025-09-02 14:26:19 +10:00
// Add a selectable row
2025-08-30 01:23:50 +10:00
void ContextMenu::AddItem(const char* label, Action act, bool enabled){
if (m_count >= (int)(sizeof(m_items)/sizeof(m_items[0]))) return;
m_items[m_count].label = label;
m_items[m_count].act = act;
m_items[m_count].enabled = enabled;
2025-09-02 14:26:19 +10:00
m_items[m_count].separator = false;
2025-08-30 01:23:50 +10:00
++m_count;
}
2025-09-02 14:26:19 +10:00
// Add a non-selectable separator row (drawn as a thin line)
void ContextMenu::AddSeparator(){
if (m_count >= (int)(sizeof(m_items)/sizeof(m_items[0]))) return;
m_items[m_count].label = ""; // not used for separators
m_items[m_count].act = ACT_OPEN; // placeholder; ignored
m_items[m_count].enabled = false;
m_items[m_count].separator = true; // <-- key bit
++m_count;
}
// Row can be chosen iff enabled and not a separator
bool ContextMenu::IsSelectable(int idx) const{
if (idx < 0 || idx >= m_count) return false;
const Item& it = m_items[idx];
return (it.enabled && !it.separator);
}
// Scan forward/backward for the next selectable row; return -1 if none
int ContextMenu::FindNextSelectable(int start, int dir) const{
int i = start;
while (i >= 0 && i < m_count){
if (IsSelectable(i)) return i;
i += dir;
}
return -1;
}
// Position/size the menu and make it active.
// Also snaps the selection onto a valid (selectable) row and
// arms a small "wait for release" window so the A/X that opened
// the menu doesn’t immediately trigger a choose/close here.
2025-08-30 01:23:50 +10:00
void ContextMenu::OpenAt(float x, float y, float width, float rowH){
m_x=x; m_y=y; m_w=width; m_rowH=rowH;
if (m_sel < 0) m_sel = 0;
if (m_sel >= m_count) m_sel = (m_count>0)?(m_count-1):0;
2025-09-02 14:26:19 +10:00
// If current selection is not selectable, try forward then backward.
if (!IsSelectable(m_sel)){
int fwd = FindNextSelectable(m_sel, +1);
int back = FindNextSelectable(m_sel, -1);
if (fwd >= 0) m_sel = fwd;
else if (back >= 0) m_sel = back;
else m_sel = 0; // no selectable items; harmless default
}
2025-08-30 01:23:50 +10:00
m_open=true;
m_waitRelease=true; // avoid immediate A/X carry-over
m_prevA=m_prevB=m_prevX=m_prevWhite=m_prevBlack=0;
m_prevButtons=0;
}
void ContextMenu::Close(){ m_open=false; }
2025-09-02 14:26:19 +10:00
// --- tiny ANSI->wide draw helpers (XDK fonts are wide) ----------------------
2025-08-30 01:23:50 +10:00
void ContextMenu::DrawAnsi(CXBFont& font, FLOAT x, FLOAT y, DWORD color, const char* text){
WCHAR wbuf[512]; MultiByteToWideChar(CP_ACP,0,text,-1,wbuf,512);
font.DrawText(x,y,color,wbuf,0,0.0f);
}
void ContextMenu::DrawRect(LPDIRECT3DDEVICE8 dev, float x,float y,float w,float h,D3DCOLOR c){
2025-08-30 03:06:35 +10:00
DrawSolidRect(dev, x, y, w, h, c);
2025-08-30 01:23:50 +10:00
}
2025-09-02 14:26:19 +10:00
// Render the menu panel + rows. Separators are drawn as thin centered lines.
// Disabled rows are dimmed and cannot be focused via navigation.
2025-08-30 01:23:50 +10:00
void ContextMenu::Draw(CXBFont& font, LPDIRECT3DDEVICE8 dev) const{
2025-08-30 16:40:43 +10:00
if (!m_open || m_count <= 0) return;
2025-08-30 01:23:50 +10:00
const FLOAT menuW = m_w;
const FLOAT rowH = m_rowH;
2025-08-30 16:40:43 +10:00
// --- measure header text height (ANSI -> wide, then GetTextExtent) ---
FLOAT hdrW = 0.0f, hdrH = 0.0f;
{
const char* title = "Select action";
WCHAR wbuf[128];
MultiByteToWideChar(CP_ACP, 0, title, -1, wbuf, 128);
font.GetTextExtent(wbuf, &hdrW, &hdrH);
}
2025-09-02 14:26:19 +10:00
// Layout constants (dynamic header height support)
const FLOAT headerTopPad = 8.0f;
const FLOAT headerBottomPad = 6.0f;
const FLOAT bottomPad = 12.0f;
2025-08-30 16:40:43 +10:00
2025-08-30 01:23:50 +10:00
const FLOAT x = m_x;
const FLOAT y = m_y;
2025-09-02 14:26:19 +10:00
const FLOAT lineY = y + headerTopPad + hdrH + headerBottomPad; // divider Y
const FLOAT listTop = lineY + 6.0f; // first row Y
const FLOAT menuH = (listTop - y) + (m_count * rowH) + bottomPad;
2025-08-30 16:40:43 +10:00
2025-09-02 14:26:19 +10:00
// Frame/background
2025-08-30 16:40:43 +10:00
DrawRect(dev, x - 6.0f, y - 6.0f, menuW + 12.0f, menuH + 12.0f, 0xA0101010);
2025-08-30 01:23:50 +10:00
DrawRect(dev, x, y, menuW, menuH, 0xE0222222);
2025-09-02 14:26:19 +10:00
// Header
2025-08-30 16:40:43 +10:00
DrawAnsi(font, x + 10.0f, y + headerTopPad, 0xFFFFFFFF, "Select action");
DrawRect(dev, x, lineY, menuW, 1.0f, 0x60FFFFFF);
2025-09-02 14:26:19 +10:00
// Rows
2025-08-30 16:40:43 +10:00
for (int i = 0; i < m_count; ++i){
const FLOAT rowY = listTop + i * rowH;
2025-09-02 14:26:19 +10:00
const Item& it = m_items[i];
2025-08-30 01:23:50 +10:00
2025-09-02 14:26:19 +10:00
if (it.separator){
// Non-selectable divider line centered within the row box
DrawRect(dev, x + 10.0f, rowY + rowH * 0.5f, menuW - 20.0f, 1.0f, 0x50FFFFFF);
continue;
}
// Selection highlight (only meaningful on selectable rows)
2025-08-30 01:23:50 +10:00
bool sel = (i == m_sel);
D3DCOLOR row = sel ? 0x60FFFF00 : 0x20202020;
2025-08-30 16:40:43 +10:00
DrawRect(dev, x + 6.0f, rowY - 2.0f, menuW - 12.0f, rowH, row);
2025-08-30 01:23:50 +10:00
2025-09-02 14:26:19 +10:00
// Text color: normal/selected/disabled
2025-08-30 16:40:43 +10:00
DWORD col = it.enabled ? (sel ? 0xFF202020 : 0xFFE0E0E0) : 0xFF7A7A7A;
2025-09-02 14:26:19 +10:00
// Measure label height for vertical centering
2025-08-30 16:40:43 +10:00
FLOAT tw = 0.0f, th = 0.0f;
{
WCHAR wbuf[256];
MultiByteToWideChar(CP_ACP, 0, it.label, -1, wbuf, 256);
font.GetTextExtent(wbuf, &tw, &th);
}
const FLOAT textY = rowY + (rowH - th) * 0.5f;
DrawAnsi(font, x + 16.0f, textY, col, it.label);
2025-08-30 01:23:50 +10:00
}
}
2025-09-02 14:26:19 +10:00
// Handle controller input for the menu.
// Returns CHOSEN with an Action when A is pressed on a selectable row,
// CLOSED when B or X is pressed, or NOOP if nothing to do this frame.
2025-08-30 01:23:50 +10:00
ContextMenu::Result ContextMenu::OnPad(const XBGAMEPAD& pad, Action& outAct){
2025-09-02 14:26:19 +10:00
outAct = ACT_OPEN; // harmless default
2025-08-30 01:23:50 +10:00
if (!m_open) return NOOP;
const DWORD btn = pad.wButtons;
unsigned char a = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
unsigned char b = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
unsigned char x = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
2025-09-02 14:26:19 +10:00
// Debounce: absorb the input that opened the menu (prevents immediate choose/close)
2025-08-30 01:23:50 +10:00
if (m_waitRelease){
2025-09-02 14:26:19 +10:00
bool held = (a>30)||(b>30)||(x>30)||
(btn & XINPUT_GAMEPAD_START)||
(btn & XINPUT_GAMEPAD_DPAD_UP)||
(btn & XINPUT_GAMEPAD_DPAD_DOWN);
2025-08-30 01:23:50 +10:00
m_prevA=a; m_prevB=b; m_prevX=x; m_prevButtons=btn;
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
if (!held) m_waitRelease=false;
return NOOP;
}
2025-09-02 14:26:19 +10:00
// Navigation (edge + analog)
2025-08-30 01:23:50 +10:00
bool up = ((btn & XINPUT_GAMEPAD_DPAD_UP) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_UP)) || (pad.sThumbLY > 16000);
bool down = ((btn & XINPUT_GAMEPAD_DPAD_DOWN) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_DOWN)) || (pad.sThumbLY < -16000);
2025-09-02 14:26:19 +10:00
if (up){
int i = FindNextSelectable(m_sel - 1, -1);
if (i >= 0) m_sel = i;
}
if (down){
int i = FindNextSelectable(m_sel + 1, +1);
if (i >= 0) m_sel = i;
}
2025-08-30 01:23:50 +10:00
2025-09-02 14:26:19 +10:00
// Button edges
2025-08-30 01:23:50 +10:00
bool aTrig = (a > 30 && m_prevA <= 30);
bool bTrig = (b > 30 && m_prevB <= 30);
bool xTrig = (x > 30 && m_prevX <= 30);
2025-09-02 14:26:19 +10:00
// Save previous for next frame's edge detection
2025-08-30 01:23:50 +10:00
m_prevButtons = btn;
m_prevA = a; m_prevB = b; m_prevX = x;
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
2025-09-02 14:26:19 +10:00
// Choose / close behavior
2025-08-30 01:23:50 +10:00
if (aTrig){
const Item& it = m_items[m_sel];
2025-09-02 14:26:19 +10:00
if (IsSelectable(m_sel)){ outAct = it.act; return CHOSEN; }
return NOOP; // ignore A on non-selectable (e.g., separator)
2025-08-30 01:23:50 +10:00
}
if (bTrig || xTrig) return CLOSED;
return NOOP;
}