#include "UICollection.h" #include "UIFrame.h" #include "UICaption.h" #include "UISprite.h" #include "UIGrid.h" #include "UILine.h" #include "UICircle.h" #include "UIArc.h" #include "McRFPy_API.h" #include "PyObjectUtils.h" #include "PythonObjectCache.h" #include #include using namespace mcrfpydef; // Local helper function to convert UIDrawable to appropriate Python object static PyObject* convertDrawableToPython(std::shared_ptr drawable) { if (!drawable) { Py_RETURN_NONE; } // Check cache first if (drawable->serial_number != 0) { PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number); if (cached) { return cached; // Already INCREF'd by lookup } } PyTypeObject* type = nullptr; PyObject* obj = nullptr; switch (drawable->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(drawable); 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(drawable); 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(drawable); 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(drawable); pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; } case PyObjectsEnum::UILINE: { type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"); if (!type) return nullptr; auto pyObj = (PyUILineObject*)type->tp_alloc(type, 0); if (pyObj) { pyObj->data = std::static_pointer_cast(drawable); pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; } case PyObjectsEnum::UICIRCLE: { type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"); if (!type) return nullptr; auto pyObj = (PyUICircleObject*)type->tp_alloc(type, 0); if (pyObj) { pyObj->data = std::static_pointer_cast(drawable); pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; } case PyObjectsEnum::UIARC: { type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"); if (!type) return nullptr; auto pyObj = (PyUIArcObject*)type->tp_alloc(type, 0); if (pyObj) { pyObj->data = std::static_pointer_cast(drawable); pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; } default: PyErr_SetString(PyExc_TypeError, "Unknown UIDrawable derived type"); return nullptr; } if (type) { Py_DECREF(type); } return obj; } int UICollectionIter::init(PyUICollectionIterObject* self, PyObject* args, PyObject* kwds) { PyErr_SetString(PyExc_TypeError, "UICollection cannot be instantiated: a C++ data source is required."); return -1; } PyObject* UICollectionIter::next(PyUICollectionIterObject* self) { // Check if self and self->data are valid if (!self || !self->data) { PyErr_SetString(PyExc_RuntimeError, "Iterator object or data is null"); return NULL; } if (self->data->size() != self->start_size) { PyErr_SetString(PyExc_RuntimeError, "collection changed size during iteration"); return NULL; } if (self->index > self->start_size - 1) { PyErr_SetNone(PyExc_StopIteration); return NULL; } self->index++; auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); return NULL; } auto target = (*vec)[self->index-1]; // Return the proper Python object for this UIDrawable return convertDrawableToPython(target); } PyObject* UICollectionIter::repr(PyUICollectionIterObject* self) { std::ostringstream ss; if (!self->data) ss << ""; else { ss << "data->size() << " child objects, @ index " << self->index << ")>"; } std::string repr_str = ss.str(); return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); } Py_ssize_t UICollection::len(PyUICollectionObject* self) { return self->data->size(); } PyObject* UICollection::getitem(PyUICollectionObject* self, Py_ssize_t index) { // build a Python version of item at self->data[index] // Copy pasted:: auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); return NULL; } while (index < 0) index += self->data->size(); if (index > self->data->size() - 1) { PyErr_SetString(PyExc_IndexError, "UICollection index out of range"); return NULL; } auto target = (*vec)[index]; return convertDrawableToPython(target); } int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value) { auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); return -1; } // Handle negative indexing while (index < 0) index += self->data->size(); // Bounds check if (index >= self->data->size()) { PyErr_SetString(PyExc_IndexError, "UICollection assignment index out of range"); return -1; } // Handle deletion if (value == NULL) { self->data->erase(self->data->begin() + index); return 0; } // Type checking - must be a UIDrawable subclass if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyErr_SetString(PyExc_TypeError, "UICollection can only contain Frame, Caption, Sprite, and Grid objects"); return -1; } // Get the C++ object from the Python object std::shared_ptr new_drawable = nullptr; int old_z_index = (*vec)[index]->z_index; // Preserve the z_index if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { PyUIFrameObject* frame = (PyUIFrameObject*)value; new_drawable = frame->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { PyUICaptionObject* caption = (PyUICaptionObject*)value; new_drawable = caption->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { PyUISpriteObject* sprite = (PyUISpriteObject*)value; new_drawable = sprite->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyUIGridObject* grid = (PyUIGridObject*)value; new_drawable = grid->data; } if (!new_drawable) { PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object"); return -1; } // Preserve the z_index of the replaced element new_drawable->z_index = old_z_index; // Replace the element (*vec)[index] = new_drawable; // Mark scene as needing resort after replacing element McRFPy_API::markSceneNeedsSort(); return 0; } int UICollection::contains(PyUICollectionObject* self, PyObject* value) { auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); return -1; } // Type checking - must be a UIDrawable subclass if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { // Not a valid type, so it can't be in the collection return 0; } // Get the C++ object from the Python object std::shared_ptr search_drawable = nullptr; if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { PyUIFrameObject* frame = (PyUIFrameObject*)value; search_drawable = frame->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { PyUICaptionObject* caption = (PyUICaptionObject*)value; search_drawable = caption->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { PyUISpriteObject* sprite = (PyUISpriteObject*)value; search_drawable = sprite->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyUIGridObject* grid = (PyUIGridObject*)value; search_drawable = grid->data; } if (!search_drawable) { return 0; } // Search for the object by comparing C++ pointers for (const auto& drawable : *vec) { if (drawable.get() == search_drawable.get()) { return 1; // Found } } return 0; // Not found } PyObject* UICollection::concat(PyUICollectionObject* self, PyObject* other) { // Create a new Python list containing elements from both collections if (!PySequence_Check(other)) { PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to UICollection"); return NULL; } Py_ssize_t self_len = self->data->size(); Py_ssize_t other_len = PySequence_Length(other); if (other_len == -1) { return NULL; // Error already set } PyObject* result_list = PyList_New(self_len + other_len); if (!result_list) { return NULL; } // Add all elements from self for (Py_ssize_t i = 0; i < self_len; i++) { PyObject* item = convertDrawableToPython((*self->data)[i]); if (!item) { Py_DECREF(result_list); return NULL; } PyList_SET_ITEM(result_list, i, item); // Steals reference } // Add all elements from other for (Py_ssize_t i = 0; i < other_len; i++) { PyObject* item = PySequence_GetItem(other, i); if (!item) { Py_DECREF(result_list); return NULL; } PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference } return result_list; } PyObject* UICollection::inplace_concat(PyUICollectionObject* self, PyObject* other) { if (!PySequence_Check(other)) { PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to UICollection"); return NULL; } // First, validate ALL items in the sequence before modifying anything Py_ssize_t other_len = PySequence_Length(other); if (other_len == -1) { return NULL; // Error already set } // Validate all items first for (Py_ssize_t i = 0; i < other_len; i++) { PyObject* item = PySequence_GetItem(other, i); if (!item) { return NULL; } // Type check if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { Py_DECREF(item); PyErr_Format(PyExc_TypeError, "UICollection can only contain Frame, Caption, Sprite, and Grid objects; " "got %s at index %zd", Py_TYPE(item)->tp_name, i); return NULL; } Py_DECREF(item); } // All items validated, now we can safely add them for (Py_ssize_t i = 0; i < other_len; i++) { PyObject* item = PySequence_GetItem(other, i); if (!item) { return NULL; // Shouldn't happen, but be safe } // Use the existing append method which handles z_index assignment PyObject* result = append(self, item); Py_DECREF(item); if (!result) { return NULL; // append() failed } Py_DECREF(result); // append returns Py_None } Py_INCREF(self); return (PyObject*)self; } PyObject* UICollection::subscript(PyUICollectionObject* self, PyObject* key) { if (PyLong_Check(key)) { // Single index - delegate to sq_item Py_ssize_t index = PyLong_AsSsize_t(key); if (index == -1 && PyErr_Occurred()) { return NULL; } return getitem(self, index); } else if (PySlice_Check(key)) { // Handle slice Py_ssize_t start, stop, step, slicelength; if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) { return NULL; } PyObject* result_list = PyList_New(slicelength); if (!result_list) { return NULL; } for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { PyObject* item = convertDrawableToPython((*self->data)[cur]); if (!item) { Py_DECREF(result_list); return NULL; } PyList_SET_ITEM(result_list, i, item); // Steals reference } return result_list; } else { PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s", Py_TYPE(key)->tp_name); return NULL; } } int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value) { if (PyLong_Check(key)) { // Single index - delegate to sq_ass_item Py_ssize_t index = PyLong_AsSsize_t(key); if (index == -1 && PyErr_Occurred()) { return -1; } return setitem(self, index, value); } else if (PySlice_Check(key)) { // Handle slice assignment/deletion Py_ssize_t start, stop, step, slicelength; if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) { return -1; } if (value == NULL) { // Deletion if (step != 1) { // For non-contiguous slices, delete from highest to lowest to maintain indices std::vector indices; for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { indices.push_back(cur); } // Sort in descending order and delete std::sort(indices.begin(), indices.end(), std::greater()); for (Py_ssize_t idx : indices) { self->data->erase(self->data->begin() + idx); } } else { // Contiguous slice - can delete in one go self->data->erase(self->data->begin() + start, self->data->begin() + stop); } // Mark scene as needing resort after slice deletion McRFPy_API::markSceneNeedsSort(); return 0; } else { // Assignment if (!PySequence_Check(value)) { PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice"); return -1; } Py_ssize_t value_len = PySequence_Length(value); if (value_len == -1) { return -1; } // Validate all items first std::vector> new_items; for (Py_ssize_t i = 0; i < value_len; i++) { PyObject* item = PySequence_GetItem(value, i); if (!item) { return -1; } // Type check and extract C++ object std::shared_ptr drawable = nullptr; if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { drawable = ((PyUIFrameObject*)item)->data; } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { drawable = ((PyUICaptionObject*)item)->data; } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { drawable = ((PyUISpriteObject*)item)->data; } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { drawable = ((PyUIGridObject*)item)->data; } else { Py_DECREF(item); PyErr_Format(PyExc_TypeError, "UICollection can only contain Frame, Caption, Sprite, and Grid objects; " "got %s at index %zd", Py_TYPE(item)->tp_name, i); return -1; } Py_DECREF(item); new_items.push_back(drawable); } // Now perform the assignment if (step == 1) { // Contiguous slice if (slicelength != value_len) { // Need to resize auto it_start = self->data->begin() + start; auto it_stop = self->data->begin() + stop; self->data->erase(it_start, it_stop); self->data->insert(self->data->begin() + start, new_items.begin(), new_items.end()); } else { // Same size, just replace for (Py_ssize_t i = 0; i < slicelength; i++) { // Preserve z_index new_items[i]->z_index = (*self->data)[start + i]->z_index; (*self->data)[start + i] = new_items[i]; } } } else { // Extended slice if (slicelength != value_len) { PyErr_Format(PyExc_ValueError, "attempt to assign sequence of size %zd to extended slice of size %zd", value_len, slicelength); return -1; } for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { // Preserve z_index new_items[i]->z_index = (*self->data)[cur]->z_index; (*self->data)[cur] = new_items[i]; } } // Mark scene as needing resort after slice assignment McRFPy_API::markSceneNeedsSort(); return 0; } } else { PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s", Py_TYPE(key)->tp_name); return -1; } } PyMappingMethods UICollection::mpmethods = { .mp_length = (lenfunc)UICollection::len, .mp_subscript = (binaryfunc)UICollection::subscript, .mp_ass_subscript = (objobjargproc)UICollection::ass_subscript }; PySequenceMethods UICollection::sqmethods = { .sq_length = (lenfunc)UICollection::len, .sq_concat = (binaryfunc)UICollection::concat, .sq_repeat = NULL, .sq_item = (ssizeargfunc)UICollection::getitem, .was_sq_slice = NULL, .sq_ass_item = (ssizeobjargproc)UICollection::setitem, .was_sq_ass_slice = NULL, .sq_contains = (objobjproc)UICollection::contains, .sq_inplace_concat = (binaryfunc)UICollection::inplace_concat, .sq_inplace_repeat = NULL }; /* Idiomatic way to fetch complete types from the API rather than referencing their PyTypeObject struct auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"); I never identified why `using namespace mcrfpydef;` doesn't solve the segfault issue. The horrible macro in UIDrawable was originally a workaround for this, but as I interact with the types outside of the monster UI.h, a more general (and less icky) solution is required. */ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o) { // if not UIDrawable subclass, reject it // self->data->push_back( c++ object inside o ); // Ensure module is initialized if (!McRFPy_API::mcrf_module) { PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized"); return NULL; } // this would be a great use case for .tp_base if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")) ) { PyErr_SetString(PyExc_TypeError, "Only Frame, Caption, Sprite, Grid, Line, Circle, and Arc objects can be added to UICollection"); return NULL; } // Calculate z_index for the new element int new_z_index = 0; if (!self->data->empty()) { // Get the z_index of the last element and add 10 int last_z = self->data->back()->z_index; if (last_z <= INT_MAX - 10) { new_z_index = last_z + 10; } else { new_z_index = INT_MAX; } } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { PyUIFrameObject* frame = (PyUIFrameObject*)o; frame->data->z_index = new_z_index; self->data->push_back(frame->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { PyUICaptionObject* caption = (PyUICaptionObject*)o; caption->data->z_index = new_z_index; self->data->push_back(caption->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { PyUISpriteObject* sprite = (PyUISpriteObject*)o; sprite->data->z_index = new_z_index; self->data->push_back(sprite->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyUIGridObject* grid = (PyUIGridObject*)o; grid->data->z_index = new_z_index; self->data->push_back(grid->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"))) { PyUILineObject* line = (PyUILineObject*)o; line->data->z_index = new_z_index; self->data->push_back(line->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"))) { PyUICircleObject* circle = (PyUICircleObject*)o; circle->data->z_index = new_z_index; self->data->push_back(circle->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) { PyUIArcObject* arc = (PyUIArcObject*)o; arc->data->z_index = new_z_index; self->data->push_back(arc->data); } // Mark scene as needing resort after adding element McRFPy_API::markSceneNeedsSort(); Py_INCREF(Py_None); return Py_None; } PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable) { // Accept any iterable of UIDrawable objects PyObject* iterator = PyObject_GetIter(iterable); if (iterator == NULL) { PyErr_SetString(PyExc_TypeError, "UICollection.extend requires an iterable"); return NULL; } // Ensure module is initialized if (!McRFPy_API::mcrf_module) { Py_DECREF(iterator); PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized"); return NULL; } // Get current highest z_index int current_z_index = 0; if (!self->data->empty()) { current_z_index = self->data->back()->z_index; } PyObject* item; while ((item = PyIter_Next(iterator)) != NULL) { // Check if item is a UIDrawable subclass if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) { Py_DECREF(item); Py_DECREF(iterator); PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, Grid, Line, Circle, or Arc objects"); return NULL; } // Increment z_index for each new element if (current_z_index <= INT_MAX - 10) { current_z_index += 10; } else { current_z_index = INT_MAX; } // Add the item based on its type if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { PyUIFrameObject* frame = (PyUIFrameObject*)item; frame->data->z_index = current_z_index; self->data->push_back(frame->data); } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { PyUICaptionObject* caption = (PyUICaptionObject*)item; caption->data->z_index = current_z_index; self->data->push_back(caption->data); } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { PyUISpriteObject* sprite = (PyUISpriteObject*)item; sprite->data->z_index = current_z_index; self->data->push_back(sprite->data); } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyUIGridObject* grid = (PyUIGridObject*)item; grid->data->z_index = current_z_index; self->data->push_back(grid->data); } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"))) { PyUILineObject* line = (PyUILineObject*)item; line->data->z_index = current_z_index; self->data->push_back(line->data); } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"))) { PyUICircleObject* circle = (PyUICircleObject*)item; circle->data->z_index = current_z_index; self->data->push_back(circle->data); } else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) { PyUIArcObject* arc = (PyUIArcObject*)item; arc->data->z_index = current_z_index; self->data->push_back(arc->data); } Py_DECREF(item); } Py_DECREF(iterator); // Check if iteration ended due to an error if (PyErr_Occurred()) { return NULL; } // Mark scene as needing resort after adding elements McRFPy_API::markSceneNeedsSort(); Py_INCREF(Py_None); return Py_None; } PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o) { auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); return NULL; } // Type checking - must be a UIDrawable subclass if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) { PyErr_SetString(PyExc_TypeError, "UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)"); return NULL; } // Get the C++ object from the Python object std::shared_ptr search_drawable = nullptr; if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { search_drawable = ((PyUIFrameObject*)o)->data; } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { search_drawable = ((PyUICaptionObject*)o)->data; } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { search_drawable = ((PyUISpriteObject*)o)->data; } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { search_drawable = ((PyUIGridObject*)o)->data; } if (!search_drawable) { PyErr_SetString(PyExc_TypeError, "UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)"); return NULL; } // Search for the object and remove first occurrence for (auto it = vec->begin(); it != vec->end(); ++it) { if (it->get() == search_drawable.get()) { vec->erase(it); McRFPy_API::markSceneNeedsSort(); Py_RETURN_NONE; } } PyErr_SetString(PyExc_ValueError, "element not in UICollection"); return NULL; } PyObject* UICollection::pop(PyUICollectionObject* self, PyObject* args) { Py_ssize_t index = -1; // Default to last element if (!PyArg_ParseTuple(args, "|n", &index)) { return NULL; } auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); return NULL; } if (vec->empty()) { PyErr_SetString(PyExc_IndexError, "pop from empty UICollection"); return NULL; } // Handle negative indexing Py_ssize_t size = static_cast(vec->size()); if (index < 0) { index += size; } if (index < 0 || index >= size) { PyErr_SetString(PyExc_IndexError, "pop index out of range"); return NULL; } // Get the element before removing std::shared_ptr drawable = (*vec)[index]; // Remove from vector vec->erase(vec->begin() + index); McRFPy_API::markSceneNeedsSort(); // Convert to Python object and return return convertDrawableToPython(drawable); } PyObject* UICollection::insert(PyUICollectionObject* self, PyObject* args) { Py_ssize_t index; PyObject* o; if (!PyArg_ParseTuple(args, "nO", &index, &o)) { return NULL; } auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); return NULL; } // Type checking - must be a UIDrawable subclass if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) { PyErr_SetString(PyExc_TypeError, "UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)"); return NULL; } // Get the C++ object from the Python object std::shared_ptr drawable = nullptr; if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { drawable = ((PyUIFrameObject*)o)->data; } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { drawable = ((PyUICaptionObject*)o)->data; } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { drawable = ((PyUISpriteObject*)o)->data; } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { drawable = ((PyUIGridObject*)o)->data; } if (!drawable) { PyErr_SetString(PyExc_TypeError, "UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)"); return NULL; } // Handle negative indexing and clamping (Python list.insert behavior) Py_ssize_t size = static_cast(vec->size()); if (index < 0) { index += size; if (index < 0) { index = 0; } } else if (index > size) { index = size; } // Insert at position vec->insert(vec->begin() + index, drawable); McRFPy_API::markSceneNeedsSort(); Py_RETURN_NONE; } PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) { auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); return NULL; } // Type checking - must be a UIDrawable subclass if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyErr_SetString(PyExc_TypeError, "UICollection.index requires a Frame, Caption, Sprite, or Grid object"); return NULL; } // Get the C++ object from the Python object std::shared_ptr search_drawable = nullptr; if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { search_drawable = ((PyUIFrameObject*)value)->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { search_drawable = ((PyUICaptionObject*)value)->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { search_drawable = ((PyUISpriteObject*)value)->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { search_drawable = ((PyUIGridObject*)value)->data; } if (!search_drawable) { PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object"); return NULL; } // Search for the object for (size_t i = 0; i < vec->size(); i++) { if ((*vec)[i].get() == search_drawable.get()) { return PyLong_FromSsize_t(i); } } PyErr_SetString(PyExc_ValueError, "value not in UICollection"); return NULL; } PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) { auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); return NULL; } // Type checking - must be a UIDrawable subclass if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && !PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { // Not a valid type, so count is 0 return PyLong_FromLong(0); } // Get the C++ object from the Python object std::shared_ptr search_drawable = nullptr; if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { search_drawable = ((PyUIFrameObject*)value)->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { search_drawable = ((PyUICaptionObject*)value)->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { search_drawable = ((PyUISpriteObject*)value)->data; } else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { search_drawable = ((PyUIGridObject*)value)->data; } if (!search_drawable) { return PyLong_FromLong(0); } // Count occurrences Py_ssize_t count = 0; for (const auto& drawable : *vec) { if (drawable.get() == search_drawable.get()) { count++; } } return PyLong_FromSsize_t(count); } // Helper function to match names with optional wildcard support static bool matchName(const std::string& name, const std::string& pattern) { // Check for wildcard pattern if (pattern.find('*') != std::string::npos) { // Simple wildcard matching: only support * at start, end, or both if (pattern == "*") { return true; // Match everything } else if (pattern.front() == '*' && pattern.back() == '*' && pattern.length() > 2) { // *substring* - contains match std::string substring = pattern.substr(1, pattern.length() - 2); return name.find(substring) != std::string::npos; } else if (pattern.front() == '*') { // *suffix - ends with std::string suffix = pattern.substr(1); return name.length() >= suffix.length() && name.compare(name.length() - suffix.length(), suffix.length(), suffix) == 0; } else if (pattern.back() == '*') { // prefix* - starts with std::string prefix = pattern.substr(0, pattern.length() - 1); return name.compare(0, prefix.length(), prefix) == 0; } // For more complex patterns, fall back to exact match return name == pattern; } // Exact match return name == pattern; } PyObject* UICollection::find(PyUICollectionObject* self, PyObject* args, PyObject* kwds) { const char* name = nullptr; int recursive = 0; static const char* kwlist[] = {"name", "recursive", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|p", const_cast(kwlist), &name, &recursive)) { return NULL; } auto vec = self->data.get(); if (!vec) { PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); return NULL; } std::string pattern(name); bool has_wildcard = (pattern.find('*') != std::string::npos); if (has_wildcard) { // Return list of all matches PyObject* results = PyList_New(0); if (!results) return NULL; for (auto& drawable : *vec) { if (matchName(drawable->name, pattern)) { PyObject* py_drawable = convertDrawableToPython(drawable); if (!py_drawable) { Py_DECREF(results); return NULL; } if (PyList_Append(results, py_drawable) < 0) { Py_DECREF(py_drawable); Py_DECREF(results); return NULL; } Py_DECREF(py_drawable); // PyList_Append increfs } // Recursive search into Frame children if (recursive && drawable->derived_type() == PyObjectsEnum::UIFRAME) { auto frame = std::static_pointer_cast(drawable); // Create temporary collection object for recursive call PyTypeObject* collType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection"); if (collType) { PyUICollectionObject* child_coll = (PyUICollectionObject*)collType->tp_alloc(collType, 0); if (child_coll) { child_coll->data = frame->children; PyObject* child_results = find(child_coll, args, kwds); if (child_results && PyList_Check(child_results)) { // Extend results with child results for (Py_ssize_t i = 0; i < PyList_Size(child_results); i++) { PyObject* item = PyList_GetItem(child_results, i); Py_INCREF(item); PyList_Append(results, item); Py_DECREF(item); } Py_DECREF(child_results); } Py_DECREF(child_coll); } Py_DECREF(collType); } } } return results; } else { // Return first exact match or None for (auto& drawable : *vec) { if (drawable->name == pattern) { return convertDrawableToPython(drawable); } // Recursive search into Frame children if (recursive && drawable->derived_type() == PyObjectsEnum::UIFRAME) { auto frame = std::static_pointer_cast(drawable); PyTypeObject* collType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection"); if (collType) { PyUICollectionObject* child_coll = (PyUICollectionObject*)collType->tp_alloc(collType, 0); if (child_coll) { child_coll->data = frame->children; PyObject* result = find(child_coll, args, kwds); Py_DECREF(child_coll); Py_DECREF(collType); if (result && result != Py_None) { return result; } Py_XDECREF(result); } else { Py_DECREF(collType); } } } } Py_RETURN_NONE; } } PyMethodDef UICollection::methods[] = { {"append", (PyCFunction)UICollection::append, METH_O, "append(element)\n\n" "Add an element to the end of the collection."}, {"extend", (PyCFunction)UICollection::extend, METH_O, "extend(iterable)\n\n" "Add all elements from an iterable to the collection."}, {"insert", (PyCFunction)UICollection::insert, METH_VARARGS, "insert(index, element)\n\n" "Insert element at index. Like list.insert(), indices past the end append.\n\n" "Note: If using z_index for sorting, insertion order may not persist after\n" "the next render. Use name-based .find() for stable element access."}, {"remove", (PyCFunction)UICollection::remove, METH_O, "remove(element)\n\n" "Remove first occurrence of element. Raises ValueError if not found."}, {"pop", (PyCFunction)UICollection::pop, METH_VARARGS, "pop([index]) -> element\n\n" "Remove and return element at index (default: last element).\n\n" "Note: If using z_index for sorting, indices may shift after render.\n" "Use name-based .find() for stable element access."}, {"index", (PyCFunction)UICollection::index_method, METH_O, "index(element) -> int\n\n" "Return index of first occurrence of element. Raises ValueError if not found."}, {"count", (PyCFunction)UICollection::count, METH_O, "count(element) -> int\n\n" "Count occurrences of element in the collection."}, {"find", (PyCFunction)UICollection::find, METH_VARARGS | METH_KEYWORDS, "find(name, recursive=False) -> element or list\n\n" "Find elements by name.\n\n" "Args:\n" " name (str): Name to search for. Supports wildcards:\n" " - 'exact' for exact match (returns single element or None)\n" " - 'prefix*' for starts-with match (returns list)\n" " - '*suffix' for ends-with match (returns list)\n" " - '*substring*' for contains match (returns list)\n" " recursive (bool): If True, search in Frame children recursively.\n\n" "Returns:\n" " Single element if exact match, list if wildcard, None if not found."}, {NULL, NULL, 0, NULL} }; PyObject* UICollection::repr(PyUICollectionObject* self) { std::ostringstream ss; if (!self->data) ss << ""; else { ss << "data->size() << " objects: "; // Count each type int frame_count = 0, caption_count = 0, sprite_count = 0, grid_count = 0, other_count = 0; for (auto& item : *self->data) { switch(item->derived_type()) { case PyObjectsEnum::UIFRAME: frame_count++; break; case PyObjectsEnum::UICAPTION: caption_count++; break; case PyObjectsEnum::UISPRITE: sprite_count++; break; case PyObjectsEnum::UIGRID: grid_count++; break; default: other_count++; break; } } // Build type summary bool first = true; if (frame_count > 0) { ss << frame_count << " Frame" << (frame_count > 1 ? "s" : ""); first = false; } if (caption_count > 0) { if (!first) ss << ", "; ss << caption_count << " Caption" << (caption_count > 1 ? "s" : ""); first = false; } if (sprite_count > 0) { if (!first) ss << ", "; ss << sprite_count << " Sprite" << (sprite_count > 1 ? "s" : ""); first = false; } if (grid_count > 0) { if (!first) ss << ", "; ss << grid_count << " Grid" << (grid_count > 1 ? "s" : ""); first = false; } if (other_count > 0) { if (!first) ss << ", "; ss << other_count << " UIDrawable" << (other_count > 1 ? "s" : ""); } ss << ")>"; } std::string repr_str = ss.str(); return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); } int UICollection::init(PyUICollectionObject* self, PyObject* args, PyObject* kwds) { PyErr_SetString(PyExc_TypeError, "UICollection cannot be instantiated: a C++ data source is required."); return -1; } PyObject* UICollection::iter(PyUICollectionObject* self) { // Get the iterator type from the module to ensure we have the registered version PyTypeObject* iterType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollectionIter"); if (!iterType) { PyErr_SetString(PyExc_RuntimeError, "Could not find UICollectionIter type in module"); return NULL; } // Allocate new iterator instance PyUICollectionIterObject* iterObj = (PyUICollectionIterObject*)iterType->tp_alloc(iterType, 0); if (iterObj == NULL) { Py_DECREF(iterType); return NULL; // Failed to allocate memory for the iterator object } iterObj->data = self->data; iterObj->index = 0; iterObj->start_size = self->data->size(); Py_DECREF(iterType); return (PyObject*)iterObj; }