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 "PyMouseButton.h"
|
||||
#include "PyInputState.h"
|
||||
#include "PyKey.h"
|
||||
|
||||
PyCallable::PyCallable(PyObject* _target)
|
||||
{
|
||||
|
|
@ -143,7 +144,32 @@ PyKeyCallable::PyKeyCallable()
|
|||
void PyKeyCallable::call(std::string key, std::string action)
|
||||
{
|
||||
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);
|
||||
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)
|
||||
if (target->click_callable && !target->click_callable->isNone()) {
|
||||
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
|
||||
}
|
||||
|
||||
// #184: Try Python subclass method
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// (This maintains consistent behavior for subclasses that don't define on_click)
|
||||
if (target->is_python_subclass) {
|
||||
|
|
@ -286,7 +315,8 @@ void PyScene::do_mouse_hover(int x, int y)
|
|||
auto grid = static_cast<UIGrid*>(drawable);
|
||||
|
||||
// #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) {
|
||||
for (auto& child : *grid->children) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyTransition.h"
|
||||
#include "PyKey.h"
|
||||
#include "PyInputState.h"
|
||||
#include <iostream>
|
||||
|
||||
// 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
|
||||
// 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)
|
||||
// 1. Subclass methods: class MyScene(Scene): def on_key(self, key, action): ...
|
||||
// 2. Instance attributes: ts.on_key = lambda k, a: ... (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());
|
||||
// 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();
|
||||
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) {
|
||||
Py_DECREF(result);
|
||||
} else {
|
||||
|
|
@ -485,7 +513,7 @@ PyGetSetDef PySceneClass::getsetters[] = {
|
|||
"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,
|
||||
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},
|
||||
{NULL}
|
||||
};
|
||||
|
|
|
|||
388
src/UIGrid.cpp
388
src/UIGrid.cpp
|
|
@ -13,6 +13,8 @@
|
|||
#include "PyHeightMap.h" // #199 - HeightMap application methods
|
||||
#include "PyShader.h" // #106: Shader support
|
||||
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||
#include "PyMouseButton.h" // For MouseButton enum
|
||||
#include "PyInputState.h" // For InputState enum
|
||||
#include <algorithm>
|
||||
#include <cmath> // #142 - for std::floor, std::isnan
|
||||
#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
|
||||
// #184: Also check for Python subclass (might have on_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));
|
||||
// #184: Also check for Python subclass (might have on_click or on_cell_click method)
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
// Store clicked cell for later callback firing (with button/action from PyScene)
|
||||
int cell_x = static_cast<int>(std::floor(grid_x));
|
||||
int cell_y = static_cast<int>(std::floor(grid_y));
|
||||
if (cell_x >= 0 && cell_x < this->grid_w && cell_y >= 0 && cell_y < this->grid_h) {
|
||||
last_clicked_cell = sf::Vector2i(cell_x, cell_y);
|
||||
} else {
|
||||
last_clicked_cell = std::nullopt;
|
||||
}
|
||||
|
||||
// #142 - Even without click_callable, fire on_cell_click if present
|
||||
// Note: We fire the callback but DON'T return this, because PyScene::do_mouse_input
|
||||
// would try to call click_callable which doesn't exist
|
||||
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 this if we have any handler (property callback, subclass method, or cell callback)
|
||||
if (click_callable || is_python_subclass || on_cell_click_callable) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
|
@ -2484,56 +2437,289 @@ sf::Vector2f UIGrid::getEffectiveCellSize() const {
|
|||
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
|
||||
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);
|
||||
|
||||
// Check if cell changed
|
||||
if (new_cell != hovered_cell) {
|
||||
// Fire exit callback for old cell
|
||||
if (hovered_cell.has_value() && on_cell_exit_callable) {
|
||||
// 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)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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hovered_cell.has_value()) {
|
||||
fireCellExit(hovered_cell.value(), button, action);
|
||||
}
|
||||
|
||||
// Fire enter callback for new cell
|
||||
if (new_cell.has_value() && on_cell_enter_callable) {
|
||||
// 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)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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (new_cell.has_value()) {
|
||||
fireCellEnter(new_cell.value(), button, action);
|
||||
}
|
||||
|
||||
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_click_callable;
|
||||
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)
|
||||
std::optional<sf::Vector2i> screenToCell(sf::Vector2f screen_pos) const;
|
||||
|
|
@ -144,7 +155,17 @@ public:
|
|||
sf::Vector2f getEffectiveCellSize() const;
|
||||
|
||||
// #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
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue