fix(input): add sliding-window multi-tap candidate bar and input tail-scrolling across plugins and fcitx5-t9

This commit is contained in:
Luxferre
2026-10-26 17:37:53 +02:00
parent a53a331111
commit 1f9138f0c6
24 changed files with 394 additions and 125 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+24 -8
View File
@@ -43,7 +43,7 @@ enum class TextCaseMode {
const char* kMultiTapLower[10] = { const char* kMultiTapLower[10] = {
" 0+", // 0 " 0+", // 0
".,-@_1", // 1 kMultiTapPunct1, // 1
"abc2", // 2 "abc2", // 2
"def3", // 3 "def3", // 3
"ghi4", // 4 "ghi4", // 4
@@ -527,14 +527,15 @@ int runContactsApp(int argc, char** argv) {
modeBadge); modeBadge);
// Field 0: Name box (y = 44..108) // Field 0: Name box (y = 44..108)
auto drawFieldBox = [&](int y, int h, bool active, const char* label, auto drawFieldBox = [&](int y, int h, bool active,
const std::string& label,
const std::string& val, const char* placeholder) { const std::string& val, const char* placeholder) {
XSetForeground(dpy, gc, active ? theme.spec.bgInputFocus : theme.spec.bgButton); XSetForeground(dpy, gc, active ? theme.spec.bgInputFocus : theme.spec.bgButton);
XFillRectangle(dpy, pix, gc, 6, y, kWinWidth - 12, h); XFillRectangle(dpy, pix, gc, 6, y, kWinWidth - 12, h);
XSetForeground(dpy, gc, active ? theme.spec.accent : theme.spec.border); XSetForeground(dpy, gc, active ? theme.spec.accent : theme.spec.border);
XDrawRectangle(dpy, pix, gc, 6, y, kWinWidth - 13, h - 1); XDrawRectangle(dpy, pix, gc, 6, y, kWinWidth - 13, h - 1);
drawPango(layoutSmall, active ? &colYellow : &colMuted, 12, y + 3, 18, drawPango(layoutSmall, active ? &colYellow : &colMuted, 12, y + 3, 18,
label); label, 214);
std::string shown = val; std::string shown = val;
if (active) { if (active) {
shown.push_back('|'); shown.push_back('|');
@@ -542,14 +543,19 @@ int runContactsApp(int argc, char** argv) {
if (val.empty() && !active) { if (val.empty() && !active) {
drawPango(layoutMain, &colMuted, 12, y + 23, 28, placeholder, 214); drawPango(layoutMain, &colMuted, 12, y + 23, 28, placeholder, 214);
} else { } else {
if (shown.size() > 21) { shown = tailFitPangoText(layoutMain, shown, 214);
shown = ".." + shown.substr(shown.size() - 19);
}
drawPango(layoutMain, &colWhite, 12, y + 23, 28, shown, 214); drawPango(layoutMain, &colWhite, 12, y + 23, 28, shown, 214);
} }
}; };
drawFieldBox(44, 56, editField == 0, "Name (# Input mode):", editName, std::string nameHdr = "Name (# Input mode):";
if (editField == 0 && lastTapDigit >= 0 && lastTapDigit <= 9 &&
(nowMs() - lastTapMs) <= 850) {
nameHdr = formatMultiTapBar(
std::to_string(lastTapDigit) + ":", kMultiTapLower[lastTapDigit],
lastTapCycle, caseMode == TextCaseMode::Upper);
}
drawFieldBox(44, 56, editField == 0, nameHdr, editName,
"Type name on keypad..."); "Type name on keypad...");
drawFieldBox(106, 56, editField == 1, drawFieldBox(106, 56, editField == 1,
"Number (Hold 0 for +):", editNumber, "Enter phone number..."); "Number (Hold 0 for +):", editNumber, "Enter phone number...");
@@ -863,7 +869,7 @@ int runContactsApp(int argc, char** argv) {
} }
const char* seq = kMultiTapLower[digit]; const char* seq = kMultiTapLower[digit];
int seqLen = static_cast<int>(std::strlen(seq)); int seqLen = static_cast<int>(std::strlen(seq));
if (digit == lastTapDigit && (now - lastTapMs) <= 650 && !editName.empty()) { if (digit == lastTapDigit && (now - lastTapMs) <= 850 && !editName.empty()) {
lastTapCycle = (lastTapCycle + 1) % seqLen; lastTapCycle = (lastTapCycle + 1) % seqLen;
char ch = seq[lastTapCycle]; char ch = seq[lastTapCycle];
bool makeUpper = false; bool makeUpper = false;
@@ -1751,6 +1757,12 @@ int runContactsApp(int argc, char** argv) {
} }
} }
if (visible && mode == ViewMode::Edit && lastTapDigit != -1 &&
(nowMs() - lastTapMs) > 850) {
lastTapDigit = -1;
render();
}
int timeoutMs = -1; int timeoutMs = -1;
if (visible) { if (visible) {
timeoutMs = 200; timeoutMs = 200;
@@ -1762,6 +1774,10 @@ int runContactsApp(int argc, char** argv) {
int64_t rem = 420 - (nowMs() - zeroPressMs); int64_t rem = 420 - (nowMs() - zeroPressMs);
timeoutMs = std::min(timeoutMs, static_cast<int>(std::max<int64_t>(5, rem))); timeoutMs = std::min(timeoutMs, static_cast<int>(std::max<int64_t>(5, rem)));
} }
if (mode == ViewMode::Edit && lastTapDigit != -1) {
int64_t rem = 851 - (nowMs() - lastTapMs);
timeoutMs = std::min(timeoutMs, static_cast<int>(std::max<int64_t>(5, rem)));
}
} }
struct pollfd pfds[2] {}; struct pollfd pfds[2] {};
+54 -36
View File
@@ -54,7 +54,7 @@ enum class TextCaseMode {
const char* kMultiTapFile[10] = { const char* kMultiTapFile[10] = {
" _-0", // 0 " _-0", // 0
".-_1", // 1 ".-_1,!?+@#$%~()[]{}", // 1
"abc2", // 2 "abc2", // 2
"def3", // 3 "def3", // 3
"ghi4", // 4 "ghi4", // 4
@@ -67,7 +67,7 @@ const char* kMultiTapFile[10] = {
const char* kMultiTapEditorAlpha[11] = { const char* kMultiTapEditorAlpha[11] = {
" 0", // 0 " 0", // 0
".,:;!?-_1/\"'()", // 1 kMultiTapPunct1, // 1
"abc2", // 2 "abc2", // 2
"def3", // 3 "def3", // 3
"ghi4", // 4 "ghi4", // 4
@@ -76,13 +76,13 @@ const char* kMultiTapEditorAlpha[11] = {
"pqrs7", // 7 "pqrs7", // 7
"tuv8", // 8 "tuv8", // 8
"wxyz9", // 9 "wxyz9", // 9
".,!?+-*/=<>()[]{}\"'_#$%&|\\~^@" // 10 (*) kMultiTapStar // 10 (*)
}; };
const char* kMultiTapEditorSym[11] = { const char* kMultiTapEditorSym[11] = {
" _0", // 0 " _0", // 0
".,:;!?1", // 1 ".,:;!?-_1", // 1
"\"'`~@2", // 2 "\"'`~@#2", // 2
"()[]{}<>3", // 3 "()[]{}<>3", // 3
"+-*/=%4", // 4 "+-*/=%4", // 4
"|&^#$5", // 5 "|&^#$5", // 5
@@ -90,7 +90,7 @@ const char* kMultiTapEditorSym[11] = {
"=<>!~7", // 7 "=<>!~7", // 7
"();,\"8", // 8 "();,\"8", // 8
"{}[]#9", // 9 "{}[]#9", // 9
"+-*/=<>()[]{}\"'.,:;!?_|&^#$%~@\\" // 10 (*) kMultiTapStar // 10 (*)
}; };
struct FileItem { struct FileItem {
@@ -1184,11 +1184,20 @@ int runFilesApp(int argc, char** argv) {
XFillRectangle(dpy, pix, gc, 10, 74, kWinWidth - 20, 38); XFillRectangle(dpy, pix, gc, 10, 74, kWinWidth - 20, 38);
XSetForeground(dpy, gc, pt.spec.accentAlt); XSetForeground(dpy, gc, pt.spec.accentAlt);
XDrawRectangle(dpy, pix, gc, 10, 74, kWinWidth - 21, 37); XDrawRectangle(dpy, pix, gc, 10, 74, kWinWidth - 21, 37);
drawPango(layoutMain, &colWhite, 16, 74, 38, inputValue + "|", std::string modalDisp =
kWinWidth - 32); tailFitPangoText(layoutMain, inputValue + "|", kWinWidth - 32);
drawPango(layoutMain, &colWhite, 16, 74, 38, modalDisp, kWinWidth - 32);
if (lastTapDigit >= 0 && lastTapDigit <= 9 &&
(nowMs() - lastTapMs) <= 850) {
std::string cand = formatMultiTapBar(
std::to_string(lastTapDigit) + ":", kMultiTapFile[lastTapDigit],
lastTapCycle, caseMode == TextCaseMode::Upper);
drawPango(layoutSmall, &colYellow, 10, 126, 20, cand, 220);
} else {
drawPango(layoutSmall, &colMuted, 10, 126, 20, drawPango(layoutSmall, &colMuted, 10, 126, 20,
"# Input mode", 220); "#:Mode *:Punct Right:Next", 220);
}
drawSoftkeyBar("Save", "Save", drawSoftkeyBar("Save", "Save",
inputValue.empty() ? "Cancel" : "Del"); inputValue.empty() ? "Cancel" : "Del");
@@ -1279,8 +1288,28 @@ int runFilesApp(int argc, char** argv) {
const std::string& rawLine = editLines[idx]; const std::string& rawLine = editLines[idx];
size_t sliceStart = 0; size_t sliceStart = 0;
if (isCurRow && editCursorCol > 22) { int cx = 0;
sliceStart = clampUtf8Pos(rawLine, editCursorCol - 22); const int maxLineTextW = kWinWidth - gutterW - 10;
if (isCurRow) {
size_t curByte =
static_cast<size_t>(clampUtf8Pos(rawLine, editCursorCol));
if (curByte > 22) {
sliceStart =
static_cast<size_t>(clampUtf8Pos(rawLine, editCursorCol - 22));
}
while (sliceStart < curByte) {
std::string beforeCur = expandTabsForDisplay(
rawLine.substr(sliceStart, curByte - sliceStart));
pango_layout_set_width(layoutSmall, -1);
pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE);
pango_layout_set_text(layoutSmall, beforeCur.c_str(),
static_cast<int>(beforeCur.size()));
int chh = 0;
pango_layout_get_pixel_size(layoutSmall, &cx, &chh);
if (cx <= maxLineTextW - 6) break;
sliceStart = static_cast<size_t>(
nextUtf8Pos(rawLine, static_cast<int>(sliceStart)));
}
} }
std::string rawSlice = rawLine.substr(sliceStart); std::string rawSlice = rawLine.substr(sliceStart);
std::string dispSlice = expandTabsForDisplay(rawSlice); std::string dispSlice = expandTabsForDisplay(rawSlice);
@@ -1289,16 +1318,6 @@ int runFilesApp(int argc, char** argv) {
ry, rowH, dispSlice, kWinWidth - gutterW - 8); ry, rowH, dispSlice, kWinWidth - gutterW - 8);
if (isCurRow && !editMenuOpen) { if (isCurRow && !editMenuOpen) {
size_t curOff =
(editCursorCol >= sliceStart) ? (editCursorCol - sliceStart) : 0;
std::string beforeCur =
expandTabsForDisplay(rawSlice.substr(0, curOff));
pango_layout_set_width(layoutSmall, -1);
pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE);
pango_layout_set_text(layoutSmall, beforeCur.c_str(),
static_cast<int>(beforeCur.size()));
int cx = 0, chh = 0;
pango_layout_get_pixel_size(layoutSmall, &cx, &chh);
int curX = std::min(kWinWidth - 4, gutterW + 4 + cx); int curX = std::min(kWinWidth - 4, gutterW + 4 + cx);
XSetForeground(dpy, gc, XSetForeground(dpy, gc,
tapActive ? pt.spec.success : pt.spec.accentAlt); tapActive ? pt.spec.success : pt.spec.accentAlt);
@@ -1318,24 +1337,13 @@ int runFilesApp(int argc, char** argv) {
(editCaseMode == TextCaseMode::Sym) (editCaseMode == TextCaseMode::Sym)
? kMultiTapEditorSym[editLastTapDigit] ? kMultiTapEditorSym[editLastTapDigit]
: kMultiTapEditorAlpha[editLastTapDigit]; : kMultiTapEditorAlpha[editLastTapDigit];
int tlen = static_cast<int>(std::strlen(table)); std::string pref =
std::string cand =
(editLastTapDigit == 10) (editLastTapDigit == 10)
? "*:" ? "*:"
: (std::to_string(editLastTapDigit) + ":"); : (std::to_string(editLastTapDigit) + ":");
for (int k = 0; k < tlen; ++k) { std::string cand = formatMultiTapBar(
char c = table[k]; pref, table, editLastTapCycle,
if (std::isalpha(static_cast<unsigned char>(c)) && editCaseMode == TextCaseMode::Upper);
editCaseMode == TextCaseMode::Upper) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
std::string label = (c == ' ') ? "SPC" : std::string(1, c);
if (k == editLastTapCycle) {
cand += "[" + label + "] ";
} else {
cand += label + " ";
}
}
drawPango(layoutSmall, &colYellow, 6, hintY, 20, cand, kWinWidth - 12); drawPango(layoutSmall, &colYellow, 6, hintY, 20, cand, kWinWidth - 12);
} else { } else {
drawPango(layoutSmall, &colMuted, 6, hintY, 20, drawPango(layoutSmall, &colMuted, 6, hintY, 20,
@@ -2116,6 +2124,7 @@ int runFilesApp(int argc, char** argv) {
handleSoftkeyAction(true); handleSoftkeyAction(true);
} else if (sym == XK_Right || sym == XK_KP_Right) { } else if (sym == XK_Right || sym == XK_KP_Right) {
lastTapDigit = -1; lastTapDigit = -1;
drawUI();
} else { } else {
char d = mapDialKey(sym, kc, ev.xkey.state); char d = mapDialKey(sym, kc, ev.xkey.state);
if (d == '#') { if (d == '#') {
@@ -2159,6 +2168,11 @@ int runFilesApp(int argc, char** argv) {
editLastTapDigit = -1; editLastTapDigit = -1;
drawUI(); drawUI();
} }
if (visible && mode == ViewMode::InputModal && lastTapDigit != -1 &&
(tNow - lastTapMs) > 850) {
lastTapDigit = -1;
drawUI();
}
int timeoutMs = -1; int timeoutMs = -1;
if (visible && softkeyPending != 0) { if (visible && softkeyPending != 0) {
@@ -2168,6 +2182,10 @@ int runFilesApp(int argc, char** argv) {
int tapRem = std::max(1, static_cast<int>(851 - (tNow - editLastTapMs))); int tapRem = std::max(1, static_cast<int>(851 - (tNow - editLastTapMs)));
timeoutMs = (timeoutMs < 0) ? tapRem : std::min(timeoutMs, tapRem); timeoutMs = (timeoutMs < 0) ? tapRem : std::min(timeoutMs, tapRem);
} }
if (visible && mode == ViewMode::InputModal && lastTapDigit != -1) {
int tapRem = std::max(1, static_cast<int>(851 - (tNow - lastTapMs)));
timeoutMs = (timeoutMs < 0) ? tapRem : std::min(timeoutMs, tapRem);
}
if (visible && !statusBanner.empty() && statusBannerUntilMs > 0) { if (visible && !statusBanner.empty() && statusBannerUntilMs > 0) {
if (tNow >= statusBannerUntilMs) { if (tNow >= statusBannerUntilMs) {
statusBanner.clear(); statusBanner.clear();
+31 -4
View File
@@ -215,7 +215,7 @@ enum class AtTextMode {
const char* kAtTapMap[10] = { const char* kAtTapMap[10] = {
" 0+=", " 0+=",
"+?=,1\"':;/-_", "+?=,1\"':;/-_*#@$%&()[]{}",
"ABC2", "ABC2",
"DEF3", "DEF3",
"GHI4", "GHI4",
@@ -969,18 +969,36 @@ int runModemApp(int argc, char** argv) {
(void)portNames; (void)portNames;
// Row 0: AT Command Input Box (Y=contentY..contentY+46) // Row 0: AT Command Input Box (Y=contentY..contentY+46)
bool focus0 = (atFocusRow == 0);
int mbW = pangoTextWidth(layoutSmall, modeBadge);
if (focus0 && atLastTapKey >= 0 && atLastTapKey <= 10 &&
(nowMs() - atLastTapMs) < 850) {
const char* seq = (atLastTapKey == 10)
? "+?=,1\"':;/-_*#"
: kAtTapMap[atLastTapKey];
std::string pref = (atLastTapKey == 10)
? "*:"
: (std::to_string(atLastTapKey) + ":");
std::string cand = formatMultiTapBar(
pref, seq, atLastTapIdx, atTextMode == AtTextMode::Upper, 5, 16);
drawPango(layoutSmall, &colYellow, 8, contentY, 18, cand,
kWinWidth - mbW - 18);
} else {
drawPango(layoutSmall, &colYellow, 8, contentY, 18, drawPango(layoutSmall, &colYellow, 8, contentY, 18,
std::string(kIconTerm) + " AT (# Input mode):"); std::string(kIconTerm) + " AT (# Input mode):");
int mbW = pangoTextWidth(layoutSmall, modeBadge); }
drawPango(layoutSmall, &colGreen, kWinWidth - mbW - 8, contentY, 18, modeBadge); drawPango(layoutSmall, &colGreen, kWinWidth - mbW - 8, contentY, 18, modeBadge);
bool focus0 = (atFocusRow == 0);
XSetForeground(dpy, gc, focus0 ? pt.spec.bgInputFocus : pt.spec.bgAltEven); XSetForeground(dpy, gc, focus0 ? pt.spec.bgInputFocus : pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 12, 26); XFillRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 12, 26);
XSetForeground(dpy, gc, focus0 ? pt.spec.accent : pt.spec.border); XSetForeground(dpy, gc, focus0 ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 13, 25); XDrawRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 13, 25);
std::string atDisp = atInput + (focus0 ? "_" : "");
if (focus0) {
atDisp = tailFitPangoText(layoutMain, atDisp, kWinWidth - 24);
}
drawPango(layoutMain, &colWhite, 12, contentY + 19, 26, drawPango(layoutMain, &colWhite, 12, contentY + 19, 26,
atInput + (focus0 ? "_" : ""), kWinWidth - 24); atDisp, kWinWidth - 24);
// Row 1: Preset & AT Port Selector (Y=contentY+49..contentY+73) // Row 1: Preset & AT Port Selector (Y=contentY+49..contentY+73)
bool focus1 = (atFocusRow == 1); bool focus1 = (atFocusRow == 1);
@@ -2061,6 +2079,15 @@ int runModemApp(int argc, char** argv) {
timeoutMs = static_cast<int>(bRem); timeoutMs = static_cast<int>(bRem);
} }
} }
if (activeTab == TabId::AtConsole && atLastTapKey != -1) {
int64_t tRem = (atLastTapMs + 851) - now;
if (tRem <= 0) {
atLastTapKey = -1;
render();
} else if (timeoutMs < 0 || tRem < timeoutMs) {
timeoutMs = static_cast<int>(tRem);
}
}
// Periodic 10-second status refresh while visible on SIM/Radio tabs // Periodic 10-second status refresh while visible on SIM/Radio tabs
if (modal == ModalMode::None && activeTab != TabId::ModemLog && if (modal == ModalMode::None && activeTab != TabId::ModemLog &&
activeTab != TabId::AtConsole) { activeTab != TabId::AtConsole) {
+42 -5
View File
@@ -50,7 +50,7 @@ enum class TextMode {
const char* kMultiTapMap[10] = { const char* kMultiTapMap[10] = {
" 0+_-", // 0 " 0+_-", // 0
".,?!@-_1:/#", // 1 kMultiTapPunct1, // 1
"abc2", // 2 "abc2", // 2
"def3", // 3 "def3", // 3
"ghi4", // 4 "ghi4", // 4
@@ -551,7 +551,7 @@ int runNetworksApp(int argc, char** argv) {
int seqLen = static_cast<int>(std::strlen(seq)); int seqLen = static_cast<int>(std::strlen(seq));
if (seqLen <= 0) return false; if (seqLen <= 0) return false;
if (digit == lastTapDigit && (now - lastTapMs) < 750 && !targetStr.empty()) { if (digit == lastTapDigit && (now - lastTapMs) <= 850 && !targetStr.empty()) {
popUtf8Char(targetStr); popUtf8Char(targetStr);
lastTapCycle = (lastTapCycle + 1) % seqLen; lastTapCycle = (lastTapCycle + 1) % seqLen;
} else { } else {
@@ -844,10 +844,23 @@ int runNetworksApp(int argc, char** argv) {
std::string disp = showPassword ? wifiPassword std::string disp = showPassword ? wifiPassword
: std::string(wifiPassword.size(), '*'); : std::string(wifiPassword.size(), '*');
drawPango(layoutMain, &colWhite, 24, 100, 34, disp + "_", kWinWidth - 48); std::string dispFit =
tailFitPangoText(layoutMain, disp + "_", kWinWidth - 48);
drawPango(layoutMain, &colWhite, 24, 100, 34, dispFit, kWinWidth - 48);
if (lastTapDigit >= 0 && lastTapDigit <= 9 &&
(nowMs() - lastTapMs) <= 850) {
const char* seq = (textMode == TextMode::Sym)
? kSymTapMap[lastTapDigit]
: kMultiTapMap[lastTapDigit];
std::string cand = formatMultiTapBar(
std::to_string(lastTapDigit) + ":", seq, lastTapCycle,
textMode == TextMode::Upper, 6, 19);
drawPango(layoutSmall, &colYellow, 18, 144, 18, cand, kWinWidth - 36);
} else {
drawPango(layoutSmall, &colMuted, 18, 144, 18, drawPango(layoutSmall, &colMuted, 18, 144, 18,
"# Input mode", kWinWidth - 36); "# Input mode", kWinWidth - 36);
}
drawPango(layoutSmall, &colMuted, 18, 164, 18, drawPango(layoutSmall, &colMuted, 18, 164, 18,
"[*] Toggle Show/Hide password"); "[*] Toggle Show/Hide password");
drawPango(layoutSmall, &colMuted, 18, 184, 18, drawPango(layoutSmall, &colMuted, 18, 184, 18,
@@ -860,7 +873,18 @@ int runNetworksApp(int argc, char** argv) {
XSetForeground(dpy, gc, pt.spec.accent); XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 10, 36, kWinWidth - 21, 219); XDrawRectangle(dpy, pix, gc, 10, 36, kWinWidth - 21, 219);
if (lastTapDigit >= 0 && lastTapDigit <= 9 &&
(nowMs() - lastTapMs) <= 850) {
const char* seq = (textMode == TextMode::Sym)
? kSymTapMap[lastTapDigit]
: kMultiTapMap[lastTapDigit];
std::string cand = formatMultiTapBar(
std::to_string(lastTapDigit) + ":", seq, lastTapCycle,
textMode == TextMode::Upper, 5, 16);
drawPango(layoutSmall, &colYellow, 18, 40, 22, cand, kWinWidth - 78);
} else {
drawPango(layoutMain, &colYellow, 18, 40, 22, "Cellular APN Setup"); drawPango(layoutMain, &colYellow, 18, 40, 22, "Cellular APN Setup");
}
drawPango(layoutSmall, &colBlue, kWinWidth - 58, 40, 22, drawPango(layoutSmall, &colBlue, kWinWidth - 58, 40, 22,
std::string("[") + textModeLabel() + "]"); std::string("[") + textModeLabel() + "]");
@@ -874,8 +898,11 @@ int runNetworksApp(int argc, char** argv) {
XFillRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 36, 24); XFillRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 36, 24);
XSetForeground(dpy, gc, isF ? pt.spec.accent : pt.spec.border); XSetForeground(dpy, gc, isF ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 37, 23); XDrawRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 37, 23);
drawPango(layoutSmall, &colWhite, 22, fy + 16, 24, std::string fText = vals[f] + (isF ? "_" : "");
vals[f] + (isF ? "_" : ""), kWinWidth - 44); if (isF) {
fText = tailFitPangoText(layoutSmall, fText, kWinWidth - 44);
}
drawPango(layoutSmall, &colWhite, 22, fy + 16, 24, fText, kWinWidth - 44);
} }
bool saveSel = (apnFieldSel == 3); bool saveSel = (apnFieldSel == 3);
@@ -1457,6 +1484,16 @@ int runNetworksApp(int argc, char** argv) {
timeoutMs = static_cast<int>(bRem); timeoutMs = static_cast<int>(bRem);
} }
} }
if (lastTapDigit != -1 &&
(modal == NetModal::WifiPassword || modal == NetModal::ApnEditor)) {
int64_t tRem = (lastTapMs + 851) - now;
if (tRem <= 0) {
lastTapDigit = -1;
render();
} else if (timeoutMs < 0 || tRem < timeoutMs) {
timeoutMs = static_cast<int>(tRem);
}
}
} }
struct pollfd pfds[2]; struct pollfd pfds[2];
+106
View File
@@ -2721,6 +2721,112 @@ inline bool qrtrWriteSimContacts(
return outExported > 0; return outExported > 0;
} }
inline constexpr const char* kMultiTapPunct1 =
".,-_!?/@:;'\"+=*#$%^&()[]{}<>|\\~`1";
inline constexpr const char* kMultiTapStar =
"*#+-=_/\\|~`@$%^&()[]{}<>'\".,!?:;";
inline std::string formatMultiTapBar(const std::string& prefix,
const char* table, int activeIdx,
bool makeUpper,
int maxItems = 7,
size_t maxChars = 21) {
if (!table || !table[0]) return "";
int total = static_cast<int>(std::strlen(table));
if (total <= 0) return "";
activeIdx = std::clamp(activeIdx, 0, total - 1);
std::vector<std::string> labels;
labels.reserve(total);
for (int i = 0; i < total; ++i) {
char c = table[i];
if (c == ' ') {
labels.push_back("SPC");
} else if (c == '\n') {
labels.push_back("NL");
} else if (c == '\t') {
labels.push_back("TAB");
} else {
if (makeUpper && std::isalpha(static_cast<unsigned char>(c))) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
labels.push_back(std::string(1, c));
}
}
int halfWin = std::max(1, maxItems / 2);
int start = std::max(0, activeIdx - halfWin);
int end = start;
size_t width = 0;
int count = 0;
while (start <= activeIdx) {
width = 0;
count = 0;
end = start;
while (end < total && count < maxItems) {
size_t iw = labels[end].size() + 2;
if (count > 0 && width + iw > maxChars) break;
width += iw;
++end;
++count;
}
if (activeIdx < end) break;
++start;
}
while (start > 0 && count < maxItems) {
size_t iw = labels[start - 1].size() + 2;
if (width + iw > maxChars) break;
--start;
width += iw;
++count;
}
std::string out = prefix;
out += (start > 0) ? "<" : " ";
for (int i = start; i < end; ++i) {
if (i == activeIdx) {
out += "[" + labels[i] + "]";
} else {
out += " " + labels[i] + " ";
}
}
if (end < total) {
out += ">";
}
return out;
}
inline std::string tailFitPangoText(PangoLayout* layout,
const std::string& text,
int maxWidthPx) {
if (!layout || text.empty() || maxWidthPx <= 0) return text;
pango_layout_set_width(layout, -1);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.size()));
int w = 0, h = 0;
pango_layout_get_pixel_size(layout, &w, &h);
if (w <= maxWidthPx) return text;
size_t pos = 0;
while (pos < text.size()) {
pos++;
while (pos < text.size() &&
(static_cast<unsigned char>(text[pos]) & 0xC0) == 0x80) {
pos++;
}
std::string candidate = ".." + text.substr(pos);
pango_layout_set_text(layout, candidate.c_str(),
static_cast<int>(candidate.size()));
pango_layout_get_pixel_size(layout, &w, &h);
if (w <= maxWidthPx) {
return candidate;
}
}
return text;
}
} // namespace bananarchy } // namespace bananarchy
#endif // DWM_PLUGINS_PHONE_DATA_H #endif // DWM_PLUGINS_PHONE_DATA_H
+35 -5
View File
@@ -41,7 +41,7 @@ enum class TextCaseMode {
const char* kMultiTapSms[10] = { const char* kMultiTapSms[10] = {
" 0\n", // 0 " 0\n", // 0
".,?!'-:;()@/1", // 1 kMultiTapPunct1, // 1
"abc2", // 2 "abc2", // 2
"def3", // 3 "def3", // 3
"ghi4", // 4 "ghi4", // 4
@@ -776,11 +776,17 @@ int runSmsApp(int argc, char** argv) {
if (!cname.empty()) { if (!cname.empty()) {
toDisp += " (" + cname + ")"; toDisp += " (" + cname + ")";
} }
if (toSel && cname.empty()) {
toDisp = tailFitPangoText(layoutMain, toDisp, kWinWidth - 20);
}
drawPango(layoutMain, toSel ? &colWhite : &colLight, 10, 36, 32, toDisp, drawPango(layoutMain, toSel ? &colWhite : &colLight, 10, 36, 32, toDisp,
kWinWidth - 20); kWinWidth - 20);
// Field 1: Message Body // Field 1: Message Body
bool msgSel = (composeField == 1); bool msgSel = (composeField == 1);
bool smsTapActive =
(msgSel && lastTapDigit >= 0 && lastTapDigit <= 9 &&
(nowMs() - lastTapMs) <= 850);
XSetForeground(dpy, gc, XSetForeground(dpy, gc,
msgSel ? pt.spec.bgInputFocus : pt.spec.bgInput); msgSel ? pt.spec.bgInputFocus : pt.spec.bgInput);
XFillRectangle(dpy, pix, gc, 6, 72, kWinWidth - 12, 156); XFillRectangle(dpy, pix, gc, 6, 72, kWinWidth - 12, 156);
@@ -789,6 +795,7 @@ int runSmsApp(int argc, char** argv) {
std::string bodyDisp = composeText; std::string bodyDisp = composeText;
if (msgSel) bodyDisp += "|"; if (msgSel) bodyDisp += "|";
int bodyMaxH = smsTapActive ? 126 : 146;
if (bodyDisp.empty()) { if (bodyDisp.empty()) {
drawPango(layoutSmall, &colMuted, 12, 78, 20, drawPango(layoutSmall, &colMuted, 12, 78, 20,
"Type message (# Input mode)...", kWinWidth - 24); "Type message (# Input mode)...", kWinWidth - 24);
@@ -801,17 +808,28 @@ int runSmsApp(int argc, char** argv) {
int tw = 0, th = 0; int tw = 0, th = 0;
pango_layout_get_pixel_size(layoutMain, &tw, &th); pango_layout_get_pixel_size(layoutMain, &tw, &th);
int textY = 76; int textY = 76;
if (th > 146) { if (th > bodyMaxH) {
textY = 76 - (th - 146); textY = 76 - (th - bodyMaxH);
} }
XRectangle clipRect {6, 73, static_cast<unsigned short>(kWinWidth - 12), XRectangle clipRect {6, 73, static_cast<unsigned short>(kWinWidth - 12),
154}; static_cast<unsigned short>(smsTapActive ? 134 : 154)};
XftDrawSetClipRectangles(xftDraw, 0, 0, &clipRect, 1); XftDrawSetClipRectangles(xftDraw, 0, 0, &clipRect, 1);
pango_xft_render_layout(xftDraw, &colWhite, layoutMain, pango_xft_render_layout(xftDraw, &colWhite, layoutMain,
12 * PANGO_SCALE, textY * PANGO_SCALE); 12 * PANGO_SCALE, textY * PANGO_SCALE);
XftDrawSetClip(xftDraw, nullptr); XftDrawSetClip(xftDraw, nullptr);
} }
if (smsTapActive) {
XSetForeground(dpy, gc, pt.spec.bgAltOdd);
XFillRectangle(dpy, pix, gc, 7, 208, kWinWidth - 14, 19);
XSetForeground(dpy, gc, pt.spec.divider);
XDrawLine(dpy, pix, gc, 7, 208, kWinWidth - 8, 208);
std::string cand = formatMultiTapBar(
std::to_string(lastTapDigit) + ":", kMultiTapSms[lastTapDigit],
lastTapCycle, caseMode == TextCaseMode::Upper);
drawPango(layoutSmall, &colYellow, 10, 208, 19, cand, kWinWidth - 20);
}
// Field 2 & 3: Send & Save Draft buttons // Field 2 & 3: Send & Save Draft buttons
bool sendSel = (composeField == 2); bool sendSel = (composeField == 2);
bool draftSel = (composeField == 3); bool draftSel = (composeField == 3);
@@ -956,7 +974,8 @@ int runSmsApp(int argc, char** argv) {
XFillRectangle(dpy, pix, gc, 12, 80, kWinWidth - 24, 34); XFillRectangle(dpy, pix, gc, 12, 80, kWinWidth - 24, 34);
XSetForeground(dpy, gc, pt.spec.accentAlt); XSetForeground(dpy, gc, pt.spec.accentAlt);
XDrawRectangle(dpy, pix, gc, 12, 80, kWinWidth - 25, 33); XDrawRectangle(dpy, pix, gc, 12, 80, kWinWidth - 25, 33);
std::string disp = addBlockInput + "|"; std::string disp =
tailFitPangoText(layoutMain, addBlockInput + "|", kWinWidth - 36);
drawPango(layoutMain, &colWhite, 18, 83, 28, disp, kWinWidth - 36); drawPango(layoutMain, &colWhite, 18, 83, 28, disp, kWinWidth - 36);
drawPango(layoutSmall, &colMuted, 12, 124, 20, drawPango(layoutSmall, &colMuted, 12, 124, 20,
"0-9 / * / #: Digits #: + prefix", 216); "0-9 / * / #: Digits #: + prefix", 216);
@@ -1660,6 +1679,12 @@ int runSmsApp(int argc, char** argv) {
zeroHeld = false; zeroHeld = false;
} }
if (visible && mode == ViewMode::Compose && lastTapDigit != -1 &&
(tNow - lastTapMs) > 850) {
lastTapDigit = -1;
drawUI();
}
int timeoutMs = -1; int timeoutMs = -1;
if (visible) { if (visible) {
timeoutMs = 250; timeoutMs = 250;
@@ -1672,6 +1697,11 @@ int runSmsApp(int argc, char** argv) {
timeoutMs, timeoutMs,
std::max(1, static_cast<int>((zeroPressMs + 420) - tNow))); std::max(1, static_cast<int>((zeroPressMs + 420) - tNow)));
} }
if (mode == ViewMode::Compose && lastTapDigit != -1) {
timeoutMs = std::min(
timeoutMs,
std::max(1, static_cast<int>(851 - (tNow - lastTapMs))));
}
} }
struct pollfd pfds[3]; struct pollfd pfds[3];
+22 -3
View File
@@ -43,7 +43,7 @@ enum class TextCaseMode {
const char* kMultiTapTheme[10] = { const char* kMultiTapTheme[10] = {
" _-0", // 0 " _-0", // 0
".-_1", // 1 ".-_1,!?+@#$%~()[]{}", // 1
"abc2", // 2 "abc2", // 2
"def3", // 3 "def3", // 3
"ghi4", // 4 "ghi4", // 4
@@ -821,13 +821,22 @@ int runThemesApp(int argc, char** argv) {
XFillRectangle(dpy, pix, gc, 10, 74, kWinWidth - 20, 38); XFillRectangle(dpy, pix, gc, 10, 74, kWinWidth - 20, 38);
XSetForeground(dpy, gc, pt.spec.accentAlt); XSetForeground(dpy, gc, pt.spec.accentAlt);
XDrawRectangle(dpy, pix, gc, 10, 74, kWinWidth - 21, 37); XDrawRectangle(dpy, pix, gc, 10, 74, kWinWidth - 21, 37);
drawPango(layoutMain, &colWhite, 16, 74, 38, nameInput + "|", std::string nameDisp =
kWinWidth - 32); tailFitPangoText(layoutMain, nameInput + "|", kWinWidth - 32);
drawPango(layoutMain, &colWhite, 16, 74, 38, nameDisp, kWinWidth - 32);
drawPango(layoutSmall, &colMuted, 10, 124, 20, drawPango(layoutSmall, &colMuted, 10, 124, 20,
"Saves to ~/.xthemes/<Name>.xth", 220); "Saves to ~/.xthemes/<Name>.xth", 220);
if (lastTapDigit >= 0 && lastTapDigit <= 9 &&
(nowMs() - lastTapMs) <= 850) {
std::string cand = formatMultiTapBar(
std::to_string(lastTapDigit) + ":", kMultiTapTheme[lastTapDigit],
lastTapCycle, caseMode == TextCaseMode::Upper);
drawPango(layoutSmall, &colYellow, 10, 146, 20, cand, 220);
} else {
drawPango(layoutSmall, &colMuted, 10, 146, 20, drawPango(layoutSmall, &colMuted, 10, 146, 20,
"# Input mode", 220); "# Input mode", 220);
}
drawSoftkeyBar("Save", "Save", nameInput.empty() ? "Cancel" : "Del"); drawSoftkeyBar("Save", "Save", nameInput.empty() ? "Cancel" : "Del");
} else if (mode == ViewMode::ConfirmDelete && !themes.empty()) { } else if (mode == ViewMode::ConfirmDelete && !themes.empty()) {
@@ -1323,10 +1332,20 @@ int runThemesApp(int argc, char** argv) {
} }
} }
if (visible && mode == ViewMode::NameModal && lastTapDigit != -1 &&
(tNow - lastTapMs) > 850) {
lastTapDigit = -1;
drawUI();
}
int timeoutMs = -1; int timeoutMs = -1;
if (visible && softkeyPending != 0) { if (visible && softkeyPending != 0) {
timeoutMs = std::max(1, static_cast<int>(softkeyDeadlineMs - tNow)); timeoutMs = std::max(1, static_cast<int>(softkeyDeadlineMs - tNow));
} }
if (visible && mode == ViewMode::NameModal && lastTapDigit != -1) {
int tapRem = std::max(1, static_cast<int>(851 - (tNow - lastTapMs)));
timeoutMs = (timeoutMs < 0) ? tapRem : std::min(timeoutMs, tapRem);
}
struct pollfd pfds[2]; struct pollfd pfds[2];
int nfds = 0; int nfds = 0;
+25 -9
View File
@@ -975,18 +975,23 @@ void T9State::showPreview(const std::vector<T9Item>& items, int index) {
if (total == 0) { if (total == 0) {
return; return;
} }
index = std::clamp(index, 0, total - 1);
constexpr int kMaxItemsPerPage = 7; constexpr int kMaxItemsPerPage = 7;
std::string prefix = modePrefix() + ":"; std::string prefix = modePrefix() + ":";
size_t maxItemsWidth = size_t maxItemsWidth =
(kFixedPreeditChars > prefix.size() + 1) (kFixedPreeditChars > prefix.size() + 2)
? (kFixedPreeditChars - prefix.size() - 1) ? (kFixedPreeditChars - prefix.size() - 2)
: 23; : 21;
int start = 0; int halfWin = kMaxItemsPerPage / 2;
int end = 0; int start = std::max(0, index - halfWin);
while (start < total) { int end = start;
size_t width = 0; size_t width = 0;
int count = 0; int count = 0;
while (start <= index) {
width = 0;
count = 0;
end = start; end = start;
while (end < total && count < kMaxItemsPerPage) { while (end < total && count < kMaxItemsPerPage) {
size_t itemWidth = items[end].label.size() + 2; size_t itemWidth = items[end].label.size() + 2;
@@ -1000,10 +1005,21 @@ void T9State::showPreview(const std::vector<T9Item>& items, int index) {
if (index < end) { if (index < end) {
break; break;
} }
start = end; ++start;
}
while (start > 0 && count < kMaxItemsPerPage) {
size_t itemWidth = items[start - 1].label.size() + 2;
if (width + itemWidth > maxItemsWidth) {
break;
}
--start;
width += itemWidth;
++count;
} }
std::string preedit = prefix; std::string preedit = prefix;
preedit += (start > 0) ? "<" : " ";
for (int i = start; i < end; ++i) { for (int i = start; i < end; ++i) {
if (i == index) { if (i == index) {
preedit += "["; preedit += "[";
@@ -1015,8 +1031,8 @@ void T9State::showPreview(const std::vector<T9Item>& items, int index) {
preedit += " "; preedit += " ";
} }
} }
if (start > 0 || end < total) { if (end < total) {
preedit += (end < total) ? ">" : "<"; preedit += ">";
} }
if (preedit.size() < kFixedPreeditChars) { if (preedit.size() < kFixedPreeditChars) {
preedit.append(kFixedPreeditChars - preedit.size(), ' '); preedit.append(kFixedPreeditChars - preedit.size(), ' ');