From 19c15ba89b3ba9eec6f88f6684cb28d1362d0a04 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 16 Nov 2025 19:32:07 +0100 Subject: [PATCH] Try to fix layout switching --- internal_filesystem/lib/mpos/ui/keyboard.py | 24 +- tests/manual_test_abc_button.py | 65 ++++ ...est_graphical_keyboard_layout_switching.py | 288 ++++++++++++++++++ 3 files changed, 368 insertions(+), 9 deletions(-) create mode 100644 tests/manual_test_abc_button.py create mode 100644 tests/test_graphical_keyboard_layout_switching.py diff --git a/internal_filesystem/lib/mpos/ui/keyboard.py b/internal_filesystem/lib/mpos/ui/keyboard.py index 63540809..4c30ad3e 100644 --- a/internal_filesystem/lib/mpos/ui/keyboard.py +++ b/internal_filesystem/lib/mpos/ui/keyboard.py @@ -62,8 +62,13 @@ class MposKeyboard: # Configure layouts self._setup_layouts() - # Set default mode to lowercase + # Initialize ALL keyboard mode maps (prevents LVGL from using default maps) 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) + + # Set default mode to lowercase self._keyboard.set_mode(self.MODE_LOWERCASE) # Add event handler for custom behavior @@ -134,6 +139,11 @@ 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: @@ -149,26 +159,22 @@ class MposKeyboard: elif text == lv.SYMBOL.UP: # Switch to uppercase - self._keyboard.set_map(self.MODE_UPPERCASE, self._uppercase_map, self._uppercase_ctrl) - self._keyboard.set_mode(self.MODE_UPPERCASE) + self.set_mode(self.MODE_UPPERCASE) return # Don't modify text elif text == lv.SYMBOL.DOWN or text == self.LABEL_LETTERS: # Switch to lowercase - self._keyboard.set_map(self.MODE_LOWERCASE, self._lowercase_map, self._lowercase_ctrl) - self._keyboard.set_mode(self.MODE_LOWERCASE) + self.set_mode(self.MODE_LOWERCASE) return # Don't modify text elif text == self.LABEL_NUMBERS_SPECIALS: # Switch to numbers/specials - self._keyboard.set_map(self.MODE_NUMBERS, self._numbers_map, self._numbers_ctrl) - self._keyboard.set_mode(self.MODE_NUMBERS) + self.set_mode(self.MODE_NUMBERS) return # Don't modify text elif text == self.LABEL_SPECIALS: # Switch to additional specials - self._keyboard.set_map(self.MODE_SPECIALS, self._specials_map, self._specials_ctrl) - self._keyboard.set_mode(self.MODE_SPECIALS) + self.set_mode(self.MODE_SPECIALS) return # Don't modify text elif text == self.LABEL_SPACE: diff --git a/tests/manual_test_abc_button.py b/tests/manual_test_abc_button.py new file mode 100644 index 00000000..93068d0e --- /dev/null +++ b/tests/manual_test_abc_button.py @@ -0,0 +1,65 @@ +""" +Manual test for the "abc" button bug. + +This test creates a keyboard and lets you manually switch modes to observe the bug. + +Run with: ./scripts/run_desktop.sh tests/manual_test_abc_button.py + +Steps to reproduce the bug: +1. Keyboard starts in lowercase mode +2. Click "?123" button to switch to numbers mode +3. Click "abc" button to switch back to lowercase +4. OBSERVE: Does it show "?123" (correct) or "1#" (wrong/default LVGL)? +""" + +import lvgl as lv +from mpos.ui.keyboard import MposKeyboard + +# Get active screen +screen = lv.screen_active() +screen.clean() + +# Create title +title = lv.label(screen) +title.set_text("ABC Button Test") +title.align(lv.ALIGN.TOP_MID, 0, 5) + +# Create instructions +instructions = lv.label(screen) +instructions.set_text( + "1. Start in lowercase (has ?123 button)\n" + "2. Click '?123' to switch to numbers\n" + "3. Click 'abc' to switch back\n" + "4. CHECK: Do you see '?123' or '1#'?\n" + " - '?123' = CORRECT (custom keyboard)\n" + " - '1#' = BUG (default LVGL keyboard)" +) +instructions.set_style_text_align(lv.TEXT_ALIGN.LEFT, 0) +instructions.align(lv.ALIGN.TOP_LEFT, 10, 30) + +# Create textarea +textarea = lv.textarea(screen) +textarea.set_size(280, 30) +textarea.set_one_line(True) +textarea.align(lv.ALIGN.TOP_MID, 0, 120) +textarea.set_placeholder_text("Type here...") + +# Create keyboard +keyboard = MposKeyboard(screen) +keyboard.set_textarea(textarea) +keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + +print("\n" + "="*60) +print("ABC Button Bug Test") +print("="*60) +print("Instructions:") +print("1. Keyboard starts in LOWERCASE mode") +print(" - Look for '?123' button (bottom left area)") +print("2. Click '?123' to switch to NUMBERS mode") +print(" - Should show numbers 1,2,3, etc.") +print(" - Should have 'abc' button (bottom left)") +print("3. Click 'abc' to return to lowercase") +print("4. CRITICAL CHECK:") +print(" - If you see '?123' button → CORRECT (custom keyboard)") +print(" - If you see '1#' button → BUG (default LVGL keyboard)") +print("="*60 + "\n") diff --git a/tests/test_graphical_keyboard_layout_switching.py b/tests/test_graphical_keyboard_layout_switching.py new file mode 100644 index 00000000..5672e5cf --- /dev/null +++ b/tests/test_graphical_keyboard_layout_switching.py @@ -0,0 +1,288 @@ +""" +Test for keyboard layout switching bug. + +This test reproduces the issue where clicking the "abc" button in numbers mode +goes to the wrong (default LVGL) keyboard layout instead of our custom lowercase layout. + +Usage: + Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_layout_switching.py + Device: ./tests/unittest.sh tests/test_graphical_keyboard_layout_switching.py --ondevice +""" + +import unittest +import lvgl as lv +from mpos.ui.keyboard import MposKeyboard +from graphical_test_helper import wait_for_render + + +class TestKeyboardLayoutSwitching(unittest.TestCase): + """Test keyboard layout switching between different modes.""" + + def setUp(self): + """Set up test fixtures.""" + self.screen = lv.obj() + self.screen.set_size(320, 240) + + # Create textarea + self.textarea = lv.textarea(self.screen) + self.textarea.set_size(280, 40) + self.textarea.align(lv.ALIGN.TOP_MID, 0, 10) + self.textarea.set_one_line(True) + + # Load screen + 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_abc_button_from_numbers_mode(self): + """ + Test that clicking "abc" button in numbers mode goes to lowercase mode. + + BUG: Currently goes to the wrong (default LVGL) keyboard layout + instead of our custom lowercase layout. + + Expected behavior: + 1. Start in lowercase mode (has "q", "w", "e", etc.) + 2. Switch to numbers mode (has "1", "2", "3", etc. and "abc" button) + 3. Click "abc" button + 4. Should return to lowercase mode (has "q", "w", "e", etc.) + """ + print("\n=== Testing 'abc' button from numbers mode ===") + + # Create keyboard + keyboard = MposKeyboard(self.screen) + keyboard.set_textarea(self.textarea) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + # Verify we start in lowercase mode + print("Step 1: Verify initial lowercase mode") + # Find 'q' button (should be in lowercase layout) + q_button_index = None + for i in range(100): + try: + text = keyboard.get_button_text(i) + if text == "q": + q_button_index = i + print(f" Found 'q' at index {i} - GOOD (lowercase mode)") + break + except: + pass + + self.assertIsNotNone(q_button_index, "Should find 'q' in lowercase mode") + + # Switch to numbers mode + print("\nStep 2: Switch to numbers mode") + keyboard.set_mode(MposKeyboard.MODE_NUMBERS) + wait_for_render(5) + + # Verify we're in numbers mode by finding '1' button + one_button_index = None + for i in range(100): + try: + text = keyboard.get_button_text(i) + if text == "1": + one_button_index = i + print(f" Found '1' at index {i} - GOOD (numbers mode)") + break + except: + pass + + self.assertIsNotNone(one_button_index, "Should find '1' in numbers mode") + + # Find the 'abc' button in numbers mode + print("\nStep 3: Find 'abc' button in numbers mode") + abc_button_index = None + for i in range(100): + try: + text = keyboard.get_button_text(i) + if text == "abc": + abc_button_index = i + print(f" Found 'abc' at index {i}") + break + except: + pass + + self.assertIsNotNone(abc_button_index, "Should find 'abc' button in numbers mode") + + # Switch back to lowercase by calling set_mode (simulating clicking 'abc') + print("\nStep 4: Click 'abc' to switch back to lowercase") + keyboard.set_mode(MposKeyboard.MODE_LOWERCASE) + wait_for_render(5) + + # Verify we're back in lowercase mode using DISTINGUISHING LABELS + # When in LOWERCASE mode: + # - Our custom keyboard has "?123" (to switch to numbers) + # - Default LVGL keyboard has "1#" (to switch to numbers) and "ABC" (to switch to uppercase) + # + # Note: "abc" only appears in NUMBERS/SPECIALS modes to switch back to lowercase + print("\nStep 5: Verify we're in OUR custom lowercase mode (not default LVGL)") + + found_labels = {} + for i in range(100): + try: + text = keyboard.get_button_text(i) + # Check for all possible distinguishing labels + if text in ["abc", "ABC", "?123", "1#", lv.SYMBOL.UP, lv.SYMBOL.DOWN]: + found_labels[text] = i + print(f" Found label '{text}' at index {i}") + except: + pass + + # Check for WRONG labels (default LVGL keyboard in lowercase mode) + if "ABC" in found_labels: + print(f" ERROR: Found 'ABC' - this is the DEFAULT LVGL keyboard!") + self.fail("BUG DETECTED: Got default LVGL lowercase keyboard with 'ABC' label instead of custom keyboard") + + if "1#" in found_labels: + print(f" ERROR: Found '1#' - this is the DEFAULT LVGL keyboard!") + self.fail("BUG DETECTED: Got default LVGL lowercase keyboard with '1#' label instead of custom keyboard with '?123'") + + # Check for CORRECT labels (our custom lowercase keyboard) + if "?123" not in found_labels: + print(f" ERROR: Did not find '?123' - should be in custom lowercase layout!") + print(f" Found labels: {list(found_labels.keys())}") + self.fail("BUG: Should find '?123' label in custom lowercase mode, but got: " + str(list(found_labels.keys()))) + + # Also verify we have the UP symbol (our custom keyboard) not ABC (default) + if lv.SYMBOL.UP not in found_labels: + print(f" ERROR: Did not find UP symbol - should be in custom lowercase layout!") + print(f" Found labels: {list(found_labels.keys())}") + self.fail("BUG: Should find UP symbol in custom lowercase mode") + + print(f" Found '?123' at index {found_labels['?123']} - GOOD (custom keyboard)") + print(f" Found UP symbol at index {found_labels[lv.SYMBOL.UP]} - GOOD (custom keyboard)") + print("\nSUCCESS: 'abc' button correctly returns to custom lowercase layout!") + + def test_layout_switching_cycle(self): + """ + Test full cycle of layout switching: lowercase -> numbers -> specials -> lowercase. + + This ensures all mode switches preserve our custom layouts. + """ + print("\n=== Testing full layout switching cycle ===") + + keyboard = MposKeyboard(self.screen) + keyboard.set_textarea(self.textarea) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + # Define what we expect to find in each mode + mode_tests = [ + (MposKeyboard.MODE_LOWERCASE, "q", "lowercase"), + (MposKeyboard.MODE_NUMBERS, "1", "numbers"), + (MposKeyboard.MODE_SPECIALS, "~", "specials"), + (MposKeyboard.MODE_LOWERCASE, "q", "lowercase (again)"), + ] + + for mode, expected_key, mode_name in mode_tests: + print(f"\nSwitching to {mode_name}...") + keyboard.set_mode(mode) + wait_for_render(5) + + # Find the expected key + found = False + for i in range(100): + try: + text = keyboard.get_button_text(i) + if text == expected_key: + print(f" Found '{expected_key}' at index {i} - GOOD") + found = True + break + except: + pass + + self.assertTrue(found, + f"Should find '{expected_key}' in {mode_name} mode") + + print("\nSUCCESS: All layout switches preserve custom layouts!") + + def test_event_handler_switches_layout(self): + """ + Test that the event handler properly switches layouts. + + This simulates what happens when the user actually CLICKS the "abc" button, + going through the _handle_events method instead of calling set_mode() directly. + """ + print("\n=== Testing event handler layout switching ===") + + keyboard = MposKeyboard(self.screen) + keyboard.set_textarea(self.textarea) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + # Switch to numbers mode first + print("Step 1: Switch to numbers mode") + keyboard.set_mode(MposKeyboard.MODE_NUMBERS) + wait_for_render(5) + + # Verify we're in numbers mode + one_found = False + for i in range(100): + try: + if keyboard.get_button_text(i) == "1": + one_found = True + print(f" Found '1' - in numbers mode") + break + except: + pass + self.assertTrue(one_found, "Should be in numbers mode") + + # Now simulate what the event handler does when "abc" is clicked + # The event handler checks: elif text == lv.SYMBOL.DOWN or text == self.LABEL_LETTERS: + # Then it calls: self._keyboard.set_map() and self._keyboard.set_mode() + print("\nStep 2: Simulate clicking 'abc' (via event handler logic)") + + # This is what the event handler does: + keyboard._keyboard.set_map( + MposKeyboard.MODE_LOWERCASE, + keyboard._lowercase_map, + keyboard._lowercase_ctrl + ) + keyboard._keyboard.set_mode(MposKeyboard.MODE_LOWERCASE) + wait_for_render(5) + + # Verify we're back in lowercase mode with OUR custom layout + # When in LOWERCASE mode: + # - Our custom keyboard has "?123" (to switch to numbers) + # - Default LVGL keyboard has "1#" (to switch to numbers) and "ABC" (to switch to uppercase) + print("\nStep 3: Verify we have custom lowercase layout (not default LVGL)") + + found_labels = {} + for i in range(100): + try: + text = keyboard.get_button_text(i) + if text in ["abc", "ABC", "?123", "1#", lv.SYMBOL.UP]: + found_labels[text] = i + print(f" Found label '{text}' at index {i}") + except: + pass + + # Check for WRONG labels (default LVGL keyboard) + if "ABC" in found_labels: + print(f" ERROR: Found 'ABC' - this is the DEFAULT LVGL keyboard!") + print(" Found these labels:", list(found_labels.keys())) + self.fail("BUG DETECTED: Event handler caused switch to default LVGL keyboard with 'ABC' label") + + if "1#" in found_labels: + print(f" ERROR: Found '1#' - this is the DEFAULT LVGL keyboard!") + print(" Found these labels:", list(found_labels.keys())) + self.fail("BUG DETECTED: Event handler caused switch to default LVGL keyboard with '1#' label") + + # Check for CORRECT labels (our custom keyboard in lowercase mode) + self.assertIn("?123", found_labels, + "Should find '?123' label in custom lowercase mode (not '1#' from default)") + self.assertIn(lv.SYMBOL.UP, found_labels, + "Should find UP symbol in custom lowercase mode") + + print(f" Found '?123' at index {found_labels['?123']} - GOOD") + print(f" Found UP symbol at index {found_labels[lv.SYMBOL.UP]} - GOOD") + print("\nSUCCESS: Event handler preserves custom layout!") + + +if __name__ == "__main__": + unittest.main()