2025-08-30 01:23:50 +10:00
|
|
|
|
#include "ContextMenu.h"
|
2025-12-06 05:18:04 -08:00
|
|
|
|
|
2025-12-14 21:02:33 -08:00
|
|
|
|
#include "main.h"
|
2025-12-15 00:18:06 -08:00
|
|
|
|
#include "textUtils.h"
|
|
|
|
|
|
#include "drawUtils.h"
|
2025-08-30 01:23:50 +10:00
|
|
|
|
|
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;
|
2025-12-04 02:31:32 -08:00
|
|
|
|
m_x=0; m_y=0; m_w=0; m_rowH=28;
|
2025-12-15 19:25:46 -08:00
|
|
|
|
m_mw = 160; m_Mw = 320;
|
2025-08-30 01:23:50 +10:00
|
|
|
|
m_prevA=m_prevB=m_prevX=m_prevWhite=m_prevBlack=0;
|
|
|
|
|
|
m_prevButtons=0;
|
2025-12-02 20:39:05 -08:00
|
|
|
|
m_parentMenu = NULL;
|
2025-12-03 00:46:34 -08:00
|
|
|
|
m_label[0] = '\0';
|
2025-08-30 01:23:50 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2025-12-02 20:39:05 -08:00
|
|
|
|
strncpy(m_items[m_count].label, label, sizeof(m_items[m_count].label) - 1);
|
|
|
|
|
|
m_items[m_count].label[sizeof(m_items[m_count].label) - 1] = '\0';
|
2025-08-30 01:23:50 +10:00
|
|
|
|
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-12-02 20:39:05 -08:00
|
|
|
|
m_items[m_count].child = NULL;
|
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;
|
2025-12-02 20:39:05 -08:00
|
|
|
|
m_items[m_count].label[0] = 0; // not used for separators
|
2025-12-04 02:31:32 -08:00
|
|
|
|
m_items[m_count].act = ACT_NONE; // placeholder; ignored
|
2025-09-02 14:26:19 +10:00
|
|
|
|
m_items[m_count].enabled = false;
|
|
|
|
|
|
m_items[m_count].separator = true; // <-- key bit
|
2025-12-02 20:39:05 -08:00
|
|
|
|
m_items[m_count].child = NULL;
|
|
|
|
|
|
++m_count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//Add a selectable submenu
|
|
|
|
|
|
void ContextMenu::AddSubMenu(const char* label, ContextMenu* submenu, bool enabled) {
|
|
|
|
|
|
if (m_count >= (int)(sizeof(m_items) / sizeof(m_items[0]))) return;
|
|
|
|
|
|
strncpy(m_items[m_count].label, label, sizeof(m_items[m_count].label) - 1);
|
|
|
|
|
|
m_items[m_count].label[sizeof(m_items[m_count].label) - 1] = '\0';
|
|
|
|
|
|
m_items[m_count].act = ACT_NONE; // not used for submenu
|
|
|
|
|
|
m_items[m_count].enabled = enabled;
|
|
|
|
|
|
m_items[m_count].separator = false;
|
|
|
|
|
|
m_items[m_count].child = submenu; // <--- SUBMENU POINTER
|
2025-09-02 14:26:19 +10:00
|
|
|
|
++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-12-06 01:32:28 -08:00
|
|
|
|
void ContextMenu::OpenAt(float x, float y, float width, float rowH) {
|
2025-12-02 20:39:05 -08:00
|
|
|
|
m_x=x; m_y=y;
|
|
|
|
|
|
m_w=width; m_rowH=rowH;
|
|
|
|
|
|
|
2025-12-06 01:32:28 -08:00
|
|
|
|
int count = m_count + 1; // Quick fix, but why?
|
|
|
|
|
|
|
2025-12-02 20:39:05 -08:00
|
|
|
|
if (m_dev) {
|
|
|
|
|
|
D3DVIEWPORT8 vp;
|
|
|
|
|
|
m_dev->GetViewport(&vp);
|
|
|
|
|
|
|
|
|
|
|
|
float screenW = (float)vp.Width;
|
2025-12-06 01:32:28 -08:00
|
|
|
|
float screenH = (float)vp.Height - 30.0f; // Safety, but why?
|
2025-12-02 20:39:05 -08:00
|
|
|
|
|
|
|
|
|
|
// Right clamp
|
|
|
|
|
|
if (m_x + m_w > screenW) m_x = screenW - m_w;
|
|
|
|
|
|
|
|
|
|
|
|
// Left clamp
|
|
|
|
|
|
if (m_x < 0) m_x = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Bottom clamp
|
2025-12-06 01:32:28 -08:00
|
|
|
|
if (m_y + (count * m_rowH) > screenH) m_y = screenH - (count * m_rowH);
|
2025-12-02 20:39:05 -08:00
|
|
|
|
|
|
|
|
|
|
// Top clamp
|
|
|
|
|
|
if (m_y < 0) m_y = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 01:23:50 +10:00
|
|
|
|
if (m_sel < 0) m_sel = 0;
|
2025-12-06 01:32:28 -08:00
|
|
|
|
if (m_sel >= count) m_sel = (count > 0) ? (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;
|
2025-12-02 20:39:05 -08:00
|
|
|
|
m_prevButtons = 0;
|
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-12-04 02:31:32 -08: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
|
|
|
|
|
2025-12-03 01:16:11 -08:00
|
|
|
|
bool drawHeader = false;
|
|
|
|
|
|
if (m_label[0] != '\0') drawHeader = true;
|
|
|
|
|
|
|
2025-12-06 01:32:28 -08:00
|
|
|
|
FLOAT tw = 0.0f;
|
2025-12-04 02:31:32 -08:00
|
|
|
|
for (int i = 0; i < m_count; ++i) {
|
|
|
|
|
|
const Item& it = m_items[i];
|
2025-12-06 01:32:28 -08:00
|
|
|
|
FLOAT cw = GetAnsiW(font, it.label);
|
2025-12-04 02:31:32 -08:00
|
|
|
|
tw = (tw > cw) ? tw : cw;
|
|
|
|
|
|
}
|
2025-08-30 16:40:43 +10:00
|
|
|
|
|
2025-12-05 21:26:20 -08:00
|
|
|
|
FLOAT hdrW, hdrH;
|
|
|
|
|
|
GetAnsiWH(font, m_label, &hdrW, &hdrH);
|
2025-08-30 16:40:43 +10:00
|
|
|
|
|
2025-12-04 02:31:32 -08:00
|
|
|
|
tw = (tw > hdrW) ? tw : hdrW;
|
|
|
|
|
|
tw = (tw > m_mw) ? tw : m_mw;
|
|
|
|
|
|
|
|
|
|
|
|
//const FLOAT menuW = m_w;
|
|
|
|
|
|
const FLOAT menuW = tw + 32.0f; // + 32.0f (label padding)
|
|
|
|
|
|
const FLOAT rowH = m_rowH;
|
|
|
|
|
|
|
|
|
|
|
|
m_w = menuW; // menuW is unneccessary but leaving it for now
|
|
|
|
|
|
|
2025-09-02 14:26:19 +10:00
|
|
|
|
// Layout constants (dynamic header height support)
|
|
|
|
|
|
const FLOAT headerTopPad = 8.0f;
|
2025-12-03 01:16:11 -08:00
|
|
|
|
FLOAT headerBottomPad = 6.0f;
|
|
|
|
|
|
if (!drawHeader) headerBottomPad = 3.0f; //inelegant solution to size correctly without the header
|
2025-12-06 01:32:28 -08:00
|
|
|
|
const FLOAT bottomPad = 10.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
|
2025-12-06 01:32:28 -08:00
|
|
|
|
const FLOAT listTop = lineY + 6.0f; // first row Y
|
2025-09-02 14:26:19 +10:00
|
|
|
|
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-12-05 21:26:20 -08:00
|
|
|
|
DrawSolidRect(dev, x - 4.5f, y - 4.5f, menuW + 9.0f, menuH + 9.0f, 0xA0101010);
|
|
|
|
|
|
DrawSolidRect(dev, x, y, menuW, menuH, 0xE0222222);
|
2025-08-30 01:23:50 +10:00
|
|
|
|
|
2025-09-02 14:26:19 +10:00
|
|
|
|
// Header
|
2025-12-03 01:16:11 -08:00
|
|
|
|
if (drawHeader) {
|
2025-12-05 21:26:20 -08:00
|
|
|
|
DrawAnsi(font, x + 10.0f, y + headerTopPad, 0xFFFFFFFF, &DefaultColors, m_label);
|
|
|
|
|
|
DrawSolidRect(dev, x, lineY, menuW, 1.0f, 0x60FFFFFF);
|
2025-12-03 01:16:11 -08:00
|
|
|
|
}
|
2025-08-30 16:40:43 +10:00
|
|
|
|
|
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-12-06 01:32:28 -08:00
|
|
|
|
if (it.separator) {
|
2025-09-02 14:26:19 +10:00
|
|
|
|
// Non-selectable divider line centered within the row box
|
2025-12-05 21:26:20 -08:00
|
|
|
|
DrawSolidRect(dev, x + 10.0f, rowY + rowH * 0.5f, menuW - 20.0f, 1.0f, 0x50FFFFFF);
|
2025-09-02 14:26:19 +10:00
|
|
|
|
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-12-05 21:26:20 -08:00
|
|
|
|
DrawSolidRect(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-12-06 01:32:28 -08:00
|
|
|
|
DrawAnsiCentered(font, x + 16.0f, rowY, col, &DefaultColors, it.label, NULL, rowH);
|
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-12-06 01:32:28 -08: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-12-06 01:32:28 -08: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-12-06 01:32:28 -08:00
|
|
|
|
if (up) {
|
2025-09-02 14:26:19 +10:00
|
|
|
|
int i = FindNextSelectable(m_sel - 1, -1);
|
|
|
|
|
|
if (i >= 0) m_sel = i;
|
|
|
|
|
|
}
|
2025-12-06 01:32:28 -08:00
|
|
|
|
if (down) {
|
2025-09-02 14:26:19 +10:00
|
|
|
|
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-12-06 01:32:28 -08:00
|
|
|
|
if (aTrig) {
|
2025-08-30 01:23:50 +10:00
|
|
|
|
const Item& it = m_items[m_sel];
|
2025-12-02 20:39:05 -08:00
|
|
|
|
|
|
|
|
|
|
if (it.child != NULL) {
|
|
|
|
|
|
float subX = m_x + m_w;
|
|
|
|
|
|
float subY = m_y + (m_sel * m_rowH);
|
|
|
|
|
|
it.child->m_parentMenu = this; // so it can close back to us
|
|
|
|
|
|
it.child->OpenAt(subX, subY, m_w, m_rowH);
|
|
|
|
|
|
|
|
|
|
|
|
return SUBMENU_OPENED;
|
|
|
|
|
|
}
|
2025-12-06 01:32:28 -08:00
|
|
|
|
if (IsSelectable(m_sel)) {
|
|
|
|
|
|
outAct = it.act;
|
|
|
|
|
|
return CHOSEN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 14:26:19 +10:00
|
|
|
|
return NOOP; // ignore A on non-selectable (e.g., separator)
|
2025-08-30 01:23:50 +10:00
|
|
|
|
}
|
2025-12-06 01:32:28 -08:00
|
|
|
|
|
2025-08-30 01:23:50 +10:00
|
|
|
|
if (bTrig || xTrig) return CLOSED;
|
|
|
|
|
|
|
|
|
|
|
|
return NOOP;
|
|
|
|
|
|
}
|