Files
FileManager/main.cpp
T
2025-12-15 20:02:13 -08:00

1384 lines
49 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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<p.items.size(); ++i) if (p.items[i].marked) ++marked;
m_ctx.Clear();
// Common operations
if (ext && _stricmp(ext, "xbe") == 0)
m_ctx.AddItem("Launch", ACT_OPEN, (hasSel));
else
m_ctx.AddItem("Open", ACT_OPEN, (hasSel));
m_ctx.AddItem("Copy", ACT_COPY, (inDir && hasSel && inDir2));
m_ctx.AddItem("Move", ACT_MOVE, (inDir && hasSel && inDir2));
if (inDir && hasSel && !p.items[p.sel].isUpEntry) { BuildConfirmDelSubMenu();
m_ctx.AddSubMenu("Delete", &m_confirmDelSubMenu, (true));}
m_ctx.AddItem("Rename", ACT_RENAME, (inDir && hasSel));
if (ext && _stricmp(ext, "ips") == 0)
m_ctx.AddItem("Apply ips", ACT_APPLYIPS, (ext2 && _stricmp(ext2, "xbe") == 0));
if (ext && _stricmp(ext, "bak") == 0)
m_ctx.AddItem("Restore bak", ACT_RESTOREBAK, (true));
if (ext && _stricmp(ext, "zip") == 0) { BuildZipSubMenu();
m_ctx.AddSubMenu("Unzip", &m_zipSubMenu, (true));}
m_ctx.AddItem("Make new folder", ACT_MKDIR, (inDir));
m_ctx.AddItem("Calculate size", ACT_CALCSIZE, (hasSel));
m_ctx.AddItem("Go to root", ACT_GOROOT, (inDir));
// Marking tools (directory mode only; skip the ".." row)
if (inDir) {
int selectable = 0;
for (size_t i=0;i<p.items.size(); ++i) if (!p.items[i].isUpEntry) ++selectable;
if (selectable > 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();
}