You've already forked FileManager
mirror of
https://github.com/Team-Resurgent/FileManager.git
synced 2026-04-30 10:38:50 -07:00
1208 lines
49 KiB
C++
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(); }
|