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;
baletiballo
committed
using UnityEngine.UIElements;
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());
Björn Eßwein
committed
webViewComponent.targetUrl = Path.Join(CommunicationEvents.Get_DataPath(), "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 labels (and potentially assignments) of all facts, so it can render the scroll content. (And then tell it to do so)
/// </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>
private async void SetScrollContent(Scroll scroll)
{
// update scroll container content
Björn Eßwein
committed
DomNodeWrapper document = await webViewComponent.tab.Document;
baletiballo
committed
DomNodeWrapper scrollContainer = await document.querySelectorAsync("#scrollContainer");
var description = scroll.description;
baletiballo
committed
if (!Regex.IsMatch(description, ".*<scroll-description.*"))
Björn Eßwein
committed
{
// 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) :
baletiballo
committed
scroll.requiredFacts.Find(fact => fact.@ref.uri == pair.Key).label)}
description = $"<scroll-description><p>{scroll.description}</p><div id='legacySlots'>{String.Join("", factSlots)}</div></scroll-description>";
baletiballo
committed
baletiballo
committed
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//else // Is done by JS. How to render the scroll is the scrolls buisness
//{
// // replace slot templates
// 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>";
// });
//}
// Display the scroll description.
await scrollContainer.setOuterHtmlAsync($"<div id='scrollContainer'>{description}</div>");
baletiballo
committed
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
DomNodeWrapper assignmentsNode = await document.querySelectorAsync("#assignments");
Dictionary<string, (string, string)> assignments = new();
foreach (MMTFact fact in scroll.requiredFacts)
{
string slotId = fact.@ref.uri;
ActiveScroll.SlotAssignment assignment = SwitchScrollUI.activeScrollData.Assignments[slotId];
if (assignment.IsSet)
{
assignments.Add(slotId, (assignment.fact.GetLabel(StageStatic.stage.factState), assignment.fact.Id));
}
else
{
assignments.Add(slotId, (fact.label, ""));
}
}
foreach (MMTFact fact in scroll.acquiredFacts)
{
assignments.Add(fact.@ref.uri, (fact.label, ""));
}
await assignmentsNode.setAttributeValue("data-assignments", JsonConvert.SerializeObject(assignments));
_ = webViewComponent.tab.Evaluate("RenderScroll()");
// Update the interactive components
dropzones = null;
RegisterBrowserEventhandlers();
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));
}
baletiballo
committed
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/// <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 can be aquired directly from the MMTServer")]
private async void UpdateAssignmentsHandler(string youHappyNowJS = "")
{
DomNodeWrapper document = await webViewComponent.tab.Document;
DomNodeWrapper assignmentsNode = await document.querySelectorAsync("#assignments");
Scroll scroll = SwitchScrollUI.activeScrollData.RenderedScroll;
Dictionary<string, (string,string)> assignments = new();
foreach (var (slotId,assignment) in SwitchScrollUI.activeScrollData.Assignments)
{
string 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 */
string fact_id = assignment.IsSet ? assignment.fact.Id : "";
assignments.Add(slotId,(label,fact_id));
}
foreach(MMTFact fact in scroll.acquiredFacts)
{
string label = scroll.acquiredFacts.Find(f => fact.@ref.uri == f.@ref.uri).label;
Debug.Log(label);
Debug.Log(fact.label);
assignments.Add(fact.@ref.uri, (fact.label, ""));
}
await assignmentsNode.setAttributeValue("data-assignments", JsonConvert.SerializeObject(assignments));
}
public string[] GetFactAssignments()
{
return dropzones.Select(dropzone => dropzone.Node.attributes.GetValueOrDefault("data-fact-id", null)).ToArray();
}
275
276
277
278
279
280
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
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}'");
}
}
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;
}
}