Parent-Child UI System (#122): - Add parent weak_ptr to UIDrawable for hierarchy tracking - Add setParent(), getParent(), removeFromParent() methods - UICollection now tracks owner and sets parent on append/insert - Auto-remove from old parent when adding to new collection Global Position Property (#102): - Add get_global_position() that walks up parent chain - Expose as read-only 'global_position' property on all UI types - Add UIDRAWABLE_PARENT_GETSETTERS macro for consistent bindings Dirty Flag System (#116): - Modify markDirty() to propagate up the parent chain - Add isDirty() and clearDirty() methods for render optimization Scene as Drawable (#118): - Add position, visible, opacity properties to Scene - Add setProperty()/getProperty() for animation support - Apply scene transformations in PyScene::render() - Fix lifecycle callbacks to clear errors when methods don't exist - Add GameEngine::getScene() public accessor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bfadab7486
commit
e3d8f54d46
|
|
@ -136,6 +136,10 @@ void GameEngine::cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
||||||
|
Scene* GameEngine::getScene(const std::string& name) {
|
||||||
|
auto it = scenes.find(name);
|
||||||
|
return (it != scenes.end()) ? it->second : nullptr;
|
||||||
|
}
|
||||||
void GameEngine::changeScene(std::string s)
|
void GameEngine::changeScene(std::string s)
|
||||||
{
|
{
|
||||||
changeScene(s, TransitionType::None, 0.0f);
|
changeScene(s, TransitionType::None, 0.0f);
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ public:
|
||||||
GameEngine(const McRogueFaceConfig& cfg);
|
GameEngine(const McRogueFaceConfig& cfg);
|
||||||
~GameEngine();
|
~GameEngine();
|
||||||
Scene* currentScene();
|
Scene* currentScene();
|
||||||
|
Scene* getScene(const std::string& name); // #118: Get scene by name
|
||||||
void changeScene(std::string);
|
void changeScene(std::string);
|
||||||
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
||||||
void createScene(std::string);
|
void createScene(std::string);
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,11 @@ void PyScene::doAction(std::string name, std::string type)
|
||||||
|
|
||||||
void PyScene::render()
|
void PyScene::render()
|
||||||
{
|
{
|
||||||
|
// #118: Skip rendering if scene is not visible
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
game->getRenderTarget().clear();
|
game->getRenderTarget().clear();
|
||||||
|
|
||||||
// Only sort if z_index values have changed
|
// Only sort if z_index values have changed
|
||||||
|
|
@ -75,7 +80,7 @@ void PyScene::render()
|
||||||
ui_elements_need_sort = false;
|
ui_elements_need_sort = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render in sorted order (no need to copy anymore)
|
// Render in sorted order with scene-level transformations
|
||||||
for (auto e: *ui_elements)
|
for (auto e: *ui_elements)
|
||||||
{
|
{
|
||||||
if (e) {
|
if (e) {
|
||||||
|
|
@ -86,7 +91,20 @@ void PyScene::render()
|
||||||
// Count this as a draw call (each visible element = 1+ draw calls)
|
// Count this as a draw call (each visible element = 1+ draw calls)
|
||||||
game->metrics.drawCalls++;
|
game->metrics.drawCalls++;
|
||||||
}
|
}
|
||||||
e->render();
|
|
||||||
|
// #118: Apply scene-level opacity to element
|
||||||
|
float original_opacity = e->opacity;
|
||||||
|
if (opacity < 1.0f) {
|
||||||
|
e->opacity = original_opacity * opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #118: Render with scene position offset
|
||||||
|
e->render(position, game->getRenderTarget());
|
||||||
|
|
||||||
|
// #118: Restore original opacity
|
||||||
|
if (opacity < 1.0f) {
|
||||||
|
e->opacity = original_opacity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,155 @@ PyObject* PySceneClass::get_active(PySceneObject* self, void* closure)
|
||||||
return PyBool_FromLong(game->scene == self->name);
|
return PyBool_FromLong(game->scene == self->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #118: Scene position getter
|
||||||
|
static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure)
|
||||||
|
{
|
||||||
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the scene by name using the public accessor
|
||||||
|
auto scene = game->getScene(self->name);
|
||||||
|
if (!scene) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Vector object
|
||||||
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||||
|
if (!type) return NULL;
|
||||||
|
PyObject* args = Py_BuildValue("(ff)", scene->position.x, scene->position.y);
|
||||||
|
PyObject* result = PyObject_CallObject((PyObject*)type, args);
|
||||||
|
Py_DECREF(type);
|
||||||
|
Py_DECREF(args);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #118: Scene position setter
|
||||||
|
static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto scene = game->getScene(self->name);
|
||||||
|
if (!scene) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept tuple or Vector
|
||||||
|
float x, y;
|
||||||
|
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
||||||
|
x = PyFloat_AsDouble(PyTuple_GetItem(value, 0));
|
||||||
|
y = PyFloat_AsDouble(PyTuple_GetItem(value, 1));
|
||||||
|
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
|
||||||
|
PyObject* xobj = PyObject_GetAttrString(value, "x");
|
||||||
|
PyObject* yobj = PyObject_GetAttrString(value, "y");
|
||||||
|
x = PyFloat_AsDouble(xobj);
|
||||||
|
y = PyFloat_AsDouble(yobj);
|
||||||
|
Py_DECREF(xobj);
|
||||||
|
Py_DECREF(yobj);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
scene->position = sf::Vector2f(x, y);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #118: Scene visible getter
|
||||||
|
static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure)
|
||||||
|
{
|
||||||
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
|
Py_RETURN_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto scene = game->getScene(self->name);
|
||||||
|
if (!scene) {
|
||||||
|
Py_RETURN_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyBool_FromLong(scene->visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #118: Scene visible setter
|
||||||
|
static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto scene = game->getScene(self->name);
|
||||||
|
if (!scene) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PyBool_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
scene->visible = PyObject_IsTrue(value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #118: Scene opacity getter
|
||||||
|
static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure)
|
||||||
|
{
|
||||||
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
|
return PyFloat_FromDouble(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto scene = game->getScene(self->name);
|
||||||
|
if (!scene) {
|
||||||
|
return PyFloat_FromDouble(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyFloat_FromDouble(scene->opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #118: Scene opacity setter
|
||||||
|
static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto scene = game->getScene(self->name);
|
||||||
|
if (!scene) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double opacity;
|
||||||
|
if (PyFloat_Check(value)) {
|
||||||
|
opacity = PyFloat_AsDouble(value);
|
||||||
|
} else if (PyLong_Check(value)) {
|
||||||
|
opacity = PyLong_AsDouble(value);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp to valid range
|
||||||
|
if (opacity < 0.0) opacity = 0.0;
|
||||||
|
if (opacity > 1.0) opacity = 1.0;
|
||||||
|
|
||||||
|
scene->opacity = opacity;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Lifecycle callbacks
|
// Lifecycle callbacks
|
||||||
void PySceneClass::call_on_enter(PySceneObject* self)
|
void PySceneClass::call_on_enter(PySceneObject* self)
|
||||||
{
|
{
|
||||||
|
|
@ -148,8 +297,12 @@ void PySceneClass::call_on_enter(PySceneObject* self)
|
||||||
} else {
|
} else {
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
}
|
}
|
||||||
}
|
Py_DECREF(method);
|
||||||
|
} else {
|
||||||
|
// Clear AttributeError if method doesn't exist
|
||||||
|
PyErr_Clear();
|
||||||
Py_XDECREF(method);
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PySceneClass::call_on_exit(PySceneObject* self)
|
void PySceneClass::call_on_exit(PySceneObject* self)
|
||||||
|
|
@ -162,8 +315,12 @@ void PySceneClass::call_on_exit(PySceneObject* self)
|
||||||
} else {
|
} else {
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
}
|
}
|
||||||
}
|
Py_DECREF(method);
|
||||||
|
} else {
|
||||||
|
// Clear AttributeError if method doesn't exist
|
||||||
|
PyErr_Clear();
|
||||||
Py_XDECREF(method);
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
|
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
|
||||||
|
|
@ -178,8 +335,12 @@ void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::s
|
||||||
} else {
|
} else {
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
}
|
}
|
||||||
}
|
Py_DECREF(method);
|
||||||
|
} else {
|
||||||
|
// Clear AttributeError if method doesn't exist
|
||||||
|
PyErr_Clear();
|
||||||
Py_XDECREF(method);
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
|
|
||||||
PyGILState_Release(gstate);
|
PyGILState_Release(gstate);
|
||||||
}
|
}
|
||||||
|
|
@ -194,8 +355,12 @@ void PySceneClass::call_update(PySceneObject* self, float dt)
|
||||||
} else {
|
} else {
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
}
|
}
|
||||||
}
|
Py_DECREF(method);
|
||||||
|
} else {
|
||||||
|
// Clear AttributeError if method doesn't exist
|
||||||
|
PyErr_Clear();
|
||||||
Py_XDECREF(method);
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
|
void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
|
||||||
|
|
@ -208,8 +373,12 @@ void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
|
||||||
} else {
|
} else {
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
}
|
}
|
||||||
}
|
Py_DECREF(method);
|
||||||
|
} else {
|
||||||
|
// Clear AttributeError if method doesn't exist
|
||||||
|
PyErr_Clear();
|
||||||
Py_XDECREF(method);
|
Py_XDECREF(method);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
|
|
@ -218,6 +387,13 @@ PyGetSetDef PySceneClass::getsetters[] = {
|
||||||
MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL},
|
MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL},
|
||||||
{"active", (getter)get_active, NULL,
|
{"active", (getter)get_active, NULL,
|
||||||
MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL},
|
MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL},
|
||||||
|
// #118: Scene-level UIDrawable-like properties
|
||||||
|
{"pos", (getter)PySceneClass_get_pos, (setter)PySceneClass_set_pos,
|
||||||
|
MCRF_PROPERTY(pos, "Scene position offset (Vector). Applied to all UI elements during rendering."), NULL},
|
||||||
|
{"visible", (getter)PySceneClass_get_visible, (setter)PySceneClass_set_visible,
|
||||||
|
MCRF_PROPERTY(visible, "Scene visibility (bool). If False, scene is not rendered."), NULL},
|
||||||
|
{"opacity", (getter)PySceneClass_get_opacity, (setter)PySceneClass_set_opacity,
|
||||||
|
MCRF_PROPERTY(opacity, "Scene opacity (0.0-1.0). Applied to all UI elements during rendering."), NULL},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,43 @@ void Scene::key_unregister()
|
||||||
*/
|
*/
|
||||||
key_callable.reset();
|
key_callable.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #118: Scene animation property support
|
||||||
|
bool Scene::setProperty(const std::string& name, float value)
|
||||||
|
{
|
||||||
|
if (name == "x") {
|
||||||
|
position.x = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "y") {
|
||||||
|
position.y = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "opacity") {
|
||||||
|
opacity = std::max(0.0f, std::min(1.0f, value));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "visible") {
|
||||||
|
visible = (value != 0.0f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Scene::setProperty(const std::string& name, const sf::Vector2f& value)
|
||||||
|
{
|
||||||
|
if (name == "pos" || name == "position") {
|
||||||
|
position = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Scene::getProperty(const std::string& name) const
|
||||||
|
{
|
||||||
|
if (name == "x") return position.x;
|
||||||
|
if (name == "y") return position.y;
|
||||||
|
if (name == "opacity") return opacity;
|
||||||
|
if (name == "visible") return visible ? 1.0f : 0.0f;
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
|
||||||
10
src/Scene.h
10
src/Scene.h
|
|
@ -43,4 +43,14 @@ public:
|
||||||
std::unique_ptr<PyKeyCallable> key_callable;
|
std::unique_ptr<PyKeyCallable> key_callable;
|
||||||
void key_register(PyObject*);
|
void key_register(PyObject*);
|
||||||
void key_unregister();
|
void key_unregister();
|
||||||
|
|
||||||
|
// #118: Scene-level UIDrawable-like properties for animations/transitions
|
||||||
|
sf::Vector2f position{0.0f, 0.0f}; // Offset applied to all ui_elements
|
||||||
|
bool visible = true; // Controls rendering of scene
|
||||||
|
float opacity = 1.0f; // Applied to all ui_elements (0.0-1.0)
|
||||||
|
|
||||||
|
// Animation support for scene properties
|
||||||
|
bool setProperty(const std::string& name, float value);
|
||||||
|
bool setProperty(const std::string& name, const sf::Vector2f& value);
|
||||||
|
float getProperty(const std::string& name) const;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,7 @@ PyGetSetDef UIArc::getsetters[] = {
|
||||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
|
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
|
||||||
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UIARC},
|
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UIARC},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
14
src/UIBase.h
14
src/UIBase.h
|
|
@ -162,4 +162,18 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
|
||||||
"Automatically clamped to valid range [0.0, 1.0]." \
|
"Automatically clamped to valid range [0.0, 1.0]." \
|
||||||
), NULL}
|
), NULL}
|
||||||
|
|
||||||
|
// #122 & #102: Macro for parent/global_position properties (requires closure with type enum)
|
||||||
|
// These need the PyObjectsEnum value in closure, so they're added separately in each class
|
||||||
|
#define UIDRAWABLE_PARENT_GETSETTERS(type_enum) \
|
||||||
|
{"parent", (getter)UIDrawable::get_parent, NULL, \
|
||||||
|
MCRF_PROPERTY(parent, \
|
||||||
|
"Parent drawable (read-only). " \
|
||||||
|
"Returns the parent Frame/Grid if nested, or None if at scene level." \
|
||||||
|
), (void*)type_enum}, \
|
||||||
|
{"global_position", (getter)UIDrawable::get_global_pos, NULL, \
|
||||||
|
MCRF_PROPERTY(global_position, \
|
||||||
|
"Global screen position (read-only). " \
|
||||||
|
"Calculates absolute position by walking up the parent chain." \
|
||||||
|
), (void*)type_enum}
|
||||||
|
|
||||||
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete
|
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,7 @@ PyGetSetDef UICaption::getsetters[] = {
|
||||||
), (void*)PyObjectsEnum::UICAPTION},
|
), (void*)PyObjectsEnum::UICAPTION},
|
||||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION},
|
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,7 @@ PyGetSetDef UICircle::getsetters[] = {
|
||||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
|
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
|
||||||
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UICIRCLE},
|
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UICIRCLE},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,8 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
||||||
|
|
||||||
// Handle deletion
|
// Handle deletion
|
||||||
if (value == NULL) {
|
if (value == NULL) {
|
||||||
|
// #122: Clear the parent before removing
|
||||||
|
(*self->data)[index]->setParent(nullptr);
|
||||||
self->data->erase(self->data->begin() + index);
|
self->data->erase(self->data->begin() + index);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -256,9 +258,20 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #122: Clear parent of old element
|
||||||
|
(*vec)[index]->setParent(nullptr);
|
||||||
|
|
||||||
|
// #122: Remove new drawable from its old parent if it has one
|
||||||
|
if (auto old_parent = new_drawable->getParent()) {
|
||||||
|
new_drawable->removeFromParent();
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve the z_index of the replaced element
|
// Preserve the z_index of the replaced element
|
||||||
new_drawable->z_index = old_z_index;
|
new_drawable->z_index = old_z_index;
|
||||||
|
|
||||||
|
// #122: Set new parent
|
||||||
|
new_drawable->setParent(self->owner.lock());
|
||||||
|
|
||||||
// Replace the element
|
// Replace the element
|
||||||
(*vec)[index] = new_drawable;
|
(*vec)[index] = new_drawable;
|
||||||
|
|
||||||
|
|
@ -638,47 +651,51 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #122: Get the owner as parent for this drawable
|
||||||
|
std::shared_ptr<UIDrawable> owner_ptr = self->owner.lock();
|
||||||
|
|
||||||
|
// Helper lambda to add drawable with parent tracking
|
||||||
|
auto addDrawable = [&](std::shared_ptr<UIDrawable> drawable) {
|
||||||
|
// #122: Remove from old parent if it has one
|
||||||
|
if (auto old_parent = drawable->getParent()) {
|
||||||
|
drawable->removeFromParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable->z_index = new_z_index;
|
||||||
|
|
||||||
|
// #122: Set new parent (owner of this collection)
|
||||||
|
drawable->setParent(owner_ptr);
|
||||||
|
|
||||||
|
self->data->push_back(drawable);
|
||||||
|
};
|
||||||
|
|
||||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")))
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")))
|
||||||
{
|
{
|
||||||
PyUIFrameObject* frame = (PyUIFrameObject*)o;
|
addDrawable(((PyUIFrameObject*)o)->data);
|
||||||
frame->data->z_index = new_z_index;
|
|
||||||
self->data->push_back(frame->data);
|
|
||||||
}
|
}
|
||||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")))
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")))
|
||||||
{
|
{
|
||||||
PyUICaptionObject* caption = (PyUICaptionObject*)o;
|
addDrawable(((PyUICaptionObject*)o)->data);
|
||||||
caption->data->z_index = new_z_index;
|
|
||||||
self->data->push_back(caption->data);
|
|
||||||
}
|
}
|
||||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")))
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")))
|
||||||
{
|
{
|
||||||
PyUISpriteObject* sprite = (PyUISpriteObject*)o;
|
addDrawable(((PyUISpriteObject*)o)->data);
|
||||||
sprite->data->z_index = new_z_index;
|
|
||||||
self->data->push_back(sprite->data);
|
|
||||||
}
|
}
|
||||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
|
||||||
{
|
{
|
||||||
PyUIGridObject* grid = (PyUIGridObject*)o;
|
addDrawable(((PyUIGridObject*)o)->data);
|
||||||
grid->data->z_index = new_z_index;
|
|
||||||
self->data->push_back(grid->data);
|
|
||||||
}
|
}
|
||||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")))
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")))
|
||||||
{
|
{
|
||||||
PyUILineObject* line = (PyUILineObject*)o;
|
addDrawable(((PyUILineObject*)o)->data);
|
||||||
line->data->z_index = new_z_index;
|
|
||||||
self->data->push_back(line->data);
|
|
||||||
}
|
}
|
||||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")))
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")))
|
||||||
{
|
{
|
||||||
PyUICircleObject* circle = (PyUICircleObject*)o;
|
addDrawable(((PyUICircleObject*)o)->data);
|
||||||
circle->data->z_index = new_z_index;
|
|
||||||
self->data->push_back(circle->data);
|
|
||||||
}
|
}
|
||||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")))
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")))
|
||||||
{
|
{
|
||||||
PyUIArcObject* arc = (PyUIArcObject*)o;
|
addDrawable(((PyUIArcObject*)o)->data);
|
||||||
arc->data->z_index = new_z_index;
|
|
||||||
self->data->push_back(arc->data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark scene as needing resort after adding element
|
// Mark scene as needing resort after adding element
|
||||||
|
|
@ -734,41 +751,41 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
||||||
current_z_index = INT_MAX;
|
current_z_index = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #122: Get the owner as parent for this drawable
|
||||||
|
std::shared_ptr<UIDrawable> owner_ptr = self->owner.lock();
|
||||||
|
|
||||||
|
// Helper lambda to add drawable with parent tracking
|
||||||
|
auto addDrawable = [&](std::shared_ptr<UIDrawable> drawable) {
|
||||||
|
// #122: Remove from old parent if it has one
|
||||||
|
if (auto old_parent = drawable->getParent()) {
|
||||||
|
drawable->removeFromParent();
|
||||||
|
}
|
||||||
|
drawable->z_index = current_z_index;
|
||||||
|
drawable->setParent(owner_ptr);
|
||||||
|
self->data->push_back(drawable);
|
||||||
|
};
|
||||||
|
|
||||||
// Add the item based on its type
|
// Add the item based on its type
|
||||||
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||||
PyUIFrameObject* frame = (PyUIFrameObject*)item;
|
addDrawable(((PyUIFrameObject*)item)->data);
|
||||||
frame->data->z_index = current_z_index;
|
|
||||||
self->data->push_back(frame->data);
|
|
||||||
}
|
}
|
||||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||||
PyUICaptionObject* caption = (PyUICaptionObject*)item;
|
addDrawable(((PyUICaptionObject*)item)->data);
|
||||||
caption->data->z_index = current_z_index;
|
|
||||||
self->data->push_back(caption->data);
|
|
||||||
}
|
}
|
||||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||||
PyUISpriteObject* sprite = (PyUISpriteObject*)item;
|
addDrawable(((PyUISpriteObject*)item)->data);
|
||||||
sprite->data->z_index = current_z_index;
|
|
||||||
self->data->push_back(sprite->data);
|
|
||||||
}
|
}
|
||||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||||
PyUIGridObject* grid = (PyUIGridObject*)item;
|
addDrawable(((PyUIGridObject*)item)->data);
|
||||||
grid->data->z_index = current_z_index;
|
|
||||||
self->data->push_back(grid->data);
|
|
||||||
}
|
}
|
||||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"))) {
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"))) {
|
||||||
PyUILineObject* line = (PyUILineObject*)item;
|
addDrawable(((PyUILineObject*)item)->data);
|
||||||
line->data->z_index = current_z_index;
|
|
||||||
self->data->push_back(line->data);
|
|
||||||
}
|
}
|
||||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"))) {
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"))) {
|
||||||
PyUICircleObject* circle = (PyUICircleObject*)item;
|
addDrawable(((PyUICircleObject*)item)->data);
|
||||||
circle->data->z_index = current_z_index;
|
|
||||||
self->data->push_back(circle->data);
|
|
||||||
}
|
}
|
||||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) {
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) {
|
||||||
PyUIArcObject* arc = (PyUIArcObject*)item;
|
addDrawable(((PyUIArcObject*)item)->data);
|
||||||
arc->data->z_index = current_z_index;
|
|
||||||
self->data->push_back(arc->data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_DECREF(item);
|
Py_DECREF(item);
|
||||||
|
|
@ -825,6 +842,8 @@ PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
|
||||||
// Search for the object and remove first occurrence
|
// Search for the object and remove first occurrence
|
||||||
for (auto it = vec->begin(); it != vec->end(); ++it) {
|
for (auto it = vec->begin(); it != vec->end(); ++it) {
|
||||||
if (it->get() == search_drawable.get()) {
|
if (it->get() == search_drawable.get()) {
|
||||||
|
// #122: Clear the parent before removing
|
||||||
|
(*it)->setParent(nullptr);
|
||||||
vec->erase(it);
|
vec->erase(it);
|
||||||
McRFPy_API::markSceneNeedsSort();
|
McRFPy_API::markSceneNeedsSort();
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
|
@ -868,6 +887,9 @@ PyObject* UICollection::pop(PyUICollectionObject* self, PyObject* args)
|
||||||
// Get the element before removing
|
// Get the element before removing
|
||||||
std::shared_ptr<UIDrawable> drawable = (*vec)[index];
|
std::shared_ptr<UIDrawable> drawable = (*vec)[index];
|
||||||
|
|
||||||
|
// #122: Clear the parent before removing
|
||||||
|
drawable->setParent(nullptr);
|
||||||
|
|
||||||
// Remove from vector
|
// Remove from vector
|
||||||
vec->erase(vec->begin() + index);
|
vec->erase(vec->begin() + index);
|
||||||
|
|
||||||
|
|
@ -929,6 +951,14 @@ PyObject* UICollection::insert(PyUICollectionObject* self, PyObject* args)
|
||||||
index = size;
|
index = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #122: Remove from old parent if it has one
|
||||||
|
if (auto old_parent = drawable->getParent()) {
|
||||||
|
drawable->removeFromParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// #122: Set new parent
|
||||||
|
drawable->setParent(self->owner.lock());
|
||||||
|
|
||||||
// Insert at position
|
// Insert at position
|
||||||
vec->insert(vec->begin() + index, drawable);
|
vec->insert(vec->begin() + index, drawable);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -685,3 +685,215 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
||||||
drawable->onPositionChanged();
|
drawable->onPositionChanged();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #122 - Parent-child hierarchy implementation
|
||||||
|
void UIDrawable::setParent(std::shared_ptr<UIDrawable> new_parent) {
|
||||||
|
parent = new_parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<UIDrawable> UIDrawable::getParent() const {
|
||||||
|
return parent.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIDrawable::removeFromParent() {
|
||||||
|
auto p = parent.lock();
|
||||||
|
if (!p) return;
|
||||||
|
|
||||||
|
// Check if parent is a UIFrame (has children vector)
|
||||||
|
if (p->derived_type() == PyObjectsEnum::UIFRAME) {
|
||||||
|
auto frame = std::static_pointer_cast<UIFrame>(p);
|
||||||
|
auto& children = *frame->children;
|
||||||
|
|
||||||
|
// Find and remove this drawable from parent's children
|
||||||
|
// We need to find ourselves - but we don't have shared_from_this
|
||||||
|
// Instead, compare raw pointers
|
||||||
|
for (auto it = children.begin(); it != children.end(); ++it) {
|
||||||
|
if (it->get() == this) {
|
||||||
|
children.erase(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame->children_need_sort = true;
|
||||||
|
}
|
||||||
|
// TODO: Handle UIGrid children when needed
|
||||||
|
|
||||||
|
parent.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// #102 - Global position calculation
|
||||||
|
sf::Vector2f UIDrawable::get_global_position() const {
|
||||||
|
sf::Vector2f global_pos = position;
|
||||||
|
|
||||||
|
auto p = parent.lock();
|
||||||
|
while (p) {
|
||||||
|
global_pos += p->position;
|
||||||
|
p = p->parent.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return global_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #116 - Dirty flag propagation up parent chain
|
||||||
|
void UIDrawable::markDirty() {
|
||||||
|
if (render_dirty) return; // Already dirty, no need to propagate
|
||||||
|
|
||||||
|
render_dirty = true;
|
||||||
|
|
||||||
|
// Propagate to parent
|
||||||
|
auto p = parent.lock();
|
||||||
|
if (p) {
|
||||||
|
p->markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python API - get parent drawable
|
||||||
|
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
|
||||||
|
UIDrawable* drawable = nullptr;
|
||||||
|
|
||||||
|
switch (objtype) {
|
||||||
|
case PyObjectsEnum::UIFRAME:
|
||||||
|
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UICAPTION:
|
||||||
|
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UISPRITE:
|
||||||
|
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UIGRID:
|
||||||
|
drawable = ((PyUIGridObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UILINE:
|
||||||
|
drawable = ((PyUILineObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UICIRCLE:
|
||||||
|
drawable = ((PyUICircleObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UIARC:
|
||||||
|
drawable = ((PyUIArcObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parent_ptr = drawable->getParent();
|
||||||
|
if (!parent_ptr) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert parent to Python object using the cache/conversion system
|
||||||
|
// Re-use the pattern from UICollection
|
||||||
|
PyTypeObject* type = nullptr;
|
||||||
|
PyObject* obj = nullptr;
|
||||||
|
|
||||||
|
switch (parent_ptr->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>(parent_ptr);
|
||||||
|
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>(parent_ptr);
|
||||||
|
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>(parent_ptr);
|
||||||
|
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>(parent_ptr);
|
||||||
|
pyObj->weakreflist = NULL;
|
||||||
|
}
|
||||||
|
obj = (PyObject*)pyObj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
Py_DECREF(type);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python API - get global position (read-only)
|
||||||
|
PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
|
||||||
|
UIDrawable* drawable = nullptr;
|
||||||
|
|
||||||
|
switch (objtype) {
|
||||||
|
case PyObjectsEnum::UIFRAME:
|
||||||
|
drawable = ((PyUIFrameObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UICAPTION:
|
||||||
|
drawable = ((PyUICaptionObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UISPRITE:
|
||||||
|
drawable = ((PyUISpriteObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UIGRID:
|
||||||
|
drawable = ((PyUIGridObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UILINE:
|
||||||
|
drawable = ((PyUILineObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UICIRCLE:
|
||||||
|
drawable = ((PyUICircleObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UIARC:
|
||||||
|
drawable = ((PyUIArcObject*)self)->data.get();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::Vector2f global_pos = drawable->get_global_position();
|
||||||
|
|
||||||
|
// Create a Python Vector object
|
||||||
|
PyObject* module = PyImport_ImportModule("mcrfpy");
|
||||||
|
if (!module) return NULL;
|
||||||
|
|
||||||
|
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
|
||||||
|
Py_DECREF(module);
|
||||||
|
if (!vector_type) return NULL;
|
||||||
|
|
||||||
|
PyObject* args = Py_BuildValue("(ff)", global_pos.x, global_pos.y);
|
||||||
|
PyObject* result = PyObject_CallObject(vector_type, args);
|
||||||
|
Py_DECREF(vector_type);
|
||||||
|
Py_DECREF(args);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "IndexTexture.h"
|
#include "IndexTexture.h"
|
||||||
#include "Resources.h"
|
#include "Resources.h"
|
||||||
#include <list>
|
#include <list>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "PyCallable.h"
|
#include "PyCallable.h"
|
||||||
#include "PyTexture.h"
|
#include "PyTexture.h"
|
||||||
|
|
@ -77,6 +78,25 @@ public:
|
||||||
// Position in pixel coordinates (moved from derived classes)
|
// Position in pixel coordinates (moved from derived classes)
|
||||||
sf::Vector2f position;
|
sf::Vector2f position;
|
||||||
|
|
||||||
|
// Parent-child hierarchy (#122)
|
||||||
|
std::weak_ptr<UIDrawable> parent;
|
||||||
|
|
||||||
|
// Set the parent of this drawable (called by collections when adding)
|
||||||
|
void setParent(std::shared_ptr<UIDrawable> new_parent);
|
||||||
|
|
||||||
|
// Get the parent drawable (returns nullptr if no parent or expired)
|
||||||
|
std::shared_ptr<UIDrawable> getParent() const;
|
||||||
|
|
||||||
|
// Remove this drawable from its current parent's children
|
||||||
|
void removeFromParent();
|
||||||
|
|
||||||
|
// Get the global (screen) position by walking up the parent chain (#102)
|
||||||
|
sf::Vector2f get_global_position() const;
|
||||||
|
|
||||||
|
// Python API for parent/global_position
|
||||||
|
static PyObject* get_parent(PyObject* self, void* closure);
|
||||||
|
static PyObject* get_global_pos(PyObject* self, void* closure);
|
||||||
|
|
||||||
// New properties for Phase 1
|
// New properties for Phase 1
|
||||||
bool visible = true; // #87 - visibility flag
|
bool visible = true; // #87 - visibility flag
|
||||||
float opacity = 1.0f; // #88 - opacity (0.0 = transparent, 1.0 = opaque)
|
float opacity = 1.0f; // #88 - opacity (0.0 = transparent, 1.0 = opaque)
|
||||||
|
|
@ -117,13 +137,20 @@ protected:
|
||||||
void updateRenderTexture();
|
void updateRenderTexture();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Mark this drawable as needing redraw
|
// Mark this drawable as needing redraw (#116 - propagates up parent chain)
|
||||||
void markDirty() { render_dirty = true; }
|
void markDirty();
|
||||||
|
|
||||||
|
// Check if this drawable needs redraw
|
||||||
|
bool isDirty() const { return render_dirty; }
|
||||||
|
|
||||||
|
// Clear dirty flag (called after rendering)
|
||||||
|
void clearDirty() { render_dirty = false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> data;
|
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> data;
|
||||||
|
std::weak_ptr<UIDrawable> owner; // #122: Parent drawable (for Frame.children, Grid.children)
|
||||||
} PyUICollectionObject;
|
} PyUICollectionObject;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
||||||
|
|
@ -168,11 +168,13 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
PyObject* UIFrame::get_children(PyUIFrameObject* self, void* closure)
|
PyObject* UIFrame::get_children(PyUIFrameObject* self, void* closure)
|
||||||
{
|
{
|
||||||
// create PyUICollection instance pointing to self->data->children
|
// create PyUICollection instance pointing to self->data->children
|
||||||
//PyUICollectionObject* o = (PyUICollectionObject*)mcrfpydef::PyUICollectionType.tp_alloc(&mcrfpydef::PyUICollectionType, 0);
|
|
||||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
|
||||||
auto o = (PyUICollectionObject*)type->tp_alloc(type, 0);
|
auto o = (PyUICollectionObject*)type->tp_alloc(type, 0);
|
||||||
if (o)
|
Py_DECREF(type);
|
||||||
|
if (o) {
|
||||||
o->data = self->data->children;
|
o->data = self->data->children;
|
||||||
|
o->owner = self->data; // #122: Set owner for parent tracking
|
||||||
|
}
|
||||||
return (PyObject*)o;
|
return (PyObject*)o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,6 +414,7 @@ PyGetSetDef UIFrame::getsetters[] = {
|
||||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UIFRAME},
|
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UIFRAME},
|
||||||
{"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL},
|
{"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1499,6 +1499,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
||||||
), (void*)PyObjectsEnum::UIGRID},
|
), (void*)PyObjectsEnum::UIGRID},
|
||||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
{NULL} /* Sentinel */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1519,8 +1520,10 @@ PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure)
|
||||||
// Returns UICollection for UIDrawable children (speech bubbles, effects, overlays)
|
// Returns UICollection for UIDrawable children (speech bubbles, effects, overlays)
|
||||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
|
||||||
auto o = (PyUICollectionObject*)type->tp_alloc(type, 0);
|
auto o = (PyUICollectionObject*)type->tp_alloc(type, 0);
|
||||||
|
Py_DECREF(type);
|
||||||
if (o) {
|
if (o) {
|
||||||
o->data = self->data->children;
|
o->data = self->data->children;
|
||||||
|
o->owner = self->data; // #122: Set owner for parent tracking
|
||||||
}
|
}
|
||||||
return (PyObject*)o;
|
return (PyObject*)o;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -435,6 +435,7 @@ PyGetSetDef UILine::getsetters[] = {
|
||||||
MCRF_PROPERTY(pos, "Position as a Vector (midpoint of line)."),
|
MCRF_PROPERTY(pos, "Position as a Vector (midpoint of line)."),
|
||||||
(void*)PyObjectsEnum::UILINE},
|
(void*)PyObjectsEnum::UILINE},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UILINE),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,7 @@ PyGetSetDef UISprite::getsetters[] = {
|
||||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE},
|
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE},
|
||||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE},
|
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE},
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test #122: Parent-Child UI System
|
||||||
|
Test #102: Global Position Property
|
||||||
|
Test #116: Dirty Flag System (partial - propagation)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_parent_property():
|
||||||
|
"""Test that children get parent reference when added to Frame"""
|
||||||
|
print("Testing parent property...")
|
||||||
|
|
||||||
|
# Create scene and get UI
|
||||||
|
mcrfpy.createScene("test")
|
||||||
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
|
# Create a parent frame
|
||||||
|
parent = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||||
|
ui.append(parent)
|
||||||
|
|
||||||
|
# Create a child caption
|
||||||
|
child = mcrfpy.Caption(text="Child", pos=(10, 10))
|
||||||
|
|
||||||
|
# Before adding, parent should be None
|
||||||
|
assert child.parent is None, f"Child should have no parent before adding, got: {child.parent}"
|
||||||
|
|
||||||
|
# Add child to parent
|
||||||
|
parent.children.append(child)
|
||||||
|
|
||||||
|
# After adding, parent should be set
|
||||||
|
assert child.parent is not None, "Child should have parent after adding"
|
||||||
|
# The parent should be the same Frame we added to
|
||||||
|
# (checking by position since identity comparison is tricky)
|
||||||
|
assert child.parent.x == parent.x, f"Parent x mismatch: {child.parent.x} vs {parent.x}"
|
||||||
|
assert child.parent.y == parent.y, f"Parent y mismatch: {child.parent.y} vs {parent.y}"
|
||||||
|
|
||||||
|
print(" - Parent property: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
def test_global_position():
|
||||||
|
"""Test global position calculation through parent chain"""
|
||||||
|
print("Testing global_position property...")
|
||||||
|
|
||||||
|
# Create scene and get UI
|
||||||
|
mcrfpy.createScene("test2")
|
||||||
|
ui = mcrfpy.sceneUI("test2")
|
||||||
|
|
||||||
|
# Create nested hierarchy:
|
||||||
|
# root (50, 50)
|
||||||
|
# -> child1 (20, 20) -> global (70, 70)
|
||||||
|
# -> child2 (10, 10) -> global (80, 80)
|
||||||
|
|
||||||
|
root = mcrfpy.Frame(pos=(50, 50), size=(200, 200))
|
||||||
|
ui.append(root)
|
||||||
|
|
||||||
|
child1 = mcrfpy.Frame(pos=(20, 20), size=(100, 100))
|
||||||
|
root.children.append(child1)
|
||||||
|
|
||||||
|
child2 = mcrfpy.Caption(text="Deep", pos=(10, 10))
|
||||||
|
child1.children.append(child2)
|
||||||
|
|
||||||
|
# Check global positions
|
||||||
|
# root has no parent, global should equal local
|
||||||
|
assert root.global_position.x == 50, f"Root global x: expected 50, got {root.global_position.x}"
|
||||||
|
assert root.global_position.y == 50, f"Root global y: expected 50, got {root.global_position.y}"
|
||||||
|
|
||||||
|
# child1 is at (20, 20) inside root at (50, 50) -> global (70, 70)
|
||||||
|
assert child1.global_position.x == 70, f"Child1 global x: expected 70, got {child1.global_position.x}"
|
||||||
|
assert child1.global_position.y == 70, f"Child1 global y: expected 70, got {child1.global_position.y}"
|
||||||
|
|
||||||
|
# child2 is at (10, 10) inside child1 at global (70, 70) -> global (80, 80)
|
||||||
|
assert child2.global_position.x == 80, f"Child2 global x: expected 80, got {child2.global_position.x}"
|
||||||
|
assert child2.global_position.y == 80, f"Child2 global y: expected 80, got {child2.global_position.y}"
|
||||||
|
|
||||||
|
print(" - Global position: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parent_changes_on_move():
|
||||||
|
"""Test that moving child to different parent updates parent reference"""
|
||||||
|
print("Testing parent changes on move...")
|
||||||
|
|
||||||
|
mcrfpy.createScene("test3")
|
||||||
|
ui = mcrfpy.sceneUI("test3")
|
||||||
|
|
||||||
|
parent1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=(255, 0, 0, 255))
|
||||||
|
parent2 = mcrfpy.Frame(pos=(200, 0), size=(100, 100), fill_color=(0, 255, 0, 255))
|
||||||
|
ui.append(parent1)
|
||||||
|
ui.append(parent2)
|
||||||
|
|
||||||
|
child = mcrfpy.Caption(text="Movable", pos=(5, 5))
|
||||||
|
parent1.children.append(child)
|
||||||
|
|
||||||
|
# Child should be in parent1
|
||||||
|
assert child.parent is not None, "Child should have parent"
|
||||||
|
assert child.parent.x == 0, f"Child parent should be parent1, x={child.parent.x}"
|
||||||
|
|
||||||
|
# Move child to parent2 (should auto-remove from parent1)
|
||||||
|
parent2.children.append(child)
|
||||||
|
|
||||||
|
# Child should now be in parent2
|
||||||
|
assert child.parent is not None, "Child should still have parent"
|
||||||
|
assert child.parent.x == 200, f"Child parent should be parent2, x={child.parent.x}"
|
||||||
|
|
||||||
|
# parent1 should have no children
|
||||||
|
assert len(parent1.children) == 0, f"parent1 should have 0 children, has {len(parent1.children)}"
|
||||||
|
|
||||||
|
# parent2 should have one child
|
||||||
|
assert len(parent2.children) == 1, f"parent2 should have 1 child, has {len(parent2.children)}"
|
||||||
|
|
||||||
|
print(" - Parent changes on move: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_clears_parent():
|
||||||
|
"""Test that removing child clears parent reference"""
|
||||||
|
print("Testing remove clears parent...")
|
||||||
|
|
||||||
|
mcrfpy.createScene("test4")
|
||||||
|
ui = mcrfpy.sceneUI("test4")
|
||||||
|
|
||||||
|
parent = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||||
|
ui.append(parent)
|
||||||
|
|
||||||
|
child = mcrfpy.Caption(text="Removable", pos=(5, 5))
|
||||||
|
parent.children.append(child)
|
||||||
|
|
||||||
|
assert child.parent is not None, "Child should have parent"
|
||||||
|
|
||||||
|
# Remove child
|
||||||
|
parent.children.remove(child)
|
||||||
|
|
||||||
|
assert child.parent is None, f"Child should have no parent after remove, got: {child.parent}"
|
||||||
|
assert len(parent.children) == 0, f"Parent should have no children after remove"
|
||||||
|
|
||||||
|
print(" - Remove clears parent: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_level_elements():
|
||||||
|
"""Test that scene-level elements have no parent"""
|
||||||
|
print("Testing scene-level elements...")
|
||||||
|
|
||||||
|
mcrfpy.createScene("test5")
|
||||||
|
ui = mcrfpy.sceneUI("test5")
|
||||||
|
|
||||||
|
frame = mcrfpy.Frame(pos=(10, 10), size=(50, 50))
|
||||||
|
ui.append(frame)
|
||||||
|
|
||||||
|
# Scene-level elements should have no parent
|
||||||
|
assert frame.parent is None, f"Scene-level element should have no parent, got: {frame.parent}"
|
||||||
|
|
||||||
|
# Global position should equal local position
|
||||||
|
assert frame.global_position.x == 10, f"Global x should equal local x"
|
||||||
|
assert frame.global_position.y == 10, f"Global y should equal local y"
|
||||||
|
|
||||||
|
print(" - Scene-level elements: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_drawable_types():
|
||||||
|
"""Test parent/global_position on all drawable types"""
|
||||||
|
print("Testing all drawable types...")
|
||||||
|
|
||||||
|
mcrfpy.createScene("test6")
|
||||||
|
ui = mcrfpy.sceneUI("test6")
|
||||||
|
|
||||||
|
parent = mcrfpy.Frame(pos=(100, 100), size=(300, 300))
|
||||||
|
ui.append(parent)
|
||||||
|
|
||||||
|
# Test all types
|
||||||
|
types_to_test = [
|
||||||
|
("Frame", mcrfpy.Frame(pos=(10, 10), size=(50, 50))),
|
||||||
|
("Caption", mcrfpy.Caption(text="Test", pos=(10, 70))),
|
||||||
|
("Sprite", mcrfpy.Sprite(pos=(10, 130))), # May need texture
|
||||||
|
("Grid", mcrfpy.Grid(grid_size=(5, 5), pos=(10, 190), size=(80, 80))),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, child in types_to_test:
|
||||||
|
parent.children.append(child)
|
||||||
|
assert child.parent is not None, f"{name} should have parent"
|
||||||
|
# Global position should be local + parent's position
|
||||||
|
expected_x = child.x + 100
|
||||||
|
expected_y = child.y + 100
|
||||||
|
assert child.global_position.x == expected_x, f"{name} global_x: expected {expected_x}, got {child.global_position.x}"
|
||||||
|
assert child.global_position.y == expected_y, f"{name} global_y: expected {expected_y}, got {child.global_position.y}"
|
||||||
|
|
||||||
|
print(" - All drawable types: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
test_parent_property()
|
||||||
|
test_global_position()
|
||||||
|
test_parent_changes_on_move()
|
||||||
|
test_remove_clears_parent()
|
||||||
|
test_scene_level_elements()
|
||||||
|
test_all_drawable_types()
|
||||||
|
|
||||||
|
print("\n=== All tests passed! ===")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nTEST FAILED: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test Scene properties (#118: Scene as Drawable)"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Create test scenes
|
||||||
|
mcrfpy.createScene("test_scene")
|
||||||
|
|
||||||
|
def test_scene_pos():
|
||||||
|
"""Test Scene pos property"""
|
||||||
|
print("Testing scene pos property...")
|
||||||
|
|
||||||
|
# Create a Scene subclass to test
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
scene = TestScene("scene_pos_test")
|
||||||
|
|
||||||
|
# Test initial position
|
||||||
|
pos = scene.pos
|
||||||
|
assert pos.x == 0.0, f"Initial pos.x should be 0.0, got {pos.x}"
|
||||||
|
assert pos.y == 0.0, f"Initial pos.y should be 0.0, got {pos.y}"
|
||||||
|
|
||||||
|
# Test setting position with tuple
|
||||||
|
scene.pos = (100.0, 200.0)
|
||||||
|
pos = scene.pos
|
||||||
|
assert pos.x == 100.0, f"pos.x should be 100.0, got {pos.x}"
|
||||||
|
assert pos.y == 200.0, f"pos.y should be 200.0, got {pos.y}"
|
||||||
|
|
||||||
|
# Test setting position with Vector
|
||||||
|
scene.pos = mcrfpy.Vector(50.0, 75.0)
|
||||||
|
pos = scene.pos
|
||||||
|
assert pos.x == 50.0, f"pos.x should be 50.0, got {pos.x}"
|
||||||
|
assert pos.y == 75.0, f"pos.y should be 75.0, got {pos.y}"
|
||||||
|
|
||||||
|
print(" - Scene pos property: PASS")
|
||||||
|
|
||||||
|
def test_scene_visible():
|
||||||
|
"""Test Scene visible property"""
|
||||||
|
print("Testing scene visible property...")
|
||||||
|
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
scene = TestScene("scene_vis_test")
|
||||||
|
|
||||||
|
# Test initial visibility (should be True)
|
||||||
|
assert scene.visible == True, f"Initial visible should be True, got {scene.visible}"
|
||||||
|
|
||||||
|
# Test setting to False
|
||||||
|
scene.visible = False
|
||||||
|
assert scene.visible == False, f"visible should be False, got {scene.visible}"
|
||||||
|
|
||||||
|
# Test setting back to True
|
||||||
|
scene.visible = True
|
||||||
|
assert scene.visible == True, f"visible should be True, got {scene.visible}"
|
||||||
|
|
||||||
|
print(" - Scene visible property: PASS")
|
||||||
|
|
||||||
|
def test_scene_opacity():
|
||||||
|
"""Test Scene opacity property"""
|
||||||
|
print("Testing scene opacity property...")
|
||||||
|
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
scene = TestScene("scene_opa_test")
|
||||||
|
|
||||||
|
# Test initial opacity (should be 1.0)
|
||||||
|
assert abs(scene.opacity - 1.0) < 0.001, f"Initial opacity should be 1.0, got {scene.opacity}"
|
||||||
|
|
||||||
|
# Test setting opacity
|
||||||
|
scene.opacity = 0.5
|
||||||
|
assert abs(scene.opacity - 0.5) < 0.001, f"opacity should be 0.5, got {scene.opacity}"
|
||||||
|
|
||||||
|
# Test clamping to 0.0
|
||||||
|
scene.opacity = -0.5
|
||||||
|
assert scene.opacity >= 0.0, f"opacity should be clamped to >= 0.0, got {scene.opacity}"
|
||||||
|
|
||||||
|
# Test clamping to 1.0
|
||||||
|
scene.opacity = 1.5
|
||||||
|
assert scene.opacity <= 1.0, f"opacity should be clamped to <= 1.0, got {scene.opacity}"
|
||||||
|
|
||||||
|
print(" - Scene opacity property: PASS")
|
||||||
|
|
||||||
|
def test_scene_name():
|
||||||
|
"""Test Scene name property (read-only)"""
|
||||||
|
print("Testing scene name property...")
|
||||||
|
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
scene = TestScene("my_test_scene")
|
||||||
|
|
||||||
|
# Test name
|
||||||
|
assert scene.name == "my_test_scene", f"name should be 'my_test_scene', got {scene.name}"
|
||||||
|
|
||||||
|
# Name should be read-only (trying to set should raise)
|
||||||
|
try:
|
||||||
|
scene.name = "other_name"
|
||||||
|
print(" - Scene name should be read-only: FAIL")
|
||||||
|
sys.exit(1)
|
||||||
|
except AttributeError:
|
||||||
|
pass # Expected
|
||||||
|
|
||||||
|
print(" - Scene name property: PASS")
|
||||||
|
|
||||||
|
def test_scene_active():
|
||||||
|
"""Test Scene active property"""
|
||||||
|
print("Testing scene active property...")
|
||||||
|
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
scene1 = TestScene("active_test_1")
|
||||||
|
scene2 = TestScene("active_test_2")
|
||||||
|
|
||||||
|
# Activate scene1
|
||||||
|
scene1.activate()
|
||||||
|
assert scene1.active == True, f"scene1.active should be True after activation"
|
||||||
|
assert scene2.active == False, f"scene2.active should be False"
|
||||||
|
|
||||||
|
# Activate scene2
|
||||||
|
scene2.activate()
|
||||||
|
assert scene1.active == False, f"scene1.active should be False after activating scene2"
|
||||||
|
assert scene2.active == True, f"scene2.active should be True"
|
||||||
|
|
||||||
|
print(" - Scene active property: PASS")
|
||||||
|
|
||||||
|
def test_scene_get_ui():
|
||||||
|
"""Test Scene get_ui method"""
|
||||||
|
print("Testing scene get_ui method...")
|
||||||
|
|
||||||
|
class TestScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
scene = TestScene("ui_test_scene")
|
||||||
|
|
||||||
|
# Get UI collection
|
||||||
|
ui = scene.get_ui()
|
||||||
|
assert ui is not None, "get_ui() should return a collection"
|
||||||
|
|
||||||
|
# Add some elements
|
||||||
|
ui.append(mcrfpy.Frame(pos=(10, 20), size=(100, 100)))
|
||||||
|
ui.append(mcrfpy.Caption(text="Test", pos=(50, 50)))
|
||||||
|
|
||||||
|
# Verify length
|
||||||
|
assert len(ui) == 2, f"UI should have 2 elements, got {len(ui)}"
|
||||||
|
|
||||||
|
print(" - Scene get_ui method: PASS")
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
test_scene_pos()
|
||||||
|
test_scene_visible()
|
||||||
|
test_scene_opacity()
|
||||||
|
test_scene_name()
|
||||||
|
test_scene_active()
|
||||||
|
test_scene_get_ui()
|
||||||
|
|
||||||
|
print("\n=== All Scene property tests passed! ===")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nFAIL: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
Loading…
Reference in New Issue