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