#include "main.h"
#include "fsUtils.h"
#include "Font.h"
#include "textUtils.h"
#include "network.h"
#include "configuration.h"
#include "drawUtils.h"
#include "stringUtility.h"
// Simple getter used by overlay/status timers.
DWORD FileBrowserApp::StatusUntilMs() const { return m_statusUntilMs; }
// ---- local helpers (no header pollution) -----------------------------------
// These helpers are file-local to avoid leaking symbols into headers/other TUs.
namespace {
// ---- character-step marquee (same tuning as panes/OSK) ---------------------
static const DWORD kProgInitPauseMs = 900;
static const DWORD kProgStepMs = 150;
static const DWORD kProgEndPauseMs = 1200;
static const int kProgStepChars = 1;
static char mCurrentIp[16] = "0.0.0.0";
// Bigger side insets on SD to avoid CRT overscan clipping.
inline FLOAT SafeMarginX(FLOAT screenW, FLOAT screenH) {
const bool isSD = (screenH <= 600.0f); // 480/576
const FLOAT pct = isSD ? 0.065f : 0.03f; // 5% each side on SD, 3% on HD
return max(24.0f, screenW * pct); // never smaller than 24px
}
struct ProgMarquee {
FLOAT px; // start index (characters)
FLOAT fitWLock; // lock width for stable end calc
DWORD nextTick; // next update tick
DWORD resetPause; // nonzero while pausing at end
char last[768]; // last string drawn (for reset-on-change)
ProgMarquee() : px(0.f), fitWLock(0.f), nextTick(0), resetPause(0) { last[0]=0; }
};
// Draw one line at (x,y) clipped to maxW using a character-step marquee.
static void DrawLabelFittedOrMarquee(CXBFont& font, FLOAT x, FLOAT y, FLOAT maxW, DWORD color, const char* text, ProgMarquee& M) {
if (!text) text = "";
// right guard + tolerance like OSK
const FLOAT kRightGuard = 1.5f;
const FLOAT kTol = 2.0f;
const FLOAT fitW_now = (maxW > kRightGuard) ? (maxW - kRightGuard) : 0.0f;
FLOAT fullW, fullH;
GetAnsiWH(font, text, &fullW, &fullH);
// If it fits, draw + reset
if (fullW <= fitW_now + kTol){
DrawAnsi(font, x, y, color, &DefaultColors, text);
M = ProgMarquee();
return;
}
// Reset if label changed
if (_stricmp(M.last, text) != 0) {
_snprintf(M.last, sizeof(M.last), "%s", text); M.last[sizeof(M.last)-1]=0;
M.px = 0.0f; M.fitWLock = 0.0f; M.resetPause = 0; M.nextTick = GetTickCount() + kProgInitPauseMs;
}
const DWORD now = GetTickCount();
if (M.fitWLock <= 0.0f) M.fitWLock = fitW_now;
const FLOAT fitW = (FLOAT)((int)(M.fitWLock + 0.5f));
const char* s = text; const int len = (int)strlen(s);
// earliest start where the whole tail fits
int lo = 0, hi = len;
while (lo < hi){
const int mid = (lo + hi) / 2;
FLOAT tw, th;
GetAnsiWH(font, s + mid, &tw, &th);
if (tw <= fitW + kTol) hi = mid; else lo = mid + 1;
}
const int lastStart = (lo > len) ? len : lo;
int startIdx = (int)M.px;
if (startIdx < 0) startIdx = 0;
if (startIdx > lastStart) startIdx = lastStart;
// longest substring from start that fits
const char* startPtr = s + startIdx;
int lo2 = 0, hi2 = (int)strlen(startPtr);
while (lo2 < hi2){
const int mid = (lo2 + hi2 + 1) / 2;
char tmp[768]; _snprintf(tmp, sizeof(tmp), "%.*s", mid, startPtr);
FLOAT tw, th;
GetAnsiWH(font, tmp, &tw, &th);
if (tw <= fitW + kTol) lo2 = mid; else hi2 = mid - 1;
}
char vis[768]; _snprintf(vis, sizeof(vis), "%.*s", lo2, startPtr); vis[sizeof(vis)-1]=0;
DrawAnsi(font, x, y, color, &DefaultColors, vis);
// step/pause/reset
if (now >= M.nextTick) {
if (M.resetPause) {
M.px = 0.0f;
M.resetPause= 0;
M.nextTick = now + kProgInitPauseMs;
}
else {
if (startIdx >= lastStart){
M.px = (FLOAT)lastStart;
M.resetPause= now + kProgEndPauseMs;
M.nextTick = M.resetPause;
}
else {
M.px = (FLOAT)(startIdx + kProgStepChars);
M.nextTick = now + kProgStepMs;
}
}
}
}
// Return first byte index whose rendered width reaches px (for marquee).
static const char* SkipToPixelOffset(CXBFont& font, const char* s, FLOAT px, FLOAT& skippedW){
skippedW = 0.0f;
if (!s || px <= 0.0f) return s ? s : "";
const char* p = s;
FLOAT tw=0, th=0;
while (*p){
int len = (int)(p - s + 1); if (len > 255) len = 255;
char tmp[256]; _snprintf(tmp, sizeof(tmp), "%.*s", len, s);
GetAnsiWH(font, tmp, &tw, &th);
if (tw >= px) break;
++p;
}
skippedW = tw;
return p;
}
// Return last component of a path (handles trailing '\'). e.g. "E:\A\B\" -> "B"
static void ExtractLastComponent(const char* path, char* out, size_t cap){
if (!out || cap == 0) return;
out[0] = 0;
if (!path) return;
size_t n = strlen(path);
if (n == 0) return;
// Skip trailing '\' (but keep root like "E:\")
if (n > 3 && path[n-1] == '\\') --n;
// Find last '\'
int i = (int)n - 1;
while (i >= 0 && path[i] != '\\') --i;
const char* start = path + i + 1;
size_t len = n - (i + 1);
if (len >= cap) len = cap - 1;
if (cap) { memcpy(out, start, len); out[len] = 0; }
}
} // anonymous namespace
FileBrowserApp& FileBrowserApp::Get() { static FileBrowserApp app; return app; }
char* FileBrowserApp::GetCurrentIp() { return mCurrentIp; }
void FileBrowserApp::SetCurrentIp(char* ip) { strncpy(mCurrentIp, ip, sizeof(mCurrentIp)); mCurrentIp[sizeof(mCurrentIp) - 1] = '\0'; }
// ----------------------------------------------------------------------------
// ComputeResponsiveLayout
// - Derives all pane/header/row metrics from the current device viewport.
// - Ensures perfect left/right symmetry: [margin] [left pane] [gap] [right pane] [margin].
// - Keeps header bars exactly the same width as the panes.
// ----------------------------------------------------------------------------
void FileBrowserApp::ComputeResponsiveLayout() {
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
// ---- outer geometry ----
const FLOAT margin = SafeMarginX((FLOAT)vp.Width, (FLOAT)vp.Height);
const FLOAT gap = max(24.0f, vp.Width * 0.035f);
const FLOAT paneW = max(260.0f, (vp.Width - (margin * 2.0f) - gap) * 0.5f);
kPaneGap = gap;
kListX_L = margin;
kListW = paneW;
// header band
kHdrW = kListW;
kHdrY = max(12.0f, vp.Height * 0.03f);
kHdrH = max(22.0f, vp.Height * 0.04f);
// row height
kLineH = max(22.0f, vp.Height * 0.036f);
// keep kListY as the "pane body top" (what the rest of the code expects)
const FLOAT headerGap = max(6.0f, kHdrH * 0.35f);
kListY = kHdrY + kHdrH + headerGap;
// ---- visible rows (match PaneRenderer::DrawPane vertical math) ----
// DrawPane uses: listTop = (kListY) + colHdrH
const FLOAT colHdrH = max(22.0f, kLineH);
const FLOAT listTopInRenderer = kListY + colHdrH;
// leave room for the footer band + a small spacer at every resolution
const FLOAT footerBand = max(48.0f, vp.Height * 0.09f);
const FLOAT footerSpacer = max(6.0f, vp.Height * 0.012f);
const FLOAT bottomY = (FLOAT)vp.Height - footerBand - footerSpacer;
FLOAT usableH = bottomY - listTopInRenderer;
if (usableH < 0) usableH = 0;
m_visible = (int)(usableH / kLineH);
if (m_visible < 6) m_visible = 6;
if (m_visible > 30) m_visible = 30;
// fixed micro-metrics
kGutterW = 24.0f;
kPaddingX = 6.0f;
kScrollBarW = 3.0f;
}
// ----------------------------------------------------------------------------
// ctor: set defaults for input, layout, D3D, and app state
FileBrowserApp::FileBrowserApp() {
m_visible=13; m_prevA=0; m_prevB=0; m_prevX=0; m_prevY=0; m_active=0;
m_prevButtons = 0; m_prevWhite = m_prevBlack = 0;
m_navUDHeld=false; m_navUDDir=0; m_navUDNext=0; m_backConfirmArmed = false;
m_backConfirmUntil = 0; m_prevBack = 0;
m_dvdUsedBytes = 0;
m_dvdTotalBytes = 0;
m_dvdHaveStats = false;
m_menuDepth = 0;
// --- Auto-detect video capabilities and set PresentParams ----------------
ZeroMemory(&m_d3dpp, sizeof(m_d3dpp));
// Dashboard video flags and TV standard
const DWORD vf = XGetVideoFlags();
const DWORD vs = XGetVideoStandard(); // XC_VIDEO_STANDARD_*
const bool hasWS = (vf & XC_VIDEO_FLAGS_WIDESCREEN) != 0;
const bool has480p = (vf & XC_VIDEO_FLAGS_HDTV_480p) != 0;
const bool has720p = (vf & XC_VIDEO_FLAGS_HDTV_720p) != 0;
const bool has1080i = (vf & XC_VIDEO_FLAGS_HDTV_1080i) != 0;
// Treat anything not NTSC-M as PAL family (PAL-I/M/N). We only need
// this to decide the 576i PAL-50 fallback when 480p isn’t enabled.
const bool isPAL = (vs != XC_VIDEO_STANDARD_NTSC_M);
// Choose a backbuffer the TV actually supports.
// Priority: 720p -> 1080i -> PAL-50 (576i) -> 480 (i/p depending on flags)
if (has720p) {
m_d3dpp.BackBufferWidth = 1280;
m_d3dpp.BackBufferHeight = 720;
m_d3dpp.Flags = D3DPRESENTFLAG_PROGRESSIVE | D3DPRESENTFLAG_WIDESCREEN; // 720p is always 16:9
m_d3dpp.FullScreen_RefreshRateInHz = 60;
}
else if (has1080i) {
m_d3dpp.BackBufferWidth = 1920;
m_d3dpp.BackBufferHeight = 1080;
m_d3dpp.Flags = D3DPRESENTFLAG_INTERLACED | D3DPRESENTFLAG_WIDESCREEN; // 1080i is always 16:9
m_d3dpp.FullScreen_RefreshRateInHz = 60;
}
else if (isPAL && !has480p) {
// PAL-50: 576i
m_d3dpp.BackBufferWidth = 720;
m_d3dpp.BackBufferHeight = 576;
m_d3dpp.Flags = D3DPRESENTFLAG_INTERLACED;
if (hasWS) m_d3dpp.Flags |= D3DPRESENTFLAG_WIDESCREEN; // 16:9 if user enabled
m_d3dpp.FullScreen_RefreshRateInHz = 50;
}
else {
// 480 path (NTSC or PAL-60/480p)
m_d3dpp.BackBufferWidth = 640;
m_d3dpp.BackBufferHeight = 480;
m_d3dpp.Flags = 0;
if (has480p) m_d3dpp.Flags |= D3DPRESENTFLAG_PROGRESSIVE; // 480p if enabled
else m_d3dpp.Flags |= D3DPRESENTFLAG_INTERLACED; // otherwise 480i
if (hasWS) m_d3dpp.Flags |= D3DPRESENTFLAG_WIDESCREEN; // 16:9 anamorphic if enabled
m_d3dpp.FullScreen_RefreshRateInHz = 60; // NTSC & PAL-60
}
// Common params
m_d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
m_d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
m_d3dpp.EnableAutoDepthStencil = TRUE;
m_d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8;
// -------------------------------------------------------------------------
m_mode=MODE_BROWSE;
m_status[0]=0; m_statusUntilMs=0;
m_dvdUsedBytes = 0;
m_dvdTotalBytes = 0;
m_dvdHaveStats = 0;
}
// --- Exit helper ------------------------------------------------------------
// Jump to dashboard via XLaunchNewImage (fallback)
void FileBrowserApp::ExitNow() {
XLaunchNewImage(NULL, NULL); // returns to dashboard
}
// ----- status ---------------------------------------------------------------
// Set a 3s status toast at the footer (printf-style).
void FileBrowserApp::SetStatus(const char* fmt, ...) {
va_list ap; va_start(ap, fmt);
_vsnprintf(m_status, sizeof(m_status), fmt, ap);
va_end(ap);
m_status[sizeof(m_status)-1]=0;
m_statusUntilMs = GetTickCount() + 3000; // show for about 3s
}
// Append the last Win32 error to a prefix and show as status.
void FileBrowserApp::SetStatusLastErr(const char* prefix) {
DWORD e = GetLastError();
char msg[64]; _snprintf(msg, sizeof(msg), "%s (err=%lu)", prefix, (unsigned long)e);
msg[sizeof(msg)-1]=0;
SetStatus("%s", msg);
}
// ----- listing helpers ------------------------------------------------------
// Refresh a pane while trying to preserve selection and scroll.
void FileBrowserApp::RefreshPane(Pane& p) {
if (p.mode==1){
int prevSel = p.sel;
int prevScroll= p.scroll;
ListDirectory(p.curPath, p.items);
if (prevSel >= p.items.size()) prevSel = p.items.size()-1;
if (prevSel < 0) prevSel = 0;
p.sel = prevSel;
int maxScroll = max(0, p.items.size() - m_visible);
if (prevScroll > maxScroll) prevScroll = maxScroll;
if (prevScroll < 0) prevScroll = 0;
p.scroll = prevScroll;
}
else {
// Drive list mode: re-enumerate mounted roots
BuildDriveItems(p.items);
if (p.sel >= p.items.size()) p.sel = p.items.size() - 1;
if (p.sel < 0) p.sel = 0;
p.scroll = 0;
}
}
// Resolve the destination directory for copy/move based on the other pane.
// Returns normalized path with trailing slash in outDst on success.
bool FileBrowserApp::ResolveDestDir(char* outDst, size_t cap) {
Pane& dst = m_pane[1 - m_active];
outDst[0] = 0;
if (dst.mode == 1) {
_snprintf(outDst, (int)cap, "%s", dst.curPath);
outDst[cap-1]=0;
NormalizeDirA(outDst);
return true;
}
// If the other pane is the drive list, allow selecting a drive as destination.
if (!dst.items.empty()){
const Item& di = dst.items[dst.sel];
if (di.isDir && !di.isUpEntry){
_snprintf(outDst, (int)cap, "%s", di.name); // e.g. "E:\"
outDst[cap-1]=0;
NormalizeDirA(outDst);
return true;
}
}
return false;
}
// Resolve the source directory for unzip based on the current pane.
// Returns normalized path with trailing slash in outDst on success.
bool FileBrowserApp::ResolveSrcDir(char* srcDst, size_t cap) {
Pane& dst = m_pane[m_active];
srcDst[0] = 0;
if (dst.mode == 1) {
_snprintf(srcDst, (int)cap, "%s", dst.curPath);
srcDst[cap - 1] = 0;
NormalizeDirA(srcDst);
return true;
}
// If the other pane is the drive list, allow selecting a drive as destination.
if (!dst.items.empty()) {
const Item& di = dst.items[dst.sel];
if (di.isDir && !di.isUpEntry) {
_snprintf(srcDst, (int)cap, "%s", di.name); // e.g. "E:\"
srcDst[cap - 1] = 0;
NormalizeDirA(srcDst);
return true;
}
}
return false;
}
// Select an item in a pane by name and adjust scroll to reveal it.
void FileBrowserApp::SelectItemInPane(Pane& p, const char* name) {
if (!name || p.items.empty()) return;
for (int i=0;i<(int)p.items.size();++i){
if (_stricmp(p.items[i].name, name) == 0){
p.sel = i;
if (p.sel < p.scroll) p.scroll = p.sel;
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
return;
}
}
}
void FileBrowserApp::BuildZipSubMenu() {
m_zipSubMenu.SetLabel("Select destination");
Pane& p = m_pane[m_active];
Pane& p2 = m_pane[1 - m_active];
bool inDir = (p.mode == 1);
bool inDir2 = (p2.mode == 1);
bool hasSel = !p.items.empty();
bool isFile = false;
if (hasSel) {
const Item& cur = p.items[p.sel];
if (inDir && !cur.isUpEntry && !cur.isDir) isFile = true;
}
m_zipSubMenu.Clear();
m_zipSubMenu.AddItem("here", ACT_UNZIPHERE, (true));
char* offset = " ";
if (isFile) {
char buf[256] = "to ";
strcat(buf, "\"");
char name[64];
strcpy(name, p.items[p.sel].name);
name[strlen(name) - 4] = '\0';
strcat(buf, name);
strcat(buf, "\\\"");
strcat(buf, offset);
char unzipTo[256];
EllipsizeAnsiToFit(m_font, buf, m_zipSubMenu.m_Mw + GetAnsiW(m_font, offset), unzipTo, sizeof(unzipTo), ELLIPSIZE_CENTER);
unzipTo[strlen(unzipTo) - strlen(offset)] = '\0';
m_zipSubMenu.AddItem(unzipTo, ACT_UNZIPTO, (true));
}
if (isFile && inDir2) {
char buf[512] = "to ";
strcat(buf, "\"");
strcat(buf, p2.curPath);
if (buf[strlen(buf) - 1] != '\\') strcat(buf, "\\");
strcat(buf, "\"");
strcat(buf, offset);
char unzipTo[256];
EllipsizeAnsiToFit(m_font, buf, m_zipSubMenu.m_Mw + GetAnsiW(m_font, offset), unzipTo, sizeof(unzipTo), ELLIPSIZE_CENTER);
unzipTo[strlen(unzipTo) - strlen(offset)] = '\0';
m_zipSubMenu.AddItem(unzipTo, ACT_UNZIPTOOTHER, (true));
}
}
void FileBrowserApp::BuildConfirmDelSubMenu() {
m_confirmDelSubMenu.SetLabel("Are you sure?");
m_confirmDelSubMenu.Clear();
m_confirmDelSubMenu.AddItem("Yes", ACT_DELETE, (true));
m_confirmDelSubMenu.AddItem("No", ACT_CANCEL, (true));
}
// Build the context menu based on current mode and selection.
void FileBrowserApp::BuildContextMenu() {
m_ctx.SetLabel("Select action");
Pane& p = m_pane[m_active];
Pane& p2 = m_pane[1 - m_active];
bool inDir = (p.mode == 1);
bool inDir2 = (p2.mode == 1);
bool hasSel = !p.items.empty();
bool hasSel2 = !p2.items.empty();
bool isFile = false;
bool isFile2 = false;
const char* ext = NULL;
const char* ext2 = NULL;
if (hasSel){
const Item& cur = p.items[p.sel];
if (inDir && !cur.isUpEntry && !cur.isDir) isFile = true;
if (isFile) ext = GetExtension(cur.name);
}
if (hasSel2){
const Item& cur2 = p2.items[p2.sel];
if (inDir2 && !cur2.isUpEntry && !cur2.isDir) isFile2 = true;
if (isFile2) ext2 = GetExtension(cur2.name);
}
int marked = 0; for (size_t i=0; i
0) {
m_ctx.AddItem("Mark all", ACT_MARK_ALL, true);
m_ctx.AddItem("Invert marks", ACT_INVERT_MARKS, true);
}
}
if (marked) m_ctx.AddItem("Clear marks", ACT_CLEAR_MARKS, true);
// Bottom-only item on drive list: destructive cache format.
if (p.mode == 0) {
m_ctx.AddSeparator(); // visual separator (non-selectable)
m_ctx.AddItem("Format cache (X/Y/Z)", ACT_FORMAT_CACHE, true);
}
}
// Open and close the menu; switch mode so browse input pauses.
void FileBrowserApp::OpenMenu() {
BuildContextMenu();
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
const FLOAT rowH = kLineH + 6.0f;
const FLOAT desiredW = 340.0f; // usual width
const FLOAT safeInset = SafeMarginX((FLOAT)vp.Width, (FLOAT)vp.Height);
// Max width we can draw while keeping the frame fully inside the safe band
const FLOAT maxWInsideSafe = (FLOAT)vp.Width - safeInset*2.0f - 12.0f; // -12 for the outer frame
const FLOAT menuW = max(220.0f, min(desiredW, min(kListW, maxWInsideSafe)));
// Center over the active pane first...
const FLOAT paneX = (m_active==0) ? kListX_L : (kListX_L + kListW + kPaneGap);
FLOAT x = paneX + (kListW - menuW)*0.5f;
// ...then clamp so it never goes past the safe area
if (x < safeInset) x = safeInset;
if (x + menuW > (FLOAT)vp.Width - safeInset) x = (FLOAT)vp.Width - safeInset - menuW;
const FLOAT listTop = kListY + kLineH - 10.0f; // Why -10.0f?
const int visibleIndex = m_pane[m_active].sel - m_pane[m_active].scroll;
const FLOAT y = listTop + (visibleIndex - 1) * kLineH;
m_ctx.OpenAt(x, y, menuW, rowH);
m_menuStack[0] = &m_ctx;
m_menuDepth = 1;
m_mode = MODE_MENU;
}
void FileBrowserApp::CloseMenu() {
for (int i = 0; i < m_menuDepth; ++i) {
if (m_menuStack[i]) m_menuStack[i]->Close();
}
m_menuDepth = 0;
m_mode=MODE_BROWSE;
}
// ----- rename lifecycle (OnScreenKeyboard) ----------------------------------
// Start rename using the OSD keyboard. Close menu first so the A press
// that opened the keyboard does not trigger other UI.
void FileBrowserApp::BeginRename(const char* parentDir, const char* oldName) {
m_ctx.Close(); // ensure menu closes
m_kb.Open(parentDir ? parentDir : "", oldName ? oldName : "");
m_mode = MODE_RENAME;
}
void FileBrowserApp::CancelRename() {
m_kb.Close();
m_mode=MODE_BROWSE;
}
void FileBrowserApp::AcceptRename() {
const char* newName = m_kb.Buffer();
if (!newName) { CancelRename(); return; }
Pane& ap = m_pane[m_active];
if (ap.mode != 1 || ap.items.empty()) { SetStatus("Rename failed: no selection"); CancelRename(); return; }
const Item& sel = ap.items[ap.sel];
if (sel.isUpEntry) { SetStatus("Rename failed: invalid selection"); CancelRename(); return; }
// Sanitize to FATX-friendly, then MoveFile within current dir.
char clean[256]; _snprintf(clean, sizeof(clean), "%s", newName); clean[sizeof(clean)-1]=0;
SanitizeFatxNameInPlace(clean);
if (_stricmp(clean, sel.name)==0){ SetStatus("No change"); CancelRename(); return; }
char oldPath[512]; JoinPath(oldPath, sizeof(oldPath), ap.curPath, sel.name);
char newPath[512]; JoinPath(newPath, sizeof(newPath), ap.curPath, clean);
if (MoveFileA(oldPath, newPath)){
SetStatus("Renamed to %s", clean);
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
SelectItemInPane(ap, clean);
}else{
SetStatusLastErr("Rename failed");
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
}
CancelRename();
}
// Absorb the current pad state so the button used to accept/cancel the keyboard
// does not fall through and act in browse/menu mode.
void FileBrowserApp::AbsorbPadState(const XBGAMEPAD& pad) {
m_prevButtons = pad.wButtons;
m_prevA = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
m_prevB = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
m_prevX = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
m_prevY = pad.bAnalogButtons[XINPUT_GAMEPAD_Y];
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
}
// ----- input: rename modal --------------------------------------------------
// While the keyboard is active, route inputs to it only.
void FileBrowserApp::OnPad_Rename(const XBGAMEPAD& pad) {
OnScreenKeyboard::Result r = m_kb.OnPad(pad);
if (r == OnScreenKeyboard::ACCEPTED){
AbsorbPadState(pad);
AcceptRename();
return;
}
if (r == OnScreenKeyboard::CANCELED){
AbsorbPadState(pad);
CancelRename();
return;
}
// Still in keyboard: keep edge state roughly synced
AbsorbPadState(pad);
}
// ----- input: context menu (delegated) -------------------------------------
// Feed pad to the menu; on selection, execute via central AppActions.
void FileBrowserApp::OnPad_Menu(const XBGAMEPAD& pad) {
Action act;
ContextMenu* top = m_menuStack[m_menuDepth - 1];
ContextMenu::Result r = top->OnPad(pad, act);
if (r == ContextMenu::CHOSEN) {
if (act == ACT_CANCEL) r = ContextMenu::CLOSED;
else {
CloseMenu();
//SetStatus("Chosen action=%d", (int)act); // debug toast
Actions::Execute(act); // perform action
}
}
if (r == ContextMenu::CLOSED) { // allow fallthrough
if (m_menuDepth > 1) {
m_menuDepth--;
ContextMenu* parent = m_menuStack[m_menuDepth - 1];
parent->AbsorbPadState(pad);
} else {
m_menuDepth = 0;
CloseMenu();
}
} else if (r == ContextMenu::SUBMENU_OPENED) {
ContextMenu* parent = m_menuStack[m_menuDepth - 1];
ContextMenu* child = parent->GetSelectedChildMenu();
if (child) {
m_menuStack[m_menuDepth] = child;
m_menuDepth++;
}
}
// Sync edge state so the next browse frame does not double-fire.
m_prevButtons = pad.wButtons;
m_prevA = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
m_prevB = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
m_prevX = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
}
// ----- input: browse mode (with auto-repeat) -------------------------------
// Main file-list navigation. Handles dpad/analog, paging, selection,
// pane switching, and quick mark/unmark on Y.
void FileBrowserApp::OnPad_Browse(const XBGAMEPAD& pad) {
const DWORD btn = pad.wButtons;
Pane& p = m_pane[m_active];
// up/down auto-repeat with initial delay
const DWORD now = GetTickCount();
const int kDead = 16000; // analog deadzone for vertical stick
const DWORD kInit = 230; // initial delay (ms)
const DWORD kRep = 120; // repeat rate (ms)
// Compute desired move: -1 up, +1 down, 0 none.
int ud = 0;
if ((btn & XINPUT_GAMEPAD_DPAD_UP) || pad.sThumbLY > kDead) ud = -1;
if ((btn & XINPUT_GAMEPAD_DPAD_DOWN) || pad.sThumbLY < -kDead) ud = +1;
// Repeat-state machine for smooth scrolling.
if (ud == 0){
m_navUDHeld = false; m_navUDDir = 0;
}else{
if (!m_navUDHeld || m_navUDDir != ud){
// first step
if (ud < 0){
if (p.sel > 0){ --p.sel; if (p.sel < p.scroll) p.scroll = p.sel; }
}else{
int maxSel = (int)p.items.size() - 1;
if (p.sel < maxSel){ ++p.sel; if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1); }
}
m_navUDHeld = true; m_navUDDir = ud; m_navUDNext = now + kInit;
}else if (now >= m_navUDNext){
// repeats
if (ud < 0){
if (p.sel > 0){ --p.sel; if (p.sel < p.scroll) p.scroll = p.sel; }
}else{
int maxSel = (int)p.items.size() - 1;
if (p.sel < maxSel){ ++p.sel; if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1); }
}
m_navUDNext = now + kRep;
}
}
// pane switch via dpad left/right (edge-detected)
bool leftTrig = (btn & XINPUT_GAMEPAD_DPAD_LEFT) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_LEFT);
bool rightTrig = (btn & XINPUT_GAMEPAD_DPAD_RIGHT) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_RIGHT);
if (leftTrig) m_active = 0;
if (rightTrig) m_active = 1;
// Other buttons (edge-detected)
unsigned char a = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
unsigned char b = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
unsigned char x = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
unsigned char y = pad.bAnalogButtons[XINPUT_GAMEPAD_Y];
unsigned char w = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE]; // page down
unsigned char k = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK]; // page up
bool aTrig = (a > 30 && m_prevA <= 30);
bool bTrig = (b > 30 && m_prevB <= 30);
bool xTrig = (x > 30 && m_prevX <= 30);
bool yTrig = (y > 30 && m_prevY <= 30);
bool wTrig = (w > 30 && m_prevWhite <= 30);
bool kTrig = (k > 30 && m_prevBlack <= 30);
// Open context menu on X
if (xTrig) {
OpenMenu();
// absorb state so the X press does not leak into menu
m_prevButtons = btn;
m_prevA = a; m_prevB = b; m_prevX = x;
m_prevWhite = w; m_prevBlack = k;
return;
}
// A = enter, B = up one
if (aTrig) EnterSelection(p);
if (bTrig) UpOne(p);
// Black/White = page up/down
if (kTrig) { // BLACK: page up
p.sel -= m_visible; if (p.sel < 0) p.sel = 0;
if (p.sel < p.scroll) p.scroll = p.sel;
}
if (wTrig) { // WHITE: page down
int maxSel = (int)p.items.size()-1;
p.sel += m_visible; if (p.sel > maxSel) p.sel = maxSel;
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
}
// Y = quick toggle mark (skips the ".." entry)
if (yTrig) {
if (p.mode==1 && !p.items.empty()) {
Item& it = p.items[p.sel];
if (!it.isUpEntry) {
it.marked = !it.marked;
SetStatus(it.marked ? "Marked" : "Unmarked");
// Optional auto-advance to speed through marking multiple items.
int maxSel = (int)p.items.size()-1;
if (p.sel < maxSel) {
++p.sel;
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
}
}
}
}
// Save edge baselines for next frame.
m_prevButtons = btn;
m_prevA = a; m_prevB = b; m_prevX = x; m_prevY = y;
m_prevWhite = w; m_prevBlack = k;
}
// ----- input router ---------------------------------------------------------
// Route pad to sub-handlers based on current modal state.
void FileBrowserApp::OnPad(const XBGAMEPAD& pad) {
// --- Back-to-exit (press Back twice) -----------------------------------
const bool backNow = (pad.wButtons & XINPUT_GAMEPAD_BACK) != 0;
const bool backTrig = backNow && !(m_prevButtons & XINPUT_GAMEPAD_BACK);
DWORD now = GetTickCount();
if (backTrig) {
if (m_backConfirmArmed && now < m_statusUntilMs) {
ExitNow();
return;
}
else {
m_backConfirmArmed = true;
SetStatus("Press \x84 again to exit");
m_backConfirmUntil = m_statusUntilMs; // optional snapshot
}
}
if (m_backConfirmArmed && now >= m_statusUntilMs){
m_backConfirmArmed = false;
}
// -----------------------------------------------------------------------
if (m_mode == MODE_RENAME) { OnPad_Rename(pad); return; }
if (m_mode == MODE_MENU) { OnPad_Menu(pad); return; }
OnPad_Browse(pad);
}
// Per-frame app logic. Also poll for drive-set changes and refresh panes.
HRESULT FileBrowserApp::FrameMove() {
XBInput_GetInput();
// Check ip
if (network::isReady() == true)
{
XNADDR addr;
memset(&addr, 0, sizeof(addr));
DWORD dwState = XNetGetTitleXnAddr(&addr);
if (dwState != XNET_GET_XNADDR_PENDING)
{
char* ipAddress = (XNetGetEthernetLinkStatus() & XNET_ETHERNET_LINK_ACTIVE) ? stringUtility::formatString("%i.%i.%i.%i",
addr.ina.S_un.S_un_b.s_b1,
addr.ina.S_un.S_un_b.s_b2,
addr.ina.S_un.S_un_b.s_b3,
addr.ina.S_un.S_un_b.s_b4) : _strdup("0.0.0.0");
char* currentIp = GetCurrentIp();
if (strcmp(ipAddress, currentIp) != 0) SetCurrentIp(ipAddress);
}
}
// --- DVD tray/media polling + serial watchdog (no stale listings) ----------
static DWORD s_nextDvdPollMs = 0;
static DWORD s_lastDvdSerial = 0xFFFFFFFF; // unknown
static BOOL s_dMapped = FALSE; // our belief about D: mapping
DWORD now2 = GetTickCount();
if (now2 >= s_nextDvdPollMs) {
s_nextDvdPollMs = now2 + 250; // ~4 Hz
DWORD code = DvdGetDriveStateOneShot(); // DRIVE_* or DRIVE_READY (no change)
if (code != TRAY_NO_CHANGE) {
BOOL needRefresh = FALSE;
switch (code) {
case TRAY_OPEN:
case TRAY_CLOSED_NO_MEDIA:
s_dMapped = FALSE;
s_lastDvdSerial = 0xFFFFFFFF; // forget old serial
m_dvdHaveStats = false; // <— clear
m_dvdUsedBytes = 0;
m_dvdTotalBytes = 0;
needRefresh = TRUE;
//SetStatus((code == DRIVE_OPEN) ? "Tray opened" : "No disc");
break;
case TRAY_CLOSED_MEDIA_PRESENT:
s_dMapped = TRUE;
DWORD curSer = 0;
if (GetDvdVolumeSerial(&curSer)) s_lastDvdSerial = curSer;
// Cache total (capacity) and used (sum of files on disc)
ULONGLONG fb = 0, tb = 0;
GetDriveFreeTotal("DVD-ROM:\\", fb, tb);
m_dvdTotalBytes = tb;
m_dvdUsedBytes = DirSizeRecursiveA("DVD-ROM:\\"); // from FsUtil
m_dvdHaveStats = true;
//{ char lbl[64]; if (DvdDetectMediaSimple(lbl, sizeof(lbl))) SetStatus("%s", lbl); }
needRefresh = TRUE;
break;
}
if (needRefresh) {
// Refresh both panes; bounce out of D:\ if it vanished
for (int iPane = 0; iPane < 2; ++iPane) {
Pane& P = m_pane[iPane];
if (P.mode == 0) {
BuildDriveItems(P.items);
if (P.sel >= (int)P.items.size()) P.sel = (int)P.items.size()-1;
if (P.sel < 0) P.sel = 0;
P.scroll = 0;
} else if (IsDPath(P.curPath)) {
if (s_dMapped) {
// HARD re-list so a new disc shows correct files
ListDirectory(P.curPath, P.items);
if (P.sel >= (int)P.items.size()) P.sel = (int)P.items.size()-1;
if (P.sel < 0) P.sel = 0;
if (P.scroll > P.sel) P.scroll = P.sel;
{ int maxScroll = (int)P.items.size() - m_visible; if (maxScroll < 0) maxScroll = 0; if (P.scroll > maxScroll) P.scroll = maxScroll; }
} else {
// D: gone => drive list
P.mode = 0; P.curPath[0] = 0;
BuildDriveItems(P.items);
P.sel = 0; P.scroll = 0;
}
} else {
RefreshPane(P);
}
}
}
}
}
OnPad(g_Gamepads[0]);
return S_OK;
}
// ----- draw: menu / rename / panes -----------------------------------------
// Draw the context menu if open (coordinates set in OpenMenu).
void FileBrowserApp::DrawMenu() {
if (m_menuDepth <= 0) return;
for (int i = 0; i < m_menuDepth; ++i) {
ContextMenu* cm = m_menuStack[i];
if (cm && cm->IsOpen()) cm->Draw(m_font, m_pd3dDevice);
}
}
// Draw OSD keyboard if active.
void FileBrowserApp::DrawRename() {
if (!m_kb.Active()) return;
m_kb.Draw(m_font, m_pd3dDevice, kLineH);
}
// ----- listing / navigation -------------------------------------------------
// Ensure a pane has items and indices are in range (after mode/path changes).
void FileBrowserApp::EnsureListing(Pane& p) {
if (p.mode==0) BuildDriveItems(p.items);
else ListDirectory(p.curPath, p.items);
if (p.sel >= (int)p.items.size()) p.sel = (int)p.items.size()-1;
if (p.sel < 0) p.sel = 0;
if (p.scroll > p.sel) p.scroll = p.sel;
int maxScroll = max(0, (int)p.items.size() - m_visible);
if (p.scroll > maxScroll) p.scroll = maxScroll;
}
// Enter current selection (drive -> directory, ".." -> up, dir -> descend,
// .xbe -> launch). Other files are no-op here.
void FileBrowserApp::EnterSelection(Pane& p) {
if (p.items.empty()) return;
const Item& it = p.items[p.sel];
// Drive list -> go into the chosen drive.
if (p.mode==0) {
strncpy(p.curPath,it.name,sizeof(p.curPath)-1); p.curPath[sizeof(p.curPath)-1]=0;
p.mode=1; p.sel=0; p.scroll=0; ListDirectory(p.curPath,p.items); return;
}
// Directory listing
if (it.isUpEntry) {
char childName[256];
ExtractLastComponent(p.curPath, childName, sizeof(childName));
// If already at drive root -> go to drive list
if (IsDriveRoot(p.curPath)) {
p.mode = 0;
p.sel = 0;
p.scroll = 0;
p.curPath[0] = 0;
BuildDriveItems(p.items);
return;
}
// Otherwise go up one directory
ParentPath(p.curPath);
p.sel = 0;
p.scroll = 0;
ListDirectory(p.curPath, p.items);
SelectItemInPane(p, childName);
return;
}
if (it.isDir) {
// Descend into subdirectory.
char next[512]; JoinPath(next,sizeof(next),p.curPath,it.name);
strncpy(p.curPath,next,sizeof(p.curPath)-1); p.curPath[sizeof(p.curPath)-1]=0;
p.sel=0; p.scroll=0; ListDirectory(p.curPath,p.items); return;
}
// Files: launch .xbe if selected (other file types are no-op here).
if (!it.isDir && !it.isUpEntry) {
if (HasXbeExt(it.name)) {
char full[512];
JoinPath(full, sizeof(full), p.curPath, it.name);
// Small Present for a snappy visual handoff before XLaunchNewImageA.
m_pd3dDevice->Present(NULL, NULL, NULL, NULL);
Sleep(10);
if (!LaunchXbeA(full)) {
SetStatusLastErr("Launch failed");
}
}
return;
}
}
// Move up one level; from root goes back to drive list.
void FileBrowserApp::UpOne(Pane& p) {
// Already at drive list -> nothing to do
if (p.mode == 0) return;
// Save child name so parent can reselect it
char childName[256];
ExtractLastComponent(p.curPath, childName, sizeof(childName));
// --- CASE 1: We are at drive root (e.g. "E:\", "HDD0-C:\") ---
if (IsDriveRoot(p.curPath)) {
char driveRoot[512];
strncpy(driveRoot, p.curPath, sizeof(driveRoot) - 1);
driveRoot[sizeof(driveRoot) - 1] = 0;
// Switch to drive list
p.mode = 0;
p.sel = 0;
p.scroll = 0;
p.curPath[0] = 0;
BuildDriveItems(p.items);
// Reselect the drive we came from
for (int i = 0; i < (int)p.items.size(); ++i) {
if (_stricmp(p.items[i].name, driveRoot) == 0) {
p.sel = i;
break;
}
}
// Clamp scroll
if (p.sel < p.scroll) p.scroll = p.sel;
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
return;
}
// --- CASE 2: Normal directory -> go up one level ---
ParentPath(p.curPath);
p.sel = 0;
p.scroll = 0;
ListDirectory(p.curPath, p.items);
// Reselect folder we came from
SelectItemInPane(p, childName);
}
// ----- Initialize / Render --------------------------------------------------
// Create font, input, initial drive mapping, and compute responsive layout.
// ----------------------------------------------------------------------------
HRESULT FileBrowserApp::Initialize() {
if (FileExistsA(FONT_XPR_FILEPATH)) {
HRESULT hr = m_font.Create(FONT_XPR_FILEPATH, 0);
if (!SUCCEEDED(hr)) {
if (WriteAllA(FONT_XPR_FILEPATH, Font, (size_t)FontSize)) {
HRESULT hr2 = m_font.Create(FONT_XPR_FILEPATH, 0);
}
}
}
else {
if (WriteAllA(FONT_XPR_FILEPATH, Font, (size_t)FontSize)) {
HRESULT hr = m_font.Create(FONT_XPR_FILEPATH, 0);
}
}
XBInput_CreateGamepads();
network::init();
BuildDriveItems(m_pane[0].items);
BuildDriveItems(m_pane[1].items);
// Layout derived from current backbuffer size (works for any resolution)
ComputeResponsiveLayout();
m_ctx.SetDevice(m_pd3dDevice);
m_zipSubMenu.SetDevice(m_pd3dDevice);
m_confirmDelSubMenu.SetDevice(m_pd3dDevice);
SetStatus(INITIAL_TOAST);
return S_OK;
}
// ----- progress overlay API -------------------------------------------------
// Begin a copy/move progress session (title and first label for marquee).
void FileBrowserApp::BeginProgress(ULONGLONG total, const char* firstLabel, const char* title) {
m_prog.active = true;
m_prog.done = 0;
m_prog.total = total;
_snprintf(m_prog.current, sizeof(m_prog.current), "%s", firstLabel ? firstLabel : "");
m_prog.current[sizeof(m_prog.current)-1] = 0;
_snprintf(m_prog.label, sizeof(m_prog.label), "%s", title ? title : "Working...");
m_prog.label[sizeof(m_prog.label)-1] = 0;
m_prog.lastPaintMs = 0;
}
// Update the overlay counters and optional label; throttles repaint to ~25 fps.
void FileBrowserApp::UpdateProgress(ULONGLONG done, ULONGLONG total, const char* label) {
m_prog.done = done;
m_prog.total = (total ? total : m_prog.total);
if (label){
_snprintf(m_prog.current, sizeof(m_prog.current), "%s", label);
m_prog.current[sizeof(m_prog.current)-1] = 0;
}
DWORD now = GetTickCount();
if (now - m_prog.lastPaintMs >= 40){
m_prog.lastPaintMs = now;
// redraw one frame (use the normal Render path)
Render();
Sleep(0);
}
}
// End and clear the progress overlay state.
void FileBrowserApp::EndProgress() {
m_prog.active = false;
}
// Draw the modal progress HUD (centered panel with marquee and bar).
void FileBrowserApp::DrawProgressOverlay() {
if (!m_prog.active) return;
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
// Ensure filename line has room for about 42 glyphs at current font size
char fortyTwo[64]; for (int i=0;i<42;++i) fortyTwo[i]='W'; fortyTwo[42]=0;
FLOAT fileW=0, fileH=0; GetAnsiWH(m_font, fortyTwo, &fileW, &fileH);
const FLOAT margin = 18.0f;
FLOAT w = max(max(420.0f, vp.Width * 0.50f), fileW + margin*2.0f);
if (w > vp.Width - margin*2.0f) w = vp.Width - margin*2.0f;
const FLOAT h = 116.0f;
const FLOAT x = Snap((vp.Width - w)*0.5f);
const FLOAT y = Snap((vp.Height - h)*0.5f);
DrawSolidRect(m_pd3dDevice, x-6, y-6, w+12, h+12, 0xA0101010);
DrawSolidRect(m_pd3dDevice, x, y, w, h, 0xE0222222);
// layout lines
const FLOAT titleY = y + 10.0f;
const FLOAT folderY = titleY + 24.0f;
const FLOAT fileY = folderY + 22.0f;
const FLOAT barY = fileY + 26.0f;
const FLOAT barH = 20.0f;
const FLOAT barX = x + margin;
const FLOAT barW = w - margin*2.0f;
// title
DrawAnsi(m_font, x + margin, titleY, 0xFFFFFFFF, &DefaultColors, m_prog.label[0] ? m_prog.label : "Working...");
// hint (right) — red "B:" + gray "Cancel"
{
char* text = "\x81 Cancel";
FLOAT tw, th;
GetAnsiWH(m_font, text, &tw, &th);
const FLOAT startX = x + w - margin - tw;
DrawAnsi(m_font, startX, titleY, 0xFFCCCCCC, &DefaultColors, text);
}
// Split current label into folder + file parts
const char* label = m_prog.current;
const char* slash = label ? strrchr(label, '\\') : NULL;
char folder[512] = {0};
char file [256] = {0};
if (slash){
size_t n = (size_t)(slash - label + 1);
if (n >= sizeof(folder)) n = sizeof(folder)-1;
memcpy(folder, label, n); folder[n] = 0;
_snprintf(file, sizeof(file), "%s", slash + 1); file[sizeof(file)-1]=0;
}
else {
_snprintf(file, sizeof(file), "%s", label ? label : ""); file[sizeof(file)-1]=0;
folder[0] = 0;
}
// --- character-step marquees (like panes/OSK) ----------------------------
static ProgMarquee s_marqFolder;
static ProgMarquee s_marqFile;
// If a new operation just started, clear state so we get the initial pause.
if (m_prog.done == 0) { s_marqFolder = ProgMarquee(); s_marqFile = ProgMarquee(); }
DrawLabelFittedOrMarquee(m_font, x + margin, folderY, barW, 0xFF5EA4FF, folder, s_marqFolder);
DrawLabelFittedOrMarquee(m_font, x + margin, fileY, barW, 0xFF89D07E, file, s_marqFile);
// progress percent and bar
FLOAT pct = 0.0f;
if (m_prog.total > 0){
pct = (FLOAT)((double)m_prog.done / (double)m_prog.total);
if (pct < 0) pct = 0; if (pct > 1) pct = 1;
}
DrawSolidRect(m_pd3dDevice, barX, barY, barW, barH, 0xFF0E0E0E);
DrawSolidRect(m_pd3dDevice, barX, barY, barW * pct, barH, 0x90FFFF00);
char t[32];
_snprintf(t, sizeof(t), "%u%%", (unsigned int)(pct * 100.0f + 0.5f));
t[sizeof(t) - 1] = 0;
DrawAnsiFromRight(m_font, barX + barW - 4.0f, barY, 0xFFEEEEEE, &DefaultColors, t, barH);
}
// ----- main render ----------------------------------------------------------
// Draw both panes, footer and hints, transient status, and any overlays.
HRESULT FileBrowserApp::Render() {
m_pd3dDevice->Clear(0,NULL,D3DCLEAR_TARGET,0x20202020,1.0f,0);
m_pd3dDevice->BeginScene();
m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
m_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
m_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
// Recompute layout if resolution changes on the fly (safe & cheap)
ComputeResponsiveLayout();
// ---- panes ----
PaneStyle st;
st.listW = kListW;
st.listY = kListY;
st.lineH = kLineH;
st.hdrY = kHdrY;
st.hdrH = kHdrH;
st.hdrW = kHdrW; // header exactly equals pane width
st.gutterW = kGutterW;
st.paddingX = kPaddingX;
st.scrollBarW = kScrollBarW;
st.visibleRows = m_visible;
// ---- panes ----
m_renderer.ResetSharedSizeColW(); // reset per frame
m_renderer.PrimeSharedSizeColW(m_font, m_pane[0], st); // prime from left
m_renderer.PrimeSharedSizeColW(m_font, m_pane[1], st); // prime from right
// Symmetric base X for panes: [margin] [left] [gap] [right] [margin]
const FLOAT leftX = kListX_L;
const FLOAT rightX = kListX_L + kListW + kPaneGap;
// Draw in order (left then right)
m_renderer.DrawPane(m_font, m_pd3dDevice, leftX, m_pane[0], m_active==0, st, 0);
m_renderer.DrawPane(m_font, m_pd3dDevice, rightX, m_pane[1], m_active==1, st, 1);
// ---- footer / hints ----
D3DVIEWPORT8 vp2; m_pd3dDevice->GetViewport(&vp2);
// Footer matches the combined pane area (two panes + gap), centered.
const FLOAT footerMargin = SafeMarginX((FLOAT)vp2.Width, (FLOAT)vp2.Height);
const FLOAT footerW = min(kHdrW * 2.0f + kPaneGap, (FLOAT)vp2.Width - footerMargin * 2.0f);
const FLOAT footerX = floorf(((FLOAT)vp2.Width - footerW) * 0.5f);
const FLOAT footerY = (FLOAT)vp2.Height - max(52.0f, (FLOAT)vp2.Height * 0.09f); // <-- unified
const FLOAT footerH = 28.0f;
// footer bar
DrawSolidRect(m_pd3dDevice, footerX, footerY, footerW, 28.0f, 0x802A2A2A);
const Pane& ap = m_pane[m_active];
const Item* cur = (ap.items.empty()? NULL : &ap.items[ap.sel]);
const char* yLab = (cur && !cur->isUpEntry && cur->marked) ? "Unmark" : "Mark";
const bool isLowRes = (vp2.Height < 700); // 480i/p or 576i count as "small"
const bool smallFooter = (isLowRes || footerW <= 620.0f);
if (m_pane[m_active].mode == 0) {
const char* hintsVerbose = "\x8A Move | \x89 Switch pane | \x80 Enter | \x82 Menu | \x87 / \x86 Page";
const char* hintsCompact = "\x8A Move | \x89 Pane | \x80 Enter | \x82 Menu | \x87 / \x86 Pg";
const char* base = smallFooter ? hintsCompact : hintsVerbose;
DrawAnsiCentered(m_font, footerX, footerY, 0xFFCCCCCC, &DefaultColors, base, footerW, footerH);
}
else {
const char* curPath = m_pane[m_active].curPath;
char leftLabel[16] = "Free";
ULONGLONG leftVal = 0, rightVal = 0;
if (IsDPath(curPath) && m_dvdHaveStats) {
strcpy(leftLabel, "Size");
leftVal = m_dvdUsedBytes;
rightVal = m_dvdTotalBytes;
}
else {
ULONGLONG fb=0, tb=0; GetDriveFreeTotal(curPath, fb, tb);
leftVal = fb; rightVal = tb;
}
char leftStr[64], rightStr[64];
FormatSize(leftVal, leftStr, sizeof(leftStr));
FormatSize(rightVal, rightStr, sizeof(rightStr));
char bar[420];
if (smallFooter) {
_snprintf(bar, sizeof(bar),
"\x81 Up | %s: %s / %s | \x82 Menu | \x83 %s | Pg \x87 / \x86", leftLabel, leftStr, rightStr, yLab);
}
else {
_snprintf(bar, sizeof(bar),
"\x81 Up | %s: %s / Total: %s | \x82 Menu | \x83 %s | \x87 / \x86 Page", leftLabel, leftStr, rightStr, yLab);
}
bar[sizeof(bar)-1] = 0;
DrawAnsiCentered(m_font, footerX, footerY, 0xFFCCCCCC, &DefaultColors, bar, footerW, 28.0f);
}
// ---- status toast (also centered and fitted) ----
DWORD now = GetTickCount();
FLOAT toastX = footerX + 5.0f, toastY = footerY + footerH + 2.0f, toastW = footerW - 10.0f;
FLOAT rx = DrawAnsiFromRight(m_font, toastX + toastW, toastY, 0xFFBBDDEE, NULL, FileBrowserApp::Get().GetCurrentIp());
rx = DrawAnsiFromRight(m_font, rx, toastY, 0x60BBDDEE, NULL, "IP: ");
FLOAT lx = DrawAnsi(m_font, toastX, toastY, 0x60BBDDEE, NULL, "Status: ");
if (now < m_statusUntilMs && m_status[0]) {
char fitted[256];
EllipsizeAnsiToFit(m_font, m_status, (rx - lx) - 5.0f, fitted, sizeof(fitted), ELLIPSIZE_LEFT);
DrawAnsi(m_font, lx, toastY, 0xFFBBDDEE, NULL, fitted);
}
// ---- overlays ----
DrawMenu();
DrawRename();
DrawProgressOverlay();
m_pd3dDevice->EndScene();
m_pd3dDevice->Present(NULL,NULL,NULL,NULL);
return S_OK;
}
int __cdecl main()
{
FileBrowserApp& app = FileBrowserApp::Get();
app.Create();
return app.Run();
}