Files

766 lines
28 KiB
C++
Raw Permalink Normal View History

2025-08-30 00:45:12 +10:00
#include "OnScreenKeyboard.h"
2025-12-06 05:18:04 -08:00
2025-12-15 00:18:06 -08:00
#include "drawUtils.h"
#include "textUtils.h"
2025-12-06 01:32:28 -08:00
#include <StdIO.h>
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
/*
============================================================================
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
============================================================================
*/
2025-09-09 18:23:51 +10:00
// ------------------ Marquee for the path area ------------------
struct KB_Marquee {
2025-09-09 20:01:36 +10:00
FLOAT px; // current start index (in CHARACTERS, not pixels)
FLOAT fitWLock; // lock of visible width for stable end calc
2025-09-09 18:23:51 +10:00
DWORD nextTick; // next update time
DWORD resetPause; // nonzero while pausing at end
2025-09-09 20:01:36 +10:00
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; }
2025-09-09 18:23:51 +10:00
};
static KB_Marquee g_kbMarq;
// Draw the header path centered vertically in a band [x,y,w,h].
2025-09-09 20:01:36 +10:00
// 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)
2025-09-09 18:23:51 +10:00
{
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
2025-09-09 20:01:36 +10:00
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
2025-09-09 18:23:51 +10:00
2025-09-09 20:01:36 +10:00
const DWORD kInitPauseMs = 900;
const DWORD kStepMs = 150;
const DWORD kEndPauseMs = 1200;
const int kStepChars = 1;
// Full band for label + path
2025-09-09 18:23:51 +10:00
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;
{
2025-12-05 21:26:20 -08:00
GetAnsiWH(font, "In:", &inW, &inH);
const FLOAT ty = Snap(y + (h - inH) * 0.5f + kBiasDown);
DrawAnsi(font, bandX, ty, color, &DefaultColors, "In:");
2025-09-09 18:23:51 +10:00
}
// ---- path draw area (to the right of "In:") ----
const FLOAT pathX = bandX + inW + gapPx;
FLOAT pathW = bandW - inW - gapPx;
if (pathW <= 0.0f) return;
2025-09-09 20:01:36 +10:00
const FLOAT fitW_now = (pathW > kRightGuard) ? (pathW - kRightGuard) : 0.0f;
2025-09-09 18:23:51 +10:00
2025-12-05 21:26:20 -08:00
FLOAT fullW, fullH;
GetAnsiWH(font, parentPath, &fullW, &fullH);
const FLOAT ty = Snap(y + (h - fullH) * 0.5f + kBiasDown);
2025-09-09 18:23:51 +10:00
2025-09-09 20:01:36 +10:00
// If it fits (or nearly fits), draw and reset
if (fullW <= fitW_now + kTol){
2025-12-05 21:26:20 -08:00
DrawAnsi(font, pathX, ty, color, &DefaultColors, parentPath);
2025-09-09 20:01:36 +10:00
g_kbMarq = KB_Marquee(); // zero it
2025-09-09 18:23:51 +10:00
return;
}
// Reset marquee when text changes
2025-09-09 20:01:36 +10:00
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;
2025-09-09 18:23:51 +10:00
}
2025-09-09 20:01:36 +10:00
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
2025-09-09 18:23:51 +10:00
2025-09-09 20:01:36 +10:00
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;
2025-09-09 18:23:51 +10:00
}
2025-09-09 20:01:36 +10:00
const int lastStart = (lo > len) ? len : lo;
2025-09-09 18:23:51 +10:00
2025-09-09 20:01:36 +10:00
// 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);
2025-12-05 21:26:20 -08:00
font.DrawText(Snap(pathX), ty, color, wvis, 0, 0.0f);
2025-09-09 20:01:36 +10:00
// Step/pause/reset (exactly like PaneRenderer)
if (now >= g_kbMarq.nextTick){
2025-09-09 18:23:51 +10:00
if (g_kbMarq.resetPause){
2025-09-09 20:01:36 +10:00
g_kbMarq.px = 0.0f;
g_kbMarq.resetPause= 0;
g_kbMarq.nextTick = now + kInitPauseMs;
2025-09-09 18:23:51 +10:00
} else {
2025-09-09 20:01:36 +10:00
if (startIdx >= lastStart){
g_kbMarq.px = (FLOAT)lastStart;
g_kbMarq.resetPause= now + kEndPauseMs;
g_kbMarq.nextTick = g_kbMarq.resetPause;
2025-09-09 18:23:51 +10:00
} else {
2025-09-09 20:01:36 +10:00
g_kbMarq.px = (FLOAT)(startIdx + kStepChars);
2025-09-09 18:23:51 +10:00
g_kbMarq.nextTick = now + kStepMs;
}
}
}
}
2025-09-02 14:26:19 +10:00
// ------------------ 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)
2025-08-30 00:45:12 +10:00
}
// ------------------ FATX params ------------------
2025-09-02 14:26:19 +10:00
static const int kFatxMaxName = 42; // includes extension; dashboards mirror this
// ------------------------------------------------------------
// Glyph filtering control (set to 0 if your font supports more)
2025-09-02 14:26:19 +10:00
// Some XPR fonts wont render certain CP-1252 glyphs (€, ©).
// ------------------------------------------------------------
#ifndef OSK_FILTER_MISSING_GLYPHS
#define OSK_FILTER_MISSING_GLYPHS 1
#endif
2025-09-02 14:26:19 +10:00
// Only hide glyphs that the current XPR font cant 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;
}
2025-09-02 14:26:19 +10:00
// 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;
2025-12-06 01:32:28 -08:00
for (const char* p = raw; *p && n < cap - 1; ++p) if (IsGlyphSupported(*p)) out[n++] = *p;
out[n] = 0;
return n;
}
2025-09-02 14:26:19 +10:00
// 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;
}
2025-09-02 14:26:19 +10:00
// 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);
}
2025-09-02 14:26:19 +10:00
// 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
2025-09-02 14:26:19 +10:00
// 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;
2025-09-02 14:26:19 +10:00
// Append to current row
if (outCols[r] < 15) {
outRows[r][outCols[r]++] = moved;
outRows[r][outCols[r]] = 0;
}
}
}
}
2025-09-02 14:26:19 +10:00
// counts-only variant for navigation bounds
static void ComputeNormalizedSymbolCounts(int outCols[5]) {
char tmp[5][16];
BuildSymbolRowsNormalized(tmp, outCols);
}
2025-09-02 14:26:19 +10:00
// 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) {
2025-09-02 14:26:19 +10:00
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 {
2025-09-02 14:26:19 +10:00
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 ------------------
2025-08-30 00:45:12 +10:00
OnScreenKeyboard::OnScreenKeyboard(){
m_active = false;
2025-09-02 14:26:19 +10:00
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;
2025-08-31 15:46:31 +10:00
m_prevA = m_prevB = m_prevY = m_prevLT = m_prevRT = m_prevX = 0;
m_prevButtons = 0;
2025-09-02 14:26:19 +10:00
// Side-column UI focus (Done/Shift/Caps/Symbols)
m_sideFocus = false;
m_sideRow = 0;
2025-09-02 14:26:19 +10:00
m_shiftOnce = false; // one-shot shift toggle in alpha mode
2025-08-30 00:45:12 +10:00
}
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;
2025-08-30 00:45:12 +10:00
_snprintf(m_old, sizeof(m_old), "%s", initialName ? initialName : "");
m_old[sizeof(m_old)-1] = 0;
2025-08-30 00:45:12 +10:00
_snprintf(m_buf, sizeof(m_buf), "%s", initialName ? initialName : "");
m_buf[sizeof(m_buf)-1] = 0;
2025-09-02 14:26:19 +10:00
// Clamp to FATX limit
int len0 = (int)strlen(m_buf);
if (len0 > kFatxMaxName) {
m_buf[kFatxMaxName] = 0;
len0 = kFatxMaxName;
}
m_cursor = len0;
2025-08-30 00:45:12 +10:00
m_row = 0; m_col = 0;
m_lower = startLowerCase;
m_symbols = false;
2025-08-30 00:45:12 +10:00
// 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;
2025-08-30 00:45:12 +10:00
}
void OnScreenKeyboard::Close(){
m_active = false;
}
// ------------------ helpers (class methods) ------------------
2025-08-30 00:45:12 +10:00
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);
2025-12-06 01:32:28 -08:00
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];
2025-09-02 14:26:19 +10:00
// 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;
}
2025-08-30 00:45:12 +10:00
}
// ------------------ draw ------------------
2025-08-30 00:45:12 +10:00
void OnScreenKeyboard::Draw(CXBFont& font, LPDIRECT3DDEVICE8 dev, FLOAT lineH){
if (!m_active) return;
2025-09-09 20:01:36 +10:00
D3DVIEWPORT8 vp;
dev->GetViewport(&vp);
2025-08-30 00:45:12 +10:00
2025-09-09 20:01:36 +10:00
// --- 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;
2025-12-05 21:26:20 -08:00
const FLOAT x = Snap((vp.Width - panelW) * 0.55f);
const FLOAT y = Snap((vp.Height - panelH) * 0.5f);
2025-08-30 00:45:12 +10:00
// frame
2025-12-05 21:26:20 -08:00
DrawSolidRect(dev, x-8, y-8, panelW+16, panelH+16, 0xA0101010);
DrawSolidRect(dev, x, y, panelW, panelH, 0xE0222222);
2025-08-30 00:45:12 +10:00
2025-09-09 20:01:36 +10:00
// --- header ---
2025-12-05 21:26:20 -08:00
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;
2025-12-05 21:26:20 -08:00
FLOAT cntW = GetAnsiW(font, cnt);
DrawAnsi(font, Snap(x + panelW - 12.0f - cntW), titleY, cntCol, &DefaultColors, cnt);
}
2025-09-02 14:26:19 +10:00
// Divider under header
2025-12-05 21:26:20 -08:00
DrawSolidRect(dev, x, y + headerH, panelW, 1.0f, 0x60FFFFFF);
2025-09-09 18:23:51 +10:00
// ----- Path band ("In: <parent>") with safe-fit + end-stop marquee -----
2025-12-05 21:26:20 -08:00
const FLOAT infoY = Snap(y + headerH + afterLinePad);
2025-09-09 18:23:51 +10:00
const FLOAT infoBandW = panelW - 24.0f; // x+12 .. x+panelW-12
2025-09-09 20:01:36 +10:00
KB_DrawHeaderPath_FixedLabel(font, x + 12.0f, infoY, infoBandW, infoBandH,
0xFFCCCCCC, m_parent);
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Input box
2025-12-05 21:26:20 -08:00
const FLOAT boxY = Snap(infoY + infoBandH + labelToBoxPad);
DrawSolidRect(dev, x+12, boxY, panelW-24, boxH, 0xFF0E0E0E);
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Current name text
2025-12-05 21:26:20 -08:00
DrawAnsi(font, x+18, boxY+4, 0xFFFFFF00, &DefaultColors, m_buf);
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Caret at current insertion point
2025-08-30 00:45:12 +10:00
char tmp = m_buf[m_cursor]; m_buf[m_cursor]=0;
2025-12-05 21:26:20 -08:00
FLOAT caretX = Snap(x+18 + GetAnsiW(font, m_buf));
2025-08-30 00:45:12 +10:00
m_buf[m_cursor]=tmp;
2025-12-05 21:26:20 -08:00
DrawSolidRect(dev, caretX, boxY+4, 2.0f, boxH-8.0f, 0x90FFFF00);
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Grid layout (side column + key grid)
2025-08-30 00:45:12 +10:00
const FLOAT padX = 12.0f;
const FLOAT contentW = panelW - 2.0f*padX;
2025-12-05 21:26:20 -08:00
const FLOAT gridTop = Snap(boxY + boxH + gridTopGap);
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Side column width uses ~2 of 12 columns
const FLOAT colW12_full = contentW / 12.0f;
2025-08-31 15:46:31 +10:00
const FLOAT sideW = MaxF(130.0f, colW12_full * 2.2f);
const FLOAT keysW = contentW - sideW - gapX;
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// ---- side column (Done / Shift / Caps / Symbols) ----
const char* sideLbl[4] = {
"Done",
m_shiftOnce ? "Shift*" : "Shift",
2025-12-05 21:26:20 -08:00
m_lower ? "Caps \x8F" : "Caps \x8F*",
2025-12-06 01:32:28 -08:00
m_symbols ? "ABC \x90" : "#+= \x90"
};
for (int r=0;r<4;++r){
2025-09-02 14:26:19 +10:00
const FLOAT sx = x + padX;
const FLOAT sw = sideW;
2025-12-05 21:26:20 -08:00
const FLOAT sy = Snap(gridTop + r*(cellH + gapY));
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
const bool disabled = (m_symbols && (r == 1 || r == 2));
const bool sel = (!disabled && m_sideFocus && m_sideRow == r);
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
D3DCOLOR bg = sel ? 0x60FFFF00 : 0x30202020;
2025-12-05 21:26:20 -08:00
DrawSolidRect(dev, sx, sy, sw, cellH, bg);
2025-08-31 15:46:31 +10:00
2025-12-05 21:26:20 -08:00
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);
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
DWORD textCol = disabled ? 0xFF7A7A7A : 0xFFE0E0E0;
2025-12-05 21:26:20 -08:00
DrawAnsi(font, tx, ty, textCol, &DefaultColors, sideLbl[r]);
2025-09-02 14:26:19 +10:00
}
2025-09-02 14:26:19 +10:00
// ---- character rows (alpha: 4 rows, symbols: 5 rows) ----
const FLOAT keysX = x + padX + sideW + gapX;
char symRows[5][16];
int symCols[5];
2025-09-09 20:01:36 +10:00
if (m_symbols) BuildSymbolRowsNormalized(symRows, symCols);
for (int row = 0; row < charRows; ++row) {
2025-12-05 21:26:20 -08:00
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) {
2025-12-05 21:26:20 -08:00
const FLOAT x0 = Snap(keysX + col * colW);
const FLOAT x1 = Snap(keysX + (col + 1) * colW);
2025-08-30 00:45:12 +10:00
const FLOAT drawX = x0 + gapX * 0.5f;
const FLOAT drawW = (x1 - x0) - gapX;
const bool sel = (!m_sideFocus && m_row == row && m_col == col);
2025-08-30 00:45:12 +10:00
2025-12-05 21:26:20 -08:00
DrawSolidRect(dev, drawX, rowY, drawW, cellH, sel ? 0x60FFFF00 : 0x30202020);
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// 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 };
2025-12-06 01:32:28 -08:00
DrawAnsiCentered(font, drawX, rowY, 0xFFE0E0E0, &DefaultColors, s, drawW, cellH);
2025-08-30 00:45:12 +10:00
}
}
2025-09-09 20:01:36 +10:00
// ---- bottom row: Backspace | Space (use same grid math as other rows) ----
2025-12-05 21:26:20 -08:00
const FLOAT bottomY = Snap(gridTop + charRows * (cellH + gapY));
2025-09-09 20:01:36 +10:00
{
const int colsB = 2;
const FLOAT colWB = keysW / (FLOAT)colsB;
// snapped cell edges (exactly like x0/x1 in normal rows)
2025-12-05 21:26:20 -08:00
const FLOAT e0 = Snap(keysX + 0 * colWB);
const FLOAT e1 = Snap(keysX + 1 * colWB);
const FLOAT e2 = Snap(keysX + 2 * colWB);
2025-09-09 20:01:36 +10:00
// 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);
2025-12-06 01:32:28 -08:00
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);
2025-09-09 20:01:36 +10:00
// 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);
2025-12-06 01:32:28 -08:00
2025-12-05 21:26:20 -08:00
DrawSolidRect(dev, sx, bottomY, sw, cellH, selSpace ? 0x60FFFF00 : 0x30202020);
2025-12-06 01:32:28 -08:00
nx = DrawAnsiCentered(font, sx, bottomY, 0xFFE0E0E0, &DefaultColors, "Space", sw, cellH);
DrawAnsiCentered(font, nx, bottomY, 0xFFE0E0E0, &DefaultColors, " \x83", NULL, cellH);
2025-09-09 20:01:36 +10:00
}
2025-09-02 14:26:19 +10:00
// Footer hints (centered)
2025-12-05 21:26:20 -08:00
const char* hints = "\x80 Select \x81 Cancel \x85 Done \x91 \x92 Move Cursor";
2025-12-06 01:32:28 -08:00
DrawAnsiCentered(font, x, y + panelH - 25, 0xFFBBBBBB, &DefaultColors, hints, panelW);
2025-09-02 14:26:19 +10:00
}
2025-08-30 00:45:12 +10:00
// ------------------ input ------------------
2025-08-30 00:45:12 +10:00
OnScreenKeyboard::Result OnScreenKeyboard::OnPad(const XBGAMEPAD& pad){
if (!m_active) return NONE;
const DWORD btn = pad.wButtons;
2025-09-02 14:26:19 +10:00
// 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];
2025-09-02 14:26:19 +10:00
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];
2025-09-02 14:26:19 +10:00
// Debounce immediately after opening so the opener press doesnt 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;
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
if (!anyHeld) m_waitRelease = false;
return NONE;
}
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// How many character rows are visible in current layout?
const int charRows = m_symbols ? 5 : 4;
const int bottomActionRow = charRows;
2025-09-02 14:26:19 +10:00
// Edge + analog navigation (thumbsticks act as D-pad with thresholds).
2025-08-30 00:45:12 +10:00
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);
2025-09-02 14:26:19 +10:00
// Navigation: either side column is focused, or the key grid is.
if (m_sideFocus){
2025-08-31 15:46:31 +10:00
if (up) {
if (m_sideRow > 0) m_sideRow--;
2025-09-02 14:26:19 +10:00
// Skip disabled Shift/Caps when in Symbols
2025-08-31 15:46:31 +10:00
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); }
2025-08-30 00:45:12 +10:00
int colsNow = VisibleColsForRowHelper(m_symbols, m_row);
if (m_col >= colsNow) m_col = colsNow - 1;
2025-08-30 00:45:12 +10:00
if (left) {
if (m_col > 0) { m_col--; Sleep(120); }
2025-08-31 15:46:31 +10:00
else {
2025-09-02 14:26:19 +10:00
// Wrap focus into side column
2025-08-31 15:46:31 +10:00
m_sideFocus = true;
if (m_symbols && (m_sideRow == 1 || m_sideRow == 2))
2025-09-02 14:26:19 +10:00
m_sideRow = 0; // land on “Done”
2025-08-31 15:46:31 +10:00
Sleep(120);
}
}
if (right) {
if (m_col < colsNow - 1) { m_col++; Sleep(120); }
}
}
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Button edges
bool aTrig = (a > 30 && m_prevA <= 30);
bool bTrig = (b > 30 && m_prevB <= 30);
2025-09-02 14:26:19 +10:00
bool xTrig = (x > 30 && m_prevX <= 30);
bool yTrig = (y > 30 && m_prevY <= 30);
2025-08-30 00:45:12 +10:00
bool startTrig = ((btn & XINPUT_GAMEPAD_START) && !(m_prevButtons & XINPUT_GAMEPAD_START));
2025-09-02 14:26:19 +10:00
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));
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// 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);
}
2025-08-30 00:45:12 +10:00
if (aTrig){
if (m_sideFocus){
2025-08-31 15:46:31 +10:00
// Make disabled rows inert in Symbols
if ((m_sideRow == 1 || m_sideRow == 2) && m_symbols) {
Sleep(120);
} else if (m_sideRow == 0){
2025-09-02 14:26:19 +10:00
// 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){
2025-08-31 15:46:31 +10:00
// Shift (one-shot) — alpha only
if (!m_symbols) m_shiftOnce = !m_shiftOnce;
Sleep(140);
} else if (m_sideRow == 2){
2025-08-31 15:46:31 +10:00
// Caps — alpha only
if (!m_symbols) m_lower = !m_lower;
Sleep(140);
} else if (m_sideRow == 3){
2025-09-02 14:26:19 +10:00
// ABC/Symbols toggle; re-clamp selection
m_symbols = !m_symbols;
2025-08-31 15:46:31 +10:00
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;
2025-08-31 15:46:31 +10:00
Sleep(140);
2025-08-30 00:45:12 +10:00
}
} else {
2025-09-02 14:26:19 +10:00
// Grid: type a character OR trigger bottom actions
if (m_row <= (charRows-1)){ // character rows
2025-08-30 00:45:12 +10:00
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) {
2025-09-02 14:26:19 +10:00
for (int i=len; i>=m_cursor; --i) m_buf[i+1] = m_buf[i];
m_buf[m_cursor++] = ch;
2025-09-02 14:26:19 +10:00
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);
2025-09-02 14:26:19 +10:00
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){
2025-09-02 14:26:19 +10:00
for (int i=len; i>=m_cursor; --i) m_buf[i+1] = m_buf[i];
m_buf[m_cursor++] = ' ';
}
2025-08-30 00:45:12 +10:00
}
}
Sleep(140);
2025-08-30 00:45:12 +10:00
}
}
2025-09-02 14:26:19 +10:00
// Start = Done
2025-08-30 00:45:12 +10:00
if (startTrig){
2025-08-31 15:46:31 +10:00
m_prevA=a; m_prevB=b; m_prevY=y; m_prevX=x; m_prevButtons=btn; m_prevLT=lt; m_prevRT=rt;
2025-08-30 00:45:12 +10:00
return ACCEPTED;
}
2025-09-02 14:26:19 +10:00
// B = Cancel
2025-08-30 00:45:12 +10:00
if (bTrig){
2025-08-31 15:46:31 +10:00
m_prevA=a; m_prevB=b; m_prevY=y; m_prevX=x; m_prevButtons=btn; m_prevLT=lt; m_prevRT=rt;
2025-08-30 00:45:12 +10:00
return CANCELED;
}
2025-09-02 14:26:19 +10:00
// L3 toggles Caps (alpha only)
if (l3Trig) {
if (!m_symbols) m_lower = !m_lower;
Sleep(140);
}
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// 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);
}
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// 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);
}
2025-08-31 15:46:31 +10:00
2025-09-02 14:26:19 +10:00
// 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++; }
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// 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;
}
2025-08-30 00:45:12 +10:00
2025-09-02 14:26:19 +10:00
// Save edges
2025-08-31 15:46:31 +10:00
m_prevA = a; m_prevB = b; m_prevY = y; m_prevX = x;
m_prevButtons = btn; m_prevLT = lt; m_prevRT = rt;
2025-08-30 00:45:12 +10:00
return NONE;
}