BREAKING CHANGE: Hover callbacks now take only (pos) instead of (pos, button, action) - Add PyHoverCallable class for on_enter/on_exit/on_move callbacks (position-only) - Add PyCellHoverCallable class for on_cell_enter/on_cell_exit callbacks - Change UIDrawable member types from PyClickCallable to PyHoverCallable - Update PyScene::do_mouse_hover() to call hover callbacks with only position - Add tryCallPythonMethod overload for position-only subclass method calls - Update UIGrid::fireCellEnter/fireCellExit to use position-only signature - Update all tests for new callback signatures New callback signatures: | Callback | Old | New | |----------------|--------------------------|------------| | on_enter | (pos, button, action) | (pos) | | on_exit | (pos, button, action) | (pos) | | on_move | (pos, button, action) | (pos) | | on_cell_enter | (cell_pos, button, action)| (cell_pos)| | on_cell_exit | (cell_pos, button, action)| (cell_pos)| | on_click | unchanged | unchanged | | on_cell_click | unchanged | unchanged | closes #230 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2421 lines
84 KiB
C++
2421 lines
84 KiB
C++
#include "UIDrawable.h"
|
|
#include "UIFrame.h"
|
|
#include "UICaption.h"
|
|
#include "UISprite.h"
|
|
#include "UIGrid.h"
|
|
#include "UILine.h"
|
|
#include "UICircle.h"
|
|
#include "UIArc.h"
|
|
#include "GameEngine.h"
|
|
#include "McRFPy_API.h"
|
|
#include "PythonObjectCache.h"
|
|
#include "Animation.h"
|
|
#include "PyAnimation.h"
|
|
#include "PyEasing.h"
|
|
#include "PySceneObject.h" // #183: For scene parent lookup
|
|
#include "PyShader.h" // #106: Shader support
|
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
|
|
|
// Helper function to extract UIDrawable* from any Python UI object
|
|
// Returns nullptr and sets Python error on failure
|
|
static UIDrawable* extractDrawable(PyObject* self, PyObjectsEnum objtype) {
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
return ((PyUIFrameObject*)self)->data.get();
|
|
case PyObjectsEnum::UICAPTION:
|
|
return ((PyUICaptionObject*)self)->data.get();
|
|
case PyObjectsEnum::UISPRITE:
|
|
return ((PyUISpriteObject*)self)->data.get();
|
|
case PyObjectsEnum::UIGRID:
|
|
return ((PyUIGridObject*)self)->data.get();
|
|
case PyObjectsEnum::UILINE:
|
|
return ((PyUILineObject*)self)->data.get();
|
|
case PyObjectsEnum::UICIRCLE:
|
|
return ((PyUICircleObject*)self)->data.get();
|
|
case PyObjectsEnum::UIARC:
|
|
return ((PyUIArcObject*)self)->data.get();
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
|
|
|
|
UIDrawable::UIDrawable(const UIDrawable& other)
|
|
: z_index(other.z_index),
|
|
name(other.name),
|
|
position(other.position),
|
|
rotation(other.rotation),
|
|
origin(other.origin),
|
|
rotate_with_camera(other.rotate_with_camera),
|
|
visible(other.visible),
|
|
opacity(other.opacity),
|
|
hovered(false), // Don't copy hover state
|
|
serial_number(0), // Don't copy serial number
|
|
use_render_texture(other.use_render_texture),
|
|
render_dirty(true) // Force redraw after copy
|
|
{
|
|
// Deep copy click_callable if it exists
|
|
if (other.click_callable) {
|
|
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
|
}
|
|
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
|
|
if (other.on_enter_callable) {
|
|
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
|
|
}
|
|
if (other.on_exit_callable) {
|
|
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
|
|
}
|
|
// #141, #230 - Deep copy move callable (now PyHoverCallable)
|
|
if (other.on_move_callable) {
|
|
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
|
|
}
|
|
|
|
// Deep copy render texture if needed
|
|
if (other.render_texture && other.use_render_texture) {
|
|
auto size = other.render_texture->getSize();
|
|
enableRenderTexture(size.x, size.y);
|
|
}
|
|
}
|
|
|
|
UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
|
|
if (this != &other) {
|
|
// Copy basic members
|
|
z_index = other.z_index;
|
|
name = other.name;
|
|
position = other.position;
|
|
rotation = other.rotation;
|
|
origin = other.origin;
|
|
rotate_with_camera = other.rotate_with_camera;
|
|
visible = other.visible;
|
|
opacity = other.opacity;
|
|
hovered = false; // Don't copy hover state
|
|
use_render_texture = other.use_render_texture;
|
|
render_dirty = true; // Force redraw after copy
|
|
|
|
// Deep copy click_callable
|
|
if (other.click_callable) {
|
|
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
|
} else {
|
|
click_callable.reset();
|
|
}
|
|
// #140, #230 - Deep copy enter/exit callables (now PyHoverCallable)
|
|
if (other.on_enter_callable) {
|
|
on_enter_callable = std::make_unique<PyHoverCallable>(*other.on_enter_callable);
|
|
} else {
|
|
on_enter_callable.reset();
|
|
}
|
|
if (other.on_exit_callable) {
|
|
on_exit_callable = std::make_unique<PyHoverCallable>(*other.on_exit_callable);
|
|
} else {
|
|
on_exit_callable.reset();
|
|
}
|
|
// #141, #230 - Deep copy move callable (now PyHoverCallable)
|
|
if (other.on_move_callable) {
|
|
on_move_callable = std::make_unique<PyHoverCallable>(*other.on_move_callable);
|
|
} else {
|
|
on_move_callable.reset();
|
|
}
|
|
|
|
// Deep copy render texture if needed
|
|
if (other.render_texture && other.use_render_texture) {
|
|
auto size = other.render_texture->getSize();
|
|
enableRenderTexture(size.x, size.y);
|
|
} else {
|
|
render_texture.reset();
|
|
use_render_texture = false;
|
|
}
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
UIDrawable::UIDrawable(UIDrawable&& other) noexcept
|
|
: z_index(other.z_index),
|
|
name(std::move(other.name)),
|
|
position(other.position),
|
|
rotation(other.rotation),
|
|
origin(other.origin),
|
|
rotate_with_camera(other.rotate_with_camera),
|
|
visible(other.visible),
|
|
opacity(other.opacity),
|
|
hovered(other.hovered),
|
|
serial_number(other.serial_number),
|
|
click_callable(std::move(other.click_callable)),
|
|
on_enter_callable(std::move(other.on_enter_callable)), // #140
|
|
on_exit_callable(std::move(other.on_exit_callable)), // #140
|
|
on_move_callable(std::move(other.on_move_callable)), // #141
|
|
render_texture(std::move(other.render_texture)),
|
|
render_sprite(std::move(other.render_sprite)),
|
|
use_render_texture(other.use_render_texture),
|
|
render_dirty(other.render_dirty)
|
|
{
|
|
// Clear the moved-from object's serial number to avoid cache issues
|
|
other.serial_number = 0;
|
|
other.hovered = false; // #140
|
|
}
|
|
|
|
UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
|
|
if (this != &other) {
|
|
// Clear our own cache entry if we have one
|
|
if (serial_number != 0) {
|
|
PythonObjectCache::getInstance().remove(serial_number);
|
|
}
|
|
|
|
// Move basic members
|
|
z_index = other.z_index;
|
|
name = std::move(other.name);
|
|
position = other.position;
|
|
rotation = other.rotation;
|
|
origin = other.origin;
|
|
rotate_with_camera = other.rotate_with_camera;
|
|
visible = other.visible;
|
|
opacity = other.opacity;
|
|
hovered = other.hovered; // #140
|
|
serial_number = other.serial_number;
|
|
use_render_texture = other.use_render_texture;
|
|
render_dirty = other.render_dirty;
|
|
|
|
// Move unique_ptr members
|
|
click_callable = std::move(other.click_callable);
|
|
on_enter_callable = std::move(other.on_enter_callable); // #140
|
|
on_exit_callable = std::move(other.on_exit_callable); // #140
|
|
on_move_callable = std::move(other.on_move_callable); // #141
|
|
render_texture = std::move(other.render_texture);
|
|
render_sprite = std::move(other.render_sprite);
|
|
|
|
// Clear the moved-from object's serial number
|
|
other.serial_number = 0;
|
|
other.hovered = false; // #140
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
UIDrawable::~UIDrawable() {
|
|
if (serial_number != 0) {
|
|
PythonObjectCache::getInstance().remove(serial_number);
|
|
}
|
|
}
|
|
|
|
void UIDrawable::click_unregister()
|
|
{
|
|
click_callable.reset();
|
|
}
|
|
|
|
void UIDrawable::render()
|
|
{
|
|
render(sf::Vector2f(), Resources::game->getRenderTarget());
|
|
}
|
|
|
|
PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure)); // trust me bro, it's an Enum
|
|
PyObject* ptr;
|
|
|
|
switch (objtype)
|
|
{
|
|
case PyObjectsEnum::UIFRAME:
|
|
if (((PyUIFrameObject*)self)->data->click_callable)
|
|
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
|
|
else
|
|
ptr = NULL;
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
if (((PyUICaptionObject*)self)->data->click_callable)
|
|
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
|
|
else
|
|
ptr = NULL;
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
if (((PyUISpriteObject*)self)->data->click_callable)
|
|
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
|
|
else
|
|
ptr = NULL;
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
if (((PyUIGridObject*)self)->data->click_callable)
|
|
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
|
|
else
|
|
ptr = NULL;
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
if (((PyUILineObject*)self)->data->click_callable)
|
|
ptr = ((PyUILineObject*)self)->data->click_callable->borrow();
|
|
else
|
|
ptr = NULL;
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
if (((PyUICircleObject*)self)->data->click_callable)
|
|
ptr = ((PyUICircleObject*)self)->data->click_callable->borrow();
|
|
else
|
|
ptr = NULL;
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
if (((PyUIArcObject*)self)->data->click_callable)
|
|
ptr = ((PyUIArcObject*)self)->data->click_callable->borrow();
|
|
else
|
|
ptr = NULL;
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
|
|
return NULL;
|
|
}
|
|
if (ptr && ptr != Py_None) {
|
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
|
return ptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure)); // trust me bro, it's an Enum
|
|
UIDrawable* target;
|
|
switch (objtype)
|
|
{
|
|
case PyObjectsEnum::UIFRAME:
|
|
target = (((PyUIFrameObject*)self)->data.get());
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
target = (((PyUICaptionObject*)self)->data.get());
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
target = (((PyUISpriteObject*)self)->data.get());
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
target = (((PyUIGridObject*)self)->data.get());
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
target = (((PyUILineObject*)self)->data.get());
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
target = (((PyUICircleObject*)self)->data.get());
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
target = (((PyUIArcObject*)self)->data.get());
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _set_click");
|
|
return -1;
|
|
}
|
|
|
|
if (value == Py_None)
|
|
{
|
|
target->click_unregister();
|
|
} else {
|
|
target->click_register(value);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void UIDrawable::click_register(PyObject* callable)
|
|
{
|
|
click_callable = std::make_unique<PyClickCallable>(callable);
|
|
}
|
|
|
|
// #140, #230 - Mouse enter/exit callback registration (now PyHoverCallable)
|
|
void UIDrawable::on_enter_register(PyObject* callable)
|
|
{
|
|
on_enter_callable = std::make_unique<PyHoverCallable>(callable);
|
|
}
|
|
|
|
void UIDrawable::on_enter_unregister()
|
|
{
|
|
on_enter_callable.reset();
|
|
}
|
|
|
|
void UIDrawable::on_exit_register(PyObject* callable)
|
|
{
|
|
on_exit_callable = std::make_unique<PyHoverCallable>(callable);
|
|
}
|
|
|
|
void UIDrawable::on_exit_unregister()
|
|
{
|
|
on_exit_callable.reset();
|
|
}
|
|
|
|
// #141, #230 - Mouse move callback registration (now PyHoverCallable)
|
|
void UIDrawable::on_move_register(PyObject* callable)
|
|
{
|
|
on_move_callable = std::make_unique<PyHoverCallable>(callable);
|
|
}
|
|
|
|
void UIDrawable::on_move_unregister()
|
|
{
|
|
on_move_callable.reset();
|
|
}
|
|
|
|
PyObject* UIDrawable::get_int(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyLong_FromLong(drawable->z_index);
|
|
}
|
|
|
|
int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
if (!PyLong_Check(value)) {
|
|
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
|
|
return -1;
|
|
}
|
|
|
|
long z = PyLong_AsLong(value);
|
|
if (z == -1 && PyErr_Occurred()) {
|
|
return -1;
|
|
}
|
|
|
|
// Clamp to int range
|
|
if (z < INT_MIN) z = INT_MIN;
|
|
if (z > INT_MAX) z = INT_MAX;
|
|
|
|
int old_z_index = drawable->z_index;
|
|
drawable->z_index = static_cast<int>(z);
|
|
|
|
// Notify of z_index change
|
|
if (old_z_index != drawable->z_index) {
|
|
drawable->notifyZIndexChanged();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void UIDrawable::notifyZIndexChanged() {
|
|
// Mark the current scene as needing sort
|
|
// This works for elements in the scene's ui_elements collection
|
|
McRFPy_API::markSceneNeedsSort();
|
|
|
|
// TODO: In the future, we could add parent tracking to handle Frame children
|
|
// For now, Frame children will need manual sorting or collection modification
|
|
// to trigger a resort
|
|
}
|
|
|
|
PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyUnicode_FromString(drawable->name.c_str());
|
|
}
|
|
|
|
int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
if (value == NULL || value == Py_None) {
|
|
drawable->name = "";
|
|
return 0;
|
|
}
|
|
|
|
if (!PyUnicode_Check(value)) {
|
|
PyErr_SetString(PyExc_TypeError, "name must be a string");
|
|
return -1;
|
|
}
|
|
|
|
const char* name_str = PyUnicode_AsUTF8(value);
|
|
if (!name_str) {
|
|
return -1;
|
|
}
|
|
|
|
drawable->name = name_str;
|
|
return 0;
|
|
}
|
|
|
|
void UIDrawable::enableRenderTexture(unsigned int width, unsigned int height) {
|
|
// Create or recreate RenderTexture if size changed
|
|
if (!render_texture || render_texture->getSize().x != width || render_texture->getSize().y != height) {
|
|
render_texture = std::make_unique<sf::RenderTexture>();
|
|
if (!render_texture->create(width, height)) {
|
|
render_texture.reset();
|
|
use_render_texture = false;
|
|
return;
|
|
}
|
|
render_sprite.setTexture(render_texture->getTexture());
|
|
}
|
|
|
|
use_render_texture = true;
|
|
render_dirty = true;
|
|
}
|
|
|
|
void UIDrawable::disableRenderTexture() {
|
|
if (!use_render_texture) return;
|
|
|
|
render_texture.reset();
|
|
render_sprite = sf::Sprite(); // Clear stale texture reference
|
|
use_render_texture = false;
|
|
render_dirty = true;
|
|
}
|
|
|
|
void UIDrawable::updateRenderTexture() {
|
|
if (!use_render_texture || !render_texture) {
|
|
return;
|
|
}
|
|
|
|
// Clear the RenderTexture
|
|
render_texture->clear(sf::Color::Transparent);
|
|
|
|
// Render content to RenderTexture
|
|
// This will be overridden by derived classes
|
|
// For now, just display the texture
|
|
render_texture->display();
|
|
|
|
// Update the sprite
|
|
render_sprite.setTexture(render_texture->getTexture());
|
|
}
|
|
|
|
PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
|
|
int member = reinterpret_cast<intptr_t>(closure) & 0xFF;
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
switch (member) {
|
|
case 0: // x
|
|
return PyFloat_FromDouble(drawable->position.x);
|
|
case 1: // y
|
|
return PyFloat_FromDouble(drawable->position.y);
|
|
case 2: // w (width) - delegate to get_bounds
|
|
return PyFloat_FromDouble(drawable->get_bounds().width);
|
|
case 3: // h (height) - delegate to get_bounds
|
|
return PyFloat_FromDouble(drawable->get_bounds().height);
|
|
default:
|
|
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
|
|
int member = reinterpret_cast<intptr_t>(closure) & 0xFF;
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
float val = 0.0f;
|
|
if (PyFloat_Check(value)) {
|
|
val = PyFloat_AsDouble(value);
|
|
} else if (PyLong_Check(value)) {
|
|
val = static_cast<float>(PyLong_AsLong(value));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
|
|
return -1;
|
|
}
|
|
|
|
switch (member) {
|
|
case 0: // x
|
|
drawable->position.x = val;
|
|
drawable->onPositionChanged();
|
|
break;
|
|
case 1: // y
|
|
drawable->position.y = val;
|
|
drawable->onPositionChanged();
|
|
break;
|
|
case 2: // w
|
|
case 3: // h
|
|
{
|
|
sf::FloatRect bounds = drawable->get_bounds();
|
|
if (member == 2) {
|
|
drawable->resize(val, bounds.height);
|
|
} else {
|
|
drawable->resize(bounds.width, val);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIDrawable::get_pos(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
// Create a Python Vector object from position
|
|
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)", drawable->position.x, drawable->position.y);
|
|
PyObject* result = PyObject_CallObject(vector_type, args);
|
|
Py_DECREF(vector_type);
|
|
Py_DECREF(args);
|
|
|
|
return result;
|
|
}
|
|
|
|
int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
// Accept tuple or Vector
|
|
float x, y;
|
|
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
|
PyObject* x_obj = PyTuple_GetItem(value, 0);
|
|
PyObject* y_obj = PyTuple_GetItem(value, 1);
|
|
|
|
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
|
|
x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Position x must be a number");
|
|
return -1;
|
|
}
|
|
|
|
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
|
|
y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Position y must be a number");
|
|
return -1;
|
|
}
|
|
} else {
|
|
// Try to get as Vector
|
|
PyObject* module = PyImport_ImportModule("mcrfpy");
|
|
if (!module) return -1;
|
|
|
|
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
|
|
Py_DECREF(module);
|
|
if (!vector_type) return -1;
|
|
|
|
int is_vector = PyObject_IsInstance(value, vector_type);
|
|
Py_DECREF(vector_type);
|
|
|
|
if (is_vector) {
|
|
PyVectorObject* vec = (PyVectorObject*)value;
|
|
x = vec->data.x;
|
|
y = vec->data.y;
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Position must be a tuple (x, y) or Vector");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
drawable->position = sf::Vector2f(x, y);
|
|
drawable->onPositionChanged();
|
|
return 0;
|
|
}
|
|
|
|
// Rotation property getter/setter
|
|
PyObject* UIDrawable::get_rotation(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyFloat_FromDouble(drawable->rotation);
|
|
}
|
|
|
|
int UIDrawable::set_rotation(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
float val = 0.0f;
|
|
if (PyFloat_Check(value)) {
|
|
val = PyFloat_AsDouble(value);
|
|
} else if (PyLong_Check(value)) {
|
|
val = static_cast<float>(PyLong_AsLong(value));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "rotation must be a number (int or float)");
|
|
return -1;
|
|
}
|
|
|
|
drawable->rotation = val;
|
|
drawable->markDirty();
|
|
return 0;
|
|
}
|
|
|
|
// Origin property getter/setter
|
|
PyObject* UIDrawable::get_origin(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
// Create a Python Vector object from origin
|
|
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)", drawable->origin.x, drawable->origin.y);
|
|
PyObject* result = PyObject_CallObject(vector_type, args);
|
|
Py_DECREF(vector_type);
|
|
Py_DECREF(args);
|
|
|
|
return result;
|
|
}
|
|
|
|
int UIDrawable::set_origin(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
// Accept tuple or Vector
|
|
float x, y;
|
|
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
|
PyObject* x_obj = PyTuple_GetItem(value, 0);
|
|
PyObject* y_obj = PyTuple_GetItem(value, 1);
|
|
|
|
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
|
|
x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "origin x must be a number");
|
|
return -1;
|
|
}
|
|
|
|
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
|
|
y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "origin y must be a number");
|
|
return -1;
|
|
}
|
|
} else {
|
|
// Try to get as Vector
|
|
PyObject* module = PyImport_ImportModule("mcrfpy");
|
|
if (!module) return -1;
|
|
|
|
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
|
|
Py_DECREF(module);
|
|
if (!vector_type) return -1;
|
|
|
|
int is_vector = PyObject_IsInstance(value, vector_type);
|
|
Py_DECREF(vector_type);
|
|
|
|
if (is_vector) {
|
|
PyVectorObject* vec = (PyVectorObject*)value;
|
|
x = vec->data.x;
|
|
y = vec->data.y;
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "origin must be a tuple (x, y) or Vector");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
drawable->origin = sf::Vector2f(x, y);
|
|
drawable->markDirty();
|
|
return 0;
|
|
}
|
|
|
|
// rotate_with_camera property getter/setter
|
|
PyObject* UIDrawable::get_rotate_with_camera(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyBool_FromLong(drawable->rotate_with_camera);
|
|
}
|
|
|
|
int UIDrawable::set_rotate_with_camera(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
if (!PyBool_Check(value)) {
|
|
PyErr_SetString(PyExc_TypeError, "rotate_with_camera must be a boolean");
|
|
return -1;
|
|
}
|
|
|
|
drawable->rotate_with_camera = PyObject_IsTrue(value);
|
|
drawable->markDirty();
|
|
return 0;
|
|
}
|
|
|
|
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
|
PyObject* UIDrawable::get_grid_pos(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
// Check if parent is a UIGrid
|
|
auto parent_ptr = drawable->getParent();
|
|
if (!parent_ptr) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
|
return NULL;
|
|
}
|
|
|
|
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
|
if (!grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
|
return NULL;
|
|
}
|
|
|
|
// Calculate grid position from pixel position
|
|
sf::Vector2f cell_size = grid->getEffectiveCellSize();
|
|
float grid_x = drawable->position.x / cell_size.x;
|
|
float grid_y = drawable->position.y / cell_size.y;
|
|
|
|
// Return as Vector
|
|
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
if (!vector_type) return NULL;
|
|
|
|
PyObject* args = Py_BuildValue("(ff)", grid_x, grid_y);
|
|
PyObject* result = PyObject_CallObject(vector_type, args);
|
|
Py_DECREF(vector_type);
|
|
Py_DECREF(args);
|
|
|
|
return result;
|
|
}
|
|
|
|
int UIDrawable::set_grid_pos(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
// Check if parent is a UIGrid
|
|
auto parent_ptr = drawable->getParent();
|
|
if (!parent_ptr) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
|
return -1;
|
|
}
|
|
|
|
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
|
if (!grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
|
return -1;
|
|
}
|
|
|
|
// Parse the grid position value
|
|
float grid_x, grid_y;
|
|
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
|
PyObject* x_obj = PyTuple_GetItem(value, 0);
|
|
PyObject* y_obj = PyTuple_GetItem(value, 1);
|
|
|
|
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
|
|
grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_pos x must be a number");
|
|
return -1;
|
|
}
|
|
|
|
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
|
|
grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_pos y must be a number");
|
|
return -1;
|
|
}
|
|
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
|
|
// Vector-like object
|
|
PyObject* x_attr = PyObject_GetAttrString(value, "x");
|
|
PyObject* y_attr = PyObject_GetAttrString(value, "y");
|
|
|
|
if (x_attr && (PyFloat_Check(x_attr) || PyLong_Check(x_attr))) {
|
|
grid_x = PyFloat_Check(x_attr) ? PyFloat_AsDouble(x_attr) : static_cast<float>(PyLong_AsLong(x_attr));
|
|
} else {
|
|
Py_XDECREF(x_attr);
|
|
Py_XDECREF(y_attr);
|
|
PyErr_SetString(PyExc_TypeError, "grid_pos x must be a number");
|
|
return -1;
|
|
}
|
|
|
|
if (y_attr && (PyFloat_Check(y_attr) || PyLong_Check(y_attr))) {
|
|
grid_y = PyFloat_Check(y_attr) ? PyFloat_AsDouble(y_attr) : static_cast<float>(PyLong_AsLong(y_attr));
|
|
} else {
|
|
Py_XDECREF(x_attr);
|
|
Py_XDECREF(y_attr);
|
|
PyErr_SetString(PyExc_TypeError, "grid_pos y must be a number");
|
|
return -1;
|
|
}
|
|
|
|
Py_DECREF(x_attr);
|
|
Py_DECREF(y_attr);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y) or Vector");
|
|
return -1;
|
|
}
|
|
|
|
// Convert grid position to pixel position
|
|
sf::Vector2f cell_size = grid->getEffectiveCellSize();
|
|
drawable->position.x = grid_x * cell_size.x;
|
|
drawable->position.y = grid_y * cell_size.y;
|
|
drawable->onPositionChanged();
|
|
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIDrawable::get_grid_size(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
// Check if parent is a UIGrid
|
|
auto parent_ptr = drawable->getParent();
|
|
if (!parent_ptr) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
|
return NULL;
|
|
}
|
|
|
|
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
|
if (!grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
|
return NULL;
|
|
}
|
|
|
|
// Calculate grid size from pixel size
|
|
sf::FloatRect bounds = drawable->get_bounds();
|
|
sf::Vector2f cell_size = grid->getEffectiveCellSize();
|
|
float grid_w = bounds.width / cell_size.x;
|
|
float grid_h = bounds.height / cell_size.y;
|
|
|
|
// Return as Vector
|
|
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
if (!vector_type) return NULL;
|
|
|
|
PyObject* args = Py_BuildValue("(ff)", grid_w, grid_h);
|
|
PyObject* result = PyObject_CallObject(vector_type, args);
|
|
Py_DECREF(vector_type);
|
|
Py_DECREF(args);
|
|
|
|
return result;
|
|
}
|
|
|
|
int UIDrawable::set_grid_size(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
// Check if parent is a UIGrid
|
|
auto parent_ptr = drawable->getParent();
|
|
if (!parent_ptr) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
|
return -1;
|
|
}
|
|
|
|
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
|
if (!grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
|
return -1;
|
|
}
|
|
|
|
// Parse the grid size value
|
|
float grid_w, grid_h;
|
|
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
|
PyObject* w_obj = PyTuple_GetItem(value, 0);
|
|
PyObject* h_obj = PyTuple_GetItem(value, 1);
|
|
|
|
if (PyFloat_Check(w_obj) || PyLong_Check(w_obj)) {
|
|
grid_w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : static_cast<float>(PyLong_AsLong(w_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_size width must be a number");
|
|
return -1;
|
|
}
|
|
|
|
if (PyFloat_Check(h_obj) || PyLong_Check(h_obj)) {
|
|
grid_h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : static_cast<float>(PyLong_AsLong(h_obj));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_size height must be a number");
|
|
return -1;
|
|
}
|
|
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
|
|
// Vector-like object
|
|
PyObject* x_attr = PyObject_GetAttrString(value, "x");
|
|
PyObject* y_attr = PyObject_GetAttrString(value, "y");
|
|
|
|
if (x_attr && (PyFloat_Check(x_attr) || PyLong_Check(x_attr))) {
|
|
grid_w = PyFloat_Check(x_attr) ? PyFloat_AsDouble(x_attr) : static_cast<float>(PyLong_AsLong(x_attr));
|
|
} else {
|
|
Py_XDECREF(x_attr);
|
|
Py_XDECREF(y_attr);
|
|
PyErr_SetString(PyExc_TypeError, "grid_size width must be a number");
|
|
return -1;
|
|
}
|
|
|
|
if (y_attr && (PyFloat_Check(y_attr) || PyLong_Check(y_attr))) {
|
|
grid_h = PyFloat_Check(y_attr) ? PyFloat_AsDouble(y_attr) : static_cast<float>(PyLong_AsLong(y_attr));
|
|
} else {
|
|
Py_XDECREF(x_attr);
|
|
Py_XDECREF(y_attr);
|
|
PyErr_SetString(PyExc_TypeError, "grid_size height must be a number");
|
|
return -1;
|
|
}
|
|
|
|
Py_DECREF(x_attr);
|
|
Py_DECREF(y_attr);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (w, h) or Vector");
|
|
return -1;
|
|
}
|
|
|
|
// Convert grid size to pixel size and resize
|
|
sf::Vector2f cell_size = grid->getEffectiveCellSize();
|
|
drawable->resize(grid_w * cell_size.x, grid_h * cell_size.y);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// #122 - Parent-child hierarchy implementation
|
|
void UIDrawable::setParent(std::shared_ptr<UIDrawable> new_parent) {
|
|
parent = new_parent;
|
|
parent_scene.clear(); // #183: Clear scene parent when setting drawable parent
|
|
|
|
// Apply alignment when parent is set (if alignment is configured)
|
|
if (new_parent && align_type != AlignmentType::NONE) {
|
|
applyAlignment();
|
|
}
|
|
}
|
|
|
|
void UIDrawable::setParentScene(const std::string& scene_name) {
|
|
parent.reset(); // #183: Clear drawable parent when setting scene parent
|
|
parent_scene = scene_name;
|
|
|
|
// Apply alignment when scene parent is set (if alignment is configured)
|
|
if (!scene_name.empty() && align_type != AlignmentType::NONE) {
|
|
applyAlignment();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<UIDrawable> UIDrawable::getParent() const {
|
|
return parent.lock();
|
|
}
|
|
|
|
void UIDrawable::removeFromParent() {
|
|
// #183: Handle scene parent removal
|
|
if (!parent_scene.empty()) {
|
|
auto ui = Resources::game->scene_ui(parent_scene);
|
|
if (ui) {
|
|
for (auto it = ui->begin(); it != ui->end(); ++it) {
|
|
if (it->get() == this) {
|
|
ui->erase(it);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
parent_scene.clear();
|
|
return;
|
|
}
|
|
|
|
// Handle drawable parent removal
|
|
auto p = parent.lock();
|
|
if (!p) return;
|
|
|
|
// Check if parent is a UIFrame or UIGrid (both have 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;
|
|
}
|
|
else if (p->derived_type() == PyObjectsEnum::UIGRID) {
|
|
auto grid = std::static_pointer_cast<UIGrid>(p);
|
|
auto& children = *grid->children;
|
|
|
|
for (auto it = children.begin(); it != children.end(); ++it) {
|
|
if (it->get() == this) {
|
|
children.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
grid->children_need_sort = true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// #138 - Global bounds (bounds in screen coordinates)
|
|
sf::FloatRect UIDrawable::get_global_bounds() const {
|
|
sf::FloatRect local_bounds = get_bounds();
|
|
sf::Vector2f global_pos = get_global_position();
|
|
|
|
// Return bounds offset to global position
|
|
return sf::FloatRect(global_pos.x, global_pos.y, local_bounds.width, local_bounds.height);
|
|
}
|
|
|
|
// #138 - Hit testing
|
|
bool UIDrawable::contains_point(float x, float y) const {
|
|
sf::FloatRect global_bounds = get_global_bounds();
|
|
return global_bounds.contains(x, y);
|
|
}
|
|
|
|
// #144: Content dirty - texture needs rebuild
|
|
void UIDrawable::markContentDirty() {
|
|
bool was_dirty = render_dirty;
|
|
render_dirty = true;
|
|
composite_dirty = true; // If content changed, composite also needs update
|
|
|
|
// Propagate to parent - parent's composite is dirty (child content changed)
|
|
// Propagate if: we weren't already dirty, OR parent was cleared (rendered) since last propagation
|
|
auto p = parent.lock();
|
|
if (p && (!was_dirty || !p->render_dirty)) {
|
|
p->markContentDirty(); // Parent also needs to rebuild to include our changes
|
|
}
|
|
}
|
|
|
|
// #144: Composite dirty - position changed, texture still valid
|
|
void UIDrawable::markCompositeDirty() {
|
|
// Don't set render_dirty - our cached texture is still valid
|
|
// Only mark composite_dirty so parent knows to re-blit us
|
|
|
|
// Propagate to parent - parent needs to re-composite
|
|
auto p = parent.lock();
|
|
if (p) {
|
|
p->composite_dirty = true;
|
|
p->render_dirty = true; // Parent needs to re-render (re-composite children)
|
|
p->markCompositeDirty(); // Continue propagating up
|
|
}
|
|
}
|
|
|
|
// Legacy method - calls markContentDirty for backwards compatibility
|
|
void UIDrawable::markDirty() {
|
|
markContentDirty();
|
|
}
|
|
|
|
// #106 - Shader support
|
|
void UIDrawable::markShaderDynamic() {
|
|
shader_dynamic = true;
|
|
|
|
// Propagate to parent to invalidate caches
|
|
auto p = parent.lock();
|
|
if (p) {
|
|
p->markShaderDynamic();
|
|
}
|
|
}
|
|
|
|
// #106: Shader uniform property helpers for animation support
|
|
bool UIDrawable::setShaderProperty(const std::string& name, float value) {
|
|
// Check if name starts with "shader."
|
|
if (name.compare(0, 7, "shader.") != 0) {
|
|
return false;
|
|
}
|
|
|
|
// Extract the uniform name after "shader."
|
|
std::string uniform_name = name.substr(7);
|
|
if (uniform_name.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// Initialize uniforms collection if needed
|
|
if (!uniforms) {
|
|
uniforms = std::make_unique<UniformCollection>();
|
|
}
|
|
|
|
// Set the uniform value
|
|
uniforms->setFloat(uniform_name, value);
|
|
markDirty();
|
|
return true;
|
|
}
|
|
|
|
bool UIDrawable::getShaderProperty(const std::string& name, float& value) const {
|
|
// Check if name starts with "shader."
|
|
if (name.compare(0, 7, "shader.") != 0) {
|
|
return false;
|
|
}
|
|
|
|
// Extract the uniform name after "shader."
|
|
std::string uniform_name = name.substr(7);
|
|
if (uniform_name.empty() || !uniforms) {
|
|
return false;
|
|
}
|
|
|
|
// Try to get the value from uniforms
|
|
const auto* entry = uniforms->getEntry(uniform_name);
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
|
|
// UniformEntry is variant<UniformValue, shared_ptr<PropertyBinding>, shared_ptr<CallableBinding>>
|
|
// UniformValue is variant<float, vec2, vec3, vec4>
|
|
// So we need to check for UniformValue first, then extract the float from it
|
|
|
|
// Try to extract static UniformValue from the entry
|
|
if (const auto* uval = std::get_if<UniformValue>(entry)) {
|
|
// Now try to extract float from UniformValue
|
|
if (const float* fval = std::get_if<float>(uval)) {
|
|
value = *fval;
|
|
return true;
|
|
}
|
|
// Could be vec2/vec3/vec4 - not a float, return false
|
|
return false;
|
|
}
|
|
|
|
// For bindings, evaluate and return
|
|
if (const auto* prop_binding = std::get_if<std::shared_ptr<PropertyBinding>>(entry)) {
|
|
auto opt_val = (*prop_binding)->evaluate();
|
|
if (opt_val) {
|
|
value = *opt_val;
|
|
return true;
|
|
}
|
|
} else if (const auto* call_binding = std::get_if<std::shared_ptr<CallableBinding>>(entry)) {
|
|
auto opt_val = (*call_binding)->evaluate();
|
|
if (opt_val) {
|
|
value = *opt_val;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UIDrawable::hasShaderProperty(const std::string& name) const {
|
|
// Check if name starts with "shader."
|
|
if (name.compare(0, 7, "shader.") != 0) {
|
|
return false;
|
|
}
|
|
|
|
// Shader uniforms are always valid property names (they'll be created on set)
|
|
return true;
|
|
}
|
|
|
|
// Python API for shader property
|
|
PyObject* UIDrawable::get_shader(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
if (!drawable->shader) {
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// Return the shader object (increment reference)
|
|
Py_INCREF(drawable->shader.get());
|
|
return (PyObject*)drawable->shader.get();
|
|
}
|
|
|
|
int UIDrawable::set_shader(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
if (value == Py_None) {
|
|
// Clear shader
|
|
drawable->shader.reset();
|
|
drawable->shader_dynamic = false;
|
|
drawable->markDirty();
|
|
return 0;
|
|
}
|
|
|
|
// Check if it's a Shader object
|
|
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyShaderType)) {
|
|
PyErr_SetString(PyExc_TypeError, "shader must be a Shader object or None");
|
|
return -1;
|
|
}
|
|
|
|
PyShaderObject* shader_obj = (PyShaderObject*)value;
|
|
if (!shader_obj->shader) {
|
|
PyErr_SetString(PyExc_ValueError, "Shader is not valid (compilation failed?)");
|
|
return -1;
|
|
}
|
|
|
|
// Store the shader
|
|
drawable->shader = std::shared_ptr<PyShaderObject>(shader_obj, [](PyShaderObject* p) {
|
|
// Custom deleter that doesn't delete the Python object
|
|
// The Python reference counting handles that
|
|
});
|
|
Py_INCREF(shader_obj); // Keep the Python object alive
|
|
|
|
// Create uniforms collection if needed
|
|
if (!drawable->uniforms) {
|
|
drawable->uniforms = std::make_unique<UniformCollection>();
|
|
}
|
|
|
|
// Set dynamic flag if shader is dynamic
|
|
if (shader_obj->dynamic) {
|
|
drawable->markShaderDynamic();
|
|
}
|
|
|
|
// Enable RenderTexture for shader rendering (if not already enabled)
|
|
auto bounds = drawable->get_bounds();
|
|
if (bounds.width > 0 && bounds.height > 0) {
|
|
drawable->enableRenderTexture(
|
|
static_cast<unsigned int>(bounds.width),
|
|
static_cast<unsigned int>(bounds.height)
|
|
);
|
|
}
|
|
|
|
drawable->markDirty();
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIDrawable::get_uniforms(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
// Create uniforms collection if needed
|
|
if (!drawable->uniforms) {
|
|
drawable->uniforms = std::make_unique<UniformCollection>();
|
|
}
|
|
|
|
// Create and return a Python wrapper for the collection
|
|
PyUniformCollectionObject* collection = (PyUniformCollectionObject*)
|
|
mcrfpydef::PyUniformCollectionType.tp_alloc(&mcrfpydef::PyUniformCollectionType, 0);
|
|
|
|
if (!collection) return NULL;
|
|
|
|
collection->collection = drawable->uniforms.get();
|
|
collection->weakreflist = NULL;
|
|
// Note: owner weak_ptr could be set here if we had access to shared_ptr
|
|
|
|
return (PyObject*)collection;
|
|
}
|
|
|
|
// Python API - get parent drawable
|
|
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
// #183: Check for scene parent first
|
|
if (!drawable->parent_scene.empty()) {
|
|
PyObject* scene = PySceneClass::get_scene_by_name(drawable->parent_scene);
|
|
if (scene) {
|
|
return scene; // Already has new reference from get_scene_by_name
|
|
}
|
|
// Scene not found in python_scenes (shouldn't happen, but fall through to None)
|
|
}
|
|
|
|
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 - set parent drawable (or None to remove from parent)
|
|
int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
std::shared_ptr<UIDrawable> drawable = nullptr;
|
|
|
|
// Get the shared_ptr for self
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
drawable = ((PyUIFrameObject*)self)->data;
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
drawable = ((PyUICaptionObject*)self)->data;
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
drawable = ((PyUISpriteObject*)self)->data;
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
drawable = ((PyUIGridObject*)self)->data;
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
drawable = ((PyUILineObject*)self)->data;
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
drawable = ((PyUICircleObject*)self)->data;
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
drawable = ((PyUIArcObject*)self)->data;
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
|
|
return -1;
|
|
}
|
|
|
|
// Handle None - remove from parent
|
|
if (value == Py_None) {
|
|
drawable->removeFromParent();
|
|
return 0;
|
|
}
|
|
|
|
// Value must be a Frame, Grid, or Scene (things with children collections)
|
|
PyTypeObject* frame_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
|
|
PyTypeObject* grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
|
PyTypeObject* scene_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Scene");
|
|
|
|
bool is_frame = frame_type && PyObject_IsInstance(value, (PyObject*)frame_type);
|
|
bool is_grid = grid_type && PyObject_IsInstance(value, (PyObject*)grid_type);
|
|
bool is_scene = scene_type && PyObject_IsInstance(value, (PyObject*)scene_type);
|
|
|
|
Py_XDECREF(frame_type);
|
|
Py_XDECREF(grid_type);
|
|
Py_XDECREF(scene_type);
|
|
|
|
if (!is_frame && !is_grid && !is_scene) {
|
|
PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Grid, Scene, or None");
|
|
return -1;
|
|
}
|
|
|
|
// Handle Scene parent specially - add to scene's children
|
|
if (is_scene) {
|
|
PySceneObject* scene_obj = (PySceneObject*)value;
|
|
std::string scene_name = scene_obj->name;
|
|
|
|
// Remove from old parent first
|
|
drawable->removeFromParent();
|
|
|
|
// Get the scene's UI elements and add
|
|
auto ui = Resources::game->scene_ui(scene_name);
|
|
if (ui) {
|
|
// Check if already in this scene (prevent duplicates)
|
|
bool already_present = false;
|
|
for (const auto& child : *ui) {
|
|
if (child.get() == drawable.get()) {
|
|
already_present = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!already_present) {
|
|
ui->push_back(drawable);
|
|
drawable->setParentScene(scene_name);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Remove from old parent first
|
|
drawable->removeFromParent();
|
|
|
|
// Get the new parent's children collection and append
|
|
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>>* children_ptr = nullptr;
|
|
std::shared_ptr<UIDrawable> new_parent = nullptr;
|
|
|
|
if (is_frame) {
|
|
auto frame = ((PyUIFrameObject*)value)->data;
|
|
children_ptr = &frame->children;
|
|
new_parent = frame;
|
|
} else if (is_grid) {
|
|
auto grid = ((PyUIGridObject*)value)->data;
|
|
children_ptr = &grid->children;
|
|
new_parent = grid;
|
|
}
|
|
|
|
if (children_ptr && *children_ptr) {
|
|
// Check if already in this parent's collection (prevent duplicates)
|
|
bool already_present = false;
|
|
for (const auto& child : **children_ptr) {
|
|
if (child.get() == drawable.get()) {
|
|
already_present = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!already_present) {
|
|
// Add to new parent's children
|
|
(*children_ptr)->push_back(drawable);
|
|
drawable->setParent(new_parent);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Python API - get global position (read-only)
|
|
PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) 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;
|
|
}
|
|
|
|
// #138, #188 - Python API for bounds property - returns (pos, size) as pair of Vectors
|
|
PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
sf::FloatRect bounds = drawable->get_bounds();
|
|
|
|
// Get Vector type from mcrfpy module
|
|
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
if (!vector_type) return NULL;
|
|
|
|
// Create pos vector
|
|
PyObject* pos_args = Py_BuildValue("(ff)", bounds.left, bounds.top);
|
|
PyObject* pos = PyObject_CallObject(vector_type, pos_args);
|
|
Py_DECREF(pos_args);
|
|
if (!pos) {
|
|
Py_DECREF(vector_type);
|
|
return NULL;
|
|
}
|
|
|
|
// Create size vector
|
|
PyObject* size_args = Py_BuildValue("(ff)", bounds.width, bounds.height);
|
|
PyObject* size = PyObject_CallObject(vector_type, size_args);
|
|
Py_DECREF(size_args);
|
|
Py_DECREF(vector_type);
|
|
if (!size) {
|
|
Py_DECREF(pos);
|
|
return NULL;
|
|
}
|
|
|
|
// Return tuple of two vectors (N steals reference)
|
|
return Py_BuildValue("(NN)", pos, size);
|
|
}
|
|
|
|
// #138, #188 - Python API for global_bounds property - returns (pos, size) as pair of Vectors
|
|
PyObject* UIDrawable::get_global_bounds_py(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
sf::FloatRect bounds = drawable->get_global_bounds();
|
|
|
|
// Get Vector type from mcrfpy module
|
|
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
if (!vector_type) return NULL;
|
|
|
|
// Create pos vector
|
|
PyObject* pos_args = Py_BuildValue("(ff)", bounds.left, bounds.top);
|
|
PyObject* pos = PyObject_CallObject(vector_type, pos_args);
|
|
Py_DECREF(pos_args);
|
|
if (!pos) {
|
|
Py_DECREF(vector_type);
|
|
return NULL;
|
|
}
|
|
|
|
// Create size vector
|
|
PyObject* size_args = Py_BuildValue("(ff)", bounds.width, bounds.height);
|
|
PyObject* size = PyObject_CallObject(vector_type, size_args);
|
|
Py_DECREF(size_args);
|
|
Py_DECREF(vector_type);
|
|
if (!size) {
|
|
Py_DECREF(pos);
|
|
return NULL;
|
|
}
|
|
|
|
// Return tuple of two vectors (N steals reference)
|
|
return Py_BuildValue("(NN)", pos, size);
|
|
}
|
|
|
|
// #140 - Python API for on_enter property
|
|
PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
PyObject* ptr = nullptr;
|
|
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
if (((PyUIFrameObject*)self)->data->on_enter_callable)
|
|
ptr = ((PyUIFrameObject*)self)->data->on_enter_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
if (((PyUICaptionObject*)self)->data->on_enter_callable)
|
|
ptr = ((PyUICaptionObject*)self)->data->on_enter_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
if (((PyUISpriteObject*)self)->data->on_enter_callable)
|
|
ptr = ((PyUISpriteObject*)self)->data->on_enter_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
if (((PyUIGridObject*)self)->data->on_enter_callable)
|
|
ptr = ((PyUIGridObject*)self)->data->on_enter_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
if (((PyUILineObject*)self)->data->on_enter_callable)
|
|
ptr = ((PyUILineObject*)self)->data->on_enter_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
if (((PyUICircleObject*)self)->data->on_enter_callable)
|
|
ptr = ((PyUICircleObject*)self)->data->on_enter_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
if (((PyUIArcObject*)self)->data->on_enter_callable)
|
|
ptr = ((PyUIArcObject*)self)->data->on_enter_callable->borrow();
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
|
|
return NULL;
|
|
}
|
|
if (ptr && ptr != Py_None) {
|
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
|
return ptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* target = nullptr;
|
|
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
target = ((PyUIFrameObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
target = ((PyUICaptionObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
target = ((PyUISpriteObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
target = ((PyUIGridObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
target = ((PyUILineObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
target = ((PyUICircleObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
target = ((PyUIArcObject*)self)->data.get();
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
|
|
return -1;
|
|
}
|
|
|
|
if (value == Py_None) {
|
|
target->on_enter_unregister();
|
|
} else {
|
|
target->on_enter_register(value);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// #140 - Python API for on_exit property
|
|
PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
PyObject* ptr = nullptr;
|
|
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
if (((PyUIFrameObject*)self)->data->on_exit_callable)
|
|
ptr = ((PyUIFrameObject*)self)->data->on_exit_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
if (((PyUICaptionObject*)self)->data->on_exit_callable)
|
|
ptr = ((PyUICaptionObject*)self)->data->on_exit_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
if (((PyUISpriteObject*)self)->data->on_exit_callable)
|
|
ptr = ((PyUISpriteObject*)self)->data->on_exit_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
if (((PyUIGridObject*)self)->data->on_exit_callable)
|
|
ptr = ((PyUIGridObject*)self)->data->on_exit_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
if (((PyUILineObject*)self)->data->on_exit_callable)
|
|
ptr = ((PyUILineObject*)self)->data->on_exit_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
if (((PyUICircleObject*)self)->data->on_exit_callable)
|
|
ptr = ((PyUICircleObject*)self)->data->on_exit_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
if (((PyUIArcObject*)self)->data->on_exit_callable)
|
|
ptr = ((PyUIArcObject*)self)->data->on_exit_callable->borrow();
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
|
|
return NULL;
|
|
}
|
|
if (ptr && ptr != Py_None) {
|
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
|
return ptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* target = nullptr;
|
|
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
target = ((PyUIFrameObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
target = ((PyUICaptionObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
target = ((PyUISpriteObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
target = ((PyUIGridObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
target = ((PyUILineObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
target = ((PyUICircleObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
target = ((PyUIArcObject*)self)->data.get();
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
|
|
return -1;
|
|
}
|
|
|
|
if (value == Py_None) {
|
|
target->on_exit_unregister();
|
|
} else {
|
|
target->on_exit_register(value);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// #140 - Python API for hovered property (read-only)
|
|
PyObject* UIDrawable::get_hovered(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyBool_FromLong(drawable->hovered);
|
|
}
|
|
|
|
// #141 - Python API for on_move property
|
|
PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
PyObject* ptr = nullptr;
|
|
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
if (((PyUIFrameObject*)self)->data->on_move_callable)
|
|
ptr = ((PyUIFrameObject*)self)->data->on_move_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
if (((PyUICaptionObject*)self)->data->on_move_callable)
|
|
ptr = ((PyUICaptionObject*)self)->data->on_move_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
if (((PyUISpriteObject*)self)->data->on_move_callable)
|
|
ptr = ((PyUISpriteObject*)self)->data->on_move_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
if (((PyUIGridObject*)self)->data->on_move_callable)
|
|
ptr = ((PyUIGridObject*)self)->data->on_move_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
if (((PyUILineObject*)self)->data->on_move_callable)
|
|
ptr = ((PyUILineObject*)self)->data->on_move_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
if (((PyUICircleObject*)self)->data->on_move_callable)
|
|
ptr = ((PyUICircleObject*)self)->data->on_move_callable->borrow();
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
if (((PyUIArcObject*)self)->data->on_move_callable)
|
|
ptr = ((PyUIArcObject*)self)->data->on_move_callable->borrow();
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
|
|
return NULL;
|
|
}
|
|
if (ptr && ptr != Py_None) {
|
|
Py_INCREF(ptr); // Return new reference, not borrowed
|
|
return ptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* target = nullptr;
|
|
|
|
switch (objtype) {
|
|
case PyObjectsEnum::UIFRAME:
|
|
target = ((PyUIFrameObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UICAPTION:
|
|
target = ((PyUICaptionObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UISPRITE:
|
|
target = ((PyUISpriteObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UIGRID:
|
|
target = ((PyUIGridObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UILINE:
|
|
target = ((PyUILineObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UICIRCLE:
|
|
target = ((PyUICircleObject*)self)->data.get();
|
|
break;
|
|
case PyObjectsEnum::UIARC:
|
|
target = ((PyUIArcObject*)self)->data.get();
|
|
break;
|
|
default:
|
|
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
|
|
return -1;
|
|
}
|
|
|
|
if (value == Py_None) {
|
|
target->on_move_unregister();
|
|
} else {
|
|
target->on_move_register(value);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Animation shorthand helper - creates and starts an animation on a UIDrawable
|
|
// This is a free function (not a member) to avoid incomplete type issues in UIBase.h template
|
|
PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* args, PyObject* kwds) {
|
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
|
|
|
|
const char* property_name;
|
|
PyObject* target_value;
|
|
float duration;
|
|
PyObject* easing_arg = Py_None;
|
|
int delta = 0;
|
|
PyObject* callback = nullptr;
|
|
const char* conflict_mode_str = nullptr;
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
|
|
&property_name, &target_value, &duration,
|
|
&easing_arg, &delta, &callback, &conflict_mode_str)) {
|
|
return NULL;
|
|
}
|
|
|
|
// Validate property exists on this drawable
|
|
if (!self->hasProperty(property_name)) {
|
|
PyErr_Format(PyExc_ValueError,
|
|
"Property '%s' is not valid for animation on this object. "
|
|
"Check spelling or use a supported property name.",
|
|
property_name);
|
|
return NULL;
|
|
}
|
|
|
|
// Validate callback is callable if provided
|
|
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
|
|
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
|
return NULL;
|
|
}
|
|
|
|
// Convert None to nullptr for C++
|
|
if (callback == Py_None) {
|
|
callback = nullptr;
|
|
}
|
|
|
|
// Convert Python target value to AnimationValue
|
|
AnimationValue animValue;
|
|
|
|
if (PyFloat_Check(target_value)) {
|
|
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
|
|
}
|
|
else if (PyLong_Check(target_value)) {
|
|
animValue = static_cast<int>(PyLong_AsLong(target_value));
|
|
}
|
|
else if (PyList_Check(target_value)) {
|
|
// List of integers for sprite animation
|
|
std::vector<int> indices;
|
|
Py_ssize_t size = PyList_Size(target_value);
|
|
for (Py_ssize_t i = 0; i < size; i++) {
|
|
PyObject* item = PyList_GetItem(target_value, i);
|
|
if (PyLong_Check(item)) {
|
|
indices.push_back(PyLong_AsLong(item));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
|
|
return NULL;
|
|
}
|
|
}
|
|
animValue = indices;
|
|
}
|
|
else if (PyTuple_Check(target_value)) {
|
|
Py_ssize_t size = PyTuple_Size(target_value);
|
|
if (size == 2) {
|
|
// Vector2f
|
|
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
|
|
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
|
|
if (PyErr_Occurred()) return NULL;
|
|
animValue = sf::Vector2f(x, y);
|
|
}
|
|
else if (size == 3 || size == 4) {
|
|
// Color (RGB or RGBA)
|
|
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
|
|
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
|
|
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
|
|
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
|
|
if (PyErr_Occurred()) return NULL;
|
|
animValue = sf::Color(r, g, b, a);
|
|
}
|
|
else {
|
|
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
|
|
return NULL;
|
|
}
|
|
}
|
|
else if (PyUnicode_Check(target_value)) {
|
|
// String for text animation
|
|
const char* str = PyUnicode_AsUTF8(target_value);
|
|
animValue = std::string(str);
|
|
}
|
|
else {
|
|
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
|
|
return NULL;
|
|
}
|
|
|
|
// Get easing function from argument
|
|
EasingFunction easingFunc;
|
|
if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) {
|
|
return NULL; // Error already set by from_arg
|
|
}
|
|
|
|
// Parse conflict mode
|
|
AnimationConflictMode conflict_mode = AnimationConflictMode::REPLACE;
|
|
if (conflict_mode_str) {
|
|
if (strcmp(conflict_mode_str, "replace") == 0) {
|
|
conflict_mode = AnimationConflictMode::REPLACE;
|
|
} else if (strcmp(conflict_mode_str, "queue") == 0) {
|
|
conflict_mode = AnimationConflictMode::QUEUE;
|
|
} else if (strcmp(conflict_mode_str, "error") == 0) {
|
|
conflict_mode = AnimationConflictMode::RAISE_ERROR;
|
|
} else {
|
|
PyErr_Format(PyExc_ValueError,
|
|
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Create the Animation
|
|
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
|
|
|
// Start on this drawable
|
|
animation->start(self);
|
|
|
|
// Add to AnimationManager
|
|
AnimationManager::getInstance().addAnimation(animation, conflict_mode);
|
|
|
|
// Check if ERROR mode raised an exception
|
|
if (PyErr_Occurred()) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create and return a PyAnimation wrapper
|
|
PyTypeObject* animType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Animation");
|
|
if (!animType) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Could not find Animation type");
|
|
return NULL;
|
|
}
|
|
|
|
PyAnimationObject* pyAnim = (PyAnimationObject*)animType->tp_alloc(animType, 0);
|
|
Py_DECREF(animType);
|
|
|
|
if (!pyAnim) {
|
|
return NULL;
|
|
}
|
|
|
|
pyAnim->data = animation;
|
|
return (PyObject*)pyAnim;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Callback Cache Support (#184) - Python subclass method resolution
|
|
// ============================================================================
|
|
|
|
// Key for storing callback generation on Python type objects
|
|
static const char* CALLBACK_GEN_ATTR = "_mcrf_callback_gen";
|
|
|
|
uint32_t UIDrawable::getCallbackGeneration(PyObject* type) {
|
|
if (!type) return 0;
|
|
|
|
PyObject* gen = PyObject_GetAttrString(type, CALLBACK_GEN_ATTR);
|
|
if (gen) {
|
|
uint32_t result = static_cast<uint32_t>(PyLong_AsUnsignedLong(gen));
|
|
Py_DECREF(gen);
|
|
return result;
|
|
}
|
|
|
|
// No generation set yet - initialize to 0
|
|
PyErr_Clear();
|
|
return 0;
|
|
}
|
|
|
|
void UIDrawable::incrementCallbackGeneration(PyObject* type) {
|
|
if (!type) return;
|
|
|
|
uint32_t current = getCallbackGeneration(type);
|
|
PyObject* new_gen = PyLong_FromUnsignedLong(current + 1);
|
|
if (new_gen) {
|
|
PyObject_SetAttrString(type, CALLBACK_GEN_ATTR, new_gen);
|
|
Py_DECREF(new_gen);
|
|
}
|
|
PyErr_Clear(); // Clear any errors from SetAttr
|
|
}
|
|
|
|
bool UIDrawable::isCallbackCacheValid(PyObject* type) const {
|
|
if (!callback_cache.valid) return false;
|
|
return callback_cache.generation == getCallbackGeneration(type);
|
|
}
|
|
|
|
void UIDrawable::refreshCallbackCache(PyObject* pyObj) {
|
|
if (!pyObj) return;
|
|
|
|
PyObject* type = (PyObject*)Py_TYPE(pyObj);
|
|
|
|
// Update generation
|
|
callback_cache.generation = getCallbackGeneration(type);
|
|
callback_cache.valid = true;
|
|
|
|
// Check for each callback method
|
|
// We check the object (not just the class) to handle instance attributes too
|
|
|
|
// on_click
|
|
PyObject* attr = PyObject_GetAttrString(pyObj, "on_click");
|
|
callback_cache.has_on_click = (attr && PyCallable_Check(attr) && attr != Py_None);
|
|
Py_XDECREF(attr);
|
|
PyErr_Clear();
|
|
|
|
// on_enter
|
|
attr = PyObject_GetAttrString(pyObj, "on_enter");
|
|
callback_cache.has_on_enter = (attr && PyCallable_Check(attr) && attr != Py_None);
|
|
Py_XDECREF(attr);
|
|
PyErr_Clear();
|
|
|
|
// on_exit
|
|
attr = PyObject_GetAttrString(pyObj, "on_exit");
|
|
callback_cache.has_on_exit = (attr && PyCallable_Check(attr) && attr != Py_None);
|
|
Py_XDECREF(attr);
|
|
PyErr_Clear();
|
|
|
|
// on_move
|
|
attr = PyObject_GetAttrString(pyObj, "on_move");
|
|
callback_cache.has_on_move = (attr && PyCallable_Check(attr) && attr != Py_None);
|
|
Py_XDECREF(attr);
|
|
PyErr_Clear();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Alignment System Implementation
|
|
// ============================================================================
|
|
|
|
void UIDrawable::applyAlignment() {
|
|
if (align_type == AlignmentType::NONE) return;
|
|
|
|
float pw, ph; // Parent width/height
|
|
auto p = parent.lock();
|
|
|
|
if (p) {
|
|
// Parent is another UIDrawable (Frame, Grid, etc.)
|
|
sf::FloatRect parent_bounds = p->get_bounds();
|
|
pw = parent_bounds.width;
|
|
ph = parent_bounds.height;
|
|
} else if (!parent_scene.empty()) {
|
|
// Parent is a Scene - use window's game resolution
|
|
GameEngine* game = McRFPy_API::game;
|
|
if (!game) return;
|
|
sf::Vector2u resolution = game->getGameResolution();
|
|
pw = static_cast<float>(resolution.x);
|
|
ph = static_cast<float>(resolution.y);
|
|
} else {
|
|
return; // No parent at all = can't align
|
|
}
|
|
|
|
sf::FloatRect self_bounds = get_bounds();
|
|
float cw = self_bounds.width, ch = self_bounds.height;
|
|
|
|
// Use specific margins if set (>= 0), otherwise inherit from general margin
|
|
// -1.0 means "inherit", any value >= 0 is an explicit override
|
|
float mx = (align_horiz_margin >= 0.0f) ? align_horiz_margin : align_margin;
|
|
float my = (align_vert_margin >= 0.0f) ? align_vert_margin : align_margin;
|
|
|
|
float x = 0, y = 0;
|
|
switch (align_type) {
|
|
case AlignmentType::TOP_LEFT:
|
|
x = mx;
|
|
y = my;
|
|
break;
|
|
case AlignmentType::TOP_CENTER:
|
|
x = (pw - cw) / 2.0f;
|
|
y = my;
|
|
break;
|
|
case AlignmentType::TOP_RIGHT:
|
|
x = pw - cw - mx;
|
|
y = my;
|
|
break;
|
|
case AlignmentType::CENTER_LEFT:
|
|
x = mx;
|
|
y = (ph - ch) / 2.0f;
|
|
break;
|
|
case AlignmentType::CENTER:
|
|
x = (pw - cw) / 2.0f;
|
|
y = (ph - ch) / 2.0f;
|
|
break;
|
|
case AlignmentType::CENTER_RIGHT:
|
|
x = pw - cw - mx;
|
|
y = (ph - ch) / 2.0f;
|
|
break;
|
|
case AlignmentType::BOTTOM_LEFT:
|
|
x = mx;
|
|
y = ph - ch - my;
|
|
break;
|
|
case AlignmentType::BOTTOM_CENTER:
|
|
x = (pw - cw) / 2.0f;
|
|
y = ph - ch - my;
|
|
break;
|
|
case AlignmentType::BOTTOM_RIGHT:
|
|
x = pw - cw - mx;
|
|
y = ph - ch - my;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// For most drawables, position IS the bounding box top-left corner
|
|
// But for Circle and Arc, position is the center, so we need to adjust
|
|
float offset_x = 0.0f;
|
|
float offset_y = 0.0f;
|
|
|
|
// Check if this is a Circle or Arc (where position = center)
|
|
auto dtype = derived_type();
|
|
if (dtype == PyObjectsEnum::UICIRCLE || dtype == PyObjectsEnum::UIARC) {
|
|
// For these, position is the center, bounds.topLeft is position - radius
|
|
// So offset = position - bounds.topLeft = (radius, radius)
|
|
offset_x = position.x - self_bounds.left;
|
|
offset_y = position.y - self_bounds.top;
|
|
}
|
|
|
|
position = sf::Vector2f(x + offset_x, y + offset_y);
|
|
onPositionChanged();
|
|
markCompositeDirty();
|
|
}
|
|
|
|
void UIDrawable::setAlignment(AlignmentType align) {
|
|
align_type = align;
|
|
if (align != AlignmentType::NONE) {
|
|
applyAlignment();
|
|
}
|
|
}
|
|
|
|
void UIDrawable::realign() {
|
|
// Reapply alignment - useful for responsive layouts
|
|
if (align_type != AlignmentType::NONE) {
|
|
applyAlignment();
|
|
}
|
|
}
|
|
|
|
PyObject* UIDrawable::py_realign(PyObject* self, PyObject* args) {
|
|
PyObjectsEnum objtype = PyObjectsEnum::UIFRAME; // Default, will be set by type check
|
|
|
|
// Determine the type from the Python object
|
|
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
|
|
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
|
|
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
|
|
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
|
PyObject* line_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line");
|
|
PyObject* circle_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle");
|
|
PyObject* arc_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc");
|
|
|
|
if (PyObject_IsInstance(self, frame_type)) objtype = PyObjectsEnum::UIFRAME;
|
|
else if (PyObject_IsInstance(self, caption_type)) objtype = PyObjectsEnum::UICAPTION;
|
|
else if (PyObject_IsInstance(self, sprite_type)) objtype = PyObjectsEnum::UISPRITE;
|
|
else if (PyObject_IsInstance(self, grid_type)) objtype = PyObjectsEnum::UIGRID;
|
|
else if (PyObject_IsInstance(self, line_type)) objtype = PyObjectsEnum::UILINE;
|
|
else if (PyObject_IsInstance(self, circle_type)) objtype = PyObjectsEnum::UICIRCLE;
|
|
else if (PyObject_IsInstance(self, arc_type)) objtype = PyObjectsEnum::UIARC;
|
|
|
|
Py_XDECREF(frame_type);
|
|
Py_XDECREF(caption_type);
|
|
Py_XDECREF(sprite_type);
|
|
Py_XDECREF(grid_type);
|
|
Py_XDECREF(line_type);
|
|
Py_XDECREF(circle_type);
|
|
Py_XDECREF(arc_type);
|
|
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
drawable->realign();
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
bool UIDrawable::validateMargins(AlignmentType align, float margin, float horiz_margin, float vert_margin, bool set_error) {
|
|
// Calculate effective margins (-1 means inherit from general margin)
|
|
float eff_horiz = (horiz_margin >= 0.0f) ? horiz_margin : margin;
|
|
float eff_vert = (vert_margin >= 0.0f) ? vert_margin : margin;
|
|
|
|
// CENTER alignment doesn't support any margins
|
|
if (align == AlignmentType::CENTER) {
|
|
if (margin != 0.0f || eff_horiz != 0.0f || eff_vert != 0.0f) {
|
|
if (set_error) {
|
|
PyErr_SetString(PyExc_ValueError,
|
|
"CENTER alignment does not support margins");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Horizontally centered alignments don't support horiz_margin override
|
|
// (margin is applied vertically only)
|
|
if (align == AlignmentType::TOP_CENTER || align == AlignmentType::BOTTOM_CENTER) {
|
|
// If horiz_margin is explicitly set (not -1), it must be 0 or error
|
|
if (horiz_margin >= 0.0f && horiz_margin != 0.0f) {
|
|
if (set_error) {
|
|
PyErr_SetString(PyExc_ValueError,
|
|
"TOP_CENTER and BOTTOM_CENTER alignments do not support horiz_margin");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Vertically centered alignments don't support vert_margin override
|
|
// (margin is applied horizontally only)
|
|
if (align == AlignmentType::CENTER_LEFT || align == AlignmentType::CENTER_RIGHT) {
|
|
// If vert_margin is explicitly set (not -1), it must be 0 or error
|
|
if (vert_margin >= 0.0f && vert_margin != 0.0f) {
|
|
if (set_error) {
|
|
PyErr_SetString(PyExc_ValueError,
|
|
"CENTER_LEFT and CENTER_RIGHT alignments do not support vert_margin");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Python API: get align property
|
|
PyObject* UIDrawable::get_align(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
if (drawable->align_type == AlignmentType::NONE) {
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// Return Alignment enum member
|
|
if (!PyAlignment::alignment_enum_class) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Alignment enum not initialized");
|
|
return NULL;
|
|
}
|
|
|
|
PyObject* value = PyLong_FromLong(static_cast<int>(drawable->align_type));
|
|
if (!value) return NULL;
|
|
|
|
PyObject* result = PyObject_CallFunctionObjArgs(PyAlignment::alignment_enum_class, value, NULL);
|
|
Py_DECREF(value);
|
|
return result;
|
|
}
|
|
|
|
// Python API: set align property
|
|
int UIDrawable::set_align(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
if (value == Py_None) {
|
|
drawable->align_type = AlignmentType::NONE;
|
|
return 0;
|
|
}
|
|
|
|
AlignmentType align;
|
|
if (!PyAlignment::from_arg(value, &align)) {
|
|
return -1;
|
|
}
|
|
|
|
// Validate margins for new alignment
|
|
if (!validateMargins(align, drawable->align_margin, drawable->align_horiz_margin, drawable->align_vert_margin)) {
|
|
return -1;
|
|
}
|
|
|
|
drawable->setAlignment(align);
|
|
return 0;
|
|
}
|
|
|
|
// Python API: get margin property
|
|
PyObject* UIDrawable::get_margin(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyFloat_FromDouble(drawable->align_margin);
|
|
}
|
|
|
|
// Python API: set margin property
|
|
int UIDrawable::set_margin(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
float margin = 0.0f;
|
|
if (PyFloat_Check(value)) {
|
|
margin = static_cast<float>(PyFloat_AsDouble(value));
|
|
} else if (PyLong_Check(value)) {
|
|
margin = static_cast<float>(PyLong_AsLong(value));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "margin must be a number");
|
|
return -1;
|
|
}
|
|
|
|
// Validate margins for current alignment
|
|
if (drawable->align_type != AlignmentType::NONE) {
|
|
if (!validateMargins(drawable->align_type, margin, drawable->align_horiz_margin, drawable->align_vert_margin)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
drawable->align_margin = margin;
|
|
if (drawable->align_type != AlignmentType::NONE) {
|
|
drawable->applyAlignment();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Python API: get horiz_margin property
|
|
PyObject* UIDrawable::get_horiz_margin(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyFloat_FromDouble(drawable->align_horiz_margin);
|
|
}
|
|
|
|
// Python API: set horiz_margin property
|
|
int UIDrawable::set_horiz_margin(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
float horiz_margin = 0.0f;
|
|
if (PyFloat_Check(value)) {
|
|
horiz_margin = static_cast<float>(PyFloat_AsDouble(value));
|
|
} else if (PyLong_Check(value)) {
|
|
horiz_margin = static_cast<float>(PyLong_AsLong(value));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "horiz_margin must be a number");
|
|
return -1;
|
|
}
|
|
|
|
// Validate margins for current alignment
|
|
if (drawable->align_type != AlignmentType::NONE) {
|
|
if (!validateMargins(drawable->align_type, drawable->align_margin, horiz_margin, drawable->align_vert_margin)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
drawable->align_horiz_margin = horiz_margin;
|
|
if (drawable->align_type != AlignmentType::NONE) {
|
|
drawable->applyAlignment();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Python API: get vert_margin property
|
|
PyObject* UIDrawable::get_vert_margin(PyObject* self, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return NULL;
|
|
|
|
return PyFloat_FromDouble(drawable->align_vert_margin);
|
|
}
|
|
|
|
// Python API: set vert_margin property
|
|
int UIDrawable::set_vert_margin(PyObject* self, PyObject* value, void* closure) {
|
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
|
if (!drawable) return -1;
|
|
|
|
float vert_margin = 0.0f;
|
|
if (PyFloat_Check(value)) {
|
|
vert_margin = static_cast<float>(PyFloat_AsDouble(value));
|
|
} else if (PyLong_Check(value)) {
|
|
vert_margin = static_cast<float>(PyLong_AsLong(value));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "vert_margin must be a number");
|
|
return -1;
|
|
}
|
|
|
|
// Validate margins for current alignment
|
|
if (drawable->align_type != AlignmentType::NONE) {
|
|
if (!validateMargins(drawable->align_type, drawable->align_margin, drawable->align_horiz_margin, vert_margin)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
drawable->align_vert_margin = vert_margin;
|
|
if (drawable->align_type != AlignmentType::NONE) {
|
|
drawable->applyAlignment();
|
|
}
|
|
return 0;
|
|
}
|