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:
John McCardle 2026-01-09 15:51:20 -05:00
commit 1d11b020b0
5 changed files with 118 additions and 10 deletions

View file

@ -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();
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str());
// Look for on_key attribute on the Python object
// This handles both:
// 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) {
Py_DECREF(result);
} else {
PyErr_Print();
}
Py_DECREF(method);
Py_DECREF(attr);
} else {
// Clear AttributeError if method doesn't exist
// Not callable or is None - nothing to call
PyErr_Clear();
Py_XDECREF(method);
Py_XDECREF(attr);
}
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
PyObject* McRFPy_API::api_get_current_scene()
{