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
This commit is contained in:
John McCardle 2025-07-06 01:06:12 -04:00
parent fe5976c425
commit c48c91e5d7
3 changed files with 223 additions and 88 deletions

164
src/PyPositionHelper.h Normal file
View File

@ -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");
}
};

View File

@ -3,6 +3,7 @@
#include "PyColor.h" #include "PyColor.h"
#include "PyVector.h" #include "PyVector.h"
#include "PyFont.h" #include "PyFont.h"
#include "PyPositionHelper.h"
#include <algorithm> #include <algorithm>
UICaption::UICaption() UICaption::UICaption()
@ -265,35 +266,59 @@ PyObject* UICaption::repr(PyUICaptionObject* self)
int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
{ {
using namespace mcrfpydef; using namespace mcrfpydef;
// Constructor switch to Vector position
//static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr }; 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; 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; char* text = NULL;
PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL, *click_handler=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<char**>(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, "|ffzOOOf",
// const_cast<char**>(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfO", if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfO",
const_cast<char**>(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler)) const_cast<char**>(alt_keywords),
&pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler))
{ {
return -1; return -1;
} }
// Handle position - default to (0, 0) if not provided // Parse position
if (pos && pos != Py_None) { if (pos && pos != Py_None) {
PyVectorObject* pos_result = PyVector::from_arg(pos); PyVectorObject* vec = PyVector::from_arg(pos);
if (!pos_result) if (!vec) {
{ PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1; return -1;
} }
self->data->text.setPosition(pos_result->data); x = vec->data.x;
} else { y = vec->data.y;
self->data->text.setPosition(0.0f, 0.0f);
} }
}
self->data->text.setPosition(x, y);
// check types for font, fill_color, outline_color // check types for font, fill_color, outline_color
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; //std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;

View File

@ -1,6 +1,7 @@
#include "UIGrid.h" #include "UIGrid.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PyPositionHelper.h"
#include <algorithm> #include <algorithm>
UIGrid::UIGrid() UIGrid::UIGrid()
@ -281,7 +282,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"grid_x", "grid_y", "texture", "pos", "size", "grid_size", NULL}; static const char* keywords[] = {"grid_x", "grid_y", "texture", "pos", "size", "grid_size", NULL};
// First try parsing with keywords // First try parsing with keywords
if (kwds && PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", const_cast<char**>(keywords), if (PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", const_cast<char**>(keywords),
&grid_x, &grid_y, &textureObj, &pos, &size, &grid_size_obj)) { &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 is provided, use it to override grid_x and grid_y
if (grid_size_obj && grid_size_obj != Py_None) { if (grid_size_obj && grid_size_obj != Py_None) {
@ -566,71 +567,16 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
static const char* keywords[] = { "x", "y", "pos", nullptr }; // Use the standardized position parser
int x = -1, y = -1; auto result = PyPositionHelper::parse_position_int(args, kwds);
PyObject* pos = nullptr;
// Try to parse with keywords first if (!result.has_position) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiO", const_cast<char**>(keywords), &x, &y, &pos)) { PyPositionHelper::set_position_int_error();
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; 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 int x = result.x;
if (pos != nullptr) { int y = result.y;
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");
return NULL;
}
// Range validation // Range validation
if (x < 0 || x >= self->data->grid_x) { if (x < 0 || x >= self->data->grid_x) {