rotor-framework 0.7.2 → 0.7.3
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 +1 -1
- package/src/source/RotorFramework.bs +77 -105
- package/src/source/RotorFrameworkTask.bs +46 -7
- package/src/source/base/BaseWidget.bs +11 -9
- package/src/source/base/{ListenerForDispatchers.bs → DispatcherCore.bs} +12 -4
- package/src/source/base/{DispatcherExternal.bs → DispatcherCrossThread.bs} +24 -27
- package/src/source/base/{DispatcherCreator.bs → DispatcherOriginal.bs} +10 -13
- package/src/source/engine/Constants.bs +1 -1
- package/src/source/engine/providers/Dispatcher.bs +17 -16
- package/src/source/engine/providers/DispatcherProvider.bs +5 -5
- package/src/source/engine/services/Tts.bs +16 -9
- package/src/source/plugins/FocusPlugin.bs +2 -2
- package/src/source/plugins/FontStylePlugin.bs +34 -109
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.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
' ▐▛▀▚▖▐▌ ▐▌ █ ▐▌ ▐▌▐▛▀▚▖ ▐▛▀▀▘▐▛▀▚▖▐▛▀▜▌▐▌ ▐▌▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▛▚▖
|
|
5
5
|
' ▐▌ ▐▌▝▚▄▞▘ █ ▝▚▄▞▘▐▌ ▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌
|
|
6
6
|
' Rotor Framework™
|
|
7
|
-
' Version 0.7.
|
|
7
|
+
' Version 0.7.3
|
|
8
8
|
' © 2025 Balázs Molnár — Apache License 2.0
|
|
9
9
|
' =========================================================================
|
|
10
10
|
|
|
@@ -21,8 +21,8 @@ import "engine/animator/Animator.bs"
|
|
|
21
21
|
|
|
22
22
|
' base classes
|
|
23
23
|
import "base/BaseWidget.bs"
|
|
24
|
-
import "base/
|
|
25
|
-
import "base/
|
|
24
|
+
import "base/DispatcherOriginal.bs"
|
|
25
|
+
import "base/DispatcherCrossThread.bs"
|
|
26
26
|
import "base/BaseReducer.bs"
|
|
27
27
|
import "base/BaseModel.bs"
|
|
28
28
|
import "base/BaseStack.bs"
|
|
@@ -86,7 +86,7 @@ namespace Rotor
|
|
|
86
86
|
class Framework
|
|
87
87
|
|
|
88
88
|
name = "Rotor Framework"
|
|
89
|
-
version = "0.7.
|
|
89
|
+
version = "0.7.3"
|
|
90
90
|
|
|
91
91
|
config = {
|
|
92
92
|
tasks: invalid, ' @array (optional)
|
|
@@ -121,10 +121,8 @@ namespace Rotor
|
|
|
121
121
|
' plugin adapter workspace
|
|
122
122
|
plugins = {}
|
|
123
123
|
|
|
124
|
-
' sync
|
|
125
|
-
|
|
126
|
-
taskSyncReadyFlag = {}
|
|
127
|
-
taskNodes = {}
|
|
124
|
+
' sync - task tracking with phase: pending → operational → synced
|
|
125
|
+
tasks = {} ' taskId -> { node, phase, dispatchers[] }
|
|
128
126
|
|
|
129
127
|
enableRendering = true
|
|
130
128
|
|
|
@@ -360,15 +358,17 @@ namespace Rotor
|
|
|
360
358
|
taskNode = rootNode.createChild(taskName)
|
|
361
359
|
taskId = Rotor.Utils.getUUIDHex(16)
|
|
362
360
|
|
|
363
|
-
m.
|
|
364
|
-
|
|
361
|
+
m.tasks[taskId] = {
|
|
362
|
+
node: taskNode,
|
|
363
|
+
phase: "pending",
|
|
364
|
+
dispatchers: []
|
|
365
|
+
}
|
|
365
366
|
|
|
366
367
|
Rotor.Utils.setCustomFields(taskNode, {
|
|
367
368
|
taskId: taskId,
|
|
368
369
|
rootNode: rootNode
|
|
369
370
|
})
|
|
370
371
|
|
|
371
|
-
' taskNode.observeFieldScoped("rotorSync", "Rotor_syncCallback")
|
|
372
372
|
taskNode.control = "RUN"
|
|
373
373
|
end sub
|
|
374
374
|
|
|
@@ -378,136 +378,108 @@ namespace Rotor
|
|
|
378
378
|
' @param {object} taskList - Array of task names to set up
|
|
379
379
|
'
|
|
380
380
|
sub setupAdditionalTasks(taskList as object)
|
|
381
|
-
' check additional tasks
|
|
382
381
|
if taskList = invalid then return
|
|
383
|
-
|
|
384
|
-
taskNames = Rotor.Utils.ensureArray(taskList)
|
|
385
|
-
for each taskName in taskNames
|
|
382
|
+
for each taskName in Rotor.Utils.ensureArray(taskList)
|
|
386
383
|
m.setupTaskForSyncing(taskName)
|
|
387
384
|
end for
|
|
388
385
|
end sub
|
|
389
386
|
|
|
390
387
|
' ---------------------------------------------------------------------
|
|
391
|
-
'
|
|
388
|
+
' allTasksInPhase - Checks if all tasks are in given phase
|
|
392
389
|
'
|
|
393
|
-
' @
|
|
390
|
+
' @param {string} phase - Phase to check ("pending", "operational", "synced")
|
|
391
|
+
' @returns {boolean} True if all tasks are in the given phase
|
|
394
392
|
'
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
for each flag in m.taskOperationalFlag.Items()
|
|
400
|
-
if flag.value <> true then return false
|
|
393
|
+
function allTasksInPhase(phase as string) as boolean
|
|
394
|
+
if m.tasks.Count() = 0 then return false
|
|
395
|
+
for each taskId in m.tasks
|
|
396
|
+
if m.tasks[taskId].phase <> phase then return false
|
|
401
397
|
end for
|
|
402
|
-
|
|
403
398
|
return true
|
|
404
399
|
end function
|
|
405
400
|
|
|
406
401
|
' ---------------------------------------------------------------------
|
|
407
|
-
'
|
|
408
|
-
'
|
|
409
|
-
'
|
|
410
|
-
'
|
|
411
|
-
'
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
for each dispatcherId in dispatcherIds
|
|
421
|
-
dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
|
|
422
|
-
externalTaskNode = dispatcherInstance.taskNode
|
|
423
|
-
|
|
424
|
-
if not taskNode.isSameNode(externalTaskNode)
|
|
425
|
-
m.taskSyncReadyFlag[taskNode.taskId] = false ' collection for later usage
|
|
426
|
-
if registrations[taskNode.taskId] = invalid then registrations[taskNode.taskId] = []
|
|
427
|
-
registrations[taskNode.taskId].push({
|
|
428
|
-
dispatcherId: dispatcherId,
|
|
429
|
-
externalTaskNode: externalTaskNode
|
|
430
|
-
})
|
|
431
|
-
end if
|
|
432
|
-
end for
|
|
402
|
+
' getDispatchersNotOnTask - Gets dispatchers that are NOT on given task
|
|
403
|
+
'
|
|
404
|
+
' @param {object} taskNode - Task node to exclude
|
|
405
|
+
' @returns {object} Array of { dispatcherId, stateNode } for foreign dispatchers
|
|
406
|
+
'
|
|
407
|
+
function getDispatchersNotOnTask(taskNode as object) as object
|
|
408
|
+
result = []
|
|
409
|
+
for each dispatcherId in m.dispatcherProvider.getAll().keys()
|
|
410
|
+
stateNode = m.dispatcherProvider.get(dispatcherId).stateNode
|
|
411
|
+
if not taskNode.isSameNode(stateNode)
|
|
412
|
+
result.push({ dispatcherId: dispatcherId, stateNode: stateNode })
|
|
413
|
+
end if
|
|
433
414
|
end for
|
|
434
|
-
|
|
435
|
-
return registrations
|
|
415
|
+
return result
|
|
436
416
|
end function
|
|
437
417
|
|
|
438
418
|
' ---------------------------------------------------------------------
|
|
439
|
-
'
|
|
440
|
-
'
|
|
441
|
-
'
|
|
442
|
-
'
|
|
443
|
-
sub
|
|
444
|
-
for each taskId in
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
419
|
+
' broadcastDispatchersToTasks - Sends foreign dispatchers to each task
|
|
420
|
+
'
|
|
421
|
+
' For each task, sends list of dispatchers that exist on other threads.
|
|
422
|
+
'
|
|
423
|
+
sub broadcastDispatchersToTasks()
|
|
424
|
+
for each taskId in m.tasks
|
|
425
|
+
task = m.tasks[taskId]
|
|
426
|
+
foreignDispatchers = m.getDispatchersNotOnTask(task.node)
|
|
427
|
+
|
|
428
|
+
if foreignDispatchers.Count() > 0
|
|
429
|
+
task.node.setField("rotorSync", {
|
|
430
|
+
type: Rotor.Const.ThreadSyncType.REGISTER_CROSS_THREAD_DISPATCHER,
|
|
431
|
+
crossThreadDispatcherList: foreignDispatchers
|
|
432
|
+
})
|
|
433
|
+
else
|
|
434
|
+
task.phase = "synced"
|
|
435
|
+
end if
|
|
450
436
|
end for
|
|
451
|
-
end sub
|
|
452
437
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
function haveAllTasksSynced() as boolean
|
|
459
|
-
' Check if all nodes ready (very basic logic (< future improvement)
|
|
460
|
-
if m.taskSyncReadyFlag.Count() = 0 then return false
|
|
461
|
-
|
|
462
|
-
for each flag in m.taskSyncReadyFlag.Items()
|
|
463
|
-
if flag.value <> true then return false
|
|
464
|
-
end for
|
|
465
|
-
|
|
466
|
-
return true
|
|
467
|
-
end function
|
|
438
|
+
' If no tasks needed dispatchers, we're done
|
|
439
|
+
if m.allTasksInPhase("synced")
|
|
440
|
+
m.isReady()
|
|
441
|
+
end if
|
|
442
|
+
end sub
|
|
468
443
|
|
|
469
444
|
' ---------------------------------------------------------------------
|
|
470
445
|
' handleTaskSyncing - Handles task syncing phase
|
|
471
446
|
'
|
|
472
|
-
'
|
|
447
|
+
' Called when a task reports its dispatchers. Registers cross-thread
|
|
448
|
+
' dispatchers and broadcasts dispatcher map when all tasks are ready.
|
|
473
449
|
'
|
|
474
|
-
' @param {object} sync - Sync payload
|
|
450
|
+
' @param {object} sync - Sync payload { taskNode, dispatcherIds, tasks }
|
|
475
451
|
'
|
|
476
452
|
sub handleTaskSyncing(sync as object)
|
|
477
453
|
taskNode = sync.taskNode
|
|
454
|
+
taskId = taskNode.taskId
|
|
478
455
|
|
|
456
|
+
' Setup any additional tasks this task declared
|
|
479
457
|
m.setupAdditionalTasks(sync.tasks)
|
|
480
458
|
|
|
481
|
-
'
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
m.dispatcherProvider.registerExternalDispatchers(dispatcherIds, taskNode)
|
|
485
|
-
end if
|
|
459
|
+
' Store dispatcher IDs for this task
|
|
460
|
+
m.tasks[taskId].dispatchers = sync.dispatcherIds ?? []
|
|
461
|
+
m.tasks[taskId].phase = "operational"
|
|
486
462
|
|
|
487
|
-
'
|
|
488
|
-
|
|
463
|
+
' Register cross-thread dispatchers on render thread
|
|
464
|
+
if sync.dispatcherIds <> invalid
|
|
465
|
+
m.dispatcherProvider.registerCrossThreadDispatchers(sync.dispatcherIds, taskNode)
|
|
466
|
+
end if
|
|
489
467
|
|
|
490
|
-
|
|
468
|
+
' Wait for all tasks to be operational
|
|
469
|
+
if not m.allTasksInPhase("operational") then return
|
|
491
470
|
|
|
492
|
-
'
|
|
493
|
-
|
|
494
|
-
if registrations.Count() > 0
|
|
495
|
-
m.dispatchExternalDispatcherRegistrations(registrations)
|
|
496
|
-
else
|
|
497
|
-
m.isReady()
|
|
498
|
-
end if
|
|
471
|
+
' All tasks ready - broadcast dispatcher map
|
|
472
|
+
m.broadcastDispatchersToTasks()
|
|
499
473
|
end sub
|
|
500
474
|
|
|
501
475
|
' ---------------------------------------------------------------------
|
|
502
476
|
' handleTaskSynced - Handles task synced confirmation
|
|
503
477
|
'
|
|
504
|
-
' Marks task as synced and checks if all tasks are ready.
|
|
505
|
-
'
|
|
506
478
|
' @param {string} taskId - ID of the task that has synced
|
|
507
479
|
'
|
|
508
480
|
sub handleTaskSynced(taskId as string)
|
|
509
|
-
m.
|
|
510
|
-
if m.
|
|
481
|
+
m.tasks[taskId].phase = "synced"
|
|
482
|
+
if m.allTasksInPhase("synced")
|
|
511
483
|
m.isReady()
|
|
512
484
|
end if
|
|
513
485
|
end sub
|
|
@@ -549,15 +521,15 @@ namespace Rotor
|
|
|
549
521
|
public sub destroy()
|
|
550
522
|
|
|
551
523
|
rootNode = m.getRootNode()
|
|
552
|
-
for each taskId in m.
|
|
553
|
-
|
|
554
|
-
|
|
524
|
+
for each taskId in m.tasks
|
|
525
|
+
task = m.tasks[taskId]
|
|
526
|
+
task.node.setField("rotorSync", {
|
|
555
527
|
type: Rotor.Const.ThreadSyncType.DESTROY
|
|
556
528
|
})
|
|
557
|
-
rootNode.removeChild(
|
|
529
|
+
rootNode.removeChild(task.node)
|
|
558
530
|
end for
|
|
559
531
|
|
|
560
|
-
m.
|
|
532
|
+
m.tasks.Clear()
|
|
561
533
|
|
|
562
534
|
rootNode.unobserveFieldScoped("rotorSync")
|
|
563
535
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
' ▐▛▀▚▖▐▌ ▐▌ █ ▐▌ ▐▌▐▛▀▚▖ ▐▛▀▀▘▐▛▀▚▖▐▛▀▜▌▐▌ ▐▌▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▛▚▖
|
|
5
5
|
' ▐▌ ▐▌▝▚▄▞▘ █ ▝▚▄▞▘▐▌ ▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌
|
|
6
6
|
' Rotor Framework™
|
|
7
|
-
' Version 0.7.
|
|
7
|
+
' Version 0.7.3
|
|
8
8
|
' © 2025 Balázs Molnár — Apache License 2.0
|
|
9
9
|
' =========================================================================
|
|
10
10
|
|
|
@@ -16,8 +16,8 @@ import "engine/providers/DispatcherProvider.bs"
|
|
|
16
16
|
import "engine/providers/Dispatcher.bs"
|
|
17
17
|
|
|
18
18
|
' base classes
|
|
19
|
-
import "base/
|
|
20
|
-
import "base/
|
|
19
|
+
import "base/DispatcherOriginal.bs"
|
|
20
|
+
import "base/DispatcherCrossThread.bs"
|
|
21
21
|
import "base/BaseReducer.bs"
|
|
22
22
|
import "base/BaseModel.bs"
|
|
23
23
|
import "base/BaseStack.bs"
|
|
@@ -70,7 +70,7 @@ namespace Rotor
|
|
|
70
70
|
class FrameworkTask
|
|
71
71
|
|
|
72
72
|
name = "Rotor Framework"
|
|
73
|
-
version = "0.7.
|
|
73
|
+
version = "0.7.3"
|
|
74
74
|
|
|
75
75
|
config = {
|
|
76
76
|
tasks: invalid, ' optional
|
|
@@ -87,6 +87,7 @@ namespace Rotor
|
|
|
87
87
|
dispatcherProvider as object
|
|
88
88
|
port as object
|
|
89
89
|
asyncTransferRegistry = {} ' transferId -> { dispatcherId, context }
|
|
90
|
+
deviceInfoRegistry = {} ' deviceInfoId -> { dispatcherId, context, deviceInfo }
|
|
90
91
|
onTick as function
|
|
91
92
|
|
|
92
93
|
' ---------------------------------------------------------------------
|
|
@@ -148,6 +149,27 @@ namespace Rotor
|
|
|
148
149
|
}
|
|
149
150
|
end sub
|
|
150
151
|
|
|
152
|
+
' ---------------------------------------------------------------------
|
|
153
|
+
' registerDeviceInfo - Registers a roDeviceInfo for event listening
|
|
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.
|
|
158
|
+
'
|
|
159
|
+
' @param {string} deviceInfoId - Unique identifier for this registration
|
|
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
|
|
163
|
+
'
|
|
164
|
+
public sub registerDeviceInfo(deviceInfoId as string, dispatcherId as string, deviceInfo as object, context = invalid as dynamic)
|
|
165
|
+
deviceInfo.SetMessagePort(m.port)
|
|
166
|
+
m.deviceInfoRegistry[deviceInfoId] = {
|
|
167
|
+
dispatcherId: dispatcherId,
|
|
168
|
+
deviceInfo: deviceInfo,
|
|
169
|
+
context: context
|
|
170
|
+
}
|
|
171
|
+
end sub
|
|
172
|
+
|
|
151
173
|
' ---------------------------------------------------------------------
|
|
152
174
|
' sync - Starts the message loop for cross-thread communication
|
|
153
175
|
'
|
|
@@ -205,10 +227,10 @@ namespace Rotor
|
|
|
205
227
|
' taskIntent = Rotor.Utils.deepCopy(intent)
|
|
206
228
|
dispatcherInstance.dispatch(intent)
|
|
207
229
|
|
|
208
|
-
else if sync.type = Rotor.Const.ThreadSyncType.
|
|
230
|
+
else if sync.type = Rotor.Const.ThreadSyncType.REGISTER_CROSS_THREAD_DISPATCHER
|
|
209
231
|
|
|
210
|
-
for each item in sync.
|
|
211
|
-
m.dispatcherProvider.
|
|
232
|
+
for each item in sync.crossThreadDispatcherList
|
|
233
|
+
m.dispatcherProvider.registerCrossThreadDispatchers(item.dispatcherId, item.stateNode)
|
|
212
234
|
end for
|
|
213
235
|
|
|
214
236
|
m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCED)
|
|
@@ -251,6 +273,22 @@ namespace Rotor
|
|
|
251
273
|
end if
|
|
252
274
|
|
|
253
275
|
end if
|
|
276
|
+
else if msgType = "roDeviceInfoEvent"
|
|
277
|
+
' Handle device info events (ignore appFocused, etc.)
|
|
278
|
+
if m.deviceInfoRegistry.count() > 0
|
|
279
|
+
eventInfo = msg.GetInfo()
|
|
280
|
+
' Only process linkStatus events, ignore appFocused and other events
|
|
281
|
+
if eventInfo?.linkStatus <> invalid
|
|
282
|
+
' Route to all registered device info dispatchers
|
|
283
|
+
for each deviceInfoId in m.deviceInfoRegistry
|
|
284
|
+
registryEntry = m.deviceInfoRegistry[deviceInfoId]
|
|
285
|
+
dispatcherInstance = m.dispatcherProvider.get(registryEntry.dispatcherId)
|
|
286
|
+
if dispatcherInstance <> invalid
|
|
287
|
+
dispatcherInstance.asyncReducerCallback(eventInfo, registryEntry?.context as dynamic)
|
|
288
|
+
end if
|
|
289
|
+
end for
|
|
290
|
+
end if
|
|
291
|
+
end if
|
|
254
292
|
end if
|
|
255
293
|
end if
|
|
256
294
|
end while
|
|
@@ -317,6 +355,7 @@ namespace Rotor
|
|
|
317
355
|
'
|
|
318
356
|
public sub destroy()
|
|
319
357
|
m.asyncTransferRegistry.clear()
|
|
358
|
+
m.deviceInfoRegistry.clear()
|
|
320
359
|
m.dispatcherProvider.destroy()
|
|
321
360
|
|
|
322
361
|
globalScope = GetGlobalAA()
|
|
@@ -87,22 +87,24 @@ namespace Rotor
|
|
|
87
87
|
' These are optionally injected by plugins
|
|
88
88
|
|
|
89
89
|
' Focus Plugin Methods (from FocusPlugin.bs)
|
|
90
|
-
optional enableFocusNavigation as function
|
|
90
|
+
optional enableFocusNavigation as function ' Enable focus navigation
|
|
91
91
|
optional isFocusNavigationEnabled as function ' Check if focus enabled
|
|
92
|
-
optional setFocus as function
|
|
93
|
-
optional getFocusedWidget as function
|
|
94
|
-
optional proceedLongPress as function
|
|
95
|
-
optional isLongPressActive as function
|
|
92
|
+
optional setFocus as function ' Set focus on widget
|
|
93
|
+
optional getFocusedWidget as function ' Get currently focused widget
|
|
94
|
+
optional proceedLongPress as function ' Trigger long press
|
|
95
|
+
optional isLongPressActive as function ' Check if long press active
|
|
96
|
+
optional triggerKeyPress as function ' Trigger key press event
|
|
97
|
+
optional setGroupLastFocusedId as function ' Set the last focused widget ID within the current focus group (used to restore focus when re-entering the group)
|
|
96
98
|
|
|
97
99
|
' =============================================================
|
|
98
100
|
' PRIVATE PROPERTIES (Engine Use Only)
|
|
99
101
|
' =============================================================
|
|
100
102
|
|
|
101
|
-
private HID as string
|
|
102
|
-
private vmHID as string
|
|
103
|
-
private isRootChild as boolean
|
|
103
|
+
private HID as string ' Hierarchical ID (e.g., "0.header.logo")
|
|
104
|
+
private vmHID as string ' Owning ViewModel's HID
|
|
105
|
+
private isRootChild as boolean ' True if direct child of root
|
|
104
106
|
private childrenHIDhash as object ' Hash of child HIDs for fast lookup
|
|
105
|
-
private parentHID as string
|
|
107
|
+
private parentHID as string ' Parent's HID
|
|
106
108
|
|
|
107
109
|
end class
|
|
108
110
|
|
|
@@ -16,10 +16,15 @@ end interface
|
|
|
16
16
|
namespace Rotor
|
|
17
17
|
|
|
18
18
|
' =====================================================================
|
|
19
|
-
'
|
|
19
|
+
' DispatcherCore - Core base class for dispatcher implementations
|
|
20
20
|
'
|
|
21
|
-
' Provides
|
|
22
|
-
'
|
|
21
|
+
' Provides shared properties and listener management for both
|
|
22
|
+
' DispatcherOriginal and DispatcherCrossThread.
|
|
23
|
+
'
|
|
24
|
+
' Shared Properties:
|
|
25
|
+
' - dispatcherId: Unique identifier for this dispatcher
|
|
26
|
+
' - stateNode: SceneGraph node for state exposure
|
|
27
|
+
' - listeners: Array of registered state change listeners
|
|
23
28
|
'
|
|
24
29
|
' Listener Features:
|
|
25
30
|
' - State mapping: mapStateToProps for updating widget props
|
|
@@ -33,12 +38,15 @@ namespace Rotor
|
|
|
33
38
|
' 3. Invoke callback functions
|
|
34
39
|
' 4. Remove 'once' listeners
|
|
35
40
|
' =====================================================================
|
|
36
|
-
class
|
|
41
|
+
class DispatcherCore
|
|
37
42
|
|
|
38
43
|
' =============================================================
|
|
39
44
|
' MEMBER VARIABLES
|
|
40
45
|
' =============================================================
|
|
41
46
|
|
|
47
|
+
dispatcherId as string ' Dispatcher identifier
|
|
48
|
+
stateNode as object ' Node for cross-thread state exposure
|
|
49
|
+
|
|
42
50
|
listeners = [] ' Array of listener configurations
|
|
43
51
|
|
|
44
52
|
' =============================================================
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import "DispatcherCore.bs"
|
|
2
2
|
|
|
3
3
|
namespace Rotor
|
|
4
4
|
|
|
5
5
|
' =====================================================================
|
|
6
|
-
'
|
|
6
|
+
' DispatcherCrossThread - Cross-thread dispatcher for MVI state management
|
|
7
7
|
'
|
|
8
8
|
' This dispatcher is used when accessing dispatchers from a different thread
|
|
9
9
|
' than where they were created (render thread accessing task thread dispatcher
|
|
@@ -11,25 +11,22 @@ namespace Rotor
|
|
|
11
11
|
'
|
|
12
12
|
' Responsibilities:
|
|
13
13
|
' - Dispatches intents across thread boundaries via rotorSync field
|
|
14
|
-
' - Observes state changes on
|
|
14
|
+
' - Observes state changes on state node field
|
|
15
15
|
' - Manages cross-thread listener notifications
|
|
16
16
|
' - Lazy observer setup (only when listeners are added)
|
|
17
17
|
' - Auto cleanup when last listener is removed
|
|
18
18
|
'
|
|
19
19
|
' MVI Cross-Thread Flow:
|
|
20
20
|
' Render Thread: dispatch(intent) → rotorSync field → Task Thread
|
|
21
|
-
' Task Thread: reduce state →
|
|
21
|
+
' Task Thread: reduce state → state node field → Render Thread observer callback
|
|
22
22
|
' =====================================================================
|
|
23
|
-
class
|
|
23
|
+
class DispatcherCrossThread extends DispatcherCore
|
|
24
24
|
|
|
25
25
|
' =============================================================
|
|
26
26
|
' MEMBER VARIABLES
|
|
27
27
|
' =============================================================
|
|
28
28
|
|
|
29
|
-
dispatcherId as string ' Dispatcher identifier
|
|
30
|
-
taskNode as object ' Task node for cross-thread communication
|
|
31
29
|
threadType as string ' Current thread type (RENDER or TASK)
|
|
32
|
-
isExternal = true ' True for external dispatchers
|
|
33
30
|
isStateObserved = false ' Tracks if observer is active
|
|
34
31
|
|
|
35
32
|
' =============================================================
|
|
@@ -37,18 +34,18 @@ namespace Rotor
|
|
|
37
34
|
' =============================================================
|
|
38
35
|
|
|
39
36
|
' ---------------------------------------------------------------------
|
|
40
|
-
' Constructor - Initializes
|
|
37
|
+
' Constructor - Initializes cross-thread dispatcher
|
|
41
38
|
'
|
|
42
39
|
' @param {string} dispatcherId - Dispatcher identifier
|
|
43
|
-
' @param {object}
|
|
40
|
+
' @param {object} stateNode - Node for cross-thread state communication
|
|
44
41
|
' @param {string} threadType - Current thread type (RENDER or TASK)
|
|
45
42
|
'
|
|
46
|
-
sub new(dispatcherId as string,
|
|
43
|
+
sub new(dispatcherId as string, stateNode as object, threadType as string)
|
|
47
44
|
super()
|
|
48
45
|
|
|
49
46
|
' Store dispatcher configuration
|
|
50
47
|
m.dispatcherId = dispatcherId
|
|
51
|
-
m.
|
|
48
|
+
m.stateNode = stateNode
|
|
52
49
|
m.threadType = threadType
|
|
53
50
|
end sub
|
|
54
51
|
|
|
@@ -73,7 +70,7 @@ namespace Rotor
|
|
|
73
70
|
end if
|
|
74
71
|
|
|
75
72
|
' Send intent to task thread via rotorSync field
|
|
76
|
-
m.
|
|
73
|
+
m.stateNode.setField("rotorSync", {
|
|
77
74
|
type: Rotor.Const.ThreadSyncType.DISPATCH,
|
|
78
75
|
payload: {
|
|
79
76
|
dispatcherId: m.dispatcherId,
|
|
@@ -87,17 +84,17 @@ namespace Rotor
|
|
|
87
84
|
' =============================================================
|
|
88
85
|
|
|
89
86
|
' ---------------------------------------------------------------------
|
|
90
|
-
' getState - Returns current state from
|
|
87
|
+
' getState - Returns current state from state node field (optionally mapped)
|
|
91
88
|
'
|
|
92
|
-
' Reads state from
|
|
89
|
+
' Reads state from state node field set by internal dispatcher.
|
|
93
90
|
'
|
|
94
91
|
' @param {dynamic} mapStateToProps - Optional mapping function
|
|
95
92
|
' @param {object} callerScope - Caller scope for mapping function
|
|
96
93
|
' @returns {object} Current state
|
|
97
94
|
'
|
|
98
95
|
function getState(mapStateToProps = invalid as Dynamic, callerScope = invalid as object) as object
|
|
99
|
-
' Read state from
|
|
100
|
-
state = m.
|
|
96
|
+
' Read state from state node field
|
|
97
|
+
state = m.stateNode.getField(m.dispatcherId)
|
|
101
98
|
|
|
102
99
|
' Apply optional state mapping
|
|
103
100
|
m.runMapStateToProps(state, mapStateToProps, callerScope)
|
|
@@ -150,7 +147,7 @@ namespace Rotor
|
|
|
150
147
|
' =============================================================
|
|
151
148
|
|
|
152
149
|
' ---------------------------------------------------------------------
|
|
153
|
-
' setupObserver - Sets up observer for state changes on
|
|
150
|
+
' setupObserver - Sets up observer for state changes on state node field
|
|
154
151
|
'
|
|
155
152
|
' Thread-specific observer setup:
|
|
156
153
|
' - RENDER thread: Uses observeFieldScoped with callback name
|
|
@@ -159,12 +156,12 @@ namespace Rotor
|
|
|
159
156
|
sub setupObserver()
|
|
160
157
|
if m.threadType = Rotor.Const.ThreadType.RENDER
|
|
161
158
|
' Render thread: use native SceneGraph observer
|
|
162
|
-
m.
|
|
159
|
+
m.stateNode.observeFieldScoped(m.dispatcherId, "Rotor_dispatcherStateCallback")
|
|
163
160
|
else
|
|
164
161
|
' Task thread: use framework observer system
|
|
165
162
|
fieldId = m.dispatcherId
|
|
166
163
|
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
167
|
-
frameworkInstance.addObserver(fieldId, m.
|
|
164
|
+
frameworkInstance.addObserver(fieldId, m.stateNode)
|
|
168
165
|
end if
|
|
169
166
|
|
|
170
167
|
m.isStateObserved = true
|
|
@@ -178,12 +175,12 @@ namespace Rotor
|
|
|
178
175
|
sub removeObserver()
|
|
179
176
|
if m.threadType = Rotor.Const.ThreadType.RENDER
|
|
180
177
|
' Render thread: unobserve SceneGraph field
|
|
181
|
-
m.
|
|
178
|
+
m.stateNode.unobserveFieldScoped(m.dispatcherId)
|
|
182
179
|
else
|
|
183
180
|
' Task thread: remove framework observer
|
|
184
181
|
fieldId = m.dispatcherId
|
|
185
182
|
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
186
|
-
frameworkInstance.removeObserver(fieldId, m.
|
|
183
|
+
frameworkInstance.removeObserver(fieldId, m.stateNode)
|
|
187
184
|
end if
|
|
188
185
|
|
|
189
186
|
m.isStateObserved = false
|
|
@@ -194,7 +191,7 @@ namespace Rotor
|
|
|
194
191
|
' =============================================================
|
|
195
192
|
|
|
196
193
|
' ---------------------------------------------------------------------
|
|
197
|
-
' destroy - Cleans up
|
|
194
|
+
' destroy - Cleans up cross-thread dispatcher resources
|
|
198
195
|
'
|
|
199
196
|
' Cleanup steps:
|
|
200
197
|
' 1. Call parent cleanup
|
|
@@ -210,16 +207,16 @@ namespace Rotor
|
|
|
210
207
|
' Remove observer if active
|
|
211
208
|
if m.isStateObserved = true
|
|
212
209
|
if m.threadType = Rotor.Const.ThreadType.RENDER
|
|
213
|
-
m.
|
|
210
|
+
m.stateNode.unobserveFieldScoped(m.dispatcherId)
|
|
214
211
|
else
|
|
215
212
|
fieldId = m.dispatcherId
|
|
216
|
-
frameworkInstance.removeObserver(fieldId, m.
|
|
213
|
+
frameworkInstance.removeObserver(fieldId, m.stateNode)
|
|
217
214
|
end if
|
|
218
215
|
end if
|
|
219
216
|
|
|
220
217
|
' Clear all references
|
|
221
218
|
m.listeners.Clear()
|
|
222
|
-
m.
|
|
219
|
+
m.stateNode = invalid
|
|
223
220
|
|
|
224
221
|
' Deregister from provider
|
|
225
222
|
frameworkInstance.dispatcherProvider.deregisterDispatcher(m.dispatcherId)
|
|
@@ -235,7 +232,7 @@ namespace Rotor
|
|
|
235
232
|
' dispatcherStateCallback - Global callback function for dispatcher state changes on render thread
|
|
236
233
|
'
|
|
237
234
|
' This function is called by SceneGraph's observeFieldScoped when a dispatcher's
|
|
238
|
-
' state field changes on the
|
|
235
|
+
' state field changes on the state node. It routes the state change to the
|
|
239
236
|
' appropriate dispatcher instance for listener notification.
|
|
240
237
|
'
|
|
241
238
|
' Process flow:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import "DispatcherCore.bs"
|
|
2
2
|
|
|
3
3
|
namespace Rotor
|
|
4
4
|
|
|
@@ -13,11 +13,11 @@ namespace Rotor
|
|
|
13
13
|
' @returns {Dispatcher} Dispatcher facade instance
|
|
14
14
|
' =====================================================================
|
|
15
15
|
sub createDispatcher(customDispatcherId as string, modelInstance as Model, reducerInstance as Reducer)
|
|
16
|
-
m.dispatcherInstance = new Rotor.
|
|
16
|
+
m.dispatcherInstance = new Rotor.DispatcherOriginal(customDispatcherId, modelInstance, reducerInstance)
|
|
17
17
|
end sub
|
|
18
18
|
|
|
19
19
|
' =====================================================================
|
|
20
|
-
'
|
|
20
|
+
' DispatcherOriginal - Internal dispatcher implementation for MVI pattern state management
|
|
21
21
|
'
|
|
22
22
|
' Responsibilities:
|
|
23
23
|
' - Dispatches intents to reducer
|
|
@@ -29,21 +29,17 @@ namespace Rotor
|
|
|
29
29
|
' MVI Flow:
|
|
30
30
|
' dispatch(intent) → reducer.reduce() → update state → notify listeners
|
|
31
31
|
' =====================================================================
|
|
32
|
-
class
|
|
32
|
+
class DispatcherOriginal extends DispatcherCore
|
|
33
33
|
|
|
34
34
|
' =============================================================
|
|
35
35
|
' MEMBER VARIABLES
|
|
36
36
|
' =============================================================
|
|
37
37
|
|
|
38
38
|
middlewares = [] ' Middleware array (unused, handled by Reducer)
|
|
39
|
-
isExternal = false ' False for internal dispatchers
|
|
40
39
|
isDispatchingInProgress = false ' Prevents reentrant dispatch
|
|
41
40
|
dispatchQueue = [] ' Queue for intents during active dispatch
|
|
42
|
-
dispatcherId as string ' Dispatcher identifier
|
|
43
|
-
taskNode as object ' Task node for cross-thread state exposure
|
|
44
41
|
reducerInstance as object ' Reducer instance
|
|
45
42
|
modelInstance as object ' Model instance holding state
|
|
46
|
-
listeners = [] ' State change listeners
|
|
47
43
|
|
|
48
44
|
' =============================================================
|
|
49
45
|
' CONSTRUCTOR
|
|
@@ -62,7 +58,7 @@ namespace Rotor
|
|
|
62
58
|
m.dispatcherId = dispatcherId
|
|
63
59
|
|
|
64
60
|
globalScope = GetGlobalAA()
|
|
65
|
-
m.
|
|
61
|
+
m.stateNode = globalScope.top
|
|
66
62
|
|
|
67
63
|
' Link reducer to this dispatcher
|
|
68
64
|
m.reducerInstance = reducerInstance
|
|
@@ -71,7 +67,7 @@ namespace Rotor
|
|
|
71
67
|
m.modelInstance = modelInstance
|
|
72
68
|
|
|
73
69
|
' Create field on task node for render thread communication
|
|
74
|
-
m.
|
|
70
|
+
m.stateNode.addField(m.dispatcherId, "node", true)
|
|
75
71
|
|
|
76
72
|
' Expose initial state
|
|
77
73
|
m.exposeState()
|
|
@@ -94,7 +90,7 @@ namespace Rotor
|
|
|
94
90
|
sub exposeState()
|
|
95
91
|
newState = CreateObject("roSGNode", "ContentNode")
|
|
96
92
|
newState.addFields(m.modelInstance.state)
|
|
97
|
-
m.
|
|
93
|
+
m.stateNode.setField(m.dispatcherId, newState)
|
|
98
94
|
end sub
|
|
99
95
|
|
|
100
96
|
' ---------------------------------------------------------------------
|
|
@@ -131,6 +127,7 @@ namespace Rotor
|
|
|
131
127
|
end if
|
|
132
128
|
|
|
133
129
|
if m.isDispatchingInProgress = false
|
|
130
|
+
|
|
134
131
|
' Execute reducer
|
|
135
132
|
currentState = m.modelInstance.state
|
|
136
133
|
newState = m.reducerInstance.reduce(currentState, intent)
|
|
@@ -148,7 +145,7 @@ namespace Rotor
|
|
|
148
145
|
m.dispatch(intent)
|
|
149
146
|
end if
|
|
150
147
|
else
|
|
151
|
-
' Queue intent
|
|
148
|
+
' Queue intent for later processing
|
|
152
149
|
m.dispatchQueue.push(intent)
|
|
153
150
|
end if
|
|
154
151
|
end sub
|
|
@@ -183,7 +180,7 @@ namespace Rotor
|
|
|
183
180
|
frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
|
|
184
181
|
frameworkInstance.dispatcherProvider.deregisterDispatcher(m.dispatcherId)
|
|
185
182
|
|
|
186
|
-
m.
|
|
183
|
+
m.stateNode = invalid
|
|
187
184
|
m.modelInstance = invalid
|
|
188
185
|
m.reducerInstance = invalid
|
|
189
186
|
end sub
|
|
@@ -57,7 +57,7 @@ namespace Rotor
|
|
|
57
57
|
|
|
58
58
|
enum ThreadSyncType
|
|
59
59
|
TASK_SYNCING = "taskSyncing"
|
|
60
|
-
|
|
60
|
+
REGISTER_CROSS_THREAD_DISPATCHER = "registerCrossThreadDispatcher"
|
|
61
61
|
DESTROY = "destroy"
|
|
62
62
|
TASK_SYNCED = "taskSynced"
|
|
63
63
|
SYNC_COMPLETED = "sync_completed"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
namespace Rotor
|
|
3
3
|
|
|
4
4
|
' =====================================================================
|
|
5
|
-
' Dispatcher - Dispatcher Facade that provides
|
|
5
|
+
' Dispatcher FACADE - Dispatcher Facade that provides an interface for state management
|
|
6
6
|
'
|
|
7
7
|
' Acts as a proxy to the underlying dispatcher instance, managing intents,
|
|
8
8
|
' listeners, and state access. The facade pattern allows for clean separation
|
|
@@ -12,11 +12,12 @@ namespace Rotor
|
|
|
12
12
|
class Dispatcher
|
|
13
13
|
|
|
14
14
|
dispatcherId as string
|
|
15
|
-
listenerId as string
|
|
16
|
-
listenerScope as object
|
|
17
|
-
dispatcherInstance as object
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
private listenerId as string
|
|
17
|
+
private listenerScope as object
|
|
18
|
+
private dispatcherInstance as object
|
|
19
|
+
|
|
20
|
+
private sub new(dispatcherInstance as dynamic, dispatcherId as string, listenerId = "" as string, listenerScope = invalid as object)
|
|
20
21
|
m.dispatcherId = dispatcherId
|
|
21
22
|
m.listenerScope = listenerScope
|
|
22
23
|
m.listenerId = listenerId
|
|
@@ -28,7 +29,7 @@ namespace Rotor
|
|
|
28
29
|
'
|
|
29
30
|
' @param {Intent} intent - The intent object containing action type and payload
|
|
30
31
|
'
|
|
31
|
-
sub dispatch(intent)
|
|
32
|
+
public sub dispatch(intent)
|
|
32
33
|
m.dispatcherInstance.dispatch(intent)
|
|
33
34
|
end sub
|
|
34
35
|
|
|
@@ -37,31 +38,31 @@ namespace Rotor
|
|
|
37
38
|
'
|
|
38
39
|
' @param {Dynamic} listenerConfig - Configuration object for the listener (can be a function or object with mapStateToProps)
|
|
39
40
|
'
|
|
40
|
-
sub addListener(listenerConfig as dynamic)
|
|
41
|
+
public sub addListener(listenerConfig as dynamic)
|
|
41
42
|
m.dispatcherInstance.addListener(listenerConfig, m.listenerId, m.listenerScope)
|
|
42
43
|
end sub
|
|
43
44
|
|
|
44
|
-
' ---------------------------------------------------------------------
|
|
45
|
-
' removeAllListenersByListenerId - Removes all listeners associated with this dispatcher's listener ID
|
|
46
|
-
'
|
|
47
|
-
sub removeAllListenersByListenerId()
|
|
48
|
-
m.dispatcherInstance.removeAllListenersByListenerId(m.listenerId)
|
|
49
|
-
end sub
|
|
50
|
-
|
|
51
45
|
' ---------------------------------------------------------------------
|
|
52
46
|
' getState - Retrieves the current state from the dispatcher, optionally mapped through a selector function
|
|
53
47
|
'
|
|
54
48
|
' @param {Dynamic} mapStateToProps - Optional function to map/select specific parts of the state
|
|
55
49
|
' @returns {Object} The current state object (mapped if selector provided)
|
|
56
50
|
'
|
|
57
|
-
function getState(mapStateToProps = invalid as Dynamic) as object
|
|
51
|
+
public function getState(mapStateToProps = invalid as Dynamic) as object
|
|
58
52
|
return m.dispatcherInstance.getState(mapStateToProps, m.listenerScope)
|
|
59
53
|
end function
|
|
60
54
|
|
|
55
|
+
' ---------------------------------------------------------------------
|
|
56
|
+
' removeAllListenersByListenerId - Removes all listeners associated with this dispatcher's listener ID
|
|
57
|
+
'
|
|
58
|
+
private sub removeAllListenersByListenerId()
|
|
59
|
+
m.dispatcherInstance.removeAllListenersByListenerId(m.listenerId)
|
|
60
|
+
end sub
|
|
61
|
+
|
|
61
62
|
' ---------------------------------------------------------------------
|
|
62
63
|
' destroy - Cleans up the dispatcher facade by removing all listeners and clearing references
|
|
63
64
|
'
|
|
64
|
-
sub destroy()
|
|
65
|
+
public sub destroy()
|
|
65
66
|
m.removeAllListenersByListenerId()
|
|
66
67
|
m.listenerScope = invalid
|
|
67
68
|
m.dispatcherInstance = invalid
|
|
@@ -6,7 +6,7 @@ namespace Rotor
|
|
|
6
6
|
'
|
|
7
7
|
' Manages a collection of dispatcher instances and provides facade access
|
|
8
8
|
' to dispatchers. Extends BaseStack to store and organize dispatchers by ID.
|
|
9
|
-
' Handles both
|
|
9
|
+
' Handles both local and cross-thread dispatcher registration/deregistration.
|
|
10
10
|
' =====================================================================
|
|
11
11
|
class DispatcherProvider extends Rotor.BaseStack
|
|
12
12
|
|
|
@@ -59,15 +59,15 @@ namespace Rotor
|
|
|
59
59
|
end sub
|
|
60
60
|
|
|
61
61
|
' ---------------------------------------------------------------------
|
|
62
|
-
'
|
|
62
|
+
' registerCrossThreadDispatchers - Registers cross-thread dispatchers from another thread
|
|
63
63
|
'
|
|
64
64
|
' @param {dynamic} dispatcherIds - Single ID string or array of dispatcher IDs
|
|
65
|
-
' @param {object}
|
|
65
|
+
' @param {object} stateNode - The node for cross-thread state communication
|
|
66
66
|
'
|
|
67
|
-
sub
|
|
67
|
+
sub registerCrossThreadDispatchers(dispatcherIds as dynamic, stateNode as object)
|
|
68
68
|
dispatcherIds = Rotor.Utils.ensureArray(dispatcherIds)
|
|
69
69
|
for each dispatcherId in dispatcherIds
|
|
70
|
-
dispatcherInstance = new Rotor.
|
|
70
|
+
dispatcherInstance = new Rotor.DispatcherCrossThread(dispatcherId, stateNode, m.threadType)
|
|
71
71
|
m.set(dispatcherId, dispatcherInstance)
|
|
72
72
|
end for
|
|
73
73
|
end sub
|
|
@@ -114,13 +114,15 @@ namespace Rotor.ViewBuilder
|
|
|
114
114
|
m.audioGuide = CreateObject("roAudioGuide")
|
|
115
115
|
m.debounceTimer = CreateObject("roTimespan")
|
|
116
116
|
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
#if not unittest
|
|
118
|
+
if m.audioGuide = invalid
|
|
119
|
+
#if debug
|
|
120
|
+
? "[TTS_SERVICE][ERROR] Failed to create roAudioGuide instance"
|
|
121
|
+
#end if
|
|
122
|
+
m.isEnabled = false
|
|
123
|
+
return
|
|
124
|
+
end if
|
|
125
|
+
#end if
|
|
124
126
|
|
|
125
127
|
' Create timer node for threshold (one-shot, started on demand)
|
|
126
128
|
rootNode = m.frameworkInstance.getRootNode()
|
|
@@ -377,7 +379,10 @@ namespace Rotor.ViewBuilder
|
|
|
377
379
|
end if
|
|
378
380
|
|
|
379
381
|
' Speak text (Say() handles flushing if shouldFlush=true)
|
|
380
|
-
speechId =
|
|
382
|
+
speechId = 0
|
|
383
|
+
if m.audioGuide <> invalid
|
|
384
|
+
speechId = m.audioGuide.Say(text, shouldFlush, dontRepeat)
|
|
385
|
+
end if
|
|
381
386
|
m.lastSpeech = text
|
|
382
387
|
|
|
383
388
|
' Set protection flag if requested
|
|
@@ -542,7 +547,9 @@ namespace Rotor.ViewBuilder
|
|
|
542
547
|
'
|
|
543
548
|
public sub stopSpeech()
|
|
544
549
|
if not m.isEnabled or not m.getIsDeviceAudioGuideEnabled() then return
|
|
545
|
-
m.audioGuide
|
|
550
|
+
if m.audioGuide <> invalid
|
|
551
|
+
m.audioGuide.Flush()
|
|
552
|
+
end if
|
|
546
553
|
m.preventNextFlushFlag = false
|
|
547
554
|
m.pendingSpeech = invalid
|
|
548
555
|
m.isPendingProtected = false
|
|
@@ -324,14 +324,14 @@ namespace Rotor
|
|
|
324
324
|
end function,
|
|
325
325
|
|
|
326
326
|
' ---------------------------------------------------------------------
|
|
327
|
-
'
|
|
327
|
+
' setGroupLastFocusedId - Updates the lastFocusedHID of this widget's focus group
|
|
328
328
|
'
|
|
329
329
|
' If called on a focusGroup widget, updates its own lastFocusedHID.
|
|
330
330
|
' If called on a focusItem widget, finds and updates the parent group's lastFocusedHID.
|
|
331
331
|
'
|
|
332
332
|
' @param {string} id - The widget id to set as lastFocusedHID
|
|
333
333
|
'
|
|
334
|
-
|
|
334
|
+
setGroupLastFocusedId: sub(id as string)
|
|
335
335
|
plugin = m.getFrameworkInstance().plugins[PRIMARY_FOCUS_PLUGIN_KEY]
|
|
336
336
|
|
|
337
337
|
' Determine ancestorHID for search context
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
import "../base/BasePlugin.bs"
|
|
2
|
-
|
|
3
1
|
namespace Rotor
|
|
4
2
|
|
|
5
3
|
' =====================================================================
|
|
6
4
|
' FontStylePlugin - Rotor Framework plugin for font styling on Labels
|
|
7
5
|
'
|
|
8
|
-
'
|
|
9
|
-
' and @-prefixed expression interpolation for dynamic font selection.
|
|
10
|
-
' Automatically updates font styles on widget lifecycle changes.
|
|
6
|
+
' Applies font styles to Label nodes with @ operator resolution.
|
|
11
7
|
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
8
|
+
' Usage:
|
|
9
|
+
' fontStyle: { uri: "pkg:/assets/fonts/Roboto.ttf", size: 24 }
|
|
10
|
+
' fontStyle: { uri: "@l10n.fonts.regular", size: 24 }
|
|
11
|
+
' fontStyle: { uri: "@fontUri", size: "@fontSize" }
|
|
12
|
+
' fontStyle: function() as object
|
|
13
|
+
' return { uri: m.props.fontUri, size: 24 }
|
|
14
|
+
' end function
|
|
17
15
|
'
|
|
18
16
|
' Expression Syntax:
|
|
19
|
-
' @key.path - Resolves from widget.viewModelState
|
|
17
|
+
' @l10n.key.path - Resolves from widget.viewModelState.l10n
|
|
18
|
+
' @key.path - Resolves from widget.viewModelState
|
|
20
19
|
'
|
|
21
20
|
' Note: Only affects widgets with nodeType = "Label"
|
|
22
21
|
' =====================================================================
|
|
@@ -24,123 +23,49 @@ namespace Rotor
|
|
|
24
23
|
|
|
25
24
|
pluginKey = "fontStyle"
|
|
26
25
|
|
|
27
|
-
' =============================================================
|
|
28
|
-
' MEMBER VARIABLES
|
|
29
|
-
' =============================================================
|
|
30
|
-
|
|
31
|
-
' Regex pattern for matching @-prefixed expressions
|
|
32
|
-
' Matches: @ followed by any characters except space, @, or comma
|
|
33
|
-
configRegex = /(\@)([^\s\@\,]*)/i
|
|
34
|
-
|
|
35
|
-
' Regex pattern for extracting plugin key prefix (unused but kept for compatibility)
|
|
36
|
-
pluginKeyRegex = /^[^\.]*/i
|
|
37
|
-
|
|
38
|
-
' =============================================================
|
|
39
|
-
' LIFECYCLE HOOKS
|
|
40
|
-
' =============================================================
|
|
41
|
-
|
|
42
26
|
hooks = {
|
|
43
|
-
' ---------------------------------------------------------------------
|
|
44
|
-
' beforeMount - Sets font style when widget is mounted
|
|
45
|
-
'
|
|
46
|
-
' Evaluates and applies font style after Label widget creation.
|
|
47
|
-
'
|
|
48
|
-
' @param {object} scope - Plugin instance (m)
|
|
49
|
-
' @param {object} widget - Widget being mounted
|
|
50
|
-
'
|
|
51
27
|
beforeMount: sub(scope as object, widget as object)
|
|
52
28
|
scope.setFontAttribute(widget)
|
|
53
29
|
end sub,
|
|
54
30
|
|
|
55
|
-
' ---------------------------------------------------------------------
|
|
56
|
-
' beforeUpdate - Updates font style when widget config changes
|
|
57
|
-
'
|
|
58
|
-
' Re-evaluates and applies font style when fontStyle property changes.
|
|
59
|
-
'
|
|
60
|
-
' @param {object} scope - Plugin instance (m)
|
|
61
|
-
' @param {object} widget - Widget being updated
|
|
62
|
-
' @param {dynamic} newValue - New font style configuration
|
|
63
|
-
' @param {dynamic} oldValue - Previous font style configuration
|
|
64
|
-
'
|
|
65
31
|
beforeUpdate: sub(scope as object, widget as object, newValue, oldValue)
|
|
66
32
|
widget[scope.pluginKey] = newValue ?? ""
|
|
67
33
|
scope.setFontAttribute(widget)
|
|
68
34
|
end sub
|
|
69
35
|
}
|
|
70
36
|
|
|
71
|
-
' =============================================================
|
|
72
|
-
' FONT STYLE PROCESSING
|
|
73
|
-
' =============================================================
|
|
74
|
-
|
|
75
|
-
' ---------------------------------------------------------------------
|
|
76
|
-
' setFontAttribute - Evaluates and applies font style to Label node
|
|
77
|
-
'
|
|
78
|
-
' Processing logic:
|
|
79
|
-
' 1. Checks if widget is a Label node (only Labels support font styles)
|
|
80
|
-
' 2. Resolves font style value through function evaluation or interpolation
|
|
81
|
-
' 3. Applies resolved font style to the node
|
|
82
|
-
'
|
|
83
|
-
' Font Style Resolution:
|
|
84
|
-
' - Functions are executed in widget scope
|
|
85
|
-
' - @-prefixed strings are interpolated from viewModelState
|
|
86
|
-
' - Direct strings are used as-is
|
|
87
|
-
'
|
|
88
|
-
' @param {object} widget - Widget instance
|
|
89
|
-
'
|
|
90
37
|
sub setFontAttribute(widget as object)
|
|
91
|
-
|
|
92
|
-
if widget.nodeType = "Label"
|
|
93
|
-
value = widget[m.pluginKey]
|
|
94
|
-
node = widget.node
|
|
95
|
-
|
|
96
|
-
' Step 1: Resolve function-based font style
|
|
97
|
-
if Rotor.Utils.isFunction(value)
|
|
98
|
-
fontStyle = Rotor.Utils.callbackScoped(value, widget)
|
|
38
|
+
if widget.nodeType <> "Label" then return
|
|
99
39
|
|
|
100
|
-
|
|
101
|
-
else if Rotor.Utils.isString(value)
|
|
102
|
-
results = m.configRegex.MatchAll(value)
|
|
40
|
+
value = widget[m.pluginKey]
|
|
103
41
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
' Determine source based on operator
|
|
110
|
-
if sourceTypeOperator = "@"
|
|
111
|
-
source = widget.viewModelState
|
|
112
|
-
else
|
|
113
|
-
source = widget
|
|
114
|
-
end if
|
|
115
|
-
|
|
116
|
-
if source <> invalid
|
|
117
|
-
' Resolve font style from key path
|
|
118
|
-
assetValue = Rotor.Utils.getValueByKeyPath(source, matchKey)
|
|
42
|
+
' Evaluate function if fontStyle is a function
|
|
43
|
+
if Rotor.Utils.isFunction(value)
|
|
44
|
+
value = Rotor.Utils.callbackScoped(value, widget)
|
|
45
|
+
end if
|
|
119
46
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
47
|
+
if type(value) <> "roAssociativeArray" then return
|
|
48
|
+
|
|
49
|
+
' Resolve @ operators in properties
|
|
50
|
+
fontStyle = {}
|
|
51
|
+
for each key in value
|
|
52
|
+
propValue = value[key]
|
|
53
|
+
if Rotor.Utils.isString(propValue)
|
|
54
|
+
if propValue.StartsWith("@l10n.")
|
|
55
|
+
' @l10n.fonts.regular -> resolve from viewModelState.l10n.fonts.regular
|
|
56
|
+
fontStyle[key] = Rotor.Utils.getValueByKeyPath(widget.viewModelState.l10n, propValue.Mid(6))
|
|
57
|
+
else if propValue.StartsWith("@")
|
|
58
|
+
' @key.path -> resolve from viewModelState
|
|
59
|
+
fontStyle[key] = Rotor.Utils.getValueByKeyPath(widget.viewModelState, propValue.Mid(1))
|
|
60
|
+
else
|
|
61
|
+
fontStyle[key] = propValue
|
|
132
62
|
end if
|
|
133
|
-
|
|
134
|
-
fontStyle = value
|
|
135
|
-
|
|
136
|
-
' Step 3: Direct value assignment
|
|
137
63
|
else
|
|
138
|
-
fontStyle =
|
|
64
|
+
fontStyle[key] = propValue
|
|
139
65
|
end if
|
|
66
|
+
end for
|
|
140
67
|
|
|
141
|
-
|
|
142
|
-
Rotor.Utils.setFontAttribute(node, fontStyle)
|
|
143
|
-
end if
|
|
68
|
+
Rotor.Utils.setFontAttribute(widget.node, fontStyle)
|
|
144
69
|
end sub
|
|
145
70
|
|
|
146
71
|
end class
|