Files
FileManager/FileManager.cpp
T
2025-08-28 20:58:07 +10:00

1208 lines
49 KiB
C++

//------------------------------------------------------------------------------
// OG Xbox File Browser (Original Xbox XDK) — VS2003 friendly
// Dual-pane GUI with auto layout, context menu, basic file ops, and rename OSD
//------------------------------------------------------------------------------
#include "stdafx.h" // Remove if you disabled PCH
#include <xtl.h>
#include <wchar.h>
#include <vector>
#include <algorithm>
#include <stdarg.h>
#include "XBApp.h"
#include "XBFont.h"
#include "XBInput.h"
extern "C" {
typedef struct _STRING { USHORT Length; USHORT MaximumLength; PCHAR Buffer; } STRING, *PSTRING;
LONG __stdcall IoCreateSymbolicLink(PSTRING SymbolicLinkName, PSTRING DeviceName);
LONG __stdcall IoDeleteSymbolicLink(PSTRING SymbolicLinkName);
}
#pragma comment(lib, "xboxkrnl.lib")
#ifndef INVALID_FILE_ATTRIBUTES
#define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF
#endif
// --- Simple on-screen keyboard layout (fixed grid, VS2003-safe) ---
static const char s_kb_r0[] = "ABCDEFGHIJKL"; // 12
static const char s_kb_r1[] = "MNOPQRSTUVWX"; // 12
static const char s_kb_r2[] = "YZ0123456789"; // 12
static const char s_kb_r3[] = "-_.()[]{}+&"; // 12 (punctuation row)
static const int s_kb_cols[5] = {12,12,12,12,5}; // last row: Back, Space, Aa, OK, Cancel
// small helpers (avoid <algorithm> max macro surprises in VS2003)
static inline FLOAT MaxF(FLOAT a, FLOAT b){ return (a>b)?a:b; }
static inline int MaxI(int a, int b){ return (a>b)?a:b; }
static inline FLOAT Snap(FLOAT v){ return (FLOAT)((int)(v + 0.5f)); } // integer pixel snap
static const char* kRoots[] = { "C:\\", "D:\\", "E:\\", "F:\\", "G:\\", "X:\\", "Y:\\", "Z:\\" };
static const int kNumRoots = sizeof(kRoots)/sizeof(kRoots[0]);
static int g_presentIdx[16];
static int g_presentCount = 0;
// -------------------- text / size / path helpers --------------------
static 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);
}
static inline void FormatSize(ULONGLONG sz, char* out, size_t cap){
const char* unit="B"; double v=(double)sz;
if(sz>=(1ULL<<30)){ v/=(double)(1ULL<<30); unit="GB"; }
else if(sz>=(1ULL<<20)){ v/=(double)(1ULL<<20); unit="MB"; }
else if(sz>=(1ULL<<10)){ v/=(double)(1ULL<<10); unit="KB"; }
_snprintf(out,(int)cap,(unit[0]=='B')?"%.0f %s":"%.1f %s",v,unit); out[cap-1]=0;
}
static inline void GetDriveFreeTotal(const char* anyPathInDrive, ULONGLONG& freeBytes, ULONGLONG& totalBytes){
freeBytes=0; totalBytes=0; char root[8]; _snprintf(root,sizeof(root),"%c:\\",anyPathInDrive[0]); root[sizeof(root)-1]=0;
ULARGE_INTEGER a,t,f; a.QuadPart=0; t.QuadPart=0; f.QuadPart=0;
if(GetDiskFreeSpaceExA(root,&a,&t,&f)){ freeBytes=f.QuadPart; totalBytes=t.QuadPart; }
}
static inline void BuildString(STRING& s,const char* z){ USHORT L=(USHORT)strlen(z); s.Length=L; s.MaximumLength=L+1; s.Buffer=(PCHAR)z; }
static inline void MakeDosString(char* out,size_t cap,const char* letter){ _snprintf(out,(int)cap,"\\??\\%s",letter); out[cap-1]=0; }
static inline void JoinPath(char* dst,size_t cap,const char* base,const char* name){
size_t bl=strlen(base);
if(bl&&base[bl-1]=='\\') _snprintf(dst,(int)cap,"%s%s",base,name);
else _snprintf(dst,(int)cap,"%s\\%s",base,name);
dst[cap-1]=0;
}
static inline void EnsureTrailingSlash(char* s,size_t cap){ size_t n=strlen(s); if(n&&s[n-1]!='\\'&&n+1<cap){ s[n]='\\'; s[n+1]=0; } }
static inline void ParentPath(char* path){
size_t n=strlen(path);
if (n <= 3) { path[0]=0; return; }
while (n && path[n-1]=='\\') { path[--n]=0; }
char* p = strrchr(path,'\\');
if (!p) { path[0]=0; return; }
if (p == path+2) *(p+1)=0; else *p=0;
}
static inline int ci_cmp(const char* a,const char* b){ return _stricmp(a,b); }
// -------------------- letter mapping --------------------
static BOOL MapLetterToDevice(const char* letter,const char* devicePath){
char dosBuf[16]={0}; MakeDosString(dosBuf,sizeof(dosBuf),letter);
STRING sDos; BuildString(sDos,dosBuf); IoDeleteSymbolicLink(&sDos);
STRING sDev; BuildString(sDev,devicePath);
if(IoCreateSymbolicLink(&sDos,&sDev)!=0) return FALSE; // STATUS_SUCCESS==0
char root[8]={0}; _snprintf(root,sizeof(root),"%s\\",letter);
if(GetFileAttributesA(root)==INVALID_FILE_ATTRIBUTES){ IoDeleteSymbolicLink(&sDos); return FALSE; }
return TRUE;
}
static void MapStandardDrives_Io(){
MapLetterToDevice("D:","\\Device\\Cdrom0");
MapLetterToDevice("C:","\\Device\\Harddisk0\\Partition2");
MapLetterToDevice("E:","\\Device\\Harddisk0\\Partition1");
MapLetterToDevice("X:","\\Device\\Harddisk0\\Partition3");
MapLetterToDevice("Y:","\\Device\\Harddisk0\\Partition4");
MapLetterToDevice("Z:","\\Device\\Harddisk0\\Partition5");
MapLetterToDevice("F:","\\Device\\Harddisk0\\Partition6");
MapLetterToDevice("G:","\\Device\\Harddisk0\\Partition7");
}
// -------------------- listing --------------------
struct Item { char name[256]; bool isDir; ULONGLONG size; bool isUpEntry; };
static void BuildDriveItems(std::vector<Item>& out){
out.clear(); for(int j=0;j<g_presentCount;++j){ int i=g_presentIdx[j]; Item it; ZeroMemory(&it,sizeof(it));
strncpy(it.name,kRoots[i],255); it.name[255]=0; it.isDir=true; it.size=0; it.isUpEntry=false; out.push_back(it); }
}
static void RescanDrives(){ g_presentCount=0; for(int i=0;i<kNumRoots && g_presentCount<(int)(sizeof(g_presentIdx)/sizeof(g_presentIdx[0])); ++i){ DWORD a=GetFileAttributesA(kRoots[i]); if(a!=INVALID_FILE_ATTRIBUTES) g_presentIdx[g_presentCount++]=i; } }
static bool ItemLess(const Item& a,const Item& b){ if(a.isDir!=b.isDir) return a.isDir>b.isDir; return ci_cmp(a.name,b.name)<0; }
static bool ListDirectory(const char* path,std::vector<Item>& out){
out.clear();
if(strlen(path)>3){ Item up; ZeroMemory(&up,sizeof(up)); strncpy(up.name,"..",3); up.isDir=true; up.size=0; up.isUpEntry=true; out.push_back(up); }
char base[512]; _snprintf(base,sizeof(base),"%s",path); base[sizeof(base)-1]=0; EnsureTrailingSlash(base,sizeof(base));
char mask[512]; _snprintf(mask,sizeof(mask),"%s*",base); mask[sizeof(mask)-1]=0;
WIN32_FIND_DATAA fd; ZeroMemory(&fd,sizeof(fd)); HANDLE h=FindFirstFileA(mask,&fd); if(h==INVALID_HANDLE_VALUE) return false;
do{ const char* n=fd.cFileName; if(!strcmp(n,".")||!strcmp(n,"..")) continue; Item it; ZeroMemory(&it,sizeof(it));
strncpy(it.name,n,255); it.name[255]=0; it.isDir=(fd.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY)!=0;
it.size=(((ULONGLONG)fd.nFileSizeHigh)<<32)|fd.nFileSizeLow; it.isUpEntry=false; out.push_back(it);
}while(FindNextFileA(h,&fd)); FindClose(h);
size_t start=(strlen(path)>3)?1:0; if(out.size()>start+1) std::sort(out.begin()+(int)start,out.end(),ItemLess);
return true;
}
// -------------------- App (dual-pane + menu) --------------------
class FileBrowserApp : public CXBApplication {
public:
// pane state
struct Pane {
std::vector<Item> items;
char curPath[512];
int mode; // 0=drives, 1=dir
int sel;
int scroll;
Pane(){ curPath[0]=0; mode=0; sel=0; scroll=0; }
};
CXBFont m_font;
int m_visible; // rows
unsigned char m_prevA, m_prevB, m_prevY;
DWORD m_prevButtons; // last frame's digital buttons
unsigned char m_prevWhite, m_prevBlack; // last frame's white/black
Pane m_pane[2];
int m_active; // 0=left, 1=right
// TL vertex for filled rects
struct TLVERT { float x,y,z,rhw; D3DCOLOR color; };
enum { FVF_TLVERT = D3DFVF_XYZRHW | D3DFVF_DIFFUSE };
// Layout (left pane base + right pane offset) — computed at runtime
static FLOAT kListX_L, kListY, kListW, kLineH;
static FLOAT kHdrX_L, kHdrY, kHdrW, kHdrH;
static FLOAT kGutterW, kPaddingX, kScrollBarW;
static FLOAT kPaneGap; // space between panes
// --- Context menu/state ---
enum Action {
ACT_OPEN, ACT_COPY, ACT_MOVE, ACT_DELETE, ACT_RENAME,
ACT_MKDIR, ACT_CALCSIZE, ACT_GOROOT, ACT_SWITCHMEDIA
};
struct MenuItem { const char* label; Action act; bool enabled; };
bool m_menuOpen;
int m_menuSel;
MenuItem m_menu[9];
int m_menuCount;
enum { MODE_BROWSE, MODE_MENU, MODE_RENAME } m_mode;
// footer status
char m_status[256];
DWORD m_statusUntilMs;
// --- Rename UI state ---
bool m_renActive;
char m_renParent[512];
char m_renOld[256];
char m_renBuf[256];
int m_renCursor; // insertion cursor in m_renBuf
int m_renSelRow; // keyboard selection
int m_renSelCol;
bool m_kbLower; // case mode
FileBrowserApp(){
m_visible=13; m_prevA=0; m_prevB=0; m_prevY=0; m_active=0;
m_prevButtons = 0;
m_prevWhite = m_prevBlack = 0;
ZeroMemory(&m_d3dpp,sizeof(m_d3dpp));
m_d3dpp.BackBufferWidth=1280; m_d3dpp.BackBufferHeight=720;
m_d3dpp.BackBufferFormat=D3DFMT_X8R8G8B8; m_d3dpp.SwapEffect=D3DSWAPEFFECT_DISCARD;
m_d3dpp.FullScreen_RefreshRateInHz=60; m_d3dpp.EnableAutoDepthStencil=TRUE; m_d3dpp.AutoDepthStencilFormat=D3DFMT_D24S8;
m_d3dpp.Flags = D3DPRESENTFLAG_PROGRESSIVE | D3DPRESENTFLAG_WIDESCREEN;
m_menuOpen=false; m_menuSel=0; m_menuCount=0; m_mode=MODE_BROWSE;
m_status[0]=0; m_statusUntilMs=0;
m_renActive=false; m_renParent[0]=0; m_renOld[0]=0; m_renBuf[0]=0;
m_renCursor=0; m_renSelRow=0; m_renSelCol=0; m_kbLower=false;
}
// --- draw prims
void DrawRect(float x,float y,float w,float h,D3DCOLOR c){
TLVERT v[4]; v[0].x=x; v[0].y=y; v[1].x=x+w; v[1].y=y; v[2].x=x; v[2].y=y+h; v[3].x=x+w; v[3].y=y+h;
for(int i=0;i<4;i++){ v[i].z=0.0f; v[i].rhw=1.0f; v[i].color=c; }
m_pd3dDevice->SetTexture(0,NULL);
m_pd3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_DISABLE);
m_pd3dDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
m_pd3dDevice->SetVertexShader(FVF_TLVERT);
m_pd3dDevice->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP,2,v,sizeof(TLVERT));
}
void DrawHLine(float x,float y,float w,D3DCOLOR c){ DrawRect(x,y,w,1.0f,c); }
void DrawVLine(float x,float y,float h,D3DCOLOR c){ DrawRect(x,y,1.0f,h,c); }
// --- layout helpers/columns
static FLOAT NameColX(FLOAT baseX){ return baseX + kGutterW + kPaddingX; }
static FLOAT HdrX (FLOAT baseX){ return baseX - 15.0f; }
// --- text measurement + right-align
FLOAT MeasureTextW(const char* s){
WCHAR wbuf[256];
MultiByteToWideChar(CP_ACP, 0, s, -1, wbuf, 256);
FLOAT w=0.0f, h=0.0f;
m_font.GetTextExtent(wbuf, &w, &h);
return w;
}
void DrawRightAligned(const char* s, FLOAT rightX, FLOAT y, DWORD color){
FLOAT w = MeasureTextW(s);
DrawAnsi(m_font, rightX - w, y, color, s);
}
// --- size column measurement (dynamic per pane)
FLOAT ComputeSizeColW(const Pane& p){
FLOAT maxW = MeasureTextW("Size");
int limit = (int)p.items.size(); if(limit>200) limit=200;
char buf[64];
for(int i=0;i<limit;++i){
const Item& it = p.items[i];
if(!it.isDir && !it.isUpEntry){
FormatSize(it.size, buf, sizeof(buf));
FLOAT w = MeasureTextW(buf); if(w>maxW) maxW=w;
}
}
maxW += 16.0f; // padding
const FLOAT minW = MaxF(90.0f, kLineH * 4.0f);
const FLOAT maxWClamp = kListW * 0.40f;
if(maxW < minW) maxW = minW;
if(maxW > maxWClamp) maxW = maxWClamp;
return maxW;
}
// --- status
void 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; // 3s
}
void 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);
}
// Return true if we can create & delete a temp file in 'dir'
bool CanWriteHereA(const char* dir){
char test[512];
JoinPath(test, sizeof(test), dir, ".__xwtest$__");
test[sizeof(test)-1]=0;
HANDLE h = CreateFileA(test, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
if (h == INVALID_HANDLE_VALUE) return false;
CloseHandle(h);
DeleteFileA(test);
return true;
}
// normalize dir for things like "E:" -> "E:\"
void NormalizeDirA(char* s){
size_t n = strlen(s);
if (n==2 && s[1]==':'){ s[2]='\\'; s[3]=0; return; }
EnsureTrailingSlash(s, 512);
}
// FATX-ish name rules: disallow control chars and obvious bad ones
bool IsBadFatxChar(char c){
if ((unsigned char)c < 32) return true;
const char* bad = "\\/:*?\"<>|+,;=[]";
return (strchr(bad, c) != NULL);
}
void SanitizeFatxNameInPlace(char* s){
for (char* p=s; *p; ++p) if (IsBadFatxChar(*p)) *p = '_';
int n = (int)strlen(s);
while (n>0 && (s[n-1]==' ' || s[n-1]=='.')) s[--n]=0;
if (n > 42) { s[42]=0; n=42; }
if (n==0 || (strcmp(s,".")==0) || (strcmp(s,"..")==0)) strcpy(s, "NewName");
}
// pick char at (row,col) for rows 0-3 of keyboard (respects case)
char KbCharAt(int row, int col){
char ch = 0;
if (row==0) ch = s_kb_r0[col];
else if (row==1) ch = s_kb_r1[col];
else if (row==2) ch = s_kb_r2[col];
else if (row==3) ch = s_kb_r3[col];
if (m_kbLower && (ch>='A' && ch<='Z')) ch = (char)(ch + ('a' - 'A'));
return ch;
}
void 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 {
// drives list
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;
}
}
bool 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 (!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;
}
// --- file ops helpers
bool EnsureDirA(const char* path){
DWORD a = GetFileAttributesA(path);
if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY)) return true;
return CreateDirectoryA(path, NULL) ? true : false;
}
bool CopyFileSimpleA(const char* s, const char* d){ return CopyFileA(s, d, FALSE) ? true : false; }
bool DeleteRecursiveA(const char* path){
DWORD a = GetFileAttributesA(path);
if (a == INVALID_FILE_ATTRIBUTES) return false;
if (a & FILE_ATTRIBUTE_DIRECTORY){
char mask[512]; JoinPath(mask, sizeof(mask), path, "*");
WIN32_FIND_DATAA fd; HANDLE h=FindFirstFileA(mask,&fd);
if (h != INVALID_HANDLE_VALUE){
do{
if (!strcmp(fd.cFileName,".") || !strcmp(fd.cFileName,"..")) continue;
char sub[512]; JoinPath(sub, sizeof(sub), path, fd.cFileName);
DeleteRecursiveA(sub);
}while(FindNextFileA(h,&fd));
FindClose(h);
}
return RemoveDirectoryA(path) ? true : false;
}else{
return DeleteFileA(path) ? true : false;
}
}
bool CopyRecursiveA(const char* srcPath, const char* dstDir){
DWORD a = GetFileAttributesA(srcPath);
if (a == INVALID_FILE_ATTRIBUTES) return false;
const char* base = strrchr(srcPath, '\\');
base = base ? base+1 : srcPath;
char dstPath[512]; JoinPath(dstPath, sizeof(dstPath), dstDir, base);
if (a & FILE_ATTRIBUTE_DIRECTORY){
if (!EnsureDirA(dstPath)) return false;
char mask[512]; JoinPath(mask, sizeof(mask), srcPath, "*");
WIN32_FIND_DATAA fd; HANDLE h=FindFirstFileA(mask, &fd);
if (h != INVALID_HANDLE_VALUE){
do{
if (!strcmp(fd.cFileName,".") || !strcmp(fd.cFileName,"..")) continue;
char subSrc[512]; JoinPath(subSrc, sizeof(subSrc), srcPath, fd.cFileName);
CopyRecursiveA(subSrc, dstPath);
}while(FindNextFileA(h,&fd));
FindClose(h);
}
return true;
}else{
return CopyFileSimpleA(srcPath, dstPath);
}
}
ULONGLONG DirSizeRecursiveA(const char* path){
ULONGLONG sum = 0;
DWORD a = GetFileAttributesA(path);
if (a == INVALID_FILE_ATTRIBUTES) return 0;
if (a & FILE_ATTRIBUTE_DIRECTORY){
char mask[512]; JoinPath(mask, sizeof(mask), path, "*");
WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(mask, &fd);
if (h != INVALID_HANDLE_VALUE){
do{
if (!strcmp(fd.cFileName,".") || !strcmp(fd.cFileName,"..")) continue;
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY){
char sub[512]; JoinPath(sub, sizeof(sub), path, fd.cFileName);
sum += DirSizeRecursiveA(sub);
} else {
sum += (((ULONGLONG)fd.nFileSizeHigh)<<32) | fd.nFileSizeLow;
}
} while (FindNextFileA(h, &fd));
FindClose(h);
}
} else {
WIN32_FILE_ATTRIBUTE_DATA fad;
if (GetFileAttributesExA(path, GetFileExInfoStandard, &fad)){
sum += (((ULONGLONG)fad.nFileSizeHigh)<<32) | fad.nFileSizeLow;
}
}
return sum;
}
void AddMenuItem(const char* label, Action act, bool enabled){
m_menu[m_menuCount].label = label;
m_menu[m_menuCount].act = act;
m_menu[m_menuCount].enabled = enabled;
++m_menuCount;
}
bool DirExistsA(const char* path){
DWORD a = GetFileAttributesA(path);
return (a != INVALID_FILE_ATTRIBUTES) && (a & FILE_ATTRIBUTE_DIRECTORY);
}
void 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;
}
}
}
// --- context menu
void BuildContextMenu(){
Pane& p = m_pane[m_active];
bool inDir = (p.mode == 1);
bool hasSel = !p.items.empty();
m_menuCount = 0;
if (inDir) AddMenuItem("Open", ACT_OPEN, hasSel);
AddMenuItem("Copy", ACT_COPY, hasSel);
AddMenuItem("Move", ACT_MOVE, hasSel);
AddMenuItem("Delete", ACT_DELETE, hasSel);
AddMenuItem("Rename", ACT_RENAME, hasSel);
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,true);
if (m_menuSel >= m_menuCount) m_menuSel = m_menuCount-1;
if (m_menuSel < 0) m_menuSel = 0;
}
void OpenMenu(){ BuildContextMenu(); m_menuOpen=true; m_mode=MODE_MENU; }
// >>> CHANGED: only switch to BROWSE if we were actually in MENU
void CloseMenu(){
m_menuOpen = false;
if (m_mode == MODE_MENU)
m_mode = MODE_BROWSE;
}
// --- Rename UI lifecycle
void BeginRename(const char* parentDir, const char* oldName){
// ensure any context menu is hidden, but do NOT stomp the new mode
m_menuOpen = false;
m_renActive = true;
_snprintf(m_renParent, sizeof(m_renParent), "%s", parentDir);
m_renParent[sizeof(m_renParent)-1]=0;
_snprintf(m_renOld, sizeof(m_renOld), "%s", oldName);
m_renOld[sizeof(m_renOld)-1]=0;
_snprintf(m_renBuf, sizeof(m_renBuf), "%s", oldName);
m_renBuf[sizeof(m_renBuf)-1]=0;
m_renCursor = (int)strlen(m_renBuf);
m_renSelRow = 0; m_renSelCol = 0;
m_mode = MODE_RENAME;
}
void CancelRename(){
m_renActive = false;
m_mode = MODE_BROWSE;
}
void AcceptRename(){
SanitizeFatxNameInPlace(m_renBuf);
if (_stricmp(m_renBuf, m_renOld)==0){ SetStatus("No change"); CancelRename(); return; }
char oldPath[512]; JoinPath(oldPath, sizeof(oldPath), m_renParent, m_renOld);
char newPath[512]; JoinPath(newPath, sizeof(newPath), m_renParent, m_renBuf);
if (MoveFileA(oldPath, newPath)){
SetStatus("Renamed to %s", m_renBuf);
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
Pane& ap = m_pane[m_active];
if (ap.mode==1 && _stricmp(ap.curPath, m_renParent)==0){
SelectItemInPane(ap, m_renBuf);
}
}else{
SetStatusLastErr("Rename failed");
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
}
CancelRename();
}
void OnPad_Rename(const XBGAMEPAD& pad)
{
const DWORD btn = pad.wButtons;
// Edge-detected digital buttons + allow quick analog nudge
bool up = ((btn & XINPUT_GAMEPAD_DPAD_UP) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_UP)) || (pad.sThumbLY > 16000);
bool down = ((btn & XINPUT_GAMEPAD_DPAD_DOWN) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_DOWN)) || (pad.sThumbLY < -16000);
bool left = ((btn & XINPUT_GAMEPAD_DPAD_LEFT) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_LEFT)) || (pad.sThumbLX < -16000);
bool right = ((btn & XINPUT_GAMEPAD_DPAD_RIGHT) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_RIGHT)) || (pad.sThumbLX > 16000);
if (up) { if (m_renSelRow > 0) m_renSelRow--; Sleep(120); }
if (down) { if (m_renSelRow < 4) m_renSelRow++; Sleep(120); }
int cols = s_kb_cols[m_renSelRow];
if (m_renSelCol >= cols) m_renSelCol = cols - 1;
if (left) { if (m_renSelCol > 0) m_renSelCol--; Sleep(120); }
if (right) { if (m_renSelCol < cols - 1) m_renSelCol++; Sleep(120); }
// Edge-detect face buttons
unsigned char a = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
unsigned char b = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
unsigned char y = pad.bAnalogButtons[XINPUT_GAMEPAD_Y];
unsigned char w = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
unsigned char k = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
bool aTrig = (a > 30 && m_prevA <= 30);
bool bTrig = (b > 30 && m_prevB <= 30);
bool yTrig = (y > 30 && m_prevY <= 30);
bool startTrig = ((btn & XINPUT_GAMEPAD_START) && !(m_prevButtons & XINPUT_GAMEPAD_START));
// Toggle case with Y
if (yTrig){ m_kbLower = !m_kbLower; Sleep(120); }
if (aTrig){
if (m_renSelRow <= 3){
// Insert selected char at cursor
int len = (int)strlen(m_renBuf);
const int cap = (int)sizeof(m_renBuf) - 1;
if (len < cap){
char ch = KbCharAt(m_renSelRow, m_renSelCol);
for (int i=len; i>=m_renCursor; --i) m_renBuf[i+1] = m_renBuf[i];
m_renBuf[m_renCursor++] = ch;
}
}else{
// Bottom-row actions: 0=Backspace 1=Space 2=Aa 3=OK 4=Cancel
if (m_renSelCol == 0){ // Backspace
if (m_renCursor > 0){
int len = (int)strlen(m_renBuf);
for (int i=m_renCursor-1; i<=len; ++i) m_renBuf[i] = m_renBuf[i+1];
m_renCursor--;
}
}else if (m_renSelCol == 1){ // Space
int len = (int)strlen(m_renBuf);
const int cap = (int)sizeof(m_renBuf) - 1;
if (len < cap){
for (int i=len; i>=m_renCursor; --i) m_renBuf[i+1] = m_renBuf[i];
m_renBuf[m_renCursor++] = ' ';
}
}else if (m_renSelCol == 2){ // Aa toggle
m_kbLower = !m_kbLower;
}else if (m_renSelCol == 3){ // OK
AcceptRename();
m_prevA=a; m_prevB=b; m_prevY=y; m_prevButtons=btn; m_prevWhite=w; m_prevBlack=k;
return;
}else if (m_renSelCol == 4){ // Cancel
CancelRename();
m_prevA=a; m_prevB=b; m_prevY=y; m_prevButtons=btn; m_prevWhite=w; m_prevBlack=k;
return;
}
}
Sleep(140);
}
if (startTrig){
AcceptRename();
m_prevA=a; m_prevB=b; m_prevY=y; m_prevButtons=btn; m_prevWhite=w; m_prevBlack=k;
return;
}
if (bTrig){
CancelRename();
m_prevA=a; m_prevB=b; m_prevY=y; m_prevButtons=btn; m_prevWhite=w; m_prevBlack=k;
return;
}
// Move caret with White/Black (edge-detected)
bool wTrig = (w > 30 && m_prevWhite <= 30);
bool kTrig = (k > 30 && m_prevBlack <= 30);
if (wTrig){ if (m_renCursor > 0) m_renCursor--; }
if (kTrig){ int len=(int)strlen(m_renBuf); if (m_renCursor < len) m_renCursor++; }
// Clamp caret
{
int len=(int)strlen(m_renBuf);
if (m_renCursor < 0) m_renCursor = 0;
if (m_renCursor > len) m_renCursor = len;
}
// Save previous for edge detection next frame
m_prevA = a;
m_prevB = b;
m_prevY = y;
m_prevButtons = btn;
m_prevWhite = w;
m_prevBlack = k;
}
void DrawRename(){
if (!m_renActive) return;
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
const FLOAT panelW = MaxF(520.0f, vp.Width * 0.55f);
const FLOAT panelH = MaxF(280.0f, vp.Height*0.45f);
const FLOAT x = Snap((vp.Width - panelW)*0.5f);
const FLOAT y = Snap((vp.Height - panelH)*0.5f);
DrawRect(x-8, y-8, panelW+16, panelH+16, 0xA0101010);
DrawRect(x, y, panelW, panelH, 0xE0222222);
DrawAnsi(m_font, x+12, y+8, 0xFFFFFFFF, "Rename");
DrawHLine(x, y+28, panelW, 0x60FFFFFF);
char hdr[640];
_snprintf(hdr, sizeof(hdr), "In: %s", m_renParent); hdr[sizeof(hdr)-1]=0;
DrawAnsi(m_font, x+12, y+34, 0xFFCCCCCC, hdr);
const FLOAT boxY = Snap(y + 60.0f);
const FLOAT boxH = 30.0f;
DrawRect(x+12, boxY, panelW-24, boxH, 0xFF0E0E0E);
// text
DrawAnsi(m_font, x+18, boxY+4, 0xFFFFFF00, m_renBuf);
// caret at cursor
char tmp = m_renBuf[m_renCursor];
m_renBuf[m_renCursor] = 0;
FLOAT caretX = Snap(x+18 + MeasureTextW(m_renBuf));
m_renBuf[m_renCursor] = tmp;
DrawRect(caretX, boxY+4, 2.0f, boxH-8.0f, 0x90FFFF00);
// keyboard grid (integer-snap to avoid drift)
const FLOAT padX = 12.0f;
const FLOAT contentW = panelW - 2.0f*padX;
const FLOAT cellH = kLineH + 6.0f;
const FLOAT gridTop = Snap(boxY + boxH + 16.0f);
const FLOAT gapX = 3.0f;
const FLOAT gapY = 4.0f;
const FLOAT colW12 = contentW / 12.0f;
for (int row=0; row<5; ++row){
int cols = s_kb_cols[row];
FLOAT rowY = Snap(gridTop + row*(cellH + gapY));
for (int col=0; col<cols; ++col){
FLOAT x0, x1;
if (row < 4){
x0 = Snap(x + padX + col * colW12);
x1 = Snap(x + padX + (col + 1) * colW12);
}else{
FLOAT colW = contentW / (FLOAT)cols;
x0 = Snap(x + padX + col * colW);
x1 = Snap(x + padX + (col + 1) * colW);
}
FLOAT w = (x1 - x0) - gapX;
if (w < 1.0f) w = 1.0f; // safety
bool sel = (row==m_renSelRow && col==m_renSelCol);
DrawRect(x0, rowY, w, cellH, sel?0x60FFFF00:0x30202020);
if (row < 4){
char s[2]; s[0]=KbCharAt(row,col); s[1]=0;
DrawAnsi(m_font, x0+6.0f, rowY+4.0f, 0xFFE0E0E0, s);
}else{
const char* cap;
if (col==0) cap = "Backspace";
else if (col==1) cap = "Space";
else if (col==2) cap = "Aa"; // case toggle
else if (col==3) cap = "OK";
else cap = "Cancel";
DrawAnsi(m_font, x0+6.0f, rowY+4.0f, 0xFFE0E0E0, cap);
}
}
}
DrawAnsi(m_font, x+12, y+panelH-20, 0xFFBBBBBB,
"A: Select B: Cancel Start: OK Y/Aa: Case White/Black: Move cursor");
}
void ExecuteAction(Action act){
Pane& src = m_pane[m_active];
Pane& dst = m_pane[1 - m_active];
const Item* sel = NULL;
if (!src.items.empty()) sel = &src.items[src.sel];
char srcFull[512]="";
if (sel && src.mode==1 && !sel->isUpEntry) JoinPath(srcFull, sizeof(srcFull), src.curPath, sel->name);
switch (act){
case ACT_OPEN:
if (sel){
if (sel->isUpEntry) { UpOne(src); }
else if (sel->isDir) { EnterSelection(src); }
}
CloseMenu(); break;
case ACT_COPY:
{
if (!sel || sel->isUpEntry) { SetStatus("Nothing to copy"); CloseMenu(); break; }
char dstDir[512];
if (!ResolveDestDir(dstDir, sizeof(dstDir))) {
SetStatus("Pick a destination (open a folder or select a drive)");
CloseMenu(); break;
}
if ((dstDir[0]=='D' || dstDir[0]=='d') && dstDir[1]==':') {
SetStatus("Cannot copy to D:\\ (read-only)");
CloseMenu(); break;
}
NormalizeDirA(dstDir);
if (!CanWriteHereA(dstDir)){
SetStatusLastErr("Dest not writable");
CloseMenu(); break;
}
if (CopyRecursiveA(srcFull, dstDir)) {
Pane& dstp = m_pane[1 - m_active];
if (dstp.mode==1 && _stricmp(dstp.curPath, dstDir)==0) ListDirectory(dstp.curPath, dstp.items);
SetStatus("Copied to %s", dstDir);
} else {
SetStatusLastErr("Copy failed");
}
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
CloseMenu();
break;
}
case ACT_MOVE:
{
if (!sel || sel->isUpEntry) { SetStatus("Nothing to move"); CloseMenu(); break; }
char dstDir[512];
if (!ResolveDestDir(dstDir, sizeof(dstDir))) {
SetStatus("Pick a destination (open a folder or select a drive)");
CloseMenu(); break;
}
if ((dstDir[0]=='D' || dstDir[0]=='d') && dstDir[1]==':') {
SetStatus("Cannot move to D:\\ (read-only)");
CloseMenu(); break;
}
NormalizeDirA(dstDir);
if (!CanWriteHereA(dstDir)){
SetStatusLastErr("Dest not writable");
CloseMenu(); break;
}
if (CopyRecursiveA(srcFull, dstDir)) {
if (!DeleteRecursiveA(srcFull)) {
SetStatusLastErr("Move: delete source failed");
} else {
Pane& srcp = m_pane[m_active];
Pane& dstp = m_pane[1 - m_active];
if (dstp.mode==1 && _stricmp(dstp.curPath, dstDir)==0) ListDirectory(dstp.curPath, dstp.items);
if (srcp.mode==1) { ListDirectory(srcp.curPath, srcp.items); if (srcp.sel >= (int)srcp.items.size()) srcp.sel = (int)srcp.items.size()-1; }
SetStatus("Moved to %s", dstDir);
}
} else {
SetStatusLastErr("Move: copy failed");
}
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
CloseMenu();
break;
}
case ACT_DELETE:
if (sel){
if (DeleteRecursiveA(srcFull)){
ListDirectory(src.curPath, src.items);
if (src.sel >= (int)src.items.size()) src.sel = (int)src.items.size()-1;
SetStatus("Deleted");
} else SetStatus("Delete failed");
}
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
CloseMenu(); break;
case ACT_RENAME:
{
if (sel && src.mode==1 && !sel->isUpEntry){
BeginRename(src.curPath, sel->name); // BeginRename will hide menu & set MODE_RENAME
CloseMenu(); // safe (won't change mode unless we were in MENU)
} else {
SetStatus("Open a folder and select an item");
CloseMenu();
}
break;
}
case ACT_MKDIR:
{
char baseDir[512] = {0};
if (src.mode == 1) {
_snprintf(baseDir, sizeof(baseDir), "%s", src.curPath);
} else if (!src.items.empty()) {
const Item& di = src.items[src.sel];
if (di.isDir && !di.isUpEntry) {
_snprintf(baseDir, sizeof(baseDir), "%s", di.name); // e.g. "E:\"
}
}
baseDir[sizeof(baseDir)-1] = 0;
if (!baseDir[0]) { SetStatus("Open a folder or select a drive first"); CloseMenu(); break; }
if ((baseDir[0]=='D' || baseDir[0]=='d') && baseDir[1]==':') {
SetStatus("Cannot create on D:\\ (read-only)"); CloseMenu(); break;
}
NormalizeDirA(baseDir);
if (!CanWriteHereA(baseDir)) { SetStatusLastErr("Dest not writable"); CloseMenu(); break; }
char nameBuf[64];
char target[512];
int idx = 0;
for (;;){
if (idx == 0) _snprintf(nameBuf, sizeof(nameBuf), "NewFolder");
else _snprintf(nameBuf, sizeof(nameBuf), "NewFolder%d", idx);
nameBuf[sizeof(nameBuf)-1]=0;
JoinPath(target, sizeof(target), baseDir, nameBuf);
if (!DirExistsA(target)) {
if (CreateDirectoryA(target, NULL)) {
SetStatus("Created %s", nameBuf);
} else {
SetStatusLastErr("Create folder failed");
}
break;
}
if (++idx > 999){ SetStatus("Create folder failed (names exhausted)"); break; }
}
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
if (src.mode == 1 && _stricmp(src.curPath, baseDir) == 0) {
SelectItemInPane(src, nameBuf);
}
CloseMenu();
break;
}
case ACT_CALCSIZE:
if (sel){
ULONGLONG bytes = DirSizeRecursiveA(srcFull);
char tmp[64]; FormatSize(bytes, tmp, sizeof(tmp));
SetStatus("%s", tmp);
}
CloseMenu(); break;
case ACT_GOROOT:
if (src.mode==1){ src.mode=0; src.curPath[0]=0; src.sel=0; src.scroll=0; BuildDriveItems(src.items); }
CloseMenu(); break;
case ACT_SWITCHMEDIA:
m_active = 1 - m_active; CloseMenu(); break;
}
}
void DrawMenu(){
if (!m_menuOpen) return;
const FLOAT menuW = 340.0f;
const FLOAT rowH = kLineH + 6.0f;
const FLOAT menuH = 12.0f + m_menuCount*rowH + 12.0f;
const FLOAT paneX = (m_active==0) ? kListX_L : (kListX_L + kListW + kPaneGap);
const FLOAT x = paneX + (kListW - menuW)*0.5f;
const FLOAT y = kListY + 20.0f;
// backdrop
DrawRect(x-6.0f, y-6.0f, menuW+12.0f, menuH+12.0f, 0xA0101010);
DrawRect(x, y, menuW, menuH, 0xE0222222);
// title
DrawAnsi(m_font, x+10.0f, y+6.0f, 0xFFFFFFFF, "Select action");
DrawHLine(x, y+26.0f, menuW, 0x60FFFFFF);
// items
FLOAT iy = y + 30.0f;
for (int i=0;i<m_menuCount;++i){
bool sel = (i == m_menuSel);
D3DCOLOR row = sel ? 0x60FFFF00 : 0x20202020;
DrawRect(x+6.0f, iy-2.0f, menuW-12.0f, rowH, row);
const MenuItem& mi = m_menu[i];
DWORD col = mi.enabled? (sel?0xFF202020:0xFFE0E0E0) : 0xFF7A7A7A;
DrawAnsi(m_font, x+16.0f, iy, col, mi.label);
iy += rowH;
}
}
HRESULT Initialize(){
if (FAILED(m_font.Create("D:\\Media\\Font.xpr", 0))) {
m_font.Create("D:\\Media\\CourierNew.xpr", 0);
}
XBInput_CreateGamepads(); MapStandardDrives_Io(); RescanDrives();
BuildDriveItems(m_pane[0].items); BuildDriveItems(m_pane[1].items);
// compute resolution-aware layout from viewport
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
const FLOAT sideMargin = MaxF(24.0f, vp.Width * 0.04f);
const FLOAT gap = MaxF(24.0f, vp.Width * 0.035f);
kPaneGap = gap;
kListX_L = sideMargin;
const FLOAT totalUsable = (FLOAT)vp.Width - (sideMargin * 2.0f) - gap;
kListW = MaxF(260.0f, (totalUsable / 2.0f) - 10.0f);
kHdrW = kListW + 30.0f;
kHdrY = MaxF(12.0f, vp.Height * 0.03f);
kHdrH = MaxF(22.0f, vp.Height * 0.04f);
kListY = MaxF(60.0f, kHdrY + kHdrH + 34.0f); // room under "Name/Size"
kLineH = MaxF(22.0f, vp.Height * 0.036f);
// compute rows by viewport (leave footer space)
FLOAT bottomY = (FLOAT)vp.Height - MaxF(48.0f, vp.Height * 0.09f);
FLOAT usableH = bottomY - kListY; if (usableH < 0) usableH = 0;
m_visible = (int)(usableH / kLineH); if (m_visible < 6) m_visible=6; if (m_visible>30) m_visible=30;
return S_OK;
}
void 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;
}
void EnterSelection(Pane& p){
if (p.items.empty()) return;
const Item& it = p.items[p.sel];
if (p.mode==0){ // into drive
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;
}
if (it.isUpEntry){
if (strlen(p.curPath)<=3){ p.mode=0; p.sel=0; p.scroll=0; BuildDriveItems(p.items); p.curPath[0]=0; }
else { ParentPath(p.curPath); p.sel=0; p.scroll=0; ListDirectory(p.curPath,p.items); }
return;
}
if (it.isDir){
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: no-op in this build
}
void UpOne(Pane& p){
if (p.mode==0) return;
if (strlen(p.curPath)<=3){ p.mode=0; p.sel=0; p.scroll=0; BuildDriveItems(p.items); p.curPath[0]=0; return; }
ParentPath(p.curPath); p.sel=0; p.scroll=0; ListDirectory(p.curPath,p.items);
}
void OnPad(const XBGAMEPAD& pad){
// RENAME MODAL TAKES OVER INPUT
if (m_mode == MODE_RENAME){
OnPad_Rename(pad);
return;
}
// CONTEXT MENU MODAL
if (m_mode == MODE_MENU){
if ((pad.wButtons & XINPUT_GAMEPAD_DPAD_UP) || pad.sThumbLY>16000){
if (m_menuSel>0) m_menuSel--; Sleep(120);
}
if ((pad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) || pad.sThumbLY<-16000){
if (m_menuSel<m_menuCount-1) m_menuSel++; Sleep(120);
}
unsigned char a=pad.bAnalogButtons[XINPUT_GAMEPAD_A], b=pad.bAnalogButtons[XINPUT_GAMEPAD_B];
if (a>30){ const MenuItem& mi=m_menu[m_menuSel]; if(mi.enabled) ExecuteAction(mi.act); Sleep(160); }
if (b>30){ CloseMenu(); Sleep(120); }
if (pad.bAnalogButtons[XINPUT_GAMEPAD_X] > 30){ CloseMenu(); Sleep(150); }
return;
}
Pane& p = m_pane[m_active];
// open context menu
if (pad.bAnalogButtons[XINPUT_GAMEPAD_X] > 30){ OpenMenu(); Sleep(150); return; }
// move
if ((pad.wButtons & XINPUT_GAMEPAD_DPAD_UP) || pad.sThumbLY>16000){
if (p.sel>0) p.sel--; if (p.sel<p.scroll) p.scroll=p.sel; Sleep(120);
}
if ((pad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) || pad.sThumbLY<-16000){
if (p.sel<(int)p.items.size()-1) p.sel++; if (p.sel>=p.scroll+m_visible) p.scroll=p.sel-m_visible+1; Sleep(120);
}
// switch active pane
if (pad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) { m_active=0; Sleep(120); }
if (pad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) { m_active=1; Sleep(120); }
// paging
if (pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK] > 30){
p.sel -= m_visible; if (p.sel<0) p.sel=0; if (p.sel<p.scroll) p.scroll=p.sel; Sleep(150);
}
if (pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE] > 30){
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; Sleep(150);
}
// A/B
unsigned char a=pad.bAnalogButtons[XINPUT_GAMEPAD_A], b=pad.bAnalogButtons[XINPUT_GAMEPAD_B];
if (a>30 && m_prevA<=30){ EnterSelection(p); Sleep(150); }
if (b>30 && m_prevB<=30){ UpOne(p); Sleep(150); }
m_prevA=a; m_prevB=b;
// remap/rescan (both panes)
if (pad.wButtons & XINPUT_GAMEPAD_START){
MapStandardDrives_Io(); RescanDrives();
EnsureListing(m_pane[0]); EnsureListing(m_pane[1]);
}
}
HRESULT 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);
// left + right panes
DrawPane(kListX_L, m_pane[0], m_active==0);
DrawPane(kListX_L + kListW + kPaneGap, m_pane[1], m_active==1);
// footer
D3DVIEWPORT8 vp2; m_pd3dDevice->GetViewport(&vp2);
const FLOAT footerY = (FLOAT)vp2.Height - MaxF(48.0f, vp2.Height * 0.09f);
DrawRect(HdrX(kListX_L), footerY, kHdrW*2 + kPaneGap + 30.0f, 28.0f, 0x802A2A2A);
if (m_pane[m_active].mode==0){
DrawAnsi(m_font, HdrX(kListX_L)+5.0f, footerY+4.0f, 0xFFCCCCCC,
"D-Pad: Move | Left/Right: Switch pane | A: Enter | X: Menu | Start: Map+Rescan | Black/White: Page");
} else {
ULONGLONG fb=0, tb=0; GetDriveFreeTotal(m_pane[m_active].curPath, fb, tb);
char fstr[64], tstr[64], bar[420];
FormatSize(fb, fstr, sizeof(fstr)); FormatSize(tb, tstr, sizeof(tstr));
_snprintf(bar,sizeof(bar),
"Active: %s | B: Up | Free: %s / Total: %s | Left/Right: Switch pane | X: Menu | Black/White: Page",
m_active==0?"Left":"Right", fstr, tstr);
bar[sizeof(bar)-1]=0;
DrawAnsi(m_font, HdrX(kListX_L)+5.0f, footerY+4.0f, 0xFFCCCCCC, bar);
}
// transient status (over footer, if any)
DWORD now = GetTickCount();
if (now < m_statusUntilMs && m_status[0]){
DrawAnsi(m_font, HdrX(kListX_L)+5.0f, footerY-18.0f, 0xFFBBDDEE, m_status);
}
// modal menu
DrawMenu();
// rename overlay (modal)
DrawRename();
m_pd3dDevice->EndScene(); m_pd3dDevice->Present(NULL,NULL,NULL,NULL);
return S_OK;
}
// input pump
HRESULT FrameMove(){
XBInput_GetInput();
OnPad(g_Gamepads[0]);
return S_OK;
}
// pane draw (placed after other methods for readability)
void DrawPane(FLOAT baseX, Pane& p, bool active){
// header
FLOAT hx = HdrX(baseX);
DrawRect(hx, kHdrY, kHdrW, kHdrH, active ? 0xFF3A3A3A : 0x802A2A2A);
if (p.mode==0) DrawAnsi(m_font, hx+5.0f, kHdrY+6.0f, 0xFFFFFFFF, "Detected Drives");
else { char hdr[600]; _snprintf(hdr,sizeof(hdr),"%s", p.curPath); hdr[sizeof(hdr)-1]=0; DrawAnsi(m_font, hx+5.0f, kHdrY+6.0f, 0xFFFFFFFF, hdr); }
// compute Size column width/X for this pane
const FLOAT sizeColW = ComputeSizeColW(p);
const FLOAT sizeColX = baseX + kListW - (kScrollBarW + kPaddingX + sizeColW);
const FLOAT sizeRight = sizeColX + sizeColW;
// column header
DrawRect(baseX-10.0f, kHdrY+kHdrH+6.0f, kListW+20.0f, 22.0f, 0x60333333);
DrawAnsi(m_font, NameColX(baseX), kHdrY+kHdrH+10.0f, 0xFFDDDDDD, "Name");
DrawRightAligned("Size", sizeRight, kHdrY+kHdrH+10.0f, 0xFFDDDDDD);
DrawHLine(baseX-10.0f, kHdrY+kHdrH+28.0f, kListW+20.0f, 0x80444444);
DrawVLine(sizeColX - 8.0f, kHdrY+kHdrH+7.0f, 22.0f, 0x40444444);
// list bg
DrawRect(baseX-10.0f, kListY-6.0f, kListW+20.0f, kLineH*m_visible+12.0f, 0x30101010);
// stripes + selection
int end=p.scroll+m_visible; if(end>(int)p.items.size()) end=(int)p.items.size();
int rowIndex=0;
for(int i=p.scroll;i<end;++i,++rowIndex){
D3DCOLOR stripe=(rowIndex&1)?0x201E1E1E:0x10000000;
DrawRect(baseX, kListY + rowIndex*kLineH - 2.0f, kListW-8.0f, kLineH, stripe);
}
if(!p.items.empty() && p.sel>=p.scroll && p.sel<end){
int selRow=p.sel-p.scroll;
DrawRect(baseX, kListY + selRow*kLineH - 2.0f, kListW-8.0f, kLineH, active?0x60FFFF00:0x30FFFF00);
}
// rows
FLOAT y=kListY;
for(int i=p.scroll, r=0; i<end; ++i,++r){
const Item& it=p.items[i];
DWORD nameCol=(i==p.sel)?0xFFFFFF00:0xFFE0E0E0;
DWORD sizeCol=(i==p.sel)?0xFFFFFF00:0xFFB0B0B0;
D3DCOLOR ico = it.isUpEntry ? 0xFFAAAAAA : (it.isDir ? 0xFF5EA4FF : 0xFF89D07E);
DrawRect(baseX+2.0f, y+6.0f, kGutterW-8.0f, kLineH-12.0f, ico);
const char* glyph = it.isUpEntry ? ".." : (it.isDir?"+":"-");
DrawAnsi(m_font, baseX+4.0f, y+4.0f, 0xFFFFFFFF, glyph);
char nameBuf[300]; _snprintf(nameBuf,sizeof(nameBuf),"%s",it.name); nameBuf[sizeof(nameBuf)-1]=0;
DrawAnsi(m_font, NameColX(baseX), y, nameCol, nameBuf);
char sz[64]=""; if(!it.isDir && !it.isUpEntry){ FormatSize(it.size, sz, sizeof(sz)); }
DrawRightAligned(sz, sizeRight, y, sizeCol);
y += kLineH;
}
// scrollbar
if((int)p.items.size()>m_visible){
FLOAT trackX = baseX + kListW - kScrollBarW - 4.0f;
FLOAT trackY = kListY - 2.0f;
FLOAT trackH = m_visible * kLineH;
DrawRect(trackX, trackY, kScrollBarW, trackH, 0x40282828);
int total=(int)p.items.size();
FLOAT thumbH=(FLOAT)m_visible/(FLOAT)total*trackH; if(thumbH<10.0f) thumbH=10.0f;
FLOAT maxScroll=(FLOAT)(total-m_visible);
FLOAT t=(maxScroll>0.0f)?((FLOAT)p.scroll/maxScroll):0.0f;
FLOAT thumbY=trackY + t*(trackH-thumbH);
DrawRect(trackX, thumbY, kScrollBarW, thumbH, 0x80C0C0C0);
}
}
};
// ---- static layout defs (defaults; overwritten in Initialize) ----
FLOAT FileBrowserApp::kListX_L = 50.0f;
FLOAT FileBrowserApp::kListY = 100.0f;
FLOAT FileBrowserApp::kListW = 540.0f;
FLOAT FileBrowserApp::kLineH = 26.0f;
FLOAT FileBrowserApp::kHdrX_L = 35.0f;
FLOAT FileBrowserApp::kHdrY = 22.0f;
FLOAT FileBrowserApp::kHdrW = 570.0f;
FLOAT FileBrowserApp::kHdrH = 28.0f;
FLOAT FileBrowserApp::kGutterW = 18.0f;
FLOAT FileBrowserApp::kPaddingX = 6.0f;
FLOAT FileBrowserApp::kScrollBarW = 3.0f;
FLOAT FileBrowserApp::kPaneGap = 60.0f;
// OG XDK prefers main() entry
int __cdecl main(){ FileBrowserApp app; app.Create(); return app.Run(); }