@Christoph-Hart
This is my entire class. It works great. I have a value change broadcaster for all my controls, and it checks for the condition (key modifiers) before setting a canPush variable for the component.
If that's true, it simply calls pushValueChange which stores the JSON file. If crosstalk is enabled in an instance, it has a listener timer that checks the appdata folder for new files. I described the properties in the previous post.
The setting of groupId isn't implemented, but it would likely be an argument of the pushValueChange function.
That's pretty much the only user facing method needed: sending the data and being able to specify targets (groups).
If string operations are an issue, you can add a method setInstanceGroupName or something like that that will take in a string or an int, and if it's a string, it'll convert it to an int code. This would be called when the user is doing label edits in the GUI for naming the instance.
namespace Crosstalk
{
// Crosstalk system using timers and a local JSON file
// Instance ID is randomly generated using system time and randomization
// Instances check themselves in by creating a file with their id and stamping it with signin time
// They continuously update the file to let everyone know they're still here
// After 5 seconds of inactivity, the file gets deleted
// ===========================================================================================
// System time + random 0-999 int
inline function generateInstanceId()
{
return ((Date.getSystemTimeMs()*1000) + Math.randInt(0, 999));
}
reg i = 0;
reg obj = 0;
reg enabled = true;
const var instanceId = generateInstanceId();
const var groupId = "";
const var TIMEOUT_MS = 1600;
const var LISTENER_MS = 400;
const var CLEANUP_MS = 1000;
const var lastExecutedInstruction = {};
// the main directory for crosstalk stuff
const var DIR = FileSystem.getFolder(FileSystem.AppData).createDirectory("Sharing");
// ===========================================================================================
// Write id and time to a check in file used for checking if instance is online
inline function checkIn()
{
local f = DIR.getChildFile("id_" + instanceId);
f.writeObject(
{
"id": instanceId,
"group": groupId,
"time": Date.getSystemTimeMs(),
});
if (UI.DEBUG_CROSSTALK) Console.print(instanceId + "checking in... || " + Date.getSystemTimeISO8601(true));
return f;
}
// Get all check-in files from the directory
inline function getInstances()
{
return FileSystem.findFiles(DIR, "id_*", true);
}
// Get all value changes from the directory
inline function getValueChanges()
{
return FileSystem.findFiles(DIR, "vc_*", true);
}
// Check write time for each file and clean up
inline function cleanUp(fileList)
{
local time = Date.getSystemTimeMs();
local obj = 0;
for (x in fileList)
{
obj = x.loadAsObject();
if (obj.id != instanceId) // obviosuly this one is active
{
if (Math.abs(time - obj.time) > TIMEOUT_MS)
{
x.deleteFileOrDirectory();
if (UI.DEBUG_CROSSTALK) Console.print("Deleted: " + obj.id);
}
}
}
}
// Push changes for crosstalking
// Call this in control callbacks, broadcasters, etc.
inline function pushValueChange(componentId, value)
{
local f = DIR.getChildFile("vc_" + componentId);
f.writeObject(
{
"sentBy": instanceId,
"targetGroup": groupId,
"time": Date.getSystemTimeMs(),
"componentId": componentId,
"value": value,
"changeId": generateInstanceId(),
});
}
// Find and evaluate value change files
inline function pullAllValueChanges()
{
local list = FileSystem.findFiles(DIR, "vc_*", true);
local obj = {};
local component = 0;
for (x in list)
{
obj = x.loadAsObject();
component = Content.getComponent(obj.componentId);
if (obj.sentBy != instanceId && obj.value != component.getValue() && groupId == obj.targetGroup && obj.changeId != lastExecutedInstruction[obj.componentId])
{
if (isDefined(Animation.valueChangeAnimators[obj.componentId]))
{
Animation.startValueChangeAnimator(component, obj.value);
}
else
{
component.setValue(obj.value);
component.changed();
}
lastExecutedInstruction[obj.componentId] = obj.changeId;
}
}
}
// ===========================================================================================
// create a file with the instance id and sign in time
// write it once on startup
checkIn();
cleanUp(getInstances()); // clean up old files on launch
// ===========================================================================================
// Timer that loads the shared data file periodically and executes the crosstalk logic
const var listener = Engine.createTimerObject();
listener.setTimerCallback(function()
{
if (TransportHandler.isOffline == false)
{
checkIn();
pullAllValueChanges();
}
if (UI.DEBUG_CROSSTALK) Console.print(instanceId + ": Listening... || " + Date.getSystemTimeISO8601(true));
});
//listener.startTimer(LISTENER_MS);
// Timer that cleans up inactive check-in files
const var cleaner = Engine.createTimerObject();
cleaner.setTimerCallback(function()
{
if (TransportHandler.isOffline == false)
{
cleanUp(getInstances());
cleanUp(getValueChanges());
}
if (UI.DEBUG_CROSSTALK) Console.print(instanceId + ": Cleanup... || " + Date.getSystemTimeISO8601(true));
});
//cleaner.startTimer(CLEANUP_MS);
// ===========================================================================================
// Transport and Crosstalk interaction
inline function setTimersOnTransport(playing)
{
if (playing)
{
listener.stopTimer();
cleaner.stopTimer();
}
else if (enabled)
{
listener.startTimer(LISTENER_MS);
cleaner.startTimer(CLEANUP_MS);
}
};
TransportHandler.transportBroadcaster.addListener(TransportHandler.transportHandler, "Start/Stop Crosstalk Timers on Transport", setTimersOnTransport);
// ===========================================================================================
// Call this when enabling or disabling crosstalk.
inline function enableCrosstalk(shouldBeEnabled)
{
enabled = shouldBeEnabled;
if (shouldBeEnabled)
{
cleanUp(getInstances());
cleanUp(getValueChanges());
checkIn();
listener.startTimer(LISTENER_MS);
cleaner.startTimer(CLEANUP_MS);
}
else
{
listener.stopTimer();
cleaner.stopTimer();
}
}
// ===========================================================================================
// Store Key Modifiers for Execution Evaluation
// Store modifiers with the broadcaster. Attach to component in control helpers.
// In the Value Change Broadcaster for Controls, check for key modifier combination before pushing value to crosstalk
const var mouseStatus = {};
const var keyModifiers = {};
const var canPush = {};
// Broadcaster definition
const var mouseBroadcaster = Engine.createBroadcaster({
"id": "mouseBroadcaster",
"args": ["component", "event"],
"tags": [],
"comment": "Store key modifier status for evaluating crosstalk execution."
});
mouseBroadcaster.addListener(keyModifiers, "Store Modifiers", function(component, event)
{
if ((event.clicked || event.drag) && !event.rightClick && event.altDown && (event.ctrlDown || event.cmdDown))
{
mouseStatus[component.get("id")] = true;
canPush[component.get("id")] = true;
}
else if (event.mouseUp)
{
mouseStatus[component.get("id")] = false;
Content.callAfterDelay(100, function[component]()
{
if (mouseStatus[component.get("id")] == false) canPush[component.get("id")] = false; // because value change gets executed after mouseUp
});
}
});
inline function attachToKeyModifierBroadcaster(component)
{
mouseBroadcaster.attachToComponentMouseEvents(component, "Clicks, Hover & Dragging", "");
}
// ===========================================================================================
}