Implement Scene subclass on_key callback support
Scene subclasses can now define on_key(self, key, state) methods that
receive keyboard events, matching the existing on_enter, on_exit, and
update lifecycle callbacks.
Changes:
- Rename call_on_keypress to call_on_key (consistent naming with property)
- Add triggerKeyEvent helper in McRFPy_API
- Call triggerKeyEvent from GameEngine when key_callable is not set
- Fix condition to check key_callable.isNone() (not just pointer existence)
- Handle both bound methods and instance-assigned callables
Usage:
class GameScene(mcrfpy.Scene):
def on_key(self, key, state):
if key == "Escape" and state == "end":
quit_game()
Property assignment (scene.on_key = callable) still works and takes
precedence when key_callable is set via the property setter.
Includes comprehensive test: tests/unit/scene_subclass_on_key_test.py
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b6eb70748a
commit
1d11b020b0
5 changed files with 118 additions and 10 deletions
|
|
@ -457,11 +457,17 @@ void GameEngine::processEvent(const sf::Event& event)
|
||||||
std::string name = currentScene()->action(actionCode);
|
std::string name = currentScene()->action(actionCode);
|
||||||
currentScene()->doAction(name, actionType);
|
currentScene()->doAction(name, actionType);
|
||||||
}
|
}
|
||||||
else if (currentScene()->key_callable &&
|
else if (currentScene()->key_callable && !currentScene()->key_callable->isNone() &&
|
||||||
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
|
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
|
||||||
{
|
{
|
||||||
|
// Property-assigned handler (scene.on_key = callable)
|
||||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||||
}
|
}
|
||||||
|
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||||
|
{
|
||||||
|
// Try subclass on_key method if no property handler is set
|
||||||
|
McRFPy_API::triggerKeyEvent(ActionCode::key_str(event.key.code), actionType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEngine::sUserInput()
|
void GameEngine::sUserInput()
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ public:
|
||||||
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
|
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
|
||||||
static void updatePythonScenes(float dt);
|
static void updatePythonScenes(float dt);
|
||||||
static void triggerResize(int width, int height);
|
static void triggerResize(int width, int height);
|
||||||
|
static void triggerKeyEvent(const std::string& key, const std::string& action);
|
||||||
|
|
||||||
// #151: Module-level scene property accessors
|
// #151: Module-level scene property accessors
|
||||||
static PyObject* api_get_current_scene();
|
static PyObject* api_get_current_scene();
|
||||||
|
|
|
||||||
|
|
@ -413,23 +413,28 @@ void PySceneClass::call_on_exit(PySceneObject* self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
|
void PySceneClass::call_on_key(PySceneObject* self, const std::string& key, const std::string& action)
|
||||||
{
|
{
|
||||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||||
|
|
||||||
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress");
|
// Look for on_key attribute on the Python object
|
||||||
if (method && PyCallable_Check(method)) {
|
// This handles both:
|
||||||
PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str());
|
// 1. Subclass methods: class MyScene(Scene): def on_key(self, k, s): ...
|
||||||
|
// 2. Instance attributes: ts.on_key = lambda k, s: ... (when subclass shadows property)
|
||||||
|
PyObject* attr = PyObject_GetAttrString((PyObject*)self, "on_key");
|
||||||
|
if (attr && PyCallable_Check(attr) && attr != Py_None) {
|
||||||
|
// Call it - works for both bound methods and regular callables
|
||||||
|
PyObject* result = PyObject_CallFunction(attr, "ss", key.c_str(), action.c_str());
|
||||||
if (result) {
|
if (result) {
|
||||||
Py_DECREF(result);
|
Py_DECREF(result);
|
||||||
} else {
|
} else {
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
}
|
}
|
||||||
Py_DECREF(method);
|
Py_DECREF(attr);
|
||||||
} else {
|
} else {
|
||||||
// Clear AttributeError if method doesn't exist
|
// Not callable or is None - nothing to call
|
||||||
PyErr_Clear();
|
PyErr_Clear();
|
||||||
Py_XDECREF(method);
|
Py_XDECREF(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyGILState_Release(gstate);
|
PyGILState_Release(gstate);
|
||||||
|
|
@ -571,6 +576,18 @@ void McRFPy_API::triggerResize(int width, int height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to trigger key events on Python scene subclasses
|
||||||
|
void McRFPy_API::triggerKeyEvent(const std::string& key, const std::string& action)
|
||||||
|
{
|
||||||
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
// Only notify the active scene if it has an on_key method (subclass)
|
||||||
|
if (python_scenes.count(game->scene) > 0) {
|
||||||
|
PySceneClass::call_on_key(python_scenes[game->scene], key, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// #151: Get the current scene as a Python Scene object
|
// #151: Get the current scene as a Python Scene object
|
||||||
PyObject* McRFPy_API::api_get_current_scene()
|
PyObject* McRFPy_API::api_get_current_scene()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public:
|
||||||
// Lifecycle callbacks (called from C++)
|
// Lifecycle callbacks (called from C++)
|
||||||
static void call_on_enter(PySceneObject* self);
|
static void call_on_enter(PySceneObject* self);
|
||||||
static void call_on_exit(PySceneObject* self);
|
static void call_on_exit(PySceneObject* self);
|
||||||
static void call_on_keypress(PySceneObject* self, std::string key, std::string action);
|
static void call_on_key(PySceneObject* self, const std::string& key, const std::string& action);
|
||||||
static void call_update(PySceneObject* self, float dt);
|
static void call_update(PySceneObject* self, float dt);
|
||||||
static void call_on_resize(PySceneObject* self, int width, int height);
|
static void call_on_resize(PySceneObject* self, int width, int height);
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ namespace mcrfpydef {
|
||||||
"Lifecycle Callbacks (override in subclass):\n"
|
"Lifecycle Callbacks (override in subclass):\n"
|
||||||
" on_enter(): Called when scene becomes active via activate().\n"
|
" on_enter(): Called when scene becomes active via activate().\n"
|
||||||
" on_exit(): Called when scene is deactivated (another scene activates).\n"
|
" on_exit(): Called when scene is deactivated (another scene activates).\n"
|
||||||
" on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.\n"
|
" on_key(key: str, action: str): Called for keyboard events (subclass method).\n"
|
||||||
" update(dt: float): Called every frame with delta time in seconds.\n"
|
" update(dt: float): Called every frame with delta time in seconds.\n"
|
||||||
" on_resize(width: int, height: int): Called when window is resized.\n\n"
|
" on_resize(width: int, height: int): Called when window is resized.\n\n"
|
||||||
"Example:\n"
|
"Example:\n"
|
||||||
|
|
|
||||||
84
tests/unit/scene_subclass_on_key_test.py
Normal file
84
tests/unit/scene_subclass_on_key_test.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
"""Test Scene subclass on_key method callback
|
||||||
|
|
||||||
|
Verifies that:
|
||||||
|
1. Subclass on_key method is called for keyboard events
|
||||||
|
2. Property assignment (scene.on_key = callable) still works
|
||||||
|
3. Property assignment on subclass overrides the method
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Test state
|
||||||
|
tests_passed = 0
|
||||||
|
tests_failed = 0
|
||||||
|
|
||||||
|
def test_subclass_method():
|
||||||
|
"""Test that subclass on_key method receives keyboard events"""
|
||||||
|
global tests_passed, tests_failed
|
||||||
|
events = []
|
||||||
|
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def on_key(self, key, state):
|
||||||
|
events.append((key, state))
|
||||||
|
|
||||||
|
ts = TestScene('test_method')
|
||||||
|
ts.activate()
|
||||||
|
automation.keyDown('a')
|
||||||
|
automation.keyUp('a')
|
||||||
|
|
||||||
|
if len(events) >= 2:
|
||||||
|
print("PASS: test_subclass_method")
|
||||||
|
tests_passed += 1
|
||||||
|
else:
|
||||||
|
print(f"FAIL: test_subclass_method - got {events}")
|
||||||
|
tests_failed += 1
|
||||||
|
|
||||||
|
def test_property_handler():
|
||||||
|
"""Test that property assignment works"""
|
||||||
|
global tests_passed, tests_failed
|
||||||
|
events = []
|
||||||
|
|
||||||
|
scene = mcrfpy.Scene('test_property')
|
||||||
|
scene.on_key = lambda k, s: events.append((k, s))
|
||||||
|
scene.activate()
|
||||||
|
automation.keyDown('b')
|
||||||
|
automation.keyUp('b')
|
||||||
|
|
||||||
|
if len(events) >= 2:
|
||||||
|
print("PASS: test_property_handler")
|
||||||
|
tests_passed += 1
|
||||||
|
else:
|
||||||
|
print(f"FAIL: test_property_handler - got {events}")
|
||||||
|
tests_failed += 1
|
||||||
|
|
||||||
|
def test_property_overrides_method():
|
||||||
|
"""Test that property assignment on subclass overrides the method"""
|
||||||
|
global tests_passed, tests_failed
|
||||||
|
method_events = []
|
||||||
|
property_events = []
|
||||||
|
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def on_key(self, key, state):
|
||||||
|
method_events.append((key, state))
|
||||||
|
|
||||||
|
ts = TestScene('test_override')
|
||||||
|
ts.activate()
|
||||||
|
ts.on_key = lambda k, s: property_events.append((k, s))
|
||||||
|
automation.keyDown('c')
|
||||||
|
automation.keyUp('c')
|
||||||
|
|
||||||
|
if len(property_events) >= 2 and len(method_events) == 0:
|
||||||
|
print("PASS: test_property_overrides_method")
|
||||||
|
tests_passed += 1
|
||||||
|
else:
|
||||||
|
print(f"FAIL: test_property_overrides_method - method={method_events}, property={property_events}")
|
||||||
|
tests_failed += 1
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test_subclass_method()
|
||||||
|
test_property_handler()
|
||||||
|
test_property_overrides_method()
|
||||||
|
|
||||||
|
print(f"\nResults: {tests_passed} passed, {tests_failed} failed")
|
||||||
|
sys.exit(0 if tests_failed == 0 else 1)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue