Skip to content
Snippets Groups Projects
WebViewController.cs 15.3 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;
    
    public class WebViewController : ScrollView
    
    {
        private WebViewComponent webViewComponent;
        private DomNodeWrapper[] dropzones;
    
        private void Awake()
        {
            webViewComponent = GetComponent<WebViewComponent>();
            WebViewComponent.serializerSettings.Converters.Add(new FactObjectUIConverter());
    
            webViewComponent.targetUrl = ("http://localhost:5173/scrollView"); //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);
    
            // 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);
    
        /// Hand the ScrollView the (partially applied) scroll, and request it to rerender the description. 
    
        /// <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.*"))
    
                scroll.description = CreateGenericScrollDescription(scroll);
    
            ForwardDataToScrollView("scrollDynamic", scroll);
    
            _ = webViewComponent.tab.Evaluate("RenderScroll()");
    
            Debug.Log("Called for scrollView to rerender");
    
            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)))
                {
    
    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>
        /// 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.
    
    
    
        /// <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 = "")
    
        {
            Scroll scroll = SwitchScrollUI.activeScrollData.RenderedScroll;
    
            Dictionary<string, (string, string)> assignments = new();
    
            foreach (MMTFact slot in scroll.requiredFacts)
    
                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, ""));
                }
    
            foreach (MMTFact fact in scroll.acquiredFacts)
    
            ForwardDataToScrollView("assignments", JsonConvert.SerializeObject(assignments));
    
        [Obsolete("Is done by JS. How to render the scroll is the scrolls buisness.")]
        private string createCustomScrollDescription(Scroll scroll)
    
            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;
    
        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;
        }
    }