Skip to content
Snippets Groups Projects
WebViewController.cs 15.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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 REST_JSON_API;
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    
    using System.Linq;
    
    using UnityEngine;
    
    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.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);
    
            // webViewComponent.tab.AddJSBinding("updateAssignments", UpdateAssignmentsHandler); // The browser can just ask the MMTServer directly
    
            if (SwitchScrollUI.activeScrollData.Scroll is not null)
            {
    
                SetScrollContent(SwitchScrollUI.activeScrollData.RenderedScroll);
    
            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
    
            _ = 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
    
            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);
    
        /// <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>
    
        /// <param name="scroll"></param>
    
        private async void SetScrollContent(Scroll scroll)
    
        {
            // update scroll container content
    
            DomNodeWrapper document = await webViewComponent.tab.Document;
    
            DomNodeWrapper scrollContainer = await document.querySelectorAsync("#scrollContainer"); 
    
            var description = scroll.description;
    
            if (!Regex.IsMatch(description, ".*<scroll-description.*"))
    
    Björn Eßwein's avatar
    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 =>
    
    Björn Eßwein's avatar
    Björn Eßwein committed
                        @$"<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)}
    
    Björn Eßwein's avatar
    Björn Eßwein committed
                description = $"<scroll-description><p>{scroll.description}</p><div id='legacySlots'>{String.Join("", factSlots)}</div></scroll-description>";
    
            //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>");
    
    
    
            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)))
                {
    
    Björn Eßwein's avatar
    Björn Eßwein committed
                    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's avatar
    Björn Eßwein committed
                // if fact has been unassigned from the scroll
    
                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))
                {
    
    Björn Eßwein's avatar
    Björn Eßwein committed
                    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)
        {
    
    Björn Eßwein's avatar
    Björn Eßwein committed
            SwitchScrollUI.activeScrollData.ButtonClicked(new ApplyScrollButton());
    
        }
    
        private void GetHintHandler(string url)
        {
            SwitchScrollUI.activeScrollData.ButtonClicked(new HintScrollButton(url));
        }
    
    
        /// <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();
        }
    
    
        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;
        }
    }