cell, scene callbacks support for derived classes + enum args
This commit is contained in:
parent
c7cf3f0e5b
commit
d12bfd224c
6 changed files with 648 additions and 109 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyMouseButton.h"
|
#include "PyMouseButton.h"
|
||||||
#include "PyInputState.h"
|
#include "PyInputState.h"
|
||||||
|
#include "PyKey.h"
|
||||||
|
|
||||||
PyCallable::PyCallable(PyObject* _target)
|
PyCallable::PyCallable(PyObject* _target)
|
||||||
{
|
{
|
||||||
|
|
@ -143,7 +144,32 @@ PyKeyCallable::PyKeyCallable()
|
||||||
void PyKeyCallable::call(std::string key, std::string action)
|
void PyKeyCallable::call(std::string key, std::string action)
|
||||||
{
|
{
|
||||||
if (target == Py_None || target == NULL) return;
|
if (target == Py_None || target == NULL) return;
|
||||||
PyObject* args = Py_BuildValue("(ss)", key.c_str(), action.c_str());
|
|
||||||
|
// Convert key string to Key enum
|
||||||
|
sf::Keyboard::Key sfml_key = PyKey::from_legacy_string(key.c_str());
|
||||||
|
PyObject* key_enum = PyObject_CallFunction(PyKey::key_enum_class, "i", static_cast<int>(sfml_key));
|
||||||
|
if (!key_enum) {
|
||||||
|
std::cerr << "Failed to create Key enum for key: " << key << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert action string to InputState enum
|
||||||
|
int action_val = (action == "start" || action == "pressed") ? 0 : 1; // PRESSED = 0, RELEASED = 1
|
||||||
|
PyObject* action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val);
|
||||||
|
if (!action_enum) {
|
||||||
|
Py_DECREF(key_enum);
|
||||||
|
std::cerr << "Failed to create InputState enum for action: " << action << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* args = Py_BuildValue("(OO)", key_enum, action_enum);
|
||||||
|
Py_DECREF(key_enum);
|
||||||
|
Py_DECREF(action_enum);
|
||||||
|
|
||||||
PyObject* retval = PyCallable::call(args, NULL);
|
PyObject* retval = PyCallable::call(args, NULL);
|
||||||
if (!retval)
|
if (!retval)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -195,14 +195,43 @@ void PyScene::do_mouse_input(std::string button, std::string type)
|
||||||
// #184: Try property-assigned callable first (fast path)
|
// #184: Try property-assigned callable first (fast path)
|
||||||
if (target->click_callable && !target->click_callable->isNone()) {
|
if (target->click_callable && !target->click_callable->isNone()) {
|
||||||
target->click_callable->call(mousepos, button, type);
|
target->click_callable->call(mousepos, button, type);
|
||||||
|
|
||||||
|
// Also fire grid cell click if applicable
|
||||||
|
if (target->derived_type() == PyObjectsEnum::UIGRID) {
|
||||||
|
auto grid = static_cast<UIGrid*>(target);
|
||||||
|
if (grid->last_clicked_cell.has_value()) {
|
||||||
|
grid->fireCellClick(grid->last_clicked_cell.value(), button, type);
|
||||||
|
grid->last_clicked_cell = std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
return; // Stop after first handler
|
return; // Stop after first handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// #184: Try Python subclass method
|
// #184: Try Python subclass method
|
||||||
if (tryCallPythonMethod(target, "on_click", mousepos, button.c_str(), type.c_str())) {
|
if (tryCallPythonMethod(target, "on_click", mousepos, button.c_str(), type.c_str())) {
|
||||||
|
// Also fire grid cell click if applicable
|
||||||
|
if (target->derived_type() == PyObjectsEnum::UIGRID) {
|
||||||
|
auto grid = static_cast<UIGrid*>(target);
|
||||||
|
if (grid->last_clicked_cell.has_value()) {
|
||||||
|
grid->fireCellClick(grid->last_clicked_cell.value(), button, type);
|
||||||
|
grid->last_clicked_cell = std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
return; // Stop after first handler
|
return; // Stop after first handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire grid cell click even if no on_click handler (but has cell click handler)
|
||||||
|
if (target->derived_type() == PyObjectsEnum::UIGRID) {
|
||||||
|
auto grid = static_cast<UIGrid*>(target);
|
||||||
|
if (grid->last_clicked_cell.has_value()) {
|
||||||
|
bool handled = grid->fireCellClick(grid->last_clicked_cell.value(), button, type);
|
||||||
|
grid->last_clicked_cell = std::nullopt;
|
||||||
|
if (handled) {
|
||||||
|
return; // Stop after handling cell click
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Element claimed the click but had no handler - still stop propagation
|
// Element claimed the click but had no handler - still stop propagation
|
||||||
// (This maintains consistent behavior for subclasses that don't define on_click)
|
// (This maintains consistent behavior for subclasses that don't define on_click)
|
||||||
if (target->is_python_subclass) {
|
if (target->is_python_subclass) {
|
||||||
|
|
@ -286,7 +315,8 @@ void PyScene::do_mouse_hover(int x, int y)
|
||||||
auto grid = static_cast<UIGrid*>(drawable);
|
auto grid = static_cast<UIGrid*>(drawable);
|
||||||
|
|
||||||
// #142 - Update cell hover tracking for grid
|
// #142 - Update cell hover tracking for grid
|
||||||
grid->updateCellHover(mousepos);
|
// Pass "none" for button and "move" for action during hover
|
||||||
|
grid->updateCellHover(mousepos, "none", "move");
|
||||||
|
|
||||||
if (grid->children) {
|
if (grid->children) {
|
||||||
for (auto& child : *grid->children) {
|
for (auto& child : *grid->children) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "McRFPy_Doc.h"
|
#include "McRFPy_Doc.h"
|
||||||
#include "PyTransition.h"
|
#include "PyTransition.h"
|
||||||
|
#include "PyKey.h"
|
||||||
|
#include "PyInputState.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
// Static map to store Python scene objects by name
|
// Static map to store Python scene objects by name
|
||||||
|
|
@ -390,12 +392,38 @@ void PySceneClass::call_on_key(PySceneObject* self, const std::string& key, cons
|
||||||
|
|
||||||
// Look for on_key attribute on the Python object
|
// Look for on_key attribute on the Python object
|
||||||
// This handles both:
|
// This handles both:
|
||||||
// 1. Subclass methods: class MyScene(Scene): def on_key(self, k, s): ...
|
// 1. Subclass methods: class MyScene(Scene): def on_key(self, key, action): ...
|
||||||
// 2. Instance attributes: ts.on_key = lambda k, s: ... (when subclass shadows property)
|
// 2. Instance attributes: ts.on_key = lambda k, a: ... (when subclass shadows property)
|
||||||
PyObject* attr = PyObject_GetAttrString((PyObject*)self, "on_key");
|
PyObject* attr = PyObject_GetAttrString((PyObject*)self, "on_key");
|
||||||
if (attr && PyCallable_Check(attr) && attr != Py_None) {
|
if (attr && PyCallable_Check(attr) && attr != Py_None) {
|
||||||
// Call it - works for both bound methods and regular callables
|
// Convert key string to Key enum
|
||||||
PyObject* result = PyObject_CallFunction(attr, "ss", key.c_str(), action.c_str());
|
sf::Keyboard::Key sfml_key = PyKey::from_legacy_string(key.c_str());
|
||||||
|
PyObject* key_enum = PyObject_CallFunction(PyKey::key_enum_class, "i", static_cast<int>(sfml_key));
|
||||||
|
if (!key_enum) {
|
||||||
|
std::cerr << "Failed to create Key enum for key: " << key << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
Py_DECREF(attr);
|
||||||
|
PyGILState_Release(gstate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert action string to InputState enum
|
||||||
|
int action_val = (action == "start" || action == "pressed") ? 0 : 1; // PRESSED = 0, RELEASED = 1
|
||||||
|
PyObject* action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val);
|
||||||
|
if (!action_enum) {
|
||||||
|
std::cerr << "Failed to create InputState enum for action: " << action << std::endl;
|
||||||
|
Py_DECREF(key_enum);
|
||||||
|
PyErr_Print();
|
||||||
|
Py_DECREF(attr);
|
||||||
|
PyGILState_Release(gstate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call it with typed args - works for both bound methods and regular callables
|
||||||
|
PyObject* result = PyObject_CallFunctionObjArgs(attr, key_enum, action_enum, NULL);
|
||||||
|
Py_DECREF(key_enum);
|
||||||
|
Py_DECREF(action_enum);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
Py_DECREF(result);
|
Py_DECREF(result);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -485,7 +513,7 @@ PyGetSetDef PySceneClass::getsetters[] = {
|
||||||
"Use to add, remove, or iterate over UI elements. Changes are reflected immediately."), NULL},
|
"Use to add, remove, or iterate over UI elements. Changes are reflected immediately."), NULL},
|
||||||
{"on_key", (getter)PySceneClass_get_on_key, (setter)PySceneClass_set_on_key,
|
{"on_key", (getter)PySceneClass_get_on_key, (setter)PySceneClass_set_on_key,
|
||||||
MCRF_PROPERTY(on_key, "Keyboard event handler (callable or None). "
|
MCRF_PROPERTY(on_key, "Keyboard event handler (callable or None). "
|
||||||
"Function receives (key: str, action: str) for keyboard events. "
|
"Function receives (key: Key, action: InputState) for keyboard events. "
|
||||||
"Set to None to remove the handler."), NULL},
|
"Set to None to remove the handler."), NULL},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
388
src/UIGrid.cpp
388
src/UIGrid.cpp
|
|
@ -13,6 +13,8 @@
|
||||||
#include "PyHeightMap.h" // #199 - HeightMap application methods
|
#include "PyHeightMap.h" // #199 - HeightMap application methods
|
||||||
#include "PyShader.h" // #106: Shader support
|
#include "PyShader.h" // #106: Shader support
|
||||||
#include "PyUniformCollection.h" // #106: Uniform collection support
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||||
|
#include "PyMouseButton.h" // For MouseButton enum
|
||||||
|
#include "PyInputState.h" // For InputState enum
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath> // #142 - for std::floor, std::isnan
|
#include <cmath> // #142 - for std::floor, std::isnan
|
||||||
#include <cstring> // #150 - for strcmp
|
#include <cstring> // #150 - for strcmp
|
||||||
|
|
@ -681,69 +683,20 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No entity handled it, check if grid itself has handler
|
// No entity handled it, check if grid itself has handler
|
||||||
// #184: Also check for Python subclass (might have on_click method)
|
// #184: Also check for Python subclass (might have on_click or on_cell_click method)
|
||||||
if (click_callable || is_python_subclass) {
|
|
||||||
// #142 - Fire on_cell_click if we have the callback and clicked on a valid cell
|
|
||||||
if (on_cell_click_callable) {
|
|
||||||
int cell_x = static_cast<int>(std::floor(grid_x));
|
|
||||||
int cell_y = static_cast<int>(std::floor(grid_y));
|
|
||||||
|
|
||||||
// Only fire if within valid grid bounds
|
// Store clicked cell for later callback firing (with button/action from PyScene)
|
||||||
if (cell_x >= 0 && cell_x < this->grid_w && cell_y >= 0 && cell_y < this->grid_h) {
|
int cell_x = static_cast<int>(std::floor(grid_x));
|
||||||
// Create Vector object for cell position - must fetch finalized type from module
|
int cell_y = static_cast<int>(std::floor(grid_y));
|
||||||
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
if (cell_x >= 0 && cell_x < this->grid_w && cell_y >= 0 && cell_y < this->grid_h) {
|
||||||
if (vector_type) {
|
last_clicked_cell = sf::Vector2i(cell_x, cell_y);
|
||||||
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell_x, (float)cell_y);
|
} else {
|
||||||
Py_DECREF(vector_type);
|
last_clicked_cell = std::nullopt;
|
||||||
if (cell_pos) {
|
|
||||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
|
||||||
Py_DECREF(cell_pos);
|
|
||||||
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
|
|
||||||
Py_DECREF(args);
|
|
||||||
if (!result) {
|
|
||||||
std::cerr << "Cell click callback raised an exception:" << std::endl;
|
|
||||||
PyErr_Print();
|
|
||||||
PyErr_Clear();
|
|
||||||
} else {
|
|
||||||
Py_DECREF(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #142 - Even without click_callable, fire on_cell_click if present
|
// Return this if we have any handler (property callback, subclass method, or cell callback)
|
||||||
// Note: We fire the callback but DON'T return this, because PyScene::do_mouse_input
|
if (click_callable || is_python_subclass || on_cell_click_callable) {
|
||||||
// would try to call click_callable which doesn't exist
|
return this;
|
||||||
if (on_cell_click_callable) {
|
|
||||||
int cell_x = static_cast<int>(std::floor(grid_x));
|
|
||||||
int cell_y = static_cast<int>(std::floor(grid_y));
|
|
||||||
|
|
||||||
// Only fire if within valid grid bounds
|
|
||||||
if (cell_x >= 0 && cell_x < this->grid_w && cell_y >= 0 && cell_y < this->grid_h) {
|
|
||||||
// Create Vector object for cell position - must fetch finalized type from module
|
|
||||||
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
||||||
if (vector_type) {
|
|
||||||
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell_x, (float)cell_y);
|
|
||||||
Py_DECREF(vector_type);
|
|
||||||
if (cell_pos) {
|
|
||||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
|
||||||
Py_DECREF(cell_pos);
|
|
||||||
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
|
|
||||||
Py_DECREF(args);
|
|
||||||
if (!result) {
|
|
||||||
std::cerr << "Cell click callback raised an exception:" << std::endl;
|
|
||||||
PyErr_Print();
|
|
||||||
PyErr_Clear();
|
|
||||||
} else {
|
|
||||||
Py_DECREF(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Don't return this - no click_callable to call
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -2484,56 +2437,289 @@ sf::Vector2f UIGrid::getEffectiveCellSize() const {
|
||||||
return sf::Vector2f(cell_w * zoom, cell_h * zoom);
|
return sf::Vector2f(cell_w * zoom, cell_h * zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to convert button string to MouseButton enum value
|
||||||
|
static int buttonStringToEnum(const std::string& button) {
|
||||||
|
if (button == "left") return 0; // MouseButton.LEFT
|
||||||
|
if (button == "right") return 1; // MouseButton.RIGHT
|
||||||
|
if (button == "middle") return 2; // MouseButton.MIDDLE
|
||||||
|
if (button == "wheel_up") return 3; // MouseButton.WHEEL_UP
|
||||||
|
if (button == "wheel_down") return 4; // MouseButton.WHEEL_DOWN
|
||||||
|
return 0; // Default to LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert action string to InputState enum value
|
||||||
|
static int actionStringToEnum(const std::string& action) {
|
||||||
|
if (action == "start" || action == "pressed") return 0; // InputState.PRESSED
|
||||||
|
if (action == "end" || action == "released") return 1; // InputState.RELEASED
|
||||||
|
return 0; // Default to PRESSED
|
||||||
|
}
|
||||||
|
|
||||||
|
// #142 - Refresh cell callback cache for Python subclass method support
|
||||||
|
void UIGrid::refreshCellCallbackCache(PyObject* pyObj) {
|
||||||
|
if (!pyObj || !is_python_subclass) {
|
||||||
|
cell_callback_cache.valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the class's callback generation counter
|
||||||
|
PyObject* cls = (PyObject*)Py_TYPE(pyObj);
|
||||||
|
uint32_t current_gen = 0;
|
||||||
|
PyObject* gen_obj = PyObject_GetAttrString(cls, "_mcrf_callback_gen");
|
||||||
|
if (gen_obj) {
|
||||||
|
current_gen = static_cast<uint32_t>(PyLong_AsUnsignedLong(gen_obj));
|
||||||
|
Py_DECREF(gen_obj);
|
||||||
|
} else {
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cache is still valid
|
||||||
|
if (cell_callback_cache.valid && cell_callback_cache.generation == current_gen) {
|
||||||
|
return; // Cache is fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh cache - check for each cell callback method
|
||||||
|
cell_callback_cache.has_on_cell_click = false;
|
||||||
|
cell_callback_cache.has_on_cell_enter = false;
|
||||||
|
cell_callback_cache.has_on_cell_exit = false;
|
||||||
|
|
||||||
|
// Check class hierarchy for each method
|
||||||
|
PyTypeObject* type = Py_TYPE(pyObj);
|
||||||
|
while (type && type != &mcrfpydef::PyUIGridType && type != &PyBaseObject_Type) {
|
||||||
|
if (type->tp_dict) {
|
||||||
|
if (!cell_callback_cache.has_on_cell_click) {
|
||||||
|
PyObject* method = PyDict_GetItemString(type->tp_dict, "on_cell_click");
|
||||||
|
if (method && PyCallable_Check(method)) {
|
||||||
|
cell_callback_cache.has_on_cell_click = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cell_callback_cache.has_on_cell_enter) {
|
||||||
|
PyObject* method = PyDict_GetItemString(type->tp_dict, "on_cell_enter");
|
||||||
|
if (method && PyCallable_Check(method)) {
|
||||||
|
cell_callback_cache.has_on_cell_enter = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cell_callback_cache.has_on_cell_exit) {
|
||||||
|
PyObject* method = PyDict_GetItemString(type->tp_dict, "on_cell_exit");
|
||||||
|
if (method && PyCallable_Check(method)) {
|
||||||
|
cell_callback_cache.has_on_cell_exit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type = type->tp_base;
|
||||||
|
}
|
||||||
|
|
||||||
|
cell_callback_cache.generation = current_gen;
|
||||||
|
cell_callback_cache.valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create typed cell callback arguments: (Vector, MouseButton, InputState)
|
||||||
|
static PyObject* createCellCallbackArgs(sf::Vector2i cell, const std::string& button, const std::string& action) {
|
||||||
|
// Create Vector object for cell position
|
||||||
|
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||||
|
if (!vector_type) {
|
||||||
|
PyErr_Print();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell.x, (float)cell.y);
|
||||||
|
Py_DECREF(vector_type);
|
||||||
|
if (!cell_pos) {
|
||||||
|
PyErr_Print();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MouseButton enum
|
||||||
|
int button_val = buttonStringToEnum(button);
|
||||||
|
PyObject* button_enum = PyObject_CallFunction(PyMouseButton::mouse_button_enum_class, "i", button_val);
|
||||||
|
if (!button_enum) {
|
||||||
|
Py_DECREF(cell_pos);
|
||||||
|
PyErr_Print();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InputState enum
|
||||||
|
int action_val = actionStringToEnum(action);
|
||||||
|
PyObject* action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val);
|
||||||
|
if (!action_enum) {
|
||||||
|
Py_DECREF(cell_pos);
|
||||||
|
Py_DECREF(button_enum);
|
||||||
|
PyErr_Print();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* args = Py_BuildValue("(OOO)", cell_pos, button_enum, action_enum);
|
||||||
|
Py_DECREF(cell_pos);
|
||||||
|
Py_DECREF(button_enum);
|
||||||
|
Py_DECREF(action_enum);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire cell click callback with full signature (cell_pos, button, action)
|
||||||
|
bool UIGrid::fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action) {
|
||||||
|
// Try property-assigned callback first
|
||||||
|
if (on_cell_click_callable && !on_cell_click_callable->isNone()) {
|
||||||
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
|
if (args) {
|
||||||
|
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
|
||||||
|
Py_DECREF(args);
|
||||||
|
if (!result) {
|
||||||
|
std::cerr << "Cell click callback raised an exception:" << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
} else {
|
||||||
|
Py_DECREF(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Python subclass method
|
||||||
|
if (is_python_subclass) {
|
||||||
|
PyObject* pyObj = PythonObjectCache::getInstance().lookup(this->serial_number);
|
||||||
|
if (pyObj) {
|
||||||
|
refreshCellCallbackCache(pyObj);
|
||||||
|
if (cell_callback_cache.has_on_cell_click) {
|
||||||
|
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_click");
|
||||||
|
if (method && PyCallable_Check(method)) {
|
||||||
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
|
if (args) {
|
||||||
|
PyObject* result = PyObject_CallObject(method, args);
|
||||||
|
Py_DECREF(args);
|
||||||
|
Py_DECREF(method);
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
if (!result) {
|
||||||
|
std::cerr << "Cell click method raised an exception:" << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
} else {
|
||||||
|
Py_DECREF(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire cell enter callback with full signature (cell_pos, button, action)
|
||||||
|
bool UIGrid::fireCellEnter(sf::Vector2i cell, const std::string& button, const std::string& action) {
|
||||||
|
// Try property-assigned callback first
|
||||||
|
if (on_cell_enter_callable && !on_cell_enter_callable->isNone()) {
|
||||||
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
|
if (args) {
|
||||||
|
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
|
||||||
|
Py_DECREF(args);
|
||||||
|
if (!result) {
|
||||||
|
std::cerr << "Cell enter callback raised an exception:" << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
} else {
|
||||||
|
Py_DECREF(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Python subclass method
|
||||||
|
if (is_python_subclass) {
|
||||||
|
PyObject* pyObj = PythonObjectCache::getInstance().lookup(this->serial_number);
|
||||||
|
if (pyObj) {
|
||||||
|
refreshCellCallbackCache(pyObj);
|
||||||
|
if (cell_callback_cache.has_on_cell_enter) {
|
||||||
|
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_enter");
|
||||||
|
if (method && PyCallable_Check(method)) {
|
||||||
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
|
if (args) {
|
||||||
|
PyObject* result = PyObject_CallObject(method, args);
|
||||||
|
Py_DECREF(args);
|
||||||
|
Py_DECREF(method);
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
if (!result) {
|
||||||
|
std::cerr << "Cell enter method raised an exception:" << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
} else {
|
||||||
|
Py_DECREF(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire cell exit callback with full signature (cell_pos, button, action)
|
||||||
|
bool UIGrid::fireCellExit(sf::Vector2i cell, const std::string& button, const std::string& action) {
|
||||||
|
// Try property-assigned callback first
|
||||||
|
if (on_cell_exit_callable && !on_cell_exit_callable->isNone()) {
|
||||||
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
|
if (args) {
|
||||||
|
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
|
||||||
|
Py_DECREF(args);
|
||||||
|
if (!result) {
|
||||||
|
std::cerr << "Cell exit callback raised an exception:" << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
} else {
|
||||||
|
Py_DECREF(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Python subclass method
|
||||||
|
if (is_python_subclass) {
|
||||||
|
PyObject* pyObj = PythonObjectCache::getInstance().lookup(this->serial_number);
|
||||||
|
if (pyObj) {
|
||||||
|
refreshCellCallbackCache(pyObj);
|
||||||
|
if (cell_callback_cache.has_on_cell_exit) {
|
||||||
|
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_exit");
|
||||||
|
if (method && PyCallable_Check(method)) {
|
||||||
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
|
if (args) {
|
||||||
|
PyObject* result = PyObject_CallObject(method, args);
|
||||||
|
Py_DECREF(args);
|
||||||
|
Py_DECREF(method);
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
if (!result) {
|
||||||
|
std::cerr << "Cell exit method raised an exception:" << std::endl;
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
} else {
|
||||||
|
Py_DECREF(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// #142 - Update cell hover state and fire callbacks
|
// #142 - Update cell hover state and fire callbacks
|
||||||
void UIGrid::updateCellHover(sf::Vector2f mousepos) {
|
void UIGrid::updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action) {
|
||||||
auto new_cell = screenToCell(mousepos);
|
auto new_cell = screenToCell(mousepos);
|
||||||
|
|
||||||
// Check if cell changed
|
// Check if cell changed
|
||||||
if (new_cell != hovered_cell) {
|
if (new_cell != hovered_cell) {
|
||||||
// Fire exit callback for old cell
|
// Fire exit callback for old cell
|
||||||
if (hovered_cell.has_value() && on_cell_exit_callable) {
|
if (hovered_cell.has_value()) {
|
||||||
// Create Vector object for cell position - must fetch finalized type from module
|
fireCellExit(hovered_cell.value(), button, action);
|
||||||
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
||||||
if (vector_type) {
|
|
||||||
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)hovered_cell->x, (float)hovered_cell->y);
|
|
||||||
Py_DECREF(vector_type);
|
|
||||||
if (cell_pos) {
|
|
||||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
|
||||||
Py_DECREF(cell_pos);
|
|
||||||
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
|
|
||||||
Py_DECREF(args);
|
|
||||||
if (!result) {
|
|
||||||
std::cerr << "Cell exit callback raised an exception:" << std::endl;
|
|
||||||
PyErr_Print();
|
|
||||||
PyErr_Clear();
|
|
||||||
} else {
|
|
||||||
Py_DECREF(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire enter callback for new cell
|
// Fire enter callback for new cell
|
||||||
if (new_cell.has_value() && on_cell_enter_callable) {
|
if (new_cell.has_value()) {
|
||||||
// Create Vector object for cell position - must fetch finalized type from module
|
fireCellEnter(new_cell.value(), button, action);
|
||||||
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
||||||
if (vector_type) {
|
|
||||||
PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)new_cell->x, (float)new_cell->y);
|
|
||||||
Py_DECREF(vector_type);
|
|
||||||
if (cell_pos) {
|
|
||||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
|
||||||
Py_DECREF(cell_pos);
|
|
||||||
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
|
|
||||||
Py_DECREF(args);
|
|
||||||
if (!result) {
|
|
||||||
std::cerr << "Cell enter callback raised an exception:" << std::endl;
|
|
||||||
PyErr_Print();
|
|
||||||
PyErr_Clear();
|
|
||||||
} else {
|
|
||||||
Py_DECREF(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hovered_cell = new_cell;
|
hovered_cell = new_cell;
|
||||||
|
|
|
||||||
23
src/UIGrid.h
23
src/UIGrid.h
|
|
@ -136,6 +136,17 @@ public:
|
||||||
std::unique_ptr<PyClickCallable> on_cell_exit_callable;
|
std::unique_ptr<PyClickCallable> on_cell_exit_callable;
|
||||||
std::unique_ptr<PyClickCallable> on_cell_click_callable;
|
std::unique_ptr<PyClickCallable> on_cell_click_callable;
|
||||||
std::optional<sf::Vector2i> hovered_cell; // Currently hovered cell or nullopt
|
std::optional<sf::Vector2i> hovered_cell; // Currently hovered cell or nullopt
|
||||||
|
std::optional<sf::Vector2i> last_clicked_cell; // Cell clicked during click_at
|
||||||
|
|
||||||
|
// Grid-specific cell callback cache (separate from UIDrawable::CallbackCache)
|
||||||
|
struct CellCallbackCache {
|
||||||
|
uint32_t generation = 0;
|
||||||
|
bool valid = false;
|
||||||
|
bool has_on_cell_click = false;
|
||||||
|
bool has_on_cell_enter = false;
|
||||||
|
bool has_on_cell_exit = false;
|
||||||
|
};
|
||||||
|
CellCallbackCache cell_callback_cache;
|
||||||
|
|
||||||
// #142 - Cell coordinate conversion (screen pos -> cell coords)
|
// #142 - Cell coordinate conversion (screen pos -> cell coords)
|
||||||
std::optional<sf::Vector2i> screenToCell(sf::Vector2f screen_pos) const;
|
std::optional<sf::Vector2i> screenToCell(sf::Vector2f screen_pos) const;
|
||||||
|
|
@ -144,7 +155,17 @@ public:
|
||||||
sf::Vector2f getEffectiveCellSize() const;
|
sf::Vector2f getEffectiveCellSize() const;
|
||||||
|
|
||||||
// #142 - Update cell hover state (called from PyScene)
|
// #142 - Update cell hover state (called from PyScene)
|
||||||
void updateCellHover(sf::Vector2f mousepos);
|
// Now takes button/action for consistent callback signatures
|
||||||
|
void updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action);
|
||||||
|
|
||||||
|
// Fire cell callbacks with full signature (cell_pos, button, action)
|
||||||
|
// Returns true if a callback was fired
|
||||||
|
bool fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action);
|
||||||
|
bool fireCellEnter(sf::Vector2i cell, const std::string& button, const std::string& action);
|
||||||
|
bool fireCellExit(sf::Vector2i cell, const std::string& button, const std::string& action);
|
||||||
|
|
||||||
|
// Refresh cell callback cache for subclass method support
|
||||||
|
void refreshCellCallbackCache(PyObject* pyObj);
|
||||||
|
|
||||||
// Property system for animations
|
// Property system for animations
|
||||||
bool setProperty(const std::string& name, float value) override;
|
bool setProperty(const std::string& name, float value) override;
|
||||||
|
|
|
||||||
248
tests/unit/test_callback_enums.py
Normal file
248
tests/unit/test_callback_enums.py
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test unified callback signatures with enum types (#184)
|
||||||
|
|
||||||
|
This tests that all callbacks now use consistent typed arguments:
|
||||||
|
- Drawable callbacks: (pos: Vector, button: MouseButton, action: InputState)
|
||||||
|
- Grid cell callbacks: (cell_pos: Vector, button: MouseButton, action: InputState)
|
||||||
|
- Scene on_key: (key: Key, action: InputState)
|
||||||
|
"""
|
||||||
|
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: Verify enum types exist and are accessible
|
||||||
|
# ==============================================================================
|
||||||
|
print("\n=== Callback Enum Signature Tests ===\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check MouseButton enum
|
||||||
|
assert hasattr(mcrfpy, 'MouseButton'), "MouseButton enum should exist"
|
||||||
|
assert hasattr(mcrfpy.MouseButton, 'LEFT'), "MouseButton.LEFT should exist"
|
||||||
|
assert hasattr(mcrfpy.MouseButton, 'RIGHT'), "MouseButton.RIGHT should exist"
|
||||||
|
|
||||||
|
# Check InputState enum
|
||||||
|
assert hasattr(mcrfpy, 'InputState'), "InputState enum should exist"
|
||||||
|
assert hasattr(mcrfpy.InputState, 'PRESSED'), "InputState.PRESSED should exist"
|
||||||
|
assert hasattr(mcrfpy.InputState, 'RELEASED'), "InputState.RELEASED should exist"
|
||||||
|
|
||||||
|
# Check Key enum
|
||||||
|
assert hasattr(mcrfpy, 'Key'), "Key enum should exist"
|
||||||
|
assert hasattr(mcrfpy.Key, 'A'), "Key.A should exist"
|
||||||
|
assert hasattr(mcrfpy.Key, 'ESCAPE'), "Key.ESCAPE should exist"
|
||||||
|
|
||||||
|
test_passed("Enum types exist and are accessible")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("Enum types exist and are accessible", e)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Test 2: Grid cell callback with enum signature (subclass method)
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
class GridWithCellCallbacks(mcrfpy.Grid):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.cell_events = []
|
||||||
|
|
||||||
|
def on_cell_click(self, cell_pos, button, action):
|
||||||
|
# Verify types
|
||||||
|
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
|
||||||
|
self.cell_events.append(('click', cell_pos.x, cell_pos.y, button, action))
|
||||||
|
|
||||||
|
def on_cell_enter(self, cell_pos, button, action):
|
||||||
|
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
|
||||||
|
self.cell_events.append(('enter', cell_pos.x, cell_pos.y, button, action))
|
||||||
|
|
||||||
|
def on_cell_exit(self, cell_pos, button, action):
|
||||||
|
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
|
||||||
|
self.cell_events.append(('exit', cell_pos.x, cell_pos.y, button, action))
|
||||||
|
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = GridWithCellCallbacks(grid_size=(5, 5), texture=texture, pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
|
# Verify grid is subclass
|
||||||
|
assert isinstance(grid, mcrfpy.Grid), "Should be Grid instance"
|
||||||
|
assert type(grid) is not mcrfpy.Grid, "Should be subclass"
|
||||||
|
|
||||||
|
# Manually call methods to verify signature works
|
||||||
|
grid.on_cell_click(mcrfpy.Vector(1.0, 2.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
|
grid.on_cell_enter(mcrfpy.Vector(3.0, 4.0), mcrfpy.MouseButton.RIGHT, mcrfpy.InputState.RELEASED)
|
||||||
|
grid.on_cell_exit(mcrfpy.Vector(5.0, 6.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
|
|
||||||
|
assert len(grid.cell_events) == 3, f"Should have 3 events, got {len(grid.cell_events)}"
|
||||||
|
assert grid.cell_events[0][0] == 'click', "First event should be click"
|
||||||
|
assert grid.cell_events[1][0] == 'enter', "Second event should be enter"
|
||||||
|
assert grid.cell_events[2][0] == 'exit', "Third event should be exit"
|
||||||
|
|
||||||
|
test_passed("Grid subclass cell callbacks with enum signature")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("Grid subclass cell callbacks with enum signature", e)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Test 3: Grid cell callback with property-assigned callable
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), texture=texture, pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
|
cell_events = []
|
||||||
|
|
||||||
|
def on_cell_click_handler(cell_pos, button, action):
|
||||||
|
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
|
||||||
|
cell_events.append(('click', cell_pos.x, cell_pos.y, button, action))
|
||||||
|
|
||||||
|
grid.on_cell_click = on_cell_click_handler
|
||||||
|
|
||||||
|
# Manually call to test (normally engine would call this)
|
||||||
|
grid.on_cell_click(mcrfpy.Vector(1.0, 2.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
|
|
||||||
|
assert len(cell_events) == 1, f"Should have 1 event, got {len(cell_events)}"
|
||||||
|
assert cell_events[0][3] == mcrfpy.MouseButton.LEFT, "Button should be MouseButton.LEFT"
|
||||||
|
assert cell_events[0][4] == mcrfpy.InputState.PRESSED, "Action should be InputState.PRESSED"
|
||||||
|
|
||||||
|
test_passed("Grid property cell callback with enum signature")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("Grid property cell callback with enum signature", e)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Test 4: Scene on_key callback with enum signature (subclass method)
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
class MyScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.key_events = []
|
||||||
|
|
||||||
|
def on_key(self, key, action):
|
||||||
|
# Verify types - key should be Key enum, action should be InputState enum
|
||||||
|
self.key_events.append((key, action))
|
||||||
|
|
||||||
|
scene = MyScene("test_key_enum_scene")
|
||||||
|
|
||||||
|
# Manually call to test signature (normally engine would call this)
|
||||||
|
scene.on_key(mcrfpy.Key.A, mcrfpy.InputState.PRESSED)
|
||||||
|
scene.on_key(mcrfpy.Key.ESCAPE, mcrfpy.InputState.RELEASED)
|
||||||
|
|
||||||
|
assert len(scene.key_events) == 2, f"Should have 2 events, got {len(scene.key_events)}"
|
||||||
|
assert scene.key_events[0][0] == mcrfpy.Key.A, f"First key should be Key.A, got {scene.key_events[0][0]}"
|
||||||
|
assert scene.key_events[0][1] == mcrfpy.InputState.PRESSED, f"First action should be PRESSED"
|
||||||
|
assert scene.key_events[1][0] == mcrfpy.Key.ESCAPE, f"Second key should be Key.ESCAPE"
|
||||||
|
assert scene.key_events[1][1] == mcrfpy.InputState.RELEASED, f"Second action should be RELEASED"
|
||||||
|
|
||||||
|
test_passed("Scene subclass on_key with enum signature")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("Scene subclass on_key with enum signature", e)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Test 5: Scene on_key callback with property-assigned callable
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
scene = mcrfpy.Scene("test_key_prop_scene")
|
||||||
|
|
||||||
|
key_events = []
|
||||||
|
|
||||||
|
def on_key_handler(key, action):
|
||||||
|
key_events.append((key, action))
|
||||||
|
|
||||||
|
scene.on_key = on_key_handler
|
||||||
|
|
||||||
|
# Manually call to test (normally engine would call this via the callable)
|
||||||
|
scene.on_key(mcrfpy.Key.SPACE, mcrfpy.InputState.PRESSED)
|
||||||
|
|
||||||
|
assert len(key_events) == 1, f"Should have 1 event, got {len(key_events)}"
|
||||||
|
# Note: When calling directly on Python side, we pass enums directly
|
||||||
|
# The engine conversion happens internally when calling through C++
|
||||||
|
|
||||||
|
test_passed("Scene property on_key accepts enum args")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("Scene property on_key accepts enum args", e)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Test 6: Verify MouseButton enum values
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
# Verify the enum values are usable in comparisons
|
||||||
|
left = mcrfpy.MouseButton.LEFT
|
||||||
|
right = mcrfpy.MouseButton.RIGHT
|
||||||
|
|
||||||
|
assert left != right, "LEFT should not equal RIGHT"
|
||||||
|
assert left == mcrfpy.MouseButton.LEFT, "LEFT should equal itself"
|
||||||
|
|
||||||
|
# Verify we can use in conditions
|
||||||
|
def check_button(button):
|
||||||
|
if button == mcrfpy.MouseButton.LEFT:
|
||||||
|
return "left"
|
||||||
|
elif button == mcrfpy.MouseButton.RIGHT:
|
||||||
|
return "right"
|
||||||
|
return "other"
|
||||||
|
|
||||||
|
assert check_button(mcrfpy.MouseButton.LEFT) == "left"
|
||||||
|
assert check_button(mcrfpy.MouseButton.RIGHT) == "right"
|
||||||
|
|
||||||
|
test_passed("MouseButton enum values work correctly")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("MouseButton enum values work correctly", e)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Test 7: Verify InputState enum values
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
pressed = mcrfpy.InputState.PRESSED
|
||||||
|
released = mcrfpy.InputState.RELEASED
|
||||||
|
|
||||||
|
assert pressed != released, "PRESSED should not equal RELEASED"
|
||||||
|
assert pressed == mcrfpy.InputState.PRESSED, "PRESSED should equal itself"
|
||||||
|
|
||||||
|
test_passed("InputState enum values work correctly")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("InputState enum values work correctly", e)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Test 8: Verify Key enum values
|
||||||
|
# ==============================================================================
|
||||||
|
try:
|
||||||
|
a_key = mcrfpy.Key.A
|
||||||
|
esc_key = mcrfpy.Key.ESCAPE
|
||||||
|
|
||||||
|
assert a_key != esc_key, "A should not equal ESCAPE"
|
||||||
|
assert a_key == mcrfpy.Key.A, "A should equal itself"
|
||||||
|
|
||||||
|
# Verify various keys exist
|
||||||
|
assert hasattr(mcrfpy.Key, 'SPACE'), "SPACE should exist"
|
||||||
|
assert hasattr(mcrfpy.Key, 'ENTER'), "ENTER should exist"
|
||||||
|
assert hasattr(mcrfpy.Key, 'UP'), "UP should exist"
|
||||||
|
assert hasattr(mcrfpy.Key, 'DOWN'), "DOWN should exist"
|
||||||
|
|
||||||
|
test_passed("Key enum values work correctly")
|
||||||
|
except Exception as e:
|
||||||
|
test_failed("Key enum values work correctly", 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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue