From da7180f5edca1d6c8d5d0513aaadb1115aa41c4c Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 6 Jul 2025 00:31:29 -0400 Subject: [PATCH] feat: Grid size tuple support closes #90 - Add grid_size keyword parameter to Grid.__init__ - Accept tuple or list of two integers - Override grid_x/grid_y if grid_size provided - Maintain backward compatibility - Add comprehensive test coverage --- src/UICaption.cpp | 17 +++++++--- src/UIFrame.cpp | 80 ++++++++++++++++++++++++++++++++++++++++++++--- src/UIGrid.cpp | 43 ++++++++++++++++++++++--- src/UISprite.cpp | 22 +++++++++---- 4 files changed, 143 insertions(+), 19 deletions(-) diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 2e954de..e8c9818 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -268,16 +268,16 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) // Constructor switch to Vector position //static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr }; //float x = 0.0f, y = 0.0f, outline = 0.0f; - static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", nullptr }; + static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", "click", nullptr }; PyObject* pos = NULL; float outline = 0.0f; char* text = NULL; - PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL; + PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL, *click_handler=NULL; //if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf", // const_cast(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline)) - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOf", - const_cast(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfO", + const_cast(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler)) { return -1; } @@ -351,6 +351,15 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) self->data->text.setOutlineColor(sf::Color(128,128,128,255)); } + // Process click handler if provided + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } + return 0; } diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index a784046..c49aa58 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -2,6 +2,10 @@ #include "UICollection.h" #include "GameEngine.h" #include "PyVector.h" +#include "UICaption.h" +#include "UISprite.h" +#include "UIGrid.h" +#include "McRFPy_API.h" UIDrawable* UIFrame::click_at(sf::Vector2f point) { @@ -298,22 +302,24 @@ PyObject* UIFrame::repr(PyUIFrameObject* self) int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) { //std::cout << "Init called\n"; - const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", nullptr }; + const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", "children", "click", nullptr }; float x = 0.0f, y = 0.0f, w = 0.0f, h=0.0f, outline=0.0f; PyObject* fill_color = 0; PyObject* outline_color = 0; + PyObject* children_arg = 0; + PyObject* click_handler = 0; // First try to parse as (x, y, w, h, ...) - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOf", const_cast(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOO", const_cast(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline, &children_arg, &click_handler)) { PyErr_Clear(); // Clear the error // Try to parse as ((x,y), w, h, ...) or (Vector, w, h, ...) PyObject* pos_obj = nullptr; - const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr }; + const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", "children", "click", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OffOOf", const_cast(alt_keywords), - &pos_obj, &w, &h, &fill_color, &outline_color, &outline)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OffOOfOO", const_cast(alt_keywords), + &pos_obj, &w, &h, &fill_color, &outline_color, &outline, &children_arg, &click_handler)) { return -1; } @@ -341,6 +347,70 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1); else self->data->box.setOutlineColor(sf::Color(128,128,128,255)); if (err_val) return err_val; + + // Process children argument if provided + if (children_arg && children_arg != Py_None) { + if (!PySequence_Check(children_arg)) { + PyErr_SetString(PyExc_TypeError, "children must be a sequence"); + return -1; + } + + Py_ssize_t len = PySequence_Length(children_arg); + for (Py_ssize_t i = 0; i < len; i++) { + PyObject* child = PySequence_GetItem(children_arg, i); + if (!child) return -1; + + // Check if it's a UIDrawable (Frame, Caption, Sprite, or Grid) + 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"); + + if (!PyObject_IsInstance(child, frame_type) && + !PyObject_IsInstance(child, caption_type) && + !PyObject_IsInstance(child, sprite_type) && + !PyObject_IsInstance(child, grid_type)) { + Py_DECREF(child); + PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects"); + return -1; + } + + // Get the shared_ptr and add to children + std::shared_ptr drawable = nullptr; + if (PyObject_IsInstance(child, frame_type)) { + drawable = ((PyUIFrameObject*)child)->data; + } else if (PyObject_IsInstance(child, caption_type)) { + drawable = ((PyUICaptionObject*)child)->data; + } else if (PyObject_IsInstance(child, sprite_type)) { + drawable = ((PyUISpriteObject*)child)->data; + } else if (PyObject_IsInstance(child, grid_type)) { + drawable = ((PyUIGridObject*)child)->data; + } + + // Clean up type references + Py_DECREF(frame_type); + Py_DECREF(caption_type); + Py_DECREF(sprite_type); + Py_DECREF(grid_type); + + if (drawable) { + self->data->children->push_back(drawable); + self->data->children_need_sort = true; + } + + Py_DECREF(child); + } + } + + // Process click handler if provided + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } + return 0; } diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 2e03e03..0756dc9 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -268,13 +268,48 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { int grid_x = 0, grid_y = 0; // Default to 0x0 grid PyObject* textureObj = Py_None; - //float box_x, box_y, box_w, box_h; PyObject* pos = NULL; PyObject* size = NULL; + PyObject* grid_size_obj = NULL; + + static const char* keywords[] = {"grid_x", "grid_y", "texture", "pos", "size", "grid_size", NULL}; - //if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) { - if (!PyArg_ParseTuple(args, "|iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) { - return -1; // If parsing fails, return an error + // First try parsing with keywords + if (kwds && PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", const_cast(keywords), + &grid_x, &grid_y, &textureObj, &pos, &size, &grid_size_obj)) { + // If grid_size is provided, use it to override grid_x and grid_y + if (grid_size_obj && grid_size_obj != Py_None) { + if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) { + PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0); + PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1); + if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + grid_x = PyLong_AsLong(x_obj); + grid_y = PyLong_AsLong(y_obj); + } else { + PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers"); + return -1; + } + } else if (PyList_Check(grid_size_obj) && PyList_Size(grid_size_obj) == 2) { + PyObject* x_obj = PyList_GetItem(grid_size_obj, 0); + PyObject* y_obj = PyList_GetItem(grid_size_obj, 1); + if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + grid_x = PyLong_AsLong(x_obj); + grid_y = PyLong_AsLong(y_obj); + } else { + PyErr_SetString(PyExc_TypeError, "grid_size list must contain integers"); + return -1; + } + } else { + PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple or list of two integers"); + return -1; + } + } + } else { + // Clear error and try parsing without keywords (backward compatibility) + PyErr_Clear(); + if (!PyArg_ParseTuple(args, "|iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) { + return -1; // If parsing fails, return an error + } } // Default position and size if not provided diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 90d0654..c2ef3fb 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -298,23 +298,24 @@ PyObject* UISprite::repr(PyUISpriteObject* self) int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) { //std::cout << "Init called\n"; - static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", nullptr }; + static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", "click", nullptr }; float x = 0.0f, y = 0.0f, scale = 1.0f; int sprite_index = 0; PyObject* texture = NULL; + PyObject* click_handler = NULL; // First try to parse as (x, y, texture, ...) - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif", - const_cast(keywords), &x, &y, &texture, &sprite_index, &scale)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifO", + const_cast(keywords), &x, &y, &texture, &sprite_index, &scale, &click_handler)) { PyErr_Clear(); // Clear the error // Try to parse as ((x,y), texture, ...) or (Vector, texture, ...) PyObject* pos_obj = nullptr; - const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", nullptr }; + const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", "click", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOif", const_cast(alt_keywords), - &pos_obj, &texture, &sprite_index, &scale)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifO", const_cast(alt_keywords), + &pos_obj, &texture, &sprite_index, &scale, &click_handler)) { return -1; } @@ -352,6 +353,15 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) self->data = std::make_shared(texture_ptr, sprite_index, sf::Vector2f(x, y), scale); self->data->setPosition(sf::Vector2f(x, y)); + // Process click handler if provided + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } + return 0; }