Files
FileManager/FileBrowserApp.cpp
T

1486 lines
55 KiB
C++
Raw Permalink Normal View History

2025-08-30 00:45:12 +10:00
#include "FileBrowserApp.h"
2025-08-30 02:48:10 +10:00
#include "AppActions.h"
2025-08-30 03:06:35 +10:00
#include "GfxPrims.h"
2025-08-31 15:46:31 +10:00
#include "FsUtil.h"
2025-08-30 00:45:12 +10:00
#include <wchar.h>
#include <stdarg.h>
#include <algorithm>
#include <stdio.h> // _snprintf
2025-09-09 18:23:51 +10:00
#include <xgraphics.h>
2025-09-22 21:40:35 +10:00
#include "font_xpr.h"
#include "DebugPrint.h"
2025-09-02 14:26:19 +10:00
/*
===============================================================================
FileBrowserApp
- Main application controller for the dual-pane file browser.
- Owns input routing, pane state, context menu, OSD keyboard, progress HUD,
and the high-level render loop.
- Keeps allocations minimal and uses ACP->UTF-16 conversions for fonts.
===============================================================================
*/
// Simple getter used by overlay/status timers.
DWORD FileBrowserApp::StatusUntilMs() const { return m_statusUntilMs; }
2025-09-02 14:26:19 +10:00
// ---- local helpers (no header pollution) -----------------------------------
// These helpers are file-local to avoid leaking symbols into headers/other TUs.
2025-08-30 00:45:12 +10:00
namespace {
2025-09-02 14:26:19 +10:00
// Small math/layout helpers
2025-08-30 00:45:12 +10:00
inline FLOAT MaxF(FLOAT a, FLOAT b){ return (a>b)?a:b; }
2025-09-09 18:23:51 +10:00
inline FLOAT MinF(FLOAT a, FLOAT b) { return (a < b) ? a : b; }
2025-08-30 00:45:12 +10:00
inline int MaxI (int a, int b){ return (a>b)?a:b; }
2025-09-02 14:26:19 +10:00
inline FLOAT Snap (FLOAT v){ return (FLOAT)((int)(v + 0.5f)); } // pixel-align
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Draw ANSI string via CXBFont (expects UTF-16); convert on the fly.
2025-08-30 00:45:12 +10:00
inline void 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);
2025-09-09 20:04:34 +10:00
}
2025-09-22 21:40:35 +10:00
static bool FileExistsA(const char* path){
DWORD a = GetFileAttributesA(path);
return (a != INVALID_FILE_ATTRIBUTES) && !(a & FILE_ATTRIBUTE_DIRECTORY);
}
static bool WriteAllA(const char* path, const void* data, DWORD size){
HANDLE h = CreateFileA(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return false;
DWORD wrote = 0; BOOL ok = WriteFile(h, data, size, &wrote, NULL); CloseHandle(h);
return ok && wrote == size;
}
2025-09-09 20:04:34 +10:00
// Footer geometry used everywhere
inline FLOAT FooterBandPx(FLOAT screenH) { return MaxF(48.0f, screenH * 0.09f); }
// Always keep a little gap above the footer so rows don't "kiss" it.
inline FLOAT FooterSpacerPx(FLOAT screenH) { return MaxF(6.0f, screenH * 0.012f); }
2025-08-31 15:46:31 +10:00
2025-09-09 20:01:36 +10:00
// ---- 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;
2025-09-09 20:40:57 +10:00
// 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 MaxF(24.0f, screenW * pct); // never smaller than 24px
}
2025-09-09 20:01:36 +10:00
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;
// Measure full once
WCHAR wfull[768]; MultiByteToWideChar(CP_ACP, 0, text, -1, wfull, 768);
FLOAT fullW=0, fullH=0; font.GetTextExtent(wfull, &fullW, &fullH);
// If it fits, draw + reset
if (fullW <= fitW_now + kTol){
font.DrawText((FLOAT)((int)(x+0.5f)), (FLOAT)((int)(y+0.5f)), color, wfull, 0, 0.0f);
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;
WCHAR wtmp[768]; MultiByteToWideChar(CP_ACP, 0, s + mid, -1, wtmp, 768);
FLOAT tw=0, th=0; font.GetTextExtent(wtmp, &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);
WCHAR wtmp[768]; MultiByteToWideChar(CP_ACP, 0, tmp, -1, wtmp, 768);
FLOAT tw=0, th=0; font.GetTextExtent(wtmp, &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;
WCHAR wvis[768]; MultiByteToWideChar(CP_ACP, 0, vis, -1, wvis, 768);
font.DrawText((FLOAT)((int)(x+0.5f)), (FLOAT)((int)(y+0.5f)), color, wvis, 0, 0.0f);
// 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;
}
}
}
}
2025-09-02 14:26:19 +10:00
// Center a one-line ANSI string horizontally in a span.
2025-08-31 15:46:31 +10:00
inline void DrawAnsiCenteredX(CXBFont& font, FLOAT left, FLOAT width, FLOAT y, DWORD color, const char* text){
WCHAR wbuf[512];
MultiByteToWideChar(CP_ACP,0,text,-1,wbuf,512);
FLOAT tw=0, th=0; font.GetTextExtent(wbuf, &tw, &th);
const FLOAT x = Snap(left + (width - tw) * 0.5f);
font.DrawText(x, y, color, wbuf, 0, 0.0f);
}
2025-09-02 14:26:19 +10:00
// Measure ANSI string (after ACP->UTF16 conversion).
inline void MeasureTextWH(CXBFont& font, const char* s, FLOAT& outW, FLOAT& outH){
WCHAR wbuf[512];
MultiByteToWideChar(CP_ACP, 0, s ? s : "", -1, wbuf, 512);
font.GetTextExtent(wbuf, &outW, &outH);
}
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// Ellipsize the left side to fit maxW, keeping the tail (useful for paths).
inline void LeftEllipsizeToFit(CXBFont& font, const char* src, FLOAT maxW,
char* out, size_t cap){
if (!src) { out[0]=0; return; }
FLOAT w=0,h=0; MeasureTextWH(font, src, w, h);
if (w <= maxW){ _snprintf(out, (int)cap, "%s", src); out[cap-1]=0; return; }
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
const char* tail = src + strlen(src);
const char* p = tail;
char buf[1024]; // temp
// Walk leftward until "...<suffix>" fits.
for (;;){
if (p == src) break;
--p;
_snprintf(buf, sizeof(buf), "...%s", p);
MeasureTextWH(font, buf, w, h);
if (w > maxW){
++p; // last good char is one ahead
break;
}
}
_snprintf(out, (int)cap, "...%s", p);
out[cap-1]=0;
}
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// Marquee helpers for progress overlay
inline void MbToW(const char* s, WCHAR* w, int capW){
MultiByteToWideChar(CP_ACP, 0, s ? s : "", -1, w, capW);
}
2025-08-31 17:16:38 +10:00
2025-09-02 14:26:19 +10:00
// 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;
WCHAR wtmp[256]; 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);
MbToW(tmp, wtmp, 256); font.GetTextExtent(wtmp, &tw, &th);
if (tw >= px) break;
++p;
}
skippedW = tw;
return p;
}
2025-08-31 15:46:31 +10:00
2025-09-02 17:03:03 +10:00
// 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; }
}
2025-09-09 20:01:36 +10:00
2025-09-02 14:26:19 +10:00
} // anonymous namespace
2025-08-30 00:45:12 +10:00
2025-09-09 18:23:51 +10:00
// ----------------------------------------------------------------------------
// 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);
2025-09-09 20:04:34 +10:00
// ---- outer geometry ----
2025-09-09 20:40:57 +10:00
const FLOAT margin = SafeMarginX((FLOAT)vp.Width, (FLOAT)vp.Height);
const FLOAT gap = MaxF(24.0f, vp.Width * 0.035f);
const FLOAT paneW = MaxF(260.0f, (vp.Width - (margin * 2.0f) - gap) * 0.5f);
2025-09-09 18:23:51 +10:00
kPaneGap = gap;
2025-09-09 20:04:34 +10:00
kListX_L = margin;
kListW = paneW;
2025-09-09 18:23:51 +10:00
2025-09-09 20:04:34 +10:00
// header band
kHdrW = kListW;
2025-09-09 18:23:51 +10:00
kHdrY = MaxF(12.0f, vp.Height * 0.03f);
kHdrH = MaxF(22.0f, vp.Height * 0.04f);
2025-09-09 20:04:34 +10:00
// row height
2025-09-09 18:23:51 +10:00
kLineH = MaxF(22.0f, vp.Height * 0.036f);
2025-09-09 20:04:34 +10:00
// keep kListY as the "pane body top" (what the rest of the code expects)
const FLOAT headerGap = MaxF(6.0f, kHdrH * 0.35f);
kListY = kHdrY + kHdrH + headerGap;
// ---- visible rows (match PaneRenderer::DrawPane vertical math) ----
// DrawPane uses: listTop = (kListY) + colHdrH
const FLOAT colHdrH = MaxF(22.0f, kLineH);
const FLOAT listTopInRenderer = kListY + colHdrH;
// leave room for the footer band + a small spacer at every resolution
const FLOAT footerBand = MaxF(48.0f, vp.Height * 0.09f);
const FLOAT footerSpacer = MaxF(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
2025-09-09 18:23:51 +10:00
kGutterW = 18.0f;
kPaddingX = 6.0f;
kScrollBarW = 3.0f;
}
2025-09-02 14:26:19 +10:00
// ----------------------------------------------------------------------------
// ctor: set defaults for input, layout, D3D, and app state
2025-08-30 00:45:12 +10:00
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;
2025-09-06 21:54:55 +10:00
m_navUDHeld=false; m_navUDDir=0; m_navUDNext=0; m_backConfirmArmed = false;
m_backConfirmUntil = 0; m_prevBack = 0;
m_dvdUsedBytes = 0;
2025-09-06 16:05:50 +10:00
m_dvdTotalBytes = 0;
m_dvdHaveStats = false;
2025-08-30 00:45:12 +10:00
2025-09-06 21:54:55 +10:00
// --- Auto-detect video capabilities and set PresentParams ----------------
2025-09-09 18:23:51 +10:00
ZeroMemory(&m_d3dpp, sizeof(m_d3dpp));
2025-09-06 21:54:55 +10:00
2025-09-09 18:23:51 +10:00
// Dashboard video flags and TV standard
const DWORD vf = XGetVideoFlags();
const DWORD vs = XGetVideoStandard(); // XC_VIDEO_STANDARD_*
2025-09-06 21:54:55 +10:00
2025-09-09 18:23:51 +10:00
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;
2025-09-06 21:54:55 +10:00
2025-09-09 18:23:51 +10:00
// 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);
2025-09-06 21:54:55 +10:00
2025-09-09 18:23:51 +10:00
// 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
}
2025-09-06 21:54:55 +10:00
2025-09-09 18:23:51 +10:00
// Common params
m_d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
m_d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
m_d3dpp.EnableAutoDepthStencil = TRUE;
m_d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8;
// -------------------------------------------------------------------------
2025-08-30 00:45:12 +10:00
2025-08-30 01:23:50 +10:00
m_mode=MODE_BROWSE;
2025-08-30 00:45:12 +10:00
m_status[0]=0; m_statusUntilMs=0;
2025-09-06 21:54:55 +10:00
m_dvdUsedBytes = 0;
2025-09-06 16:05:50 +10:00
m_dvdTotalBytes = 0;
2025-09-06 21:54:55 +10:00
m_dvdHaveStats = 0;
2025-08-30 00:45:12 +10:00
}
2025-09-06 21:54:55 +10:00
2025-09-09 18:23:51 +10:00
2025-09-02 14:26:19 +10:00
// ----- draw prims ------------------------------------------------------------
// Small wrapper to match project primitive helpers.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::DrawRect(float x,float y,float w,float h,D3DCOLOR c){
2025-08-30 03:06:35 +10:00
DrawSolidRect(m_pd3dDevice, x, y, w, h, c);
2025-08-30 00:45:12 +10:00
}
2025-09-02 15:04:29 +10:00
// --- Exit helper ------------------------------------------------------------
// Jump to dashboard via XLaunchNewImage (fallback)
void FileBrowserApp::ExitNow(){
XLaunchNewImage(NULL, NULL); // returns to dashboard
}
2025-09-02 14:26:19 +10:00
// ----- status ---------------------------------------------------------------
// Set a 3s status toast at the footer (printf-style).
2025-08-30 00:45:12 +10:00
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;
2025-09-02 14:26:19 +10:00
m_statusUntilMs = GetTickCount() + 3000; // show for about 3s
2025-08-30 00:45:12 +10:00
}
2025-09-02 14:26:19 +10:00
// Append the last Win32 error to a prefix and show as status.
2025-08-30 00:45:12 +10:00
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);
}
2025-09-02 14:26:19 +10:00
// ----- listing helpers ------------------------------------------------------
// Refresh a pane while trying to preserve selection and scroll.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::RefreshPane(Pane& p){
if (p.mode==1){
int prevSel = p.sel;
int prevScroll= p.scroll;
ListDirectory(p.curPath, p.items);
if (prevSel >= (int)p.items.size()) prevSel = (int)p.items.size()-1;
if (prevSel < 0) prevSel = 0;
p.sel = prevSel;
int maxScroll = MaxI(0, (int)p.items.size() - m_visible);
if (prevScroll > maxScroll) prevScroll = maxScroll;
if (prevScroll < 0) prevScroll = 0;
p.scroll = prevScroll;
} else {
2025-09-02 14:26:19 +10:00
// Drive list mode: re-enumerate mounted roots
2025-08-30 00:45:12 +10:00
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;
}
}
2025-09-02 14:26:19 +10:00
// Resolve the destination directory for copy/move based on the other pane.
// Returns normalized path with trailing slash in outDst on success.
2025-08-30 00:45:12 +10:00
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;
}
2025-09-02 14:26:19 +10:00
// If the other pane is the drive list, allow selecting a drive as destination.
2025-08-30 00:45:12 +10:00
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;
}
2025-11-26 16:13:00 -08:00
// 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;
}
2025-09-02 14:26:19 +10:00
// Select an item in a pane by name and adjust scroll to reveal it.
2025-08-30 00:45:12 +10:00
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;
}
}
}
2025-09-02 14:26:19 +10:00
// ----- context menu (delegated) ---------------------------------------------
// Pass-through to the ContextMenu component.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::AddMenuItem(const char* label, Action act, bool enabled){
2025-08-30 01:23:50 +10:00
m_ctx.AddItem(label, act, enabled);
2025-08-30 00:45:12 +10:00
}
2025-09-02 14:26:19 +10:00
// Build the context menu based on current mode and selection.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::BuildContextMenu(){
2025-11-24 02:19:47 -08:00
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 (isFile) ext2 = GetExtension(cur2.name);
}
2025-09-02 14:26:19 +10:00
int marked = 0; for (size_t i=0; i<p.items.size(); ++i) if (p.items[i].marked) ++marked;
2025-08-30 00:45:12 +10:00
2025-08-30 01:23:50 +10:00
m_ctx.Clear();
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// Common operations
2025-11-24 02:19:47 -08:00
if (ext && _stricmp(ext, "xbe") == 0)
AddMenuItem("Launch", ACT_OPEN, (hasSel));
else
AddMenuItem("Open", ACT_OPEN, (hasSel));
AddMenuItem("Copy", ACT_COPY, (inDir && hasSel && inDir2));
AddMenuItem("Move", ACT_MOVE, (inDir && hasSel && inDir2));
AddMenuItem("Delete", ACT_DELETE, (inDir && hasSel));
AddMenuItem("Rename", ACT_RENAME, (inDir && hasSel));
if (ext && _stricmp(ext, "ips") == 0)
AddMenuItem("Apply ips", ACT_APPLYIPS, (ext2 && _stricmp(ext2, "xbe") == 0));
if (ext && _stricmp(ext, "xbe") == 0)
AddMenuItem("Create bak", ACT_CREATEBAK, (true));
if (ext && _stricmp(ext, "bak") == 0)
AddMenuItem("Restore bak", ACT_RESTOREBAK, (true));
2025-11-26 16:13:00 -08:00
if (ext && _stricmp(ext, "zip") == 0)
AddMenuItem("Unzip here", ACT_UNZIPHERE, (true));
if (ext && _stricmp(ext, "zip") == 0)
AddMenuItem("Unzip to..", ACT_UNZIPTO, (inDir2));
2025-11-24 02:19:47 -08:00
AddMenuItem("Make new folder", ACT_MKDIR, (inDir));
AddMenuItem("Calculate size", ACT_CALCSIZE, (hasSel));
AddMenuItem("Go to root", ACT_GOROOT, (inDir));
AddMenuItem("Switch pane", ACT_SWITCHMEDIA, (hasSel));
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// Marking tools (directory mode only; skip the ".." row)
2025-08-31 15:46:31 +10:00
if (inDir) {
int selectable = 0;
for (size_t i=0;i<p.items.size(); ++i) if (!p.items[i].isUpEntry) ++selectable;
if (selectable > 0) {
2025-09-02 14:26:19 +10:00
AddMenuItem("Mark all", ACT_MARK_ALL, true);
AddMenuItem("Invert marks", ACT_INVERT_MARKS, true);
2025-08-31 15:46:31 +10:00
}
}
if (marked) AddMenuItem("Clear marks", ACT_CLEAR_MARKS, true);
2025-09-02 14:26:19 +10:00
// Bottom-only item on drive list: destructive cache format.
if (p.mode == 0) {
m_ctx.AddSeparator(); // visual separator (non-selectable)
AddMenuItem("Format cache (X/Y/Z)", ACT_FORMAT_CACHE, true);
}
2025-08-30 00:45:12 +10:00
}
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// Open and close the menu; switch mode so browse input pauses.
2025-08-30 01:23:50 +10:00
void FileBrowserApp::OpenMenu(){
BuildContextMenu();
2025-08-30 00:45:12 +10:00
2025-09-09 20:40:57 +10:00
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
2025-08-30 01:23:50 +10:00
2025-09-09 20:40:57 +10:00
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 = MaxF(220.0f, MinF(desiredW, MinF(kListW, maxWInsideSafe)));
// Center over the active pane first...
2025-08-30 01:23:50 +10:00
const FLOAT paneX = (m_active==0) ? kListX_L : (kListX_L + kListW + kPaneGap);
2025-09-09 20:40:57 +10:00
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;
// A bit below the list header (vertical fit is fine; menu is much shorter than the pane)
2025-08-30 01:23:50 +10:00
const FLOAT y = kListY + 20.0f;
m_ctx.OpenAt(x, y, menuW, rowH);
2025-09-09 20:40:57 +10:00
m_mode = MODE_MENU;
2025-08-30 01:23:50 +10:00
}
2025-09-09 20:40:57 +10:00
2025-08-30 01:23:50 +10:00
void FileBrowserApp::CloseMenu(){
m_ctx.Close();
if (m_mode==MODE_MENU) m_mode=MODE_BROWSE;
}
2025-09-02 14:26:19 +10:00
// ----- 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.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::BeginRename(const char* parentDir, const char* oldName){
2025-08-30 01:23:50 +10:00
m_ctx.Close(); // ensure menu closes
2025-08-30 00:45:12 +10:00
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; }
2025-09-02 14:26:19 +10:00
// Sanitize to FATX-friendly, then MoveFile within current dir.
2025-08-30 00:45:12 +10:00
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]);
2025-08-30 01:23:50 +10:00
SelectItemInPane(ap, clean);
2025-08-30 00:45:12 +10:00
}else{
SetStatusLastErr("Rename failed");
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
}
CancelRename();
}
2025-09-02 14:26:19 +10:00
// Absorb the current pad state so the button used to accept/cancel the keyboard
// does not fall through and act in browse/menu mode.
2025-08-30 02:48:10 +10:00
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];
}
2025-09-02 14:26:19 +10:00
// ----- input: rename modal --------------------------------------------------
// While the keyboard is active, route inputs to it only.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::OnPad_Rename(const XBGAMEPAD& pad){
OnScreenKeyboard::Result r = m_kb.OnPad(pad);
2025-08-30 02:48:10 +10:00
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);
2025-08-30 00:45:12 +10:00
}
2025-08-30 02:48:10 +10:00
2025-09-02 14:26:19 +10:00
// ----- input: context menu (delegated) -------------------------------------
// Feed pad to the menu; on selection, execute via central AppActions.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::OnPad_Menu(const XBGAMEPAD& pad){
2025-08-30 01:23:50 +10:00
Action act;
ContextMenu::Result r = m_ctx.OnPad(pad, act);
if (r == ContextMenu::CHOSEN){
2025-09-02 17:46:21 +10:00
//SetStatus("Chosen action=%d", (int)act); // debug toast
2025-09-02 14:26:19 +10:00
AppActions::Execute(act, *this); // perform action
2025-08-30 01:23:50 +10:00
CloseMenu();
} else if (r == ContextMenu::CLOSED){
2025-08-30 00:45:12 +10:00
CloseMenu();
}
2025-09-02 14:26:19 +10:00
// Sync edge state so the next browse frame does not double-fire.
2025-08-30 01:23:50 +10:00
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];
2025-08-30 00:45:12 +10:00
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
}
2025-09-02 14:26:19 +10:00
// ----- input: browse mode (with auto-repeat) -------------------------------
// Main file-list navigation. Handles dpad/analog, paging, selection,
// pane switching, and quick mark/unmark on Y.
2025-08-30 00:45:12 +10:00
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();
2025-09-02 14:26:19 +10:00
const int kDead = 16000; // analog deadzone for vertical stick
const DWORD kInit = 230; // initial delay (ms)
const DWORD kRep = 120; // repeat rate (ms)
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Compute desired move: -1 up, +1 down, 0 none.
2025-08-30 00:45:12 +10:00
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;
2025-09-02 14:26:19 +10:00
// Repeat-state machine for smooth scrolling.
2025-08-30 00:45:12 +10:00
if (ud == 0){
m_navUDHeld = false; m_navUDDir = 0;
}else{
if (!m_navUDHeld || m_navUDDir != ud){
2025-09-02 14:26:19 +10:00
// first step
2025-08-30 00:45:12 +10:00
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){
2025-09-02 14:26:19 +10:00
// repeats
2025-08-30 00:45:12 +10:00
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;
}
}
2025-09-02 14:26:19 +10:00
// pane switch via dpad left/right (edge-detected)
2025-08-30 00:45:12 +10:00
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;
2025-09-02 14:26:19 +10:00
// Other buttons (edge-detected)
2025-08-30 00:45:12 +10:00
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
unsigned char y = pad.bAnalogButtons[XINPUT_GAMEPAD_Y];
2025-08-30 00:45:12 +10:00
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);
2025-09-02 14:26:19 +10:00
bool yTrig = (y > 30 && m_prevY <= 30);
2025-08-30 00:45:12 +10:00
bool wTrig = (w > 30 && m_prevWhite <= 30);
bool kTrig = (k > 30 && m_prevBlack <= 30);
2025-09-02 14:26:19 +10:00
// Open context menu on X
2025-08-30 03:06:35 +10:00
if (xTrig) {
2025-09-02 14:26:19 +10:00
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;
}
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// A = enter, B = up one
2025-08-30 00:45:12 +10:00
if (aTrig) EnterSelection(p);
if (bTrig) UpOne(p);
2025-09-02 14:26:19 +10:00
// Black/White = page up/down
2025-08-30 00:45:12 +10:00
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);
}
2025-09-02 14:26:19 +10:00
// 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");
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// 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);
}
2025-08-31 15:46:31 +10:00
}
}
2025-08-30 00:45:12 +10:00
}
2025-09-02 14:26:19 +10:00
// Save edge baselines for next frame.
2025-08-30 00:45:12 +10:00
m_prevButtons = btn;
2025-08-31 15:46:31 +10:00
m_prevA = a; m_prevB = b; m_prevX = x; m_prevY = y;
2025-08-30 00:45:12 +10:00
m_prevWhite = w; m_prevBlack = k;
}
2025-09-02 14:26:19 +10:00
// ----- input router ---------------------------------------------------------
// Route pad to sub-handlers based on current modal state.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::OnPad(const XBGAMEPAD& pad){
2025-09-02 15:04:29 +10:00
// --- 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 Back again to exit");
m_backConfirmUntil = m_statusUntilMs; // optional snapshot
}
}
if (m_backConfirmArmed && now >= m_statusUntilMs){
m_backConfirmArmed = false;
}
// -----------------------------------------------------------------------
2025-08-30 00:45:12 +10:00
if (m_mode == MODE_RENAME){ OnPad_Rename(pad); return; }
if (m_mode == MODE_MENU) { OnPad_Menu(pad); return; }
OnPad_Browse(pad);
}
2025-09-02 15:04:29 +10:00
2025-09-02 14:26:19 +10:00
// Per-frame app logic. Also poll for drive-set changes and refresh panes.
2025-08-30 00:45:12 +10:00
HRESULT FileBrowserApp::FrameMove(){
XBInput_GetInput();
2025-08-31 15:46:31 +10:00
2025-09-06 16:05:50 +10:00
// --- Poll for general drive-set changes (ignore D:) ----------------------
2025-08-31 15:46:31 +10:00
{
2025-09-06 16:05:50 +10:00
static DWORD s_nextPollMs = 0;
static unsigned int s_lastMaskNoD = 0xFFFFFFFF; // sentinel = uninitialized
2025-08-31 15:46:31 +10:00
DWORD now = GetTickCount();
if (now >= s_nextPollMs) {
2025-09-06 16:05:50 +10:00
s_nextPollMs = now + 1200; // ~1.2s
2025-08-31 15:46:31 +10:00
2025-09-06 16:05:50 +10:00
const unsigned int dBit = (1u << ('D' - 'A'));
unsigned int mask = QueryDriveMaskAZ(); // A:..Z:
unsigned int maskNoD = (mask & ~dBit); // exclude D:
2025-08-31 15:46:31 +10:00
2025-09-06 16:05:50 +10:00
if (s_lastMaskNoD == 0xFFFFFFFF) {
s_lastMaskNoD = maskNoD; // prime (no toast)
} else if (maskNoD != s_lastMaskNoD) {
s_lastMaskNoD = maskNoD;
RescanDrives(); // non-D changes
EnsureListing(m_pane[0]);
2025-08-31 15:46:31 +10:00
EnsureListing(m_pane[1]);
2025-09-06 16:05:50 +10:00
// comment out if you never want this toast:
SetStatus("Drives refreshed");
2025-08-31 15:46:31 +10:00
}
}
}
2025-09-06 16:05:50 +10:00
// --- 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 != DRIVE_READY) {
BOOL needRefresh = FALSE;
switch (code) {
case DRIVE_OPEN:
case DRIVE_CLOSED_NO_MEDIA:
DvdUnmap_Io();
s_dMapped = FALSE;
s_lastDvdSerial = 0xFFFFFFFF; // forget old serial
m_dvdHaveStats = false; // <— clear
m_dvdUsedBytes = 0;
m_dvdTotalBytes = 0;
needRefresh = TRUE;
if (code == DRIVE_OPEN) SetStatus("DVD: Tray Open");
else SetStatus("DVD: No Disc");
break;
case DRIVE_CLOSED_MEDIA_PRESENT: {
DvdColdRemount();
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("D:\\", fb, tb);
m_dvdTotalBytes = tb;
m_dvdUsedBytes = DirSizeRecursiveA("D:\\"); // from FsUtil
m_dvdHaveStats = true;
{ char lbl[64]; if (DvdDetectMediaSimple(lbl, sizeof(lbl))) SetStatus("%s", lbl); }
needRefresh = TRUE;
break; }
case DRIVE_NOT_READY:
default:
break;
}
if (needRefresh) {
RescanDrives();
// 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);
}
}
}
}
}
// Serial watchdog: catches fast swaps if tray state change was missed
{
static DWORD s_nextSerChk = 0;
DWORD now3 = GetTickCount();
if (now3 >= s_nextSerChk) {
s_nextSerChk = now3 + 800; // ~1.25 Hz
if (s_dMapped) {
DWORD curSer = 0;
if (GetDvdVolumeSerial(&curSer)) {
if (s_lastDvdSerial != 0xFFFFFFFF && curSer != s_lastDvdSerial) {
// New disc detected silently — force remount and refresh
DvdColdRemount();
s_lastDvdSerial = curSer;
// refresh cached used/total
ULONGLONG fb=0, tb=0;
GetDriveFreeTotal("D:\\", fb, tb);
m_dvdTotalBytes = tb;
m_dvdUsedBytes = DirSizeRecursiveA("D:\\");
m_dvdHaveStats = true;
RescanDrives();
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)) {
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 {
RefreshPane(P);
}
}
SetStatus("DVD: Media changed");
}
}
}
}
}
}
2025-08-30 00:45:12 +10:00
OnPad(g_Gamepads[0]);
return S_OK;
}
2025-09-06 16:05:50 +10:00
2025-09-02 14:26:19 +10:00
// ----- draw: menu / rename / panes -----------------------------------------
// Draw the context menu if open (coordinates set in OpenMenu).
2025-08-30 00:45:12 +10:00
void FileBrowserApp::DrawMenu(){
2025-08-30 01:23:50 +10:00
if (!m_ctx.IsOpen()) return;
m_ctx.Draw(m_font, m_pd3dDevice);
2025-08-30 00:45:12 +10:00
}
2025-09-02 14:26:19 +10:00
// Draw OSD keyboard if active.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::DrawRename(){
if (!m_kb.Active()) return;
m_kb.Draw(m_font, m_pd3dDevice, kLineH);
}
2025-09-02 14:26:19 +10:00
// ----- listing / navigation -------------------------------------------------
// Ensure a pane has items and indices are in range (after mode/path changes).
2025-08-30 00:45:12 +10:00
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 = MaxI(0, (int)p.items.size() - m_visible);
if (p.scroll > maxScroll) p.scroll = maxScroll;
}
2025-09-02 14:26:19 +10:00
// Enter current selection (drive -> directory, ".." -> up, dir -> descend,
// .xbe -> launch). Other files are no-op here.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::EnterSelection(Pane& p){
if (p.items.empty()) return;
const Item& it = p.items[p.sel];
2025-09-02 14:26:19 +10:00
// Drive list -> go into the chosen drive.
if (p.mode==0){
2025-08-30 00:45:12 +10:00
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;
}
2025-09-02 14:26:19 +10:00
// Directory listing
2025-09-02 17:03:03 +10:00
if (it.isUpEntry){
// Reselect the folder we’re leaving (same as UpOne)
char childName[256]; ExtractLastComponent(p.curPath, childName, sizeof(childName));
if (strlen(p.curPath) <= 3){
char driveRoot[4] = { p.curPath[0], ':', '\\', 0 };
p.mode = 0;
p.sel = 0; p.scroll = 0;
BuildDriveItems(p.items);
for (int i=0; i<(int)p.items.size(); ++i){
if (_stricmp(p.items[i].name, driveRoot) == 0){ p.sel = i; break; }
}
if (p.sel < p.scroll) p.scroll = p.sel;
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
p.curPath[0] = 0;
} else {
ParentPath(p.curPath);
p.sel = 0; p.scroll = 0;
ListDirectory(p.curPath, p.items);
SelectItemInPane(p, childName);
}
return;
}
2025-08-30 00:45:12 +10:00
if (it.isDir){
2025-09-02 14:26:19 +10:00
// Descend into subdirectory.
2025-08-30 00:45:12 +10:00
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;
}
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// 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);
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// Small Present for a snappy visual handoff before XLaunchNewImageA.
m_pd3dDevice->Present(NULL, NULL, NULL, NULL);
Sleep(10);
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
if (!LaunchXbeA(full)) {
SetStatusLastErr("Launch failed");
}
}
return;
}
2025-08-30 00:45:12 +10:00
}
2025-09-02 14:26:19 +10:00
// Move up one level; from root goes back to drive list.
2025-08-30 00:45:12 +10:00
void FileBrowserApp::UpOne(Pane& p){
if (p.mode==0) return;
2025-09-02 17:03:03 +10:00
// Name of the child we’re currently inside (to reselect in parent)
char childName[256]; ExtractLastComponent(p.curPath, childName, sizeof(childName));
// If at drive root, go back to drive list and select that drive
if (strlen(p.curPath) <= 3){
char driveRoot[4] = { p.curPath[0], ':', '\\', 0 };
p.mode = 0;
p.sel = 0; p.scroll = 0;
BuildDriveItems(p.items);
// Try to select 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; }
}
if (p.sel < p.scroll) p.scroll = p.sel;
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
p.curPath[0] = 0;
return;
}
// Go to parent and select the child folder we just left
ParentPath(p.curPath);
p.sel = 0; p.scroll = 0;
ListDirectory(p.curPath, p.items);
SelectItemInPane(p, childName);
2025-08-30 00:45:12 +10:00
}
2025-09-02 17:03:03 +10:00
2025-09-02 14:26:19 +10:00
// ----- Initialize / Render --------------------------------------------------
// Create font, input, initial drive mapping, and compute responsive layout.
2025-09-22 21:40:35 +10:00
// ----------------------------------------------------------------------------
2025-08-30 00:45:12 +10:00
HRESULT FileBrowserApp::Initialize(){
2025-09-22 21:40:35 +10:00
const char* userFont = "D:\\Media\\Font.xpr";
const char* titleFont = "T:\\Daemon-X_Font.xpr"; // title-scoped cache
bool fontOk = false;
XBUtil_DebugPrint("Init: starting FileBrowserApp::Initialize");
// 1) User-supplied override on D:
if (FileExistsA(userFont)) {
XBUtil_DebugPrint("Init: User font found at %s", userFont);
HRESULT hr = m_font.Create(userFont, 0);
XBUtil_DebugPrint("Init: m_font.Create(user) -> 0x%08lX", hr);
fontOk = SUCCEEDED(hr);
} else {
XBUtil_DebugPrint("Init: No user font at %s", userFont);
}
// 2) Otherwise, try cached copy on T:\ first (no rewrite if it already exists)
if (!fontOk) {
if (FileExistsA(titleFont)) {
XBUtil_DebugPrint("Init: found cached font at %s, loading", titleFont);
HRESULT hr = m_font.Create(titleFont, 0);
XBUtil_DebugPrint("Init: m_font.Create(cached) -> 0x%08lX", hr);
fontOk = SUCCEEDED(hr);
// If cached file is bad/corrupt, rewrite it from the embedded bytes.
if (!fontOk) {
XBUtil_DebugPrint("Init: Cached font load failed, rewriting from embedded (%lu bytes)",
(unsigned long)g_FontXprSize);
const BOOL wrote = WriteAllA(titleFont, g_FontXpr, (size_t)g_FontXprSize);
XBUtil_DebugPrint("Init: WriteAllA('%s') -> %s", titleFont, wrote ? "OK" : "FAIL");
if (wrote) {
HRESULT hr2 = m_font.Create(titleFont, 0);
XBUtil_DebugPrint("Init: m_font.Create(rewritten) -> 0x%08lX", hr2);
fontOk = SUCCEEDED(hr2);
}
}
} else {
// 3) No cached file: write embedded once and load it
XBUtil_DebugPrint("Init: No cached font; writing embedded to %s (%lu bytes)",
titleFont, (unsigned long)g_FontXprSize);
const BOOL wrote = WriteAllA(titleFont, g_FontXpr, (size_t)g_FontXprSize);
XBUtil_DebugPrint("Init: WriteAllA('%s') -> %s", titleFont, wrote ? "OK" : "FAIL");
if (wrote) {
HRESULT hr = m_font.Create(titleFont, 0);
XBUtil_DebugPrint("Init: m_font.Create(embedded) -> 0x%08lX", hr);
fontOk = SUCCEEDED(hr);
}
}
}
if (!fontOk) {
XBUtil_DebugPrint("Init: WARNING - All font loads failed; text may not render");
} else {
XBUtil_DebugPrint("Init: Font loaded successfully");
2025-08-30 00:45:12 +10:00
}
2025-09-09 18:23:51 +10:00
XBInput_CreateGamepads();
MapStandardDrives_Io();
RescanDrives();
BuildDriveItems(m_pane[0].items);
BuildDriveItems(m_pane[1].items);
2025-08-30 00:45:12 +10:00
2025-09-09 18:23:51 +10:00
// Layout derived from current backbuffer size (works for any resolution)
ComputeResponsiveLayout();
2025-09-22 21:40:35 +10:00
XBUtil_DebugPrint("Init: Layout computed");
2025-08-30 00:45:12 +10:00
return S_OK;
}
2025-09-02 14:26:19 +10:00
// ----- progress overlay API -------------------------------------------------
// Begin a copy/move progress session (title and first label for marquee).
2025-08-31 16:23:57 +10:00
void FileBrowserApp::BeginProgress(ULONGLONG total, const char* firstLabel, const char* title) {
2025-08-31 15:46:31 +10:00
m_prog.active = true;
m_prog.done = 0;
m_prog.total = total;
2025-08-31 16:23:57 +10:00
2025-08-31 15:46:31 +10:00
_snprintf(m_prog.current, sizeof(m_prog.current), "%s", firstLabel ? firstLabel : "");
m_prog.current[sizeof(m_prog.current)-1] = 0;
2025-08-31 16:23:57 +10:00
_snprintf(m_prog.title, sizeof(m_prog.title), "%s", title ? title : "Working...");
m_prog.title[sizeof(m_prog.title)-1] = 0;
2025-08-31 15:46:31 +10:00
m_prog.lastPaintMs = 0;
}
2025-09-02 14:26:19 +10:00
// Update the overlay counters and optional label; throttles repaint to ~25 fps.
2025-08-31 15:46:31 +10:00
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;
2025-09-02 14:26:19 +10:00
// redraw one frame (use the normal Render path)
2025-08-31 15:46:31 +10:00
Render();
Sleep(0);
}
}
2025-09-02 14:26:19 +10:00
// End and clear the progress overlay state.
2025-08-31 15:46:31 +10:00
void FileBrowserApp::EndProgress(){
m_prog.active = false;
}
2025-09-02 14:26:19 +10:00
// Draw the modal progress HUD (centered panel with marquee and bar).
2025-08-31 15:46:31 +10:00
void FileBrowserApp::DrawProgressOverlay(){
if (!m_prog.active) return;
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
2025-09-09 20:01:36 +10:00
// Ensure filename line has room for about 42 glyphs at current font size
2025-08-31 15:46:31 +10:00
char fortyTwo[64]; for (int i=0;i<42;++i) fortyTwo[i]='W'; fortyTwo[42]=0;
FLOAT fileW=0, fileH=0; MeasureTextWH(m_font, fortyTwo, fileW, fileH);
const FLOAT margin = 18.0f;
FLOAT w = MaxF(MaxF(420.0f, vp.Width * 0.50f), fileW + margin*2.0f);
if (w > vp.Width - margin*2.0f) w = vp.Width - margin*2.0f;
2025-09-09 20:01:36 +10:00
const FLOAT h = 116.0f;
2025-08-31 15:46:31 +10:00
const FLOAT x = Snap((vp.Width - w)*0.5f);
const FLOAT y = Snap((vp.Height - h)*0.5f);
DrawRect(x-6, y-6, w+12, h+12, 0xA0101010);
DrawRect(x, y, w, h, 0xE0222222);
2025-09-09 20:01:36 +10:00
// layout lines
2025-08-31 15:46:31 +10:00
const FLOAT titleY = y + 10.0f;
const FLOAT folderY = titleY + 24.0f;
const FLOAT fileY = folderY + 22.0f;
2025-09-02 14:26:19 +10:00
const FLOAT barY = fileY + 26.0f;
2025-08-31 15:46:31 +10:00
const FLOAT barH = 20.0f;
const FLOAT barX = x + margin;
const FLOAT barW = w - margin*2.0f;
2025-09-09 20:01:36 +10:00
// title
2025-08-31 16:23:57 +10:00
DrawAnsi(m_font, x + margin, titleY, 0xFFFFFFFF, m_prog.title[0] ? m_prog.title : "Working...");
2025-08-31 15:46:31 +10:00
2025-09-22 17:03:07 +10:00
// hint (right) — red "B:" + gray "Cancel"
{
const char* pre = "B:";
const char* gap = " ";
const char* suf = "Cancel";
FLOAT pw, ph, gw, gh, sw, sh;
MeasureTextWH(m_font, pre, pw, ph);
MeasureTextWH(m_font, gap, gw, gh);
MeasureTextWH(m_font, suf, sw, sh);
const FLOAT totalW = pw + gw + sw;
const FLOAT startX = x + w - margin - totalW;
DrawAnsi(m_font, startX, titleY, 0xFFFF4040, pre); // red
DrawAnsi(m_font, startX + pw, titleY, 0xFFCCCCCC, gap); // space
DrawAnsi(m_font, startX + pw + gw, titleY, 0xFFCCCCCC, suf); // gray
}
2025-09-09 20:01:36 +10:00
// Split current label into folder + file parts
2025-08-31 15:46:31 +10:00
const char* label = m_prog.current;
const char* slash = label ? strrchr(label, '\\') : NULL;
char folder[512] = {0};
char file [256] = {0};
if (slash){
2025-09-09 20:01:36 +10:00
size_t n = (size_t)(slash - label + 1);
2025-08-31 15:46:31 +10:00
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 {
2025-09-09 20:01:36 +10:00
_snprintf(file, sizeof(file), "%s", label ? label : ""); file[sizeof(file)-1]=0;
2025-08-31 15:46:31 +10:00
folder[0] = 0;
}
2025-09-09 20:01:36 +10:00
// --- character-step marquees (like panes/OSK) ----------------------------
static ProgMarquee s_marqFolder;
static ProgMarquee s_marqFile;
2025-08-31 17:16:38 +10:00
2025-09-09 20:01:36 +10:00
// 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(); }
2025-08-31 17:16:38 +10:00
2025-09-09 20:01:36 +10:00
DrawLabelFittedOrMarquee(m_font, x + margin, folderY, barW, 0xFF5EA4FF, folder, s_marqFolder);
DrawLabelFittedOrMarquee(m_font, x + margin, fileY, barW, 0xFF89D07E, file, s_marqFile);
2025-08-31 17:16:38 +10:00
2025-09-09 20:01:36 +10:00
// progress percent and bar
2025-08-31 15:46:31 +10:00
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;
}
DrawRect(barX, barY, barW, barH, 0xFF0E0E0E);
DrawRect(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;
FLOAT tw=0, th=0; MeasureTextWH(m_font, t, tw, th);
const FLOAT tx = Snap(barX + barW - tw);
const FLOAT ty = Snap(barY + (barH - th) * 0.5f);
DrawAnsi(m_font, tx, ty, 0xFFEEEEEE, t);
}
2025-09-02 14:26:19 +10:00
// ----- main render ----------------------------------------------------------
// Draw both panes, footer and hints, transient status, and any overlays.
2025-08-30 00:45:12 +10:00
HRESULT FileBrowserApp::Render(){
m_pd3dDevice->Clear(0,NULL,D3DCLEAR_TARGET,0x20202020,1.0f,0);
m_pd3dDevice->BeginScene();
2025-09-06 19:35:38 +10:00
2025-08-30 00:45:12 +10:00
m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
2025-09-06 19:35:38 +10:00
m_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
2025-08-30 00:45:12 +10:00
m_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
2025-09-09 18:23:51 +10:00
// Recompute layout if resolution changes on the fly (safe & cheap)
ComputeResponsiveLayout();
2025-09-06 19:35:38 +10:00
// ---- panes ----
2025-08-30 01:55:30 +10:00
PaneStyle st;
st.listW = kListW;
st.listY = kListY;
st.lineH = kLineH;
st.hdrY = kHdrY;
st.hdrH = kHdrH;
2025-09-09 18:23:51 +10:00
st.hdrW = kHdrW; // header exactly equals pane width
2025-08-30 01:55:30 +10:00
st.gutterW = kGutterW;
st.paddingX = kPaddingX;
st.scrollBarW = kScrollBarW;
st.visibleRows = m_visible;
2025-09-09 18:23:51 +10:00
// ---- panes ----
PaneRenderer::BeginFrameSharedCols(); // 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);
2025-08-30 00:45:12 +10:00
2025-09-06 19:35:38 +10:00
// ---- footer / hints ----
2025-09-09 18:23:51 +10:00
D3DVIEWPORT8 vp2; m_pd3dDevice->GetViewport(&vp2);
2025-08-31 15:46:31 +10:00
2025-09-09 18:23:51 +10:00
// Footer matches the combined pane area (two panes + gap), centered.
2025-09-09 20:40:57 +10:00
const FLOAT footerMargin = SafeMarginX((FLOAT)vp2.Width, (FLOAT)vp2.Height);
2025-09-09 20:04:34 +10:00
const FLOAT footerW = MinF(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 - FooterBandPx((FLOAT)vp2.Height); // <-- unified
2025-08-31 15:46:31 +10:00
2025-09-09 18:23:51 +10:00
// footer bar
DrawRect(footerX, footerY, footerW, 28.0f, 0x802A2A2A);
2025-08-31 15:46:31 +10:00
2025-09-09 18:23:51 +10:00
// --- (unchanged) footer text building and status toast follow here ---
{
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";
2025-09-06 19:35:38 +10:00
2025-09-09 18:23:51 +10:00
const bool isLowRes = (vp2.Height < 700); // 480i/p or 576i count as "small"
const bool smallFooter = (isLowRes || footerW <= 620.0f);
2025-09-06 19:35:38 +10:00
2025-09-09 18:23:51 +10:00
if (m_pane[m_active].mode == 0){
const char* hintsVerbose = "D-Pad: Move | Left/Right: Switch pane | A: Enter | X: Menu | Black/White: Page";
const char* hintsCompact = "DPad:Move | L/R:Pane | A:Enter | X:Menu | Pg:Blk/Wht";
const char* base = smallFooter ? hintsCompact : hintsVerbose;
2025-09-06 19:35:38 +10:00
2025-09-09 18:23:51 +10:00
char fitted[256];
LeftEllipsizeToFit(m_font, base, footerW - 10.0f, fitted, sizeof(fitted));
DrawAnsiCenteredX(m_font, footerX, footerW, footerY + 4.0f, 0xFFCCCCCC, fitted);
} 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;
}
2025-09-06 19:35:38 +10:00
2025-09-09 18:23:51 +10:00
char leftStr[64], rightStr[64];
FormatSize(leftVal, leftStr, sizeof(leftStr));
FormatSize(rightVal, rightStr, sizeof(rightStr));
2025-09-06 19:35:38 +10:00
2025-09-09 18:23:51 +10:00
char bar[420];
if (smallFooter) {
_snprintf(bar, sizeof(bar),
"Active:%s | B:Up | %s:%s/%s | X:Menu | Y:%s | Pg:Blk/Wht",
(m_active==0 ? "L" : "R"), leftLabel, leftStr, rightStr, yLab);
} else {
_snprintf(bar, sizeof(bar),
"Active: %s | B: Up | %s: %s / Total: %s | X: Menu | Y: %s | Black/White: Page",
(m_active==0 ? "Left" : "Right"), leftLabel, leftStr, rightStr, yLab);
}
bar[sizeof(bar)-1] = 0;
2025-09-06 21:54:55 +10:00
2025-09-09 18:23:51 +10:00
char fitted[420];
LeftEllipsizeToFit(m_font, bar, footerW - 10.0f, fitted, sizeof(fitted));
DrawAnsiCenteredX(m_font, footerX, footerW, footerY + 4.0f, 0xFFCCCCCC, fitted);
}
2025-08-30 00:45:12 +10:00
2025-09-09 18:23:51 +10:00
// ---- status toast (also centered and fitted) ----
DWORD now = GetTickCount();
if (now < m_statusUntilMs && m_status[0]){
char fitted[256];
LeftEllipsizeToFit(m_font, m_status, footerW - 10.0f, fitted, sizeof(fitted));
DrawAnsiCenteredX(m_font, footerX, footerW, footerY + 25.0f, 0xFFBBDDEE, fitted);
}
}
2025-08-30 00:45:12 +10:00
2025-09-06 19:35:38 +10:00
// ---- overlays ----
2025-08-30 00:45:12 +10:00
DrawMenu();
DrawRename();
2025-08-31 15:46:31 +10:00
DrawProgressOverlay();
2025-08-30 00:45:12 +10:00
2025-09-06 19:35:38 +10:00
m_pd3dDevice->EndScene();
m_pd3dDevice->Present(NULL,NULL,NULL,NULL);
2025-08-30 00:45:12 +10:00
return S_OK;
}