You've already forked FileManager
mirror of
https://github.com/Team-Resurgent/FileManager.git
synced 2026-04-30 10:38:50 -07:00
767 lines
28 KiB
C++
767 lines
28 KiB
C++
#include "FileBrowserApp.h"
|
|
#include <wchar.h>
|
|
#include <stdarg.h>
|
|
#include <algorithm>
|
|
#include <stdio.h> // _snprintf
|
|
|
|
// ---- local helpers (no header pollution) ----
|
|
namespace {
|
|
inline FLOAT MaxF(FLOAT a, FLOAT b){ return (a>b)?a:b; }
|
|
inline int MaxI (int a, int b){ return (a>b)?a:b; }
|
|
inline FLOAT Snap (FLOAT v){ return (FLOAT)((int)(v + 0.5f)); }
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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_mode=MODE_BROWSE;
|
|
m_status[0]=0; m_statusUntilMs=0;
|
|
}
|
|
|
|
// ----- draw prims -----
|
|
void FileBrowserApp::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));
|
|
}
|
|
|
|
FLOAT FileBrowserApp::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 FileBrowserApp::MeasureTextWH(const char* s, FLOAT& outW, FLOAT& outH){
|
|
WCHAR wbuf[256];
|
|
MultiByteToWideChar(CP_ACP, 0, s, -1, wbuf, 256);
|
|
m_font.GetTextExtent(wbuf, &outW, &outH);
|
|
}
|
|
void FileBrowserApp::DrawRightAligned(const char* s, FLOAT rightX, FLOAT y, DWORD color){
|
|
DrawAnsi(m_font, rightX - MeasureTextW(s), y, color, s);
|
|
}
|
|
|
|
// ----- size column -----
|
|
FLOAT FileBrowserApp::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;
|
|
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 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; // 3s
|
|
}
|
|
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 -----
|
|
void FileBrowserApp::RefreshPane(Pane& p){
|
|
if (p.mode==1){
|
|
int prevSel = p.sel;
|
|
int prevScroll= p.scroll;
|
|
|
|
ListDirectory(p.curPath, p.items);
|
|
|
|
if (prevSel >= (int)p.items.size()) prevSel = (int)p.items.size()-1;
|
|
if (prevSel < 0) prevSel = 0;
|
|
p.sel = prevSel;
|
|
|
|
int maxScroll = MaxI(0, (int)p.items.size() - m_visible);
|
|
if (prevScroll > maxScroll) prevScroll = maxScroll;
|
|
if (prevScroll < 0) prevScroll = 0;
|
|
p.scroll = prevScroll;
|
|
} else {
|
|
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 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 (!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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----- context menu (delegated) -----
|
|
void FileBrowserApp::AddMenuItem(const char* label, Action act, bool enabled){
|
|
m_ctx.AddItem(label, act, enabled);
|
|
}
|
|
void FileBrowserApp::BuildContextMenu(){
|
|
Pane& p = m_pane[m_active];
|
|
bool inDir = (p.mode == 1);
|
|
bool hasSel = !p.items.empty();
|
|
|
|
m_ctx.Clear();
|
|
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);
|
|
}
|
|
void FileBrowserApp::OpenMenu(){
|
|
BuildContextMenu();
|
|
|
|
const FLOAT menuW = 340.0f;
|
|
const FLOAT rowH = kLineH + 6.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;
|
|
|
|
m_ctx.OpenAt(x, y, menuW, rowH);
|
|
m_mode=MODE_MENU;
|
|
}
|
|
void FileBrowserApp::CloseMenu(){
|
|
m_ctx.Close();
|
|
if (m_mode==MODE_MENU) m_mode=MODE_BROWSE;
|
|
}
|
|
|
|
// ----- rename lifecycle (delegated to OnScreenKeyboard) -----
|
|
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; }
|
|
|
|
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();
|
|
}
|
|
|
|
// ----- input: rename modal -----
|
|
void FileBrowserApp::OnPad_Rename(const XBGAMEPAD& pad){
|
|
OnScreenKeyboard::Result r = m_kb.OnPad(pad);
|
|
if (r == OnScreenKeyboard::ACCEPTED){ AcceptRename(); return; }
|
|
if (r == OnScreenKeyboard::CANCELED){ CancelRename(); return; }
|
|
|
|
// keep edge state roughly synced (optional)
|
|
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: context menu (delegated) -----
|
|
void FileBrowserApp::OnPad_Menu(const XBGAMEPAD& pad){
|
|
Action act;
|
|
ContextMenu::Result r = m_ctx.OnPad(pad, act);
|
|
if (r == ContextMenu::CHOSEN){
|
|
ExecuteAction(act);
|
|
CloseMenu();
|
|
} else if (r == ContextMenu::CLOSED){
|
|
CloseMenu();
|
|
}
|
|
|
|
// sync browse edge state to avoid carry-over triggers
|
|
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) -----
|
|
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;
|
|
const DWORD kInit = 230;
|
|
const DWORD kRep = 120;
|
|
|
|
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;
|
|
|
|
if (ud == 0){
|
|
m_navUDHeld = false; m_navUDDir = 0;
|
|
}else{
|
|
if (!m_navUDHeld || m_navUDDir != ud){
|
|
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){
|
|
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 (edge)
|
|
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)
|
|
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 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 wTrig = (w > 30 && m_prevWhite <= 30);
|
|
bool kTrig = (k > 30 && m_prevBlack <= 30);
|
|
bool startTrig = ((btn & XINPUT_GAMEPAD_START) && !(m_prevButtons & XINPUT_GAMEPAD_START));
|
|
|
|
if (xTrig) { OpenMenu(); goto store_prev; }
|
|
|
|
if (aTrig) EnterSelection(p);
|
|
if (bTrig) UpOne(p);
|
|
|
|
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);
|
|
}
|
|
|
|
if (startTrig){
|
|
MapStandardDrives_Io(); RescanDrives();
|
|
EnsureListing(m_pane[0]); EnsureListing(m_pane[1]);
|
|
}
|
|
|
|
store_prev:
|
|
m_prevButtons = btn;
|
|
m_prevA = a; m_prevB = b; m_prevX = x;
|
|
m_prevWhite = w; m_prevBlack = k;
|
|
}
|
|
|
|
// ----- input router -----
|
|
void FileBrowserApp::OnPad(const XBGAMEPAD& pad){
|
|
if (m_mode == MODE_RENAME){ OnPad_Rename(pad); return; }
|
|
if (m_mode == MODE_MENU) { OnPad_Menu(pad); return; }
|
|
OnPad_Browse(pad);
|
|
}
|
|
|
|
HRESULT FileBrowserApp::FrameMove(){
|
|
XBInput_GetInput();
|
|
OnPad(g_Gamepads[0]);
|
|
return S_OK;
|
|
}
|
|
|
|
// ----- draw: menu / rename / panes -----
|
|
void FileBrowserApp::DrawMenu(){
|
|
if (!m_ctx.IsOpen()) return;
|
|
|
|
const FLOAT menuW = 340.0f;
|
|
const FLOAT rowH = kLineH + 6.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;
|
|
|
|
// (coordinates are already set on open; draw just uses them)
|
|
m_ctx.Draw(m_font, m_pd3dDevice);
|
|
}
|
|
|
|
void FileBrowserApp::DrawRename(){
|
|
if (!m_kb.Active()) return;
|
|
m_kb.Draw(m_font, m_pd3dDevice, kLineH);
|
|
}
|
|
|
|
void FileBrowserApp::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); CloseMenu();
|
|
} 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){
|
|
if (!IsDriveRoot(src.curPath)){
|
|
char root[4] = { src.curPath[0], ':', '\\', 0 };
|
|
strncpy(src.curPath, root, sizeof(src.curPath)-1);
|
|
src.curPath[3] = 0;
|
|
src.sel = 0; src.scroll = 0;
|
|
ListDirectory(src.curPath, src.items);
|
|
} else {
|
|
src.mode = 0; src.curPath[0] = 0; src.sel = 0; src.scroll = 0;
|
|
BuildDriveItems(src.items);
|
|
}
|
|
} else {
|
|
BuildDriveItems(src.items);
|
|
}
|
|
CloseMenu(); break;
|
|
|
|
case ACT_SWITCHMEDIA:
|
|
m_active = 1 - m_active; CloseMenu(); break;
|
|
}
|
|
}
|
|
|
|
// ----- listing / navigation -----
|
|
void FileBrowserApp::EnsureListing(Pane& p){
|
|
if (p.mode==0) BuildDriveItems(p.items);
|
|
else ListDirectory(p.curPath, p.items);
|
|
|
|
if (p.sel >= (int)p.items.size()) p.sel = (int)p.items.size()-1;
|
|
if (p.sel < 0) p.sel = 0;
|
|
|
|
if (p.scroll > p.sel) p.scroll = p.sel;
|
|
int maxScroll = MaxI(0, (int)p.items.size() - m_visible);
|
|
if (p.scroll > maxScroll) p.scroll = maxScroll;
|
|
}
|
|
|
|
void FileBrowserApp::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
|
|
}
|
|
|
|
void FileBrowserApp::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);
|
|
}
|
|
|
|
// ----- Initialize / Render -----
|
|
HRESULT FileBrowserApp::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);
|
|
|
|
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);
|
|
|
|
kLineH = MaxF(22.0f, vp.Height * 0.036f);
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
|
|
// 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
|
|
DWORD now = GetTickCount();
|
|
if (now < m_statusUntilMs && m_status[0]){
|
|
DrawAnsi(m_font, HdrX(kListX_L)+5.0f, footerY-18.0f, 0xFFBBDDEE, m_status);
|
|
}
|
|
|
|
DrawMenu();
|
|
DrawRename();
|
|
|
|
m_pd3dDevice->EndScene(); m_pd3dDevice->Present(NULL,NULL,NULL,NULL);
|
|
return S_OK;
|
|
}
|
|
|
|
// ----- pane draw -----
|
|
void FileBrowserApp::DrawPane(FLOAT baseX, Pane& p, bool active){
|
|
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
|
|
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 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;
|