Compare commits
3 commits
da434dcc64
...
c7cf3f0e5b
| Author | SHA1 | Date | |
|---|---|---|---|
| c7cf3f0e5b | |||
| 86bfebefcb | |||
| 16b5508233 |
14 changed files with 737 additions and 93 deletions
|
|
@ -10,9 +10,10 @@ static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
|
||||||
PyObject* ptr = self->data->click_callable->borrow();
|
PyObject* ptr = self->data->click_callable->borrow();
|
||||||
if (ptr && ptr != Py_None)
|
if (ptr && ptr != Py_None) {
|
||||||
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
||||||
return ptr;
|
return ptr;
|
||||||
else
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
|
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
|
||||||
#include "PythonObjectCache.h" // #184 - For subclass callback support
|
#include "PythonObjectCache.h" // #184 - For subclass callback support
|
||||||
|
#include "McRFPy_API.h" // For Vector type access
|
||||||
|
#include "PyMouseButton.h" // For MouseButton enum
|
||||||
|
#include "PyInputState.h" // For InputState enum
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
|
@ -15,6 +18,7 @@
|
||||||
|
|
||||||
// Try to call a Python method on a UIDrawable subclass
|
// Try to call a Python method on a UIDrawable subclass
|
||||||
// Returns true if a method was found and called, false otherwise
|
// Returns true if a method was found and called, false otherwise
|
||||||
|
// Signature matches property callbacks: (Vector, MouseButton, InputState)
|
||||||
static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name,
|
static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name,
|
||||||
sf::Vector2f mousepos, const char* button, const char* action) {
|
sf::Vector2f mousepos, const char* button, const char* action) {
|
||||||
if (!drawable->is_python_subclass) return false;
|
if (!drawable->is_python_subclass) return false;
|
||||||
|
|
@ -45,14 +49,69 @@ static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get and call the method
|
// Get the method
|
||||||
PyObject* method = PyObject_GetAttrString(pyObj, method_name);
|
PyObject* method = PyObject_GetAttrString(pyObj, method_name);
|
||||||
bool called = false;
|
bool called = false;
|
||||||
|
|
||||||
if (method && PyCallable_Check(method) && method != Py_None) {
|
if (method && PyCallable_Check(method) && method != Py_None) {
|
||||||
// Call with (x, y, button, action) signature
|
// Create Vector object for position (matches property callback signature)
|
||||||
PyObject* result = PyObject_CallFunction(method, "ffss",
|
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||||
mousepos.x, mousepos.y, button, action);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert button string to MouseButton enum
|
||||||
|
int button_val = 0;
|
||||||
|
if (strcmp(button, "left") == 0) button_val = 0;
|
||||||
|
else if (strcmp(button, "right") == 0) button_val = 1;
|
||||||
|
else if (strcmp(button, "middle") == 0) button_val = 2;
|
||||||
|
else if (strcmp(button, "x1") == 0) button_val = 3;
|
||||||
|
else if (strcmp(button, "x2") == 0) button_val = 4;
|
||||||
|
// For hover events, button might be "enter", "exit", "move" - use LEFT as default
|
||||||
|
|
||||||
|
PyObject* button_enum = nullptr;
|
||||||
|
if (PyMouseButton::mouse_button_enum_class) {
|
||||||
|
button_enum = PyObject_CallFunction(PyMouseButton::mouse_button_enum_class, "i", button_val);
|
||||||
|
}
|
||||||
|
if (!button_enum) {
|
||||||
|
PyErr_Clear();
|
||||||
|
button_enum = PyLong_FromLong(button_val); // Fallback to int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert action string to InputState enum
|
||||||
|
int action_val = (strcmp(action, "start") == 0) ? 0 : 1; // PRESSED=0, RELEASED=1
|
||||||
|
|
||||||
|
PyObject* action_enum = nullptr;
|
||||||
|
if (PyInputState::input_state_enum_class) {
|
||||||
|
action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val);
|
||||||
|
}
|
||||||
|
if (!action_enum) {
|
||||||
|
PyErr_Clear();
|
||||||
|
action_enum = PyLong_FromLong(action_val); // Fallback to int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call with (Vector, MouseButton, InputState) signature
|
||||||
|
PyObject* args = Py_BuildValue("(OOO)", pos, button_enum, action_enum);
|
||||||
|
Py_DECREF(pos);
|
||||||
|
Py_DECREF(button_enum);
|
||||||
|
Py_DECREF(action_enum);
|
||||||
|
|
||||||
|
PyObject* result = PyObject_Call(method, args, NULL);
|
||||||
|
Py_DECREF(args);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
Py_DECREF(result);
|
Py_DECREF(result);
|
||||||
called = true;
|
called = true;
|
||||||
|
|
|
||||||
41
src/UIArc.h
41
src/UIArc.h
|
|
@ -118,14 +118,21 @@ namespace mcrfpydef {
|
||||||
.tp_itemsize = 0,
|
.tp_itemsize = 0,
|
||||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||||
PyUIArcObject* obj = (PyUIArcObject*)self;
|
PyUIArcObject* obj = (PyUIArcObject*)self;
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
if (obj->weakreflist != NULL) {
|
if (obj->weakreflist != NULL) {
|
||||||
PyObject_ClearWeakRefs(self);
|
PyObject_ClearWeakRefs(self);
|
||||||
}
|
}
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)UIArc::repr,
|
.tp_repr = (reprfunc)UIArc::repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
.tp_doc = PyDoc_STR(
|
.tp_doc = PyDoc_STR(
|
||||||
"Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs)\n\n"
|
"Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs)\n\n"
|
||||||
"An arc UI element for drawing curved line segments.\n\n"
|
"An arc UI element for drawing curved line segments.\n\n"
|
||||||
|
|
@ -162,6 +169,38 @@ namespace mcrfpydef {
|
||||||
" horiz_margin (float): Horizontal margin override\n"
|
" horiz_margin (float): Horizontal margin override\n"
|
||||||
" vert_margin (float): Vertical margin override\n"
|
" vert_margin (float): Vertical margin override\n"
|
||||||
),
|
),
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyUIArcObject* obj = (PyUIArcObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* cb = obj->data->click_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* cb = obj->data->on_enter_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* cb = obj->data->on_exit_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* cb = obj->data->on_move_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyUIArcObject* obj = (PyUIArcObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
.tp_methods = UIArc_methods,
|
.tp_methods = UIArc_methods,
|
||||||
.tp_getset = UIArc::getsetters,
|
.tp_getset = UIArc::getsetters,
|
||||||
.tp_base = &mcrfpydef::PyDrawableType,
|
.tp_base = &mcrfpydef::PyDrawableType,
|
||||||
|
|
|
||||||
|
|
@ -59,13 +59,21 @@ namespace mcrfpydef {
|
||||||
.tp_dealloc = (destructor)[](PyObject* self)
|
.tp_dealloc = (destructor)[](PyObject* self)
|
||||||
{
|
{
|
||||||
PyUICaptionObject* obj = (PyUICaptionObject*)self;
|
PyUICaptionObject* obj = (PyUICaptionObject*)self;
|
||||||
|
// Untrack from GC before destroying
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
// Clear weak references
|
// Clear weak references
|
||||||
if (obj->weakreflist != NULL) {
|
if (obj->weakreflist != NULL) {
|
||||||
PyObject_ClearWeakRefs(self);
|
PyObject_ClearWeakRefs(self);
|
||||||
}
|
}
|
||||||
// TODO - reevaluate with PyFont usage; UICaption does not own the font
|
// Clear Python references to break cycles
|
||||||
// release reference to font object
|
if (obj->data) {
|
||||||
if (obj->font) Py_DECREF(obj->font);
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
// Release reference to font object
|
||||||
|
Py_CLEAR(obj->font);
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
|
|
@ -73,7 +81,7 @@ namespace mcrfpydef {
|
||||||
//.tp_hash = NULL,
|
//.tp_hash = NULL,
|
||||||
//.tp_iter
|
//.tp_iter
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
.tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n"
|
.tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n"
|
||||||
"A text display UI element with customizable font and styling.\n\n"
|
"A text display UI element with customizable font and styling.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
|
|
@ -114,6 +122,42 @@ namespace mcrfpydef {
|
||||||
" margin (float): General margin for alignment\n"
|
" margin (float): General margin for alignment\n"
|
||||||
" horiz_margin (float): Horizontal margin override\n"
|
" horiz_margin (float): Horizontal margin override\n"
|
||||||
" vert_margin (float): Vertical margin override"),
|
" vert_margin (float): Vertical margin override"),
|
||||||
|
// tp_traverse visits Python object references for GC cycle detection
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyUICaptionObject* obj = (PyUICaptionObject*)self;
|
||||||
|
Py_VISIT(obj->font);
|
||||||
|
if (obj->data) {
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* callback = obj->data->click_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* callback = obj->data->on_enter_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* callback = obj->data->on_exit_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* callback = obj->data->on_move_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
// tp_clear breaks reference cycles by clearing Python references
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyUICaptionObject* obj = (PyUICaptionObject*)self;
|
||||||
|
Py_CLEAR(obj->font);
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
.tp_methods = UICaption_methods,
|
.tp_methods = UICaption_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
.tp_getset = UICaption::getsetters,
|
.tp_getset = UICaption::getsetters,
|
||||||
|
|
|
||||||
|
|
@ -107,14 +107,21 @@ namespace mcrfpydef {
|
||||||
.tp_itemsize = 0,
|
.tp_itemsize = 0,
|
||||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||||
PyUICircleObject* obj = (PyUICircleObject*)self;
|
PyUICircleObject* obj = (PyUICircleObject*)self;
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
if (obj->weakreflist != NULL) {
|
if (obj->weakreflist != NULL) {
|
||||||
PyObject_ClearWeakRefs(self);
|
PyObject_ClearWeakRefs(self);
|
||||||
}
|
}
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)UICircle::repr,
|
.tp_repr = (reprfunc)UICircle::repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
.tp_doc = PyDoc_STR(
|
.tp_doc = PyDoc_STR(
|
||||||
"Circle(radius=0, center=None, fill_color=None, outline_color=None, outline=0, **kwargs)\n\n"
|
"Circle(radius=0, center=None, fill_color=None, outline_color=None, outline=0, **kwargs)\n\n"
|
||||||
"A circle UI element for drawing filled or outlined circles.\n\n"
|
"A circle UI element for drawing filled or outlined circles.\n\n"
|
||||||
|
|
@ -149,6 +156,38 @@ namespace mcrfpydef {
|
||||||
" horiz_margin (float): Horizontal margin override\n"
|
" horiz_margin (float): Horizontal margin override\n"
|
||||||
" vert_margin (float): Vertical margin override\n"
|
" vert_margin (float): Vertical margin override\n"
|
||||||
),
|
),
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyUICircleObject* obj = (PyUICircleObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* cb = obj->data->click_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* cb = obj->data->on_enter_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* cb = obj->data->on_exit_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* cb = obj->data->on_move_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyUICircleObject* obj = (PyUICircleObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
.tp_methods = UICircle_methods,
|
.tp_methods = UICircle_methods,
|
||||||
.tp_getset = UICircle::getsetters,
|
.tp_getset = UICircle::getsetters,
|
||||||
.tp_base = &mcrfpydef::PyDrawableType,
|
.tp_base = &mcrfpydef::PyDrawableType,
|
||||||
|
|
|
||||||
|
|
@ -259,10 +259,11 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
|
||||||
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
|
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (ptr && ptr != Py_None)
|
if (ptr && ptr != Py_None) {
|
||||||
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
||||||
return ptr;
|
return ptr;
|
||||||
else
|
}
|
||||||
return Py_None;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) {
|
int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) {
|
||||||
|
|
@ -1615,9 +1616,10 @@ PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) {
|
||||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (ptr && ptr != Py_None)
|
if (ptr && ptr != Py_None) {
|
||||||
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
||||||
return ptr;
|
return ptr;
|
||||||
else
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1698,9 +1700,10 @@ PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) {
|
||||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (ptr && ptr != Py_None)
|
if (ptr && ptr != Py_None) {
|
||||||
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
||||||
return ptr;
|
return ptr;
|
||||||
else
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1790,9 +1793,10 @@ PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) {
|
||||||
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (ptr && ptr != Py_None)
|
if (ptr && ptr != Py_None) {
|
||||||
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
||||||
return ptr;
|
return ptr;
|
||||||
else
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,19 @@ namespace mcrfpydef {
|
||||||
.tp_dealloc = (destructor)[](PyObject* self)
|
.tp_dealloc = (destructor)[](PyObject* self)
|
||||||
{
|
{
|
||||||
PyUIFrameObject* obj = (PyUIFrameObject*)self;
|
PyUIFrameObject* obj = (PyUIFrameObject*)self;
|
||||||
|
// Untrack from GC before destroying
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
// Clear weak references
|
// Clear weak references
|
||||||
if (obj->weakreflist != NULL) {
|
if (obj->weakreflist != NULL) {
|
||||||
PyObject_ClearWeakRefs(self);
|
PyObject_ClearWeakRefs(self);
|
||||||
}
|
}
|
||||||
|
// Clear Python references to break cycles
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
|
|
@ -95,7 +104,7 @@ namespace mcrfpydef {
|
||||||
//.tp_hash = NULL,
|
//.tp_hash = NULL,
|
||||||
//.tp_iter
|
//.tp_iter
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
.tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n"
|
.tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n"
|
||||||
"A rectangular frame UI element that can contain other drawable elements.\n\n"
|
"A rectangular frame UI element that can contain other drawable elements.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
|
|
@ -139,6 +148,49 @@ namespace mcrfpydef {
|
||||||
" margin (float): General margin for alignment\n"
|
" margin (float): General margin for alignment\n"
|
||||||
" horiz_margin (float): Horizontal margin override\n"
|
" horiz_margin (float): Horizontal margin override\n"
|
||||||
" vert_margin (float): Vertical margin override"),
|
" vert_margin (float): Vertical margin override"),
|
||||||
|
// tp_traverse visits Python object references for GC cycle detection
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyUIFrameObject* obj = (PyUIFrameObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
// Visit callback references
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* callback = obj->data->click_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* callback = obj->data->on_enter_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* callback = obj->data->on_exit_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* callback = obj->data->on_move_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
// tp_clear breaks reference cycles by clearing Python references
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyUIFrameObject* obj = (PyUIFrameObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
.tp_methods = UIFrame_methods,
|
.tp_methods = UIFrame_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
.tp_getset = UIFrame::getsetters,
|
.tp_getset = UIFrame::getsetters,
|
||||||
|
|
@ -150,6 +202,9 @@ namespace mcrfpydef {
|
||||||
if (self) {
|
if (self) {
|
||||||
self->data = std::make_shared<UIFrame>();
|
self->data = std::make_shared<UIFrame>();
|
||||||
self->weakreflist = nullptr;
|
self->weakreflist = nullptr;
|
||||||
|
// Note: For GC types, tracking happens automatically via tp_alloc
|
||||||
|
// when Py_TPFLAGS_HAVE_GC is set. Do NOT call PyObject_GC_Track here
|
||||||
|
// as it would double-track and cause corruption.
|
||||||
}
|
}
|
||||||
return (PyObject*)self;
|
return (PyObject*)self;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
src/UIGrid.h
66
src/UIGrid.h
|
|
@ -235,16 +235,29 @@ namespace mcrfpydef {
|
||||||
.tp_dealloc = (destructor)[](PyObject* self)
|
.tp_dealloc = (destructor)[](PyObject* self)
|
||||||
{
|
{
|
||||||
PyUIGridObject* obj = (PyUIGridObject*)self;
|
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||||
|
// Untrack from GC before destroying
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
// Clear weak references
|
// Clear weak references
|
||||||
if (obj->weakreflist != NULL) {
|
if (obj->weakreflist != NULL) {
|
||||||
PyObject_ClearWeakRefs(self);
|
PyObject_ClearWeakRefs(self);
|
||||||
}
|
}
|
||||||
|
// Clear Python references to break cycles
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
// Grid-specific cell callbacks
|
||||||
|
obj->data->on_cell_enter_callable.reset();
|
||||||
|
obj->data->on_cell_exit_callable.reset();
|
||||||
|
obj->data->on_cell_click_callable.reset();
|
||||||
|
}
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)UIGrid::repr,
|
.tp_repr = (reprfunc)UIGrid::repr,
|
||||||
.tp_as_mapping = &UIGrid::mpmethods, // Enable grid[x, y] subscript access
|
.tp_as_mapping = &UIGrid::mpmethods, // Enable grid[x, y] subscript access
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
|
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
|
||||||
"A grid-based UI element for tile-based rendering and entity management.\n\n"
|
"A grid-based UI element for tile-based rendering and entity management.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
|
|
@ -296,6 +309,57 @@ namespace mcrfpydef {
|
||||||
" margin (float): General margin for alignment\n"
|
" margin (float): General margin for alignment\n"
|
||||||
" horiz_margin (float): Horizontal margin override\n"
|
" horiz_margin (float): Horizontal margin override\n"
|
||||||
" vert_margin (float): Vertical margin override"),
|
" vert_margin (float): Vertical margin override"),
|
||||||
|
// tp_traverse visits Python object references for GC cycle detection
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
// Base class callbacks
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* callback = obj->data->click_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* callback = obj->data->on_enter_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* callback = obj->data->on_exit_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* callback = obj->data->on_move_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
// Grid-specific cell callbacks
|
||||||
|
if (obj->data->on_cell_enter_callable) {
|
||||||
|
PyObject* callback = obj->data->on_cell_enter_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_cell_exit_callable) {
|
||||||
|
PyObject* callback = obj->data->on_cell_exit_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_cell_click_callable) {
|
||||||
|
PyObject* callback = obj->data->on_cell_click_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
// tp_clear breaks reference cycles by clearing Python references
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
obj->data->on_cell_enter_callable.reset();
|
||||||
|
obj->data->on_cell_exit_callable.reset();
|
||||||
|
obj->data->on_cell_click_callable.reset();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
.tp_methods = UIGrid_all_methods,
|
.tp_methods = UIGrid_all_methods,
|
||||||
//.tp_members = UIGrid::members,
|
//.tp_members = UIGrid::members,
|
||||||
.tp_getset = UIGrid::getsetters,
|
.tp_getset = UIGrid::getsetters,
|
||||||
|
|
|
||||||
41
src/UILine.h
41
src/UILine.h
|
|
@ -104,14 +104,21 @@ namespace mcrfpydef {
|
||||||
.tp_itemsize = 0,
|
.tp_itemsize = 0,
|
||||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||||
PyUILineObject* obj = (PyUILineObject*)self;
|
PyUILineObject* obj = (PyUILineObject*)self;
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
if (obj->weakreflist != NULL) {
|
if (obj->weakreflist != NULL) {
|
||||||
PyObject_ClearWeakRefs(self);
|
PyObject_ClearWeakRefs(self);
|
||||||
}
|
}
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)UILine::repr,
|
.tp_repr = (reprfunc)UILine::repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
.tp_doc = PyDoc_STR(
|
.tp_doc = PyDoc_STR(
|
||||||
"Line(start=None, end=None, thickness=1.0, color=None, **kwargs)\n\n"
|
"Line(start=None, end=None, thickness=1.0, color=None, **kwargs)\n\n"
|
||||||
"A line UI element for drawing straight lines between two points.\n\n"
|
"A line UI element for drawing straight lines between two points.\n\n"
|
||||||
|
|
@ -144,6 +151,38 @@ namespace mcrfpydef {
|
||||||
" horiz_margin (float): Horizontal margin override\n"
|
" horiz_margin (float): Horizontal margin override\n"
|
||||||
" vert_margin (float): Vertical margin override\n"
|
" vert_margin (float): Vertical margin override\n"
|
||||||
),
|
),
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyUILineObject* obj = (PyUILineObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* cb = obj->data->click_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* cb = obj->data->on_enter_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* cb = obj->data->on_exit_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* cb = obj->data->on_move_callable->borrow();
|
||||||
|
if (cb && cb != Py_None) Py_VISIT(cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyUILineObject* obj = (PyUILineObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
.tp_methods = UILine_methods,
|
.tp_methods = UILine_methods,
|
||||||
.tp_getset = UILine::getsetters,
|
.tp_getset = UILine::getsetters,
|
||||||
.tp_base = &mcrfpydef::PyDrawableType,
|
.tp_base = &mcrfpydef::PyDrawableType,
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,19 @@ namespace mcrfpydef {
|
||||||
.tp_dealloc = (destructor)[](PyObject* self)
|
.tp_dealloc = (destructor)[](PyObject* self)
|
||||||
{
|
{
|
||||||
PyUISpriteObject* obj = (PyUISpriteObject*)self;
|
PyUISpriteObject* obj = (PyUISpriteObject*)self;
|
||||||
|
// Untrack from GC before destroying
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
// Clear weak references
|
// Clear weak references
|
||||||
if (obj->weakreflist != NULL) {
|
if (obj->weakreflist != NULL) {
|
||||||
PyObject_ClearWeakRefs(self);
|
PyObject_ClearWeakRefs(self);
|
||||||
}
|
}
|
||||||
// release reference to font object
|
// Clear Python references to break cycles
|
||||||
//if (obj->texture) Py_DECREF(obj->texture);
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
|
|
@ -104,7 +111,7 @@ namespace mcrfpydef {
|
||||||
//.tp_hash = NULL,
|
//.tp_hash = NULL,
|
||||||
//.tp_iter
|
//.tp_iter
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
.tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
|
.tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
|
||||||
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
|
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
|
|
@ -143,6 +150,40 @@ namespace mcrfpydef {
|
||||||
" margin (float): General margin for alignment\n"
|
" margin (float): General margin for alignment\n"
|
||||||
" horiz_margin (float): Horizontal margin override\n"
|
" horiz_margin (float): Horizontal margin override\n"
|
||||||
" vert_margin (float): Vertical margin override"),
|
" vert_margin (float): Vertical margin override"),
|
||||||
|
// tp_traverse visits Python object references for GC cycle detection
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyUISpriteObject* obj = (PyUISpriteObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* callback = obj->data->click_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* callback = obj->data->on_enter_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* callback = obj->data->on_exit_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* callback = obj->data->on_move_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
// tp_clear breaks reference cycles by clearing Python references
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyUISpriteObject* obj = (PyUISpriteObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
.tp_methods = UISprite_methods,
|
.tp_methods = UISprite_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
.tp_getset = UISprite::getsetters,
|
.tp_getset = UISprite::getsetters,
|
||||||
|
|
|
||||||
157
tests/regression/issue_callback_refcount_test.py
Normal file
157
tests/regression/issue_callback_refcount_test.py
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test for callback property reference counting fix.
|
||||||
|
|
||||||
|
This test verifies that accessing callback properties (on_click, on_enter, etc.)
|
||||||
|
returns correctly reference-counted objects, preventing use-after-free bugs.
|
||||||
|
|
||||||
|
The bug: Callback getters were returning borrowed references instead of new
|
||||||
|
references, causing objects to be freed prematurely when Python DECREFs them.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
import gc
|
||||||
|
|
||||||
|
def test_callback_refcount():
|
||||||
|
"""Test that callback getters return new references."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Create a scene
|
||||||
|
scene = mcrfpy.Scene("test_callback_refcount")
|
||||||
|
|
||||||
|
# Test Frame
|
||||||
|
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
|
# Set a callback
|
||||||
|
def my_callback(pos, button, action):
|
||||||
|
pass
|
||||||
|
|
||||||
|
frame.on_click = my_callback
|
||||||
|
|
||||||
|
# Read the callback back multiple times
|
||||||
|
# If borrowing incorrectly, this could cause use-after-free
|
||||||
|
for i in range(10):
|
||||||
|
cb = frame.on_click
|
||||||
|
if cb is None:
|
||||||
|
errors.append(f"on_click returned None on iteration {i}")
|
||||||
|
break
|
||||||
|
if not callable(cb):
|
||||||
|
errors.append(f"on_click returned non-callable on iteration {i}: {type(cb)}")
|
||||||
|
break
|
||||||
|
# Explicitly delete to trigger any refcount issues
|
||||||
|
del cb
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
# Final check - should still return the callback
|
||||||
|
final_cb = frame.on_click
|
||||||
|
if final_cb is None:
|
||||||
|
errors.append("on_click returned None after repeated access")
|
||||||
|
elif not callable(final_cb):
|
||||||
|
errors.append(f"on_click returned non-callable after repeated access: {type(final_cb)}")
|
||||||
|
|
||||||
|
# Test on_enter, on_exit, on_move
|
||||||
|
frame.on_enter = lambda pos, button, action: None
|
||||||
|
frame.on_exit = lambda pos, button, action: None
|
||||||
|
frame.on_move = lambda pos, button, action: None
|
||||||
|
|
||||||
|
for name in ['on_enter', 'on_exit', 'on_move']:
|
||||||
|
for i in range(5):
|
||||||
|
cb = getattr(frame, name)
|
||||||
|
if cb is None:
|
||||||
|
errors.append(f"{name} returned None on iteration {i}")
|
||||||
|
break
|
||||||
|
del cb
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_grid_cell_callbacks():
|
||||||
|
"""Test Grid cell callback getters (these were already correct)."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(pos=(0, 0), size=(100, 100), grid_size=(5, 5),
|
||||||
|
texture=mcrfpy.default_texture, zoom=1.0)
|
||||||
|
|
||||||
|
grid.on_cell_enter = lambda pos: None
|
||||||
|
grid.on_cell_exit = lambda pos: None
|
||||||
|
grid.on_cell_click = lambda pos: None
|
||||||
|
|
||||||
|
for name in ['on_cell_enter', 'on_cell_exit', 'on_cell_click']:
|
||||||
|
for i in range(5):
|
||||||
|
cb = getattr(grid, name)
|
||||||
|
if cb is None:
|
||||||
|
errors.append(f"{name} returned None on iteration {i}")
|
||||||
|
break
|
||||||
|
del cb
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_subclass_callback():
|
||||||
|
"""Test callback access on Python subclasses."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
class MyFrame(mcrfpy.Frame):
|
||||||
|
pass
|
||||||
|
|
||||||
|
obj = MyFrame(pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
|
# Set callback via property
|
||||||
|
obj.on_click = lambda pos, button, action: print("clicked")
|
||||||
|
|
||||||
|
# Read back multiple times
|
||||||
|
for i in range(5):
|
||||||
|
cb = obj.on_click
|
||||||
|
if cb is None:
|
||||||
|
errors.append(f"Subclass on_click returned None on iteration {i}")
|
||||||
|
break
|
||||||
|
if not callable(cb):
|
||||||
|
errors.append(f"Subclass on_click returned non-callable: {type(cb)}")
|
||||||
|
break
|
||||||
|
del cb
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
"""Run all callback refcount tests."""
|
||||||
|
all_errors = []
|
||||||
|
|
||||||
|
print("Testing callback property refcount...")
|
||||||
|
errors = test_callback_refcount()
|
||||||
|
if errors:
|
||||||
|
all_errors.extend(errors)
|
||||||
|
print(f" FAIL: {len(errors)} errors")
|
||||||
|
else:
|
||||||
|
print(" PASS: on_click, on_enter, on_exit, on_move")
|
||||||
|
|
||||||
|
print("Testing Grid cell callbacks...")
|
||||||
|
errors = test_grid_cell_callbacks()
|
||||||
|
if errors:
|
||||||
|
all_errors.extend(errors)
|
||||||
|
print(f" FAIL: {len(errors)} errors")
|
||||||
|
else:
|
||||||
|
print(" PASS: on_cell_enter, on_cell_exit, on_cell_click")
|
||||||
|
|
||||||
|
print("Testing subclass callbacks...")
|
||||||
|
errors = test_subclass_callback()
|
||||||
|
if errors:
|
||||||
|
all_errors.extend(errors)
|
||||||
|
print(f" FAIL: {len(errors)} errors")
|
||||||
|
else:
|
||||||
|
print(" PASS: MyFrame(Frame) subclass")
|
||||||
|
|
||||||
|
if all_errors:
|
||||||
|
print(f"\nFAILED with {len(all_errors)} errors:")
|
||||||
|
for e in all_errors:
|
||||||
|
print(f" - {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("\nAll callback refcount tests PASSED")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_tests()
|
||||||
87
tests/regression/subclass_callback_segfault_test.py
Normal file
87
tests/regression/subclass_callback_segfault_test.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Minimal reproduction of segfault when calling subclass method callback.
|
||||||
|
|
||||||
|
The issue: When a Frame subclass assigns self.on_click = self._on_click,
|
||||||
|
reading it back works but there's a segfault during cleanup.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
import gc
|
||||||
|
|
||||||
|
class MyFrame(mcrfpy.Frame):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.on_click = self._on_click
|
||||||
|
|
||||||
|
def _on_click(self, pos, button, action):
|
||||||
|
print(f"Clicked at {pos}, button={button}, action={action}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_minimal():
|
||||||
|
"""Minimal test case."""
|
||||||
|
print("Creating MyFrame...")
|
||||||
|
obj = MyFrame(pos=(100, 100), size=(100, 100))
|
||||||
|
|
||||||
|
print(f"Reading on_click: {obj.on_click}")
|
||||||
|
print(f"Type: {type(obj.on_click)}")
|
||||||
|
|
||||||
|
print("Attempting to call on_click...")
|
||||||
|
try:
|
||||||
|
obj.on_click((50, 50), "left", "start")
|
||||||
|
print("Call succeeded!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
print("Clearing callback...")
|
||||||
|
obj.on_click = None
|
||||||
|
|
||||||
|
print("Deleting object...")
|
||||||
|
del obj
|
||||||
|
|
||||||
|
print("Running GC...")
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
print("About to exit...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_without_callback_clear():
|
||||||
|
"""Test without clearing callback first."""
|
||||||
|
print("Creating MyFrame...")
|
||||||
|
obj = MyFrame(pos=(100, 100), size=(100, 100))
|
||||||
|
|
||||||
|
print("Calling...")
|
||||||
|
obj.on_click((50, 50), "left", "start")
|
||||||
|
|
||||||
|
print("Deleting without clearing callback...")
|
||||||
|
del obj
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
print("About to exit...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_added_to_scene():
|
||||||
|
"""Test when added to scene."""
|
||||||
|
print("Creating scene and MyFrame...")
|
||||||
|
scene = mcrfpy.Scene("test")
|
||||||
|
obj = MyFrame(pos=(100, 100), size=(100, 100))
|
||||||
|
scene.children.append(obj)
|
||||||
|
|
||||||
|
print("Calling via scene.children[0]...")
|
||||||
|
scene.children[0].on_click((50, 50), "left", "start")
|
||||||
|
|
||||||
|
print("About to exit...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Try different scenarios
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if sys.argv[1] == "2":
|
||||||
|
test_without_callback_clear()
|
||||||
|
elif sys.argv[1] == "3":
|
||||||
|
test_added_to_scene()
|
||||||
|
else:
|
||||||
|
test_minimal()
|
||||||
|
|
@ -5,6 +5,9 @@ Test monkey-patching support for UIDrawable subclass callbacks (#184)
|
||||||
This tests that users can dynamically add callback methods at runtime
|
This tests that users can dynamically add callback methods at runtime
|
||||||
(monkey-patching) and have them work correctly with the callback cache
|
(monkey-patching) and have them work correctly with the callback cache
|
||||||
invalidation system.
|
invalidation system.
|
||||||
|
|
||||||
|
Callback signature: (pos: Vector, button: MouseButton, action: InputState)
|
||||||
|
This matches property callbacks for consistency.
|
||||||
"""
|
"""
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -21,6 +24,12 @@ def test_failed(name, error):
|
||||||
print(f" FAIL: {name}: {error}")
|
print(f" FAIL: {name}: {error}")
|
||||||
|
|
||||||
|
|
||||||
|
# Helper to create typed callback arguments
|
||||||
|
def make_click_args(x=0.0, y=0.0):
|
||||||
|
"""Create properly typed callback arguments for testing."""
|
||||||
|
return (mcrfpy.Vector(x, y), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Test Classes
|
# Test Classes
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
@ -38,8 +47,8 @@ class PartialFrame(mcrfpy.Frame):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.call_log = []
|
self.call_log = []
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.call_log.append(('click', x, y))
|
self.call_log.append(('click', pos.x, pos.y))
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
@ -59,8 +68,8 @@ try:
|
||||||
"EmptyFrame should not have on_click in its own __dict__ initially"
|
"EmptyFrame should not have on_click in its own __dict__ initially"
|
||||||
|
|
||||||
# Add on_click method to class dynamically
|
# Add on_click method to class dynamically
|
||||||
def dynamic_on_click(self, x, y, button, action):
|
def dynamic_on_click(self, pos, button, action):
|
||||||
self.call_log.append(('dynamic_click', x, y))
|
self.call_log.append(('dynamic_click', pos.x, pos.y))
|
||||||
|
|
||||||
EmptyFrame.on_click = dynamic_on_click
|
EmptyFrame.on_click = dynamic_on_click
|
||||||
|
|
||||||
|
|
@ -68,13 +77,13 @@ try:
|
||||||
assert 'on_click' in EmptyFrame.__dict__, "EmptyFrame should now have on_click in __dict__"
|
assert 'on_click' in EmptyFrame.__dict__, "EmptyFrame should now have on_click in __dict__"
|
||||||
|
|
||||||
# Test calling the method directly
|
# Test calling the method directly
|
||||||
frame1.on_click(10.0, 20.0, "left", "start")
|
frame1.on_click(*make_click_args(10.0, 20.0))
|
||||||
assert ('dynamic_click', 10.0, 20.0) in frame1.call_log, \
|
assert ('dynamic_click', 10.0, 20.0) in frame1.call_log, \
|
||||||
f"Dynamic method should have been called, log: {frame1.call_log}"
|
f"Dynamic method should have been called, log: {frame1.call_log}"
|
||||||
|
|
||||||
# Create new instance - should also have the method
|
# Create new instance - should also have the method
|
||||||
frame2 = EmptyFrame(pos=(0, 0), size=(100, 100))
|
frame2 = EmptyFrame(pos=(0, 0), size=(100, 100))
|
||||||
frame2.on_click(30.0, 40.0, "left", "start")
|
frame2.on_click(*make_click_args(30.0, 40.0))
|
||||||
assert ('dynamic_click', 30.0, 40.0) in frame2.call_log, \
|
assert ('dynamic_click', 30.0, 40.0) in frame2.call_log, \
|
||||||
f"New instance should have dynamic method, log: {frame2.call_log}"
|
f"New instance should have dynamic method, log: {frame2.call_log}"
|
||||||
|
|
||||||
|
|
@ -87,20 +96,20 @@ try:
|
||||||
frame = PartialFrame(pos=(0, 0), size=(100, 100))
|
frame = PartialFrame(pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
# Call original method
|
# Call original method
|
||||||
frame.on_click(1.0, 2.0, "left", "start")
|
frame.on_click(*make_click_args(1.0, 2.0))
|
||||||
assert ('click', 1.0, 2.0) in frame.call_log, \
|
assert ('click', 1.0, 2.0) in frame.call_log, \
|
||||||
f"Original method should work, log: {frame.call_log}"
|
f"Original method should work, log: {frame.call_log}"
|
||||||
|
|
||||||
frame.call_log.clear()
|
frame.call_log.clear()
|
||||||
|
|
||||||
# Replace the method
|
# Replace the method
|
||||||
def new_on_click(self, x, y, button, action):
|
def new_on_click(self, pos, button, action):
|
||||||
self.call_log.append(('replaced_click', x, y))
|
self.call_log.append(('replaced_click', pos.x, pos.y))
|
||||||
|
|
||||||
PartialFrame.on_click = new_on_click
|
PartialFrame.on_click = new_on_click
|
||||||
|
|
||||||
# Call again - should use new method
|
# Call again - should use new method
|
||||||
frame.on_click(3.0, 4.0, "left", "start")
|
frame.on_click(*make_click_args(3.0, 4.0))
|
||||||
assert ('replaced_click', 3.0, 4.0) in frame.call_log, \
|
assert ('replaced_click', 3.0, 4.0) in frame.call_log, \
|
||||||
f"Replaced method should work, log: {frame.call_log}"
|
f"Replaced method should work, log: {frame.call_log}"
|
||||||
|
|
||||||
|
|
@ -119,20 +128,17 @@ try:
|
||||||
frame_a = FreshFrame(pos=(0, 0), size=(100, 100))
|
frame_a = FreshFrame(pos=(0, 0), size=(100, 100))
|
||||||
frame_b = FreshFrame(pos=(0, 0), size=(100, 100))
|
frame_b = FreshFrame(pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
# Add method to instance only
|
# Add method to instance only (property callback style - no self)
|
||||||
def instance_on_click(x, y, button, action):
|
def instance_on_click(pos, button, action):
|
||||||
frame_a.instance_log.append(('instance_click', x, y))
|
frame_a.instance_log.append(('instance_click', pos.x, pos.y))
|
||||||
|
|
||||||
frame_a.on_click = instance_on_click
|
frame_a.on_click = instance_on_click
|
||||||
|
|
||||||
# frame_a should have the method
|
# frame_a should have the method
|
||||||
assert hasattr(frame_a, 'on_click'), "frame_a should have on_click"
|
assert hasattr(frame_a, 'on_click'), "frame_a should have on_click"
|
||||||
|
|
||||||
# frame_b should NOT have the method (unless inherited from class)
|
|
||||||
# Actually, both will have on_click now since it's an instance attribute
|
|
||||||
|
|
||||||
# Verify instance method works
|
# Verify instance method works
|
||||||
frame_a.on_click(5.0, 6.0, "left", "start")
|
frame_a.on_click(*make_click_args(5.0, 6.0))
|
||||||
assert ('instance_click', 5.0, 6.0) in frame_a.instance_log, \
|
assert ('instance_click', 5.0, 6.0) in frame_a.instance_log, \
|
||||||
f"Instance method should work, log: {frame_a.instance_log}"
|
f"Instance method should work, log: {frame_a.instance_log}"
|
||||||
|
|
||||||
|
|
@ -150,7 +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
|
||||||
def tracked_on_enter(self, x, y, button, action):
|
def tracked_on_enter(self, pos, button, action):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
TrackedFrame.on_enter = tracked_on_enter
|
TrackedFrame.on_enter = tracked_on_enter
|
||||||
|
|
@ -174,30 +180,30 @@ try:
|
||||||
frame = MultiCallbackFrame(pos=(0, 0), size=(100, 100))
|
frame = MultiCallbackFrame(pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
# Add on_click
|
# Add on_click
|
||||||
def multi_on_click(self, x, y, button, action):
|
def multi_on_click(self, pos, button, action):
|
||||||
self.events.append('click')
|
self.events.append('click')
|
||||||
MultiCallbackFrame.on_click = multi_on_click
|
MultiCallbackFrame.on_click = multi_on_click
|
||||||
|
|
||||||
# Add on_enter
|
# Add on_enter
|
||||||
def multi_on_enter(self, x, y, button, action):
|
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
|
# Add on_exit
|
||||||
def multi_on_exit(self, x, y, button, action):
|
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
|
# Add on_move
|
||||||
def multi_on_move(self, x, y, button, action):
|
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(0, 0, "left", "start")
|
frame.on_click(*make_click_args())
|
||||||
frame.on_enter(0, 0, "enter", "start")
|
frame.on_enter(*make_click_args())
|
||||||
frame.on_exit(0, 0, "exit", "start")
|
frame.on_exit(*make_click_args())
|
||||||
frame.on_move(0, 0, "move", "start")
|
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}"
|
||||||
|
|
@ -213,7 +219,7 @@ try:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicked = False
|
self.clicked = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicked = True
|
self.clicked = True
|
||||||
|
|
||||||
frame = DeletableFrame(pos=(0, 0), size=(100, 100))
|
frame = DeletableFrame(pos=(0, 0), size=(100, 100))
|
||||||
|
|
@ -222,7 +228,7 @@ try:
|
||||||
assert 'on_click' in DeletableFrame.__dict__, "Should have on_click in __dict__ initially"
|
assert 'on_click' in DeletableFrame.__dict__, "Should have on_click in __dict__ initially"
|
||||||
|
|
||||||
# Call it
|
# Call it
|
||||||
frame.on_click(0, 0, "left", "start")
|
frame.on_click(*make_click_args())
|
||||||
assert frame.clicked, "Method should work"
|
assert frame.clicked, "Method should work"
|
||||||
|
|
||||||
# Delete the method from subclass
|
# Delete the method from subclass
|
||||||
|
|
@ -246,13 +252,13 @@ try:
|
||||||
self.method_called = False
|
self.method_called = False
|
||||||
self.property_called = False
|
self.property_called = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.method_called = True
|
self.method_called = True
|
||||||
|
|
||||||
frame = MixedFrame(pos=(0, 0), size=(100, 100))
|
frame = MixedFrame(pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
# Set property callback
|
# Set property callback (no self parameter)
|
||||||
def prop_callback(x, y, button, action):
|
def prop_callback(pos, button, action):
|
||||||
frame.property_called = True
|
frame.property_called = True
|
||||||
|
|
||||||
frame.click = prop_callback
|
frame.click = prop_callback
|
||||||
|
|
@ -264,11 +270,11 @@ try:
|
||||||
assert hasattr(frame, 'on_click'), "on_click method should exist"
|
assert hasattr(frame, 'on_click'), "on_click method should exist"
|
||||||
|
|
||||||
# Can call method directly
|
# Can call method directly
|
||||||
frame.on_click(0, 0, "left", "start")
|
frame.on_click(*make_click_args())
|
||||||
assert frame.method_called, "Method should be callable directly"
|
assert frame.method_called, "Method should be callable directly"
|
||||||
|
|
||||||
# Can call property callback
|
# Can call property callback
|
||||||
frame.click(0, 0, "left", "start")
|
frame.click(*make_click_args())
|
||||||
assert frame.property_called, "Property callback should be callable"
|
assert frame.property_called, "Property callback should be callable"
|
||||||
|
|
||||||
test_passed("Property callback and method coexist")
|
test_passed("Property callback and method coexist")
|
||||||
|
|
@ -282,16 +288,16 @@ try:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicks = []
|
self.clicks = []
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicks.append('base')
|
self.clicks.append('base')
|
||||||
|
|
||||||
class DerivedClickable(BaseClickable):
|
class DerivedClickable(BaseClickable):
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
super().on_click(x, y, button, action)
|
super().on_click(pos, button, action)
|
||||||
self.clicks.append('derived')
|
self.clicks.append('derived')
|
||||||
|
|
||||||
frame = DerivedClickable(pos=(0, 0), size=(100, 100))
|
frame = DerivedClickable(pos=(0, 0), size=(100, 100))
|
||||||
frame.on_click(0, 0, "left", "start")
|
frame.on_click(*make_click_args())
|
||||||
|
|
||||||
assert frame.clicks == ['base', 'derived'], \
|
assert frame.clicks == ['base', 'derived'], \
|
||||||
f"Inheritance chain should work, got: {frame.clicks}"
|
f"Inheritance chain should work, got: {frame.clicks}"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ 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 signature: (pos: Vector, button: MouseButton, action: InputState)
|
||||||
|
This matches property callbacks for consistency.
|
||||||
"""
|
"""
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -31,9 +34,9 @@ class ClickableFrame(mcrfpy.Frame):
|
||||||
self.click_count = 0
|
self.click_count = 0
|
||||||
self.last_click_args = None
|
self.last_click_args = None
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.click_count += 1
|
self.click_count += 1
|
||||||
self.last_click_args = (x, y, button, action)
|
self.last_click_args = (pos, button, action)
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
@ -45,14 +48,14 @@ class HoverFrame(mcrfpy.Frame):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.events = []
|
self.events = []
|
||||||
|
|
||||||
def on_enter(self, x, y, button, action):
|
def on_enter(self, pos, button, action):
|
||||||
self.events.append(('enter', x, y))
|
self.events.append(('enter', pos.x, pos.y))
|
||||||
|
|
||||||
def on_exit(self, x, y, button, action):
|
def on_exit(self, pos, button, action):
|
||||||
self.events.append(('exit', x, y))
|
self.events.append(('exit', pos.x, pos.y))
|
||||||
|
|
||||||
def on_move(self, x, y, button, action):
|
def on_move(self, pos, button, action):
|
||||||
self.events.append(('move', x, y))
|
self.events.append(('move', pos.x, pos.y))
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
@ -64,7 +67,7 @@ class ClickableCaption(mcrfpy.Caption):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicked = False
|
self.clicked = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicked = True
|
self.clicked = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -77,7 +80,7 @@ class ClickableSprite(mcrfpy.Sprite):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicked = False
|
self.clicked = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicked = True
|
self.clicked = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,7 +93,7 @@ class ClickableGrid(mcrfpy.Grid):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicked = False
|
self.clicked = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicked = True
|
self.clicked = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -103,7 +106,7 @@ class ClickableCircle(mcrfpy.Circle):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicked = False
|
self.clicked = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicked = True
|
self.clicked = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -116,7 +119,7 @@ class ClickableLine(mcrfpy.Line):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicked = False
|
self.clicked = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicked = True
|
self.clicked = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -129,7 +132,7 @@ class ClickableArc(mcrfpy.Arc):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.clicked = False
|
self.clicked = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.clicked = True
|
self.clicked = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -143,7 +146,7 @@ class FrameWithBoth(mcrfpy.Frame):
|
||||||
self.method_called = False
|
self.method_called = False
|
||||||
self.property_called = False
|
self.property_called = False
|
||||||
|
|
||||||
def on_click(self, x, y, button, action):
|
def on_click(self, pos, button, action):
|
||||||
self.method_called = True
|
self.method_called = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -240,26 +243,32 @@ try:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
test_failed("Base types are NOT marked as subclasses", e)
|
test_failed("Base types are NOT marked as subclasses", e)
|
||||||
|
|
||||||
# Test 10: Verify subclass methods are callable
|
# Test 10: Verify subclass methods are callable with typed arguments
|
||||||
try:
|
try:
|
||||||
frame = ClickableFrame(pos=(100, 100), size=(100, 100))
|
frame = ClickableFrame(pos=(100, 100), size=(100, 100))
|
||||||
# Verify method exists and is callable
|
# Verify method exists and is callable
|
||||||
assert hasattr(frame, 'on_click'), "ClickableFrame should have on_click method"
|
assert hasattr(frame, 'on_click'), "ClickableFrame should have on_click method"
|
||||||
assert callable(frame.on_click), "on_click should be callable"
|
assert callable(frame.on_click), "on_click should be callable"
|
||||||
# Manually call to verify it works
|
# Manually call with proper typed objects to verify it works
|
||||||
frame.on_click(50.0, 50.0, "left", "start")
|
pos = mcrfpy.Vector(50.0, 50.0)
|
||||||
|
button = mcrfpy.MouseButton.LEFT
|
||||||
|
action = mcrfpy.InputState.PRESSED
|
||||||
|
frame.on_click(pos, button, action)
|
||||||
assert frame.click_count == 1, f"click_count should be 1, got {frame.click_count}"
|
assert frame.click_count == 1, f"click_count should be 1, got {frame.click_count}"
|
||||||
assert frame.last_click_args == (50.0, 50.0, "left", "start"), f"last_click_args mismatch: {frame.last_click_args}"
|
assert frame.last_click_args[0].x == 50.0, f"pos.x mismatch: {frame.last_click_args[0].x}"
|
||||||
|
assert frame.last_click_args[0].y == 50.0, f"pos.y mismatch: {frame.last_click_args[0].y}"
|
||||||
|
assert frame.last_click_args[1] == mcrfpy.MouseButton.LEFT, f"button mismatch: {frame.last_click_args[1]}"
|
||||||
|
assert frame.last_click_args[2] == mcrfpy.InputState.PRESSED, f"action mismatch: {frame.last_click_args[2]}"
|
||||||
test_passed("Subclass methods are callable and work")
|
test_passed("Subclass methods are callable and work")
|
||||||
except Exception as e:
|
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
|
# Test 11: Verify HoverFrame methods work with typed arguments
|
||||||
try:
|
try:
|
||||||
hover = HoverFrame(pos=(250, 100), size=(100, 100))
|
hover = HoverFrame(pos=(250, 100), size=(100, 100))
|
||||||
hover.on_enter(10.0, 20.0, "enter", "start")
|
hover.on_enter(mcrfpy.Vector(10.0, 20.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
hover.on_exit(30.0, 40.0, "exit", "start")
|
hover.on_exit(mcrfpy.Vector(30.0, 40.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
hover.on_move(50.0, 60.0, "move", "start")
|
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]}"
|
||||||
|
|
@ -272,7 +281,7 @@ except Exception as e:
|
||||||
try:
|
try:
|
||||||
both = FrameWithBoth(pos=(400, 250), size=(100, 100))
|
both = FrameWithBoth(pos=(400, 250), size=(100, 100))
|
||||||
property_was_called = [False]
|
property_was_called = [False]
|
||||||
def property_callback(x, y, btn, action):
|
def property_callback(pos, btn, action):
|
||||||
property_was_called[0] = True
|
property_was_called[0] = True
|
||||||
both.click = property_callback # Assign to property
|
both.click = property_callback # Assign to property
|
||||||
# Property callback should be set
|
# Property callback should be set
|
||||||
|
|
@ -288,7 +297,7 @@ try:
|
||||||
frame = ClickableFrame(pos=(100, 100), size=(100, 100))
|
frame = ClickableFrame(pos=(100, 100), size=(100, 100))
|
||||||
frame.custom_attr = "test_value"
|
frame.custom_attr = "test_value"
|
||||||
assert frame.custom_attr == "test_value", "Custom attribute should persist"
|
assert frame.custom_attr == "test_value", "Custom attribute should persist"
|
||||||
frame.on_click(0, 0, "left", "start")
|
frame.on_click(mcrfpy.Vector(0, 0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED)
|
||||||
assert frame.click_count == 1, "Click count should be 1"
|
assert frame.click_count == 1, "Click count should be 1"
|
||||||
# Verify frame is still usable after attribute access
|
# Verify frame is still usable after attribute access
|
||||||
assert frame.x == 100, f"Frame x should be 100, got {frame.x}"
|
assert frame.x == 100, f"Frame x should be 100, got {frame.x}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue