You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Fix MposKeyboard layout switch crashes
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user