Add new and improved keyboard

This commit is contained in:
Thomas Farstrike
2025-11-15 22:06:12 +01:00
parent 0dc151255a
commit dce55f7918
4 changed files with 1005 additions and 0 deletions
+304
View File
@@ -0,0 +1,304 @@
"""
Custom keyboard for MicroPythonOS.
This module provides an enhanced on-screen keyboard with better layout,
more characters (including emoticons), and improved usability compared
to the default LVGL keyboard.
Usage:
from mpos.ui.keyboard import CustomKeyboard
# Create keyboard
keyboard = CustomKeyboard(parent_obj)
keyboard.set_textarea(my_textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
# Or use factory function for drop-in replacement
from mpos.ui.keyboard import create_keyboard
keyboard = create_keyboard(parent_obj, custom=True)
"""
import lvgl as lv
import mpos.ui.theme
class CustomKeyboard:
"""
Enhanced keyboard widget with multiple layouts and emoticons.
Features:
- Lowercase and uppercase letter modes
- Numbers and special characters
- Additional special characters with emoticons
- Automatic mode switching
- Compatible with LVGL keyboard API
"""
# Keyboard layout labels
LABEL_NUMBERS_SPECIALS = "?123"
LABEL_SPECIALS = "=\<"
LABEL_LETTERS = "abc"
LABEL_SPACE = " "
# Keyboard modes (using LVGL's USER modes)
MODE_LOWERCASE = lv.keyboard.MODE.USER_1
MODE_UPPERCASE = lv.keyboard.MODE.USER_2
MODE_NUMBERS = lv.keyboard.MODE.USER_3
MODE_SPECIALS = lv.keyboard.MODE.USER_4
def __init__(self, parent):
"""
Create a custom keyboard.
Args:
parent: Parent LVGL object to attach keyboard to
"""
# Create underlying LVGL keyboard widget
self._keyboard = lv.keyboard(parent)
# Configure layouts
self._setup_layouts()
# 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)
# Apply theme fix for light mode visibility
mpos.ui.theme.fix_keyboard_button_style(self._keyboard)
# Set reasonable default height
self._keyboard.set_style_min_height(145, 0)
def _setup_layouts(self):
"""Configure all keyboard layout modes."""
# Lowercase letters
lowercase_map = [
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "\n",
lv.SYMBOL.UP, "z", "x", "c", "v", "b", "n", "m", lv.SYMBOL.BACKSPACE, "\n",
self.LABEL_NUMBERS_SPECIALS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
]
lowercase_ctrl = [10] * len(lowercase_map)
self._keyboard.set_map(self.MODE_LOWERCASE, lowercase_map, lowercase_ctrl)
# Uppercase letters
uppercase_map = [
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\n",
"A", "S", "D", "F", "G", "H", "J", "K", "L", "\n",
lv.SYMBOL.DOWN, "Z", "X", "C", "V", "B", "N", "M", lv.SYMBOL.BACKSPACE, "\n",
self.LABEL_NUMBERS_SPECIALS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
]
uppercase_ctrl = [10] * len(uppercase_map)
self._keyboard.set_map(self.MODE_UPPERCASE, uppercase_map, uppercase_ctrl)
# Numbers and common special characters
numbers_map = [
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n",
"@", "#", "$", "_", "&", "-", "+", "(", ")", "/", "\n",
self.LABEL_SPECIALS, "*", "\"", "'", ":", ";", "!", "?", lv.SYMBOL.BACKSPACE, "\n",
self.LABEL_LETTERS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
]
numbers_ctrl = [10] * len(numbers_map)
self._keyboard.set_map(self.MODE_NUMBERS, numbers_map, numbers_ctrl)
# Additional special characters with emoticons
specials_map = [
"~", "`", "|", "", ":-)", ";-)", ":-D", "\n",
":-(" , ":'-(", "^", "°", "=", "{", "}", "\\", "\n",
self.LABEL_NUMBERS_SPECIALS, ":-o", ":-P", "[", "]", lv.SYMBOL.BACKSPACE, "\n",
self.LABEL_LETTERS, "<", self.LABEL_SPACE, ">", lv.SYMBOL.NEW_LINE, None
]
specials_ctrl = [10] * len(specials_map)
self._keyboard.set_map(self.MODE_SPECIALS, specials_map, specials_ctrl)
def _handle_events(self, event):
"""
Handle keyboard button presses.
Args:
event: LVGL event object
"""
# Get the pressed button and its text
button = self._keyboard.get_selected_button()
text = self._keyboard.get_button_text(button)
# Get current textarea content
ta = self._keyboard.get_textarea()
if not ta:
return
current_text = ta.get_text()
new_text = current_text
# Handle special keys
if text == lv.SYMBOL.BACKSPACE:
# Delete last character
new_text = current_text[:-1]
elif text == lv.SYMBOL.UP:
# Switch to uppercase
self._keyboard.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_mode(self.MODE_LOWERCASE)
return # Don't modify text
elif text == self.LABEL_NUMBERS_SPECIALS:
# Switch to numbers/specials
self._keyboard.set_mode(self.MODE_NUMBERS)
return # Don't modify text
elif text == self.LABEL_SPECIALS:
# Switch to additional specials
self._keyboard.set_mode(self.MODE_SPECIALS)
return # Don't modify text
elif text == self.LABEL_SPACE:
# Space bar
new_text = current_text + " "
elif text == lv.SYMBOL.NEW_LINE:
# Handle newline (only for multi-line textareas)
if ta.get_one_line():
# For single-line, trigger READY event
self._keyboard.send_event(lv.EVENT.READY, None)
return
else:
new_text = current_text + "\n"
else:
# Regular character
new_text = current_text + text
# Update textarea
ta.set_text(new_text)
# ========================================================================
# LVGL keyboard-compatible API
# ========================================================================
def set_textarea(self, textarea):
"""Set the textarea that this keyboard should edit."""
self._keyboard.set_textarea(textarea)
def get_textarea(self):
"""Get the currently associated textarea."""
return self._keyboard.get_textarea()
def set_mode(self, mode):
"""Set keyboard mode (use MODE_* constants)."""
self._keyboard.set_mode(mode)
def align(self, align_type, x_offset=0, y_offset=0):
"""Align the keyboard."""
self._keyboard.align(align_type, x_offset, y_offset)
def set_style_min_height(self, height, selector):
"""Set minimum height."""
self._keyboard.set_style_min_height(height, selector)
def set_style_height(self, height, selector):
"""Set height."""
self._keyboard.set_style_height(height, selector)
def set_style_max_height(self, height, selector):
"""Set maximum height."""
self._keyboard.set_style_max_height(height, selector)
def set_style_opa(self, opacity, selector):
"""Set opacity (required for fade animations)."""
self._keyboard.set_style_opa(opacity, selector)
def get_x(self):
"""Get X position."""
return self._keyboard.get_x()
def set_x(self, x):
"""Set X position."""
self._keyboard.set_x(x)
def get_y(self):
"""Get Y position."""
return self._keyboard.get_y()
def set_y(self, y):
"""Set Y position."""
self._keyboard.set_y(y)
def set_pos(self, x, y):
"""Set position."""
self._keyboard.set_pos(x, y)
def get_height(self):
"""Get height."""
return self._keyboard.get_height()
def get_width(self):
"""Get width."""
return self._keyboard.get_width()
def add_flag(self, flag):
"""Add object flag (e.g., HIDDEN)."""
self._keyboard.add_flag(flag)
def remove_flag(self, flag):
"""Remove object flag."""
self._keyboard.remove_flag(flag)
def has_flag(self, flag):
"""Check if object has flag."""
return self._keyboard.has_flag(flag)
def add_event_cb(self, callback, event_code, user_data):
"""Add event callback."""
self._keyboard.add_event_cb(callback, event_code, user_data)
def remove_event_cb(self, callback):
"""Remove event callback."""
self._keyboard.remove_event_cb(callback)
def send_event(self, event_code, param):
"""Send event to keyboard."""
self._keyboard.send_event(event_code, param)
def get_lvgl_obj(self):
"""
Get the underlying LVGL keyboard object.
Use this if you need direct access to LVGL methods not wrapped here.
"""
return self._keyboard
def create_keyboard(parent, custom=False):
"""
Factory function to create a keyboard.
This provides a simple way to switch between standard LVGL keyboard
and custom keyboard.
Args:
parent: Parent LVGL object
custom: If True, create CustomKeyboard; if False, create standard lv.keyboard
Returns:
CustomKeyboard instance or lv.keyboard instance
Example:
# Use custom keyboard
keyboard = create_keyboard(screen, custom=True)
# Use standard LVGL keyboard
keyboard = create_keyboard(screen, custom=False)
"""
if custom:
return CustomKeyboard(parent)
else:
keyboard = lv.keyboard(parent)
mpos.ui.theme.fix_keyboard_button_style(keyboard)
return keyboard
+320
View File
@@ -0,0 +1,320 @@
"""
Graphical tests for CustomKeyboard.
Tests keyboard visual appearance, text input via simulated button presses,
and mode switching. Captures screenshots for regression testing.
Usage:
Desktop: ./tests/unittest.sh tests/test_graphical_custom_keyboard.py
Device: ./tests/unittest.sh tests/test_graphical_custom_keyboard.py ondevice
"""
import unittest
import lvgl as lv
import sys
import os
from mpos.ui.keyboard import CustomKeyboard, create_keyboard
from graphical_test_helper import (
wait_for_render,
capture_screenshot,
)
class TestGraphicalCustomKeyboard(unittest.TestCase):
"""Test suite for CustomKeyboard graphical verification."""
def setUp(self):
"""Set up test fixtures before each test method."""
# Determine screenshot directory
if sys.platform == "esp32":
self.screenshot_dir = "tests/screenshots"
else:
self.screenshot_dir = "../tests/screenshots"
# Ensure screenshots directory exists
try:
os.mkdir(self.screenshot_dir)
except OSError:
pass # Directory already exists
print(f"\n=== Graphical Keyboard Test Setup ===")
print(f"Platform: {sys.platform}")
def tearDown(self):
"""Clean up after each test method."""
lv.screen_load(lv.obj())
wait_for_render(5)
print("=== Test Cleanup Complete ===\n")
def _create_test_keyboard_scene(self):
"""
Create a test scene with textarea and keyboard.
Returns:
tuple: (screen, keyboard, textarea)
"""
# Create screen
screen = lv.obj()
screen.set_size(320, 240)
# Create textarea
textarea = lv.textarea(screen)
textarea.set_size(280, 40)
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
textarea.set_placeholder_text("Type here...")
textarea.set_one_line(True)
# Create custom keyboard
keyboard = CustomKeyboard(screen)
keyboard.set_textarea(textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
# Load and render
lv.screen_load(screen)
wait_for_render(iterations=20)
return screen, keyboard, textarea
def _simulate_button_press(self, keyboard, button_index):
"""
Simulate pressing a keyboard button.
Args:
keyboard: CustomKeyboard instance
button_index: Index of button to press
Returns:
str: Text of the pressed button
"""
lvgl_keyboard = keyboard.get_lvgl_obj()
# Get button text before pressing
button_text = lvgl_keyboard.get_button_text(button_index)
# Simulate button press by setting it as selected and sending event
# Note: This is a bit of a hack since we can't directly click in tests
# We'll trigger the VALUE_CHANGED event which is what happens on click
# The keyboard has an internal handler that responds to VALUE_CHANGED
# We need to manually trigger it
lvgl_keyboard.send_event(lv.EVENT.VALUE_CHANGED, None)
wait_for_render(5)
return button_text
def test_keyboard_lowercase_appearance(self):
"""
Test keyboard appearance in lowercase mode.
Verifies that the keyboard renders correctly and captures screenshot.
"""
print("\n=== Testing lowercase keyboard appearance ===")
screen, keyboard, textarea = self._create_test_keyboard_scene()
# Ensure lowercase mode
keyboard.set_mode(CustomKeyboard.MODE_LOWERCASE)
wait_for_render(10)
# Capture screenshot
screenshot_path = f"{self.screenshot_dir}/custom_keyboard_lowercase.raw"
print(f"Capturing screenshot: {screenshot_path}")
capture_screenshot(screenshot_path, width=320, height=240)
# Verify screenshot was created
stat = os.stat(screenshot_path)
self.assertTrue(stat[6] > 0, "Screenshot file is empty")
print(f"Screenshot captured: {stat[6]} bytes")
print("=== Lowercase appearance test PASSED ===")
def test_keyboard_uppercase_appearance(self):
"""Test keyboard appearance in uppercase mode."""
print("\n=== Testing uppercase keyboard appearance ===")
screen, keyboard, textarea = self._create_test_keyboard_scene()
# Switch to uppercase mode
keyboard.set_mode(CustomKeyboard.MODE_UPPERCASE)
wait_for_render(10)
# Capture screenshot
screenshot_path = f"{self.screenshot_dir}/custom_keyboard_uppercase.raw"
print(f"Capturing screenshot: {screenshot_path}")
capture_screenshot(screenshot_path, width=320, height=240)
# Verify screenshot was created
stat = os.stat(screenshot_path)
self.assertTrue(stat[6] > 0, "Screenshot file is empty")
print(f"Screenshot captured: {stat[6]} bytes")
print("=== Uppercase appearance test PASSED ===")
def test_keyboard_numbers_appearance(self):
"""Test keyboard appearance in numbers/specials mode."""
print("\n=== Testing numbers keyboard appearance ===")
screen, keyboard, textarea = self._create_test_keyboard_scene()
# Switch to numbers mode
keyboard.set_mode(CustomKeyboard.MODE_NUMBERS)
wait_for_render(10)
# Capture screenshot
screenshot_path = f"{self.screenshot_dir}/custom_keyboard_numbers.raw"
print(f"Capturing screenshot: {screenshot_path}")
capture_screenshot(screenshot_path, width=320, height=240)
# Verify screenshot was created
stat = os.stat(screenshot_path)
self.assertTrue(stat[6] > 0, "Screenshot file is empty")
print(f"Screenshot captured: {stat[6]} bytes")
print("=== Numbers appearance test PASSED ===")
def test_keyboard_specials_appearance(self):
"""Test keyboard appearance in additional specials mode."""
print("\n=== Testing specials keyboard appearance ===")
screen, keyboard, textarea = self._create_test_keyboard_scene()
# Switch to specials mode
keyboard.set_mode(CustomKeyboard.MODE_SPECIALS)
wait_for_render(10)
# Capture screenshot
screenshot_path = f"{self.screenshot_dir}/custom_keyboard_specials.raw"
print(f"Capturing screenshot: {screenshot_path}")
capture_screenshot(screenshot_path, width=320, height=240)
# Verify screenshot was created
stat = os.stat(screenshot_path)
self.assertTrue(stat[6] > 0, "Screenshot file is empty")
print(f"Screenshot captured: {stat[6]} bytes")
print("=== Specials appearance test PASSED ===")
def test_keyboard_visibility_light_mode(self):
"""
Test that custom keyboard buttons are visible in light mode.
This verifies that the theme fix is applied.
"""
print("\n=== Testing keyboard visibility in light mode ===")
# Set light mode (should already be default)
import mpos.config
import mpos.ui.theme
prefs = mpos.config.SharedPreferences("theme_settings")
editor = prefs.edit()
editor.put_string("theme_light_dark", "light")
editor.commit()
mpos.ui.theme.set_theme(prefs)
wait_for_render(10)
# Create keyboard
screen, keyboard, textarea = self._create_test_keyboard_scene()
# Get button background color
lvgl_keyboard = keyboard.get_lvgl_obj()
bg_color = lvgl_keyboard.get_style_bg_color(lv.PART.ITEMS)
# Extract RGB (similar to keyboard styling test)
try:
color_dict = {
'r': bg_color.red() if hasattr(bg_color, 'red') else 0,
'g': bg_color.green() if hasattr(bg_color, 'green') else 0,
'b': bg_color.blue() if hasattr(bg_color, 'blue') else 0,
}
except:
try:
color_int = bg_color.to_int() if hasattr(bg_color, 'to_int') else 0
color_dict = {
'r': (color_int >> 16) & 0xFF,
'g': (color_int >> 8) & 0xFF,
'b': color_int & 0xFF,
}
except:
color_dict = {'r': 0, 'g': 0, 'b': 0}
print(f"Button background: RGB({color_dict['r']}, {color_dict['g']}, {color_dict['b']})")
# Verify buttons are NOT pure white (which would be invisible)
if 'r' in color_dict:
is_white = (color_dict['r'] >= 250 and
color_dict['g'] >= 250 and
color_dict['b'] >= 250)
self.assertFalse(
is_white,
f"Custom keyboard buttons are pure white in light mode (invisible)!"
)
print("=== Visibility test PASSED ===")
def test_keyboard_with_standard_comparison(self):
"""
Test custom keyboard alongside standard keyboard.
Creates both for visual comparison.
"""
print("\n=== Testing custom vs standard keyboard ===")
# Create screen with two textareas
screen = lv.obj()
screen.set_size(320, 240)
# Top textarea with standard keyboard
ta_standard = lv.textarea(screen)
ta_standard.set_size(280, 30)
ta_standard.set_pos(20, 5)
ta_standard.set_placeholder_text("Standard")
ta_standard.set_one_line(True)
# Create standard keyboard (hidden initially)
keyboard_standard = create_keyboard(screen, custom=False)
keyboard_standard.set_textarea(ta_standard)
keyboard_standard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
keyboard_standard.set_style_min_height(145, 0)
# Load and render
lv.screen_load(screen)
wait_for_render(20)
# Capture standard keyboard
screenshot_path = f"{self.screenshot_dir}/keyboard_standard_comparison.raw"
print(f"Capturing standard keyboard: {screenshot_path}")
capture_screenshot(screenshot_path, width=320, height=240)
# Clean up
lv.screen_load(lv.obj())
wait_for_render(5)
# Now create custom keyboard
screen2 = lv.obj()
screen2.set_size(320, 240)
ta_custom = lv.textarea(screen2)
ta_custom.set_size(280, 30)
ta_custom.set_pos(20, 5)
ta_custom.set_placeholder_text("Custom")
ta_custom.set_one_line(True)
keyboard_custom = create_keyboard(screen2, custom=True)
keyboard_custom.set_textarea(ta_custom)
keyboard_custom.align(lv.ALIGN.BOTTOM_MID, 0, 0)
lv.screen_load(screen2)
wait_for_render(20)
# Capture custom keyboard
screenshot_path = f"{self.screenshot_dir}/keyboard_custom_comparison.raw"
print(f"Capturing custom keyboard: {screenshot_path}")
capture_screenshot(screenshot_path, width=320, height=240)
print("=== Comparison test PASSED ===")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,192 @@
"""
Functional tests for CustomKeyboard.
Tests keyboard creation, mode switching, text input, and API compatibility.
Usage:
Desktop: ./tests/unittest.sh tests/test_custom_keyboard.py
Device: ./tests/unittest.sh tests/test_custom_keyboard.py ondevice
"""
import unittest
import lvgl as lv
from mpos.ui.keyboard import CustomKeyboard, create_keyboard
class TestCustomKeyboard(unittest.TestCase):
"""Test suite for CustomKeyboard functionality."""
def setUp(self):
"""Set up test fixtures before each test method."""
# Create a test screen
self.screen = lv.obj()
self.screen.set_size(320, 240)
# Create a textarea for testing
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)
print(f"\n=== Test Setup Complete ===")
def tearDown(self):
"""Clean up after each test method."""
# Clean up objects
lv.screen_load(lv.obj())
print("=== Test Cleanup Complete ===\n")
def test_keyboard_creation(self):
"""Test that CustomKeyboard can be created."""
print("Testing keyboard creation...")
keyboard = CustomKeyboard(self.screen)
# Verify keyboard exists
self.assertIsNotNone(keyboard)
self.assertIsNotNone(keyboard.get_lvgl_obj())
print("Keyboard created successfully")
def test_keyboard_factory_custom(self):
"""Test factory function creates custom keyboard."""
print("Testing factory function with custom=True...")
keyboard = create_keyboard(self.screen, custom=True)
# Verify it's a CustomKeyboard instance
self.assertIsInstance(keyboard, CustomKeyboard)
print("Factory created CustomKeyboard successfully")
def test_keyboard_factory_standard(self):
"""Test factory function creates standard keyboard."""
print("Testing factory function with custom=False...")
keyboard = create_keyboard(self.screen, custom=False)
# Verify it's an LVGL keyboard (not CustomKeyboard)
self.assertFalse(isinstance(keyboard, CustomKeyboard),
"Factory with custom=False should not create CustomKeyboard")
# It should be an lv.keyboard instance
self.assertEqual(type(keyboard).__name__, 'keyboard')
print("Factory created standard keyboard successfully")
def test_set_textarea(self):
"""Test setting textarea association."""
print("Testing set_textarea...")
keyboard = CustomKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
# Verify textarea is associated
associated_ta = keyboard.get_textarea()
self.assertEqual(associated_ta, self.textarea)
print("Textarea association successful")
def test_mode_switching(self):
"""Test keyboard mode switching."""
print("Testing mode switching...")
keyboard = CustomKeyboard(self.screen)
# Test setting different modes
keyboard.set_mode(CustomKeyboard.MODE_LOWERCASE)
keyboard.set_mode(CustomKeyboard.MODE_UPPERCASE)
keyboard.set_mode(CustomKeyboard.MODE_NUMBERS)
keyboard.set_mode(CustomKeyboard.MODE_SPECIALS)
print("Mode switching successful")
def test_alignment(self):
"""Test keyboard alignment."""
print("Testing alignment...")
keyboard = CustomKeyboard(self.screen)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
print("Alignment successful")
def test_height_settings(self):
"""Test height configuration."""
print("Testing height settings...")
keyboard = CustomKeyboard(self.screen)
keyboard.set_style_min_height(160, 0)
keyboard.set_style_height(160, 0)
print("Height settings successful")
def test_flags(self):
"""Test object flags (show/hide)."""
print("Testing flags...")
keyboard = CustomKeyboard(self.screen)
# Test hiding
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
self.assertTrue(keyboard.has_flag(lv.obj.FLAG.HIDDEN))
# Test showing
keyboard.remove_flag(lv.obj.FLAG.HIDDEN)
self.assertFalse(keyboard.has_flag(lv.obj.FLAG.HIDDEN))
print("Flag operations successful")
def test_event_callback(self):
"""Test adding event callbacks."""
print("Testing event callbacks...")
keyboard = CustomKeyboard(self.screen)
callback_called = [False]
def test_callback(event):
callback_called[0] = True
# Add callback
keyboard.add_event_cb(test_callback, lv.EVENT.READY, None)
# Send READY event
keyboard.send_event(lv.EVENT.READY, None)
# Verify callback was called
self.assertTrue(callback_called[0], "Callback was not called")
print("Event callback successful")
def test_api_compatibility(self):
"""Test that CustomKeyboard has same API as lv.keyboard."""
print("Testing API compatibility...")
keyboard = CustomKeyboard(self.screen)
# Check that all essential methods exist
essential_methods = [
'set_textarea',
'get_textarea',
'set_mode',
'align',
'add_flag',
'remove_flag',
'has_flag',
'add_event_cb',
'send_event',
]
for method_name in essential_methods:
self.assertTrue(
hasattr(keyboard, method_name),
f"CustomKeyboard missing method: {method_name}"
)
self.assertTrue(
callable(getattr(keyboard, method_name)),
f"CustomKeyboard.{method_name} is not callable"
)
print("API compatibility verified")
if __name__ == "__main__":
unittest.main()
+189
View File
@@ -0,0 +1,189 @@
"""
Test CustomKeyboard animation support (show/hide with mpos.ui.anim).
This test reproduces the bug where CustomKeyboard is missing methods
required by mpos.ui.anim.smooth_show() and smooth_hide().
Usage:
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_animation.py
Device: ./tests/unittest.sh tests/test_graphical_keyboard_animation.py ondevice
"""
import unittest
import lvgl as lv
import mpos.ui.anim
from mpos.ui.keyboard import CustomKeyboard
class TestKeyboardAnimation(unittest.TestCase):
"""Test CustomKeyboard compatibility with animation system."""
def setUp(self):
"""Set up test fixtures."""
# Create a test screen
self.screen = lv.obj()
self.screen.set_size(320, 240)
lv.screen_load(self.screen)
# 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)
print("\n=== Animation Test Setup Complete ===")
def tearDown(self):
"""Clean up after test."""
lv.screen_load(lv.obj())
print("=== Test Cleanup Complete ===\n")
def test_keyboard_has_set_style_opa(self):
"""
Test that CustomKeyboard has set_style_opa method.
This method is required by mpos.ui.anim for fade animations.
"""
print("Testing that CustomKeyboard has set_style_opa...")
keyboard = CustomKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
# Verify method exists
self.assertTrue(
hasattr(keyboard, 'set_style_opa'),
"CustomKeyboard missing set_style_opa method"
)
self.assertTrue(
callable(getattr(keyboard, 'set_style_opa')),
"CustomKeyboard.set_style_opa is not callable"
)
# Try calling it (should not raise AttributeError)
try:
keyboard.set_style_opa(128, 0)
print("set_style_opa called successfully")
except AttributeError as e:
self.fail(f"set_style_opa raised AttributeError: {e}")
print("=== set_style_opa test PASSED ===")
def test_keyboard_smooth_show(self):
"""
Test that CustomKeyboard can be shown with smooth_show animation.
This reproduces the actual user interaction in QuasiNametag.
"""
print("Testing smooth_show animation...")
keyboard = CustomKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
# This should work without raising AttributeError
try:
mpos.ui.anim.smooth_show(keyboard)
print("smooth_show called successfully")
except AttributeError as e:
self.fail(f"smooth_show raised AttributeError: {e}\n"
"This is the bug - CustomKeyboard missing animation methods")
# Verify keyboard is no longer hidden
self.assertFalse(
keyboard.has_flag(lv.obj.FLAG.HIDDEN),
"Keyboard should not be hidden after smooth_show"
)
print("=== smooth_show test PASSED ===")
def test_keyboard_smooth_hide(self):
"""
Test that CustomKeyboard can be hidden with smooth_hide animation.
This reproduces the hide behavior in QuasiNametag.
"""
print("Testing smooth_hide animation...")
keyboard = CustomKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
# Start visible
keyboard.remove_flag(lv.obj.FLAG.HIDDEN)
# This should work without raising AttributeError
try:
mpos.ui.anim.smooth_hide(keyboard)
print("smooth_hide called successfully")
except AttributeError as e:
self.fail(f"smooth_hide raised AttributeError: {e}\n"
"This is the bug - CustomKeyboard missing animation methods")
print("=== smooth_hide test PASSED ===")
def test_keyboard_show_hide_cycle(self):
"""
Test full show/hide animation cycle.
This mimics the actual user flow:
1. Click textarea -> show keyboard
2. Press Enter/Cancel -> hide keyboard
"""
print("Testing full show/hide cycle...")
keyboard = CustomKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
# Initial state: hidden
self.assertTrue(keyboard.has_flag(lv.obj.FLAG.HIDDEN))
# Show keyboard (simulates textarea click)
try:
mpos.ui.anim.smooth_show(keyboard)
except AttributeError as e:
self.fail(f"Failed during smooth_show: {e}")
# Should be visible now
self.assertFalse(keyboard.has_flag(lv.obj.FLAG.HIDDEN))
# Hide keyboard (simulates pressing Enter)
try:
mpos.ui.anim.smooth_hide(keyboard)
except AttributeError as e:
self.fail(f"Failed during smooth_hide: {e}")
print("=== Full cycle test PASSED ===")
def test_keyboard_has_get_y_and_set_y(self):
"""
Test that CustomKeyboard has get_y and set_y methods.
These are required for slide animations (though not currently used).
"""
print("Testing get_y and set_y methods...")
keyboard = CustomKeyboard(self.screen)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
# Verify methods exist
self.assertTrue(hasattr(keyboard, 'get_y'), "Missing get_y method")
self.assertTrue(hasattr(keyboard, 'set_y'), "Missing set_y method")
# Try using them
try:
y = keyboard.get_y()
keyboard.set_y(y + 10)
new_y = keyboard.get_y()
print(f"Position test: {y} -> {new_y}")
except AttributeError as e:
self.fail(f"Position methods raised AttributeError: {e}")
print("=== Position methods test PASSED ===")
if __name__ == "__main__":
unittest.main()