Compare commits
No commits in common. "2daebc84b5e11f949007a217dc0a2da89b4e455c" and "d12bfd224cf3c22a99b0870bfd718a32ca7fae51" have entirely different histories.
2daebc84b5
...
d12bfd224c
16 changed files with 132 additions and 919 deletions
|
|
@ -5,14 +5,6 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
// #229 - Includes for animation callback target conversion
|
|
||||||
#include "UIFrame.h"
|
|
||||||
#include "UICaption.h"
|
|
||||||
#include "UISprite.h"
|
|
||||||
#include "UIGrid.h"
|
|
||||||
#include "UILine.h"
|
|
||||||
#include "UICircle.h"
|
|
||||||
#include "UIArc.h"
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
@ -403,185 +395,6 @@ void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
|
||||||
}, value);
|
}, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #229 - Helper to convert UIDrawable target to Python object
|
|
||||||
static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|
||||||
if (!drawable) {
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
if (drawable->serial_number != 0) {
|
|
||||||
PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number);
|
|
||||||
if (cached) {
|
|
||||||
return cached; // Already INCREF'd by lookup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PyTypeObject* type = nullptr;
|
|
||||||
PyObject* obj = nullptr;
|
|
||||||
|
|
||||||
switch (drawable->derived_type()) {
|
|
||||||
case PyObjectsEnum::UIFRAME:
|
|
||||||
{
|
|
||||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
|
|
||||||
if (!type) return nullptr;
|
|
||||||
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
|
||||||
if (pyObj) {
|
|
||||||
pyObj->data = std::static_pointer_cast<UIFrame>(drawable);
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
}
|
|
||||||
obj = (PyObject*)pyObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PyObjectsEnum::UICAPTION:
|
|
||||||
{
|
|
||||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
|
|
||||||
if (!type) return nullptr;
|
|
||||||
auto pyObj = (PyUICaptionObject*)type->tp_alloc(type, 0);
|
|
||||||
if (pyObj) {
|
|
||||||
pyObj->data = std::static_pointer_cast<UICaption>(drawable);
|
|
||||||
pyObj->font = nullptr;
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
}
|
|
||||||
obj = (PyObject*)pyObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PyObjectsEnum::UISPRITE:
|
|
||||||
{
|
|
||||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
|
|
||||||
if (!type) return nullptr;
|
|
||||||
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
|
|
||||||
if (pyObj) {
|
|
||||||
pyObj->data = std::static_pointer_cast<UISprite>(drawable);
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
}
|
|
||||||
obj = (PyObject*)pyObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PyObjectsEnum::UIGRID:
|
|
||||||
{
|
|
||||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
|
||||||
if (!type) return nullptr;
|
|
||||||
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
|
|
||||||
if (pyObj) {
|
|
||||||
pyObj->data = std::static_pointer_cast<UIGrid>(drawable);
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
}
|
|
||||||
obj = (PyObject*)pyObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PyObjectsEnum::UILINE:
|
|
||||||
{
|
|
||||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line");
|
|
||||||
if (!type) return nullptr;
|
|
||||||
auto pyObj = (PyUILineObject*)type->tp_alloc(type, 0);
|
|
||||||
if (pyObj) {
|
|
||||||
pyObj->data = std::static_pointer_cast<UILine>(drawable);
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
}
|
|
||||||
obj = (PyObject*)pyObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PyObjectsEnum::UICIRCLE:
|
|
||||||
{
|
|
||||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle");
|
|
||||||
if (!type) return nullptr;
|
|
||||||
auto pyObj = (PyUICircleObject*)type->tp_alloc(type, 0);
|
|
||||||
if (pyObj) {
|
|
||||||
pyObj->data = std::static_pointer_cast<UICircle>(drawable);
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
}
|
|
||||||
obj = (PyObject*)pyObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PyObjectsEnum::UIARC:
|
|
||||||
{
|
|
||||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc");
|
|
||||||
if (!type) return nullptr;
|
|
||||||
auto pyObj = (PyUIArcObject*)type->tp_alloc(type, 0);
|
|
||||||
if (pyObj) {
|
|
||||||
pyObj->data = std::static_pointer_cast<UIArc>(drawable);
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
}
|
|
||||||
obj = (PyObject*)pyObj;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type) {
|
|
||||||
Py_DECREF(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj ? obj : Py_None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// #229 - Helper to convert UIEntity target to Python object
|
|
||||||
static PyObject* convertEntityToPython(std::shared_ptr<UIEntity> entity) {
|
|
||||||
if (!entity) {
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
if (entity->serial_number != 0) {
|
|
||||||
PyObject* cached = PythonObjectCache::getInstance().lookup(entity->serial_number);
|
|
||||||
if (cached) {
|
|
||||||
return cached; // Already INCREF'd by lookup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
||||||
if (!type) {
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto pyObj = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
||||||
Py_DECREF(type);
|
|
||||||
|
|
||||||
if (!pyObj) {
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
pyObj->data = entity;
|
|
||||||
pyObj->weakreflist = NULL;
|
|
||||||
|
|
||||||
return (PyObject*)pyObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
// #229 - Helper to convert AnimationValue to Python object
|
|
||||||
static PyObject* animationValueToPython(const AnimationValue& value) {
|
|
||||||
return std::visit([](const auto& val) -> PyObject* {
|
|
||||||
using T = std::decay_t<decltype(val)>;
|
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
return PyFloat_FromDouble(val);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
|
||||||
return PyLong_FromLong(val);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
|
||||||
// Sprite frame list - return current frame as int
|
|
||||||
// (the interpolate function returns the current frame)
|
|
||||||
if (!val.empty()) {
|
|
||||||
return PyLong_FromLong(val.back());
|
|
||||||
}
|
|
||||||
return PyLong_FromLong(0);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
|
||||||
return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
|
||||||
return Py_BuildValue("(ff)", val.x, val.y);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::string>) {
|
|
||||||
return PyUnicode_FromString(val.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Animation::triggerCallback() {
|
void Animation::triggerCallback() {
|
||||||
if (!pythonCallback) return;
|
if (!pythonCallback) return;
|
||||||
|
|
||||||
|
|
@ -591,47 +404,13 @@ void Animation::triggerCallback() {
|
||||||
|
|
||||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||||
|
|
||||||
// #229 - Pass (target, property, final_value) instead of (None, None)
|
// TODO: In future, create PyAnimation wrapper for this animation
|
||||||
// Convert target to Python object
|
// For now, pass None for both parameters
|
||||||
PyObject* targetObj = nullptr;
|
PyObject* args = PyTuple_New(2);
|
||||||
if (auto drawable = targetWeak.lock()) {
|
Py_INCREF(Py_None);
|
||||||
targetObj = convertDrawableToPython(drawable);
|
Py_INCREF(Py_None);
|
||||||
} else if (auto entity = entityTargetWeak.lock()) {
|
PyTuple_SetItem(args, 0, Py_None); // animation parameter
|
||||||
targetObj = convertEntityToPython(entity);
|
PyTuple_SetItem(args, 1, Py_None); // target parameter
|
||||||
}
|
|
||||||
|
|
||||||
// If target conversion failed, use None
|
|
||||||
if (!targetObj) {
|
|
||||||
targetObj = Py_None;
|
|
||||||
Py_INCREF(targetObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Property name
|
|
||||||
PyObject* propertyObj = PyUnicode_FromString(targetProperty.c_str());
|
|
||||||
if (!propertyObj) {
|
|
||||||
Py_DECREF(targetObj);
|
|
||||||
PyGILState_Release(gstate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final value (interpolated at t=1.0)
|
|
||||||
PyObject* valueObj = animationValueToPython(interpolate(1.0f));
|
|
||||||
if (!valueObj) {
|
|
||||||
Py_DECREF(targetObj);
|
|
||||||
Py_DECREF(propertyObj);
|
|
||||||
PyGILState_Release(gstate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject* args = Py_BuildValue("(OOO)", targetObj, propertyObj, valueObj);
|
|
||||||
Py_DECREF(targetObj);
|
|
||||||
Py_DECREF(propertyObj);
|
|
||||||
Py_DECREF(valueObj);
|
|
||||||
|
|
||||||
if (!args) {
|
|
||||||
PyGILState_Release(gstate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject* result = PyObject_CallObject(pythonCallback, args);
|
PyObject* result = PyObject_CallObject(pythonCallback, args);
|
||||||
Py_DECREF(args);
|
Py_DECREF(args);
|
||||||
|
|
|
||||||
|
|
@ -186,123 +186,3 @@ 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;
|
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,33 +39,3 @@ public:
|
||||||
PyKeyCallable(PyObject*);
|
PyKeyCallable(PyObject*);
|
||||||
PyKeyCallable();
|
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,81 +127,6 @@ static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name,
|
||||||
return called;
|
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
|
// Check if a UIDrawable can potentially handle an event
|
||||||
// (has either a callable property OR is a Python subclass that might have a method)
|
// (has either a callable property OR is a Python subclass that might have a method)
|
||||||
static bool canHandleEvent(UIDrawable* drawable, const char* event_type) {
|
static bool canHandleEvent(UIDrawable* drawable, const char* event_type) {
|
||||||
|
|
@ -349,33 +274,30 @@ void PyScene::do_mouse_hover(int x, int y)
|
||||||
// Mouse entered
|
// Mouse entered
|
||||||
drawable->hovered = true;
|
drawable->hovered = true;
|
||||||
// #184: Try property-assigned callable first, then Python subclass method
|
// #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()) {
|
if (drawable->on_enter_callable && !drawable->on_enter_callable->isNone()) {
|
||||||
drawable->on_enter_callable->call(mousepos);
|
drawable->on_enter_callable->call(mousepos, "enter", "start");
|
||||||
} else if (drawable->is_python_subclass) {
|
} else if (drawable->is_python_subclass) {
|
||||||
tryCallPythonMethod(drawable, "on_enter", mousepos);
|
tryCallPythonMethod(drawable, "on_enter", mousepos, "enter", "start");
|
||||||
}
|
}
|
||||||
} else if (!is_inside && was_hovered) {
|
} else if (!is_inside && was_hovered) {
|
||||||
// Mouse exited
|
// Mouse exited
|
||||||
drawable->hovered = false;
|
drawable->hovered = false;
|
||||||
// #184: Try property-assigned callable first, then Python subclass method
|
// #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()) {
|
if (drawable->on_exit_callable && !drawable->on_exit_callable->isNone()) {
|
||||||
drawable->on_exit_callable->call(mousepos);
|
drawable->on_exit_callable->call(mousepos, "exit", "start");
|
||||||
} else if (drawable->is_python_subclass) {
|
} else if (drawable->is_python_subclass) {
|
||||||
tryCallPythonMethod(drawable, "on_exit", mousepos);
|
tryCallPythonMethod(drawable, "on_exit", mousepos, "exit", "start");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #141 - Fire on_move if mouse is inside and has a move/on_move callback
|
// #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
|
// #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
|
// Check is_python_subclass before function call to avoid overhead on hot path
|
||||||
if (is_inside) {
|
if (is_inside) {
|
||||||
if (drawable->on_move_callable && !drawable->on_move_callable->isNone()) {
|
if (drawable->on_move_callable && !drawable->on_move_callable->isNone()) {
|
||||||
drawable->on_move_callable->call(mousepos);
|
drawable->on_move_callable->call(mousepos, "move", "start");
|
||||||
} else if (drawable->is_python_subclass) {
|
} else if (drawable->is_python_subclass) {
|
||||||
tryCallPythonMethod(drawable, "on_move", mousepos);
|
tryCallPythonMethod(drawable, "on_move", mousepos, "move", "start");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,16 +60,16 @@ UIDrawable::UIDrawable(const UIDrawable& other)
|
||||||
if (other.click_callable) {
|
if (other.click_callable) {
|
||||||
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
||||||
}
|
}
|
||||||
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
|
// #140 - Deep copy enter/exit callables
|
||||||
if (other.on_enter_callable) {
|
if (other.on_enter_callable) {
|
||||||
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
|
on_enter_callable = std::make_unique<PyClickCallable>(*other.on_enter_callable);
|
||||||
}
|
}
|
||||||
if (other.on_exit_callable) {
|
if (other.on_exit_callable) {
|
||||||
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
|
on_exit_callable = std::make_unique<PyClickCallable>(*other.on_exit_callable);
|
||||||
}
|
}
|
||||||
// #141, #230 - Deep copy move callable (now PyHoverCallable)
|
// #141 - Deep copy move callable
|
||||||
if (other.on_move_callable) {
|
if (other.on_move_callable) {
|
||||||
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
|
on_move_callable = std::make_unique<PyClickCallable>(*other.on_move_callable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deep copy render texture if needed
|
// Deep copy render texture if needed
|
||||||
|
|
@ -100,20 +100,20 @@ UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
|
||||||
} else {
|
} else {
|
||||||
click_callable.reset();
|
click_callable.reset();
|
||||||
}
|
}
|
||||||
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
|
// #140 - Deep copy enter/exit callables
|
||||||
if (other.on_enter_callable) {
|
if (other.on_enter_callable) {
|
||||||
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
|
on_enter_callable = std::make_unique<PyClickCallable>(*other.on_enter_callable);
|
||||||
} else {
|
} else {
|
||||||
on_enter_callable.reset();
|
on_enter_callable.reset();
|
||||||
}
|
}
|
||||||
if (other.on_exit_callable) {
|
if (other.on_exit_callable) {
|
||||||
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
|
on_exit_callable = std::make_unique<PyClickCallable>(*other.on_exit_callable);
|
||||||
} else {
|
} else {
|
||||||
on_exit_callable.reset();
|
on_exit_callable.reset();
|
||||||
}
|
}
|
||||||
// #141, #230 - Deep copy move callable (now PyHoverCallable)
|
// #141 - Deep copy move callable
|
||||||
if (other.on_move_callable) {
|
if (other.on_move_callable) {
|
||||||
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
|
on_move_callable = std::make_unique<PyClickCallable>(*other.on_move_callable);
|
||||||
} else {
|
} else {
|
||||||
on_move_callable.reset();
|
on_move_callable.reset();
|
||||||
}
|
}
|
||||||
|
|
@ -311,10 +311,10 @@ void UIDrawable::click_register(PyObject* callable)
|
||||||
click_callable = std::make_unique<PyClickCallable>(callable);
|
click_callable = std::make_unique<PyClickCallable>(callable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #140, #230 - Mouse enter/exit callback registration (now PyHoverCallable)
|
// #140 - Mouse enter/exit callback registration
|
||||||
void UIDrawable::on_enter_register(PyObject* callable)
|
void UIDrawable::on_enter_register(PyObject* callable)
|
||||||
{
|
{
|
||||||
on_enter_callable = std::make_unique<PyHoverCallable>(callable);
|
on_enter_callable = std::make_unique<PyClickCallable>(callable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIDrawable::on_enter_unregister()
|
void UIDrawable::on_enter_unregister()
|
||||||
|
|
@ -324,7 +324,7 @@ void UIDrawable::on_enter_unregister()
|
||||||
|
|
||||||
void UIDrawable::on_exit_register(PyObject* callable)
|
void UIDrawable::on_exit_register(PyObject* callable)
|
||||||
{
|
{
|
||||||
on_exit_callable = std::make_unique<PyHoverCallable>(callable);
|
on_exit_callable = std::make_unique<PyClickCallable>(callable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIDrawable::on_exit_unregister()
|
void UIDrawable::on_exit_unregister()
|
||||||
|
|
@ -332,10 +332,10 @@ void UIDrawable::on_exit_unregister()
|
||||||
on_exit_callable.reset();
|
on_exit_callable.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// #141, #230 - Mouse move callback registration (now PyHoverCallable)
|
// #141 - Mouse move callback registration
|
||||||
void UIDrawable::on_move_register(PyObject* callable)
|
void UIDrawable::on_move_register(PyObject* callable)
|
||||||
{
|
{
|
||||||
on_move_callable = std::make_unique<PyHoverCallable>(callable);
|
on_move_callable = std::make_unique<PyClickCallable>(callable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIDrawable::on_move_unregister()
|
void UIDrawable::on_move_unregister()
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ public:
|
||||||
|
|
||||||
// Mouse input handling - callable objects for click, enter, exit, move events
|
// Mouse input handling - callable objects for click, enter, exit, move events
|
||||||
std::unique_ptr<PyClickCallable> click_callable;
|
std::unique_ptr<PyClickCallable> click_callable;
|
||||||
std::unique_ptr<PyHoverCallable> on_enter_callable; // #140, #230 - position-only
|
std::unique_ptr<PyClickCallable> on_enter_callable; // #140
|
||||||
std::unique_ptr<PyHoverCallable> on_exit_callable; // #140, #230 - position-only
|
std::unique_ptr<PyClickCallable> on_exit_callable; // #140
|
||||||
std::unique_ptr<PyHoverCallable> on_move_callable; // #141, #230 - position-only
|
std::unique_ptr<PyClickCallable> on_move_callable; // #141
|
||||||
|
|
||||||
virtual UIDrawable* click_at(sf::Vector2f point) = 0;
|
virtual UIDrawable* click_at(sf::Vector2f point) = 0;
|
||||||
void click_register(PyObject*);
|
void click_register(PyObject*);
|
||||||
|
|
|
||||||
108
src/UIGrid.cpp
108
src/UIGrid.cpp
|
|
@ -40,9 +40,8 @@ UIGrid::UIGrid()
|
||||||
box.setPosition(position); // Sync box position
|
box.setPosition(position); // Sync box position
|
||||||
box.setFillColor(sf::Color(0, 0, 0, 0));
|
box.setFillColor(sf::Color(0, 0, 0, 0));
|
||||||
|
|
||||||
// #228 - Initialize render texture to game resolution (small default until game init)
|
// Initialize render texture (small default size)
|
||||||
renderTexture.create(1, 1);
|
renderTexture.create(1, 1);
|
||||||
renderTextureSize = {1, 1};
|
|
||||||
|
|
||||||
// Initialize output sprite
|
// Initialize output sprite
|
||||||
output.setTextureRect(sf::IntRect(0, 0, 0, 0));
|
output.setTextureRect(sf::IntRect(0, 0, 0, 0));
|
||||||
|
|
@ -77,8 +76,8 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
||||||
box.setPosition(position); // Sync box position
|
box.setPosition(position); // Sync box position
|
||||||
|
|
||||||
box.setFillColor(sf::Color(0,0,0,0));
|
box.setFillColor(sf::Color(0,0,0,0));
|
||||||
// #228 - create renderTexture sized to game resolution (dynamically resized as needed)
|
// create renderTexture with maximum theoretical size; sprite can resize to show whatever amount needs to be rendered
|
||||||
ensureRenderTextureSize();
|
renderTexture.create(1920, 1080); // TODO - renderTexture should be window size; above 1080p this will cause rendering errors
|
||||||
|
|
||||||
// Only initialize sprite if texture is available
|
// Only initialize sprite if texture is available
|
||||||
if (ptex) {
|
if (ptex) {
|
||||||
|
|
@ -146,9 +145,6 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// Check visibility
|
// Check visibility
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
|
|
||||||
// #228 - Ensure renderTexture matches current game resolution
|
|
||||||
ensureRenderTextureSize();
|
|
||||||
|
|
||||||
// TODO: Apply opacity to output sprite
|
// TODO: Apply opacity to output sprite
|
||||||
|
|
||||||
// Get cell dimensions - use texture if available, otherwise defaults
|
// Get cell dimensions - use texture if available, otherwise defaults
|
||||||
|
|
@ -468,26 +464,6 @@ UIGrid::~UIGrid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIGrid::ensureRenderTextureSize()
|
|
||||||
{
|
|
||||||
// Get game resolution (or use sensible defaults during early init)
|
|
||||||
sf::Vector2u resolution{1920, 1080};
|
|
||||||
if (Resources::game) {
|
|
||||||
resolution = Resources::game->getGameResolution();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp to reasonable maximum (SFML texture size limits)
|
|
||||||
unsigned int required_w = std::min(resolution.x, 4096u);
|
|
||||||
unsigned int required_h = std::min(resolution.y, 4096u);
|
|
||||||
|
|
||||||
// Only recreate if size changed
|
|
||||||
if (renderTextureSize.x != required_w || renderTextureSize.y != required_h) {
|
|
||||||
renderTexture.create(required_w, required_h);
|
|
||||||
renderTextureSize = {required_w, required_h};
|
|
||||||
output.setTexture(renderTexture.getTexture());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObjectsEnum UIGrid::derived_type()
|
PyObjectsEnum UIGrid::derived_type()
|
||||||
{
|
{
|
||||||
return PyObjectsEnum::UIGRID;
|
return PyObjectsEnum::UIGRID;
|
||||||
|
|
@ -2363,12 +2339,11 @@ PyObject* UIGrid::get_on_cell_enter(PyUIGridObject* self, void* closure) {
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #230 - Cell hover callbacks now use PyCellHoverCallable (position-only)
|
|
||||||
int UIGrid::set_on_cell_enter(PyUIGridObject* self, PyObject* value, void* closure) {
|
int UIGrid::set_on_cell_enter(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||||
if (value == Py_None) {
|
if (value == Py_None) {
|
||||||
self->data->on_cell_enter_callable.reset();
|
self->data->on_cell_enter_callable.reset();
|
||||||
} else {
|
} else {
|
||||||
self->data->on_cell_enter_callable = std::make_unique<PyCellHoverCallable>(value);
|
self->data->on_cell_enter_callable = std::make_unique<PyClickCallable>(value);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -2382,12 +2357,11 @@ PyObject* UIGrid::get_on_cell_exit(PyUIGridObject* self, void* closure) {
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #230 - Cell hover callbacks now use PyCellHoverCallable (position-only)
|
|
||||||
int UIGrid::set_on_cell_exit(PyUIGridObject* self, PyObject* value, void* closure) {
|
int UIGrid::set_on_cell_exit(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||||
if (value == Py_None) {
|
if (value == Py_None) {
|
||||||
self->data->on_cell_exit_callable.reset();
|
self->data->on_cell_exit_callable.reset();
|
||||||
} else {
|
} else {
|
||||||
self->data->on_cell_exit_callable = std::make_unique<PyCellHoverCallable>(value);
|
self->data->on_cell_exit_callable = std::make_unique<PyClickCallable>(value);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -2579,26 +2553,6 @@ static PyObject* createCellCallbackArgs(sf::Vector2i cell, const std::string& bu
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #230 - Helper to create cell hover callback arguments: (Vector) only
|
|
||||||
static PyObject* createCellHoverArgs(sf::Vector2i cell) {
|
|
||||||
// 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, "ii", cell.x, cell.y);
|
|
||||||
Py_DECREF(vector_type);
|
|
||||||
if (!cell_pos) {
|
|
||||||
PyErr_Print();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
|
||||||
Py_DECREF(cell_pos);
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire cell click callback with full signature (cell_pos, button, action)
|
// 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) {
|
bool UIGrid::fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action) {
|
||||||
// Try property-assigned callback first
|
// Try property-assigned callback first
|
||||||
|
|
@ -2650,13 +2604,24 @@ bool UIGrid::fireCellClick(sf::Vector2i cell, const std::string& button, const s
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #230 - Fire cell enter callback with position-only signature (cell_pos)
|
// Fire cell enter callback with full signature (cell_pos, button, action)
|
||||||
bool UIGrid::fireCellEnter(sf::Vector2i cell) {
|
bool UIGrid::fireCellEnter(sf::Vector2i cell, const std::string& button, const std::string& action) {
|
||||||
// Try property-assigned callback first (now PyCellHoverCallable)
|
// Try property-assigned callback first
|
||||||
if (on_cell_enter_callable && !on_cell_enter_callable->isNone()) {
|
if (on_cell_enter_callable && !on_cell_enter_callable->isNone()) {
|
||||||
on_cell_enter_callable->call(cell);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try Python subclass method
|
// Try Python subclass method
|
||||||
if (is_python_subclass) {
|
if (is_python_subclass) {
|
||||||
|
|
@ -2666,8 +2631,7 @@ bool UIGrid::fireCellEnter(sf::Vector2i cell) {
|
||||||
if (cell_callback_cache.has_on_cell_enter) {
|
if (cell_callback_cache.has_on_cell_enter) {
|
||||||
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_enter");
|
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_enter");
|
||||||
if (method && PyCallable_Check(method)) {
|
if (method && PyCallable_Check(method)) {
|
||||||
// #230: Cell hover takes only (cell_pos)
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
PyObject* args = createCellHoverArgs(cell);
|
|
||||||
if (args) {
|
if (args) {
|
||||||
PyObject* result = PyObject_CallObject(method, args);
|
PyObject* result = PyObject_CallObject(method, args);
|
||||||
Py_DECREF(args);
|
Py_DECREF(args);
|
||||||
|
|
@ -2691,13 +2655,24 @@ bool UIGrid::fireCellEnter(sf::Vector2i cell) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #230 - Fire cell exit callback with position-only signature (cell_pos)
|
// Fire cell exit callback with full signature (cell_pos, button, action)
|
||||||
bool UIGrid::fireCellExit(sf::Vector2i cell) {
|
bool UIGrid::fireCellExit(sf::Vector2i cell, const std::string& button, const std::string& action) {
|
||||||
// Try property-assigned callback first (now PyCellHoverCallable)
|
// Try property-assigned callback first
|
||||||
if (on_cell_exit_callable && !on_cell_exit_callable->isNone()) {
|
if (on_cell_exit_callable && !on_cell_exit_callable->isNone()) {
|
||||||
on_cell_exit_callable->call(cell);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try Python subclass method
|
// Try Python subclass method
|
||||||
if (is_python_subclass) {
|
if (is_python_subclass) {
|
||||||
|
|
@ -2707,8 +2682,7 @@ bool UIGrid::fireCellExit(sf::Vector2i cell) {
|
||||||
if (cell_callback_cache.has_on_cell_exit) {
|
if (cell_callback_cache.has_on_cell_exit) {
|
||||||
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_exit");
|
PyObject* method = PyObject_GetAttrString(pyObj, "on_cell_exit");
|
||||||
if (method && PyCallable_Check(method)) {
|
if (method && PyCallable_Check(method)) {
|
||||||
// #230: Cell hover takes only (cell_pos)
|
PyObject* args = createCellCallbackArgs(cell, button, action);
|
||||||
PyObject* args = createCellHoverArgs(cell);
|
|
||||||
if (args) {
|
if (args) {
|
||||||
PyObject* result = PyObject_CallObject(method, args);
|
PyObject* result = PyObject_CallObject(method, args);
|
||||||
Py_DECREF(args);
|
Py_DECREF(args);
|
||||||
|
|
@ -2733,23 +2707,19 @@ bool UIGrid::fireCellExit(sf::Vector2i cell) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// #142 - Update cell hover state and fire callbacks
|
// #142 - Update cell hover state and fire callbacks
|
||||||
// #230 - Cell hover callbacks now take only (cell_pos), no button/action
|
|
||||||
void UIGrid::updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action) {
|
void UIGrid::updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action) {
|
||||||
(void)button; // #230 - No longer used for hover callbacks
|
|
||||||
(void)action; // #230 - No longer used for hover callbacks
|
|
||||||
|
|
||||||
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()) {
|
if (hovered_cell.has_value()) {
|
||||||
fireCellExit(hovered_cell.value());
|
fireCellExit(hovered_cell.value(), button, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire enter callback for new cell
|
// Fire enter callback for new cell
|
||||||
if (new_cell.has_value()) {
|
if (new_cell.has_value()) {
|
||||||
fireCellEnter(new_cell.value());
|
fireCellEnter(new_cell.value(), button, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
hovered_cell = new_cell;
|
hovered_cell = new_cell;
|
||||||
|
|
|
||||||
19
src/UIGrid.h
19
src/UIGrid.h
|
|
@ -84,10 +84,6 @@ public:
|
||||||
std::shared_ptr<PyTexture> getTexture();
|
std::shared_ptr<PyTexture> getTexture();
|
||||||
sf::Sprite sprite, output;
|
sf::Sprite sprite, output;
|
||||||
sf::RenderTexture renderTexture;
|
sf::RenderTexture renderTexture;
|
||||||
sf::Vector2u renderTextureSize{0, 0}; // Track current allocation for resize detection
|
|
||||||
|
|
||||||
// Helper to ensure renderTexture matches game resolution
|
|
||||||
void ensureRenderTextureSize();
|
|
||||||
|
|
||||||
// Intermediate texture for camera_rotation (larger than viewport to hold rotated content)
|
// Intermediate texture for camera_rotation (larger than viewport to hold rotated content)
|
||||||
sf::RenderTexture rotationTexture;
|
sf::RenderTexture rotationTexture;
|
||||||
|
|
@ -135,10 +131,9 @@ public:
|
||||||
TCOD_fov_algorithm_t fov_algorithm; // Default FOV algorithm (from mcrfpy.default_fov)
|
TCOD_fov_algorithm_t fov_algorithm; // Default FOV algorithm (from mcrfpy.default_fov)
|
||||||
int fov_radius; // Default FOV radius
|
int fov_radius; // Default FOV radius
|
||||||
|
|
||||||
// #142, #230 - Grid cell mouse events
|
// #142 - Grid cell mouse events
|
||||||
// Cell hover callbacks take only (cell_pos); cell click still takes (cell_pos, button, action)
|
std::unique_ptr<PyClickCallable> on_cell_enter_callable;
|
||||||
std::unique_ptr<PyCellHoverCallable> on_cell_enter_callable;
|
std::unique_ptr<PyClickCallable> on_cell_exit_callable;
|
||||||
std::unique_ptr<PyCellHoverCallable> 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
|
std::optional<sf::Vector2i> last_clicked_cell; // Cell clicked during click_at
|
||||||
|
|
@ -163,13 +158,11 @@ public:
|
||||||
// Now takes button/action for consistent callback signatures
|
// Now takes button/action for consistent callback signatures
|
||||||
void updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action);
|
void updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action);
|
||||||
|
|
||||||
// Fire cell callbacks
|
// Fire cell callbacks with full signature (cell_pos, button, action)
|
||||||
// #230: Cell hover callbacks (enter/exit) now take only (cell_pos)
|
|
||||||
// Cell click still takes (cell_pos, button, action)
|
|
||||||
// Returns true if a callback was fired
|
// Returns true if a callback was fired
|
||||||
bool fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action);
|
bool fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action);
|
||||||
bool fireCellEnter(sf::Vector2i cell);
|
bool fireCellEnter(sf::Vector2i cell, const std::string& button, const std::string& action);
|
||||||
bool fireCellExit(sf::Vector2i cell);
|
bool fireCellExit(sf::Vector2i cell, const std::string& button, const std::string& action);
|
||||||
|
|
||||||
// Refresh cell callback cache for subclass method support
|
// Refresh cell callback cache for subclass method support
|
||||||
void refreshCellCallbackCache(PyObject* pyObj);
|
void refreshCellCallbackCache(PyObject* pyObj);
|
||||||
|
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Drag and Drop (Frame) Demo - Sort colored frames into target bins
|
|
||||||
|
|
||||||
Interactive controls:
|
|
||||||
Mouse drag: Move frames
|
|
||||||
ESC: Return to menu
|
|
||||||
|
|
||||||
This demonstrates:
|
|
||||||
- Frame drag and drop using on_click + on_move (Pythonic method override pattern)
|
|
||||||
- Hit testing for drop targets
|
|
||||||
- State tracking and validation
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
|
|
||||||
class DraggableFrame(mcrfpy.Frame):
|
|
||||||
"""A frame that can be dragged around the screen.
|
|
||||||
|
|
||||||
Uses Pythonic method override pattern - just define on_click and on_move
|
|
||||||
methods directly, no need for self.on_click = self._on_click assignment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, pos, size, color, color_type):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
pos: Initial position tuple (x, y)
|
|
||||||
size: Size tuple (w, h)
|
|
||||||
color: Fill color tuple (r, g, b)
|
|
||||||
color_type: 'red' or 'blue' for sorting validation
|
|
||||||
"""
|
|
||||||
super().__init__(pos, size, fill_color=color, outline=2, outline_color=(255, 255, 255))
|
|
||||||
self.color_type = color_type
|
|
||||||
self.dragging = False
|
|
||||||
self.drag_offset = (0, 0)
|
|
||||||
self.original_pos = pos
|
|
||||||
# No need for self.on_click = self._on_click - just define on_click method below!
|
|
||||||
|
|
||||||
def on_click(self, pos, button, action):
|
|
||||||
"""Handle click events for drag start/end.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pos: mcrfpy.Vector with x, y coordinates
|
|
||||||
button: mcrfpy.MouseButton enum (LEFT, RIGHT, etc.)
|
|
||||||
action: mcrfpy.InputState enum (PRESSED, RELEASED)
|
|
||||||
"""
|
|
||||||
if button != mcrfpy.MouseButton.LEFT:
|
|
||||||
return
|
|
||||||
|
|
||||||
if action == mcrfpy.InputState.PRESSED:
|
|
||||||
# Begin dragging - calculate offset from frame origin
|
|
||||||
self.dragging = True
|
|
||||||
self.drag_offset = (pos.x - self.x, pos.y - self.y)
|
|
||||||
elif action == mcrfpy.InputState.RELEASED:
|
|
||||||
if self.dragging:
|
|
||||||
self.dragging = False
|
|
||||||
# Notify demo of drop
|
|
||||||
if hasattr(self, 'on_drop_callback'):
|
|
||||||
self.on_drop_callback(self)
|
|
||||||
|
|
||||||
def on_move(self, pos):
|
|
||||||
"""Handle mouse movement for dragging.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pos: mcrfpy.Vector with x, y coordinates
|
|
||||||
Note: #230 - on_move now only receives position, not button/action
|
|
||||||
"""
|
|
||||||
if self.dragging:
|
|
||||||
self.x = pos.x - self.drag_offset[0]
|
|
||||||
self.y = pos.y - self.drag_offset[1]
|
|
||||||
|
|
||||||
|
|
||||||
class DragDropFrameDemo:
|
|
||||||
"""Demo showing frame drag and drop with sorting bins."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.scene = mcrfpy.Scene("demo_drag_drop_frame")
|
|
||||||
self.ui = self.scene.children
|
|
||||||
self.draggables = []
|
|
||||||
self.setup()
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
"""Build the demo UI."""
|
|
||||||
# Background
|
|
||||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(30, 30, 35))
|
|
||||||
self.ui.append(bg)
|
|
||||||
|
|
||||||
# Title
|
|
||||||
title = mcrfpy.Caption(
|
|
||||||
text="Drag & Drop: Sort by Color",
|
|
||||||
pos=(512, 30),
|
|
||||||
font_size=28,
|
|
||||||
fill_color=(255, 255, 255)
|
|
||||||
)
|
|
||||||
title.outline = 2
|
|
||||||
title.outline_color = (0, 0, 0)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
# Score caption
|
|
||||||
self.score_caption = mcrfpy.Caption(
|
|
||||||
text="Sorted: 0 / 8",
|
|
||||||
pos=(512, 70),
|
|
||||||
font_size=20,
|
|
||||||
fill_color=(200, 200, 200)
|
|
||||||
)
|
|
||||||
self.ui.append(self.score_caption)
|
|
||||||
|
|
||||||
# Target bins (bottom half)
|
|
||||||
# Red bin on the left
|
|
||||||
self.red_bin = mcrfpy.Frame(
|
|
||||||
pos=(20, 500),
|
|
||||||
size=(482, 248),
|
|
||||||
fill_color=(96, 0, 0),
|
|
||||||
outline=3,
|
|
||||||
outline_color=(200, 50, 50)
|
|
||||||
)
|
|
||||||
self.ui.append(self.red_bin)
|
|
||||||
|
|
||||||
red_label = mcrfpy.Caption(
|
|
||||||
text="RED BIN",
|
|
||||||
pos=(261, 600),
|
|
||||||
font_size=32,
|
|
||||||
fill_color=(200, 100, 100)
|
|
||||||
)
|
|
||||||
self.ui.append(red_label)
|
|
||||||
|
|
||||||
# Blue bin on the right
|
|
||||||
self.blue_bin = mcrfpy.Frame(
|
|
||||||
pos=(522, 500),
|
|
||||||
size=(482, 248),
|
|
||||||
fill_color=(0, 0, 96),
|
|
||||||
outline=3,
|
|
||||||
outline_color=(50, 50, 200)
|
|
||||||
)
|
|
||||||
self.ui.append(self.blue_bin)
|
|
||||||
|
|
||||||
blue_label = mcrfpy.Caption(
|
|
||||||
text="BLUE BIN",
|
|
||||||
pos=(763, 600),
|
|
||||||
font_size=32,
|
|
||||||
fill_color=(100, 100, 200)
|
|
||||||
)
|
|
||||||
self.ui.append(blue_label)
|
|
||||||
|
|
||||||
# Create draggable frames (top half)
|
|
||||||
# 4 red frames, 4 blue frames, arranged in 2 rows
|
|
||||||
frame_size = (100, 80)
|
|
||||||
spacing = 20
|
|
||||||
start_x = 100
|
|
||||||
start_y = 120
|
|
||||||
|
|
||||||
positions = []
|
|
||||||
for row in range(2):
|
|
||||||
for col in range(4):
|
|
||||||
x = start_x + col * (frame_size[0] + spacing + 80)
|
|
||||||
y = start_y + row * (frame_size[1] + spacing + 40)
|
|
||||||
positions.append((x, y))
|
|
||||||
|
|
||||||
# Interleave red and blue
|
|
||||||
colors = [
|
|
||||||
((255, 64, 64), 'red'),
|
|
||||||
((64, 64, 255), 'blue'),
|
|
||||||
((255, 64, 64), 'red'),
|
|
||||||
((64, 64, 255), 'blue'),
|
|
||||||
((64, 64, 255), 'blue'),
|
|
||||||
((255, 64, 64), 'red'),
|
|
||||||
((64, 64, 255), 'blue'),
|
|
||||||
((255, 64, 64), 'red'),
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, (pos, (color, color_type)) in enumerate(zip(positions, colors)):
|
|
||||||
frame = DraggableFrame(pos, frame_size, color, color_type)
|
|
||||||
frame.on_drop_callback = self._on_frame_drop
|
|
||||||
self.draggables.append(frame)
|
|
||||||
self.ui.append(frame)
|
|
||||||
|
|
||||||
# Add label inside frame
|
|
||||||
label = mcrfpy.Caption(
|
|
||||||
text=f"{i+1}",
|
|
||||||
pos=(40, 25),
|
|
||||||
font_size=24,
|
|
||||||
fill_color=(255, 255, 255)
|
|
||||||
)
|
|
||||||
frame.children.append(label)
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
instr = mcrfpy.Caption(
|
|
||||||
text="Drag red frames to red bin, blue frames to blue bin | ESC to exit",
|
|
||||||
pos=(512, 470),
|
|
||||||
font_size=14,
|
|
||||||
fill_color=(150, 150, 150)
|
|
||||||
)
|
|
||||||
self.ui.append(instr)
|
|
||||||
|
|
||||||
# Initial score update
|
|
||||||
self._update_score()
|
|
||||||
|
|
||||||
def _point_in_frame(self, x, y, frame):
|
|
||||||
"""Check if point (x, y) is inside frame."""
|
|
||||||
return (frame.x <= x <= frame.x + frame.w and
|
|
||||||
frame.y <= y <= frame.y + frame.h)
|
|
||||||
|
|
||||||
def _frame_in_bin(self, draggable, bin_frame):
|
|
||||||
"""Check if draggable frame's center is in bin."""
|
|
||||||
center_x = draggable.x + draggable.w / 2
|
|
||||||
center_y = draggable.y + draggable.h / 2
|
|
||||||
return self._point_in_frame(center_x, center_y, bin_frame)
|
|
||||||
|
|
||||||
def _on_frame_drop(self, frame):
|
|
||||||
"""Called when a frame is dropped."""
|
|
||||||
self._update_score()
|
|
||||||
|
|
||||||
def _update_score(self):
|
|
||||||
"""Count and display correctly sorted frames."""
|
|
||||||
correct = 0
|
|
||||||
for frame in self.draggables:
|
|
||||||
if frame.color_type == 'red' and self._frame_in_bin(frame, self.red_bin):
|
|
||||||
correct += 1
|
|
||||||
frame.outline_color = (0, 255, 0) # Green outline for correct
|
|
||||||
elif frame.color_type == 'blue' and self._frame_in_bin(frame, self.blue_bin):
|
|
||||||
correct += 1
|
|
||||||
frame.outline_color = (0, 255, 0)
|
|
||||||
else:
|
|
||||||
frame.outline_color = (255, 255, 255) # White outline otherwise
|
|
||||||
|
|
||||||
self.score_caption.text = f"Sorted: {correct} / 8"
|
|
||||||
|
|
||||||
if correct == 8:
|
|
||||||
self.score_caption.text = "All Sorted! Well done!"
|
|
||||||
self.score_caption.fill_color = (100, 255, 100)
|
|
||||||
|
|
||||||
def on_key(self, key, state):
|
|
||||||
"""Handle keyboard input."""
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
if key == "Escape":
|
|
||||||
# Return to cookbook menu or exit
|
|
||||||
try:
|
|
||||||
from cookbook_main import main
|
|
||||||
main()
|
|
||||||
except:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
"""Activate the demo scene."""
|
|
||||||
self.scene.on_key = self.on_key
|
|
||||||
mcrfpy.current_scene = self.scene
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run the demo."""
|
|
||||||
demo = DragDropFrameDemo()
|
|
||||||
demo.activate()
|
|
||||||
|
|
||||||
# Headless screenshot
|
|
||||||
try:
|
|
||||||
if mcrfpy.headless_mode():
|
|
||||||
from mcrfpy import automation
|
|
||||||
# Move some frames to bins for screenshot
|
|
||||||
demo.draggables[0].x = 100
|
|
||||||
demo.draggables[0].y = 550
|
|
||||||
demo.draggables[1].x = 600
|
|
||||||
demo.draggables[1].y = 550
|
|
||||||
demo._update_score()
|
|
||||||
|
|
||||||
mcrfpy.Timer("screenshot", lambda rt: (
|
|
||||||
automation.screenshot("screenshots/primitives/drag_drop_frame.png"),
|
|
||||||
sys.exit(0)
|
|
||||||
), 100)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -49,10 +49,9 @@ def test_callback_refcount():
|
||||||
errors.append(f"on_click returned non-callable after repeated access: {type(final_cb)}")
|
errors.append(f"on_click returned non-callable after repeated access: {type(final_cb)}")
|
||||||
|
|
||||||
# Test on_enter, on_exit, on_move
|
# Test on_enter, on_exit, on_move
|
||||||
# #230 - Hover callbacks now take only (pos)
|
frame.on_enter = lambda pos, button, action: None
|
||||||
frame.on_enter = lambda pos: None
|
frame.on_exit = lambda pos, button, action: None
|
||||||
frame.on_exit = lambda pos: None
|
frame.on_move = lambda pos, button, action: None
|
||||||
frame.on_move = lambda pos: None
|
|
||||||
|
|
||||||
for name in ['on_enter', 'on_exit', 'on_move']:
|
for name in ['on_enter', 'on_exit', 'on_move']:
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,11 @@ print("=" * 30)
|
||||||
# Global state to track callback
|
# Global state to track callback
|
||||||
callback_count = 0
|
callback_count = 0
|
||||||
|
|
||||||
# #229 - Animation callbacks now receive (target, property, value) instead of (anim, target)
|
def my_callback(anim, target):
|
||||||
def my_callback(target, prop, value):
|
|
||||||
"""Simple callback that prints when animation completes"""
|
"""Simple callback that prints when animation completes"""
|
||||||
global callback_count
|
global callback_count
|
||||||
callback_count += 1
|
callback_count += 1
|
||||||
print(f"Animation completed! Callback #{callback_count}")
|
print(f"Animation completed! Callback #{callback_count}")
|
||||||
print(f" Target: {type(target).__name__}, Property: {prop}, Value: {value}")
|
|
||||||
|
|
||||||
# Create scene
|
# Create scene
|
||||||
callback_demo = mcrfpy.Scene("callback_demo")
|
callback_demo = mcrfpy.Scene("callback_demo")
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,13 @@ try:
|
||||||
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
|
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))
|
self.cell_events.append(('click', cell_pos.x, cell_pos.y, button, action))
|
||||||
|
|
||||||
# #230 - Cell hover callbacks now only receive (cell_pos)
|
def on_cell_enter(self, cell_pos, button, action):
|
||||||
def on_cell_enter(self, cell_pos):
|
|
||||||
assert isinstance(cell_pos, mcrfpy.Vector), f"cell_pos should be Vector, got {type(cell_pos)}"
|
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))
|
self.cell_events.append(('enter', cell_pos.x, cell_pos.y, button, action))
|
||||||
|
|
||||||
def on_cell_exit(self, cell_pos):
|
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)}"
|
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))
|
self.cell_events.append(('exit', cell_pos.x, cell_pos.y, button, action))
|
||||||
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
grid = GridWithCellCallbacks(grid_size=(5, 5), texture=texture, pos=(0, 0), size=(100, 100))
|
grid = GridWithCellCallbacks(grid_size=(5, 5), texture=texture, pos=(0, 0), size=(100, 100))
|
||||||
|
|
@ -79,9 +78,8 @@ try:
|
||||||
|
|
||||||
# Manually call methods to verify signature works
|
# 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_click(mcrfpy.Vector(1.0, 2.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
# #230 - Cell hover callbacks now only receive (cell_pos)
|
grid.on_cell_enter(mcrfpy.Vector(3.0, 4.0), mcrfpy.MouseButton.RIGHT, mcrfpy.InputState.RELEASED)
|
||||||
grid.on_cell_enter(mcrfpy.Vector(3.0, 4.0))
|
grid.on_cell_exit(mcrfpy.Vector(5.0, 6.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
grid.on_cell_exit(mcrfpy.Vector(5.0, 6.0))
|
|
||||||
|
|
||||||
assert len(grid.cell_events) == 3, f"Should have 3 events, got {len(grid.cell_events)}"
|
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[0][0] == 'click', "First event should be click"
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@ def test_click_callback_signature(pos, button, action):
|
||||||
results.append(("on_click button/action are strings", False))
|
results.append(("on_click button/action are strings", False))
|
||||||
print(f"FAIL: button={type(button).__name__}, action={type(action).__name__}")
|
print(f"FAIL: button={type(button).__name__}, action={type(action).__name__}")
|
||||||
|
|
||||||
# #230 - Hover callbacks now receive only (pos), not (pos, button, action)
|
def test_on_enter_callback_signature(pos, button, action):
|
||||||
def test_on_enter_callback_signature(pos):
|
|
||||||
"""Test on_enter callback receives Vector."""
|
"""Test on_enter callback receives Vector."""
|
||||||
if isinstance(pos, mcrfpy.Vector):
|
if isinstance(pos, mcrfpy.Vector):
|
||||||
results.append(("on_enter pos is Vector", True))
|
results.append(("on_enter pos is Vector", True))
|
||||||
|
|
@ -35,7 +34,7 @@ def test_on_enter_callback_signature(pos):
|
||||||
results.append(("on_enter pos is Vector", False))
|
results.append(("on_enter pos is Vector", False))
|
||||||
print(f"FAIL: on_enter receives {type(pos).__name__} instead of Vector")
|
print(f"FAIL: on_enter receives {type(pos).__name__} instead of Vector")
|
||||||
|
|
||||||
def test_on_exit_callback_signature(pos):
|
def test_on_exit_callback_signature(pos, button, action):
|
||||||
"""Test on_exit callback receives Vector."""
|
"""Test on_exit callback receives Vector."""
|
||||||
if isinstance(pos, mcrfpy.Vector):
|
if isinstance(pos, mcrfpy.Vector):
|
||||||
results.append(("on_exit pos is Vector", True))
|
results.append(("on_exit pos is Vector", True))
|
||||||
|
|
@ -44,7 +43,7 @@ def test_on_exit_callback_signature(pos):
|
||||||
results.append(("on_exit pos is Vector", False))
|
results.append(("on_exit pos is Vector", False))
|
||||||
print(f"FAIL: on_exit receives {type(pos).__name__} instead of Vector")
|
print(f"FAIL: on_exit receives {type(pos).__name__} instead of Vector")
|
||||||
|
|
||||||
def test_on_move_callback_signature(pos):
|
def test_on_move_callback_signature(pos, button, action):
|
||||||
"""Test on_move callback receives Vector."""
|
"""Test on_move callback receives Vector."""
|
||||||
if isinstance(pos, mcrfpy.Vector):
|
if isinstance(pos, mcrfpy.Vector):
|
||||||
results.append(("on_move pos is Vector", True))
|
results.append(("on_move pos is Vector", True))
|
||||||
|
|
@ -53,9 +52,8 @@ def test_on_move_callback_signature(pos):
|
||||||
results.append(("on_move pos is Vector", False))
|
results.append(("on_move pos is Vector", False))
|
||||||
print(f"FAIL: on_move receives {type(pos).__name__} instead of Vector")
|
print(f"FAIL: on_move receives {type(pos).__name__} instead of Vector")
|
||||||
|
|
||||||
# #230 - Cell click still receives (cell_pos, button, action)
|
def test_cell_click_callback_signature(cell_pos):
|
||||||
def test_cell_click_callback_signature(cell_pos, button, action):
|
"""Test on_cell_click callback receives Vector."""
|
||||||
"""Test on_cell_click callback receives Vector, MouseButton, InputState."""
|
|
||||||
if isinstance(cell_pos, mcrfpy.Vector):
|
if isinstance(cell_pos, mcrfpy.Vector):
|
||||||
results.append(("on_cell_click pos is Vector", True))
|
results.append(("on_cell_click pos is Vector", True))
|
||||||
print(f"PASS: on_cell_click receives Vector: {cell_pos}")
|
print(f"PASS: on_cell_click receives Vector: {cell_pos}")
|
||||||
|
|
@ -63,7 +61,6 @@ def test_cell_click_callback_signature(cell_pos, button, action):
|
||||||
results.append(("on_cell_click pos is Vector", False))
|
results.append(("on_cell_click pos is Vector", False))
|
||||||
print(f"FAIL: on_cell_click receives {type(cell_pos).__name__} instead of Vector")
|
print(f"FAIL: on_cell_click receives {type(cell_pos).__name__} instead of Vector")
|
||||||
|
|
||||||
# #230 - Cell hover callbacks now receive only (cell_pos)
|
|
||||||
def test_cell_enter_callback_signature(cell_pos):
|
def test_cell_enter_callback_signature(cell_pos):
|
||||||
"""Test on_cell_enter callback receives Vector."""
|
"""Test on_cell_enter callback receives Vector."""
|
||||||
if isinstance(cell_pos, mcrfpy.Vector):
|
if isinstance(cell_pos, mcrfpy.Vector):
|
||||||
|
|
@ -122,15 +119,11 @@ def run_test(runtime):
|
||||||
print("\n--- Simulating callback calls ---")
|
print("\n--- Simulating callback calls ---")
|
||||||
|
|
||||||
# Test that the callbacks are set up correctly
|
# Test that the callbacks are set up correctly
|
||||||
# on_click still takes (pos, button, action)
|
|
||||||
test_click_callback_signature(mcrfpy.Vector(150, 150), "left", "start")
|
test_click_callback_signature(mcrfpy.Vector(150, 150), "left", "start")
|
||||||
# #230 - Hover callbacks now take only (pos)
|
test_on_enter_callback_signature(mcrfpy.Vector(100, 100), "enter", "start")
|
||||||
test_on_enter_callback_signature(mcrfpy.Vector(100, 100))
|
test_on_exit_callback_signature(mcrfpy.Vector(300, 300), "exit", "start")
|
||||||
test_on_exit_callback_signature(mcrfpy.Vector(300, 300))
|
test_on_move_callback_signature(mcrfpy.Vector(125, 175), "move", "start")
|
||||||
test_on_move_callback_signature(mcrfpy.Vector(125, 175))
|
test_cell_click_callback_signature(mcrfpy.Vector(5, 3))
|
||||||
# #230 - on_cell_click still takes (cell_pos, button, action)
|
|
||||||
test_cell_click_callback_signature(mcrfpy.Vector(5, 3), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
|
||||||
# #230 - Cell hover callbacks now take only (cell_pos)
|
|
||||||
test_cell_enter_callback_signature(mcrfpy.Vector(2, 7))
|
test_cell_enter_callback_signature(mcrfpy.Vector(2, 7))
|
||||||
test_cell_exit_callback_signature(mcrfpy.Vector(8, 1))
|
test_cell_exit_callback_signature(mcrfpy.Vector(8, 1))
|
||||||
|
|
||||||
|
|
@ -154,4 +147,4 @@ def run_test(runtime):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Run the test
|
# Run the test
|
||||||
mcrfpy.Timer("test", run_test, 100)
|
mcrfpy.setTimer("test", run_test, 100)
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ def test_callback_assignment():
|
||||||
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
# #230 - Hover callbacks now receive only (pos) - 1 argument
|
# Callbacks receive (x, y, button, action) - 4 arguments
|
||||||
def on_enter_cb(pos):
|
def on_enter_cb(x, y, button, action):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_exit_cb(pos):
|
def on_exit_cb(x, y, button, action):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Test assignment
|
# Test assignment
|
||||||
|
|
@ -87,8 +87,7 @@ def test_all_types_have_events():
|
||||||
("Grid", mcrfpy.Grid(grid_size=(5, 5), pos=(0, 0), size=(100, 100))),
|
("Grid", mcrfpy.Grid(grid_size=(5, 5), pos=(0, 0), size=(100, 100))),
|
||||||
]
|
]
|
||||||
|
|
||||||
# #230 - Hover callbacks now receive only (pos)
|
def dummy_cb(x, y, button, action):
|
||||||
def dummy_cb(pos):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for name, obj in types_to_test:
|
for name, obj in types_to_test:
|
||||||
|
|
@ -130,16 +129,15 @@ def test_enter_exit_simulation():
|
||||||
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
# #230 - Hover callbacks now receive only (pos)
|
def on_enter(x, y, button, action):
|
||||||
def on_enter(pos):
|
|
||||||
global enter_count, enter_positions
|
global enter_count, enter_positions
|
||||||
enter_count += 1
|
enter_count += 1
|
||||||
enter_positions.append((pos.x, pos.y))
|
enter_positions.append((x, y))
|
||||||
|
|
||||||
def on_exit(pos):
|
def on_exit(x, y, button, action):
|
||||||
global exit_count, exit_positions
|
global exit_count, exit_positions
|
||||||
exit_count += 1
|
exit_count += 1
|
||||||
exit_positions.append((pos.x, pos.y))
|
exit_positions.append((x, y))
|
||||||
|
|
||||||
frame.on_enter = on_enter
|
frame.on_enter = on_enter
|
||||||
frame.on_exit = on_exit
|
frame.on_exit = on_exit
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,9 @@ def test_failed(name, error):
|
||||||
|
|
||||||
# Helper to create typed callback arguments
|
# Helper to create typed callback arguments
|
||||||
def make_click_args(x=0.0, y=0.0):
|
def make_click_args(x=0.0, y=0.0):
|
||||||
"""Create properly typed callback arguments for testing on_click."""
|
"""Create properly typed callback arguments for testing."""
|
||||||
return (mcrfpy.Vector(x, y), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
return (mcrfpy.Vector(x, y), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
|
|
||||||
# #230 - Hover callbacks now only receive position
|
|
||||||
def make_hover_args(x=0.0, y=0.0):
|
|
||||||
"""Create properly typed callback arguments for testing on_enter/on_exit/on_move."""
|
|
||||||
return (mcrfpy.Vector(x, y),)
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Test Classes
|
# Test Classes
|
||||||
|
|
@ -161,8 +156,7 @@ try:
|
||||||
initial_gen = getattr(TrackedFrame, '_mcrf_callback_gen', 0)
|
initial_gen = getattr(TrackedFrame, '_mcrf_callback_gen', 0)
|
||||||
|
|
||||||
# Add a callback method
|
# Add a callback method
|
||||||
# #230 - Hover callbacks now only receive (pos)
|
def tracked_on_enter(self, pos, button, action):
|
||||||
def tracked_on_enter(self, pos):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
TrackedFrame.on_enter = tracked_on_enter
|
TrackedFrame.on_enter = tracked_on_enter
|
||||||
|
|
@ -190,26 +184,26 @@ try:
|
||||||
self.events.append('click')
|
self.events.append('click')
|
||||||
MultiCallbackFrame.on_click = multi_on_click
|
MultiCallbackFrame.on_click = multi_on_click
|
||||||
|
|
||||||
# Add on_enter - #230: now only takes (pos)
|
# Add on_enter
|
||||||
def multi_on_enter(self, pos):
|
def multi_on_enter(self, pos, button, action):
|
||||||
self.events.append('enter')
|
self.events.append('enter')
|
||||||
MultiCallbackFrame.on_enter = multi_on_enter
|
MultiCallbackFrame.on_enter = multi_on_enter
|
||||||
|
|
||||||
# Add on_exit - #230: now only takes (pos)
|
# Add on_exit
|
||||||
def multi_on_exit(self, pos):
|
def multi_on_exit(self, pos, button, action):
|
||||||
self.events.append('exit')
|
self.events.append('exit')
|
||||||
MultiCallbackFrame.on_exit = multi_on_exit
|
MultiCallbackFrame.on_exit = multi_on_exit
|
||||||
|
|
||||||
# Add on_move - #230: now only takes (pos)
|
# Add on_move
|
||||||
def multi_on_move(self, pos):
|
def multi_on_move(self, pos, button, action):
|
||||||
self.events.append('move')
|
self.events.append('move')
|
||||||
MultiCallbackFrame.on_move = multi_on_move
|
MultiCallbackFrame.on_move = multi_on_move
|
||||||
|
|
||||||
# Call all methods
|
# Call all methods
|
||||||
frame.on_click(*make_click_args())
|
frame.on_click(*make_click_args())
|
||||||
frame.on_enter(*make_hover_args())
|
frame.on_enter(*make_click_args())
|
||||||
frame.on_exit(*make_hover_args())
|
frame.on_exit(*make_click_args())
|
||||||
frame.on_move(*make_hover_args())
|
frame.on_move(*make_click_args())
|
||||||
|
|
||||||
assert frame.events == ['click', 'enter', 'exit', 'move'], \
|
assert frame.events == ['click', 'enter', 'exit', 'move'], \
|
||||||
f"All callbacks should fire, got: {frame.events}"
|
f"All callbacks should fire, got: {frame.events}"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Test UIDrawable subclass callback methods (#184, #230)
|
Test UIDrawable subclass callback methods (#184)
|
||||||
|
|
||||||
This tests the ability to define callback methods (on_click, on_enter,
|
This tests the ability to define callback methods (on_click, on_enter,
|
||||||
on_exit, on_move) directly in Python subclasses of UIDrawable types
|
on_exit, on_move) directly in Python subclasses of UIDrawable types
|
||||||
(Frame, Caption, Sprite, Grid, Line, Circle, Arc).
|
(Frame, Caption, Sprite, Grid, Line, Circle, Arc).
|
||||||
|
|
||||||
Callback signatures:
|
Callback signature: (pos: Vector, button: MouseButton, action: InputState)
|
||||||
- on_click: (pos: Vector, button: MouseButton, action: InputState)
|
This matches property callbacks for consistency.
|
||||||
- on_enter/on_exit/on_move: (pos: Vector) - #230: simplified to position-only
|
|
||||||
"""
|
"""
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -42,7 +41,6 @@ class ClickableFrame(mcrfpy.Frame):
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Test 2: Frame subclass with all hover callbacks
|
# Test 2: Frame subclass with all hover callbacks
|
||||||
# #230: Hover callbacks now take only (pos), not (pos, button, action)
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
class HoverFrame(mcrfpy.Frame):
|
class HoverFrame(mcrfpy.Frame):
|
||||||
"""Frame subclass with on_enter, on_exit, on_move"""
|
"""Frame subclass with on_enter, on_exit, on_move"""
|
||||||
|
|
@ -50,13 +48,13 @@ class HoverFrame(mcrfpy.Frame):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.events = []
|
self.events = []
|
||||||
|
|
||||||
def on_enter(self, pos):
|
def on_enter(self, pos, button, action):
|
||||||
self.events.append(('enter', pos.x, pos.y))
|
self.events.append(('enter', pos.x, pos.y))
|
||||||
|
|
||||||
def on_exit(self, pos):
|
def on_exit(self, pos, button, action):
|
||||||
self.events.append(('exit', pos.x, pos.y))
|
self.events.append(('exit', pos.x, pos.y))
|
||||||
|
|
||||||
def on_move(self, pos):
|
def on_move(self, pos, button, action):
|
||||||
self.events.append(('move', pos.x, pos.y))
|
self.events.append(('move', pos.x, pos.y))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -266,12 +264,11 @@ except Exception as e:
|
||||||
test_failed("Subclass methods are callable and work", e)
|
test_failed("Subclass methods are callable and work", e)
|
||||||
|
|
||||||
# Test 11: Verify HoverFrame methods work with typed arguments
|
# Test 11: Verify HoverFrame methods work with typed arguments
|
||||||
# #230: Hover callbacks now take only (pos)
|
|
||||||
try:
|
try:
|
||||||
hover = HoverFrame(pos=(250, 100), size=(100, 100))
|
hover = HoverFrame(pos=(250, 100), size=(100, 100))
|
||||||
hover.on_enter(mcrfpy.Vector(10.0, 20.0))
|
hover.on_enter(mcrfpy.Vector(10.0, 20.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
hover.on_exit(mcrfpy.Vector(30.0, 40.0))
|
hover.on_exit(mcrfpy.Vector(30.0, 40.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
hover.on_move(mcrfpy.Vector(50.0, 60.0))
|
hover.on_move(mcrfpy.Vector(50.0, 60.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
assert len(hover.events) == 3, f"Should have 3 events, got {len(hover.events)}"
|
assert len(hover.events) == 3, f"Should have 3 events, got {len(hover.events)}"
|
||||||
assert hover.events[0] == ('enter', 10.0, 20.0), f"Event mismatch: {hover.events[0]}"
|
assert hover.events[0] == ('enter', 10.0, 20.0), f"Event mismatch: {hover.events[0]}"
|
||||||
assert hover.events[1] == ('exit', 30.0, 40.0), f"Event mismatch: {hover.events[1]}"
|
assert hover.events[1] == ('exit', 30.0, 40.0), f"Event mismatch: {hover.events[1]}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue