Compare commits
No commits in common. "1d11b020b0a9e93981c1ed124ec944731e31328c" and "2c320effc6dbceac287e7484d3e7cec3e38538d1" have entirely different histories.
1d11b020b0
...
2c320effc6
12 changed files with 215 additions and 118 deletions
|
|
@ -457,17 +457,11 @@ void GameEngine::processEvent(const sf::Event& event)
|
||||||
std::string name = currentScene()->action(actionCode);
|
std::string name = currentScene()->action(actionCode);
|
||||||
currentScene()->doAction(name, actionType);
|
currentScene()->doAction(name, actionType);
|
||||||
}
|
}
|
||||||
else if (currentScene()->key_callable && !currentScene()->key_callable->isNone() &&
|
else if (currentScene()->key_callable &&
|
||||||
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
|
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
|
||||||
{
|
{
|
||||||
// Property-assigned handler (scene.on_key = callable)
|
|
||||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||||
}
|
}
|
||||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
|
||||||
{
|
|
||||||
// Try subclass on_key method if no property handler is set
|
|
||||||
McRFPy_API::triggerKeyEvent(ActionCode::key_str(event.key.code), actionType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEngine::sUserInput()
|
void GameEngine::sUserInput()
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,13 @@ void GridChunk::markDirty() {
|
||||||
// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed
|
// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed
|
||||||
// GridChunk now only provides data storage for GridPoints
|
// GridChunk now only provides data storage for GridPoints
|
||||||
|
|
||||||
|
sf::FloatRect GridChunk::getWorldBounds(int cell_width, int cell_height) const {
|
||||||
|
return sf::FloatRect(
|
||||||
|
sf::Vector2f(world_x * cell_width, world_y * cell_height),
|
||||||
|
sf::Vector2f(width * cell_width, height * cell_height)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool GridChunk::isVisible(float left_edge, float top_edge,
|
bool GridChunk::isVisible(float left_edge, float top_edge,
|
||||||
float right_edge, float bottom_edge) const {
|
float right_edge, float bottom_edge) const {
|
||||||
// Check if chunk's cell range overlaps with viewport's cell range
|
// Check if chunk's cell range overlaps with viewport's cell range
|
||||||
|
|
@ -140,6 +147,26 @@ const UIGridPoint& ChunkManager::at(int x, int y) const {
|
||||||
return chunk->at(local_x, local_y);
|
return chunk->at(local_x, local_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChunkManager::markAllDirty() {
|
||||||
|
for (auto& chunk : chunks) {
|
||||||
|
chunk->markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GridChunk*> ChunkManager::getVisibleChunks(float left_edge, float top_edge,
|
||||||
|
float right_edge, float bottom_edge) {
|
||||||
|
std::vector<GridChunk*> visible;
|
||||||
|
visible.reserve(chunks.size()); // Pre-allocate for worst case
|
||||||
|
|
||||||
|
for (auto& chunk : chunks) {
|
||||||
|
if (chunk->isVisible(left_edge, top_edge, right_edge, bottom_edge)) {
|
||||||
|
visible.push_back(chunk.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
void ChunkManager::resize(int new_grid_x, int new_grid_y) {
|
void ChunkManager::resize(int new_grid_x, int new_grid_y) {
|
||||||
// For now, simple rebuild - could be optimized to preserve data
|
// For now, simple rebuild - could be optimized to preserve data
|
||||||
grid_x = new_grid_x;
|
grid_x = new_grid_x;
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ public:
|
||||||
// Mark chunk as dirty
|
// Mark chunk as dirty
|
||||||
void markDirty();
|
void markDirty();
|
||||||
|
|
||||||
|
// Get pixel bounds of this chunk in world coordinates
|
||||||
|
sf::FloatRect getWorldBounds(int cell_width, int cell_height) const;
|
||||||
|
|
||||||
// Check if chunk overlaps with viewport
|
// Check if chunk overlaps with viewport
|
||||||
bool isVisible(float left_edge, float top_edge,
|
bool isVisible(float left_edge, float top_edge,
|
||||||
float right_edge, float bottom_edge) const;
|
float right_edge, float bottom_edge) const;
|
||||||
|
|
@ -87,6 +90,13 @@ public:
|
||||||
UIGridPoint& at(int x, int y);
|
UIGridPoint& at(int x, int y);
|
||||||
const UIGridPoint& at(int x, int y) const;
|
const UIGridPoint& at(int x, int y) const;
|
||||||
|
|
||||||
|
// Mark all chunks dirty (for full rebuild)
|
||||||
|
void markAllDirty();
|
||||||
|
|
||||||
|
// Get chunks that overlap with viewport
|
||||||
|
std::vector<GridChunk*> getVisibleChunks(float left_edge, float top_edge,
|
||||||
|
float right_edge, float bottom_edge);
|
||||||
|
|
||||||
// Resize grid (rebuilds chunks)
|
// Resize grid (rebuilds chunks)
|
||||||
void resize(int new_grid_x, int new_grid_y);
|
void resize(int new_grid_x, int new_grid_y);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ int GridLayer::getChunkIndex(int cell_x, int cell_y) const {
|
||||||
return cy * chunks_x + cx;
|
return cy * chunks_x + cx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GridLayer::getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const {
|
||||||
|
chunk_x = cell_x / CHUNK_SIZE;
|
||||||
|
chunk_y = cell_y / CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
void GridLayer::getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const {
|
void GridLayer::getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const {
|
||||||
start_x = chunk_x * CHUNK_SIZE;
|
start_x = chunk_x * CHUNK_SIZE;
|
||||||
start_y = chunk_y * CHUNK_SIZE;
|
start_y = chunk_y * CHUNK_SIZE;
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ public:
|
||||||
// Get chunk index for a cell
|
// Get chunk index for a cell
|
||||||
int getChunkIndex(int cell_x, int cell_y) const;
|
int getChunkIndex(int cell_x, int cell_y) const;
|
||||||
|
|
||||||
|
// Get chunk coordinates for a cell
|
||||||
|
void getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const;
|
||||||
|
|
||||||
// Get cell bounds for a chunk
|
// Get cell bounds for a chunk
|
||||||
void getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const;
|
void getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@ public:
|
||||||
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
|
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
|
||||||
static void updatePythonScenes(float dt);
|
static void updatePythonScenes(float dt);
|
||||||
static void triggerResize(int width, int height);
|
static void triggerResize(int width, int height);
|
||||||
static void triggerKeyEvent(const std::string& key, const std::string& action);
|
|
||||||
|
|
||||||
// #151: Module-level scene property accessors
|
// #151: Module-level scene property accessors
|
||||||
static PyObject* api_get_current_scene();
|
static PyObject* api_get_current_scene();
|
||||||
|
|
|
||||||
|
|
@ -413,28 +413,23 @@ void PySceneClass::call_on_exit(PySceneObject* self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PySceneClass::call_on_key(PySceneObject* self, const std::string& key, const std::string& action)
|
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
|
||||||
{
|
{
|
||||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||||
|
|
||||||
// Look for on_key attribute on the Python object
|
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress");
|
||||||
// This handles both:
|
if (method && PyCallable_Check(method)) {
|
||||||
// 1. Subclass methods: class MyScene(Scene): def on_key(self, k, s): ...
|
PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str());
|
||||||
// 2. Instance attributes: ts.on_key = lambda k, s: ... (when subclass shadows property)
|
|
||||||
PyObject* attr = PyObject_GetAttrString((PyObject*)self, "on_key");
|
|
||||||
if (attr && PyCallable_Check(attr) && attr != Py_None) {
|
|
||||||
// Call it - works for both bound methods and regular callables
|
|
||||||
PyObject* result = PyObject_CallFunction(attr, "ss", key.c_str(), action.c_str());
|
|
||||||
if (result) {
|
if (result) {
|
||||||
Py_DECREF(result);
|
Py_DECREF(result);
|
||||||
} else {
|
} else {
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
}
|
}
|
||||||
Py_DECREF(attr);
|
Py_DECREF(method);
|
||||||
} else {
|
} else {
|
||||||
// Not callable or is None - nothing to call
|
// Clear AttributeError if method doesn't exist
|
||||||
PyErr_Clear();
|
PyErr_Clear();
|
||||||
Py_XDECREF(attr);
|
Py_XDECREF(method);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyGILState_Release(gstate);
|
PyGILState_Release(gstate);
|
||||||
|
|
@ -576,18 +571,6 @@ void McRFPy_API::triggerResize(int width, int height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to trigger key events on Python scene subclasses
|
|
||||||
void McRFPy_API::triggerKeyEvent(const std::string& key, const std::string& action)
|
|
||||||
{
|
|
||||||
GameEngine* game = McRFPy_API::game;
|
|
||||||
if (!game) return;
|
|
||||||
|
|
||||||
// Only notify the active scene if it has an on_key method (subclass)
|
|
||||||
if (python_scenes.count(game->scene) > 0) {
|
|
||||||
PySceneClass::call_on_key(python_scenes[game->scene], key, action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// #151: Get the current scene as a Python Scene object
|
// #151: Get the current scene as a Python Scene object
|
||||||
PyObject* McRFPy_API::api_get_current_scene()
|
PyObject* McRFPy_API::api_get_current_scene()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public:
|
||||||
// Lifecycle callbacks (called from C++)
|
// Lifecycle callbacks (called from C++)
|
||||||
static void call_on_enter(PySceneObject* self);
|
static void call_on_enter(PySceneObject* self);
|
||||||
static void call_on_exit(PySceneObject* self);
|
static void call_on_exit(PySceneObject* self);
|
||||||
static void call_on_key(PySceneObject* self, const std::string& key, const std::string& action);
|
static void call_on_keypress(PySceneObject* self, std::string key, std::string action);
|
||||||
static void call_update(PySceneObject* self, float dt);
|
static void call_update(PySceneObject* self, float dt);
|
||||||
static void call_on_resize(PySceneObject* self, int width, int height);
|
static void call_on_resize(PySceneObject* self, int width, int height);
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ namespace mcrfpydef {
|
||||||
"Lifecycle Callbacks (override in subclass):\n"
|
"Lifecycle Callbacks (override in subclass):\n"
|
||||||
" on_enter(): Called when scene becomes active via activate().\n"
|
" on_enter(): Called when scene becomes active via activate().\n"
|
||||||
" on_exit(): Called when scene is deactivated (another scene activates).\n"
|
" on_exit(): Called when scene is deactivated (another scene activates).\n"
|
||||||
" on_key(key: str, action: str): Called for keyboard events (subclass method).\n"
|
" on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.\n"
|
||||||
" update(dt: float): Called every frame with delta time in seconds.\n"
|
" update(dt: float): Called every frame with delta time in seconds.\n"
|
||||||
" on_resize(width: int, height: int): Called when window is resized.\n\n"
|
" on_resize(width: int, height: int): Called when window is resized.\n\n"
|
||||||
"Example:\n"
|
"Example:\n"
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,24 @@ std::vector<std::pair<int, int>> SpatialHash::getBucketsInRadius(float x, float
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<int, int>> SpatialHash::getBucketsInRect(float x, float y, float width, float height) const
|
||||||
|
{
|
||||||
|
std::vector<std::pair<int, int>> result;
|
||||||
|
|
||||||
|
int min_bx = static_cast<int>(std::floor(x / bucket_size));
|
||||||
|
int max_bx = static_cast<int>(std::floor((x + width) / bucket_size));
|
||||||
|
int min_by = static_cast<int>(std::floor(y / bucket_size));
|
||||||
|
int max_by = static_cast<int>(std::floor((y + height) / bucket_size));
|
||||||
|
|
||||||
|
for (int bx = min_bx; bx <= max_bx; ++bx) {
|
||||||
|
for (int by = min_by; by <= max_by; ++by) {
|
||||||
|
result.emplace_back(bx, by);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::shared_ptr<UIEntity>> SpatialHash::queryRadius(float x, float y, float radius) const
|
std::vector<std::shared_ptr<UIEntity>> SpatialHash::queryRadius(float x, float y, float radius) const
|
||||||
{
|
{
|
||||||
std::vector<std::shared_ptr<UIEntity>> result;
|
std::vector<std::shared_ptr<UIEntity>> result;
|
||||||
|
|
@ -119,7 +137,57 @@ std::vector<std::shared_ptr<UIEntity>> SpatialHash::queryRadius(float x, float y
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<UIEntity>> SpatialHash::queryRect(float x, float y, float width, float height) const
|
||||||
|
{
|
||||||
|
std::vector<std::shared_ptr<UIEntity>> result;
|
||||||
|
|
||||||
|
auto bucket_coords = getBucketsInRect(x, y, width, height);
|
||||||
|
|
||||||
|
for (const auto& coord : bucket_coords) {
|
||||||
|
auto it = buckets.find(coord);
|
||||||
|
if (it == buckets.end()) continue;
|
||||||
|
|
||||||
|
for (const auto& wp : it->second) {
|
||||||
|
auto entity = wp.lock();
|
||||||
|
if (!entity) continue;
|
||||||
|
|
||||||
|
// Check if entity is within the rectangle
|
||||||
|
float ex = entity->position.x;
|
||||||
|
float ey = entity->position.y;
|
||||||
|
if (ex >= x && ex < x + width && ey >= y && ey < y + height) {
|
||||||
|
result.push_back(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void SpatialHash::clear()
|
void SpatialHash::clear()
|
||||||
{
|
{
|
||||||
buckets.clear();
|
buckets.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t SpatialHash::totalEntities() const
|
||||||
|
{
|
||||||
|
size_t count = 0;
|
||||||
|
for (const auto& [coord, bucket] : buckets) {
|
||||||
|
for (const auto& wp : bucket) {
|
||||||
|
if (wp.lock()) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpatialHash::cleanBucket(std::vector<std::weak_ptr<UIEntity>>& bucket)
|
||||||
|
{
|
||||||
|
bucket.erase(
|
||||||
|
std::remove_if(bucket.begin(), bucket.end(),
|
||||||
|
[](const std::weak_ptr<UIEntity>& wp) {
|
||||||
|
return wp.expired();
|
||||||
|
}),
|
||||||
|
bucket.end()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,15 @@ public:
|
||||||
// Returns entities whose positions are within the circular radius
|
// Returns entities whose positions are within the circular radius
|
||||||
std::vector<std::shared_ptr<UIEntity>> queryRadius(float x, float y, float radius) const;
|
std::vector<std::shared_ptr<UIEntity>> queryRadius(float x, float y, float radius) const;
|
||||||
|
|
||||||
|
// Query all entities within a rectangular region
|
||||||
|
std::vector<std::shared_ptr<UIEntity>> queryRect(float x, float y, float width, float height) const;
|
||||||
|
|
||||||
// Clear all entities from the hash
|
// Clear all entities from the hash
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
// Get statistics for debugging
|
// Get statistics for debugging
|
||||||
size_t bucketCount() const { return buckets.size(); }
|
size_t bucketCount() const { return buckets.size(); }
|
||||||
|
size_t totalEntities() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int bucket_size;
|
int bucket_size;
|
||||||
|
|
@ -68,4 +72,10 @@ private:
|
||||||
|
|
||||||
// Get all bucket coordinates that overlap with a radius query
|
// Get all bucket coordinates that overlap with a radius query
|
||||||
std::vector<std::pair<int, int>> getBucketsInRadius(float x, float y, float radius) const;
|
std::vector<std::pair<int, int>> getBucketsInRadius(float x, float y, float radius) const;
|
||||||
|
|
||||||
|
// Get all bucket coordinates that overlap with a rectangle
|
||||||
|
std::vector<std::pair<int, int>> getBucketsInRect(float x, float y, float width, float height) const;
|
||||||
|
|
||||||
|
// Clean expired weak_ptrs from a bucket
|
||||||
|
void cleanBucket(std::vector<std::weak_ptr<UIEntity>>& bucket);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
82
src/UIContainerBase.h
Normal file
82
src/UIContainerBase.h
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#pragma once
|
||||||
|
#include "UIDrawable.h"
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Base class for UI containers that provides common click handling logic
|
||||||
|
class UIContainerBase {
|
||||||
|
protected:
|
||||||
|
// Transform a point from parent coordinates to this container's local coordinates
|
||||||
|
virtual sf::Vector2f toLocalCoordinates(sf::Vector2f point) const = 0;
|
||||||
|
|
||||||
|
// Transform a point from this container's local coordinates to child coordinates
|
||||||
|
virtual sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const = 0;
|
||||||
|
|
||||||
|
// Get the bounds of this container in parent coordinates
|
||||||
|
virtual sf::FloatRect getBounds() const = 0;
|
||||||
|
|
||||||
|
// Check if a local point is within this container's bounds
|
||||||
|
virtual bool containsPoint(sf::Vector2f localPoint) const = 0;
|
||||||
|
|
||||||
|
// Get click handler if this container has one
|
||||||
|
virtual UIDrawable* getClickHandler() = 0;
|
||||||
|
|
||||||
|
// Get children to check for clicks (can be empty)
|
||||||
|
virtual std::vector<UIDrawable*> getClickableChildren() = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Standard click handling algorithm for all containers
|
||||||
|
// Returns the deepest UIDrawable that has a click handler and contains the point
|
||||||
|
UIDrawable* handleClick(sf::Vector2f point) {
|
||||||
|
// Transform to local coordinates
|
||||||
|
sf::Vector2f localPoint = toLocalCoordinates(point);
|
||||||
|
|
||||||
|
// Check if point is within our bounds
|
||||||
|
if (!containsPoint(localPoint)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children in reverse z-order (top-most first)
|
||||||
|
// This ensures that elements rendered on top get first chance at clicks
|
||||||
|
auto children = getClickableChildren();
|
||||||
|
|
||||||
|
// TODO: Sort by z-index if not already sorted
|
||||||
|
// std::sort(children.begin(), children.end(),
|
||||||
|
// [](UIDrawable* a, UIDrawable* b) { return a->z_index > b->z_index; });
|
||||||
|
|
||||||
|
for (int i = children.size() - 1; i >= 0; --i) {
|
||||||
|
if (!children[i]->visible) continue;
|
||||||
|
|
||||||
|
sf::Vector2f childPoint = toChildCoordinates(localPoint, i);
|
||||||
|
if (auto target = children[i]->click_at(childPoint)) {
|
||||||
|
// Child (or its descendant) handled the click
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
// If child didn't handle it, continue checking other children
|
||||||
|
// This allows click-through for elements without handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
// No child consumed the click
|
||||||
|
// Now check if WE have a click handler
|
||||||
|
return getClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper for containers with simple box bounds
|
||||||
|
class RectangularContainer : public UIContainerBase {
|
||||||
|
protected:
|
||||||
|
sf::FloatRect bounds;
|
||||||
|
|
||||||
|
sf::Vector2f toLocalCoordinates(sf::Vector2f point) const override {
|
||||||
|
return point - sf::Vector2f(bounds.left, bounds.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool containsPoint(sf::Vector2f localPoint) const override {
|
||||||
|
return localPoint.x >= 0 && localPoint.y >= 0 &&
|
||||||
|
localPoint.x < bounds.width && localPoint.y < bounds.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::FloatRect getBounds() const override {
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
"""Test Scene subclass on_key method callback
|
|
||||||
|
|
||||||
Verifies that:
|
|
||||||
1. Subclass on_key method is called for keyboard events
|
|
||||||
2. Property assignment (scene.on_key = callable) still works
|
|
||||||
3. Property assignment on subclass overrides the method
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Test state
|
|
||||||
tests_passed = 0
|
|
||||||
tests_failed = 0
|
|
||||||
|
|
||||||
def test_subclass_method():
|
|
||||||
"""Test that subclass on_key method receives keyboard events"""
|
|
||||||
global tests_passed, tests_failed
|
|
||||||
events = []
|
|
||||||
|
|
||||||
class TestScene(mcrfpy.Scene):
|
|
||||||
def on_key(self, key, state):
|
|
||||||
events.append((key, state))
|
|
||||||
|
|
||||||
ts = TestScene('test_method')
|
|
||||||
ts.activate()
|
|
||||||
automation.keyDown('a')
|
|
||||||
automation.keyUp('a')
|
|
||||||
|
|
||||||
if len(events) >= 2:
|
|
||||||
print("PASS: test_subclass_method")
|
|
||||||
tests_passed += 1
|
|
||||||
else:
|
|
||||||
print(f"FAIL: test_subclass_method - got {events}")
|
|
||||||
tests_failed += 1
|
|
||||||
|
|
||||||
def test_property_handler():
|
|
||||||
"""Test that property assignment works"""
|
|
||||||
global tests_passed, tests_failed
|
|
||||||
events = []
|
|
||||||
|
|
||||||
scene = mcrfpy.Scene('test_property')
|
|
||||||
scene.on_key = lambda k, s: events.append((k, s))
|
|
||||||
scene.activate()
|
|
||||||
automation.keyDown('b')
|
|
||||||
automation.keyUp('b')
|
|
||||||
|
|
||||||
if len(events) >= 2:
|
|
||||||
print("PASS: test_property_handler")
|
|
||||||
tests_passed += 1
|
|
||||||
else:
|
|
||||||
print(f"FAIL: test_property_handler - got {events}")
|
|
||||||
tests_failed += 1
|
|
||||||
|
|
||||||
def test_property_overrides_method():
|
|
||||||
"""Test that property assignment on subclass overrides the method"""
|
|
||||||
global tests_passed, tests_failed
|
|
||||||
method_events = []
|
|
||||||
property_events = []
|
|
||||||
|
|
||||||
class TestScene(mcrfpy.Scene):
|
|
||||||
def on_key(self, key, state):
|
|
||||||
method_events.append((key, state))
|
|
||||||
|
|
||||||
ts = TestScene('test_override')
|
|
||||||
ts.activate()
|
|
||||||
ts.on_key = lambda k, s: property_events.append((k, s))
|
|
||||||
automation.keyDown('c')
|
|
||||||
automation.keyUp('c')
|
|
||||||
|
|
||||||
if len(property_events) >= 2 and len(method_events) == 0:
|
|
||||||
print("PASS: test_property_overrides_method")
|
|
||||||
tests_passed += 1
|
|
||||||
else:
|
|
||||||
print(f"FAIL: test_property_overrides_method - method={method_events}, property={property_events}")
|
|
||||||
tests_failed += 1
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
test_subclass_method()
|
|
||||||
test_property_handler()
|
|
||||||
test_property_overrides_method()
|
|
||||||
|
|
||||||
print(f"\nResults: {tests_passed} passed, {tests_failed} failed")
|
|
||||||
sys.exit(0 if tests_failed == 0 else 1)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue