Simplify on_enter/on_exit callbacks to position-only signature
BREAKING CHANGE: Hover callbacks now take only (pos) instead of (pos, button, action) - Add PyHoverCallable class for on_enter/on_exit/on_move callbacks (position-only) - Add PyCellHoverCallable class for on_cell_enter/on_cell_exit callbacks - Change UIDrawable member types from PyClickCallable to PyHoverCallable - Update PyScene::do_mouse_hover() to call hover callbacks with only position - Add tryCallPythonMethod overload for position-only subclass method calls - Update UIGrid::fireCellEnter/fireCellExit to use position-only signature - Update all tests for new callback signatures New callback signatures: | Callback | Old | New | |----------------|--------------------------|------------| | on_enter | (pos, button, action) | (pos) | | on_exit | (pos, button, action) | (pos) | | on_move | (pos, button, action) | (pos) | | on_cell_enter | (cell_pos, button, action)| (cell_pos)| | on_cell_exit | (cell_pos, button, action)| (cell_pos)| | on_click | unchanged | unchanged | | on_cell_click | unchanged | unchanged | closes #230 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e14f3cb9fc
commit
2daebc84b5
12 changed files with 598 additions and 71 deletions
|
|
@ -186,3 +186,123 @@ void PyKeyCallable::call(std::string key, std::string action)
|
|||
std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// #230 - PyHoverCallable implementation (position-only for on_enter/on_exit/on_move)
|
||||
PyHoverCallable::PyHoverCallable(PyObject* _target)
|
||||
: PyCallable(_target)
|
||||
{}
|
||||
|
||||
PyHoverCallable::PyHoverCallable()
|
||||
: PyCallable(Py_None)
|
||||
{}
|
||||
|
||||
void PyHoverCallable::call(sf::Vector2f mousepos)
|
||||
{
|
||||
if (target == Py_None || target == NULL) return;
|
||||
|
||||
// Create a Vector object for the position
|
||||
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!vector_type) {
|
||||
std::cerr << "Failed to get Vector type for hover callback" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
return;
|
||||
}
|
||||
PyObject* pos = PyObject_CallFunction(vector_type, "ff", mousepos.x, mousepos.y);
|
||||
Py_DECREF(vector_type);
|
||||
if (!pos) {
|
||||
std::cerr << "Failed to create Vector object for hover callback" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// #230 - Hover callbacks take only (pos), not (pos, button, action)
|
||||
PyObject* args = Py_BuildValue("(O)", pos);
|
||||
Py_DECREF(pos);
|
||||
|
||||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
Py_DECREF(args);
|
||||
if (!retval)
|
||||
{
|
||||
std::cerr << "Hover callback raised an exception:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "HoverCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
Py_DECREF(retval);
|
||||
} else {
|
||||
Py_DECREF(retval);
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* PyHoverCallable::borrow()
|
||||
{
|
||||
return target;
|
||||
}
|
||||
|
||||
// #230 - PyCellHoverCallable implementation (cell position-only for on_cell_enter/on_cell_exit)
|
||||
PyCellHoverCallable::PyCellHoverCallable(PyObject* _target)
|
||||
: PyCallable(_target)
|
||||
{}
|
||||
|
||||
PyCellHoverCallable::PyCellHoverCallable()
|
||||
: PyCallable(Py_None)
|
||||
{}
|
||||
|
||||
void PyCellHoverCallable::call(sf::Vector2i cellpos)
|
||||
{
|
||||
if (target == Py_None || target == NULL) return;
|
||||
|
||||
// Create a Vector object for the cell position
|
||||
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!vector_type) {
|
||||
std::cerr << "Failed to get Vector type for cell hover callback" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
return;
|
||||
}
|
||||
PyObject* pos = PyObject_CallFunction(vector_type, "ii", cellpos.x, cellpos.y);
|
||||
Py_DECREF(vector_type);
|
||||
if (!pos) {
|
||||
std::cerr << "Failed to create Vector object for cell hover callback" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// #230 - Cell hover callbacks take only (cell_pos), not (cell_pos, button, action)
|
||||
PyObject* args = Py_BuildValue("(O)", pos);
|
||||
Py_DECREF(pos);
|
||||
|
||||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
Py_DECREF(args);
|
||||
if (!retval)
|
||||
{
|
||||
std::cerr << "Cell hover callback raised an exception:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "CellHoverCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
Py_DECREF(retval);
|
||||
} else {
|
||||
Py_DECREF(retval);
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* PyCellHoverCallable::borrow()
|
||||
{
|
||||
return target;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,3 +39,33 @@ public:
|
|||
PyKeyCallable(PyObject*);
|
||||
PyKeyCallable();
|
||||
};
|
||||
|
||||
// #230 - Hover callbacks (on_enter, on_exit, on_move) take only position
|
||||
class PyHoverCallable: public PyCallable
|
||||
{
|
||||
public:
|
||||
void call(sf::Vector2f mousepos);
|
||||
PyObject* borrow();
|
||||
PyHoverCallable(PyObject*);
|
||||
PyHoverCallable();
|
||||
PyHoverCallable(const PyHoverCallable& other) : PyCallable(other) {}
|
||||
PyHoverCallable& operator=(const PyHoverCallable& other) {
|
||||
PyCallable::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// #230 - Cell hover callbacks (on_cell_enter, on_cell_exit) take only cell position
|
||||
class PyCellHoverCallable: public PyCallable
|
||||
{
|
||||
public:
|
||||
void call(sf::Vector2i cellpos);
|
||||
PyObject* borrow();
|
||||
PyCellHoverCallable(PyObject*);
|
||||
PyCellHoverCallable();
|
||||
PyCellHoverCallable(const PyCellHoverCallable& other) : PyCallable(other) {}
|
||||
PyCellHoverCallable& operator=(const PyCellHoverCallable& other) {
|
||||
PyCallable::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -127,6 +127,81 @@ static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name,
|
|||
return called;
|
||||
}
|
||||
|
||||
// #230: Overload for hover events that take only position (no button/action)
|
||||
static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name,
|
||||
sf::Vector2f mousepos) {
|
||||
if (!drawable->is_python_subclass) return false;
|
||||
|
||||
PyObject* pyObj = PythonObjectCache::getInstance().lookup(drawable->serial_number);
|
||||
if (!pyObj) return false;
|
||||
|
||||
// Check and refresh cache if needed
|
||||
PyObject* type = (PyObject*)Py_TYPE(pyObj);
|
||||
if (!drawable->isCallbackCacheValid(type)) {
|
||||
drawable->refreshCallbackCache(pyObj);
|
||||
}
|
||||
|
||||
// Check if this method exists in the cache
|
||||
bool has_method = false;
|
||||
if (strcmp(method_name, "on_enter") == 0) {
|
||||
has_method = drawable->callback_cache.has_on_enter;
|
||||
} else if (strcmp(method_name, "on_exit") == 0) {
|
||||
has_method = drawable->callback_cache.has_on_exit;
|
||||
} else if (strcmp(method_name, "on_move") == 0) {
|
||||
has_method = drawable->callback_cache.has_on_move;
|
||||
}
|
||||
|
||||
if (!has_method) {
|
||||
Py_DECREF(pyObj);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the method
|
||||
PyObject* method = PyObject_GetAttrString(pyObj, method_name);
|
||||
bool called = false;
|
||||
|
||||
if (method && PyCallable_Check(method) && method != Py_None) {
|
||||
// Create Vector object for position
|
||||
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!vector_type) {
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
Py_DECREF(pyObj);
|
||||
return false;
|
||||
}
|
||||
PyObject* pos = PyObject_CallFunction(vector_type, "ff", mousepos.x, mousepos.y);
|
||||
Py_DECREF(vector_type);
|
||||
if (!pos) {
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
Py_DECREF(pyObj);
|
||||
return false;
|
||||
}
|
||||
|
||||
// #230: Call with just (Vector) signature for hover events
|
||||
PyObject* args = Py_BuildValue("(O)", pos);
|
||||
Py_DECREF(pos);
|
||||
|
||||
PyObject* result = PyObject_Call(method, args, NULL);
|
||||
Py_DECREF(args);
|
||||
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
called = true;
|
||||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
}
|
||||
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
Py_DECREF(pyObj);
|
||||
|
||||
return called;
|
||||
}
|
||||
|
||||
// Check if a UIDrawable can potentially handle an event
|
||||
// (has either a callable property OR is a Python subclass that might have a method)
|
||||
static bool canHandleEvent(UIDrawable* drawable, const char* event_type) {
|
||||
|
|
@ -274,30 +349,33 @@ void PyScene::do_mouse_hover(int x, int y)
|
|||
// Mouse entered
|
||||
drawable->hovered = true;
|
||||
// #184: Try property-assigned callable first, then Python subclass method
|
||||
// #230: Hover callbacks now take only (pos)
|
||||
if (drawable->on_enter_callable && !drawable->on_enter_callable->isNone()) {
|
||||
drawable->on_enter_callable->call(mousepos, "enter", "start");
|
||||
drawable->on_enter_callable->call(mousepos);
|
||||
} else if (drawable->is_python_subclass) {
|
||||
tryCallPythonMethod(drawable, "on_enter", mousepos, "enter", "start");
|
||||
tryCallPythonMethod(drawable, "on_enter", mousepos);
|
||||
}
|
||||
} else if (!is_inside && was_hovered) {
|
||||
// Mouse exited
|
||||
drawable->hovered = false;
|
||||
// #184: Try property-assigned callable first, then Python subclass method
|
||||
// #230: Hover callbacks now take only (pos)
|
||||
if (drawable->on_exit_callable && !drawable->on_exit_callable->isNone()) {
|
||||
drawable->on_exit_callable->call(mousepos, "exit", "start");
|
||||
drawable->on_exit_callable->call(mousepos);
|
||||
} else if (drawable->is_python_subclass) {
|
||||
tryCallPythonMethod(drawable, "on_exit", mousepos, "exit", "start");
|
||||
tryCallPythonMethod(drawable, "on_exit", mousepos);
|
||||
}
|
||||
}
|
||||
|
||||
// #141 - Fire on_move if mouse is inside and has a move/on_move callback
|
||||
// #184: Try property-assigned callable first, then Python subclass method
|
||||
// #230: Hover callbacks now take only (pos)
|
||||
// Check is_python_subclass before function call to avoid overhead on hot path
|
||||
if (is_inside) {
|
||||
if (drawable->on_move_callable && !drawable->on_move_callable->isNone()) {
|
||||
drawable->on_move_callable->call(mousepos, "move", "start");
|
||||
drawable->on_move_callable->call(mousepos);
|
||||
} else if (drawable->is_python_subclass) {
|
||||
tryCallPythonMethod(drawable, "on_move", mousepos, "move", "start");
|
||||
tryCallPythonMethod(drawable, "on_move", mousepos);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,16 +60,16 @@ UIDrawable::UIDrawable(const UIDrawable& other)
|
|||
if (other.click_callable) {
|
||||
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
||||
}
|
||||
// #140 - Deep copy enter/exit callables
|
||||
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
|
||||
if (other.on_enter_callable) {
|
||||
on_enter_callable = std::make_unique<PyClickCallable>(*other.on_enter_callable);
|
||||
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
|
||||
}
|
||||
if (other.on_exit_callable) {
|
||||
on_exit_callable = std::make_unique<PyClickCallable>(*other.on_exit_callable);
|
||||
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
|
||||
}
|
||||
// #141 - Deep copy move callable
|
||||
// #141, #230 - Deep copy move callable (now PyHoverCallable)
|
||||
if (other.on_move_callable) {
|
||||
on_move_callable = std::make_unique<PyClickCallable>(*other.on_move_callable);
|
||||
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
|
||||
}
|
||||
|
||||
// Deep copy render texture if needed
|
||||
|
|
@ -100,20 +100,20 @@ UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
|
|||
} else {
|
||||
click_callable.reset();
|
||||
}
|
||||
// #140 - Deep copy enter/exit callables
|
||||
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
|
||||
if (other.on_enter_callable) {
|
||||
on_enter_callable = std::make_unique<PyClickCallable>(*other.on_enter_callable);
|
||||
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
|
||||
} else {
|
||||
on_enter_callable.reset();
|
||||
}
|
||||
if (other.on_exit_callable) {
|
||||
on_exit_callable = std::make_unique<PyClickCallable>(*other.on_exit_callable);
|
||||
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
|
||||
} else {
|
||||
on_exit_callable.reset();
|
||||
}
|
||||
// #141 - Deep copy move callable
|
||||
// #141, #230 - Deep copy move callable (now PyHoverCallable)
|
||||
if (other.on_move_callable) {
|
||||
on_move_callable = std::make_unique<PyClickCallable>(*other.on_move_callable);
|
||||
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
|
||||
} else {
|
||||
on_move_callable.reset();
|
||||
}
|
||||
|
|
@ -311,10 +311,10 @@ void UIDrawable::click_register(PyObject* callable)
|
|||
click_callable = std::make_unique<PyClickCallable>(callable);
|
||||
}
|
||||
|
||||
// #140 - Mouse enter/exit callback registration
|
||||
// #140, #230 - Mouse enter/exit callback registration (now PyHoverCallable)
|
||||
void UIDrawable::on_enter_register(PyObject* callable)
|
||||
{
|
||||
on_enter_callable = std::make_unique<PyClickCallable>(callable);
|
||||
on_enter_callable = std::make_unique<PyHoverCallable>(callable);
|
||||
}
|
||||
|
||||
void UIDrawable::on_enter_unregister()
|
||||
|
|
@ -324,7 +324,7 @@ void UIDrawable::on_enter_unregister()
|
|||
|
||||
void UIDrawable::on_exit_register(PyObject* callable)
|
||||
{
|
||||
on_exit_callable = std::make_unique<PyClickCallable>(callable);
|
||||
on_exit_callable = std::make_unique<PyHoverCallable>(callable);
|
||||
}
|
||||
|
||||
void UIDrawable::on_exit_unregister()
|
||||
|
|
@ -332,10 +332,10 @@ void UIDrawable::on_exit_unregister()
|
|||
on_exit_callable.reset();
|
||||
}
|
||||
|
||||
// #141 - Mouse move callback registration
|
||||
// #141, #230 - Mouse move callback registration (now PyHoverCallable)
|
||||
void UIDrawable::on_move_register(PyObject* callable)
|
||||
{
|
||||
on_move_callable = std::make_unique<PyClickCallable>(callable);
|
||||
on_move_callable = std::make_unique<PyHoverCallable>(callable);
|
||||
}
|
||||
|
||||
void UIDrawable::on_move_unregister()
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ public:
|
|||
|
||||
// Mouse input handling - callable objects for click, enter, exit, move events
|
||||
std::unique_ptr<PyClickCallable> click_callable;
|
||||
std::unique_ptr<PyClickCallable> on_enter_callable; // #140
|
||||
std::unique_ptr<PyClickCallable> on_exit_callable; // #140
|
||||
std::unique_ptr<PyClickCallable> on_move_callable; // #141
|
||||
std::unique_ptr<PyHoverCallable> on_enter_callable; // #140, #230 - position-only
|
||||
std::unique_ptr<PyHoverCallable> on_exit_callable; // #140, #230 - position-only
|
||||
std::unique_ptr<PyHoverCallable> on_move_callable; // #141, #230 - position-only
|
||||
|
||||
virtual UIDrawable* click_at(sf::Vector2f point) = 0;
|
||||
void click_register(PyObject*);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue