From c48c91e5d772a35d263bdaae60404d1150547a79 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 6 Jul 2025 01:06:12 -0400 Subject: [PATCH] feat: Standardize position arguments across all UI classes - Create PyPositionHelper for consistent position parsing - Grid.at() now accepts (x,y), ((x,y)), x=x, y=y, pos=(x,y) - Caption now accepts x,y args in addition to pos - Grid init fully supports keyword arguments - Maintain backward compatibility for all formats - Consistent error messages across classes --- src/PyPositionHelper.h | 164 +++++++++++++++++++++++++++++++++++++++++ src/UICaption.cpp | 73 ++++++++++++------ src/UIGrid.cpp | 74 +++---------------- 3 files changed, 223 insertions(+), 88 deletions(-) create mode 100644 src/PyPositionHelper.h diff --git a/src/PyPositionHelper.h b/src/PyPositionHelper.h new file mode 100644 index 0000000..1f46820 --- /dev/null +++ b/src/PyPositionHelper.h @@ -0,0 +1,164 @@ +#pragma once +#include "Python.h" +#include "PyVector.h" +#include "McRFPy_API.h" + +// Helper class for standardized position argument parsing across UI classes +class PyPositionHelper { +public: + // Template structure for parsing results + struct ParseResult { + float x = 0.0f; + float y = 0.0f; + bool has_position = false; + }; + + struct ParseResultInt { + int x = 0; + int y = 0; + bool has_position = false; + }; + + // Parse position from multiple formats for UI class constructors + // Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector + static ParseResult parse_position(PyObject* args, PyObject* kwds, + int* arg_index = nullptr) + { + ParseResult result; + float x = 0.0f, y = 0.0f; + PyObject* pos_obj = nullptr; + int start_index = arg_index ? *arg_index : 0; + + // Check for positional tuple (x, y) first + if (!kwds && PyTuple_Size(args) > start_index + 1) { + PyObject* first = PyTuple_GetItem(args, start_index); + PyObject* second = PyTuple_GetItem(args, start_index + 1); + + // Check if both are numbers + if ((PyFloat_Check(first) || PyLong_Check(first)) && + (PyFloat_Check(second) || PyLong_Check(second))) { + x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first); + y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second); + result.x = x; + result.y = y; + result.has_position = true; + if (arg_index) *arg_index += 2; + return result; + } + } + + // Check for single positional argument that might be tuple or Vector + if (!kwds && PyTuple_Size(args) > start_index) { + PyObject* first = PyTuple_GetItem(args, start_index); + PyVectorObject* vec = PyVector::from_arg(first); + if (vec) { + result.x = vec->data.x; + result.y = vec->data.y; + result.has_position = true; + if (arg_index) *arg_index += 1; + return result; + } + } + + // Try keyword arguments + if (kwds) { + PyObject* x_obj = PyDict_GetItemString(kwds, "x"); + PyObject* y_obj = PyDict_GetItemString(kwds, "y"); + PyObject* pos_kw = PyDict_GetItemString(kwds, "pos"); + + if (x_obj && y_obj) { + if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) && + (PyFloat_Check(y_obj) || PyLong_Check(y_obj))) { + result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj); + result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj); + result.has_position = true; + return result; + } + } + + if (pos_kw) { + PyVectorObject* vec = PyVector::from_arg(pos_kw); + if (vec) { + result.x = vec->data.x; + result.y = vec->data.y; + result.has_position = true; + return result; + } + } + } + + return result; + } + + // Parse integer position for Grid.at() and similar + static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds) + { + ParseResultInt result; + + // Check for positional tuple (x, y) first + if (!kwds && PyTuple_Size(args) >= 2) { + PyObject* first = PyTuple_GetItem(args, 0); + PyObject* second = PyTuple_GetItem(args, 1); + + if (PyLong_Check(first) && PyLong_Check(second)) { + result.x = PyLong_AsLong(first); + result.y = PyLong_AsLong(second); + result.has_position = true; + return result; + } + } + + // Check for single tuple argument + if (!kwds && PyTuple_Size(args) == 1) { + PyObject* first = PyTuple_GetItem(args, 0); + if (PyTuple_Check(first) && PyTuple_Size(first) == 2) { + PyObject* x_obj = PyTuple_GetItem(first, 0); + PyObject* y_obj = PyTuple_GetItem(first, 1); + if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + result.x = PyLong_AsLong(x_obj); + result.y = PyLong_AsLong(y_obj); + result.has_position = true; + return result; + } + } + } + + // Try keyword arguments + if (kwds) { + PyObject* x_obj = PyDict_GetItemString(kwds, "x"); + PyObject* y_obj = PyDict_GetItemString(kwds, "y"); + PyObject* pos_obj = PyDict_GetItemString(kwds, "pos"); + + if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + result.x = PyLong_AsLong(x_obj); + result.y = PyLong_AsLong(y_obj); + result.has_position = true; + return result; + } + + if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { + PyObject* x_val = PyTuple_GetItem(pos_obj, 0); + PyObject* y_val = PyTuple_GetItem(pos_obj, 1); + if (PyLong_Check(x_val) && PyLong_Check(y_val)) { + result.x = PyLong_AsLong(x_val); + result.y = PyLong_AsLong(y_val); + result.has_position = true; + return result; + } + } + } + + return result; + } + + // Error message helper + static void set_position_error() { + PyErr_SetString(PyExc_TypeError, + "Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector"); + } + + static void set_position_int_error() { + PyErr_SetString(PyExc_TypeError, + "Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values"); + } +}; \ No newline at end of file diff --git a/src/UICaption.cpp b/src/UICaption.cpp index e8c9818..9a3b5c2 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -3,6 +3,7 @@ #include "PyColor.h" #include "PyVector.h" #include "PyFont.h" +#include "PyPositionHelper.h" #include UICaption::UICaption() @@ -265,35 +266,59 @@ PyObject* UICaption::repr(PyUICaptionObject* self) int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) { using namespace mcrfpydef; - // 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", "click", nullptr }; - PyObject* pos = NULL; - float outline = 0.0f; - char* text = 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, "|OzOOOfO", - const_cast(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler)) - { - return -1; - } - // Handle position - default to (0, 0) if not provided - if (pos && pos != Py_None) { - PyVectorObject* pos_result = PyVector::from_arg(pos); - if (!pos_result) + static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr }; + float x = 0.0f, y = 0.0f, outline = 0.0f; + char* text = NULL; + PyObject* font = NULL; + PyObject* fill_color = NULL; + PyObject* outline_color = NULL; + PyObject* click_handler = NULL; + PyObject* pos_obj = NULL; + + // Try parsing all arguments with keywords + if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO", + const_cast(keywords), + &x, &y, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj)) + { + // If pos was provided, it overrides x,y + if (pos_obj && pos_obj != Py_None) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); + return -1; + } + x = vec->data.x; + y = vec->data.y; + } + } + else { + PyErr_Clear(); + + // Try alternative: first arg is pos tuple/Vector + static const char* alt_keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", "click", nullptr }; + PyObject* pos = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfO", + const_cast(alt_keywords), + &pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler)) { - PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); return -1; } - self->data->text.setPosition(pos_result->data); - } else { - self->data->text.setPosition(0.0f, 0.0f); + + // Parse position + if (pos && pos != Py_None) { + PyVectorObject* vec = PyVector::from_arg(pos); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); + return -1; + } + x = vec->data.x; + y = vec->data.y; + } } + + self->data->text.setPosition(x, y); // check types for font, fill_color, outline_color //std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index d589185..ed91056 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1,6 +1,7 @@ #include "UIGrid.h" #include "GameEngine.h" #include "McRFPy_API.h" +#include "PyPositionHelper.h" #include UIGrid::UIGrid() @@ -281,8 +282,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { static const char* keywords[] = {"grid_x", "grid_y", "texture", "pos", "size", "grid_size", NULL}; // 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 (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) { @@ -566,72 +567,17 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) { PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = { "x", "y", "pos", nullptr }; - int x = -1, y = -1; - PyObject* pos = nullptr; + // Use the standardized position parser + auto result = PyPositionHelper::parse_position_int(args, kwds); - // Try to parse with keywords first - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiO", const_cast(keywords), &x, &y, &pos)) { - PyErr_Clear(); // Clear the error and try different parsing - - // Check if we have a single tuple argument (x, y) - if (PyTuple_Size(args) == 1 && kwds == nullptr) { - PyObject* arg = PyTuple_GetItem(args, 0); - if (PyTuple_Check(arg) && PyTuple_Size(arg) == 2) { - // It's a tuple, extract x and y - PyObject* x_obj = PyTuple_GetItem(arg, 0); - PyObject* y_obj = PyTuple_GetItem(arg, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - x = PyLong_AsLong(x_obj); - y = PyLong_AsLong(y_obj); - } else { - PyErr_SetString(PyExc_TypeError, "Tuple elements must be integers"); - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "UIGrid.at accepts: (x, y), x, y, x=x, y=y, or pos=(x,y)"); - return NULL; - } - } else if (PyTuple_Size(args) == 2 && kwds == nullptr) { - // Two positional arguments - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { - PyErr_SetString(PyExc_TypeError, "Arguments must be integers"); - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "UIGrid.at accepts: (x, y), x, y, x=x, y=y, or pos=(x,y)"); - return NULL; - } - } - - // Handle pos keyword argument - if (pos != nullptr) { - if (x != -1 || y != -1) { - PyErr_SetString(PyExc_TypeError, "Cannot specify both pos and x/y arguments"); - return NULL; - } - if (PyTuple_Check(pos) && PyTuple_Size(pos) == 2) { - PyObject* x_obj = PyTuple_GetItem(pos, 0); - PyObject* y_obj = PyTuple_GetItem(pos, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - x = PyLong_AsLong(x_obj); - y = PyLong_AsLong(y_obj); - } else { - PyErr_SetString(PyExc_TypeError, "pos tuple elements must be integers"); - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two integers"); - return NULL; - } - } - - // Validate we have both x and y - if (x == -1 || y == -1) { - PyErr_SetString(PyExc_TypeError, "UIGrid.at requires both x and y coordinates"); + if (!result.has_position) { + PyPositionHelper::set_position_int_error(); return NULL; } + int x = result.x; + int y = result.y; + // Range validation if (x < 0 || x >= self->data->grid_x) { PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)");