Skip to content
Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
Loading...
Searching...
No Matches
fontkeyboard.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2026 Twilight
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15*/
16
22
23#include <pse-db/fontsdb.h>
25#include "./fontkeyboard.h"
26
27namespace {
28
29// The 47 assignable keys, in the order they appear on the deck: the number row, then
30// QWERTY, then the home row, then the bottom row. EVERY page table below is written in
31// exactly this order, so a page reads like the keyboard looks.
32//
33// The punctuation keys (` - = [ ] \ ; ' , . /) are in here because the tiles that
34// BELONG on them should be on them: `.` types `.`, `,` types `,`, Shift+`/` types `?`,
35// Shift+`;` types `:`. They were dead filler in the first cut, which meant `.` and `,`
36// were exiled to a number row they have no business being on.
37const char* const kKeys[FontKeyboard::keyTotal] = {
38 "`","1","2","3","4","5","6","7","8","9","0","-","=",
39 "Q","W","E","R","T","Y","U","I","O","P","[","]","\\",
40 "A","S","D","F","G","H","J","K","L",";","'",
41 "Z","X","C","V","B","N","M",",",".","/",
42};
43
44// ---------------------------------------------------------------------------
45// THE MAP. Font codes (FontDBEntry::ind); 0 = an empty key.
46//
47// Read each block as the deck: row 1 is the number row, row 2 is QWERTYUIOP,
48// row 3 is ASDFGHJKL, row 4 is ZXCVBNM.
49//
50// Every tile 1-255 appears exactly once across these 8 pages + kSpaceInd. Do not
51// "tidy" a tile from one page to another without re-running tst_font_keyboard --
52// it checks the invariant, and a duplicate or a hole would be a silent bug.
53//
54// NOTE the tables are named, not numbered, and are wired to pages by MODIFIER MASK
55// (kPages, below): a page's index IS its modifier bits (shift=1, ctrl=2, alt=4), so
56// Alt is page 4 -- not page 4-in-reading-order. Mixing those two up silently swaps
57// two whole pages, which is exactly what tst_font_keyboard caught the first time.
58// The human reading order lives in kPageOrder.
59// ---------------------------------------------------------------------------
60
61// Each block is written as the deck reads, one line per row:
62// row 1: ` 1 2 3 4 5 6 7 8 9 0 - =
63// row 2: Q W E R T Y U I O P [ ] '\'
64// row 3: A S D F G H J K L ; '
65// row 4: Z X C V B N M , . /
66
67// (no modifier) -- LETTERS. The base layer of a real keyboard: press a key, get the
68// LOWERCASE letter; press a digit, get the digit; press "." and get ".". Pure identity,
69// nothing to learn.
70//
71// (This was uppercase-unshifted at first, on the reasoning that Gen 1 names are all
72// caps. Twilight rejected it, and she's right: it's the one place the deck would have
73// contradicted every keyboard the user has ever touched. Caps Lock is how you type a
74// name in caps -- see effectivePage().)
75const int kPageLetters[FontKeyboard::keyTotal] = {
76 0, 247, 248, 249, 250, 251, 252, 253, 254, 255, 246, 227, 0, // 1..0 -
77 176, 182, 164, 177, 179, 184, 180, 168, 174, 175, 158, 159, 0, // q..p [ ]
78 160, 178, 163, 165, 166, 167, 169, 170, 171, 157, 224, // a..l ; '
79 185, 183, 162, 181, 161, 173, 172, 244, 232, 243, // z..m , . /
80};
81
82// Shift -- UPPERCASE + the Normal symbols, each on the key a real keyboard puts it on
83// wherever the game HAS that glyph:
84// Shift+1 = ! Shift+4 = $ Shift+8 = x (the "*" key -- multiply)
85// Shift+9 = ( Shift+0 = ) Shift+; = : Shift+' = " Shift+/ = ?
86// The glyphs the game simply doesn't have (@ # % ^ & _ + { } | < >) leave their keys
87// empty -- except that the five spare number-row slots take the Normal characters with
88// nowhere else to be: the gender signs and Pk/Mn.
89//
90// Pages 0+1 together are the entire in-game-legal character set: everything you can
91// type on the real Game Boy's name screen.
92const int kPageCaps[FontKeyboard::keyTotal] = {
93 0, 231, 239, 245, 240, 225, 226, 0, 241, 154, 155, 0, 0, // ! M F $ Pk Mn _ x ( )
94 144, 150, 132, 145, 147, 152, 148, 136, 142, 143, 0, 0, 0, // Q..P
95 128, 146, 131, 133, 134, 135, 137, 138, 139, 156, 114, // A..L : "
96 153, 151, 130, 149, 129, 141, 140, 0, 0, 230, // Z..M ?
97};
98
99// Ctrl -- SYMBOLS. The 13 bold letters sit on their own letters, so Ctrl+B is bold B --
100// the same thing it means in every other program on earth. The rest is the punctuation
101// that has a real partner one layer up: the closing double quote under the opening one
102// on ', the narrow colon under ":" on ;, the ellipsis on "." (three dots on the dot
103// key), and the middle dot on ",".
104//
105// The alternate period `<.>` sits on the spare "=" key, NOT on "/" where it was: a
106// period that appears on the SLASH key -- while a perfectly good period sits on the
107// period key one layer down -- is a false affordance. If a tile can go where a real
108// keyboard would put it, it goes there; if it can't, it must not pretend.
109const int kPageSymbols[FontKeyboard::keyTotal] = {
110 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 116, // middle-dot on "="
111 0, 0, 100, 0, 0, 0, 0, 104, 0, 0, 112, 113, 0, // boldE boldI ' '
112 96, 106, 99, 101, 102, 103, 0, 186, 107, 109, 115, // boldA..H e-acute boldL :(narrow) "
113 0, 0, 98, 105, 97, 0, 108, 242, 117, 0, // boldC boldV boldB boldM .(alt) ...
114};
115
116// Alt -- CODES. Contractions sit on their own letter ('s on S, 't on T...). The
117// variables pair up: Player on P with Rival ("Opponent") right beside it on O, and the
118// two usage codes on Y ("Your usage") / U ("User"). The kana -- the rarest things here
119// -- take the number row.
120const int kPageCodes[FontKeyboard::keyTotal] = {
121 0, 118, 110, 111, 119, 120, 233, 234, 235, 0, 0, 0, 0, // a i u e o + small a u e
122 0, 0, 84, 228, 190, 89, 90, 0, 83, 82, 0, 0, 0, // Poke 'r 't targ user rival player
123 0, 189, 187, 86, 94, 93, 92, 74, 188, 0, 0, // 's 'd ...... ROCKET TRAINER TM PkMn 'l
124 0, 0, 91, 191, 0, 0, 229, 0, 0, 0, // PC 'v 'm
125};
126
127// Shift+Ctrl -- TILES I. The box-frame glyphs are laid out AS A BOX you can type:
128//
129// Q W E ╔ ═ ╗
130// A S D --> ║ ║ (S is the box's hollow middle, so it's empty)
131// Z X C ╚ ═ ╝
132//
133// so a border is literally "Q W W W E" then "Z X X X C". Twilight's idea, and it's a
134// better one than parking them in reading order: the keys ARE the picture.
135//
136// This is why the horizontal (122) and vertical (124) edges appear TWICE on this page --
137// deliberately, and the only duplicated tiles anywhere in the map. You cannot draw a box
138// with one vertical edge on one side. `tst_font_keyboard` pins exactly this: every tile
139// reachable, and these two the only ones reachable from more than one key.
140//
141// The three cursor arrows take , . / -- they're arrow-ish keys, and it keeps the whole
142// "things that point" family together.
143// (tile14 sits on the "1" key by request -- it's the one people reach for -- so it and
144// the tile that would have been there simply trade places.)
145const int kPageTiles1[FontKeyboard::keyTotal] = {
146 1, 20, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // tile14 on "1"
147 121, 122, 123, 14, 15, 16, 17, 18, 19, 2, 21, 22, 23, // ╔ ═ ╗ + tiles
148 124, 0, 124, 24, 25, 26, 27, 28, 29, 30, 31, // ║ (hollow) ║ + tiles
149 125, 122, 126, 32, 33, 34, 35, 237, 238, 236, // ╚ ═ ╝ + tiles, arrows on , . /
150};
151
152// Shift+Alt -- TILES II. The rest of the real tileset: tile24..tile48 + tile4D.
153const int kPageTiles2[FontKeyboard::keyTotal] = {
154 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
155 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
156 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, // ...tile48
157 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, // tile4D
158};
159
160// Ctrl+Alt -- TILES III. tileC0..tileDF -- the high half of the tilemap, which is
161// USUALLY BLANK. These only hold anything in particular game states, which is exactly
162// why they're all together on their own page behind their own chord, with a description
163// that says so rather than leaving you wondering why the keys look empty.
164const int kPageTiles3[FontKeyboard::keyTotal] = {
165 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204,
166 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,
167 218, 219, 220, 221, 222, 223, 0, 0, 0, 0, 0,
168 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
169};
170
171// Shift+Ctrl+Alt -- CONTROLS. The codes that GLITCH a name live behind the most
172// deliberate key combination on the deck -- you cannot hit <end> by accident. Mnemonic
173// letters throughout (E=end, L=line, N=next, P=page, X=dex...).
174const int kPageControls[FontKeyboard::keyTotal] = {
175 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
176 0, 0, 80, 88, 0, 0, 0, 0, 0, 73, 0, 0, 0, // end prompt ... page
177 81, 0, 87, 0, 0, 0, 0, 0, 79, 0, 0, // para done line
178 0, 95, 85, 75, 76, 78, 0, 0, 0, 0, // dex cont cont_ autocont next
179};
180
181// A page's INDEX IS ITS MODIFIER MASK (shift = 1, ctrl = 2, alt = 4). That's the
182// whole trick: pageFor() is just the mask, no lookup, and the deck can never show a
183// page that disagrees with the keys you're holding.
184const int* const kPages[FontKeyboard::pageTotal] = {
185 kPageLetters, // 0 = --- (nothing held)
186 kPageCaps, // 1 = Shift
187 kPageSymbols, // 2 = Ctrl
188 kPageTiles1, // 3 = Shift+Ctrl
189 kPageCodes, // 4 = Alt
190 kPageTiles2, // 5 = Shift+Alt
191 kPageTiles3, // 6 = Ctrl+Alt
192 kPageControls, // 7 = Shift+Ctrl+Alt
193};
194
195// The 255th tile. Space is the only tile NOT on an alphanumeric key -- it rides
196// the deck's real spacebar, which is where every human already expects it.
197constexpr int kSpaceInd = 127;
198
199// Indexed by mask, like kPages.
200const char* const kPageNames[FontKeyboard::pageTotal] = {
201 "Letters", "Uppercase", "Symbols", "Tiles I",
202 "Codes", "Tiles II", "Tiles III", "Controls",
203};
204
205const char* const kPageBadges[FontKeyboard::pageTotal] = {
206 "", "Shift", "Ctrl", "Shift+Ctrl",
207 "Alt", "Shift+Alt", "Ctrl+Alt", "Shift+Ctrl+Alt",
208};
209
210// The order a HUMAN should meet the pages -- by category (Normal, Single-Char,
211// Multi-Char, Variable, Picture, Control), cheapest chord first. This is NOT the mask
212// order: Alt (4) is a one-key chord and belongs before Shift+Ctrl (3), which is two.
213// The page strip renders in this order; everything else indexes by mask.
214const int kPageOrder[FontKeyboard::pageTotal] = { 0, 1, 2, 4, 3, 5, 6, 7 };
215
216// A quiet line under the deck saying what you're looking at. Indexed by mask.
217const char* const kPageDescs[FontKeyboard::pageTotal] = {
218 "The characters the game itself lets you type into a name.",
219 "The same set, shifted -- capitals and the punctuation above the number row.",
220 "Bold letters and the extra marks: accents, quotes, dots.",
221 "Tiles from the loaded tileset. Q W E / A D / Z X C draws a box.",
222 "One byte, many characters: names pulled from memory, and words the game spells out for you.",
223 "The rest of the loaded tileset -- these change as you play.",
224 "Usually blank: these only hold anything in certain game states.",
225 "Text-engine codes. These do not print -- they END, WRAP or PAUSE the text, and in a name they glitch it.",
226};
227
228// What a REAL keyboard produces for this key, so a tile that lands on its natural key
229// can drop the little legend in the corner (there is nothing to teach: the key IS the
230// character). US layout, unshifted then shifted.
231const char* const kNaturalPlain[] = {
232 "`","1","2","3","4","5","6","7","8","9","0","-","=",
233 "[","]","\\",";","'",",",".","/",
234};
235const char* const kNaturalShift[] = {
236 "~","!","@","#","$","%","^","&","*","(",")","_","+",
237 "{","}","|",":","\"","<",">","?",
238};
239constexpr int kNaturalCount = 21;
240
244FontKeyboard::Category categoryOf(const FontDBEntry* f)
245{
246 if(f == nullptr) return FontKeyboard::CatNone;
247 if(f->getNormal()) return FontKeyboard::CatNormal;
248 if(f->getControl()) return FontKeyboard::CatControl;
249 if(f->getPicture()) return FontKeyboard::CatPicture;
252 if(f->getMultiChar()) return FontKeyboard::CatMulti;
253
255}
256
259FontKeyboard::Render renderOf(const FontDBEntry* f)
260{
261 if(f == nullptr) return FontKeyboard::RenderEmpty;
264
266}
267
268} // namespace
269
271 : QObject(parent)
272{}
273
274int FontKeyboard::getPage() const
275{
276 return page;
277}
278
279void FontKeyboard::setPage(int val)
280{
281 if(val < 0 || val >= pageTotal || val == page)
282 return;
283
284 page = val;
285 emit pageChanged();
286}
287
288QStringList FontKeyboard::getPageNames() const
289{
290 QStringList ret;
291 ret.reserve(pageTotal);
292
293 for(int i = 0; i < pageTotal; i++)
294 ret.append(QString(kPageNames[i]));
295
296 return ret;
297}
298
299int FontKeyboard::getPageCount() const
300{
301 return pageTotal;
302}
303
304QVariantList FontKeyboard::getPageOrder() const
305{
306 QVariantList ret;
307 ret.reserve(pageTotal);
308
309 for(int i = 0; i < pageTotal; i++)
310 ret.append(kPageOrder[i]);
311
312 return ret;
313}
314
315QStringList FontKeyboard::keyLabels()
316{
317 QStringList ret;
318 ret.reserve(keyTotal);
319
320 for(int i = 0; i < keyTotal; i++)
321 ret.append(QString(kKeys[i]));
322
323 return ret;
324}
325
326int FontKeyboard::pageFor(bool shift, bool ctrl, bool alt)
327{
328 return (shift ? 1 : 0) | (ctrl ? 2 : 0) | (alt ? 4 : 0);
329}
330
331QString FontKeyboard::pageBadge(int page)
332{
333 if(page < 0 || page >= pageTotal)
334 return QString();
335
336 return QString(kPageBadges[page]);
337}
338
339int FontKeyboard::effectivePage(bool shift, bool ctrl, bool alt, bool caps)
340{
341 // CAPS LOCK LOCKS THE SHIFT PAGE. It is a page selector like every other modifier
342 // here -- not a per-key letter-case rule (2026-07-11, Twilight).
343 //
344 // The first cut gave caps its real-keyboard behaviour: letters only, leaving the
345 // number row typing digits. That is what a physical keyboard does, but it produces a
346 // layer that is NOT one of the 8 pages -- uppercase letters over an unshifted number
347 // row -- so the page strip could not say where you were, and it read as a bug
348 // ("why are there two different page 2s?"). A model the UI cannot display is a bad
349 // model however correct it is. So caps now moves the WHOLE page, and every state the
350 // deck can be in is exactly one page.
351 //
352 // Two rules keep it sane:
353 // * Shift INVERTS it (shift XOR caps) -- caps + Shift is the base page, just as
354 // caps + Shift is lowercase on a real keyboard.
355 // * Ctrl/Alt IGNORE it -- Ctrl+B is bold B with the caps light on or off. Without
356 // this, typing a name in caps and then reaching for Ctrl would land you on Tiles
357 // I instead of Symbols, which nobody would expect or want.
358 //
359 // The cost is that the punctuation row rides along with caps, so typing a digit
360 // means tapping caps off. That is a rare need (digits aren't even in-game-legal in a
361 // name), and the deck SHOWS the number row change, so nothing is hidden.
362 if(ctrl || alt)
363 return pageFor(shift, ctrl, alt);
364
365 return pageFor(shift != caps, false, false); // XOR
366}
367
368QVariantMap FontKeyboard::keyDataFor(const QString& key,
369 bool shift, bool ctrl, bool alt, bool caps) const
370{
371 return keyData(effectivePage(shift, ctrl, alt, caps), key);
372}
373
374int FontKeyboard::indFor(int page, const QString& key)
375{
376 if(page < 0 || page >= pageTotal)
377 return 0;
378
379 const QString wanted = key.toUpper();
380
381 for(int i = 0; i < keyTotal; i++) {
382 if(wanted != QString(kKeys[i]))
383 continue;
384
385 return kPages[page][i];
386 }
387
388 return 0;
389}
390
391QString FontKeyboard::naturalChar(const QString& key, bool shift)
392{
393 if(key.size() == 1) {
394 const QChar c = key.at(0);
395 if(c.isLetter())
396 return shift ? key.toUpper() : key.toLower();
397 }
398
399 for(int i = 0; i < kNaturalCount; i++) {
400 if(key != QString(kNaturalPlain[i]))
401 continue;
402
403 return QString(shift ? kNaturalShift[i] : kNaturalPlain[i]);
404 }
405
406 return QString();
407}
408
410{
411 if(page < 0 || page >= pageTotal)
412 return QString();
413
414 return QString(kPageDescs[page]);
415}
416
417QVariantMap FontKeyboard::keyData(int page, const QString& key) const
418{
419 return dataForInd(indFor(page, key), key.toUpper(), page);
420}
421
422QVariantMap FontKeyboard::spaceData() const
423{
424 return dataForInd(kSpaceInd, QStringLiteral("Space"), 0);
425}
426
427QVariantMap FontKeyboard::dataForInd(int ind, const QString& key, int page) const
428{
429 QVariantMap ret;
430
431 ret["key"] = key;
432 ret["ind"] = ind;
433
434 // Does this key produce, on THIS page, exactly what the same key + the same modifiers
435 // would produce on a real keyboard? (Shift+1 = "!", the A key = "a", "." = ".") If so
436 // the corner legend is noise -- the key is telling you what it types by BEING that
437 // key. Ctrl/Alt pages never match: a real keyboard produces no character for those, so
438 // those keys always keep their legend.
439 const bool chorded = (page & 2) || (page & 4);
440 const QString natural = chorded
441 ? QString()
442 : naturalChar(key, (page & 1) != 0);
443
444 // An unmapped key is a normal, expected thing (34 of the 288 slots are empty) --
445 // it must come back as a well-formed "empty" map, never a null the QML has to
446 // guard against.
447 const FontDBEntry* f = (ind > 0)
449 : nullptr;
450
451 if(f == nullptr) {
452 ret["empty"] = true;
453 ret["code"] = QString();
454 ret["title"] = QString();
455 ret["tip"] = QString();
456 ret["category"] = static_cast<int>(CatNone);
457 ret["render"] = static_cast<int>(RenderEmpty);
458 ret["natural"] = false;
459 return ret;
460 }
461
462 const QString alias = f->getAlias();
463
464 ret["empty"] = false;
465 ret["code"] = f->getName();
466 ret["title"] = (alias != "") ? alias : f->getName();
467 ret["tip"] = f->getTip();
468 ret["category"] = static_cast<int>(categoryOf(f));
469 ret["render"] = static_cast<int>(renderOf(f));
470 // Two ways a corner legend earns its silence:
471 //
472 // 1. `natural` -- the deck types exactly what a real keyboard would for this key +
473 // these modifiers ("a" on A, "!" on Shift+1).
474 // 2. the tile IS this key's letter, whatever the chord: bold A (`<A>`) on the A key
475 // is still, unmistakably, A. Printing a little "A" in the corner of a key showing
476 // a big bold A tells the user nothing they can't see.
477 //
478 // Note this deliberately does NOT catch `<'s>` on S: that tile prints "'s", not "s",
479 // and the legend is the only thing saying which key produced it.
480 QString bare = f->getName();
481 if(bare.startsWith('<') && bare.endsWith('>'))
482 bare = bare.mid(1, bare.size() - 2);
483
484 const bool sameLetter = (bare.size() == 1)
485 && (bare.compare(key, Qt::CaseInsensitive) == 0);
486
487 ret["natural"] = (!natural.isEmpty() && natural == f->getName()) || sameLetter;
488
489 return ret;
490}
491
492QString FontKeyboard::chopLastToken(const QString& str)
493{
494 if(str.isEmpty())
495 return str;
496
497 // A trailing "<...>" is ONE tile, not five characters. Backspace deletes the
498 // whole token or it corrupts the string into something that no longer round-trips
499 // through the codec.
500 if(str.endsWith('>')) {
501 const int open = str.lastIndexOf('<');
502 if(open >= 0)
503 return str.left(open);
504 }
505
506 return str.left(str.size() - 1);
507}
static QString pageDescription(int page)
The quiet line under the deck saying what this page is.
static int indFor(int page, const QString &key)
The font code on key of page, or 0 when that key is empty.
static QString chopLastToken(const QString &str)
Drop the last whole token off a code string – "RED<player>" -> "RED".
static int effectivePage(bool shift, bool ctrl, bool alt, bool caps)
Assignable keys per page: 26 letters + 10 digits + the 11 punctuation keys ( - = [ ] \ ; ' ,...
Render
< Current page = its modifier mask, 0-7.
@ RenderEmpty
No tile on this key.
@ RenderLabel
Control code: no glyph at all, so the cap shows its name.
@ RenderTile
A single 8x8 tile, clipped from the shared animated sheet.
@ RenderPreview
Multi-char/variable: render the expanded text (<poke> -> "Poke").
QVariantMap keyDataFor(const QString &key, bool shift, bool ctrl, bool alt, bool caps) const
keyData() for the page this key actually reads. What the deck binds to.
Category
Tile category. Order matches the old filter strip = the usage order.
protected::void pageChanged()
FontKeyboard(QObject *parent=nullptr)
static QString naturalChar(const QString &key, bool shift)
What a REAL keyboard (US layout) types for key with/without Shift – "a"/"A", "1"/"!...
static constexpr int pageTotal
Modifier combos: none/S/C/A/SC/SA/CA/SCA.
QVariantMap spaceData() const
The same map for the spacebar (the Space tile lives there, not on a letter).
static QString pageBadge(int page)
The modifier badge for a page, e.g. "" / "Shift" / "Ctrl+Alt".
QVariantMap keyData(int page, const QString &key) const
Everything a keycap needs: { ind, code, title, tip, category, render, empty, natural }...
static FontsDB * inst()
< Number of font glyphs.
Definition fontsdb.cpp:367
const FontDBEntry * getStoreByVal(int ind) const
Glyph by its font-code value.
Definition fontsdb.cpp:401
One in-game font character: its code, output text, and classification flags.
Definition fontdbentry.h:43
bool getPicture() const
const QString getTip() const
bool getControl() const
const QString getName() const
bool getNormal() const
bool getSingleChar() const
bool getMultiChar() const
bool getVariable() const
const QString getAlias() const