Newer
Older
using bessw.Unity.WebView;
using bessw.Unity.WebView.ChromeDevTools;
using bessw.Unity.WebView.ChromeDevTools.Protocol.DOM;
using MoreLinq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
Björn Eßwein
committed
using System.IO;
Björn Eßwein
committed
using System.Text.RegularExpressions;
Björn Eßwein
committed
public class WebViewController : ScrollView
{
private WebViewComponent webViewComponent;
private DomNodeWrapper[] dropzones;
private void Awake()
{
webViewComponent = GetComponent<WebViewComponent>();
WebViewComponent.serializerSettings.Converters.Add(new FactObjectUIConverter());
webViewComponent.targetUrl = Path.Join(CommunicationEvents.Get_DataPath(), "../ScrollView_Server/scrollView.html");
webViewComponent.OnWebViewComponentReady += OnWebViewComponentReady;
// TODO: handle webViewComponent.onDomDocumentUpdated
}
private void OnWebViewComponentReady()
{
webViewComponent.onDomDocumentUpdated += DocumentUpdatedHandler;
SwitchScrollUI.activeScrollData.OnScrollChanged.AddListener(SetScrollContent);
SwitchScrollUI.activeScrollData.OnScrollDynamicInfoUpdated.AddListener(SetScrollContent);
SwitchScrollUI.activeScrollData.HintAvailableEvent.AddListener(OnHintAvailable);
webViewComponent.tab.AddJSBinding("applyScroll", ApplyScrollHandler);
webViewComponent.tab.AddJSBinding("getHint", GetHintHandler);
baletiballo
committed
// webViewComponent.tab.AddJSBinding("updateAssignments", UpdateAssignmentsHandler); // The browser can just ask the MMTServer directly
Björn Eßwein
committed
if (SwitchScrollUI.activeScrollData.Scroll is not null)
{
baletiballo
committed
SetScrollContent(SwitchScrollUI.activeScrollData.RenderedScroll);
Björn Eßwein
committed
}
RegisterBrowserEventhandlers();
}
private void OnDisable()
{
webViewComponent.onDomDocumentUpdated -= DocumentUpdatedHandler;
SwitchScrollUI.activeScrollData.OnScrollChanged.RemoveListener(SetScrollContent);
SwitchScrollUI.activeScrollData.OnScrollDynamicInfoUpdated.RemoveListener(SetScrollContent);
webViewComponent.tab.RemoveJSBinding("applyScroll");
webViewComponent.tab.RemoveJSBinding("getHint");
DeRegisterBrowserEventhandlers();
}
private void DocumentUpdatedHandler(documentUpdatedEvent _)
{
// all old DomNodeWrapper objects are invalid, because the whole document has been updated
dropzones = null;
RegisterBrowserEventhandlers();
}
private async void RegisterBrowserEventhandlers()
{
// register js event handlers in the browser
Björn Eßwein
committed
_ = webViewComponent.tab.Evaluate("addDropZoneEventListeners()");
// register c# event handlers
// get the dropzones from the scroll and add the event handler to track when something is dropped
Björn Eßwein
committed
DomNodeWrapper document = await webViewComponent.tab.Document;
dropzones = await document.querySelectorAllAsync("[dropzone='copy']");
dropzones.ForEach(dropzone => dropzone.OnAttributeModified += DropzoneAttributeModifiedHandler);
}
private void DeRegisterBrowserEventhandlers()
{
dropzones?.ForEach(dropzone => dropzone.OnAttributeModified -= DropzoneAttributeModifiedHandler);
baletiballo
committed
/// <para>
/// Hand the ScrollView the (partially applied) scroll, and request it to rerender the description.
baletiballo
committed
/// </para>
/// <param name="scroll">The (partially applied) scroll to be rendered</param>
private void SetScrollContent(Scroll scroll)
// If the description is plaintext, create a generic MathML version
if (!Regex.IsMatch(scroll.description, ".*<scroll-description.*"))
Björn Eßwein
committed
{
scroll.description = CreateGenericScrollDescription(scroll);
ForwardDataToScrollView("scrollDynamic", scroll);
baletiballo
committed
_ = webViewComponent.tab.Evaluate("RenderScroll()");
Debug.Log("Called for scrollView to rerender");
baletiballo
committed
// Update the interactive components
dropzones = null;
RegisterBrowserEventhandlers();
/// <summary>
/// Create a MathML description for a scroll without one.
/// Obviously cannot be very fancy.
/// </summary>
/// <param name="scroll">The rendered scroll to create a description for</param>
/// <returns>A fitting scroll-description element as a string</returns>
private string CreateGenericScrollDescription(Scroll scroll)
{
// scroll is a legacy plain text scroll, generate html with slots for the required facts
var factSlots = SwitchScrollUI.activeScrollData.Assignments
.Where(pair => pair.Value.IsVisible)
.Select(pair =>
@$"<span class='legacySlot' dropzone='copy' data-slot-id='{pair.Key}' {(pair.Value.IsSet ? $"data-fact-id='{pair.Value.fact.Id}'" : "")}>
{(pair.Value.IsSet ?
pair.Value.fact.GetLabel(StageStatic.stage.factState) :
scroll.requiredFacts.Find(fact => fact.@ref.uri == pair.Key).label)}
</span>");
return $"<scroll-description><p>{scroll.description}</p><div id='legacySlots'>{String.Join("", factSlots)}</div></scroll-description>";
}
private async void DropzoneAttributeModifiedHandler(attributeModifiedEvent attributeModifiedEvent)
//Debug.Log($"dropzoneAttributeModifiedHandler: '{attributeModifiedEvent.name}'");
// call the onFactAssignment event if the data-fact-id attribute was modified
if (attributeModifiedEvent.name == "data-fact-id")
{
// get the slot id
var node = await webViewComponent.tab.GetNode(attributeModifiedEvent.nodeId);
if (!(node.Node.attributes.TryGetValue("data-slot-id", out string slot)
|| (await node.getAttributesAsync()).TryGetValue("data-slot-id", out slot)))
{
Debug.LogError($"dropzoneAttributeModifiedHandler: data-slot-id attribute not found on dropzone");
throw new Exception("data-slot-id attribute not found on dropzone");
}
Björn Eßwein
committed
if (attributeModifiedEvent.value == "null")
{
// remove fact from slot
SwitchScrollUI.activeScrollData.AssignFact(slot, null);
return;
}
// get the fact from the fact id
if (!FactRecorder.AllFacts.TryGetValue(attributeModifiedEvent.value, out Fact fact))
{
Debug.LogError($"dropzoneAttributeModifiedHandler: fact with id '{attributeModifiedEvent.value}' not found");
throw new Exception($"fact with id '{attributeModifiedEvent.value}' not found");
}
// assign fact to slot
SwitchScrollUI.activeScrollData.AssignFact(slot, fact);
private async void OnHintAvailable(IReadOnlyList<string> url)
{
dropzones = await (await webViewComponent.tab.Document).querySelectorAllAsync("[dropzone='copy']");
foreach (var dropzone in dropzones)
{
if (url.Contains(dropzone.Node.attributes["data-slot-id"]))
{
_ = dropzone.setAttributeValue("data-hint-available", "true");
}
else if (dropzone.Node.attributes.ContainsKey("data-hint-available"))
{
_ = dropzone.removeAttributeAsync("data-hint-available");
}
}
}
private void ApplyScrollHandler(string button)
{
SwitchScrollUI.activeScrollData.ButtonClicked(new ApplyScrollButton());
}
private void GetHintHandler(string url)
{
SwitchScrollUI.activeScrollData.ButtonClicked(new HintScrollButton(url));
}
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/// <summary>
/// Send data to the ScrollView.
/// <para>JSON serialize the <paramref name="dataValue"/> and add it to the dataset of the Unity-Data-Interface Node. JS can then retrieve it by calling
/// <code>const unityData = JSON.parse(document.querySelector("#Unity-Data-Interface").dataset.<paramref name="dataID"/>);</code>
/// </para>
/// </summary>
/// <param name="dataID">The name of the dataset-member <paramref name="dataValue"/> will be assigned to</param>
/// <param name="dataValue">The data to transmit</param>
public async void ForwardDataToScrollView(string dataID, object dataValue)
{
string payload = JsonConvert.SerializeObject(dataValue);
//payload = WebUtility.HtmlEncode(payload);
// Using the DomNodeWrapper methods requires 3 requests, and since those go to an altogether
// different prozess, I would like to avoid that.
await webViewComponent.tab.Evaluate(
$"document.querySelector('#Unity-Data-Interface').dataset.{dataID} = '{payload}'"
);
}
public string[] GetFactAssignments()
{
return dropzones.Select(dropzone => dropzone.Node.attributes.GetValueOrDefault("data-fact-id", null)).ToArray();
}
#region ObsoleteFunctions : Functions that are no longer used. Since similar functionality may be of use sometime later, they are here for reference.
baletiballo
committed
/// <summary>
/// <para>
/// Hand the ScrollView the labels (and potentially assignments) of all facts, so it can render the scroll.
/// </para><para>
/// More precisely: Put a stringified and Json serialized Dictionary
/// <see cref="OMS.uri"/> slotID -> (<see cref="string"/> label, <see cref="Fact.Id"/> factId) <br/>
/// into the "data-assignments" attribute of the "assignments" node
/// </para>
/// </summary>
/// <param name="youHappyNowJS">Is ignored. JS bindings need to take a string argument, so here it is.</param>
[Obsolete("This data needs to be updated iff the assignment is changed in Unity, JS doesn't need a way to to request it independently.")]
private void UpdateAssignmentsHandler(string youHappyNowJS = "")
baletiballo
committed
{
Scroll scroll = SwitchScrollUI.activeScrollData.RenderedScroll;
Dictionary<string, (string, string)> assignments = new();
foreach (MMTFact slot in scroll.requiredFacts)
baletiballo
committed
{
string slotId = slot.@ref.uri;
ActiveScroll.SlotAssignment assignment = SwitchScrollUI.activeScrollData.Assignments[slotId];
if (assignment.IsSet)
{
Fact fact = assignment.fact;
assignments.Add(slotId, (fact.GetLabel(StageStatic.stage.factState), fact.Id));
}
else
{
assignments.Add(slotId, (slot.label, ""));
}
baletiballo
committed
}
foreach (MMTFact fact in scroll.acquiredFacts)
baletiballo
committed
{
assignments.Add(fact.@ref.uri, (fact.label, ""));
}
ForwardDataToScrollView("assignments", JsonConvert.SerializeObject(assignments));
baletiballo
committed
}
[Obsolete("Is done by JS. How to render the scroll is the scrolls buisness.")]
private string createCustomScrollDescription(Scroll scroll)
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
string description = scroll.description;
// replace slot templates
//the scroll-slot syntax has changed by now, so the match would have to be adapted.
description = Regex.Replace(description, "<scroll-slot([^>]*)>([^<]*)</scroll-slot>", match =>
{
var extraAttributes = match.Groups[1].Value;
var slotId = match.Groups[2].Value;
var assignment = SwitchScrollUI.activeScrollData.Assignments[slotId];
/** label of the assigned fact, or the slot label if nothing assigned */
var label = assignment.IsSet ? assignment.fact.GetLabel(StageStatic.stage.factState)
: scroll.requiredFacts.Find(fact => fact.@ref.uri == slotId).label;
/** id of the assigned fact. If nothing is assigned don't add the attribute */
var fact_id = assignment.IsSet ? $"data-fact-id='{assignment.fact.Id}'"
: "";
return $"<mi dropzone='copy' data-slot-id='{slotId}' {fact_id} {extraAttributes}>{label}</mi>";
});
// replace solution templates
description = Regex.Replace(description, "<scroll-solution([^>]*)>([^<]*)</scroll-solution>", match =>
{
var extraAttributes = match.Groups[1].Value;
var solutionId = match.Groups[2].Value;
var label = scroll.acquiredFacts.Find(fact => fact.@ref.uri == solutionId).label;
return $"<mi data-solution-id='{solutionId}' {extraAttributes}>{label}</mi>";
});
return description;
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
private async void GetDropzoneStateAsync()
{
// alternative way to get the dropzone state
DomNodeWrapper document = await DomNodeWrapper.getDocumentAsync(webViewComponent.tab);
Debug.LogWarning($"dropzone 1: '{document}'");
var dropzones = await document.querySelectorAllAsync("[dropzone='copy']");
Debug.LogWarning($"dropzone 2: '{dropzones.Index().Aggregate("", (currentString, dropzone) => $"{currentString}, {dropzone.Key}: {dropzone.Value.Node.attributes.GetValueOrDefault("data-fact-id")}")}'");
string[] factIDs = new string[dropzones.Length];
// get attributes for each dropzone
//for (int i = 0; i < dropzones.Length; i++)
//{
// factIDs[i] = ( await dropzones[i].getAttributesAsync() ).GetValueOrDefault("data-fact-id", null);
//}
//Debug.LogWarning($"dropzone 3: '{string.Join(", ", factIDs)}'");
}
/// <summary>
/// First try to get the dropzone state using coroutines.
/// Doesn't work because coroutines can't access method local temporary variables.
/// </summary>
/// <returns></returns>
[Obsolete("Use getDropzoneStateAsync instead")]
private IEnumerator GetDropzoneState()
{
Debug.LogWarning($"dropzone pre");
DomNodeWrapper doc = null;
yield return DomNodeWrapper.getDocument(webViewComponent.tab, (document) =>
{
Debug.LogWarning($"dropzone 1: '{document}'");
doc = document;
StartCoroutine(document.querySelectorAll("[dropzone='copy']", (dropzones) =>
{
foreach (var dropzone in dropzones)
{
Debug.LogWarning($"dropzone 2: Node is Null?: '{dropzone.Node == null}'");
StartCoroutine(dropzone.getAttributes((attributes) =>
{
Debug.LogWarning($"dropzone 3 getAttributes: '{string.Join(", ", attributes.Values)}'");
}));
}
}));
});
Debug.LogWarning($"dropzone post: '{doc}'");
}
#endregion ObsoleteFunctions
public class FactObjectUIConverter : JsonConverter<FactObjectUI>
{
public override void WriteJson(JsonWriter writer, FactObjectUI value, JsonSerializer serializer)
{
//serializer.Serialize(writer, value.Fact);
JObject o = JObject.FromObject(value.Fact, WebViewComponent.serializer);
o.AddFirst(new JProperty("id", value.Fact.Id));
o.WriteTo(writer);
}
public override FactObjectUI ReadJson(JsonReader reader, Type objectType, FactObjectUI existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var factObject = new FactObjectUI();
factObject.Fact = serializer.Deserialize<Fact>(reader);
return factObject;
}
}