rotor-framework 0.7.7 β 0.8.0
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/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/FocusPlugin.bs +8 -15
- package/src/source/plugins/ObserverPlugin.bs +26 -46
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.0",
|
|
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.0
|
|
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.0"
|
|
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.0
|
|
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.0"
|
|
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
|
|
|
@@ -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,
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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
|
-
'
|
|
38
|
+
' WORKAROUND: Hooks must be assigned here in the constructor body
|
|
39
|
+
' (after super()), NOT as a class-level field initializer.
|
|
59
40
|
'
|
|
60
|
-
'
|
|
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.
|
|
61
46
|
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
|
|
66
|
-
|
|
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
|