McRogueFace needs to accept callable objects (properties on C++ objects) and also support subclassing (getattr on user objects). Only direct properties were supported previously, now shadowing a callback by name will allow custom objects to "just work". - Added CallbackCache struct and is_python_subclass flag to UIDrawable.h - Created metaclass for tracking class-level callback changes - Updated all UI type init functions to detect subclasses - Modified PyScene.cpp event dispatch to try subclass methods
313 lines
13 KiB
Python
313 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Test UIDrawable subclass callback methods (#184)
|
|
|
|
This tests the ability to define callback methods (on_click, on_enter,
|
|
on_exit, on_move) directly in Python subclasses of UIDrawable types
|
|
(Frame, Caption, Sprite, Grid, Line, Circle, Arc).
|
|
"""
|
|
import mcrfpy
|
|
import sys
|
|
|
|
# Test results tracking
|
|
results = []
|
|
|
|
def test_passed(name):
|
|
results.append((name, True, None))
|
|
print(f" PASS: {name}")
|
|
|
|
def test_failed(name, error):
|
|
results.append((name, False, str(error)))
|
|
print(f" FAIL: {name}: {error}")
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 1: Basic Frame subclass with on_click method
|
|
# ==============================================================================
|
|
class ClickableFrame(mcrfpy.Frame):
|
|
"""Frame subclass with on_click method"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.click_count = 0
|
|
self.last_click_args = None
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.click_count += 1
|
|
self.last_click_args = (x, y, button, action)
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 2: Frame subclass with all hover callbacks
|
|
# ==============================================================================
|
|
class HoverFrame(mcrfpy.Frame):
|
|
"""Frame subclass with on_enter, on_exit, on_move"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.events = []
|
|
|
|
def on_enter(self, x, y, button, action):
|
|
self.events.append(('enter', x, y))
|
|
|
|
def on_exit(self, x, y, button, action):
|
|
self.events.append(('exit', x, y))
|
|
|
|
def on_move(self, x, y, button, action):
|
|
self.events.append(('move', x, y))
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 3: Caption subclass with on_click
|
|
# ==============================================================================
|
|
class ClickableCaption(mcrfpy.Caption):
|
|
"""Caption subclass with on_click method"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.clicked = False
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.clicked = True
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 4: Sprite subclass with on_click
|
|
# ==============================================================================
|
|
class ClickableSprite(mcrfpy.Sprite):
|
|
"""Sprite subclass with on_click method"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.clicked = False
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.clicked = True
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 5: Grid subclass with on_click
|
|
# ==============================================================================
|
|
class ClickableGrid(mcrfpy.Grid):
|
|
"""Grid subclass with on_click method"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.clicked = False
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.clicked = True
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 6: Circle subclass
|
|
# ==============================================================================
|
|
class ClickableCircle(mcrfpy.Circle):
|
|
"""Circle subclass with callbacks"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.clicked = False
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.clicked = True
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 7: Line subclass
|
|
# ==============================================================================
|
|
class ClickableLine(mcrfpy.Line):
|
|
"""Line subclass with callbacks"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.clicked = False
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.clicked = True
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 8: Arc subclass
|
|
# ==============================================================================
|
|
class ClickableArc(mcrfpy.Arc):
|
|
"""Arc subclass with callbacks"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.clicked = False
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.clicked = True
|
|
|
|
|
|
# ==============================================================================
|
|
# Test 9: Property callback takes precedence over subclass method
|
|
# ==============================================================================
|
|
class FrameWithBoth(mcrfpy.Frame):
|
|
"""Frame with both property callback and method"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.method_called = False
|
|
self.property_called = False
|
|
|
|
def on_click(self, x, y, button, action):
|
|
self.method_called = True
|
|
|
|
|
|
# ==============================================================================
|
|
# Main test execution
|
|
# ==============================================================================
|
|
print("\n=== UIDrawable Subclass Callback Tests ===\n")
|
|
|
|
# Test 1: Verify ClickableFrame is detected as subclass
|
|
try:
|
|
frame = ClickableFrame(pos=(100, 100), size=(100, 100))
|
|
# The frame should be marked as a Python subclass internally
|
|
# We verify this indirectly by checking the type relationship
|
|
assert isinstance(frame, mcrfpy.Frame), "ClickableFrame should be instance of Frame"
|
|
assert type(frame) is not mcrfpy.Frame, "ClickableFrame should be a subclass, not Frame itself"
|
|
assert type(frame).__name__ == "ClickableFrame", f"Type name should be ClickableFrame, got {type(frame).__name__}"
|
|
test_passed("ClickableFrame is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("ClickableFrame is properly subclassed", e)
|
|
|
|
# Test 2: Verify HoverFrame is detected as subclass
|
|
try:
|
|
hover = HoverFrame(pos=(250, 100), size=(100, 100))
|
|
assert isinstance(hover, mcrfpy.Frame), "HoverFrame should be instance of Frame"
|
|
assert type(hover) is not mcrfpy.Frame, "HoverFrame should be a subclass"
|
|
assert type(hover).__name__ == "HoverFrame", "Type name should be HoverFrame"
|
|
test_passed("HoverFrame is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("HoverFrame is properly subclassed", e)
|
|
|
|
# Test 3: Verify ClickableCaption is detected as subclass
|
|
try:
|
|
cap = ClickableCaption(text="Click Me", pos=(400, 100))
|
|
assert isinstance(cap, mcrfpy.Caption), "ClickableCaption should be instance of Caption"
|
|
assert type(cap) is not mcrfpy.Caption, "ClickableCaption should be a subclass"
|
|
test_passed("ClickableCaption is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("ClickableCaption is properly subclassed", e)
|
|
|
|
# Test 4: Verify ClickableSprite is detected as subclass
|
|
try:
|
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
|
sprite = ClickableSprite(pos=(100, 200), texture=texture)
|
|
assert isinstance(sprite, mcrfpy.Sprite), "ClickableSprite should be instance of Sprite"
|
|
assert type(sprite) is not mcrfpy.Sprite, "ClickableSprite should be a subclass"
|
|
test_passed("ClickableSprite is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("ClickableSprite is properly subclassed", e)
|
|
|
|
# Test 5: Verify ClickableGrid is detected as subclass
|
|
try:
|
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
|
grid = ClickableGrid(grid_size=(5, 5), texture=texture, pos=(100, 250), size=(100, 100))
|
|
assert isinstance(grid, mcrfpy.Grid), "ClickableGrid should be instance of Grid"
|
|
assert type(grid) is not mcrfpy.Grid, "ClickableGrid should be a subclass"
|
|
test_passed("ClickableGrid is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("ClickableGrid is properly subclassed", e)
|
|
|
|
# Test 6: Verify ClickableCircle is detected as subclass
|
|
try:
|
|
circle = ClickableCircle(center=(250, 250), radius=50)
|
|
assert isinstance(circle, mcrfpy.Circle), "ClickableCircle should be instance of Circle"
|
|
assert type(circle) is not mcrfpy.Circle, "ClickableCircle should be a subclass"
|
|
test_passed("ClickableCircle is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("ClickableCircle is properly subclassed", e)
|
|
|
|
# Test 7: Verify ClickableLine is detected as subclass
|
|
try:
|
|
line = ClickableLine(start=(0, 0), end=(100, 100), thickness=5, color=mcrfpy.Color(255, 255, 255))
|
|
assert isinstance(line, mcrfpy.Line), "ClickableLine should be instance of Line"
|
|
assert type(line) is not mcrfpy.Line, "ClickableLine should be a subclass"
|
|
test_passed("ClickableLine is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("ClickableLine is properly subclassed", e)
|
|
|
|
# Test 8: Verify ClickableArc is detected as subclass
|
|
try:
|
|
arc = ClickableArc(center=(350, 250), radius=50, start_angle=0.0, end_angle=90.0, color=mcrfpy.Color(255, 255, 255))
|
|
assert isinstance(arc, mcrfpy.Arc), "ClickableArc should be instance of Arc"
|
|
assert type(arc) is not mcrfpy.Arc, "ClickableArc should be a subclass"
|
|
test_passed("ClickableArc is properly subclassed")
|
|
except Exception as e:
|
|
test_failed("ClickableArc is properly subclassed", e)
|
|
|
|
# Test 9: Test that base types don't have spurious is_python_subclass flag
|
|
try:
|
|
base_frame = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
|
|
assert type(base_frame) is mcrfpy.Frame, "Base Frame should be exactly Frame type"
|
|
base_caption = mcrfpy.Caption(text="test", pos=(0, 0))
|
|
assert type(base_caption) is mcrfpy.Caption, "Base Caption should be exactly Caption type"
|
|
test_passed("Base types are NOT marked as subclasses")
|
|
except Exception as e:
|
|
test_failed("Base types are NOT marked as subclasses", e)
|
|
|
|
# Test 10: Verify subclass methods are callable
|
|
try:
|
|
frame = ClickableFrame(pos=(100, 100), size=(100, 100))
|
|
# Verify method exists and is callable
|
|
assert hasattr(frame, 'on_click'), "ClickableFrame should have on_click method"
|
|
assert callable(frame.on_click), "on_click should be callable"
|
|
# Manually call to verify it works
|
|
frame.on_click(50.0, 50.0, "left", "start")
|
|
assert frame.click_count == 1, f"click_count should be 1, got {frame.click_count}"
|
|
assert frame.last_click_args == (50.0, 50.0, "left", "start"), f"last_click_args mismatch: {frame.last_click_args}"
|
|
test_passed("Subclass methods are callable and work")
|
|
except Exception as e:
|
|
test_failed("Subclass methods are callable and work", e)
|
|
|
|
# Test 11: Verify HoverFrame methods work
|
|
try:
|
|
hover = HoverFrame(pos=(250, 100), size=(100, 100))
|
|
hover.on_enter(10.0, 20.0, "enter", "start")
|
|
hover.on_exit(30.0, 40.0, "exit", "start")
|
|
hover.on_move(50.0, 60.0, "move", "start")
|
|
assert len(hover.events) == 3, f"Should have 3 events, got {len(hover.events)}"
|
|
assert hover.events[0] == ('enter', 10.0, 20.0), f"Event mismatch: {hover.events[0]}"
|
|
assert hover.events[1] == ('exit', 30.0, 40.0), f"Event mismatch: {hover.events[1]}"
|
|
assert hover.events[2] == ('move', 50.0, 60.0), f"Event mismatch: {hover.events[2]}"
|
|
test_passed("HoverFrame methods work correctly")
|
|
except Exception as e:
|
|
test_failed("HoverFrame methods work correctly", e)
|
|
|
|
# Test 12: FrameWithBoth - verify property assignment works alongside method
|
|
try:
|
|
both = FrameWithBoth(pos=(400, 250), size=(100, 100))
|
|
property_was_called = [False]
|
|
def property_callback(x, y, btn, action):
|
|
property_was_called[0] = True
|
|
both.click = property_callback # Assign to property
|
|
# Property callback should be set
|
|
assert both.click is not None, "click property should be set"
|
|
# Method should still exist
|
|
assert hasattr(both, 'on_click'), "on_click method should still exist"
|
|
test_passed("FrameWithBoth allows both property and method")
|
|
except Exception as e:
|
|
test_failed("FrameWithBoth allows both property and method", e)
|
|
|
|
# Test 13: Verify subclass instance attributes persist
|
|
try:
|
|
frame = ClickableFrame(pos=(100, 100), size=(100, 100))
|
|
frame.custom_attr = "test_value"
|
|
assert frame.custom_attr == "test_value", "Custom attribute should persist"
|
|
frame.on_click(0, 0, "left", "start")
|
|
assert frame.click_count == 1, "Click count should be 1"
|
|
# Verify frame is still usable after attribute access
|
|
assert frame.x == 100, f"Frame x should be 100, got {frame.x}"
|
|
test_passed("Subclass instance attributes persist")
|
|
except Exception as e:
|
|
test_failed("Subclass instance attributes persist", e)
|
|
|
|
# Summary
|
|
print("\n=== Test Summary ===")
|
|
passed = sum(1 for _, p, _ in results if p)
|
|
total = len(results)
|
|
print(f"Passed: {passed}/{total}")
|
|
|
|
if passed == total:
|
|
print("\nAll tests passed!")
|
|
sys.exit(0)
|
|
else:
|
|
print("\nSome tests failed:")
|
|
for name, p, err in results:
|
|
if not p:
|
|
print(f" - {name}: {err}")
|
|
sys.exit(1)
|