Skip to content

Commit c9f6952

Browse files
Fix MposKeyboard layout switch crashes
1 parent 19c15ba commit c9f6952

File tree

2 files changed

+232
-9
lines changed

2 files changed

+232
-9
lines changed

internal_filesystem/lib/mpos/ui/keyboard.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class MposKeyboard:
4040
LABEL_LETTERS = "abc"
4141
LABEL_SPACE = " "
4242

43-
# Keyboard modes (using LVGL's USER modes)
43+
# Keyboard modes - use USER modes for our API
44+
# We'll also register to standard modes to catch LVGL's internal switches
4445
MODE_LOWERCASE = lv.keyboard.MODE.USER_1
4546
MODE_UPPERCASE = lv.keyboard.MODE.USER_2
4647
MODE_NUMBERS = lv.keyboard.MODE.USER_3
@@ -62,17 +63,29 @@ def __init__(self, parent):
6263
# Configure layouts
6364
self._setup_layouts()
6465

65-
# Initialize ALL keyboard mode maps (prevents LVGL from using default maps)
66+
# Initialize ALL keyboard mode maps
67+
# Register to BOTH our USER modes AND standard LVGL modes
68+
# This prevents LVGL from using default maps when it internally switches modes
69+
70+
# Our USER modes (what we use in our API)
6671
self._keyboard.set_map(self.MODE_LOWERCASE, self._lowercase_map, self._lowercase_ctrl)
6772
self._keyboard.set_map(self.MODE_UPPERCASE, self._uppercase_map, self._uppercase_ctrl)
6873
self._keyboard.set_map(self.MODE_NUMBERS, self._numbers_map, self._numbers_ctrl)
6974
self._keyboard.set_map(self.MODE_SPECIALS, self._specials_map, self._specials_ctrl)
7075

76+
# ALSO register to standard LVGL modes (what LVGL uses internally)
77+
# This catches cases where LVGL internally calls set_mode(TEXT_LOWER)
78+
self._keyboard.set_map(lv.keyboard.MODE.TEXT_LOWER, self._lowercase_map, self._lowercase_ctrl)
79+
self._keyboard.set_map(lv.keyboard.MODE.TEXT_UPPER, self._uppercase_map, self._uppercase_ctrl)
80+
self._keyboard.set_map(lv.keyboard.MODE.NUMBER, self._numbers_map, self._numbers_ctrl)
81+
self._keyboard.set_map(lv.keyboard.MODE.SPECIAL, self._specials_map, self._specials_ctrl)
82+
7183
# Set default mode to lowercase
7284
self._keyboard.set_mode(self.MODE_LOWERCASE)
7385

7486
# Add event handler for custom behavior
75-
self._keyboard.add_event_cb(self._handle_events, lv.EVENT.VALUE_CHANGED, None)
87+
# We need to handle ALL events to catch mode changes that LVGL might trigger
88+
self._keyboard.add_event_cb(self._handle_events, lv.EVENT.ALL, None)
7689

7790
# Apply theme fix for light mode visibility
7891
mpos.ui.theme.fix_keyboard_button_style(self._keyboard)
@@ -126,11 +139,28 @@ def _handle_events(self, event):
126139
Args:
127140
event: LVGL event object
128141
"""
129-
# Only process VALUE_CHANGED events
130142
event_code = event.get_code()
143+
144+
# Intercept READY event to prevent LVGL from changing modes
145+
if event_code == lv.EVENT.READY:
146+
# Stop LVGL from processing READY (which might trigger mode changes)
147+
event.stop_processing()
148+
# Forward READY event to external handlers if needed
149+
return
150+
151+
# Intercept CANCEL event similarly
152+
if event_code == lv.EVENT.CANCEL:
153+
event.stop_processing()
154+
return
155+
156+
# Only process VALUE_CHANGED events for actual typing
131157
if event_code != lv.EVENT.VALUE_CHANGED:
132158
return
133159

160+
# Stop event propagation FIRST, before doing anything else
161+
# This prevents LVGL's default handler from interfering
162+
event.stop_processing()
163+
134164
# Get the pressed button and its text
135165
button = self._keyboard.get_selected_button()
136166
text = self._keyboard.get_button_text(button)
@@ -139,11 +169,6 @@ def _handle_events(self, event):
139169
if text is None:
140170
return
141171

142-
# Stop event propagation to prevent LVGL's default mode-switching behavior
143-
# This is critical to prevent LVGL from switching to its default TEXT_LOWER,
144-
# TEXT_UPPER, NUMBER modes when it sees mode-switching buttons
145-
event.stop_processing()
146-
147172
# Get current textarea content (from our own reference, not LVGL's)
148173
ta = self._textarea
149174
if not ta:
@@ -231,17 +256,26 @@ def set_mode(self, mode):
231256
232257
Args:
233258
mode: One of MODE_LOWERCASE, MODE_UPPERCASE, MODE_NUMBERS, MODE_SPECIALS
259+
(can also accept standard LVGL modes)
234260
"""
235261
# Map mode constants to their corresponding map arrays
262+
# Support both our USER modes and standard LVGL modes
236263
mode_maps = {
237264
self.MODE_LOWERCASE: (self._lowercase_map, self._lowercase_ctrl),
238265
self.MODE_UPPERCASE: (self._uppercase_map, self._uppercase_ctrl),
239266
self.MODE_NUMBERS: (self._numbers_map, self._numbers_ctrl),
240267
self.MODE_SPECIALS: (self._specials_map, self._specials_ctrl),
268+
# Also map standard LVGL modes
269+
lv.keyboard.MODE.TEXT_LOWER: (self._lowercase_map, self._lowercase_ctrl),
270+
lv.keyboard.MODE.TEXT_UPPER: (self._uppercase_map, self._uppercase_ctrl),
271+
lv.keyboard.MODE.NUMBER: (self._numbers_map, self._numbers_ctrl),
272+
lv.keyboard.MODE.SPECIAL: (self._specials_map, self._specials_ctrl),
241273
}
242274

