You've already forked FileManager
mirror of
https://github.com/Team-Resurgent/FileManager.git
synced 2026-04-30 10:38:50 -07:00
1384 lines
49 KiB
C++
1384 lines
49 KiB
C++
#include "main.h"
|
||
|
||
#include "fsUtils.h"
|
||
#include "Font.h"
|
||
#include "textUtils.h"
|
||
#include "network.h"
|
||
#include "configuration.h"
|
||
#include "drawUtils.h"
|
||
#include "stringUtility.h"
|
||
|
||
// Simple getter used by overlay/status timers.
|
||
DWORD FileBrowserApp::StatusUntilMs() const { return m_statusUntilMs; }
|
||
|
||
// ---- local helpers (no header pollution) -----------------------------------
|
||
// These helpers are file-local to avoid leaking symbols into headers/other TUs.
|
||
namespace {
|
||
|
||
// ---- character-step marquee (same tuning as panes/OSK) ---------------------
|
||
static const DWORD kProgInitPauseMs = 900;
|
||
static const DWORD kProgStepMs = 150;
|
||
static const DWORD kProgEndPauseMs = 1200;
|
||
static const int kProgStepChars = 1;
|
||
|
||
static char mCurrentIp[16] = "0.0.0.0";
|
||
|
||
// Bigger side insets on SD to avoid CRT overscan clipping.
|
||
inline FLOAT SafeMarginX(FLOAT screenW, FLOAT screenH) {
|
||
const bool isSD = (screenH <= 600.0f); // 480/576
|
||
const FLOAT pct = isSD ? 0.065f : 0.03f; // 5% each side on SD, 3% on HD
|
||
return max(24.0f, screenW * pct); // never smaller than 24px
|
||
}
|
||
|
||
struct ProgMarquee {
|
||
FLOAT px; // start index (characters)
|
||
FLOAT fitWLock; // lock width for stable end calc
|
||
DWORD nextTick; // next update tick
|
||
DWORD resetPause; // nonzero while pausing at end
|
||
char last[768]; // last string drawn (for reset-on-change)
|
||
ProgMarquee() : px(0.f), fitWLock(0.f), nextTick(0), resetPause(0) { last[0]=0; }
|
||
};
|
||
|
||
// Draw one line at (x,y) clipped to maxW using a character-step marquee.
|
||
static void DrawLabelFittedOrMarquee(CXBFont& font, FLOAT x, FLOAT y, FLOAT maxW, DWORD color, const char* text, ProgMarquee& M) {
|
||
if (!text) text = "";
|
||
|
||
// right guard + tolerance like OSK
|
||
const FLOAT kRightGuard = 1.5f;
|
||
const FLOAT kTol = 2.0f;
|
||
|
||
const FLOAT fitW_now = (maxW > kRightGuard) ? (maxW - kRightGuard) : 0.0f;
|
||
|
||
FLOAT fullW, fullH;
|
||
GetAnsiWH(font, text, &fullW, &fullH);
|
||
|
||
// If it fits, draw + reset
|
||
if (fullW <= fitW_now + kTol){
|
||
DrawAnsi(font, x, y, color, &DefaultColors, text);
|
||
M = ProgMarquee();
|
||
return;
|
||
}
|
||
|
||
// Reset if label changed
|
||
if (_stricmp(M.last, text) != 0) {
|
||
_snprintf(M.last, sizeof(M.last), "%s", text); M.last[sizeof(M.last)-1]=0;
|
||
M.px = 0.0f; M.fitWLock = 0.0f; M.resetPause = 0; M.nextTick = GetTickCount() + kProgInitPauseMs;
|
||
}
|
||
|
||
const DWORD now = GetTickCount();
|
||
if (M.fitWLock <= 0.0f) M.fitWLock = fitW_now;
|
||
const FLOAT fitW = (FLOAT)((int)(M.fitWLock + 0.5f));
|
||
|
||
const char* s = text; const int len = (int)strlen(s);
|
||
|
||
// earliest start where the whole tail fits
|
||
int lo = 0, hi = len;
|
||
while (lo < hi){
|
||
const int mid = (lo + hi) / 2;
|
||
FLOAT tw, th;
|
||
GetAnsiWH(font, s + mid, &tw, &th);
|
||
if (tw <= fitW + kTol) hi = mid; else lo = mid + 1;
|
||
}
|
||
const int lastStart = (lo > len) ? len : lo;
|
||
|
||
int startIdx = (int)M.px;
|
||
if (startIdx < 0) startIdx = 0;
|
||
if (startIdx > lastStart) startIdx = lastStart;
|
||
|
||
// longest substring from start that fits
|
||
const char* startPtr = s + startIdx;
|
||
int lo2 = 0, hi2 = (int)strlen(startPtr);
|
||
while (lo2 < hi2){
|
||
const int mid = (lo2 + hi2 + 1) / 2;
|
||
char tmp[768]; _snprintf(tmp, sizeof(tmp), "%.*s", mid, startPtr);
|
||
FLOAT tw, th;
|
||
GetAnsiWH(font, tmp, &tw, &th);
|
||
if (tw <= fitW + kTol) lo2 = mid; else hi2 = mid - 1;
|
||
}
|
||
|
||
char vis[768]; _snprintf(vis, sizeof(vis), "%.*s", lo2, startPtr); vis[sizeof(vis)-1]=0;
|
||
DrawAnsi(font, x, y, color, &DefaultColors, vis);
|
||
|
||
// step/pause/reset
|
||
if (now >= M.nextTick) {
|
||
if (M.resetPause) {
|
||
M.px = 0.0f;
|
||
M.resetPause= 0;
|
||
M.nextTick = now + kProgInitPauseMs;
|
||
}
|
||
else {
|
||
if (startIdx >= lastStart){
|
||
M.px = (FLOAT)lastStart;
|
||
M.resetPause= now + kProgEndPauseMs;
|
||
M.nextTick = M.resetPause;
|
||
}
|
||
else {
|
||
M.px = (FLOAT)(startIdx + kProgStepChars);
|
||
M.nextTick = now + kProgStepMs;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Return first byte index whose rendered width reaches px (for marquee).
|
||
static const char* SkipToPixelOffset(CXBFont& font, const char* s, FLOAT px, FLOAT& skippedW){
|
||
skippedW = 0.0f;
|
||
if (!s || px <= 0.0f) return s ? s : "";
|
||
const char* p = s;
|
||
FLOAT tw=0, th=0;
|
||
while (*p){
|
||
int len = (int)(p - s + 1); if (len > 255) len = 255;
|
||
char tmp[256]; _snprintf(tmp, sizeof(tmp), "%.*s", len, s);
|
||
GetAnsiWH(font, tmp, &tw, &th);
|
||
if (tw >= px) break;
|
||
++p;
|
||
}
|
||
skippedW = tw;
|
||
return p;
|
||
}
|
||
|
||
// Return last component of a path (handles trailing '\'). e.g. "E:\A\B\" -> "B"
|
||
static void ExtractLastComponent(const char* path, char* out, size_t cap){
|
||
if (!out || cap == 0) return;
|
||
out[0] = 0;
|
||
if (!path) return;
|
||
|
||
size_t n = strlen(path);
|
||
if (n == 0) return;
|
||
|
||
// Skip trailing '\' (but keep root like "E:\")
|
||
if (n > 3 && path[n-1] == '\\') --n;
|
||
|
||
// Find last '\'
|
||
int i = (int)n - 1;
|
||
while (i >= 0 && path[i] != '\\') --i;
|
||
|
||
const char* start = path + i + 1;
|
||
size_t len = n - (i + 1);
|
||
if (len >= cap) len = cap - 1;
|
||
if (cap) { memcpy(out, start, len); out[len] = 0; }
|
||
}
|
||
|
||
} // anonymous namespace
|
||
|
||
FileBrowserApp& FileBrowserApp::Get() { static FileBrowserApp app; return app; }
|
||
|
||
char* FileBrowserApp::GetCurrentIp() { return mCurrentIp; }
|
||
void FileBrowserApp::SetCurrentIp(char* ip) { strncpy(mCurrentIp, ip, sizeof(mCurrentIp)); mCurrentIp[sizeof(mCurrentIp) - 1] = '\0'; }
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// ComputeResponsiveLayout
|
||
// - Derives all pane/header/row metrics from the current device viewport.
|
||
// - Ensures perfect left/right symmetry: [margin] [left pane] [gap] [right pane] [margin].
|
||
// - Keeps header bars exactly the same width as the panes.
|
||
// ----------------------------------------------------------------------------
|
||
void FileBrowserApp::ComputeResponsiveLayout() {
|
||
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
|
||
|
||
// ---- outer geometry ----
|
||
const FLOAT margin = SafeMarginX((FLOAT)vp.Width, (FLOAT)vp.Height);
|
||
const FLOAT gap = max(24.0f, vp.Width * 0.035f);
|
||
const FLOAT paneW = max(260.0f, (vp.Width - (margin * 2.0f) - gap) * 0.5f);
|
||
|
||
kPaneGap = gap;
|
||
kListX_L = margin;
|
||
kListW = paneW;
|
||
|
||
// header band
|
||
kHdrW = kListW;
|
||
kHdrY = max(12.0f, vp.Height * 0.03f);
|
||
kHdrH = max(22.0f, vp.Height * 0.04f);
|
||
|
||
// row height
|
||
kLineH = max(22.0f, vp.Height * 0.036f);
|
||
|
||
// keep kListY as the "pane body top" (what the rest of the code expects)
|
||
const FLOAT headerGap = max(6.0f, kHdrH * 0.35f);
|
||
kListY = kHdrY + kHdrH + headerGap;
|
||
|
||
// ---- visible rows (match PaneRenderer::DrawPane vertical math) ----
|
||
// DrawPane uses: listTop = (kListY) + colHdrH
|
||
const FLOAT colHdrH = max(22.0f, kLineH);
|
||
const FLOAT listTopInRenderer = kListY + colHdrH;
|
||
|
||
// leave room for the footer band + a small spacer at every resolution
|
||
const FLOAT footerBand = max(48.0f, vp.Height * 0.09f);
|
||
const FLOAT footerSpacer = max(6.0f, vp.Height * 0.012f);
|
||
const FLOAT bottomY = (FLOAT)vp.Height - footerBand - footerSpacer;
|
||
|
||
FLOAT usableH = bottomY - listTopInRenderer;
|
||
if (usableH < 0) usableH = 0;
|
||
|
||
m_visible = (int)(usableH / kLineH);
|
||
if (m_visible < 6) m_visible = 6;
|
||
if (m_visible > 30) m_visible = 30;
|
||
|
||
// fixed micro-metrics
|
||
kGutterW = 24.0f;
|
||
kPaddingX = 6.0f;
|
||
kScrollBarW = 3.0f;
|
||
}
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// ctor: set defaults for input, layout, D3D, and app state
|
||
FileBrowserApp::FileBrowserApp() {
|
||
m_visible=13; m_prevA=0; m_prevB=0; m_prevX=0; m_prevY=0; m_active=0;
|
||
m_prevButtons = 0; m_prevWhite = m_prevBlack = 0;
|
||
m_navUDHeld=false; m_navUDDir=0; m_navUDNext=0; m_backConfirmArmed = false;
|
||
m_backConfirmUntil = 0; m_prevBack = 0;
|
||
m_dvdUsedBytes = 0;
|
||
m_dvdTotalBytes = 0;
|
||
m_dvdHaveStats = false;
|
||
m_menuDepth = 0;
|
||
|
||
// --- Auto-detect video capabilities and set PresentParams ----------------
|
||
ZeroMemory(&m_d3dpp, sizeof(m_d3dpp));
|
||
|
||
// Dashboard video flags and TV standard
|
||
const DWORD vf = XGetVideoFlags();
|
||
const DWORD vs = XGetVideoStandard(); // XC_VIDEO_STANDARD_*
|
||
|
||
const bool hasWS = (vf & XC_VIDEO_FLAGS_WIDESCREEN) != 0;
|
||
const bool has480p = (vf & XC_VIDEO_FLAGS_HDTV_480p) != 0;
|
||
const bool has720p = (vf & XC_VIDEO_FLAGS_HDTV_720p) != 0;
|
||
const bool has1080i = (vf & XC_VIDEO_FLAGS_HDTV_1080i) != 0;
|
||
|
||
// Treat anything not NTSC-M as PAL family (PAL-I/M/N). We only need
|
||
// this to decide the 576i PAL-50 fallback when 480p isn’t enabled.
|
||
const bool isPAL = (vs != XC_VIDEO_STANDARD_NTSC_M);
|
||
|
||
// Choose a backbuffer the TV actually supports.
|
||
// Priority: 720p -> 1080i -> PAL-50 (576i) -> 480 (i/p depending on flags)
|
||
if (has720p) {
|
||
m_d3dpp.BackBufferWidth = 1280;
|
||
m_d3dpp.BackBufferHeight = 720;
|
||
m_d3dpp.Flags = D3DPRESENTFLAG_PROGRESSIVE | D3DPRESENTFLAG_WIDESCREEN; // 720p is always 16:9
|
||
m_d3dpp.FullScreen_RefreshRateInHz = 60;
|
||
}
|
||
else if (has1080i) {
|
||
m_d3dpp.BackBufferWidth = 1920;
|
||
m_d3dpp.BackBufferHeight = 1080;
|
||
m_d3dpp.Flags = D3DPRESENTFLAG_INTERLACED | D3DPRESENTFLAG_WIDESCREEN; // 1080i is always 16:9
|
||
m_d3dpp.FullScreen_RefreshRateInHz = 60;
|
||
}
|
||
else if (isPAL && !has480p) {
|
||
// PAL-50: 576i
|
||
m_d3dpp.BackBufferWidth = 720;
|
||
m_d3dpp.BackBufferHeight = 576;
|
||
m_d3dpp.Flags = D3DPRESENTFLAG_INTERLACED;
|
||
if (hasWS) m_d3dpp.Flags |= D3DPRESENTFLAG_WIDESCREEN; // 16:9 if user enabled
|
||
m_d3dpp.FullScreen_RefreshRateInHz = 50;
|
||
}
|
||
else {
|
||
// 480 path (NTSC or PAL-60/480p)
|
||
m_d3dpp.BackBufferWidth = 640;
|
||
m_d3dpp.BackBufferHeight = 480;
|
||
m_d3dpp.Flags = 0;
|
||
if (has480p) m_d3dpp.Flags |= D3DPRESENTFLAG_PROGRESSIVE; // 480p if enabled
|
||
else m_d3dpp.Flags |= D3DPRESENTFLAG_INTERLACED; // otherwise 480i
|
||
if (hasWS) m_d3dpp.Flags |= D3DPRESENTFLAG_WIDESCREEN; // 16:9 anamorphic if enabled
|
||
m_d3dpp.FullScreen_RefreshRateInHz = 60; // NTSC & PAL-60
|
||
}
|
||
|
||
// Common params
|
||
m_d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
|
||
m_d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||
m_d3dpp.EnableAutoDepthStencil = TRUE;
|
||
m_d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8;
|
||
// -------------------------------------------------------------------------
|
||
|
||
m_mode=MODE_BROWSE;
|
||
m_status[0]=0; m_statusUntilMs=0;
|
||
m_dvdUsedBytes = 0;
|
||
m_dvdTotalBytes = 0;
|
||
m_dvdHaveStats = 0;
|
||
|
||
}
|
||
|
||
// --- Exit helper ------------------------------------------------------------
|
||
// Jump to dashboard via XLaunchNewImage (fallback)
|
||
void FileBrowserApp::ExitNow() {
|
||
XLaunchNewImage(NULL, NULL); // returns to dashboard
|
||
}
|
||
|
||
// ----- status ---------------------------------------------------------------
|
||
// Set a 3s status toast at the footer (printf-style).
|
||
void FileBrowserApp::SetStatus(const char* fmt, ...) {
|
||
va_list ap; va_start(ap, fmt);
|
||
_vsnprintf(m_status, sizeof(m_status), fmt, ap);
|
||
va_end(ap);
|
||
m_status[sizeof(m_status)-1]=0;
|
||
m_statusUntilMs = GetTickCount() + 3000; // show for about 3s
|
||
}
|
||
|
||
// Append the last Win32 error to a prefix and show as status.
|
||
void FileBrowserApp::SetStatusLastErr(const char* prefix) {
|
||
DWORD e = GetLastError();
|
||
char msg[64]; _snprintf(msg, sizeof(msg), "%s (err=%lu)", prefix, (unsigned long)e);
|
||
msg[sizeof(msg)-1]=0;
|
||
SetStatus("%s", msg);
|
||
}
|
||
|
||
// ----- listing helpers ------------------------------------------------------
|
||
// Refresh a pane while trying to preserve selection and scroll.
|
||
void FileBrowserApp::RefreshPane(Pane& p) {
|
||
if (p.mode==1){
|
||
int prevSel = p.sel;
|
||
int prevScroll= p.scroll;
|
||
|
||
ListDirectory(p.curPath, p.items);
|
||
|
||
if (prevSel >= p.items.size()) prevSel = p.items.size()-1;
|
||
if (prevSel < 0) prevSel = 0;
|
||
p.sel = prevSel;
|
||
|
||
int maxScroll = max(0, p.items.size() - m_visible);
|
||
if (prevScroll > maxScroll) prevScroll = maxScroll;
|
||
if (prevScroll < 0) prevScroll = 0;
|
||
p.scroll = prevScroll;
|
||
}
|
||
else {
|
||
// Drive list mode: re-enumerate mounted roots
|
||
BuildDriveItems(p.items);
|
||
if (p.sel >= p.items.size()) p.sel = p.items.size() - 1;
|
||
if (p.sel < 0) p.sel = 0;
|
||
p.scroll = 0;
|
||
}
|
||
}
|
||
|
||
// Resolve the destination directory for copy/move based on the other pane.
|
||
// Returns normalized path with trailing slash in outDst on success.
|
||
bool FileBrowserApp::ResolveDestDir(char* outDst, size_t cap) {
|
||
Pane& dst = m_pane[1 - m_active];
|
||
outDst[0] = 0;
|
||
|
||
if (dst.mode == 1) {
|
||
_snprintf(outDst, (int)cap, "%s", dst.curPath);
|
||
outDst[cap-1]=0;
|
||
NormalizeDirA(outDst);
|
||
return true;
|
||
}
|
||
// If the other pane is the drive list, allow selecting a drive as destination.
|
||
if (!dst.items.empty()){
|
||
const Item& di = dst.items[dst.sel];
|
||
if (di.isDir && !di.isUpEntry){
|
||
_snprintf(outDst, (int)cap, "%s", di.name); // e.g. "E:\"
|
||
outDst[cap-1]=0;
|
||
NormalizeDirA(outDst);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Resolve the source directory for unzip based on the current pane.
|
||
// Returns normalized path with trailing slash in outDst on success.
|
||
bool FileBrowserApp::ResolveSrcDir(char* srcDst, size_t cap) {
|
||
Pane& dst = m_pane[m_active];
|
||
srcDst[0] = 0;
|
||
|
||
if (dst.mode == 1) {
|
||
_snprintf(srcDst, (int)cap, "%s", dst.curPath);
|
||
srcDst[cap - 1] = 0;
|
||
NormalizeDirA(srcDst);
|
||
return true;
|
||
}
|
||
// If the other pane is the drive list, allow selecting a drive as destination.
|
||
if (!dst.items.empty()) {
|
||
const Item& di = dst.items[dst.sel];
|
||
if (di.isDir && !di.isUpEntry) {
|
||
_snprintf(srcDst, (int)cap, "%s", di.name); // e.g. "E:\"
|
||
srcDst[cap - 1] = 0;
|
||
NormalizeDirA(srcDst);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Select an item in a pane by name and adjust scroll to reveal it.
|
||
void FileBrowserApp::SelectItemInPane(Pane& p, const char* name) {
|
||
if (!name || p.items.empty()) return;
|
||
for (int i=0;i<(int)p.items.size();++i){
|
||
if (_stricmp(p.items[i].name, name) == 0){
|
||
p.sel = i;
|
||
if (p.sel < p.scroll) p.scroll = p.sel;
|
||
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void FileBrowserApp::BuildZipSubMenu() {
|
||
m_zipSubMenu.SetLabel("Select destination");
|
||
|
||
Pane& p = m_pane[m_active];
|
||
Pane& p2 = m_pane[1 - m_active];
|
||
bool inDir = (p.mode == 1);
|
||
bool inDir2 = (p2.mode == 1);
|
||
bool hasSel = !p.items.empty();
|
||
bool isFile = false;
|
||
if (hasSel) {
|
||
const Item& cur = p.items[p.sel];
|
||
if (inDir && !cur.isUpEntry && !cur.isDir) isFile = true;
|
||
}
|
||
|
||
m_zipSubMenu.Clear();
|
||
m_zipSubMenu.AddItem("here", ACT_UNZIPHERE, (true));
|
||
|
||
char* offset = " ";
|
||
if (isFile) {
|
||
char buf[256] = "to ";
|
||
strcat(buf, "\"");
|
||
char name[64];
|
||
strcpy(name, p.items[p.sel].name);
|
||
name[strlen(name) - 4] = '\0';
|
||
strcat(buf, name);
|
||
strcat(buf, "\\\"");
|
||
strcat(buf, offset);
|
||
|
||
char unzipTo[256];
|
||
EllipsizeAnsiToFit(m_font, buf, m_zipSubMenu.m_Mw + GetAnsiW(m_font, offset), unzipTo, sizeof(unzipTo), ELLIPSIZE_CENTER);
|
||
unzipTo[strlen(unzipTo) - strlen(offset)] = '\0';
|
||
|
||
m_zipSubMenu.AddItem(unzipTo, ACT_UNZIPTO, (true));
|
||
}
|
||
|
||
if (isFile && inDir2) {
|
||
char buf[512] = "to ";
|
||
strcat(buf, "\"");
|
||
strcat(buf, p2.curPath);
|
||
if (buf[strlen(buf) - 1] != '\\') strcat(buf, "\\");
|
||
strcat(buf, "\"");
|
||
strcat(buf, offset);
|
||
|
||
char unzipTo[256];
|
||
EllipsizeAnsiToFit(m_font, buf, m_zipSubMenu.m_Mw + GetAnsiW(m_font, offset), unzipTo, sizeof(unzipTo), ELLIPSIZE_CENTER);
|
||
unzipTo[strlen(unzipTo) - strlen(offset)] = '\0';
|
||
|
||
m_zipSubMenu.AddItem(unzipTo, ACT_UNZIPTOOTHER, (true));
|
||
}
|
||
}
|
||
|
||
void FileBrowserApp::BuildConfirmDelSubMenu() {
|
||
m_confirmDelSubMenu.SetLabel("Are you sure?");
|
||
|
||
m_confirmDelSubMenu.Clear();
|
||
|
||
m_confirmDelSubMenu.AddItem("Yes", ACT_DELETE, (true));
|
||
m_confirmDelSubMenu.AddItem("No", ACT_CANCEL, (true));
|
||
}
|
||
|
||
// Build the context menu based on current mode and selection.
|
||
void FileBrowserApp::BuildContextMenu() {
|
||
m_ctx.SetLabel("Select action");
|
||
|
||
Pane& p = m_pane[m_active];
|
||
Pane& p2 = m_pane[1 - m_active];
|
||
bool inDir = (p.mode == 1);
|
||
bool inDir2 = (p2.mode == 1);
|
||
bool hasSel = !p.items.empty();
|
||
bool hasSel2 = !p2.items.empty();
|
||
bool isFile = false;
|
||
bool isFile2 = false;
|
||
const char* ext = NULL;
|
||
const char* ext2 = NULL;
|
||
if (hasSel){
|
||
const Item& cur = p.items[p.sel];
|
||
if (inDir && !cur.isUpEntry && !cur.isDir) isFile = true;
|
||
if (isFile) ext = GetExtension(cur.name);
|
||
}
|
||
if (hasSel2){
|
||
const Item& cur2 = p2.items[p2.sel];
|
||
if (inDir2 && !cur2.isUpEntry && !cur2.isDir) isFile2 = true;
|
||
if (isFile2) ext2 = GetExtension(cur2.name);
|
||
}
|
||
|
||
int marked = 0; for (size_t i=0; i<p.items.size(); ++i) if (p.items[i].marked) ++marked;
|
||
|
||
m_ctx.Clear();
|
||
|
||
// Common operations
|
||
if (ext && _stricmp(ext, "xbe") == 0)
|
||
m_ctx.AddItem("Launch", ACT_OPEN, (hasSel));
|
||
else
|
||
m_ctx.AddItem("Open", ACT_OPEN, (hasSel));
|
||
m_ctx.AddItem("Copy", ACT_COPY, (inDir && hasSel && inDir2));
|
||
m_ctx.AddItem("Move", ACT_MOVE, (inDir && hasSel && inDir2));
|
||
if (inDir && hasSel && !p.items[p.sel].isUpEntry) { BuildConfirmDelSubMenu();
|
||
m_ctx.AddSubMenu("Delete", &m_confirmDelSubMenu, (true));}
|
||
m_ctx.AddItem("Rename", ACT_RENAME, (inDir && hasSel));
|
||
|
||
if (ext && _stricmp(ext, "ips") == 0)
|
||
m_ctx.AddItem("Apply ips", ACT_APPLYIPS, (ext2 && _stricmp(ext2, "xbe") == 0));
|
||
if (ext && _stricmp(ext, "bak") == 0)
|
||
m_ctx.AddItem("Restore bak", ACT_RESTOREBAK, (true));
|
||
if (ext && _stricmp(ext, "zip") == 0) { BuildZipSubMenu();
|
||
m_ctx.AddSubMenu("Unzip", &m_zipSubMenu, (true));}
|
||
|
||
m_ctx.AddItem("Make new folder", ACT_MKDIR, (inDir));
|
||
m_ctx.AddItem("Calculate size", ACT_CALCSIZE, (hasSel));
|
||
m_ctx.AddItem("Go to root", ACT_GOROOT, (inDir));
|
||
|
||
// Marking tools (directory mode only; skip the ".." row)
|
||
if (inDir) {
|
||
int selectable = 0;
|
||
for (size_t i=0;i<p.items.size(); ++i) if (!p.items[i].isUpEntry) ++selectable;
|
||
if (selectable > 0) {
|
||
m_ctx.AddItem("Mark all", ACT_MARK_ALL, true);
|
||
m_ctx.AddItem("Invert marks", ACT_INVERT_MARKS, true);
|
||
}
|
||
}
|
||
if (marked) m_ctx.AddItem("Clear marks", ACT_CLEAR_MARKS, true);
|
||
|
||
// Bottom-only item on drive list: destructive cache format.
|
||
if (p.mode == 0) {
|
||
m_ctx.AddSeparator(); // visual separator (non-selectable)
|
||
m_ctx.AddItem("Format cache (X/Y/Z)", ACT_FORMAT_CACHE, true);
|
||
}
|
||
}
|
||
|
||
// Open and close the menu; switch mode so browse input pauses.
|
||
void FileBrowserApp::OpenMenu() {
|
||
BuildContextMenu();
|
||
|
||
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
|
||
|
||
const FLOAT rowH = kLineH + 6.0f;
|
||
const FLOAT desiredW = 340.0f; // usual width
|
||
const FLOAT safeInset = SafeMarginX((FLOAT)vp.Width, (FLOAT)vp.Height);
|
||
|
||
// Max width we can draw while keeping the frame fully inside the safe band
|
||
const FLOAT maxWInsideSafe = (FLOAT)vp.Width - safeInset*2.0f - 12.0f; // -12 for the outer frame
|
||
const FLOAT menuW = max(220.0f, min(desiredW, min(kListW, maxWInsideSafe)));
|
||
|
||
// Center over the active pane first...
|
||
const FLOAT paneX = (m_active==0) ? kListX_L : (kListX_L + kListW + kPaneGap);
|
||
FLOAT x = paneX + (kListW - menuW)*0.5f;
|
||
|
||
// ...then clamp so it never goes past the safe area
|
||
if (x < safeInset) x = safeInset;
|
||
if (x + menuW > (FLOAT)vp.Width - safeInset) x = (FLOAT)vp.Width - safeInset - menuW;
|
||
|
||
const FLOAT listTop = kListY + kLineH - 10.0f; // Why -10.0f?
|
||
const int visibleIndex = m_pane[m_active].sel - m_pane[m_active].scroll;
|
||
const FLOAT y = listTop + (visibleIndex - 1) * kLineH;
|
||
|
||
m_ctx.OpenAt(x, y, menuW, rowH);
|
||
|
||
m_menuStack[0] = &m_ctx;
|
||
m_menuDepth = 1;
|
||
|
||
m_mode = MODE_MENU;
|
||
}
|
||
|
||
void FileBrowserApp::CloseMenu() {
|
||
for (int i = 0; i < m_menuDepth; ++i) {
|
||
if (m_menuStack[i]) m_menuStack[i]->Close();
|
||
}
|
||
|
||
m_menuDepth = 0;
|
||
|
||
m_mode=MODE_BROWSE;
|
||
}
|
||
|
||
// ----- rename lifecycle (OnScreenKeyboard) ----------------------------------
|
||
// Start rename using the OSD keyboard. Close menu first so the A press
|
||
// that opened the keyboard does not trigger other UI.
|
||
void FileBrowserApp::BeginRename(const char* parentDir, const char* oldName) {
|
||
m_ctx.Close(); // ensure menu closes
|
||
|
||
m_kb.Open(parentDir ? parentDir : "", oldName ? oldName : "");
|
||
m_mode = MODE_RENAME;
|
||
}
|
||
void FileBrowserApp::CancelRename() {
|
||
m_kb.Close();
|
||
m_mode=MODE_BROWSE;
|
||
}
|
||
void FileBrowserApp::AcceptRename() {
|
||
const char* newName = m_kb.Buffer();
|
||
if (!newName) { CancelRename(); return; }
|
||
|
||
Pane& ap = m_pane[m_active];
|
||
if (ap.mode != 1 || ap.items.empty()) { SetStatus("Rename failed: no selection"); CancelRename(); return; }
|
||
|
||
const Item& sel = ap.items[ap.sel];
|
||
if (sel.isUpEntry) { SetStatus("Rename failed: invalid selection"); CancelRename(); return; }
|
||
|
||
// Sanitize to FATX-friendly, then MoveFile within current dir.
|
||
char clean[256]; _snprintf(clean, sizeof(clean), "%s", newName); clean[sizeof(clean)-1]=0;
|
||
SanitizeFatxNameInPlace(clean);
|
||
if (_stricmp(clean, sel.name)==0){ SetStatus("No change"); CancelRename(); return; }
|
||
|
||
char oldPath[512]; JoinPath(oldPath, sizeof(oldPath), ap.curPath, sel.name);
|
||
char newPath[512]; JoinPath(newPath, sizeof(newPath), ap.curPath, clean);
|
||
|
||
if (MoveFileA(oldPath, newPath)){
|
||
SetStatus("Renamed to %s", clean);
|
||
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
|
||
SelectItemInPane(ap, clean);
|
||
}else{
|
||
SetStatusLastErr("Rename failed");
|
||
RefreshPane(m_pane[0]); RefreshPane(m_pane[1]);
|
||
}
|
||
CancelRename();
|
||
}
|
||
|
||
// Absorb the current pad state so the button used to accept/cancel the keyboard
|
||
// does not fall through and act in browse/menu mode.
|
||
void FileBrowserApp::AbsorbPadState(const XBGAMEPAD& pad) {
|
||
m_prevButtons = pad.wButtons;
|
||
m_prevA = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
|
||
m_prevB = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
|
||
m_prevX = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
|
||
m_prevY = pad.bAnalogButtons[XINPUT_GAMEPAD_Y];
|
||
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
|
||
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
|
||
}
|
||
|
||
// ----- input: rename modal --------------------------------------------------
|
||
// While the keyboard is active, route inputs to it only.
|
||
void FileBrowserApp::OnPad_Rename(const XBGAMEPAD& pad) {
|
||
OnScreenKeyboard::Result r = m_kb.OnPad(pad);
|
||
|
||
if (r == OnScreenKeyboard::ACCEPTED){
|
||
AbsorbPadState(pad);
|
||
AcceptRename();
|
||
return;
|
||
}
|
||
if (r == OnScreenKeyboard::CANCELED){
|
||
AbsorbPadState(pad);
|
||
CancelRename();
|
||
return;
|
||
}
|
||
|
||
// Still in keyboard: keep edge state roughly synced
|
||
AbsorbPadState(pad);
|
||
}
|
||
|
||
|
||
// ----- input: context menu (delegated) -------------------------------------
|
||
// Feed pad to the menu; on selection, execute via central AppActions.
|
||
void FileBrowserApp::OnPad_Menu(const XBGAMEPAD& pad) {
|
||
Action act;
|
||
ContextMenu* top = m_menuStack[m_menuDepth - 1];
|
||
ContextMenu::Result r = top->OnPad(pad, act);
|
||
if (r == ContextMenu::CHOSEN) {
|
||
if (act == ACT_CANCEL) r = ContextMenu::CLOSED;
|
||
else {
|
||
CloseMenu();
|
||
//SetStatus("Chosen action=%d", (int)act); // debug toast
|
||
Actions::Execute(act); // perform action
|
||
}
|
||
}
|
||
if (r == ContextMenu::CLOSED) { // allow fallthrough
|
||
if (m_menuDepth > 1) {
|
||
m_menuDepth--;
|
||
ContextMenu* parent = m_menuStack[m_menuDepth - 1];
|
||
parent->AbsorbPadState(pad);
|
||
} else {
|
||
m_menuDepth = 0;
|
||
CloseMenu();
|
||
}
|
||
|
||
} else if (r == ContextMenu::SUBMENU_OPENED) {
|
||
ContextMenu* parent = m_menuStack[m_menuDepth - 1];
|
||
ContextMenu* child = parent->GetSelectedChildMenu();
|
||
|
||
if (child) {
|
||
m_menuStack[m_menuDepth] = child;
|
||
m_menuDepth++;
|
||
}
|
||
}
|
||
|
||
// Sync edge state so the next browse frame does not double-fire.
|
||
m_prevButtons = pad.wButtons;
|
||
m_prevA = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
|
||
m_prevB = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
|
||
m_prevX = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
|
||
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
|
||
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
|
||
}
|
||
|
||
// ----- input: browse mode (with auto-repeat) -------------------------------
|
||
// Main file-list navigation. Handles dpad/analog, paging, selection,
|
||
// pane switching, and quick mark/unmark on Y.
|
||
void FileBrowserApp::OnPad_Browse(const XBGAMEPAD& pad) {
|
||
const DWORD btn = pad.wButtons;
|
||
Pane& p = m_pane[m_active];
|
||
|
||
// up/down auto-repeat with initial delay
|
||
const DWORD now = GetTickCount();
|
||
const int kDead = 16000; // analog deadzone for vertical stick
|
||
const DWORD kInit = 230; // initial delay (ms)
|
||
const DWORD kRep = 120; // repeat rate (ms)
|
||
|
||
// Compute desired move: -1 up, +1 down, 0 none.
|
||
int ud = 0;
|
||
if ((btn & XINPUT_GAMEPAD_DPAD_UP) || pad.sThumbLY > kDead) ud = -1;
|
||
if ((btn & XINPUT_GAMEPAD_DPAD_DOWN) || pad.sThumbLY < -kDead) ud = +1;
|
||
|
||
// Repeat-state machine for smooth scrolling.
|
||
if (ud == 0){
|
||
m_navUDHeld = false; m_navUDDir = 0;
|
||
}else{
|
||
if (!m_navUDHeld || m_navUDDir != ud){
|
||
// first step
|
||
if (ud < 0){
|
||
if (p.sel > 0){ --p.sel; if (p.sel < p.scroll) p.scroll = p.sel; }
|
||
}else{
|
||
int maxSel = (int)p.items.size() - 1;
|
||
if (p.sel < maxSel){ ++p.sel; if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1); }
|
||
}
|
||
m_navUDHeld = true; m_navUDDir = ud; m_navUDNext = now + kInit;
|
||
}else if (now >= m_navUDNext){
|
||
// repeats
|
||
if (ud < 0){
|
||
if (p.sel > 0){ --p.sel; if (p.sel < p.scroll) p.scroll = p.sel; }
|
||
}else{
|
||
int maxSel = (int)p.items.size() - 1;
|
||
if (p.sel < maxSel){ ++p.sel; if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1); }
|
||
}
|
||
m_navUDNext = now + kRep;
|
||
}
|
||
}
|
||
|
||
// pane switch via dpad left/right (edge-detected)
|
||
bool leftTrig = (btn & XINPUT_GAMEPAD_DPAD_LEFT) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_LEFT);
|
||
bool rightTrig = (btn & XINPUT_GAMEPAD_DPAD_RIGHT) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_RIGHT);
|
||
if (leftTrig) m_active = 0;
|
||
if (rightTrig) m_active = 1;
|
||
|
||
// Other buttons (edge-detected)
|
||
unsigned char a = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
|
||
unsigned char b = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
|
||
unsigned char x = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
|
||
unsigned char y = pad.bAnalogButtons[XINPUT_GAMEPAD_Y];
|
||
unsigned char w = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE]; // page down
|
||
unsigned char k = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK]; // page up
|
||
|
||
bool aTrig = (a > 30 && m_prevA <= 30);
|
||
bool bTrig = (b > 30 && m_prevB <= 30);
|
||
bool xTrig = (x > 30 && m_prevX <= 30);
|
||
bool yTrig = (y > 30 && m_prevY <= 30);
|
||
bool wTrig = (w > 30 && m_prevWhite <= 30);
|
||
bool kTrig = (k > 30 && m_prevBlack <= 30);
|
||
|
||
// Open context menu on X
|
||
if (xTrig) {
|
||
OpenMenu();
|
||
// absorb state so the X press does not leak into menu
|
||
m_prevButtons = btn;
|
||
m_prevA = a; m_prevB = b; m_prevX = x;
|
||
m_prevWhite = w; m_prevBlack = k;
|
||
return;
|
||
}
|
||
|
||
// A = enter, B = up one
|
||
if (aTrig) EnterSelection(p);
|
||
if (bTrig) UpOne(p);
|
||
|
||
// Black/White = page up/down
|
||
if (kTrig) { // BLACK: page up
|
||
p.sel -= m_visible; if (p.sel < 0) p.sel = 0;
|
||
if (p.sel < p.scroll) p.scroll = p.sel;
|
||
}
|
||
if (wTrig) { // WHITE: page down
|
||
int maxSel = (int)p.items.size()-1;
|
||
p.sel += m_visible; if (p.sel > maxSel) p.sel = maxSel;
|
||
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
|
||
}
|
||
|
||
// Y = quick toggle mark (skips the ".." entry)
|
||
if (yTrig) {
|
||
if (p.mode==1 && !p.items.empty()) {
|
||
Item& it = p.items[p.sel];
|
||
if (!it.isUpEntry) {
|
||
it.marked = !it.marked;
|
||
SetStatus(it.marked ? "Marked" : "Unmarked");
|
||
|
||
// Optional auto-advance to speed through marking multiple items.
|
||
int maxSel = (int)p.items.size()-1;
|
||
if (p.sel < maxSel) {
|
||
++p.sel;
|
||
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Save edge baselines for next frame.
|
||
m_prevButtons = btn;
|
||
m_prevA = a; m_prevB = b; m_prevX = x; m_prevY = y;
|
||
m_prevWhite = w; m_prevBlack = k;
|
||
}
|
||
|
||
// ----- input router ---------------------------------------------------------
|
||
// Route pad to sub-handlers based on current modal state.
|
||
void FileBrowserApp::OnPad(const XBGAMEPAD& pad) {
|
||
|
||
// --- Back-to-exit (press Back twice) -----------------------------------
|
||
const bool backNow = (pad.wButtons & XINPUT_GAMEPAD_BACK) != 0;
|
||
const bool backTrig = backNow && !(m_prevButtons & XINPUT_GAMEPAD_BACK);
|
||
DWORD now = GetTickCount();
|
||
|
||
if (backTrig) {
|
||
if (m_backConfirmArmed && now < m_statusUntilMs) {
|
||
ExitNow();
|
||
return;
|
||
}
|
||
else {
|
||
m_backConfirmArmed = true;
|
||
SetStatus("Press \x84 again to exit");
|
||
m_backConfirmUntil = m_statusUntilMs; // optional snapshot
|
||
}
|
||
}
|
||
|
||
if (m_backConfirmArmed && now >= m_statusUntilMs){
|
||
m_backConfirmArmed = false;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
|
||
if (m_mode == MODE_RENAME) { OnPad_Rename(pad); return; }
|
||
if (m_mode == MODE_MENU) { OnPad_Menu(pad); return; }
|
||
OnPad_Browse(pad);
|
||
}
|
||
|
||
|
||
// Per-frame app logic. Also poll for drive-set changes and refresh panes.
|
||
HRESULT FileBrowserApp::FrameMove() {
|
||
XBInput_GetInput();
|
||
|
||
// Check ip
|
||
if (network::isReady() == true)
|
||
{
|
||
XNADDR addr;
|
||
memset(&addr, 0, sizeof(addr));
|
||
DWORD dwState = XNetGetTitleXnAddr(&addr);
|
||
if (dwState != XNET_GET_XNADDR_PENDING)
|
||
{
|
||
char* ipAddress = (XNetGetEthernetLinkStatus() & XNET_ETHERNET_LINK_ACTIVE) ? stringUtility::formatString("%i.%i.%i.%i",
|
||
addr.ina.S_un.S_un_b.s_b1,
|
||
addr.ina.S_un.S_un_b.s_b2,
|
||
addr.ina.S_un.S_un_b.s_b3,
|
||
addr.ina.S_un.S_un_b.s_b4) : _strdup("0.0.0.0");
|
||
char* currentIp = GetCurrentIp();
|
||
if (strcmp(ipAddress, currentIp) != 0) SetCurrentIp(ipAddress);
|
||
}
|
||
}
|
||
|
||
// --- DVD tray/media polling + serial watchdog (no stale listings) ----------
|
||
static DWORD s_nextDvdPollMs = 0;
|
||
static DWORD s_lastDvdSerial = 0xFFFFFFFF; // unknown
|
||
static BOOL s_dMapped = FALSE; // our belief about D: mapping
|
||
|
||
DWORD now2 = GetTickCount();
|
||
if (now2 >= s_nextDvdPollMs) {
|
||
s_nextDvdPollMs = now2 + 250; // ~4 Hz
|
||
|
||
DWORD code = DvdGetDriveStateOneShot(); // DRIVE_* or DRIVE_READY (no change)
|
||
if (code != TRAY_NO_CHANGE) {
|
||
BOOL needRefresh = FALSE;
|
||
|
||
switch (code) {
|
||
case TRAY_OPEN:
|
||
case TRAY_CLOSED_NO_MEDIA:
|
||
s_dMapped = FALSE;
|
||
s_lastDvdSerial = 0xFFFFFFFF; // forget old serial
|
||
m_dvdHaveStats = false; // <— clear
|
||
m_dvdUsedBytes = 0;
|
||
m_dvdTotalBytes = 0;
|
||
needRefresh = TRUE;
|
||
//SetStatus((code == DRIVE_OPEN) ? "Tray opened" : "No disc");
|
||
break;
|
||
|
||
case TRAY_CLOSED_MEDIA_PRESENT:
|
||
s_dMapped = TRUE;
|
||
|
||
DWORD curSer = 0;
|
||
if (GetDvdVolumeSerial(&curSer)) s_lastDvdSerial = curSer;
|
||
|
||
// Cache total (capacity) and used (sum of files on disc)
|
||
ULONGLONG fb = 0, tb = 0;
|
||
GetDriveFreeTotal("DVD-ROM:\\", fb, tb);
|
||
m_dvdTotalBytes = tb;
|
||
m_dvdUsedBytes = DirSizeRecursiveA("DVD-ROM:\\"); // from FsUtil
|
||
m_dvdHaveStats = true;
|
||
|
||
//{ char lbl[64]; if (DvdDetectMediaSimple(lbl, sizeof(lbl))) SetStatus("%s", lbl); }
|
||
needRefresh = TRUE;
|
||
break;
|
||
}
|
||
|
||
if (needRefresh) {
|
||
// Refresh both panes; bounce out of D:\ if it vanished
|
||
for (int iPane = 0; iPane < 2; ++iPane) {
|
||
Pane& P = m_pane[iPane];
|
||
|
||
if (P.mode == 0) {
|
||
BuildDriveItems(P.items);
|
||
if (P.sel >= (int)P.items.size()) P.sel = (int)P.items.size()-1;
|
||
if (P.sel < 0) P.sel = 0;
|
||
P.scroll = 0;
|
||
} else if (IsDPath(P.curPath)) {
|
||
if (s_dMapped) {
|
||
// HARD re-list so a new disc shows correct files
|
||
ListDirectory(P.curPath, P.items);
|
||
if (P.sel >= (int)P.items.size()) P.sel = (int)P.items.size()-1;
|
||
if (P.sel < 0) P.sel = 0;
|
||
if (P.scroll > P.sel) P.scroll = P.sel;
|
||
{ int maxScroll = (int)P.items.size() - m_visible; if (maxScroll < 0) maxScroll = 0; if (P.scroll > maxScroll) P.scroll = maxScroll; }
|
||
} else {
|
||
// D: gone => drive list
|
||
P.mode = 0; P.curPath[0] = 0;
|
||
BuildDriveItems(P.items);
|
||
P.sel = 0; P.scroll = 0;
|
||
}
|
||
} else {
|
||
RefreshPane(P);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
OnPad(g_Gamepads[0]);
|
||
return S_OK;
|
||
}
|
||
|
||
// ----- draw: menu / rename / panes -----------------------------------------
|
||
// Draw the context menu if open (coordinates set in OpenMenu).
|
||
void FileBrowserApp::DrawMenu() {
|
||
if (m_menuDepth <= 0) return;
|
||
|
||
for (int i = 0; i < m_menuDepth; ++i) {
|
||
ContextMenu* cm = m_menuStack[i];
|
||
if (cm && cm->IsOpen()) cm->Draw(m_font, m_pd3dDevice);
|
||
}
|
||
}
|
||
|
||
// Draw OSD keyboard if active.
|
||
void FileBrowserApp::DrawRename() {
|
||
if (!m_kb.Active()) return;
|
||
m_kb.Draw(m_font, m_pd3dDevice, kLineH);
|
||
}
|
||
|
||
// ----- listing / navigation -------------------------------------------------
|
||
// Ensure a pane has items and indices are in range (after mode/path changes).
|
||
void FileBrowserApp::EnsureListing(Pane& p) {
|
||
if (p.mode==0) BuildDriveItems(p.items);
|
||
else ListDirectory(p.curPath, p.items);
|
||
|
||
if (p.sel >= (int)p.items.size()) p.sel = (int)p.items.size()-1;
|
||
if (p.sel < 0) p.sel = 0;
|
||
|
||
if (p.scroll > p.sel) p.scroll = p.sel;
|
||
int maxScroll = max(0, (int)p.items.size() - m_visible);
|
||
if (p.scroll > maxScroll) p.scroll = maxScroll;
|
||
}
|
||
|
||
// Enter current selection (drive -> directory, ".." -> up, dir -> descend,
|
||
// .xbe -> launch). Other files are no-op here.
|
||
void FileBrowserApp::EnterSelection(Pane& p) {
|
||
if (p.items.empty()) return;
|
||
const Item& it = p.items[p.sel];
|
||
|
||
// Drive list -> go into the chosen drive.
|
||
if (p.mode==0) {
|
||
strncpy(p.curPath,it.name,sizeof(p.curPath)-1); p.curPath[sizeof(p.curPath)-1]=0;
|
||
p.mode=1; p.sel=0; p.scroll=0; ListDirectory(p.curPath,p.items); return;
|
||
}
|
||
|
||
// Directory listing
|
||
if (it.isUpEntry) {
|
||
|
||
char childName[256];
|
||
ExtractLastComponent(p.curPath, childName, sizeof(childName));
|
||
|
||
// If already at drive root -> go to drive list
|
||
if (IsDriveRoot(p.curPath)) {
|
||
p.mode = 0;
|
||
p.sel = 0;
|
||
p.scroll = 0;
|
||
p.curPath[0] = 0;
|
||
BuildDriveItems(p.items);
|
||
return;
|
||
}
|
||
|
||
// Otherwise go up one directory
|
||
ParentPath(p.curPath);
|
||
p.sel = 0;
|
||
p.scroll = 0;
|
||
ListDirectory(p.curPath, p.items);
|
||
|
||
SelectItemInPane(p, childName);
|
||
|
||
return;
|
||
}
|
||
|
||
if (it.isDir) {
|
||
// Descend into subdirectory.
|
||
char next[512]; JoinPath(next,sizeof(next),p.curPath,it.name);
|
||
strncpy(p.curPath,next,sizeof(p.curPath)-1); p.curPath[sizeof(p.curPath)-1]=0;
|
||
p.sel=0; p.scroll=0; ListDirectory(p.curPath,p.items); return;
|
||
}
|
||
|
||
// Files: launch .xbe if selected (other file types are no-op here).
|
||
if (!it.isDir && !it.isUpEntry) {
|
||
if (HasXbeExt(it.name)) {
|
||
char full[512];
|
||
JoinPath(full, sizeof(full), p.curPath, it.name);
|
||
|
||
// Small Present for a snappy visual handoff before XLaunchNewImageA.
|
||
m_pd3dDevice->Present(NULL, NULL, NULL, NULL);
|
||
Sleep(10);
|
||
|
||
if (!LaunchXbeA(full)) {
|
||
SetStatusLastErr("Launch failed");
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Move up one level; from root goes back to drive list.
|
||
void FileBrowserApp::UpOne(Pane& p) {
|
||
// Already at drive list -> nothing to do
|
||
if (p.mode == 0) return;
|
||
|
||
// Save child name so parent can reselect it
|
||
char childName[256];
|
||
ExtractLastComponent(p.curPath, childName, sizeof(childName));
|
||
|
||
// --- CASE 1: We are at drive root (e.g. "E:\", "HDD0-C:\") ---
|
||
if (IsDriveRoot(p.curPath)) {
|
||
char driveRoot[512];
|
||
strncpy(driveRoot, p.curPath, sizeof(driveRoot) - 1);
|
||
driveRoot[sizeof(driveRoot) - 1] = 0;
|
||
|
||
// Switch to drive list
|
||
p.mode = 0;
|
||
p.sel = 0;
|
||
p.scroll = 0;
|
||
p.curPath[0] = 0;
|
||
|
||
BuildDriveItems(p.items);
|
||
|
||
// Reselect the drive we came from
|
||
for (int i = 0; i < (int)p.items.size(); ++i) {
|
||
if (_stricmp(p.items[i].name, driveRoot) == 0) {
|
||
p.sel = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Clamp scroll
|
||
if (p.sel < p.scroll) p.scroll = p.sel;
|
||
if (p.sel >= p.scroll + m_visible) p.scroll = p.sel - (m_visible - 1);
|
||
|
||
return;
|
||
}
|
||
|
||
// --- CASE 2: Normal directory -> go up one level ---
|
||
ParentPath(p.curPath);
|
||
|
||
p.sel = 0;
|
||
p.scroll = 0;
|
||
ListDirectory(p.curPath, p.items);
|
||
|
||
// Reselect folder we came from
|
||
SelectItemInPane(p, childName);
|
||
}
|
||
|
||
|
||
// ----- Initialize / Render --------------------------------------------------
|
||
// Create font, input, initial drive mapping, and compute responsive layout.
|
||
// ----------------------------------------------------------------------------
|
||
HRESULT FileBrowserApp::Initialize() {
|
||
if (FileExistsA(FONT_XPR_FILEPATH)) {
|
||
HRESULT hr = m_font.Create(FONT_XPR_FILEPATH, 0);
|
||
|
||
if (!SUCCEEDED(hr)) {
|
||
if (WriteAllA(FONT_XPR_FILEPATH, Font, (size_t)FontSize)) {
|
||
HRESULT hr2 = m_font.Create(FONT_XPR_FILEPATH, 0);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if (WriteAllA(FONT_XPR_FILEPATH, Font, (size_t)FontSize)) {
|
||
HRESULT hr = m_font.Create(FONT_XPR_FILEPATH, 0);
|
||
}
|
||
}
|
||
|
||
XBInput_CreateGamepads();
|
||
|
||
network::init();
|
||
|
||
BuildDriveItems(m_pane[0].items);
|
||
BuildDriveItems(m_pane[1].items);
|
||
|
||
// Layout derived from current backbuffer size (works for any resolution)
|
||
ComputeResponsiveLayout();
|
||
|
||
m_ctx.SetDevice(m_pd3dDevice);
|
||
m_zipSubMenu.SetDevice(m_pd3dDevice);
|
||
m_confirmDelSubMenu.SetDevice(m_pd3dDevice);
|
||
|
||
SetStatus(INITIAL_TOAST);
|
||
|
||
return S_OK;
|
||
}
|
||
|
||
// ----- progress overlay API -------------------------------------------------
|
||
// Begin a copy/move progress session (title and first label for marquee).
|
||
void FileBrowserApp::BeginProgress(ULONGLONG total, const char* firstLabel, const char* title) {
|
||
m_prog.active = true;
|
||
m_prog.done = 0;
|
||
m_prog.total = total;
|
||
|
||
_snprintf(m_prog.current, sizeof(m_prog.current), "%s", firstLabel ? firstLabel : "");
|
||
m_prog.current[sizeof(m_prog.current)-1] = 0;
|
||
|
||
_snprintf(m_prog.label, sizeof(m_prog.label), "%s", title ? title : "Working...");
|
||
m_prog.label[sizeof(m_prog.label)-1] = 0;
|
||
|
||
m_prog.lastPaintMs = 0;
|
||
}
|
||
|
||
// Update the overlay counters and optional label; throttles repaint to ~25 fps.
|
||
void FileBrowserApp::UpdateProgress(ULONGLONG done, ULONGLONG total, const char* label) {
|
||
m_prog.done = done;
|
||
m_prog.total = (total ? total : m_prog.total);
|
||
if (label){
|
||
_snprintf(m_prog.current, sizeof(m_prog.current), "%s", label);
|
||
m_prog.current[sizeof(m_prog.current)-1] = 0;
|
||
}
|
||
|
||
DWORD now = GetTickCount();
|
||
if (now - m_prog.lastPaintMs >= 40){
|
||
m_prog.lastPaintMs = now;
|
||
|
||
// redraw one frame (use the normal Render path)
|
||
Render();
|
||
Sleep(0);
|
||
}
|
||
}
|
||
|
||
// End and clear the progress overlay state.
|
||
void FileBrowserApp::EndProgress() {
|
||
m_prog.active = false;
|
||
}
|
||
|
||
// Draw the modal progress HUD (centered panel with marquee and bar).
|
||
void FileBrowserApp::DrawProgressOverlay() {
|
||
if (!m_prog.active) return;
|
||
|
||
D3DVIEWPORT8 vp; m_pd3dDevice->GetViewport(&vp);
|
||
|
||
// Ensure filename line has room for about 42 glyphs at current font size
|
||
char fortyTwo[64]; for (int i=0;i<42;++i) fortyTwo[i]='W'; fortyTwo[42]=0;
|
||
FLOAT fileW=0, fileH=0; GetAnsiWH(m_font, fortyTwo, &fileW, &fileH);
|
||
|
||
const FLOAT margin = 18.0f;
|
||
FLOAT w = max(max(420.0f, vp.Width * 0.50f), fileW + margin*2.0f);
|
||
if (w > vp.Width - margin*2.0f) w = vp.Width - margin*2.0f;
|
||
|
||
const FLOAT h = 116.0f;
|
||
const FLOAT x = Snap((vp.Width - w)*0.5f);
|
||
const FLOAT y = Snap((vp.Height - h)*0.5f);
|
||
|
||
DrawSolidRect(m_pd3dDevice, x-6, y-6, w+12, h+12, 0xA0101010);
|
||
DrawSolidRect(m_pd3dDevice, x, y, w, h, 0xE0222222);
|
||
|
||
// layout lines
|
||
const FLOAT titleY = y + 10.0f;
|
||
const FLOAT folderY = titleY + 24.0f;
|
||
const FLOAT fileY = folderY + 22.0f;
|
||
const FLOAT barY = fileY + 26.0f;
|
||
const FLOAT barH = 20.0f;
|
||
const FLOAT barX = x + margin;
|
||
const FLOAT barW = w - margin*2.0f;
|
||
|
||
// title
|
||
DrawAnsi(m_font, x + margin, titleY, 0xFFFFFFFF, &DefaultColors, m_prog.label[0] ? m_prog.label : "Working...");
|
||
|
||
// hint (right) — red "B:" + gray "Cancel"
|
||
{
|
||
char* text = "\x81 Cancel";
|
||
FLOAT tw, th;
|
||
GetAnsiWH(m_font, text, &tw, &th);
|
||
const FLOAT startX = x + w - margin - tw;
|
||
DrawAnsi(m_font, startX, titleY, 0xFFCCCCCC, &DefaultColors, text);
|
||
}
|
||
|
||
// Split current label into folder + file parts
|
||
const char* label = m_prog.current;
|
||
const char* slash = label ? strrchr(label, '\\') : NULL;
|
||
|
||
char folder[512] = {0};
|
||
char file [256] = {0};
|
||
if (slash){
|
||
size_t n = (size_t)(slash - label + 1);
|
||
if (n >= sizeof(folder)) n = sizeof(folder)-1;
|
||
memcpy(folder, label, n); folder[n] = 0;
|
||
_snprintf(file, sizeof(file), "%s", slash + 1); file[sizeof(file)-1]=0;
|
||
}
|
||
else {
|
||
_snprintf(file, sizeof(file), "%s", label ? label : ""); file[sizeof(file)-1]=0;
|
||
folder[0] = 0;
|
||
}
|
||
|
||
// --- character-step marquees (like panes/OSK) ----------------------------
|
||
static ProgMarquee s_marqFolder;
|
||
static ProgMarquee s_marqFile;
|
||
|
||
// If a new operation just started, clear state so we get the initial pause.
|
||
if (m_prog.done == 0) { s_marqFolder = ProgMarquee(); s_marqFile = ProgMarquee(); }
|
||
|
||
DrawLabelFittedOrMarquee(m_font, x + margin, folderY, barW, 0xFF5EA4FF, folder, s_marqFolder);
|
||
DrawLabelFittedOrMarquee(m_font, x + margin, fileY, barW, 0xFF89D07E, file, s_marqFile);
|
||
|
||
// progress percent and bar
|
||
FLOAT pct = 0.0f;
|
||
if (m_prog.total > 0){
|
||
pct = (FLOAT)((double)m_prog.done / (double)m_prog.total);
|
||
if (pct < 0) pct = 0; if (pct > 1) pct = 1;
|
||
}
|
||
|
||
DrawSolidRect(m_pd3dDevice, barX, barY, barW, barH, 0xFF0E0E0E);
|
||
DrawSolidRect(m_pd3dDevice, barX, barY, barW * pct, barH, 0x90FFFF00);
|
||
|
||
char t[32];
|
||
_snprintf(t, sizeof(t), "%u%%", (unsigned int)(pct * 100.0f + 0.5f));
|
||
t[sizeof(t) - 1] = 0;
|
||
DrawAnsiFromRight(m_font, barX + barW - 4.0f, barY, 0xFFEEEEEE, &DefaultColors, t, barH);
|
||
}
|
||
|
||
// ----- main render ----------------------------------------------------------
|
||
// Draw both panes, footer and hints, transient status, and any overlays.
|
||
HRESULT FileBrowserApp::Render() {
|
||
m_pd3dDevice->Clear(0,NULL,D3DCLEAR_TARGET,0x20202020,1.0f,0);
|
||
m_pd3dDevice->BeginScene();
|
||
|
||
m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
||
m_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
||
m_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
||
|
||
// Recompute layout if resolution changes on the fly (safe & cheap)
|
||
ComputeResponsiveLayout();
|
||
|
||
// ---- panes ----
|
||
PaneStyle st;
|
||
st.listW = kListW;
|
||
st.listY = kListY;
|
||
st.lineH = kLineH;
|
||
st.hdrY = kHdrY;
|
||
st.hdrH = kHdrH;
|
||
st.hdrW = kHdrW; // header exactly equals pane width
|
||
st.gutterW = kGutterW;
|
||
st.paddingX = kPaddingX;
|
||
st.scrollBarW = kScrollBarW;
|
||
st.visibleRows = m_visible;
|
||
|
||
// ---- panes ----
|
||
m_renderer.ResetSharedSizeColW(); // reset per frame
|
||
m_renderer.PrimeSharedSizeColW(m_font, m_pane[0], st); // prime from left
|
||
m_renderer.PrimeSharedSizeColW(m_font, m_pane[1], st); // prime from right
|
||
|
||
// Symmetric base X for panes: [margin] [left] [gap] [right] [margin]
|
||
const FLOAT leftX = kListX_L;
|
||
const FLOAT rightX = kListX_L + kListW + kPaneGap;
|
||
|
||
// Draw in order (left then right)
|
||
m_renderer.DrawPane(m_font, m_pd3dDevice, leftX, m_pane[0], m_active==0, st, 0);
|
||
m_renderer.DrawPane(m_font, m_pd3dDevice, rightX, m_pane[1], m_active==1, st, 1);
|
||
|
||
|
||
// ---- footer / hints ----
|
||
D3DVIEWPORT8 vp2; m_pd3dDevice->GetViewport(&vp2);
|
||
|
||
// Footer matches the combined pane area (two panes + gap), centered.
|
||
const FLOAT footerMargin = SafeMarginX((FLOAT)vp2.Width, (FLOAT)vp2.Height);
|
||
const FLOAT footerW = min(kHdrW * 2.0f + kPaneGap, (FLOAT)vp2.Width - footerMargin * 2.0f);
|
||
const FLOAT footerX = floorf(((FLOAT)vp2.Width - footerW) * 0.5f);
|
||
const FLOAT footerY = (FLOAT)vp2.Height - max(52.0f, (FLOAT)vp2.Height * 0.09f); // <-- unified
|
||
const FLOAT footerH = 28.0f;
|
||
|
||
// footer bar
|
||
DrawSolidRect(m_pd3dDevice, footerX, footerY, footerW, 28.0f, 0x802A2A2A);
|
||
|
||
const Pane& ap = m_pane[m_active];
|
||
const Item* cur = (ap.items.empty()? NULL : &ap.items[ap.sel]);
|
||
const char* yLab = (cur && !cur->isUpEntry && cur->marked) ? "Unmark" : "Mark";
|
||
|
||
const bool isLowRes = (vp2.Height < 700); // 480i/p or 576i count as "small"
|
||
const bool smallFooter = (isLowRes || footerW <= 620.0f);
|
||
|
||
if (m_pane[m_active].mode == 0) {
|
||
const char* hintsVerbose = "\x8A Move | \x89 Switch pane | \x80 Enter | \x82 Menu | \x87 / \x86 Page";
|
||
const char* hintsCompact = "\x8A Move | \x89 Pane | \x80 Enter | \x82 Menu | \x87 / \x86 Pg";
|
||
const char* base = smallFooter ? hintsCompact : hintsVerbose;
|
||
|
||
DrawAnsiCentered(m_font, footerX, footerY, 0xFFCCCCCC, &DefaultColors, base, footerW, footerH);
|
||
}
|
||
else {
|
||
const char* curPath = m_pane[m_active].curPath;
|
||
char leftLabel[16] = "Free";
|
||
ULONGLONG leftVal = 0, rightVal = 0;
|
||
if (IsDPath(curPath) && m_dvdHaveStats) {
|
||
strcpy(leftLabel, "Size");
|
||
leftVal = m_dvdUsedBytes;
|
||
rightVal = m_dvdTotalBytes;
|
||
}
|
||
else {
|
||
ULONGLONG fb=0, tb=0; GetDriveFreeTotal(curPath, fb, tb);
|
||
leftVal = fb; rightVal = tb;
|
||
}
|
||
|
||
char leftStr[64], rightStr[64];
|
||
FormatSize(leftVal, leftStr, sizeof(leftStr));
|
||
FormatSize(rightVal, rightStr, sizeof(rightStr));
|
||
|
||
char bar[420];
|
||
if (smallFooter) {
|
||
_snprintf(bar, sizeof(bar),
|
||
"\x81 Up | %s: %s / %s | \x82 Menu | \x83 %s | Pg \x87 / \x86", leftLabel, leftStr, rightStr, yLab);
|
||
}
|
||
else {
|
||
_snprintf(bar, sizeof(bar),
|
||
"\x81 Up | %s: %s / Total: %s | \x82 Menu | \x83 %s | \x87 / \x86 Page", leftLabel, leftStr, rightStr, yLab);
|
||
}
|
||
bar[sizeof(bar)-1] = 0;
|
||
|
||
DrawAnsiCentered(m_font, footerX, footerY, 0xFFCCCCCC, &DefaultColors, bar, footerW, 28.0f);
|
||
}
|
||
|
||
// ---- status toast (also centered and fitted) ----
|
||
DWORD now = GetTickCount();
|
||
FLOAT toastX = footerX + 5.0f, toastY = footerY + footerH + 2.0f, toastW = footerW - 10.0f;
|
||
FLOAT rx = DrawAnsiFromRight(m_font, toastX + toastW, toastY, 0xFFBBDDEE, NULL, FileBrowserApp::Get().GetCurrentIp());
|
||
rx = DrawAnsiFromRight(m_font, rx, toastY, 0x60BBDDEE, NULL, "IP: ");
|
||
FLOAT lx = DrawAnsi(m_font, toastX, toastY, 0x60BBDDEE, NULL, "Status: ");
|
||
if (now < m_statusUntilMs && m_status[0]) {
|
||
char fitted[256];
|
||
EllipsizeAnsiToFit(m_font, m_status, (rx - lx) - 5.0f, fitted, sizeof(fitted), ELLIPSIZE_LEFT);
|
||
DrawAnsi(m_font, lx, toastY, 0xFFBBDDEE, NULL, fitted);
|
||
}
|
||
|
||
// ---- overlays ----
|
||
DrawMenu();
|
||
DrawRename();
|
||
DrawProgressOverlay();
|
||
|
||
m_pd3dDevice->EndScene();
|
||
m_pd3dDevice->Present(NULL,NULL,NULL,NULL);
|
||
return S_OK;
|
||
}
|
||
|
||
int __cdecl main()
|
||
{
|
||
FileBrowserApp& app = FileBrowserApp::Get();
|
||
app.Create();
|
||
return app.Run();
|
||
} |