You've already forked FileManager
mirror of
https://github.com/Team-Resurgent/FileManager.git
synced 2026-04-30 10:38:50 -07:00
766 lines
28 KiB
C++
766 lines
28 KiB
C++
#include "OnScreenKeyboard.h"
|
||
|
||
#include "drawUtils.h"
|
||
#include "textUtils.h"
|
||
|
||
#include <StdIO.h>
|
||
|
||
/*
|
||
============================================================================
|
||
OnScreenKeyboard
|
||
Modal on-screen keyboard used for renaming / text entry.
|
||
- Two layouts: Alpha (QWERTY + digits) and Symbols
|
||
- Side column actions: Done, Shift (one-shot), Caps (toggle), ABC/Symbols toggle
|
||
- Bottom action row: Backspace | Space
|
||
- FATX-safe length (42) enforced
|
||
============================================================================
|
||
*/
|
||
|
||
// ------------------ Marquee for the path area ------------------
|
||
struct KB_Marquee {
|
||
FLOAT px; // current start index (in CHARACTERS, not pixels)
|
||
FLOAT fitWLock; // lock of visible width for stable end calc
|
||
DWORD nextTick; // next update time
|
||
DWORD resetPause; // nonzero while pausing at end
|
||
char last[512]; // last path used (for reset-on-change)
|
||
KB_Marquee() : px(0.0f), fitWLock(0.0f), nextTick(0), resetPause(0) { last[0] = 0; }
|
||
};
|
||
static KB_Marquee g_kbMarq;
|
||
|
||
// Draw the header path centered vertically in a band [x,y,w,h].
|
||
// Fixed "In:" label + path. When the path is too long: character-step marquee
|
||
// with initial/end pauses (mirrors PaneRenderer).
|
||
static void KB_DrawHeaderPath_FixedLabel(
|
||
CXBFont& font, FLOAT x, FLOAT y, FLOAT w, FLOAT h,
|
||
DWORD color, const char* parentPath)
|
||
{
|
||
if (!parentPath) parentPath = "";
|
||
|
||
// --- layout constants (match pane approach) ---
|
||
const FLOAT leftPad = 6.0f;
|
||
const FLOAT rightPad = 6.0f;
|
||
const FLOAT gapPx = 4.0f; // space between "In:" and the path
|
||
const FLOAT kTol = 2.0f; // measurement slack
|
||
const FLOAT kBiasDown = 1.0f; // tiny downward bias
|
||
const FLOAT kRightGuard = 1.5f; // leave a hair on the right
|
||
|
||
const DWORD kInitPauseMs = 900;
|
||
const DWORD kStepMs = 150;
|
||
const DWORD kEndPauseMs = 1200;
|
||
const int kStepChars = 1;
|
||
|
||
// Full band for label + path
|
||
const FLOAT bandX = x + leftPad;
|
||
const FLOAT bandW = (w > leftPad + rightPad) ? (w - leftPad - rightPad) : 0.0f;
|
||
if (bandW <= 0.0f) return;
|
||
|
||
// ---- draw the fixed "In:" label ----
|
||
FLOAT inW=0, inH=0;
|
||
{
|
||
GetAnsiWH(font, "In:", &inW, &inH);
|
||
const FLOAT ty = Snap(y + (h - inH) * 0.5f + kBiasDown);
|
||
DrawAnsi(font, bandX, ty, color, &DefaultColors, "In:");
|
||
}
|
||
|
||
// ---- path draw area (to the right of "In:") ----
|
||
const FLOAT pathX = bandX + inW + gapPx;
|
||
FLOAT pathW = bandW - inW - gapPx;
|
||
if (pathW <= 0.0f) return;
|
||
|
||
const FLOAT fitW_now = (pathW > kRightGuard) ? (pathW - kRightGuard) : 0.0f;
|
||
|
||
FLOAT fullW, fullH;
|
||
GetAnsiWH(font, parentPath, &fullW, &fullH);
|
||
const FLOAT ty = Snap(y + (h - fullH) * 0.5f + kBiasDown);
|
||
|
||
// If it fits (or nearly fits), draw and reset
|
||
if (fullW <= fitW_now + kTol){
|
||
DrawAnsi(font, pathX, ty, color, &DefaultColors, parentPath);
|
||
g_kbMarq = KB_Marquee(); // zero it
|
||
return;
|
||
}
|
||
|
||
// Reset marquee when text changes
|
||
if (_stricmp(g_kbMarq.last, parentPath) != 0){
|
||
_snprintf(g_kbMarq.last, sizeof(g_kbMarq.last), "%s", parentPath);
|
||
g_kbMarq.last[sizeof(g_kbMarq.last)-1] = 0;
|
||
g_kbMarq.px = 0.0f; g_kbMarq.fitWLock = 0.0f; g_kbMarq.resetPause = 0; g_kbMarq.nextTick = GetTickCount() + kInitPauseMs;
|
||
}
|
||
|
||
const DWORD now = GetTickCount();
|
||
if (g_kbMarq.fitWLock <= 0.0f) g_kbMarq.fitWLock = fitW_now;
|
||
const FLOAT fitW = (FLOAT)((int)(g_kbMarq.fitWLock + 0.5f)); // snap compare width
|
||
|
||
const char* s = parentPath;
|
||
const int len = (int)strlen(s);
|
||
|
||
// Find earliest start index where the whole tail fits (binary search)
|
||
int lo = 0, hi = len;
|
||
while (lo < hi){
|
||
const int mid = (lo + hi) / 2;
|
||
WCHAR wtmp[1024]; MultiByteToWideChar(CP_ACP, 0, s + mid, -1, wtmp, 1024);
|
||
FLOAT tw=0, th=0; font.GetTextExtent(wtmp, &tw, &th);
|
||
if (tw <= fitW + kTol) hi = mid; else lo = mid + 1;
|
||
}
|
||
const int lastStart = (lo > len) ? len : lo;
|
||
|
||
// Current start index (stored as float)
|
||
int startIdx = (int)g_kbMarq.px;
|
||
if (startIdx < 0) startIdx = 0;
|
||
if (startIdx > lastStart) startIdx = lastStart;
|
||
|
||
// Longest substring from s+startIdx that fits (binary search)
|
||
const char* startPtr = s + startIdx;
|
||
const int remaining = len - startIdx;
|
||
int lo2 = 0, hi2 = remaining;
|
||
while (lo2 < hi2){
|
||
const int mid = (lo2 + hi2 + 1) / 2;
|
||
char tmp[1024]; _snprintf(tmp, sizeof(tmp), "%.*s", mid, startPtr);
|
||
WCHAR wtmp[1024]; MultiByteToWideChar(CP_ACP, 0, tmp, -1, wtmp, 1024);
|
||
FLOAT tw=0, th=0; font.GetTextExtent(wtmp, &tw, &th);
|
||
if (tw <= fitW + kTol) lo2 = mid; else hi2 = mid - 1;
|
||
}
|
||
|
||
// Draw visible slice
|
||
char vis[1024]; _snprintf(vis, sizeof(vis), "%.*s", lo2, startPtr); vis[sizeof(vis)-1]=0;
|
||
WCHAR wvis[1024]; MultiByteToWideChar(CP_ACP, 0, vis, -1, wvis, 1024);
|
||
font.DrawText(Snap(pathX), ty, color, wvis, 0, 0.0f);
|
||
|
||
// Step/pause/reset (exactly like PaneRenderer)
|
||
if (now >= g_kbMarq.nextTick){
|
||
if (g_kbMarq.resetPause){
|
||
g_kbMarq.px = 0.0f;
|
||
g_kbMarq.resetPause= 0;
|
||
g_kbMarq.nextTick = now + kInitPauseMs;
|
||
} else {
|
||
if (startIdx >= lastStart){
|
||
g_kbMarq.px = (FLOAT)lastStart;
|
||
g_kbMarq.resetPause= now + kEndPauseMs;
|
||
g_kbMarq.nextTick = g_kbMarq.resetPause;
|
||
} else {
|
||
g_kbMarq.px = (FLOAT)(startIdx + kStepChars);
|
||
g_kbMarq.nextTick = now + kStepMs;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------ Local keyboard layouts ------------------
|
||
// Alpha/number layer (your original). Rows target 10 keys each.
|
||
namespace {
|
||
static const char s_kb_a0[] = "1234567890"; // row 0 (10)
|
||
static const char s_kb_a1[] = "QWERTYUIOP"; // row 1 (10)
|
||
static const char s_kb_a2[] = "ASDFGHJKL-"; // row 2 (10)
|
||
static const char s_kb_a3[] = "ZXCVBNM_@."; // row 3 (10)
|
||
|
||
// Symbols layer: 5 raw rows. We normalize to keep first 4 rows at 10 keys.
|
||
static const char s_kb_s0[] = "1234567890"; // row 0
|
||
static const char s_kb_s1[] = ",;:'\"!?¡¿%"; // row 1
|
||
static const char s_kb_s2[] = "[]{}\\`$£«»"; // row 2
|
||
static const char s_kb_s3[] = "<>()^~¥|=&"; // row 3
|
||
static const char s_kb_s4[] = "#*/+-@_.€©"; // row 4 (may be short; €/© may be missing in some fonts)
|
||
}
|
||
|
||
// ------------------ FATX params ------------------
|
||
static const int kFatxMaxName = 42; // includes extension; dashboards mirror this
|
||
|
||
// ------------------------------------------------------------
|
||
// Glyph filtering control (set to 0 if your font supports more)
|
||
// Some XPR fonts won’t render certain CP-1252 glyphs (€, ©).
|
||
// ------------------------------------------------------------
|
||
#ifndef OSK_FILTER_MISSING_GLYPHS
|
||
#define OSK_FILTER_MISSING_GLYPHS 1
|
||
#endif
|
||
|
||
// Only hide glyphs that the current XPR font can’t render.
|
||
// Keep this strict—better to omit than draw tofu boxes.
|
||
static bool IsGlyphSupported(char c) {
|
||
#if OSK_FILTER_MISSING_GLYPHS
|
||
unsigned char uc = (unsigned char)c; // CP-1252
|
||
if (uc == 0x80 /*€*/ || uc == 0xA9 /*©*/) return false;
|
||
#endif
|
||
return true;
|
||
}
|
||
|
||
// Filter a raw layout row down to visible glyphs (NUL-terminated).
|
||
// Returns number of characters copied.
|
||
static int BuildVisibleRow(const char* raw, char* out, int cap) {
|
||
int n = 0;
|
||
for (const char* p = raw; *p && n < cap - 1; ++p) if (IsGlyphSupported(*p)) out[n++] = *p;
|
||
out[n] = 0;
|
||
return n;
|
||
}
|
||
|
||
// Helper: map symbol row index to its raw string.
|
||
static const char* SymRawRow(int r) {
|
||
return (r==0) ? s_kb_s0 :
|
||
(r==1) ? s_kb_s1 :
|
||
(r==2) ? s_kb_s2 :
|
||
(r==3) ? s_kb_s3 :
|
||
s_kb_s4;
|
||
}
|
||
|
||
// Build symbol rows ensuring rows 0..3 end with exactly 10 keys (borrowing
|
||
// from later rows if needed). Row 4 may end up short. This keeps a clean grid.
|
||
static void BuildSymbolRowsNormalized(char outRows[5][16], int outCols[5]) {
|
||
int r, i, j;
|
||
|
||
// 1) Filter each raw row (preserve order)
|
||
for (r = 0; r < 5; ++r) {
|
||
outCols[r] = BuildVisibleRow(SymRawRow(r), outRows[r], 16);
|
||
}
|
||
|
||
// 2) Borrow forward so rows 0..3 reach 10 keys
|
||
for (r = 0; r < 4; ++r) {
|
||
while (outCols[r] < 10) {
|
||
int takeFrom = -1;
|
||
for (j = r + 1; j < 5; ++j) {
|
||
if (outCols[j] > 0) { takeFrom = j; break; }
|
||
}
|
||
if (takeFrom < 0) break; // nothing left to borrow
|
||
|
||
// Pop front from donor row
|
||
char moved = outRows[takeFrom][0];
|
||
for (i = 0; i < outCols[takeFrom]-1; ++i)
|
||
outRows[takeFrom][i] = outRows[takeFrom][i+1];
|
||
outCols[takeFrom]--;
|
||
outRows[takeFrom][outCols[takeFrom]] = 0;
|
||
|
||
// Append to current row
|
||
if (outCols[r] < 15) {
|
||
outRows[r][outCols[r]++] = moved;
|
||
outRows[r][outCols[r]] = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// counts-only variant for navigation bounds
|
||
static void ComputeNormalizedSymbolCounts(int outCols[5]) {
|
||
char tmp[5][16];
|
||
BuildSymbolRowsNormalized(tmp, outCols);
|
||
}
|
||
|
||
// VC7.1-safe row length helper (no lambdas).
|
||
// For symbols: rows 0..4 are characters, row 5 is action row.
|
||
// For alpha: rows 0..3 are characters, row 4 is action row.
|
||
static int VisibleColsForRowHelper(bool symbols, int r) {
|
||
if (!symbols) {
|
||
if (r >= 4) return 2; // Backspace | Space
|
||
const char* raw =
|
||
(r==0) ? s_kb_a0 :
|
||
(r==1) ? s_kb_a1 :
|
||
(r==2) ? s_kb_a2 :
|
||
s_kb_a3;
|
||
char vis[32];
|
||
return BuildVisibleRow(raw, vis, sizeof(vis));
|
||
} else {
|
||
if (r == 5) return 2; // Backspace | Space (symbols mode)
|
||
int cols[5];
|
||
ComputeNormalizedSymbolCounts(cols);
|
||
return (r >= 0 && r < 5) ? cols[r] : 0;
|
||
}
|
||
}
|
||
|
||
// ------------------ ctor / open / close ------------------
|
||
OnScreenKeyboard::OnScreenKeyboard(){
|
||
m_active = false;
|
||
m_lower = false; // false = UPPER, true = lower (caps state for alpha)
|
||
m_symbols = false; // false = ABC, true = Symbols
|
||
m_waitRelease = false;
|
||
|
||
m_parent[0] = m_old[0] = m_buf[0] = 0;
|
||
m_cursor = 0; m_row = 0; m_col = 0;
|
||
|
||
m_prevA = m_prevB = m_prevY = m_prevLT = m_prevRT = m_prevX = 0;
|
||
m_prevButtons = 0;
|
||
|
||
// Side-column UI focus (Done/Shift/Caps/Symbols)
|
||
m_sideFocus = false;
|
||
m_sideRow = 0;
|
||
m_shiftOnce = false; // one-shot shift toggle in alpha mode
|
||
}
|
||
|
||
void OnScreenKeyboard::Open(const char* parentDir, const char* initialName, bool startLowerCase){
|
||
_snprintf(m_parent, sizeof(m_parent), "%s", parentDir ? parentDir : "");
|
||
m_parent[sizeof(m_parent)-1] = 0;
|
||
|
||
_snprintf(m_old, sizeof(m_old), "%s", initialName ? initialName : "");
|
||
m_old[sizeof(m_old)-1] = 0;
|
||
|
||
_snprintf(m_buf, sizeof(m_buf), "%s", initialName ? initialName : "");
|
||
m_buf[sizeof(m_buf)-1] = 0;
|
||
|
||
// Clamp to FATX limit
|
||
int len0 = (int)strlen(m_buf);
|
||
if (len0 > kFatxMaxName) {
|
||
m_buf[kFatxMaxName] = 0;
|
||
len0 = kFatxMaxName;
|
||
}
|
||
|
||
m_cursor = len0;
|
||
m_row = 0; m_col = 0;
|
||
m_lower = startLowerCase;
|
||
m_symbols = false;
|
||
|
||
// side column / shift state
|
||
m_sideFocus = false;
|
||
m_sideRow = 0;
|
||
m_shiftOnce = false;
|
||
|
||
// Activate + reset edges; require a full release before accepting input
|
||
m_active = true;
|
||
m_prevA = m_prevB = m_prevY = m_prevLT = m_prevRT = 0;
|
||
m_prevButtons = 0;
|
||
m_waitRelease = true;
|
||
}
|
||
|
||
void OnScreenKeyboard::Close(){
|
||
m_active = false;
|
||
}
|
||
|
||
// ------------------ helpers (class methods) ------------------
|
||
char OnScreenKeyboard::KbCharAt(int row, int col) const{
|
||
if (m_symbols) {
|
||
if (row < 0 || row > 4) return 0; // 5 symbol rows
|
||
char rows[5][16];
|
||
int cols[5];
|
||
BuildSymbolRowsNormalized(rows, cols);
|
||
if (col >= 0 && col < cols[row]) return rows[row][col];
|
||
return 0;
|
||
} else {
|
||
if (row < 0 || row > 3) return 0;
|
||
const char* raw =
|
||
(row==0) ? s_kb_a0 :
|
||
(row==1) ? s_kb_a1 :
|
||
(row==2) ? s_kb_a2 :
|
||
s_kb_a3;
|
||
|
||
char vis[32];
|
||
int cols = BuildVisibleRow(raw, vis, sizeof(vis));
|
||
if (col < 0 || col >= cols) return 0;
|
||
|
||
char ch = vis[col];
|
||
|
||
// Effective case: caps ^ shiftOnce (alpha only)
|
||
bool lowerEff = m_lower;
|
||
if (!m_symbols && m_shiftOnce) lowerEff = !lowerEff;
|
||
if (!m_symbols && lowerEff && ch >= 'A' && ch <= 'Z')
|
||
ch = (char)(ch + ('a' - 'A'));
|
||
return ch;
|
||
}
|
||
}
|
||
|
||
// ------------------ draw ------------------
|
||
void OnScreenKeyboard::Draw(CXBFont& font, LPDIRECT3DDEVICE8 dev, FLOAT lineH){
|
||
if (!m_active) return;
|
||
|
||
D3DVIEWPORT8 vp;
|
||
dev->GetViewport(&vp);
|
||
|
||
// --- layout constants used for both sizing and drawing ---
|
||
const FLOAT headerH = 32.0f; // title band
|
||
const FLOAT afterLinePad = 10.0f; // gap under divider
|
||
const FLOAT labelToBoxPad = 12.0f; // gap to input box
|
||
const FLOAT boxH = 30.0f; // input box height
|
||
const FLOAT gridTopGap = 16.0f; // gap before key grid
|
||
const FLOAT gapY = 4.0f; // vertical gap between rows
|
||
const FLOAT gapX = 6.0f; // horizontal gap between keys
|
||
const FLOAT infoBandH = (lineH > 22.0f ? lineH : 22.0f);
|
||
const FLOAT cellH = lineH + 6.0f; // key cell height
|
||
const FLOAT footerH = 26.0f; // room for footer hints
|
||
|
||
const int charRows = m_symbols ? 5 : 4;
|
||
|
||
// --- panel geometry (auto-height; grow to fit content, then clamp) ---
|
||
const FLOAT panelW = MaxF(520.0f, vp.Width * 0.55f);
|
||
|
||
const FLOAT contentNeededH =
|
||
headerH + 1.0f /*divider*/ + afterLinePad + infoBandH + labelToBoxPad + boxH +
|
||
gridTopGap + (charRows * (cellH + gapY)) + cellH /*bottom row*/ + footerH;
|
||
|
||
FLOAT panelH = MaxF(320.0f, vp.Height * 0.52f);
|
||
if (panelH < contentNeededH) panelH = contentNeededH;
|
||
const FLOAT maxH = vp.Height - 20.0f; // breathing room so frame fits
|
||
if (panelH > maxH) panelH = maxH;
|
||
|
||
const FLOAT x = Snap((vp.Width - panelW) * 0.55f);
|
||
const FLOAT y = Snap((vp.Height - panelH) * 0.5f);
|
||
|
||
// frame
|
||
DrawSolidRect(dev, x-8, y-8, panelW+16, panelH+16, 0xA0101010);
|
||
DrawSolidRect(dev, x, y, panelW, panelH, 0xE0222222);
|
||
|
||
// --- header ---
|
||
FLOAT titleW, titleH; GetAnsiWH(font, "Rename", &titleW, &titleH);
|
||
const FLOAT titleY = Snap(y + (headerH - titleH) * 0.5f);
|
||
DrawAnsi(font, x + 12, titleY, 0xFFFFFFFF, &DefaultColors, "Rename");
|
||
|
||
{
|
||
int len = (int)strlen(m_buf);
|
||
char cnt[32];
|
||
_snprintf(cnt, sizeof(cnt), "%d/%d", len, kFatxMaxName);
|
||
cnt[sizeof(cnt)-1] = 0;
|
||
|
||
DWORD cntCol = 0xFFCCCCCC;
|
||
if (len >= kFatxMaxName) cntCol = 0xFFFF6060;
|
||
else if (len >= kFatxMaxName-4) cntCol = 0xFFEED060;
|
||
|
||
FLOAT cntW = GetAnsiW(font, cnt);
|
||
DrawAnsi(font, Snap(x + panelW - 12.0f - cntW), titleY, cntCol, &DefaultColors, cnt);
|
||
}
|
||
|
||
// Divider under header
|
||
DrawSolidRect(dev, x, y + headerH, panelW, 1.0f, 0x60FFFFFF);
|
||
|
||
// ----- Path band ("In: <parent>") with safe-fit + end-stop marquee -----
|
||
const FLOAT infoY = Snap(y + headerH + afterLinePad);
|
||
const FLOAT infoBandW = panelW - 24.0f; // x+12 .. x+panelW-12
|
||
KB_DrawHeaderPath_FixedLabel(font, x + 12.0f, infoY, infoBandW, infoBandH,
|
||
0xFFCCCCCC, m_parent);
|
||
|
||
// Input box
|
||
const FLOAT boxY = Snap(infoY + infoBandH + labelToBoxPad);
|
||
DrawSolidRect(dev, x+12, boxY, panelW-24, boxH, 0xFF0E0E0E);
|
||
|
||
// Current name text
|
||
DrawAnsi(font, x+18, boxY+4, 0xFFFFFF00, &DefaultColors, m_buf);
|
||
|
||
// Caret at current insertion point
|
||
char tmp = m_buf[m_cursor]; m_buf[m_cursor]=0;
|
||
FLOAT caretX = Snap(x+18 + GetAnsiW(font, m_buf));
|
||
m_buf[m_cursor]=tmp;
|
||
DrawSolidRect(dev, caretX, boxY+4, 2.0f, boxH-8.0f, 0x90FFFF00);
|
||
|
||
// Grid layout (side column + key grid)
|
||
const FLOAT padX = 12.0f;
|
||
const FLOAT contentW = panelW - 2.0f*padX;
|
||
const FLOAT gridTop = Snap(boxY + boxH + gridTopGap);
|
||
|
||
// Side column width uses ~2 of 12 columns
|
||
const FLOAT colW12_full = contentW / 12.0f;
|
||
const FLOAT sideW = MaxF(130.0f, colW12_full * 2.2f);
|
||
const FLOAT keysW = contentW - sideW - gapX;
|
||
|
||
// ---- side column (Done / Shift / Caps / Symbols) ----
|
||
const char* sideLbl[4] = {
|
||
"Done",
|
||
m_shiftOnce ? "Shift*" : "Shift",
|
||
m_lower ? "Caps \x8F" : "Caps \x8F*",
|
||
m_symbols ? "ABC \x90" : "#+= \x90"
|
||
};
|
||
for (int r=0;r<4;++r){
|
||
const FLOAT sx = x + padX;
|
||
const FLOAT sw = sideW;
|
||
const FLOAT sy = Snap(gridTop + r*(cellH + gapY));
|
||
|
||
const bool disabled = (m_symbols && (r == 1 || r == 2));
|
||
const bool sel = (!disabled && m_sideFocus && m_sideRow == r);
|
||
|
||
D3DCOLOR bg = sel ? 0x60FFFF00 : 0x30202020;
|
||
DrawSolidRect(dev, sx, sy, sw, cellH, bg);
|
||
|
||
FLOAT tw, th; GetAnsiWH(font, sideLbl[r], &tw, &th);
|
||
const FLOAT tx = Snap(sx + (sw - tw) * 0.5f);
|
||
const FLOAT ty = Snap(sy + (cellH - th) * 0.5f);
|
||
|
||
DWORD textCol = disabled ? 0xFF7A7A7A : 0xFFE0E0E0;
|
||
DrawAnsi(font, tx, ty, textCol, &DefaultColors, sideLbl[r]);
|
||
}
|
||
|
||
// ---- character rows (alpha: 4 rows, symbols: 5 rows) ----
|
||
const FLOAT keysX = x + padX + sideW + gapX;
|
||
|
||
char symRows[5][16];
|
||
int symCols[5];
|
||
if (m_symbols) BuildSymbolRowsNormalized(symRows, symCols);
|
||
|
||
for (int row = 0; row < charRows; ++row) {
|
||
const FLOAT rowY = Snap(gridTop + row * (cellH + gapY));
|
||
|
||
const char* visChars;
|
||
int cols;
|
||
|
||
if (m_symbols) {
|
||
visChars = symRows[row];
|
||
cols = symCols[row];
|
||
} else {
|
||
const char* raw =
|
||
(row==0) ? s_kb_a0 :
|
||
(row==1) ? s_kb_a1 :
|
||
(row==2) ? s_kb_a2 :
|
||
s_kb_a3;
|
||
static char vis[32];
|
||
cols = BuildVisibleRow(raw, vis, sizeof(vis));
|
||
visChars = vis;
|
||
}
|
||
|
||
const FLOAT colW = (cols > 0) ? (keysW / (FLOAT)cols) : keysW;
|
||
|
||
for (int col = 0; col < cols; ++col) {
|
||
const FLOAT x0 = Snap(keysX + col * colW);
|
||
const FLOAT x1 = Snap(keysX + (col + 1) * colW);
|
||
const FLOAT drawX = x0 + gapX * 0.5f;
|
||
const FLOAT drawW = (x1 - x0) - gapX;
|
||
const bool sel = (!m_sideFocus && m_row == row && m_col == col);
|
||
|
||
DrawSolidRect(dev, drawX, rowY, drawW, cellH, sel ? 0x60FFFF00 : 0x30202020);
|
||
|
||
// Visualize applied case on alpha rows
|
||
char c = visChars[col];
|
||
if (!m_symbols && c >= 'A' && c <= 'Z') {
|
||
bool lowerEff = m_lower;
|
||
if (m_shiftOnce) lowerEff = !lowerEff;
|
||
if (lowerEff) c = (char)(c + ('a' - 'A'));
|
||
}
|
||
|
||
char s[2] = { c, 0 };
|
||
DrawAnsiCentered(font, drawX, rowY, 0xFFE0E0E0, &DefaultColors, s, drawW, cellH);
|
||
}
|
||
}
|
||
|
||
// ---- bottom row: Backspace | Space (use same grid math as other rows) ----
|
||
const FLOAT bottomY = Snap(gridTop + charRows * (cellH + gapY));
|
||
{
|
||
const int colsB = 2;
|
||
const FLOAT colWB = keysW / (FLOAT)colsB;
|
||
|
||
// snapped cell edges (exactly like x0/x1 in normal rows)
|
||
const FLOAT e0 = Snap(keysX + 0 * colWB);
|
||
const FLOAT e1 = Snap(keysX + 1 * colWB);
|
||
const FLOAT e2 = Snap(keysX + 2 * colWB);
|
||
|
||
// Backspace cell
|
||
const FLOAT bx = e0 + gapX * 0.5f;
|
||
const FLOAT bw = (e1 - e0) - gapX;
|
||
const bool selBack = (!m_sideFocus && m_row == charRows && m_col == 0);
|
||
|
||
DrawSolidRect(dev, bx, bottomY, bw, cellH, selBack ? 0x60FFFF00 : 0x30202020);
|
||
FLOAT nx = DrawAnsiCentered(font, bx, bottomY, 0xFFE0E0E0, &DefaultColors, "Backspace", bw, cellH);
|
||
DrawAnsiCentered(font, nx, bottomY, 0xFFE0E0E0, &DefaultColors, " \x82", NULL, cellH);
|
||
|
||
// Space cell
|
||
const FLOAT sx = e1 + gapX * 0.5f;
|
||
const FLOAT sw = (e2 - e1) - gapX;
|
||
const bool selSpace = (!m_sideFocus && m_row == charRows && m_col == 1);
|
||
|
||
DrawSolidRect(dev, sx, bottomY, sw, cellH, selSpace ? 0x60FFFF00 : 0x30202020);
|
||
nx = DrawAnsiCentered(font, sx, bottomY, 0xFFE0E0E0, &DefaultColors, "Space", sw, cellH);
|
||
DrawAnsiCentered(font, nx, bottomY, 0xFFE0E0E0, &DefaultColors, " \x83", NULL, cellH);
|
||
}
|
||
|
||
// Footer hints (centered)
|
||
const char* hints = "\x80 Select \x81 Cancel \x85 Done \x91 \x92 Move Cursor";
|
||
DrawAnsiCentered(font, x, y + panelH - 25, 0xFFBBBBBB, &DefaultColors, hints, panelW);
|
||
}
|
||
|
||
// ------------------ input ------------------
|
||
OnScreenKeyboard::Result OnScreenKeyboard::OnPad(const XBGAMEPAD& pad){
|
||
if (!m_active) return NONE;
|
||
|
||
const DWORD btn = pad.wButtons;
|
||
|
||
// Analog 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 x = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
|
||
unsigned char lt = pad.bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER];
|
||
unsigned char rt = pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER];
|
||
|
||
// Debounce immediately after opening so the opener press doesn’t leak in.
|
||
if (m_waitRelease) {
|
||
bool anyHeld = (a > 30) || (b > 30) || (y > 30) || (x > 30) ||
|
||
(lt > 30) || (rt > 30) ||
|
||
(btn & XINPUT_GAMEPAD_START) ||
|
||
(btn & XINPUT_GAMEPAD_LEFT_THUMB) ||
|
||
(btn & XINPUT_GAMEPAD_RIGHT_THUMB);
|
||
m_prevA = a; m_prevB = b; m_prevY = y; m_prevX = x;
|
||
m_prevLT = lt; m_prevRT = rt; m_prevButtons = btn;
|
||
|
||
if (!anyHeld) m_waitRelease = false;
|
||
return NONE;
|
||
}
|
||
|
||
// How many character rows are visible in current layout?
|
||
const int charRows = m_symbols ? 5 : 4;
|
||
const int bottomActionRow = charRows;
|
||
|
||
// Edge + analog navigation (thumbsticks act as D-pad with thresholds).
|
||
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);
|
||
|
||
// Navigation: either side column is focused, or the key grid is.
|
||
if (m_sideFocus){
|
||
if (up) {
|
||
if (m_sideRow > 0) m_sideRow--;
|
||
// Skip disabled Shift/Caps when in Symbols
|
||
if (m_symbols && (m_sideRow == 1 || m_sideRow == 2)) m_sideRow = 0;
|
||
Sleep(120);
|
||
}
|
||
if (down) {
|
||
if (m_sideRow < 3) m_sideRow++;
|
||
if (m_symbols && (m_sideRow == 1 || m_sideRow == 2)) m_sideRow = 3;
|
||
Sleep(120);
|
||
}
|
||
if (right){ m_sideFocus = false; Sleep(120); }
|
||
} else {
|
||
if (up) { if (m_row > 0) m_row--; Sleep(120); }
|
||
if (down) { if (m_row < bottomActionRow) m_row++; Sleep(120); }
|
||
|
||
int colsNow = VisibleColsForRowHelper(m_symbols, m_row);
|
||
if (m_col >= colsNow) m_col = colsNow - 1;
|
||
|
||
if (left) {
|
||
if (m_col > 0) { m_col--; Sleep(120); }
|
||
else {
|
||
// Wrap focus into side column
|
||
m_sideFocus = true;
|
||
if (m_symbols && (m_sideRow == 1 || m_sideRow == 2))
|
||
m_sideRow = 0; // land on “Done”
|
||
Sleep(120);
|
||
}
|
||
}
|
||
if (right) {
|
||
if (m_col < colsNow - 1) { m_col++; Sleep(120); }
|
||
}
|
||
}
|
||
|
||
// Button edges
|
||
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 startTrig = ((btn & XINPUT_GAMEPAD_START) && !(m_prevButtons & XINPUT_GAMEPAD_START));
|
||
bool l3Trig = ((btn & XINPUT_GAMEPAD_LEFT_THUMB) && !(m_prevButtons & XINPUT_GAMEPAD_LEFT_THUMB));
|
||
bool r3Trig = ((btn & XINPUT_GAMEPAD_RIGHT_THUMB) && !(m_prevButtons & XINPUT_GAMEPAD_RIGHT_THUMB));
|
||
|
||
// Y inserts a space at cursor (shortcut)
|
||
if (yTrig) {
|
||
int len = (int)strlen(m_buf);
|
||
const int cap = (int)sizeof(m_buf) - 1;
|
||
if (len < cap && len < kFatxMaxName) {
|
||
for (int i = len; i >= m_cursor; --i) m_buf[i+1] = m_buf[i];
|
||
m_buf[m_cursor++] = ' ';
|
||
}
|
||
Sleep(120);
|
||
}
|
||
|
||
if (aTrig){
|
||
if (m_sideFocus){
|
||
// Make disabled rows inert in Symbols
|
||
if ((m_sideRow == 1 || m_sideRow == 2) && m_symbols) {
|
||
Sleep(120);
|
||
} else if (m_sideRow == 0){
|
||
// Done
|
||
m_prevA=a; m_prevB=b; m_prevY=y; m_prevButtons=btn; m_prevLT=lt; m_prevRT=rt;
|
||
return ACCEPTED;
|
||
} else if (m_sideRow == 1){
|
||
// Shift (one-shot) — alpha only
|
||
if (!m_symbols) m_shiftOnce = !m_shiftOnce;
|
||
Sleep(140);
|
||
} else if (m_sideRow == 2){
|
||
// Caps — alpha only
|
||
if (!m_symbols) m_lower = !m_lower;
|
||
Sleep(140);
|
||
} else if (m_sideRow == 3){
|
||
// ABC/Symbols toggle; re-clamp selection
|
||
m_symbols = !m_symbols;
|
||
if (m_symbols && (m_sideRow == 1 || m_sideRow == 2)) m_sideRow = 3;
|
||
int newCols = VisibleColsForRowHelper(m_symbols, m_row);
|
||
if (m_col >= newCols) m_col = newCols - 1;
|
||
Sleep(140);
|
||
}
|
||
} else {
|
||
// Grid: type a character OR trigger bottom actions
|
||
if (m_row <= (charRows-1)){ // character rows
|
||
int len = (int)strlen(m_buf);
|
||
const int cap = (int)sizeof(m_buf) - 1;
|
||
if (len < cap && len < kFatxMaxName){
|
||
char ch = KbCharAt(m_row, m_col);
|
||
if (ch) {
|
||
for (int i=len; i>=m_cursor; --i) m_buf[i+1] = m_buf[i];
|
||
m_buf[m_cursor++] = ch;
|
||
if (m_shiftOnce) m_shiftOnce = false; // one-shot consumed
|
||
}
|
||
}
|
||
} else { // bottom row: Backspace | Space
|
||
if (m_col == 0){ // Backspace
|
||
if (m_cursor > 0){
|
||
int len = (int)strlen(m_buf);
|
||
for (int i=m_cursor-1; i<=len; ++i) m_buf[i] = m_buf[i+1];
|
||
m_cursor--;
|
||
}
|
||
} else { // Space
|
||
int len = (int)strlen(m_buf);
|
||
const int cap = (int)sizeof(m_buf) - 1;
|
||
if (len < cap && len < kFatxMaxName){
|
||
for (int i=len; i>=m_cursor; --i) m_buf[i+1] = m_buf[i];
|
||
m_buf[m_cursor++] = ' ';
|
||
}
|
||
}
|
||
}
|
||
Sleep(140);
|
||
}
|
||
}
|
||
|
||
// Start = Done
|
||
if (startTrig){
|
||
m_prevA=a; m_prevB=b; m_prevY=y; m_prevX=x; m_prevButtons=btn; m_prevLT=lt; m_prevRT=rt;
|
||
return ACCEPTED;
|
||
}
|
||
// B = Cancel
|
||
if (bTrig){
|
||
m_prevA=a; m_prevB=b; m_prevY=y; m_prevX=x; m_prevButtons=btn; m_prevLT=lt; m_prevRT=rt;
|
||
return CANCELED;
|
||
}
|
||
|
||
// L3 toggles Caps (alpha only)
|
||
if (l3Trig) {
|
||
if (!m_symbols) m_lower = !m_lower;
|
||
Sleep(140);
|
||
}
|
||
|
||
// R3 toggles Symbols; re-clamp selection and side focus sanity
|
||
if (r3Trig) {
|
||
m_symbols = !m_symbols;
|
||
if (m_sideFocus && m_symbols && (m_sideRow == 1 || m_sideRow == 2))
|
||
m_sideRow = 3;
|
||
int newCols = VisibleColsForRowHelper(m_symbols, m_row);
|
||
if (m_col >= newCols) m_col = (newCols > 0) ? (newCols - 1) : 0;
|
||
Sleep(140);
|
||
}
|
||
|
||
// X = Backspace (anywhere)
|
||
if (xTrig) {
|
||
if (m_cursor > 0) {
|
||
int len = (int)strlen(m_buf);
|
||
for (int i = m_cursor - 1; i <= len; ++i)
|
||
m_buf[i] = m_buf[i + 1];
|
||
m_cursor--;
|
||
}
|
||
Sleep(120);
|
||
}
|
||
|
||
// Triggers move caret (LT left, RT right)
|
||
bool ltTrig = (lt > 30 && m_prevLT <= 30);
|
||
bool rtTrig = (rt > 30 && m_prevRT <= 30);
|
||
if (ltTrig){ if (m_cursor > 0) m_cursor--; }
|
||
if (rtTrig){ int len=(int)strlen(m_buf); if (m_cursor < len) m_cursor++; }
|
||
|
||
// Clamp caret within current text
|
||
{
|
||
int len=(int)strlen(m_buf);
|
||
if (m_cursor < 0) m_cursor = 0;
|
||
if (m_cursor > len) m_cursor = len;
|
||
}
|
||
|
||
// Save edges
|
||
m_prevA = a; m_prevB = b; m_prevY = y; m_prevX = x;
|
||
m_prevButtons = btn; m_prevLT = lt; m_prevRT = rt;
|
||
|
||
return NONE;
|
||
}
|