243275
if mode in mode_maps:
244276
key_map, ctrl_map = mode_maps[mode]
277+
# CRITICAL: Always call set_map() BEFORE set_mode()
278+
# This prevents lv_keyboard_update_map() crashes
245279
self._keyboard.set_map(mode, key_map, ctrl_map)
246280

247281
self._keyboard.set_mode(mode)
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""
2+
Test comparing default LVGL keyboard with custom MposKeyboard.
3+
4+
This test helps identify the differences between the two keyboard types
5+
so we can properly detect when the bug occurs (switching to default instead of custom).
6+
7+
Usage:
8+
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_default_vs_custom.py
9+
"""
10+
11+
import unittest
12+
import lvgl as lv
13+
from mpos.ui.keyboard import MposKeyboard
14+
from graphical_test_helper import wait_for_render
15+
16+
17+
class TestDefaultVsCustomKeyboard(unittest.TestCase):
18+
"""Compare default LVGL keyboard with custom MposKeyboard."""
19+
20+
def setUp(self):
21+
"""Set up test fixtures."""
22+
self.screen = lv.obj()
23+
self.screen.set_size(320, 240)
24+
lv.screen_load(self.screen)
25+
wait_for_render(5)
26+
27+
def tearDown(self):
28+
"""Clean up."""
29+
lv.screen_load(lv.obj())
30+
wait_for_render(5)
31+
32+
def test_default_lvgl_keyboard_layout(self):
33+
"""
34+
Examine the default LVGL keyboard to understand its layout.
35+
36+
This helps us know what we're looking for when detecting the bug.
37+
"""
38+
print("\n=== Examining DEFAULT LVGL keyboard ===")
39+
40+
# Create textarea
41+
textarea = lv.textarea(self.screen)
42+
textarea.set_size(280, 40)
43+
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
44+
textarea.set_one_line(True)
45+
wait_for_render(5)
46+
47+
# Create DEFAULT LVGL keyboard
48+
keyboard = lv.keyboard(self.screen)
49+
keyboard.set_textarea(textarea)
50+
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
51+
wait_for_render(10)
52+
53+
print("\nDefault LVGL keyboard buttons (first 40):")
54+
found_special_labels = {}
55+
for i in range(40):
56+
try:
57+
text = keyboard.get_button_text(i)
58+
if text and text not in ["\n", ""]:
59+
print(f" Index {i}: '{text}'")
60+
# Track special labels
61+
if text in ["ABC", "abc", "1#", "?123", "#+=", lv.SYMBOL.UP, lv.SYMBOL.DOWN]:
62+
found_special_labels[text] = i
63+
except:
64+
pass
65+
66+
print("\n--- DEFAULT LVGL keyboard has these special labels ---")
67+
for label, idx in found_special_labels.items():
68+
print(f" '{label}' at index {idx}")
69+
70+
print("\n--- Characteristics of DEFAULT LVGL keyboard ---")
71+
if "ABC" in found_special_labels:
72+
print(" ✓ Has 'ABC' (uppercase label)")
73+
if "1#" in found_special_labels:
74+
print(" ✓ Has '1#' (numbers label)")
75+
if "#+" in found_special_labels or "#+=" in found_special_labels:
76+
print(" ✓ Has '#+=/-' type labels")
77+
78+
def test_custom_mpos_keyboard_layout(self):
79+
"""
80+
Examine our custom MposKeyboard to understand its layout.
81+
82+
This shows what the CORRECT layout should look like.
83+
"""
84+
print("\n=== Examining CUSTOM MposKeyboard ===")
85+
86+
# Create textarea
87+
textarea = lv.textarea(self.screen)
88+
textarea.set_size(280, 40)
89+
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
90+
textarea.set_one_line(True)
91+
wait_for_render(5)
92+
93+
# Create CUSTOM MposKeyboard
94+
keyboard = MposKeyboard(self.screen)
95+
keyboard.set_textarea(textarea)
96+
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
97+
wait_for_render(10)
98+
99+
print("\nCustom MposKeyboard buttons (first 40):")
100+
found_special_labels = {}
101+
for i in range(40):
102+
try:
103+
text = keyboard.get_button_text(i)
104+
if text and text not in ["\n", ""]:
105+
print(f" Index {i}: '{text}'")
106+
# Track special labels
107+
if text in ["ABC", "abc", "1#", "?123", "=\\<", lv.SYMBOL.UP, lv.SYMBOL.DOWN]:
108+
found_special_labels[text] = i
109+
except:
110+
pass
111+
112+
print("\n--- CUSTOM MposKeyboard has these special labels ---")
113+
for label, idx in found_special_labels.items():
114+
print(f" '{label}' at index {idx}")
115+
116+
print("\n--- Characteristics of CUSTOM MposKeyboard ---")
117+
if "?123" in found_special_labels:
118+
print(" ✓ Has '?123' (numbers label)")
119+
if "=\\<" in found_special_labels:
120+
print(" ✓ Has '=\\<' (specials label)")
121+
if lv.SYMBOL.UP in found_special_labels:
122+
print(" ✓ Has UP symbol (shift to uppercase)")
123+
124+
def test_mode_switching_bug_reproduction(self):
125+
"""
126+
Try to reproduce the bug: numbers -> abc -> wrong layout.
127+
"""
128+
print("\n=== Attempting to reproduce the bug ===")
129+
130+
textarea = lv.textarea(self.screen)
131+
textarea.set_size(280, 40)
132+
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
133+
textarea.set_one_line(True)
134+
wait_for_render(5)
135+
136+
keyboard = MposKeyboard(self.screen)
137+
keyboard.set_textarea(textarea)
138+
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
139+
wait_for_render(10)
140+
141+
# Step 1: Start in lowercase
142+
print("\nStep 1: Initial lowercase mode")
143+
labels_step1 = self._get_special_labels(keyboard)
144+
print(f" Labels: {list(labels_step1.keys())}")
145+
self.assertIn("?123", labels_step1, "Should start with custom lowercase (?123)")
146+
147+
# Step 2: Switch to numbers
148+
print("\nStep 2: Switch to numbers mode")
149+
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
150+
wait_for_render(5)
151+
labels_step2 = self._get_special_labels(keyboard)
152+
print(f" Labels: {list(labels_step2.keys())}")
153+
self.assertIn("abc", labels_step2, "Should have 'abc' in numbers mode")
154+
155+
# Step 3: Switch back to lowercase (this is where bug might happen)
156+
print("\nStep 3: Switch back to lowercase via set_mode()")
157+
keyboard.set_mode(MposKeyboard.MODE_LOWERCASE)
158+
wait_for_render(5)
159+
labels_step3 = self._get_special_labels(keyboard)
160+
print(f" Labels: {list(labels_step3.keys())}")
161+
162+
# Check for bug
163+
if "ABC" in labels_step3 or "1#" in labels_step3:
164+
print(" ❌ BUG DETECTED: Got default LVGL keyboard!")
165+
print(f" Found these labels: {list(labels_step3.keys())}")
166+
self.fail("BUG: Switched to default LVGL keyboard instead of custom")
167+
168+
if "?123" not in labels_step3:
169+
print(" ❌ BUG DETECTED: Missing '?123' label!")
170+
print(f" Found these labels: {list(labels_step3.keys())}")
171+
self.fail("BUG: Missing '?123' label from custom keyboard")
172+
173+
print(" ✓ Correct: Has custom layout with '?123'")
174+
175+
def _get_special_labels(self, keyboard):
176+
"""Helper to get special labels from keyboard."""
177+
labels = {}
178+
for i in range(100):
179+
try:
180+
text = keyboard.get_button_text(i)
181+
if text in ["ABC", "abc", "1#", "?123", "=\\<", "#+=", lv.SYMBOL.UP, lv.SYMBOL.DOWN]:
182+
labels[text] = i
183+
except:
184+
pass
185+
return labels
186+
187+
188+
if __name__ == "__main__":
189+
unittest.main()

0 commit comments

Comments
 (0)