You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Simplify keyboard, robustify animations
This commit is contained in:
@@ -1,5 +1,31 @@
|
||||
import lvgl as lv
|
||||
|
||||
|
||||
def safe_widget_access(callback):
|
||||
"""
|
||||
Wrapper to safely access a widget, catching LvReferenceError.
|
||||
|
||||
If the widget has been deleted, the callback is silently skipped.
|
||||
This prevents crashes when animations try to access deleted widgets.
|
||||
|
||||
Args:
|
||||
callback: Function to call (should access a widget)
|
||||
|
||||
Returns:
|
||||
None (always, even if callback returns a value)
|
||||
"""
|
||||
try:
|
||||
callback()
|
||||
except Exception as e:
|
||||
# Check if it's an LvReferenceError (widget was deleted)
|
||||
if "LvReferenceError" in str(type(e).__name__) or "Referenced object was deleted" in str(e):
|
||||
# Widget was deleted - silently ignore
|
||||
pass
|
||||
else:
|
||||
# Some other error - re-raise it
|
||||
raise
|
||||
|
||||
|
||||
class WidgetAnimator:
|
||||
|
||||
# def __init__(self):
|
||||
@@ -27,10 +53,10 @@ class WidgetAnimator:
|
||||
anim.set_values(0, 255)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
anim.set_custom_exec_cb(lambda anim, value: widget.set_style_opa(value, 0))
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_style_opa(value, 0)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Ensure opacity is reset after animation
|
||||
anim.set_completed_cb(lambda *args: widget.set_style_opa(255, 0))
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_style_opa(255, 0)))
|
||||
elif anim_type == "slide_down":
|
||||
print("doing slide_down")
|
||||
# Create slide-down animation (y from -height to original y)
|
||||
@@ -42,10 +68,10 @@ class WidgetAnimator:
|
||||
anim.set_values(original_y - height, original_y)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
anim.set_custom_exec_cb(lambda anim, value: widget.set_y(value))
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Reset y position after animation
|
||||
anim.set_completed_cb(lambda *args: widget.set_y(original_y))
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_y(original_y)))
|
||||
elif anim_type == "slide_up":
|
||||
# Create slide-up animation (y from +height to original y)
|
||||
# Seems to cause scroll bars to be added somehow if done to a keyboard at the bottom of the screen...
|
||||
@@ -57,10 +83,10 @@ class WidgetAnimator:
|
||||
anim.set_values(original_y + height, original_y)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
anim.set_custom_exec_cb(lambda anim, value: widget.set_y(value))
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Reset y position after animation
|
||||
anim.set_completed_cb(lambda *args: widget.set_y(original_y))
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_y(original_y)))
|
||||
|
||||
# Store and start animation
|
||||
#self.animations[widget] = anim
|
||||
@@ -77,10 +103,10 @@ class WidgetAnimator:
|
||||
anim.set_values(255, 0)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
anim.set_custom_exec_cb(lambda anim, value: widget.set_style_opa(value, 0))
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_style_opa(value, 0)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator.hide_complete_cb(widget, hide=hide))
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, hide=hide)))
|
||||
elif anim_type == "slide_down":
|
||||
# Create slide-down animation (y from original y to +height)
|
||||
# Seems to cause scroll bars to be added somehow if done to a keyboard at the bottom of the screen...
|
||||
@@ -92,10 +118,10 @@ class WidgetAnimator:
|
||||
anim.set_values(original_y, original_y + height)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
anim.set_custom_exec_cb(lambda anim, value: widget.set_y(value))
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator.hide_complete_cb(widget, original_y, hide))
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, original_y, hide)))
|
||||
elif anim_type == "slide_up":
|
||||
print("hide with slide_up")
|
||||
# Create slide-up animation (y from original y to -height)
|
||||
@@ -107,10 +133,10 @@ class WidgetAnimator:
|
||||
anim.set_values(original_y, original_y - height)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
anim.set_custom_exec_cb(lambda anim, value: widget.set_y(value))
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator.hide_complete_cb(widget, original_y, hide))
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, original_y, hide)))
|
||||
|
||||
# Store and start animation
|
||||
#self.animations[widget] = anim
|
||||
|
||||
@@ -179,98 +179,32 @@ class CustomKeyboard:
|
||||
ta.set_text(new_text)
|
||||
|
||||
# ========================================================================
|
||||
# LVGL keyboard-compatible API
|
||||
# Python magic method for automatic method forwarding
|
||||
# ========================================================================
|
||||
|
||||
def set_textarea(self, textarea):
|
||||
"""Set the textarea that this keyboard should edit."""
|
||||
self._keyboard.set_textarea(textarea)
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Forward any undefined method/attribute to the underlying LVGL keyboard.
|
||||
|
||||
def get_textarea(self):
|
||||
"""Get the currently associated textarea."""
|
||||
return self._keyboard.get_textarea()
|
||||
This allows CustomKeyboard to support ALL lv.keyboard methods automatically
|
||||
without needing to manually wrap each one. Any method not defined on
|
||||
CustomKeyboard will be forwarded to self._keyboard.
|
||||
|
||||
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)
|
||||
Examples:
|
||||
keyboard.set_textarea(ta) # Works
|
||||
keyboard.align(lv.ALIGN.CENTER) # Works
|
||||
keyboard.set_style_opa(128, 0) # Works
|
||||
keyboard.any_lvgl_method() # Works!
|
||||
"""
|
||||
# Forward to the underlying keyboard object
|
||||
return getattr(self._keyboard, name)
|
||||
|
||||
def get_lvgl_obj(self):
|
||||
"""
|
||||
Get the underlying LVGL keyboard object.
|
||||
|
||||
Use this if you need direct access to LVGL methods not wrapped here.
|
||||
This is now rarely needed since __getattr__ forwards everything automatically.
|
||||
Kept for backwards compatibility.
|
||||
"""
|
||||
return self._keyboard
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
Test that animations handle deleted widgets gracefully.
|
||||
|
||||
This test reproduces the crash that occurs when:
|
||||
1. An animation is started on a widget (e.g., keyboard fade-in)
|
||||
2. The widget is deleted while the animation is running (e.g., user closes app)
|
||||
3. The animation callback tries to access the deleted widget
|
||||
4. Result: LvReferenceError crash
|
||||
|
||||
The fix should make animations check if the widget still exists before
|
||||
trying to access it.
|
||||
|
||||
Usage:
|
||||
Desktop: ./tests/unittest.sh tests/test_graphical_animation_deleted_widget.py
|
||||
Device: ./tests/unittest.sh tests/test_graphical_animation_deleted_widget.py ondevice
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import lvgl as lv
|
||||
import mpos.ui.anim
|
||||
import time
|
||||
from graphical_test_helper import wait_for_render
|
||||
|
||||
|
||||
class TestAnimationDeletedWidget(unittest.TestCase):
|
||||
"""Test that animations don't crash when widget is deleted."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.screen = lv.obj()
|
||||
self.screen.set_size(320, 240)
|
||||
lv.screen_load(self.screen)
|
||||
print("\n=== Animation Deletion Test Setup ===")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
lv.screen_load(lv.obj())
|
||||
wait_for_render(5)
|
||||
print("=== Test Cleanup Complete ===\n")
|
||||
|
||||
def test_smooth_show_with_deleted_widget(self):
|
||||
"""
|
||||
Test that smooth_show doesn't crash if widget is deleted during animation.
|
||||
|
||||
This reproduces the exact scenario:
|
||||
- User opens keyboard (smooth_show animation starts)
|
||||
- User presses escape (app closes, deleting all widgets)
|
||||
- Animation tries to complete on deleted widget
|
||||
"""
|
||||
print("Testing smooth_show with deleted widget...")
|
||||
|
||||
# Create a widget
|
||||
widget = lv.obj(self.screen)
|
||||
widget.set_size(200, 100)
|
||||
widget.center()
|
||||
widget.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
# Start fade-in animation (500ms duration)
|
||||
print("Starting smooth_show animation...")
|
||||
mpos.ui.anim.smooth_show(widget)
|
||||
|
||||
# Give animation time to start
|
||||
wait_for_render(2)
|
||||
|
||||
# Delete the widget while animation is running (simulates app close)
|
||||
print("Deleting widget while animation is running...")
|
||||
widget.delete()
|
||||
|
||||
# Process LVGL tasks - this should trigger animation callbacks
|
||||
# If not fixed, this will crash with LvReferenceError
|
||||
print("Processing LVGL tasks (animation callbacks)...")
|
||||
try:
|
||||
for _ in range(100):
|
||||
lv.task_handler()
|
||||
time.sleep(0.01) # 1 second total to let animation complete
|
||||
print("SUCCESS: No crash when accessing deleted widget")
|
||||
except Exception as e:
|
||||
if "LvReferenceError" in str(type(e).__name__):
|
||||
self.fail(f"CRASH: Animation tried to access deleted widget: {e}")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("=== smooth_show deletion test PASSED ===")
|
||||
|
||||
def test_smooth_hide_with_deleted_widget(self):
|
||||
"""
|
||||
Test that smooth_hide doesn't crash if widget is deleted during animation.
|
||||
"""
|
||||
print("Testing smooth_hide with deleted widget...")
|
||||
|
||||
# Create a visible widget
|
||||
widget = lv.obj(self.screen)
|
||||
widget.set_size(200, 100)
|
||||
widget.center()
|
||||
# Start visible
|
||||
widget.remove_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
# Start fade-out animation
|
||||
print("Starting smooth_hide animation...")
|
||||
mpos.ui.anim.smooth_hide(widget)
|
||||
|
||||
# Give animation time to start
|
||||
wait_for_render(2)
|
||||
|
||||
# Delete the widget while animation is running
|
||||
print("Deleting widget while animation is running...")
|
||||
widget.delete()
|
||||
|
||||
# Process LVGL tasks
|
||||
print("Processing LVGL tasks (animation callbacks)...")
|
||||
try:
|
||||
for _ in range(100):
|
||||
lv.task_handler()
|
||||
time.sleep(0.01)
|
||||
print("SUCCESS: No crash when accessing deleted widget")
|
||||
except Exception as e:
|
||||
if "LvReferenceError" in str(type(e).__name__):
|
||||
self.fail(f"CRASH: Animation tried to access deleted widget: {e}")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("=== smooth_hide deletion test PASSED ===")
|
||||
|
||||
def test_keyboard_scenario(self):
|
||||
"""
|
||||
Test the exact scenario from QuasiNametag:
|
||||
1. Create keyboard with smooth_show
|
||||
2. Delete screen (simulating app close with ESC)
|
||||
3. Should not crash
|
||||
"""
|
||||
print("Testing keyboard deletion scenario...")
|
||||
|
||||
from mpos.ui.keyboard import CustomKeyboard
|
||||
|
||||
# Create textarea and keyboard (like QuasiNametag does)
|
||||
textarea = lv.textarea(self.screen)
|
||||
textarea.set_size(280, 40)
|
||||
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
|
||||
|
||||
keyboard = CustomKeyboard(self.screen)
|
||||
keyboard.set_textarea(textarea)
|
||||
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
|
||||
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
# User clicks textarea - keyboard shows with animation
|
||||
print("Showing keyboard with animation...")
|
||||
mpos.ui.anim.smooth_show(keyboard)
|
||||
|
||||
# Give animation time to start
|
||||
wait_for_render(2)
|
||||
|
||||
# User presses ESC - app closes, screen is deleted
|
||||
print("Deleting screen (simulating app close)...")
|
||||
# Create new screen first, then delete old one
|
||||
new_screen = lv.obj()
|
||||
lv.screen_load(new_screen)
|
||||
self.screen.delete()
|
||||
self.screen = new_screen
|
||||
|
||||
# Process LVGL tasks - animation callbacks should not crash
|
||||
print("Processing LVGL tasks after deletion...")
|
||||
try:
|
||||
for _ in range(100):
|
||||
lv.task_handler()
|
||||
time.sleep(0.01)
|
||||
print("SUCCESS: No crash after deleting screen with animating keyboard")
|
||||
except Exception as e:
|
||||
if "LvReferenceError" in str(type(e).__name__):
|
||||
self.fail(f"CRASH: Keyboard animation tried to access deleted widget: {e}")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("=== Keyboard scenario test PASSED ===")
|
||||
|
||||
def test_multiple_animations_deleted(self):
|
||||
"""
|
||||
Test that multiple widgets with animations can be deleted safely.
|
||||
"""
|
||||
print("Testing multiple animated widgets deletion...")
|
||||
|
||||
widgets = []
|
||||
for i in range(5):
|
||||
w = lv.obj(self.screen)
|
||||
w.set_size(50, 50)
|
||||
w.set_pos(i * 60, 50)
|
||||
w.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
widgets.append(w)
|
||||
|
||||
# Start animations on all widgets
|
||||
print("Starting animations on 5 widgets...")
|
||||
for w in widgets:
|
||||
mpos.ui.anim.smooth_show(w)
|
||||
|
||||
wait_for_render(2)
|
||||
|
||||
# Delete all widgets while animations are running
|
||||
print("Deleting all widgets while animations are running...")
|
||||
for w in widgets:
|
||||
w.delete()
|
||||
|
||||
# Process tasks
|
||||
print("Processing LVGL tasks...")
|
||||
try:
|
||||
for _ in range(100):
|
||||
lv.task_handler()
|
||||
time.sleep(0.01)
|
||||
print("SUCCESS: No crash with multiple deleted widgets")
|
||||
except Exception as e:
|
||||
if "LvReferenceError" in str(type(e).__name__):
|
||||
self.fail(f"CRASH: Multiple animations crashed on deleted widgets: {e}")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("=== Multiple animations test PASSED ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Test that CustomKeyboard forwards all methods to underlying lv.keyboard.
|
||||
|
||||
This demonstrates the __getattr__ magic method works correctly and that
|
||||
CustomKeyboard supports any LVGL keyboard method without manual wrapping.
|
||||
|
||||
Usage:
|
||||
Desktop: ./tests/unittest.sh tests/test_keyboard_method_forwarding.py
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import lvgl as lv
|
||||
from mpos.ui.keyboard import CustomKeyboard
|
||||
|
||||
|
||||
class TestMethodForwarding(unittest.TestCase):
|
||||
"""Test that arbitrary LVGL methods are forwarded correctly."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.screen = lv.obj()
|
||||
self.screen.set_size(320, 240)
|
||||
lv.screen_load(self.screen)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
lv.screen_load(lv.obj())
|
||||
|
||||
def test_common_methods_work(self):
|
||||
"""Test commonly used LVGL methods work via __getattr__."""
|
||||
print("\nTesting common LVGL methods...")
|
||||
|
||||
keyboard = CustomKeyboard(self.screen)
|
||||
|
||||
# These should all work without explicit wrapper methods:
|
||||
methods_to_test = [
|
||||
('set_style_opa', (128, 0)),
|
||||
('get_x', ()),
|
||||
('get_y', ()),
|
||||
('get_width', ()),
|
||||
('get_height', ()),
|
||||
('add_flag', (lv.obj.FLAG.HIDDEN,)),
|
||||
('has_flag', (lv.obj.FLAG.HIDDEN,)),
|
||||
('remove_flag', (lv.obj.FLAG.HIDDEN,)),
|
||||
]
|
||||
|
||||
for method_name, args in methods_to_test:
|
||||
try:
|
||||
method = getattr(keyboard, method_name)
|
||||
result = method(*args)
|
||||
print(f" ✓ {method_name}{args} -> {result}")
|
||||
except Exception as e:
|
||||
self.fail(f"{method_name} failed: {e}")
|
||||
|
||||
print("All common methods work!")
|
||||
|
||||
def test_style_methods_work(self):
|
||||
"""Test various style methods work."""
|
||||
print("\nTesting style methods...")
|
||||
|
||||
keyboard = CustomKeyboard(self.screen)
|
||||
|
||||
# All these style methods should work:
|
||||
keyboard.set_style_min_height(100, 0)
|
||||
keyboard.set_style_max_height(200, 0)
|
||||
keyboard.set_style_height(150, 0)
|
||||
keyboard.set_style_opa(255, 0)
|
||||
|
||||
print("All style methods work!")
|
||||
|
||||
def test_position_methods_work(self):
|
||||
"""Test position methods work."""
|
||||
print("\nTesting position methods...")
|
||||
|
||||
keyboard = CustomKeyboard(self.screen)
|
||||
|
||||
# Position methods:
|
||||
x = keyboard.get_x()
|
||||
y = keyboard.get_y()
|
||||
print(f" Initial position: ({x}, {y})")
|
||||
|
||||
keyboard.set_x(50)
|
||||
keyboard.set_y(100)
|
||||
keyboard.set_pos(25, 75)
|
||||
|
||||
new_x = keyboard.get_x()
|
||||
new_y = keyboard.get_y()
|
||||
print(f" After set_pos(25, 75): ({new_x}, {new_y})")
|
||||
|
||||
print("All position methods work!")
|
||||
|
||||
def test_undocumented_methods_still_work(self):
|
||||
"""
|
||||
Test that even undocumented/obscure LVGL methods work.
|
||||
|
||||
The beauty of __getattr__ is that ANY lv.keyboard method works,
|
||||
even ones we didn't explicitly think about.
|
||||
"""
|
||||
print("\nTesting that arbitrary LVGL methods work...")
|
||||
|
||||
keyboard = CustomKeyboard(self.screen)
|
||||
|
||||
# Try some less common methods:
|
||||
try:
|
||||
# Get the parent object
|
||||
parent = keyboard.get_parent()
|
||||
print(f" ✓ get_parent() -> {parent}")
|
||||
|
||||
# Get style properties
|
||||
border_width = keyboard.get_style_border_width(lv.PART.MAIN)
|
||||
print(f" ✓ get_style_border_width() -> {border_width}")
|
||||
|
||||
# These methods exist on lv.obj and should work:
|
||||
keyboard.set_style_border_width(2, 0)
|
||||
print(f" ✓ set_style_border_width(2, 0)")
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f"Arbitrary LVGL method failed: {e}")
|
||||
|
||||
print("Even undocumented methods work via __getattr__!")
|
||||
|
||||
def test_method_forwarding_preserves_behavior(self):
|
||||
"""
|
||||
Test that forwarded methods behave identically to native calls.
|
||||
"""
|
||||
print("\nTesting that forwarding preserves behavior...")
|
||||
|
||||
keyboard = CustomKeyboard(self.screen)
|
||||
textarea = lv.textarea(self.screen)
|
||||
|
||||
# Set textarea through CustomKeyboard
|
||||
keyboard.set_textarea(textarea)
|
||||
|
||||
# Get it back
|
||||
returned_ta = keyboard.get_textarea()
|
||||
|
||||
# Should be the same object
|
||||
self.assertEqual(returned_ta, textarea,
|
||||
"Forwarded methods should preserve object identity")
|
||||
|
||||
print("Method forwarding preserves behavior correctly!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user