Fix MposKeyboard layout switch crashes

This commit is contained in:
Thomas Farstrike
2025-11-16 20:30:06 +01:00
parent 19c15ba89b
commit c9f6952971
2 changed files with 232 additions and 9 deletions
+43 -9
View File
@@ -40,7 +40,8 @@ class MposKeyboard:
LABEL_LETTERS = "abc"
LABEL_SPACE = " "
# Keyboard modes (using LVGL's USER modes)
# Keyboard modes - use USER modes for our API
# We'll also register to standard modes to catch LVGL's internal switches
MODE_LOWERCASE = lv.keyboard.MODE.USER_1
MODE_UPPERCASE = lv.keyboard.MODE.USER_2
MODE_NUMBERS = lv.keyboard.MODE.USER_3
@@ -62,17 +63,29 @@ class MposKeyboard:
# Configure layouts
self._setup_layouts()
# Initialize ALL keyboard mode maps (prevents LVGL from using default maps)
# Initialize ALL keyboard mode maps
# Register to BOTH our USER modes AND standard LVGL modes
# This prevents LVGL from using default maps when it internally switches modes
# Our USER modes (what we use in our API)
self._keyboard.set_map(self.MODE_LOWERCASE, self._lowercase_map, self._lowercase_ctrl)
self._keyboard.set_map(self.MODE_UPPERCASE, self._uppercase_map, self._uppercase_ctrl)
self._keyboard.set_map(self.MODE_NUMBERS, self._numbers_map, self._numbers_ctrl)
self._keyboard.set_map(self.MODE_SPECIALS, self._specials_map, self._specials_ctrl)
# ALSO register to standard LVGL modes (what LVGL uses internally)
# This catches cases where LVGL internally calls set_mode(TEXT_LOWER)
self._keyboard.set_map(lv.keyboard.MODE.TEXT_LOWER, self._lowercase_map, self._lowercase_ctrl)
self._keyboard.set_map(lv.keyboard.MODE.TEXT_UPPER, self._uppercase_map, self._uppercase_ctrl)
self._keyboard.set_map(lv.keyboard.MODE.NUMBER, self._numbers_map, self._numbers_ctrl)
self._keyboard.set_map(lv.keyboard.MODE.SPECIAL, self._specials_map, self._specials_ctrl)
# Set default mode to lowercase
self._keyboard.set_mode(self.MODE_LOWERCASE)
# Add event handler for custom behavior
self._keyboard.add_event_cb(self._handle_events, lv.EVENT.VALUE_CHANGED, None)
# We need to handle ALL events to catch mode changes that LVGL might trigger
self._keyboard.add_event_cb(self._handle_events, lv.EVENT.ALL, None)
# Apply theme fix for light mode visibility
mpos.ui.theme.fix_keyboard_button_style(self._keyboard)
@@ -126,11 +139,28 @@ class MposKeyboard:
Args:
event: LVGL event object
"""
# Only process VALUE_CHANGED events
event_code = event.get_code()
# Intercept READY event to prevent LVGL from changing modes
if event_code == lv.EVENT.READY:
# Stop LVGL from processing READY (which might trigger mode changes)
event.stop_processing()
# Forward READY event to external handlers if needed
return
# Intercept CANCEL event similarly
if event_code == lv.EVENT.CANCEL:
event.stop_processing()
return
# Only process VALUE_CHANGED events for actual typing
if event_code != lv.EVENT.VALUE_CHANGED:
return
# Stop event propagation FIRST, before doing anything else
# This prevents LVGL's default handler from interfering
event.stop_processing()
# Get the pressed button and its text
button = self._keyboard.get_selected_button()
text = self._keyboard.get_button_text(button)
@@ -139,11 +169,6 @@ class MposKeyboard:
if text is None:
return
# Stop event propagation to prevent LVGL's default mode-switching behavior
# This is critical to prevent LVGL from switching to its default TEXT_LOWER,
# TEXT_UPPER, NUMBER modes when it sees mode-switching buttons
event.stop_processing()
# Get current textarea content (from our own reference, not LVGL's)
ta = self._textarea
if not ta:
@@ -231,17 +256,26 @@ class MposKeyboard:
Args:
mode: One of MODE_LOWERCASE, MODE_UPPERCASE, MODE_NUMBERS, MODE_SPECIALS
(can also accept standard LVGL modes)
"""
# Map mode constants to their corresponding map arrays
# Support both our USER modes and standard LVGL modes
mode_maps = {
self.MODE_LOWERCASE: (self._lowercase_map, self._lowercase_ctrl),
self.MODE_UPPERCASE: (self._uppercase_map, self._uppercase_ctrl),
self.MODE_NUMBERS: (self._numbers_map, self._numbers_ctrl),
self.MODE_SPECIALS: (self._specials_map, self._specials_ctrl),
# Also map standard LVGL modes
lv.keyboard.MODE.TEXT_LOWER: (self._lowercase_map, self._lowercase_ctrl),
lv.keyboard.MODE.TEXT_UPPER: (self._uppercase_map, self._uppercase_ctrl),
lv.keyboard.MODE.NUMBER: (self._numbers_map, self._numbers_ctrl),
lv.keyboard.MODE.SPECIAL: (self._specials_map, self._specials_ctrl),
}
if mode in mode_maps:
key_map, ctrl_map = mode_maps[mode]
# CRITICAL: Always call set_map() BEFORE set_mode()
# This prevents lv_keyboard_update_map() crashes
self._keyboard.set_map(mode, key_map, ctrl_map)
self._keyboard.set_mode(mode)
@@ -0,0 +1,189 @@
"""
Test comparing default LVGL keyboard with custom MposKeyboard.
This test helps identify the differences between the two keyboard types
so we can properly detect when the bug occurs (switching to default instead of custom).
Usage:
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_default_vs_custom.py
"""
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
class TestDefaultVsCustomKeyboard(unittest.TestCase):
"""Compare default LVGL keyboard with custom MposKeyboard."""
def setUp(self):
"""Set up test fixtures."""
self.screen = lv.obj()
self.screen.set_size(320, 240)
lv.screen_load(self.screen)
wait_for_render(5)
def tearDown(self):
"""Clean up."""
lv.screen_load(lv.obj())
wait_for_render(5)
def test_default_lvgl_keyboard_layout(self):
"""
Examine the default LVGL keyboard to understand its layout.
This helps us know what we're looking for when detecting the bug.
"""
print("\n=== Examining DEFAULT LVGL keyboard ===")
# Create textarea
textarea = lv.textarea(self.screen)
textarea.set_size(280, 40)
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
textarea.set_one_line(True)
wait_for_render(5)
# Create DEFAULT LVGL keyboard
keyboard = lv.keyboard(self.screen)
keyboard.set_textarea(textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
print("\nDefault LVGL keyboard buttons (first 40):")
found_special_labels = {}
for i in range(40):
try:
text = keyboard.get_button_text(i)
if text and text not in ["\n", ""]:
print(f" Index {i}: '{text}'")
# Track special labels
if text in ["ABC", "abc", "1#", "?123", "#+=", lv.SYMBOL.UP, lv.SYMBOL.DOWN]:
found_special_labels[text] = i
except:
pass
print("\n--- DEFAULT LVGL keyboard has these special labels ---")
for label, idx in found_special_labels.items():
print(f" '{label}' at index {idx}")
print("\n--- Characteristics of DEFAULT LVGL keyboard ---")
if "ABC" in found_special_labels:
print(" ✓ Has 'ABC' (uppercase label)")
if "1#" in found_special_labels:
print(" ✓ Has '1#' (numbers label)")
if "#+" in found_special_labels or "#+=" in found_special_labels:
print(" ✓ Has '#+=/-' type labels")
def test_custom_mpos_keyboard_layout(self):
"""
Examine our custom MposKeyboard to understand its layout.
This shows what the CORRECT layout should look like.
"""
print("\n=== Examining CUSTOM MposKeyboard ===")
# Create textarea
textarea = lv.textarea(self.screen)
textarea.set_size(280, 40)
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
textarea.set_one_line(True)
wait_for_render(5)
# Create CUSTOM MposKeyboard
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
print("\nCustom MposKeyboard buttons (first 40):")
found_special_labels = {}
for i in range(40):
try:
text = keyboard.get_button_text(i)
if text and text not in ["\n", ""]:
print(f" Index {i}: '{text}'")
# Track special labels
if text in ["ABC", "abc", "1#", "?123", "=\\<", lv.SYMBOL.UP, lv.SYMBOL.DOWN]:
found_special_labels[text] = i
except:
pass
print("\n--- CUSTOM MposKeyboard has these special labels ---")
for label, idx in found_special_labels.items():
print(f" '{label}' at index {idx}")
print("\n--- Characteristics of CUSTOM MposKeyboard ---")
if "?123" in found_special_labels:
print(" ✓ Has '?123' (numbers label)")
if "=\\<" in found_special_labels:
print(" ✓ Has '=\\<' (specials label)")
if lv.SYMBOL.UP in found_special_labels:
print(" ✓ Has UP symbol (shift to uppercase)")
def test_mode_switching_bug_reproduction(self):
"""
Try to reproduce the bug: numbers -> abc -> wrong layout.
"""
print("\n=== Attempting to reproduce the bug ===")
textarea = lv.textarea(self.screen)
textarea.set_size(280, 40)
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
textarea.set_one_line(True)
wait_for_render(5)
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
# Step 1: Start in lowercase
print("\nStep 1: Initial lowercase mode")
labels_step1 = self._get_special_labels(keyboard)
print(f" Labels: {list(labels_step1.keys())}")
self.assertIn("?123", labels_step1, "Should start with custom lowercase (?123)")
# Step 2: Switch to numbers
print("\nStep 2: Switch to numbers mode")
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
wait_for_render(5)
labels_step2 = self._get_special_labels(keyboard)
print(f" Labels: {list(labels_step2.keys())}")
self.assertIn("abc", labels_step2, "Should have 'abc' in numbers mode")
# Step 3: Switch back to lowercase (this is where bug might happen)
print("\nStep 3: Switch back to lowercase via set_mode()")
keyboard.set_mode(MposKeyboard.MODE_LOWERCASE)
wait_for_render(5)
labels_step3 = self._get_special_labels(keyboard)
print(f" Labels: {list(labels_step3.keys())}")
# Check for bug
if "ABC" in labels_step3 or "1#" in labels_step3:
print(" ❌ BUG DETECTED: Got default LVGL keyboard!")
print(f" Found these labels: {list(labels_step3.keys())}")
self.fail("BUG: Switched to default LVGL keyboard instead of custom")
if "?123" not in labels_step3:
print(" ❌ BUG DETECTED: Missing '?123' label!")
print(f" Found these labels: {list(labels_step3.keys())}")
self.fail("BUG: Missing '?123' label from custom keyboard")
print(" ✓ Correct: Has custom layout with '?123'")
def _get_special_labels(self, keyboard):
"""Helper to get special labels from keyboard."""
labels = {}
for i in range(100):
try:
text = keyboard.get_button_text(i)
if text in ["ABC", "abc", "1#", "?123", "=\\<", "#+=", lv.SYMBOL.UP, lv.SYMBOL.DOWN]:
labels[text] = i
except:
pass
return labels
if __name__ == "__main__":
unittest.main()