rotor-framework 0.7.7 β 0.8.1
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.
- package/README.md +1 -1
- package/package.json +5 -4
- package/src/source/RotorFramework.bs +16 -2
- package/src/source/RotorFrameworkTask.bs +102 -72
- package/src/source/base/BaseReducer.bs +62 -29
- package/src/source/base/BaseViewModel.bs +0 -2
- package/src/source/base/DispatcherCore.bs +3 -3
- package/src/source/base/DispatcherOriginal.bs +9 -5
- package/src/source/engine/builder/WidgetCreate.bs +1 -1
- package/src/source/engine/services/Tts.bs +0 -9
- package/src/source/plugins/FieldsPlugin.bs +3 -0
- package/src/source/plugins/FocusPlugin.bs +10 -17
- package/src/source/plugins/ObserverPlugin.bs +45 -55
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ You can find [π±](./docs/ai/readme.opt.yaml) symbols in all documentation page
|
|
|
96
96
|
|
|
97
97
|
## π Learn More
|
|
98
98
|
|
|
99
|
-

|
|
100
100
|
|
|
101
101
|
### Framework Core
|
|
102
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rotor-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Roku toolkit library providing a ViewBuilder, full UI lifecycle with focus handling and many core features, plus MVI-based state management.",
|
|
5
5
|
"author": "BalΓ‘zs MolnΓ‘r",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -52,8 +52,9 @@
|
|
|
52
52
|
},
|
|
53
53
|
"homepage": "https://github.com/mobalazs/rotor-framework#readme",
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@rokucommunity/bslint": "
|
|
56
|
-
"brighterscript": "
|
|
57
|
-
"
|
|
55
|
+
"@rokucommunity/bslint": "1.0.0-alpha.50",
|
|
56
|
+
"brighterscript": "1.0.0-alpha.50",
|
|
57
|
+
"roku-deploy": "^3.16.1",
|
|
58
|
+
"rooibos-roku": "6.0.0-alpha.50"
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
' βββββββ ββ β ββ βββββββ βββββββββββββββββ βββββββββ ββββ βββββββββββ
|
|
5
5
|
' ββ βββββββ β βββββββ ββ ββ ββ ββββ ββββ βββββββββββββββββββ ββββ ββ
|
|
6
6
|
' Rotor Frameworkβ’
|
|
7
|
-
' Version 0.
|
|
7
|
+
' Version 0.8.1
|
|
8
8
|
' Β© 2025-2026 BalΓ‘zs MolnΓ‘r β Apache License 2.0
|
|
9
9
|
' =========================================================================
|
|
10
10
|
|
|
@@ -86,7 +86,7 @@ namespace Rotor
|
|
|
86
86
|
class Framework
|
|
87
87
|
|
|
88
88
|
name = "Rotor Framework"
|
|
89
|
-
version = "0.
|
|
89
|
+
version = "0.8.1"
|
|
90
90
|
|
|
91
91
|
config = {
|
|
92
92
|
tasks: invalid, ' @array (optional)
|
|
@@ -333,6 +333,20 @@ namespace Rotor
|
|
|
333
333
|
return m.builder.nodePool.presetNodePool(config)
|
|
334
334
|
end function
|
|
335
335
|
|
|
336
|
+
' ---------------------------------------------------------------------
|
|
337
|
+
' registerSourceObject - Not available on render thread
|
|
338
|
+
'
|
|
339
|
+
public sub registerSourceObject(objectId as string, dispatcherId as string, sourceObject as object, eventFilter = invalid as dynamic)
|
|
340
|
+
throw "registerSourceObject() is only available on the task thread. Source objects require a task thread message port for event routing."
|
|
341
|
+
end sub
|
|
342
|
+
|
|
343
|
+
' ---------------------------------------------------------------------
|
|
344
|
+
' unregisterSourceObject - Not available on render thread
|
|
345
|
+
'
|
|
346
|
+
public sub unregisterSourceObject(objectId as string)
|
|
347
|
+
throw "unregisterSourceObject() is only available on the task thread."
|
|
348
|
+
end sub
|
|
349
|
+
|
|
336
350
|
' =====================================================================
|
|
337
351
|
' INTERNAL METHODS - INITIALIZATION AND TASK SYNCING
|
|
338
352
|
' =====================================================================
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
' βββββββ ββ β ββ βββββββ βββββββββββββββββ βββββββββ ββββ βββββββββββ
|
|
5
5
|
' ββ βββββββ β βββββββ ββ ββ ββ ββββ ββββ βββββββββββββββββββ ββββ ββ
|
|
6
6
|
' Rotor Frameworkβ’
|
|
7
|
-
' Version 0.
|
|
7
|
+
' Version 0.8.1
|
|
8
8
|
' Β© 2025-2026 BalΓ‘zs MolnΓ‘r β Apache License 2.0
|
|
9
9
|
' =========================================================================
|
|
10
10
|
|
|
@@ -70,7 +70,7 @@ namespace Rotor
|
|
|
70
70
|
class FrameworkTask
|
|
71
71
|
|
|
72
72
|
name = "Rotor Framework"
|
|
73
|
-
version = "0.
|
|
73
|
+
version = "0.8.1"
|
|
74
74
|
|
|
75
75
|
config = {
|
|
76
76
|
tasks: invalid, ' optional
|
|
@@ -86,8 +86,10 @@ namespace Rotor
|
|
|
86
86
|
taskNode as object
|
|
87
87
|
dispatcherProvider as object
|
|
88
88
|
port as object
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
sourceObjectRegistry = {} ' identity -> { dispatcherId, objectId, eventFilter }
|
|
90
|
+
sourceObjectIdIndex = {} ' objectId -> identity (reverse index for unregistration)
|
|
91
|
+
sourceObjectTypeRegistry = [] ' [{ dispatcherId, objectId, eventFilter }] - broadcast routing
|
|
92
|
+
private _eventFilterFn = invalid as dynamic
|
|
91
93
|
onTick as function
|
|
92
94
|
|
|
93
95
|
' ---------------------------------------------------------------------
|
|
@@ -132,42 +134,59 @@ namespace Rotor
|
|
|
132
134
|
end function
|
|
133
135
|
|
|
134
136
|
' ---------------------------------------------------------------------
|
|
135
|
-
'
|
|
137
|
+
' registerSourceObject - Registers a source object for event routing
|
|
136
138
|
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
139
|
+
' Generic registry for any Roku source object. Auto-detects routing mode:
|
|
140
|
+
' - Identity-based: If sourceObject implements ifSourceIdentity, routes via
|
|
141
|
+
' GetSourceIdentity() on the event (roUrlTransfer, roChannelStore).
|
|
142
|
+
' - Broadcast: If sourceObject does NOT implement ifSourceIdentity, broadcasts
|
|
143
|
+
' to all registered dispatchers (roDeviceInfo, roInput, roAppManager).
|
|
140
144
|
'
|
|
141
|
-
' @param {string}
|
|
142
|
-
' @param {string} dispatcherId - Dispatcher ID that
|
|
143
|
-
' @param {object}
|
|
145
|
+
' @param {string} objectId - Unique identifier for this registration
|
|
146
|
+
' @param {string} dispatcherId - Dispatcher ID that will handle events
|
|
147
|
+
' @param {object} sourceObject - The source object (SetMessagePort will be called)
|
|
148
|
+
' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
|
|
144
149
|
'
|
|
145
|
-
public sub
|
|
146
|
-
m.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
public sub registerSourceObject(objectId as string, dispatcherId as string, sourceObject as object, eventFilter = invalid as dynamic)
|
|
151
|
+
sourceObject.SetMessagePort(m.port)
|
|
152
|
+
if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
|
|
153
|
+
' Identity-based routing
|
|
154
|
+
identity = sourceObject.GetIdentity().ToStr()
|
|
155
|
+
m.sourceObjectRegistry[identity] = {
|
|
156
|
+
dispatcherId: dispatcherId,
|
|
157
|
+
objectId: objectId,
|
|
158
|
+
eventFilter: eventFilter
|
|
159
|
+
}
|
|
160
|
+
m.sourceObjectIdIndex[objectId] = identity
|
|
161
|
+
else
|
|
162
|
+
' Broadcast routing
|
|
163
|
+
m.sourceObjectTypeRegistry.push({
|
|
164
|
+
dispatcherId: dispatcherId,
|
|
165
|
+
objectId: objectId,
|
|
166
|
+
eventFilter: eventFilter
|
|
167
|
+
})
|
|
168
|
+
end if
|
|
150
169
|
end sub
|
|
151
170
|
|
|
152
171
|
' ---------------------------------------------------------------------
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
' This method associates a roDeviceInfo object with a dispatcher ID.
|
|
156
|
-
' When device info events occur (linkStatus, etc.), the framework will
|
|
157
|
-
' route them to the correct dispatcher's asyncReducerCallback.
|
|
172
|
+
' unregisterSourceObject - Unregisters a port-based object by its objectId
|
|
158
173
|
'
|
|
159
|
-
' @param {string}
|
|
160
|
-
' @param {string} dispatcherId - Dispatcher ID that will handle events
|
|
161
|
-
' @param {object} deviceInfo - The roDeviceInfo object (must have SetMessagePort called)
|
|
162
|
-
' @param {object} context - Optional user data to pass back in callback
|
|
174
|
+
' @param {string} objectId - The unique identifier used during registration
|
|
163
175
|
'
|
|
164
|
-
public sub
|
|
165
|
-
|
|
166
|
-
m.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
public sub unregisterSourceObject(objectId as string)
|
|
177
|
+
' Try identity-based registry
|
|
178
|
+
if m.sourceObjectIdIndex.DoesExist(objectId)
|
|
179
|
+
identity = m.sourceObjectIdIndex[objectId]
|
|
180
|
+
m.sourceObjectRegistry.Delete(identity)
|
|
181
|
+
m.sourceObjectIdIndex.Delete(objectId)
|
|
182
|
+
return
|
|
183
|
+
end if
|
|
184
|
+
' Try broadcast registry
|
|
185
|
+
for i = m.sourceObjectTypeRegistry.count() - 1 to 0 step -1
|
|
186
|
+
if m.sourceObjectTypeRegistry[i].objectId = objectId
|
|
187
|
+
m.sourceObjectTypeRegistry.Delete(i)
|
|
188
|
+
end if
|
|
189
|
+
end for
|
|
171
190
|
end sub
|
|
172
191
|
|
|
173
192
|
' ---------------------------------------------------------------------
|
|
@@ -241,53 +260,63 @@ namespace Rotor
|
|
|
241
260
|
|
|
242
261
|
end if
|
|
243
262
|
else
|
|
244
|
-
|
|
263
|
+
' Cross-thread state change: notify task-side listeners
|
|
245
264
|
data = msg.getData()
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
' Catch by dispatcherId
|
|
250
|
-
m.dispatcherProvider.get(extraInfo?.dispatcherId).asyncReducerCallback(msg, extraInfo?.context as dynamic)
|
|
251
|
-
else
|
|
252
|
-
dispatcherId = fieldId
|
|
253
|
-
dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
|
|
265
|
+
dispatcherId = fieldId
|
|
266
|
+
dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
|
|
267
|
+
if dispatcherInstance <> invalid
|
|
254
268
|
dispatcherInstance.notifyListeners(data)
|
|
255
269
|
end if
|
|
256
270
|
|
|
257
271
|
end if
|
|
258
|
-
else
|
|
259
|
-
'
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
else
|
|
273
|
+
' Generic source object routing
|
|
274
|
+
routed = false
|
|
275
|
+
|
|
276
|
+
' Try identity-based routing
|
|
277
|
+
if m.sourceObjectRegistry.count() > 0
|
|
278
|
+
try
|
|
279
|
+
sourceIdentity = msg.GetSourceIdentity().ToStr()
|
|
280
|
+
if m.sourceObjectRegistry.DoesExist(sourceIdentity)
|
|
281
|
+
entry = m.sourceObjectRegistry[sourceIdentity]
|
|
282
|
+
|
|
283
|
+
' Apply event filter if provided
|
|
284
|
+
allowed = true
|
|
285
|
+
if entry.eventFilter <> invalid
|
|
286
|
+
m._eventFilterFn = entry.eventFilter
|
|
287
|
+
allowed = m._eventFilterFn(msg)
|
|
288
|
+
end if
|
|
274
289
|
|
|
290
|
+
if allowed
|
|
291
|
+
dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
|
|
292
|
+
if dispatcherInstance <> invalid
|
|
293
|
+
dispatcherInstance.onSourceEvent(msg)
|
|
294
|
+
end if
|
|
295
|
+
end if
|
|
296
|
+
routed = true
|
|
297
|
+
end if
|
|
298
|
+
catch e
|
|
299
|
+
' Event doesn't support GetSourceIdentity - fall through to broadcast
|
|
300
|
+
end try
|
|
275
301
|
end if
|
|
276
|
-
|
|
277
|
-
'
|
|
278
|
-
if
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
302
|
+
|
|
303
|
+
' Broadcast to all non-identity registered dispatchers
|
|
304
|
+
if not routed
|
|
305
|
+
for each entry in m.sourceObjectTypeRegistry
|
|
306
|
+
' Apply event filter if provided
|
|
307
|
+
allowed = true
|
|
308
|
+
if entry.eventFilter <> invalid
|
|
309
|
+
m._eventFilterFn = entry.eventFilter
|
|
310
|
+
allowed = m._eventFilterFn(msg)
|
|
311
|
+
end if
|
|
312
|
+
|
|
313
|
+
if allowed
|
|
314
|
+
dispatcherInstance = m.dispatcherProvider.get(entry.dispatcherId)
|
|
286
315
|
if dispatcherInstance <> invalid
|
|
287
|
-
dispatcherInstance.
|
|
316
|
+
dispatcherInstance.onSourceEvent(msg)
|
|
288
317
|
end if
|
|
289
|
-
end
|
|
290
|
-
end
|
|
318
|
+
end if
|
|
319
|
+
end for
|
|
291
320
|
end if
|
|
292
321
|
end if
|
|
293
322
|
end if
|
|
@@ -354,8 +383,9 @@ namespace Rotor
|
|
|
354
383
|
' Destroys dispatcher provider and clears global framework helper.
|
|
355
384
|
'
|
|
356
385
|
public sub destroy()
|
|
357
|
-
m.
|
|
358
|
-
m.
|
|
386
|
+
m.sourceObjectRegistry.clear()
|
|
387
|
+
m.sourceObjectIdIndex.clear()
|
|
388
|
+
m.sourceObjectTypeRegistry.clear()
|
|
359
389
|
m.dispatcherProvider.destroy()
|
|
360
390
|
|
|
361
391
|
globalScope = GetGlobalAA()
|
|
@@ -32,12 +32,12 @@ namespace Rotor
|
|
|
32
32
|
dispatch as function ' Dispatch intent to owner dispatcher
|
|
33
33
|
dispatchTo as function ' Dispatch intent to dispatcher by ID
|
|
34
34
|
getStateFrom as function ' Get state from dispatcher by ID
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
dispatcher as object ' Owning dispatcher instance
|
|
36
|
+
dispatcherId as string ' Owning dispatcher ID
|
|
37
37
|
|
|
38
38
|
' Internal
|
|
39
39
|
middlewareFnScoped as dynamic ' Currently executing middleware
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
' =============================================================
|
|
42
42
|
' CONSTRUCTOR
|
|
43
43
|
' =============================================================
|
|
@@ -56,12 +56,12 @@ namespace Rotor
|
|
|
56
56
|
|
|
57
57
|
' Inject dispatch method for dispatching intents
|
|
58
58
|
m.dispatch = sub(intent as object)
|
|
59
|
-
m.
|
|
59
|
+
m.dispatcher.dispatch(intent)
|
|
60
60
|
end sub
|
|
61
61
|
|
|
62
62
|
' Inject getState method for accessing current state
|
|
63
63
|
m.getState = function() as Rotor.Model
|
|
64
|
-
return m.
|
|
64
|
+
return m.dispatcher.getState()
|
|
65
65
|
end function
|
|
66
66
|
|
|
67
67
|
' Inject dispatchTo method for dispatching intents to other dispatchers
|
|
@@ -105,6 +105,19 @@ namespace Rotor
|
|
|
105
105
|
return state
|
|
106
106
|
end function
|
|
107
107
|
|
|
108
|
+
' ---------------------------------------------------------------------
|
|
109
|
+
' onCreateDispatcher - Lifecycle hook called after dispatcher is fully registered
|
|
110
|
+
'
|
|
111
|
+
' Called by the framework after the dispatcher is created and registered
|
|
112
|
+
' with the DispatcherProvider. At this point, dispatcherId and all
|
|
113
|
+
' framework integrations are available.
|
|
114
|
+
'
|
|
115
|
+
' Override this method to perform initialization that requires
|
|
116
|
+
' the dispatcher to be fully set up (e.g., source object registration).
|
|
117
|
+
'
|
|
118
|
+
sub onCreateDispatcher()
|
|
119
|
+
end sub
|
|
120
|
+
|
|
108
121
|
' =============================================================
|
|
109
122
|
' MIDDLEWARE
|
|
110
123
|
' =============================================================
|
|
@@ -174,34 +187,37 @@ namespace Rotor
|
|
|
174
187
|
end function
|
|
175
188
|
|
|
176
189
|
' =============================================================
|
|
177
|
-
'
|
|
190
|
+
' SOURCE OBJECT SUPPORT
|
|
178
191
|
' =============================================================
|
|
179
192
|
|
|
180
193
|
' ---------------------------------------------------------------------
|
|
181
|
-
'
|
|
182
|
-
'
|
|
183
|
-
' Helper method to simplify async HTTP request tracking. Call this after
|
|
184
|
-
' creating a roUrlTransfer but BEFORE calling AsyncGetToString() or other
|
|
185
|
-
' async methods.
|
|
194
|
+
' registerSourceObject - Registers a source object with the framework
|
|
186
195
|
'
|
|
187
|
-
'
|
|
188
|
-
'
|
|
196
|
+
' Helper method for registering any Roku source object (roUrlTransfer,
|
|
197
|
+
' roDeviceInfo, roChannelStore, etc.). The framework will call
|
|
198
|
+
' SetMessagePort and route events to this reducer's onSourceEvent.
|
|
199
|
+
' Routing mode is auto-detected: identity-based if sourceObject has GetIdentity(),
|
|
200
|
+
' broadcast otherwise.
|
|
189
201
|
'
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
' transfer.SetUrl("https://api.example.com/data")
|
|
193
|
-
' transfer.SetMessagePort(m.port)
|
|
194
|
-
' m.registerAsyncTransfer(transfer, { userId: 123 })
|
|
195
|
-
' transfer.AsyncGetToString()
|
|
202
|
+
' @param {object} sourceObject - The source object
|
|
203
|
+
' @param {function} eventFilter - Optional filter function. Receives msg, returns boolean.
|
|
196
204
|
'
|
|
197
|
-
sub
|
|
205
|
+
sub registerSourceObject(sourceObject as object, eventFilter = invalid as dynamic)
|
|
198
206
|
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
199
|
-
transferId = transfer.GetIdentity().ToStr()
|
|
200
207
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
objectId = m.getSourceObjectId(sourceObject)
|
|
209
|
+
frameworkInstance.registerSourceObject(objectId, m.dispatcherId, sourceObject, eventFilter)
|
|
210
|
+
end sub
|
|
211
|
+
|
|
212
|
+
' ---------------------------------------------------------------------
|
|
213
|
+
' unregisterSourceObject - Unregisters a source object from the framework
|
|
214
|
+
'
|
|
215
|
+
' @param {object} sourceObject - The source object to unregister
|
|
216
|
+
'
|
|
217
|
+
sub unregisterSourceObject(sourceObject as object)
|
|
218
|
+
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
219
|
+
objectId = m.getSourceObjectId(sourceObject)
|
|
220
|
+
frameworkInstance.unregisterSourceObject(objectId)
|
|
205
221
|
end sub
|
|
206
222
|
|
|
207
223
|
' =============================================================
|
|
@@ -209,11 +225,28 @@ namespace Rotor
|
|
|
209
225
|
' =============================================================
|
|
210
226
|
|
|
211
227
|
' ---------------------------------------------------------------------
|
|
212
|
-
'
|
|
228
|
+
' getSourceObjectId - Generates unique objectId for a source object
|
|
229
|
+
'
|
|
230
|
+
' Identity-based objects (roUrlTransfer, etc.): dispatcherId + GetIdentity()
|
|
231
|
+
' Broadcast objects (roDeviceInfo, etc.): dispatcherId + type name
|
|
232
|
+
'
|
|
233
|
+
' @param {object} sourceObject - The source object
|
|
234
|
+
' @returns {string} Unique objectId
|
|
235
|
+
'
|
|
236
|
+
private function getSourceObjectId(sourceObject as object) as string
|
|
237
|
+
if FindMemberFunction(sourceObject, "GetIdentity") <> invalid
|
|
238
|
+
return `${m.dispatcherId}_${sourceObject.GetIdentity()}`
|
|
239
|
+
else
|
|
240
|
+
return `${m.dispatcherId}_${type(sourceObject)}`
|
|
241
|
+
end if
|
|
242
|
+
end function
|
|
243
|
+
|
|
244
|
+
' ---------------------------------------------------------------------
|
|
245
|
+
' onSourceEvent - Callback for source object events
|
|
213
246
|
'
|
|
214
|
-
' @param {object} msg -
|
|
247
|
+
' @param {object} msg - Raw message from source object (roUrlEvent, roDeviceInfoEvent, etc.)
|
|
215
248
|
'
|
|
216
|
-
sub
|
|
249
|
+
sub onSourceEvent(msg as object)
|
|
217
250
|
' Override in subclass to handle async responses
|
|
218
251
|
end sub
|
|
219
252
|
|
|
@@ -225,7 +258,7 @@ namespace Rotor
|
|
|
225
258
|
' destroy - Cleans up reducer references
|
|
226
259
|
'
|
|
227
260
|
sub destroy()
|
|
228
|
-
m.
|
|
261
|
+
m.dispatcher = invalid
|
|
229
262
|
m.port = invalid
|
|
230
263
|
end sub
|
|
231
264
|
|
|
@@ -69,11 +69,12 @@ namespace Rotor
|
|
|
69
69
|
' @param {object} state - New state to notify listeners about
|
|
70
70
|
'
|
|
71
71
|
sub notifyListeners(state as object)
|
|
72
|
-
listenerCount = m.listeners.Count()
|
|
73
72
|
listenerIndex = 0
|
|
74
73
|
|
|
75
74
|
' Iterate through all listeners
|
|
76
|
-
|
|
75
|
+
' Use m.listeners.Count() directly (not cached) to handle external listener
|
|
76
|
+
' removal during iteration (e.g., ViewModel destroyed in a callback)
|
|
77
|
+
while listenerIndex < m.listeners.Count()
|
|
77
78
|
listener = m.listeners[listenerIndex]
|
|
78
79
|
scope = listener.listenerScope
|
|
79
80
|
|
|
@@ -116,7 +117,6 @@ namespace Rotor
|
|
|
116
117
|
listener.listenerScope = invalid
|
|
117
118
|
listener.callback = invalid
|
|
118
119
|
m.listeners.Delete(listenerIndex)
|
|
119
|
-
listenerCount--
|
|
120
120
|
else
|
|
121
121
|
listenerIndex++
|
|
122
122
|
end if
|
|
@@ -62,8 +62,8 @@ namespace Rotor
|
|
|
62
62
|
|
|
63
63
|
' Link reducer to this dispatcher
|
|
64
64
|
m.reducerInstance = reducerInstance
|
|
65
|
-
m.reducerInstance.
|
|
66
|
-
m.reducerInstance.
|
|
65
|
+
m.reducerInstance.dispatcher = m
|
|
66
|
+
m.reducerInstance.dispatcherId = dispatcherId
|
|
67
67
|
m.modelInstance = modelInstance
|
|
68
68
|
|
|
69
69
|
' Create field on task node for render thread communication
|
|
@@ -75,6 +75,10 @@ namespace Rotor
|
|
|
75
75
|
' Register with dispatcher provider
|
|
76
76
|
dispatcherProvider = globalScope.rotor_framework_helper.frameworkInstance.dispatcherProvider
|
|
77
77
|
dispatcherProvider.registerDispatcher(m, dispatcherId)
|
|
78
|
+
|
|
79
|
+
' Lifecycle hook - called after dispatcher is fully registered
|
|
80
|
+
m.reducerInstance.onCreateDispatcher()
|
|
81
|
+
|
|
78
82
|
end sub
|
|
79
83
|
|
|
80
84
|
' =============================================================
|
|
@@ -155,12 +159,12 @@ namespace Rotor
|
|
|
155
159
|
' =============================================================
|
|
156
160
|
|
|
157
161
|
' ---------------------------------------------------------------------
|
|
158
|
-
'
|
|
162
|
+
' onSourceEvent - Routes async callback to reducer
|
|
159
163
|
'
|
|
160
164
|
' @param {object} msg - Message from async operation
|
|
161
165
|
'
|
|
162
|
-
sub
|
|
163
|
-
m.reducerInstance.
|
|
166
|
+
sub onSourceEvent(msg as object)
|
|
167
|
+
m.reducerInstance.onSourceEvent(msg)
|
|
164
168
|
end sub
|
|
165
169
|
|
|
166
170
|
' =============================================================
|
|
@@ -97,7 +97,7 @@ namespace Rotor.ViewBuilder
|
|
|
97
97
|
end function
|
|
98
98
|
|
|
99
99
|
' render - Renders widget updates (self, descendants, or children) *'
|
|
100
|
-
widget.render = sub(payloads as dynamic, params = {} as object)
|
|
100
|
+
widget.render = sub(payloads = invalid as dynamic, params = {} as object)
|
|
101
101
|
if payloads = invalid and m.isViewModel = true
|
|
102
102
|
' Self update (only for viewModels)
|
|
103
103
|
payloads = m.template()
|
|
@@ -264,15 +264,6 @@ namespace Rotor.ViewBuilder
|
|
|
264
264
|
' Apply threshold to ALL speech (both flush=true and flush=false)
|
|
265
265
|
' This prevents rapid interruptions from focus changes (e.g., "Home Home Home")
|
|
266
266
|
' If new request comes within 300ms, replace pending (filter rapid changes)
|
|
267
|
-
#if debug
|
|
268
|
-
if m.pendingSpeech <> invalid
|
|
269
|
-
' ? "[TTS_SERVICE][THRESHOLD] Replacing pending: '"; m.pendingSpeech.textToSpeak; "' with: '"; textToSpeak; "'"
|
|
270
|
-
else
|
|
271
|
-
flushLabel = ""
|
|
272
|
-
if shouldFlush then flushLabel = " (will flush)"
|
|
273
|
-
' ? "[TTS_SERVICE][THRESHOLD] Pending: '"; textToSpeak; "' ("; m.debounceDelay; "ms)"; flushLabel
|
|
274
|
-
end if
|
|
275
|
-
#end if
|
|
276
267
|
m.pendingSpeech = {
|
|
277
268
|
textToSpeak: textToSpeak,
|
|
278
269
|
shouldFlush: shouldFlush,
|
|
@@ -149,6 +149,9 @@ namespace Rotor
|
|
|
149
149
|
' Resolve value from key path
|
|
150
150
|
asset = Rotor.Utils.getValueByKeyPath(source, matchKey)
|
|
151
151
|
|
|
152
|
+
' Skip unresolved keys (e.g. @ in email addresses)
|
|
153
|
+
if asset = invalid then goto nextResult
|
|
154
|
+
|
|
152
155
|
' Handle string vs non-string results
|
|
153
156
|
if Rotor.Utils.isString(asset)
|
|
154
157
|
' String interpolation - replace in original string
|
|
@@ -1154,8 +1154,6 @@ namespace Rotor
|
|
|
1154
1154
|
if not isGroup and candidate.isEnabled = false then continue for
|
|
1155
1155
|
|
|
1156
1156
|
candidateMetrics = candidate.refreshBounding()
|
|
1157
|
-
candidateLeft = candidateMetrics.segments[Rotor.Const.Segment.LEFT]
|
|
1158
|
-
candidateTop = candidateMetrics.segments[Rotor.Const.Segment.TOP]
|
|
1159
1157
|
' Pass appropriate reference segments based on direction
|
|
1160
1158
|
if direction = "left" or direction = "right"
|
|
1161
1159
|
result = validator(candidateMetrics.segments, refSegmentLeft, refSegmentRight)
|
|
@@ -1237,7 +1235,7 @@ namespace Rotor
|
|
|
1237
1235
|
foundHID = ""
|
|
1238
1236
|
for each HID in possibleFocusItems
|
|
1239
1237
|
focusItem = m.get(HID)
|
|
1240
|
-
if focusItem
|
|
1238
|
+
if focusItem?.id = nodeId
|
|
1241
1239
|
foundHID = focusItem.HID
|
|
1242
1240
|
exit for
|
|
1243
1241
|
end if
|
|
@@ -1539,33 +1537,28 @@ namespace Rotor
|
|
|
1539
1537
|
if defaultFocusId <> ""
|
|
1540
1538
|
focusItemsHIDlist = m.getGroupMembersHIDs()
|
|
1541
1539
|
if focusItemsHIDlist.Count() > 0
|
|
1542
|
-
|
|
1543
1540
|
' Try find valid HID in focusItems by node id
|
|
1544
1541
|
focusItemHID = m.findHIDinFocusItemsByNodeId(defaultFocusId, focusItemsHIDlist)
|
|
1545
1542
|
if focusItemHID <> ""
|
|
1546
|
-
|
|
1543
|
+
return focusItemHID
|
|
1547
1544
|
end if
|
|
1548
|
-
|
|
1549
|
-
else
|
|
1550
|
-
|
|
1551
|
-
return defaultFocusId
|
|
1552
|
-
|
|
1553
1545
|
end if
|
|
1546
|
+
' If not found as focusItem, return defaultFocusId string
|
|
1547
|
+
' so capturingFocus_recursively can try to resolve it as a group
|
|
1548
|
+
return defaultFocusId
|
|
1554
1549
|
end if
|
|
1555
1550
|
|
|
1556
1551
|
return HID
|
|
1557
1552
|
end function
|
|
1558
1553
|
|
|
1559
1554
|
function findHIDinFocusItemsByNodeId(nodeId as string, focusItemsHIDlist as object) as string
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
focusItem = m.focusItemsRef.get(HID)
|
|
1555
|
+
for each itemHID in focusItemsHIDlist
|
|
1556
|
+
focusItem = m.focusItemsRef.get(itemHID)
|
|
1563
1557
|
if focusItem <> invalid and focusItem.id = nodeId
|
|
1564
|
-
|
|
1565
|
-
exit for
|
|
1558
|
+
return focusItem.HID
|
|
1566
1559
|
end if
|
|
1567
1560
|
end for
|
|
1568
|
-
return
|
|
1561
|
+
return ""
|
|
1569
1562
|
end function
|
|
1570
1563
|
|
|
1571
1564
|
sub applyFocus(isFocused as boolean)
|
|
@@ -1574,7 +1567,7 @@ namespace Rotor
|
|
|
1574
1567
|
m.isFocused = isFocused
|
|
1575
1568
|
|
|
1576
1569
|
if m.autoSetIsFocusedState
|
|
1577
|
-
m.widget.viewModelState.
|
|
1570
|
+
m.widget.viewModelState.isFocused = isFocused
|
|
1578
1571
|
end if
|
|
1579
1572
|
m.node.setField("isFocused", isFocused)
|
|
1580
1573
|
m.callOnFocusedFnOnWidget(isFocused)
|
|
@@ -34,56 +34,36 @@ namespace Rotor
|
|
|
34
34
|
sub new(pluginKey = "observer" as string)
|
|
35
35
|
super()
|
|
36
36
|
m.pluginKey = pluginKey
|
|
37
|
-
end sub
|
|
38
|
-
|
|
39
|
-
' =============================================================
|
|
40
|
-
' LIFECYCLE HOOKS
|
|
41
|
-
' =============================================================
|
|
42
|
-
|
|
43
|
-
hooks = {
|
|
44
|
-
' ---------------------------------------------------------------------
|
|
45
|
-
' beforeMount - Attaches observers when widget is mounted
|
|
46
|
-
'
|
|
47
|
-
' Called during widget creation to register all observers defined in the widget config.
|
|
48
|
-
'
|
|
49
|
-
' @param {object} scope - Plugin instance (m)
|
|
50
|
-
' @param {object} widget - Widget being mounted
|
|
51
|
-
'
|
|
52
|
-
beforeMount: sub(scope as object, widget as object)
|
|
53
|
-
config = widget[scope.pluginKey]
|
|
54
|
-
scope.attach(widget.node, config, widget)
|
|
55
|
-
end sub,
|
|
56
37
|
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
' Detaches old observers and attaches new ones based on updated configuration.
|
|
38
|
+
' WORKAROUND: Hooks must be assigned here in the constructor body
|
|
39
|
+
' (after super()), NOT as a class-level field initializer.
|
|
61
40
|
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
41
|
+
' Rooibos code coverage instrumentation (tested up to v6.0.0-alpha.50)
|
|
42
|
+
' reorders the compiled constructor so that class-level field initializers
|
|
43
|
+
' are placed BEFORE the super() call. Since BasePlugin's constructor sets
|
|
44
|
+
' m.hooks = invalid, this causes the parent to overwrite the hooks AA that
|
|
45
|
+
' was already set by the field initializer.
|
|
66
46
|
'
|
|
67
|
-
|
|
68
|
-
|
|
47
|
+
' Only affects classes with an explicit sub new() + super() AND
|
|
48
|
+
' class-level field initializers. Other plugins (FieldsPlugin, FocusPlugin,
|
|
49
|
+
' etc.) are not affected because they have no explicit constructor.
|
|
50
|
+
m.hooks = {
|
|
51
|
+
beforeMount: sub(scope as object, widget as object)
|
|
52
|
+
config = widget[scope.pluginKey]
|
|
53
|
+
scope.attach(widget.node, config, widget)
|
|
54
|
+
end sub,
|
|
55
|
+
beforeUpdate: sub(scope as object, widget as object, newValue, oldValue = {})
|
|
56
|
+
if oldValue <> invalid
|
|
57
|
+
scope.detach(widget.node)
|
|
58
|
+
end if
|
|
59
|
+
widget[scope.pluginKey] = newValue
|
|
60
|
+
scope.attach(widget.node, newValue, widget)
|
|
61
|
+
end sub,
|
|
62
|
+
beforeDestroy: sub(scope as object, widget as object)
|
|
69
63
|
scope.detach(widget.node)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
end sub,
|
|
74
|
-
|
|
75
|
-
' ---------------------------------------------------------------------
|
|
76
|
-
' beforeDestroy - Detaches all observers before widget destruction
|
|
77
|
-
'
|
|
78
|
-
' Ensures cleanup of all observers when widget is removed from scene graph.
|
|
79
|
-
'
|
|
80
|
-
' @param {object} scope - Plugin instance (m)
|
|
81
|
-
' @param {object} widget - Widget being destroyed
|
|
82
|
-
'
|
|
83
|
-
beforeDestroy: sub(scope as object, widget as object)
|
|
84
|
-
scope.detach(widget.node)
|
|
85
|
-
end sub
|
|
86
|
-
}
|
|
64
|
+
end sub
|
|
65
|
+
}
|
|
66
|
+
end sub
|
|
87
67
|
|
|
88
68
|
' =============================================================
|
|
89
69
|
' INITIALIZATION
|
|
@@ -405,7 +385,7 @@ namespace Rotor
|
|
|
405
385
|
' Represents a single observer configuration for a node field.
|
|
406
386
|
'
|
|
407
387
|
' Responsibilities:
|
|
408
|
-
' - Stores observer configuration (callback, conditions, etc.)
|
|
388
|
+
' - Stores observer configuration (callback or handler, conditions, etc.)
|
|
409
389
|
' - Sets up initial field value if provided
|
|
410
390
|
' - Provides info fields for observeFieldScoped
|
|
411
391
|
' - Executes callbacks in correct scope
|
|
@@ -427,7 +407,8 @@ namespace Rotor
|
|
|
427
407
|
value as dynamic ' Initial field value (if any)
|
|
428
408
|
once as boolean ' Remove observer after first trigger
|
|
429
409
|
until as function ' Conditional removal function
|
|
430
|
-
callback as
|
|
410
|
+
callback as dynamic ' Callback function called WITH payload (mutually exclusive with handler)
|
|
411
|
+
handler as dynamic ' Handler function called WITHOUT arguments (mutually exclusive with callback)
|
|
431
412
|
parsePayload as function ' Payload transformation function
|
|
432
413
|
alwaysNotify as boolean ' Field alwaysNotify flag
|
|
433
414
|
|
|
@@ -462,9 +443,13 @@ namespace Rotor
|
|
|
462
443
|
m.once = config?.once ?? false
|
|
463
444
|
m.until = config?.until
|
|
464
445
|
|
|
465
|
-
' Set callback (required)
|
|
466
|
-
m.callback = config?.callback
|
|
467
|
-
|
|
446
|
+
' Set callback or handler (one required, mutually exclusive)
|
|
447
|
+
m.callback = config?.callback
|
|
448
|
+
m.handler = config?.handler
|
|
449
|
+
|
|
450
|
+
if m.callback = invalid and m.handler = invalid
|
|
451
|
+
throw "Observer requires either 'callback' or 'handler' configuration"
|
|
452
|
+
end if
|
|
468
453
|
|
|
469
454
|
' Set payload parser (optional)
|
|
470
455
|
m.parsePayload = config?.parsePayload ?? function(payload)
|
|
@@ -518,14 +503,19 @@ namespace Rotor
|
|
|
518
503
|
' =============================================================
|
|
519
504
|
|
|
520
505
|
' ---------------------------------------------------------------------
|
|
521
|
-
' notify - Executes observer callback
|
|
506
|
+
' notify - Executes observer callback or handler
|
|
522
507
|
'
|
|
523
|
-
' Calls the configured callback
|
|
508
|
+
' Calls the configured callback (with payload) or handler (without arguments)
|
|
509
|
+
' in the correct widget scope.
|
|
524
510
|
'
|
|
525
|
-
' @param {dynamic} payload - Data to pass to callback
|
|
511
|
+
' @param {dynamic} payload - Data to pass to callback (ignored for handler)
|
|
526
512
|
'
|
|
527
513
|
sub notify(payload as dynamic)
|
|
528
|
-
|
|
514
|
+
if m.handler <> invalid
|
|
515
|
+
Rotor.Utils.callbackScoped(m.handler, m.listenerScope)
|
|
516
|
+
else
|
|
517
|
+
Rotor.Utils.callbackScoped(m.callback, m.listenerScope, payload)
|
|
518
|
+
end if
|
|
529
519
|
end sub
|
|
530
520
|
|
|
531
521
|
' =============================================================
|