rocket-welder-sdk 1.1.36.dev14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rocket_welder_sdk/__init__.py +95 -0
- rocket_welder_sdk/bytes_size.py +234 -0
- rocket_welder_sdk/connection_string.py +291 -0
- rocket_welder_sdk/controllers.py +831 -0
- rocket_welder_sdk/external_controls/__init__.py +30 -0
- rocket_welder_sdk/external_controls/contracts.py +100 -0
- rocket_welder_sdk/external_controls/contracts_old.py +105 -0
- rocket_welder_sdk/frame_metadata.py +138 -0
- rocket_welder_sdk/gst_metadata.py +411 -0
- rocket_welder_sdk/high_level/__init__.py +54 -0
- rocket_welder_sdk/high_level/client.py +235 -0
- rocket_welder_sdk/high_level/connection_strings.py +331 -0
- rocket_welder_sdk/high_level/data_context.py +169 -0
- rocket_welder_sdk/high_level/frame_sink_factory.py +118 -0
- rocket_welder_sdk/high_level/schema.py +195 -0
- rocket_welder_sdk/high_level/transport_protocol.py +238 -0
- rocket_welder_sdk/keypoints_protocol.py +642 -0
- rocket_welder_sdk/opencv_controller.py +278 -0
- rocket_welder_sdk/periodic_timer.py +303 -0
- rocket_welder_sdk/py.typed +2 -0
- rocket_welder_sdk/rocket_welder_client.py +497 -0
- rocket_welder_sdk/segmentation_result.py +420 -0
- rocket_welder_sdk/session_id.py +238 -0
- rocket_welder_sdk/transport/__init__.py +31 -0
- rocket_welder_sdk/transport/frame_sink.py +122 -0
- rocket_welder_sdk/transport/frame_source.py +74 -0
- rocket_welder_sdk/transport/nng_transport.py +197 -0
- rocket_welder_sdk/transport/stream_transport.py +193 -0
- rocket_welder_sdk/transport/tcp_transport.py +154 -0
- rocket_welder_sdk/transport/unix_socket_transport.py +339 -0
- rocket_welder_sdk/ui/__init__.py +48 -0
- rocket_welder_sdk/ui/controls.py +362 -0
- rocket_welder_sdk/ui/icons.py +21628 -0
- rocket_welder_sdk/ui/ui_events_projection.py +226 -0
- rocket_welder_sdk/ui/ui_service.py +358 -0
- rocket_welder_sdk/ui/value_types.py +72 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/METADATA +845 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/RECORD +40 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/WHEEL +5 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""UI Events Projection for handling incoming events from the UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from threading import Event as ThreadingEvent
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
10
|
+
|
|
11
|
+
from rocket_welder_sdk.external_controls.contracts import (
|
|
12
|
+
ButtonDown,
|
|
13
|
+
ButtonUp,
|
|
14
|
+
KeyDown,
|
|
15
|
+
KeyUp,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from esdbclient import EventStoreDBClient, RecordedEvent
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IEventQueue(Protocol):
|
|
25
|
+
"""Protocol for event queue."""
|
|
26
|
+
|
|
27
|
+
def enqueue_event(self, event: Any) -> None:
|
|
28
|
+
"""Add event to the queue."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UiEventsProjection:
|
|
33
|
+
"""
|
|
34
|
+
Projection that subscribes to UI events stream and forwards them to the UiService.
|
|
35
|
+
|
|
36
|
+
Implements RAII pattern for resource management.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
session_id: str,
|
|
42
|
+
event_queue: IEventQueue,
|
|
43
|
+
eventstore_client: EventStoreDBClient,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Initialize the UI events projection.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
session_id: The session ID to subscribe to
|
|
50
|
+
event_queue: Queue to forward events to (typically UiService)
|
|
51
|
+
eventstore_client: EventStore client for subscription
|
|
52
|
+
"""
|
|
53
|
+
self._session_id: str = session_id
|
|
54
|
+
self._event_queue: IEventQueue = event_queue
|
|
55
|
+
self._eventstore_client: EventStoreDBClient = eventstore_client
|
|
56
|
+
self._stream_name: str = f"Ui.Events-{session_id}"
|
|
57
|
+
self._subscription: Any | None = None
|
|
58
|
+
self._subscription_task: asyncio.Task[None] | None = None
|
|
59
|
+
self._cancellation_token: ThreadingEvent = ThreadingEvent()
|
|
60
|
+
self._is_running: bool = False
|
|
61
|
+
|
|
62
|
+
async def start(self) -> None:
|
|
63
|
+
"""Start the subscription to UI events."""
|
|
64
|
+
if self._is_running:
|
|
65
|
+
raise RuntimeError("Projection is already running")
|
|
66
|
+
|
|
67
|
+
self._is_running = True
|
|
68
|
+
self._cancellation_token.clear()
|
|
69
|
+
|
|
70
|
+
# Start subscription task
|
|
71
|
+
self._subscription_task = asyncio.create_task(self._run_subscription())
|
|
72
|
+
logger.info(f"Started UI events projection for session {self._session_id}")
|
|
73
|
+
|
|
74
|
+
async def stop(self) -> None:
|
|
75
|
+
"""Stop the subscription and clean up resources."""
|
|
76
|
+
if not self._is_running:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
self._is_running = False
|
|
80
|
+
self._cancellation_token.set()
|
|
81
|
+
|
|
82
|
+
# Cancel subscription task
|
|
83
|
+
if self._subscription_task:
|
|
84
|
+
self._subscription_task.cancel()
|
|
85
|
+
with suppress(asyncio.CancelledError):
|
|
86
|
+
await self._subscription_task
|
|
87
|
+
self._subscription_task = None
|
|
88
|
+
|
|
89
|
+
# Close subscription
|
|
90
|
+
if self._subscription:
|
|
91
|
+
try:
|
|
92
|
+
# Context manager will handle cleanup
|
|
93
|
+
pass
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Error closing subscription: {e}")
|
|
96
|
+
finally:
|
|
97
|
+
self._subscription = None
|
|
98
|
+
|
|
99
|
+
logger.info(f"Stopped UI events projection for session {self._session_id}")
|
|
100
|
+
|
|
101
|
+
async def _run_subscription(self) -> None:
|
|
102
|
+
"""Run the subscription loop."""
|
|
103
|
+
while self._is_running and not self._cancellation_token.is_set():
|
|
104
|
+
try:
|
|
105
|
+
# Subscribe from the beginning of the stream
|
|
106
|
+
# Using catch-up subscription to get all events
|
|
107
|
+
with self._eventstore_client.subscribe_to_stream(
|
|
108
|
+
stream_name=self._stream_name,
|
|
109
|
+
stream_position=None, # Start from beginning
|
|
110
|
+
include_caught_up=True,
|
|
111
|
+
) as subscription:
|
|
112
|
+
self._subscription = subscription
|
|
113
|
+
logger.debug(f"Subscribed to stream {self._stream_name}")
|
|
114
|
+
|
|
115
|
+
for recorded_event in subscription:
|
|
116
|
+
if self._cancellation_token.is_set():
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
# Check if this is a caught-up event
|
|
120
|
+
if hasattr(recorded_event, "is_caught_up") and recorded_event.is_caught_up:
|
|
121
|
+
logger.debug(f"Caught up with stream {self._stream_name}")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Process the event
|
|
125
|
+
await self._handle_event(recorded_event)
|
|
126
|
+
|
|
127
|
+
except asyncio.CancelledError:
|
|
128
|
+
# Task was cancelled, exit cleanly
|
|
129
|
+
break
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Error in subscription: {e}")
|
|
132
|
+
if self._is_running and not self._cancellation_token.is_set():
|
|
133
|
+
# Wait before retrying
|
|
134
|
+
await asyncio.sleep(5)
|
|
135
|
+
else:
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
async def _handle_event(self, recorded_event: RecordedEvent) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Handle a recorded event from the stream.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
recorded_event: The recorded event from EventStore
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
# Parse event type and data
|
|
147
|
+
event_type: str = recorded_event.type
|
|
148
|
+
# Handle both dict and bytes (esdbclient may return bytes)
|
|
149
|
+
event_data: dict[str, Any]
|
|
150
|
+
if isinstance(recorded_event.data, bytes):
|
|
151
|
+
import json
|
|
152
|
+
|
|
153
|
+
event_data = json.loads(recorded_event.data)
|
|
154
|
+
else:
|
|
155
|
+
event_data = recorded_event.data
|
|
156
|
+
|
|
157
|
+
# Map event type to control event class
|
|
158
|
+
event_class: type[Any] | None = None
|
|
159
|
+
|
|
160
|
+
if event_type == "ButtonDown":
|
|
161
|
+
event_class = ButtonDown
|
|
162
|
+
elif event_type == "ButtonUp":
|
|
163
|
+
event_class = ButtonUp
|
|
164
|
+
elif event_type == "KeyDown":
|
|
165
|
+
event_class = KeyDown
|
|
166
|
+
elif event_type == "KeyUp":
|
|
167
|
+
event_class = KeyUp
|
|
168
|
+
else:
|
|
169
|
+
logger.debug(f"Ignoring unknown event type: {event_type}")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Create event instance
|
|
173
|
+
if event_class:
|
|
174
|
+
# Convert from PascalCase to snake_case if needed
|
|
175
|
+
normalized_data = self._normalize_event_data(event_data)
|
|
176
|
+
event = event_class.model_validate(normalized_data)
|
|
177
|
+
|
|
178
|
+
# Enqueue event for processing
|
|
179
|
+
self._event_queue.enqueue_event(event)
|
|
180
|
+
logger.debug(f"Enqueued {event_type} for control {event.control_id}")
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Error handling event {recorded_event.id}: {e}")
|
|
184
|
+
|
|
185
|
+
def _normalize_event_data(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
186
|
+
"""
|
|
187
|
+
Normalize event data from PascalCase to snake_case.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
data: Event data dictionary
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Normalized data dictionary
|
|
194
|
+
"""
|
|
195
|
+
# Map common field names
|
|
196
|
+
field_mapping = {
|
|
197
|
+
"ControlId": "control_id",
|
|
198
|
+
"Code": "code",
|
|
199
|
+
"Direction": "direction",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
normalized: dict[str, Any] = {}
|
|
203
|
+
for key, value in data.items():
|
|
204
|
+
# Use mapping if available, otherwise convert to snake_case
|
|
205
|
+
if key in field_mapping:
|
|
206
|
+
normalized[field_mapping[key]] = value
|
|
207
|
+
else:
|
|
208
|
+
# Simple PascalCase to snake_case conversion
|
|
209
|
+
snake_key = key[0].lower() + key[1:]
|
|
210
|
+
normalized[snake_key] = value
|
|
211
|
+
|
|
212
|
+
return normalized
|
|
213
|
+
|
|
214
|
+
async def __aenter__(self) -> UiEventsProjection:
|
|
215
|
+
"""Async context manager entry."""
|
|
216
|
+
await self.start()
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
220
|
+
"""Async context manager exit - ensures cleanup."""
|
|
221
|
+
await self.stop()
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def is_running(self) -> bool:
|
|
225
|
+
"""Check if the projection is running."""
|
|
226
|
+
return self._is_running
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""UI Service for managing controls and commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import UserList
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from py_micro_plumberd import CommandBus, EventStoreClient
|
|
9
|
+
|
|
10
|
+
from rocket_welder_sdk.external_controls.contracts import (
|
|
11
|
+
ChangeControls,
|
|
12
|
+
DefineControl,
|
|
13
|
+
DeleteControls,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .controls import (
|
|
17
|
+
ArrowGridControl,
|
|
18
|
+
ControlBase,
|
|
19
|
+
IconButtonControl,
|
|
20
|
+
LabelControl,
|
|
21
|
+
)
|
|
22
|
+
from .ui_events_projection import UiEventsProjection
|
|
23
|
+
from .value_types import RegionName
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ItemsControl(UserList[ControlBase]):
|
|
27
|
+
"""Collection of controls for a region with automatic command scheduling."""
|
|
28
|
+
|
|
29
|
+
data: list[ControlBase] # Type annotation for the data attribute
|
|
30
|
+
|
|
31
|
+
def __init__(self, ui_service: UiService, region_name: RegionName) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Initialize items control for a region.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
ui_service: Parent UiService
|
|
37
|
+
region_name: Region where controls are placed
|
|
38
|
+
"""
|
|
39
|
+
super().__init__()
|
|
40
|
+
self._ui_service: UiService = ui_service
|
|
41
|
+
self._region_name: RegionName = region_name
|
|
42
|
+
|
|
43
|
+
def append(self, item: ControlBase) -> None:
|
|
44
|
+
"""Add control and schedule DefineControl command."""
|
|
45
|
+
if not isinstance(item, ControlBase):
|
|
46
|
+
raise TypeError("Only ControlBase instances can be added")
|
|
47
|
+
|
|
48
|
+
# Schedule DefineControl command
|
|
49
|
+
self._ui_service.schedule_define_control(item, self._region_name)
|
|
50
|
+
super().append(item)
|
|
51
|
+
|
|
52
|
+
def add(self, item: ControlBase) -> None:
|
|
53
|
+
"""Add control (alias for append to match C# API)."""
|
|
54
|
+
self.append(item)
|
|
55
|
+
|
|
56
|
+
def remove(self, item: ControlBase) -> None:
|
|
57
|
+
"""Remove control and schedule deletion."""
|
|
58
|
+
if item in self.data:
|
|
59
|
+
self._ui_service.schedule_delete(item.id)
|
|
60
|
+
super().remove(item)
|
|
61
|
+
|
|
62
|
+
def clear(self) -> None:
|
|
63
|
+
"""Clear all controls and schedule deletions."""
|
|
64
|
+
for control in self.data:
|
|
65
|
+
self._ui_service.schedule_delete(control.id)
|
|
66
|
+
super().clear()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class UiControlFactory:
|
|
70
|
+
"""Factory for creating UI controls."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, ui_service: UiService) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Initialize factory with UiService reference.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ui_service: Parent UiService
|
|
78
|
+
"""
|
|
79
|
+
self._ui_service: UiService = ui_service
|
|
80
|
+
|
|
81
|
+
def define_icon_button(
|
|
82
|
+
self, control_id: str, icon: str, properties: dict[str, str] | None = None
|
|
83
|
+
) -> IconButtonControl:
|
|
84
|
+
"""
|
|
85
|
+
Create an icon button control.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
control_id: Unique identifier
|
|
89
|
+
icon: SVG path for the icon
|
|
90
|
+
properties: Additional properties
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Created IconButtonControl
|
|
94
|
+
"""
|
|
95
|
+
control = IconButtonControl(control_id, self._ui_service, icon, properties)
|
|
96
|
+
self._ui_service.register_control(control)
|
|
97
|
+
return control
|
|
98
|
+
|
|
99
|
+
def define_arrow_grid(
|
|
100
|
+
self, control_id: str, properties: dict[str, str] | None = None
|
|
101
|
+
) -> ArrowGridControl:
|
|
102
|
+
"""
|
|
103
|
+
Create an arrow grid control.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
control_id: Unique identifier
|
|
107
|
+
properties: Additional properties
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Created ArrowGridControl
|
|
111
|
+
"""
|
|
112
|
+
control = ArrowGridControl(control_id, self._ui_service, properties)
|
|
113
|
+
self._ui_service.register_control(control)
|
|
114
|
+
return control
|
|
115
|
+
|
|
116
|
+
def define_label(
|
|
117
|
+
self, control_id: str, text: str, properties: dict[str, str] | None = None
|
|
118
|
+
) -> LabelControl:
|
|
119
|
+
"""
|
|
120
|
+
Create a label control.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
control_id: Unique identifier
|
|
124
|
+
text: Label text
|
|
125
|
+
properties: Additional properties
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Created LabelControl
|
|
129
|
+
"""
|
|
130
|
+
control = LabelControl(control_id, self._ui_service, text, properties)
|
|
131
|
+
self._ui_service.register_control(control)
|
|
132
|
+
return control
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class UiService:
|
|
136
|
+
"""Main service for managing UI controls and commands."""
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_session_id(cls, session_id: str | Any) -> UiService:
|
|
140
|
+
"""
|
|
141
|
+
Create UiService from session ID.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
session_id: Session ID (string or UUID)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
New UiService instance
|
|
148
|
+
"""
|
|
149
|
+
# Handle UUID or string
|
|
150
|
+
session_str = str(session_id) if not isinstance(session_id, str) else session_id
|
|
151
|
+
return cls(session_str)
|
|
152
|
+
|
|
153
|
+
def __init__(self, session_id: str) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Initialize UiService with session ID.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
session_id: UI session ID for command routing
|
|
159
|
+
"""
|
|
160
|
+
self.session_id: str = session_id
|
|
161
|
+
self.command_bus: CommandBus | None = None
|
|
162
|
+
self.factory: UiControlFactory = UiControlFactory(self)
|
|
163
|
+
|
|
164
|
+
# Control tracking
|
|
165
|
+
self._index: dict[str, ControlBase] = {}
|
|
166
|
+
|
|
167
|
+
# Scheduled operations
|
|
168
|
+
self._scheduled_definitions: list[tuple[ControlBase, RegionName]] = []
|
|
169
|
+
self._scheduled_deletions: list[str] = []
|
|
170
|
+
|
|
171
|
+
# Initialize regions - include all standard and preview regions
|
|
172
|
+
self._regions: dict[RegionName, ItemsControl] = {
|
|
173
|
+
RegionName.TOP: ItemsControl(self, RegionName.TOP),
|
|
174
|
+
RegionName.TOP_LEFT: ItemsControl(self, RegionName.TOP_LEFT),
|
|
175
|
+
RegionName.TOP_RIGHT: ItemsControl(self, RegionName.TOP_RIGHT),
|
|
176
|
+
RegionName.BOTTOM: ItemsControl(self, RegionName.BOTTOM),
|
|
177
|
+
RegionName.BOTTOM_LEFT: ItemsControl(self, RegionName.BOTTOM_LEFT),
|
|
178
|
+
RegionName.BOTTOM_RIGHT: ItemsControl(self, RegionName.BOTTOM_RIGHT),
|
|
179
|
+
# Preview regions for compatibility
|
|
180
|
+
RegionName.PREVIEW_TOP: ItemsControl(self, RegionName.PREVIEW_TOP),
|
|
181
|
+
RegionName.PREVIEW_TOP_LEFT: ItemsControl(self, RegionName.PREVIEW_TOP_LEFT),
|
|
182
|
+
RegionName.PREVIEW_TOP_RIGHT: ItemsControl(self, RegionName.PREVIEW_TOP_RIGHT),
|
|
183
|
+
RegionName.PREVIEW_BOTTOM: ItemsControl(self, RegionName.PREVIEW_BOTTOM),
|
|
184
|
+
RegionName.PREVIEW_BOTTOM_LEFT: ItemsControl(self, RegionName.PREVIEW_BOTTOM_LEFT),
|
|
185
|
+
RegionName.PREVIEW_BOTTOM_RIGHT: ItemsControl(self, RegionName.PREVIEW_BOTTOM_RIGHT),
|
|
186
|
+
RegionName.PREVIEW_BOTTOM_CENTER: ItemsControl(self, RegionName.PREVIEW_BOTTOM_CENTER),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Event queue
|
|
190
|
+
self._event_queue: list[Any] = []
|
|
191
|
+
|
|
192
|
+
# Event projection
|
|
193
|
+
self._events_projection: UiEventsProjection | None = None
|
|
194
|
+
|
|
195
|
+
def __getitem__(self, region: RegionName) -> ItemsControl:
|
|
196
|
+
"""Get controls for a region."""
|
|
197
|
+
return self._regions[region]
|
|
198
|
+
|
|
199
|
+
async def initialize(self, eventstore_client: EventStoreClient) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Initialize with EventStore client and start event projection.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
eventstore_client: EventStore client for commands and events
|
|
205
|
+
"""
|
|
206
|
+
self.command_bus = CommandBus(eventstore_client)
|
|
207
|
+
|
|
208
|
+
# Start the events projection to receive UI events
|
|
209
|
+
self._events_projection = UiEventsProjection(
|
|
210
|
+
session_id=self.session_id,
|
|
211
|
+
event_queue=self, # UiService implements the IEventQueue protocol
|
|
212
|
+
eventstore_client=eventstore_client._client, # Use the underlying esdbclient
|
|
213
|
+
)
|
|
214
|
+
await self._events_projection.start()
|
|
215
|
+
|
|
216
|
+
async def dispose(self) -> None:
|
|
217
|
+
"""Dispose the service and clean up resources."""
|
|
218
|
+
# Stop the events projection
|
|
219
|
+
if self._events_projection:
|
|
220
|
+
await self._events_projection.stop()
|
|
221
|
+
self._events_projection = None
|
|
222
|
+
|
|
223
|
+
# Clear all controls
|
|
224
|
+
for control_id in list(self._index.keys()):
|
|
225
|
+
control = self._index[control_id]
|
|
226
|
+
control.dispose()
|
|
227
|
+
|
|
228
|
+
# Clear regions
|
|
229
|
+
for region in self._regions.values():
|
|
230
|
+
region.clear()
|
|
231
|
+
|
|
232
|
+
def register_control(self, control: ControlBase) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Register a control in the index.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
control: Control to register
|
|
238
|
+
"""
|
|
239
|
+
self._index[control.id] = control
|
|
240
|
+
|
|
241
|
+
def schedule_define_control(self, control: ControlBase, region: RegionName) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Schedule a DefineControl command.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
control: Control to define
|
|
247
|
+
region: Region where control is placed
|
|
248
|
+
"""
|
|
249
|
+
self._scheduled_definitions.append((control, region))
|
|
250
|
+
|
|
251
|
+
def schedule_delete(self, control_id: str) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Schedule a control deletion.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
control_id: ID of control to delete
|
|
257
|
+
"""
|
|
258
|
+
self._scheduled_deletions.append(control_id)
|
|
259
|
+
|
|
260
|
+
def enqueue_event(self, event: Any) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Enqueue an event for processing.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
event: Event to enqueue
|
|
266
|
+
"""
|
|
267
|
+
self._event_queue.append(event)
|
|
268
|
+
|
|
269
|
+
async def do(self) -> None:
|
|
270
|
+
"""Process all scheduled operations and events."""
|
|
271
|
+
# Dispatch events
|
|
272
|
+
self._dispatch_events()
|
|
273
|
+
|
|
274
|
+
# Process scheduled definitions
|
|
275
|
+
await self._process_scheduled_definitions()
|
|
276
|
+
|
|
277
|
+
# Process scheduled deletions
|
|
278
|
+
await self._process_scheduled_deletions()
|
|
279
|
+
|
|
280
|
+
# Send property updates
|
|
281
|
+
await self._send_property_updates()
|
|
282
|
+
|
|
283
|
+
def _dispatch_events(self) -> None:
|
|
284
|
+
"""Dispatch queued events to controls."""
|
|
285
|
+
for event in self._event_queue:
|
|
286
|
+
if hasattr(event, "control_id"):
|
|
287
|
+
control_id: str = event.control_id
|
|
288
|
+
control = self._index.get(control_id)
|
|
289
|
+
if control:
|
|
290
|
+
control.handle_event(event)
|
|
291
|
+
self._event_queue.clear()
|
|
292
|
+
|
|
293
|
+
async def _process_scheduled_definitions(self) -> None:
|
|
294
|
+
"""Process scheduled DefineControl commands."""
|
|
295
|
+
for control, region in self._scheduled_definitions:
|
|
296
|
+
# Add to index when actually defining
|
|
297
|
+
self._index[control.id] = control
|
|
298
|
+
|
|
299
|
+
command = DefineControl(
|
|
300
|
+
control_id=control.id,
|
|
301
|
+
type=control.control_type,
|
|
302
|
+
properties=control.properties,
|
|
303
|
+
region_name=region.value,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if self.command_bus:
|
|
307
|
+
await self.command_bus.send_async(recipient_id=self.session_id, command=command)
|
|
308
|
+
|
|
309
|
+
control.commit_changes()
|
|
310
|
+
|
|
311
|
+
self._scheduled_definitions.clear()
|
|
312
|
+
|
|
313
|
+
async def _process_scheduled_deletions(self) -> None:
|
|
314
|
+
"""Process scheduled DeleteControls commands."""
|
|
315
|
+
if not self._scheduled_deletions:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# Batch delete command
|
|
319
|
+
command = DeleteControls(control_ids=self._scheduled_deletions.copy())
|
|
320
|
+
|
|
321
|
+
if self.command_bus:
|
|
322
|
+
await self.command_bus.send_async(recipient_id=self.session_id, command=command)
|
|
323
|
+
|
|
324
|
+
# Remove from index and regions
|
|
325
|
+
for control_id in self._scheduled_deletions:
|
|
326
|
+
control = self._index.pop(control_id, None)
|
|
327
|
+
if control:
|
|
328
|
+
for region in self._regions.values():
|
|
329
|
+
if control in region:
|
|
330
|
+
region.data.remove(control)
|
|
331
|
+
|
|
332
|
+
self._scheduled_deletions.clear()
|
|
333
|
+
|
|
334
|
+
async def _send_property_updates(self) -> None:
|
|
335
|
+
"""Send ChangeControls command for dirty controls."""
|
|
336
|
+
updates: dict[str, dict[str, str]] = {}
|
|
337
|
+
|
|
338
|
+
for region in self._regions.values():
|
|
339
|
+
for control in region:
|
|
340
|
+
if control.is_dirty:
|
|
341
|
+
updates[control.id] = control.changed
|
|
342
|
+
|
|
343
|
+
if updates and self.command_bus:
|
|
344
|
+
command = ChangeControls(updates=updates)
|
|
345
|
+
|
|
346
|
+
await self.command_bus.send_async(recipient_id=self.session_id, command=command)
|
|
347
|
+
|
|
348
|
+
# Commit changes
|
|
349
|
+
for control_id in updates:
|
|
350
|
+
self._index[control_id].commit_changes()
|
|
351
|
+
|
|
352
|
+
async def __aenter__(self) -> UiService:
|
|
353
|
+
"""Async context manager entry."""
|
|
354
|
+
return self
|
|
355
|
+
|
|
356
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
357
|
+
"""Async context manager exit - ensures cleanup."""
|
|
358
|
+
await self.dispose()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Value types for UI controls matching C# value types."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ControlType(str, Enum):
|
|
7
|
+
"""Control types matching C# ControlType enum."""
|
|
8
|
+
|
|
9
|
+
ICON_BUTTON = "IconButton"
|
|
10
|
+
ARROW_GRID = "ArrowGrid"
|
|
11
|
+
LABEL = "Label"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RegionName(str, Enum):
|
|
15
|
+
"""Region names for control placement."""
|
|
16
|
+
|
|
17
|
+
TOP = "Top"
|
|
18
|
+
TOP_LEFT = "TopLeft"
|
|
19
|
+
TOP_RIGHT = "TopRight"
|
|
20
|
+
BOTTOM = "Bottom"
|
|
21
|
+
BOTTOM_LEFT = "BottomLeft"
|
|
22
|
+
BOTTOM_RIGHT = "BottomRight"
|
|
23
|
+
|
|
24
|
+
# Legacy names for compatibility
|
|
25
|
+
PREVIEW_TOP = "preview-top"
|
|
26
|
+
PREVIEW_TOP_LEFT = "preview-top-left"
|
|
27
|
+
PREVIEW_TOP_RIGHT = "preview-top-right"
|
|
28
|
+
PREVIEW_BOTTOM = "preview-bottom"
|
|
29
|
+
PREVIEW_BOTTOM_LEFT = "preview-bottom-left"
|
|
30
|
+
PREVIEW_BOTTOM_RIGHT = "preview-bottom-right"
|
|
31
|
+
PREVIEW_BOTTOM_CENTER = "preview-bottom-center"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Color(str, Enum):
|
|
35
|
+
"""Color values for controls."""
|
|
36
|
+
|
|
37
|
+
PRIMARY = "Primary"
|
|
38
|
+
SECONDARY = "Secondary"
|
|
39
|
+
SUCCESS = "Success"
|
|
40
|
+
INFO = "Info"
|
|
41
|
+
WARNING = "Warning"
|
|
42
|
+
ERROR = "Error"
|
|
43
|
+
TEXT_PRIMARY = "TextPrimary"
|
|
44
|
+
TEXT_SECONDARY = "TextSecondary"
|
|
45
|
+
DEFAULT = "Default"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Size(str, Enum):
|
|
49
|
+
"""Size values for controls."""
|
|
50
|
+
|
|
51
|
+
EXTRA_SMALL = "ExtraSmall"
|
|
52
|
+
SMALL = "Small"
|
|
53
|
+
MEDIUM = "Medium"
|
|
54
|
+
LARGE = "Large"
|
|
55
|
+
EXTRA_LARGE = "ExtraLarge"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Typography(str, Enum):
|
|
59
|
+
"""Typography values for text controls."""
|
|
60
|
+
|
|
61
|
+
H1 = "h1"
|
|
62
|
+
H2 = "h2"
|
|
63
|
+
H3 = "h3"
|
|
64
|
+
H4 = "h4"
|
|
65
|
+
H5 = "h5"
|
|
66
|
+
H6 = "h6"
|
|
67
|
+
SUBTITLE1 = "subtitle1"
|
|
68
|
+
SUBTITLE2 = "subtitle2"
|
|
69
|
+
BODY1 = "body1"
|
|
70
|
+
BODY2 = "body2"
|
|
71
|
+
CAPTION = "caption"
|
|
72
|
+
OVERLINE = "overline"
|