Browse Source

Merge pull request #68 from AtomicGameEngine/TSH-ATOMIC-EDITOREXT

Project based custom editors
JoshEngebretson 9 years ago
parent
commit
ecf9d99d2b

+ 63 - 0
EditorPluginExample/Resources/EditorData/JSExample.plugin.js

@@ -60,6 +60,66 @@ function showInfobox(title, msg) {
     msgLabel.text = msg;
     msgLabel.text = msg;
 }
 }
 
 
+/**
+ * Full path is the fully qualified path from the root of the filesystem.  In order to take advantage
+ * of the resource caching system, let's trim it down to just the path inside the resources directory
+ * including the Resources directory so that the casing is correct
+ */
+function getNormalizedPath(path) {
+    var RESOURCES_MARKER = "resources/";
+    return path.substring(path.toLowerCase().indexOf(RESOURCES_MARKER));
+}
+
+var CustomEditorBuilder = { }
+
+/**
+ * Returns true if this builder can generate an editor for this resource type
+ */
+CustomEditorBuilder.canHandleResource = function(resourcePath) {
+    return resourcePath.indexOf("custom.editorjs.json") > 0;
+}
+
+/**
+ * Generates a resource editor for the provided resource type
+ * @param  resourcePath
+ * @param  tabContainer
+ */
+CustomEditorBuilder.getEditor = function(resourceFrame, resourcePath, tabContainer) {
+
+    // point to a custom page
+    var editorUrl = "atomic://" + ToolCore.toolSystem.project.resourcePath + "EditorData/customEditor.html";
+    var editor = new Editor.JSResourceEditor(resourcePath, tabContainer, editorUrl);
+
+    // one time subscriptions waiting for the web view to finish loading.  This event
+    // actually hits the editor instance before we can hook it, so listen to it on the
+    // frame and then unhook it
+    editor.subscribeToEvent("WebViewLoadEnd", function(data) {
+        editor.unsubscribeFromEvent("WebViewLoadEnd");
+
+        var webClient = editor.webView.webClient;
+        webClient.executeJavaScript('HOST_loadCode("atomic://' + getNormalizedPath(editor.fullPath) + '");');
+    });
+
+    editor.subscribeToEvent("DeleteResourceNotification", function(data){
+        var webClient = editor.webView.webClient;
+        webClient.executeJavaScript('HOST_resourceDeleted("atomic://' + getNormalizedPath(data.path) + '");');
+    });
+
+    editor.subscribeToEvent("UserPreferencesChangedNotification", function(data) {
+        var prefsPath = ToolCore.toolSystem.project.userPrefsFullPath;
+        if (Atomic.fileSystem.fileExists(prefsPath)) {
+            // Get a reference to the web client so we can call the load preferences method
+            var webClient = editor.webView.webClient;
+            webClient.executeJavaScript('HOST_loadPreferences("atomic://' + prefsPath + '");');
+        }
+    });
+
+    return editor;
+}
+
+
+
+
 // Definition of the plugin
 // Definition of the plugin
 var JSExamplePlugin = {
 var JSExamplePlugin = {
     name: "JSExamplePlugin",
     name: "JSExamplePlugin",
@@ -80,6 +140,7 @@ JSExamplePlugin.projectUnloaded = function() {
     serviceLocator.uiServices.removeProjectContextMenuItemSource(ExamplePluginUILabel);
     serviceLocator.uiServices.removeProjectContextMenuItemSource(ExamplePluginUILabel);
     serviceLocator.uiServices.removeHierarchyContextMenuItemSource(ExamplePluginUILabel);
     serviceLocator.uiServices.removeHierarchyContextMenuItemSource(ExamplePluginUILabel);
     serviceLocator.uiServices.removePluginMenuItemSource(ExamplePluginUILabel);
     serviceLocator.uiServices.removePluginMenuItemSource(ExamplePluginUILabel);
+    serviceLocator.uiServices.unregisterCustomEditor(CustomEditorBuilder);
 
 
     console.log("JSExamplePluginService.projectUnloaded");
     console.log("JSExamplePluginService.projectUnloaded");
     if (serviceLocator) {
     if (serviceLocator) {
@@ -100,6 +161,8 @@ JSExamplePlugin.projectLoaded = function(ev) {
         "Get name": ["jsexampleplugin project context"]
         "Get name": ["jsexampleplugin project context"]
     });
     });
 
 
+    serviceLocator.uiServices.registerCustomEditor(CustomEditorBuilder);
+
     totalUses = serviceLocator.projectServices.getUserPreference("JSExamplePlugin", "UsageCount", 0);
     totalUses = serviceLocator.projectServices.getUserPreference("JSExamplePlugin", "UsageCount", 0);
 };
 };
 
 

+ 37 - 1
EditorPluginExample/Resources/EditorData/TSExample.plugin.js

@@ -1,8 +1,42 @@
-/// <reference path="../../typings/Atomic/Atomic.d.ts" />
 "use strict";
 "use strict";
 var ExamplePluginUILabel = "TS Example Plugin";
 var ExamplePluginUILabel = "TS Example Plugin";
 var ExamplePluginTBPath = "EditorData/Example.tb.txt";
 var ExamplePluginTBPath = "EditorData/Example.tb.txt";
 var InfoboxTBPath = "EditorData/Infobox.tb.txt";
 var InfoboxTBPath = "EditorData/Infobox.tb.txt";
+var CustomEditorBuilder = (function () {
+    function CustomEditorBuilder() {
+    }
+    CustomEditorBuilder.prototype.canHandleResource = function (resourcePath) {
+        return resourcePath.indexOf("custom.editor.json") > 0;
+    };
+    CustomEditorBuilder.prototype.getNormalizedPath = function (path) {
+        var RESOURCES_MARKER = "resources/";
+        return path.substring(path.toLowerCase().indexOf(RESOURCES_MARKER));
+    };
+    CustomEditorBuilder.prototype.getEditor = function (resourceFrame, resourcePath, tabContainer) {
+        var _this = this;
+        var editorUrl = "atomic://" + ToolCore.toolSystem.project.resourcePath + "EditorData/customEditor.html";
+        var editor = new Editor.JSResourceEditor(resourcePath, tabContainer, editorUrl);
+        editor.subscribeToEvent("WebViewLoadEnd", function (data) {
+            editor.unsubscribeFromEvent("WebViewLoadEnd");
+            var webClient = editor.webView.webClient;
+            webClient.executeJavaScript("HOST_loadCode(\"atomic://" + _this.getNormalizedPath(editor.fullPath) + "\");");
+        });
+        editor.subscribeToEvent("DeleteResourceNotification", function (data) {
+            var webClient = editor.webView.webClient;
+            webClient.executeJavaScript("HOST_resourceDeleted(\"atomic://" + _this.getNormalizedPath(data.path) + "\");");
+        });
+        editor.subscribeToEvent("UserPreferencesChangedNotification", function (data) {
+            var prefsPath = ToolCore.toolSystem.project.userPrefsFullPath;
+            if (Atomic.fileSystem.fileExists(prefsPath)) {
+                var webClient = editor.webView.webClient;
+                webClient.executeJavaScript("HOST_loadPreferences(\"atomic://" + prefsPath + "\");");
+            }
+        });
+        return editor;
+    };
+    return CustomEditorBuilder;
+}());
+var customEditorBuilder = new CustomEditorBuilder();
 var TSExamplePluginService = (function () {
 var TSExamplePluginService = (function () {
     function TSExamplePluginService() {
     function TSExamplePluginService() {
         var _this = this;
         var _this = this;
@@ -43,6 +77,7 @@ var TSExamplePluginService = (function () {
         this.serviceLocator.uiServices.removeProjectContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removeProjectContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removeHierarchyContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removeHierarchyContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removePluginMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removePluginMenuItemSource(ExamplePluginUILabel);
+        this.serviceLocator.uiServices.unregisterCustomEditor(customEditorBuilder);
         Atomic.print("TSExamplePluginService.projectUnloaded");
         Atomic.print("TSExamplePluginService.projectUnloaded");
         if (this.serviceLocator) {
         if (this.serviceLocator) {
             this.serviceLocator.projectServices.unregister(this);
             this.serviceLocator.projectServices.unregister(this);
@@ -55,6 +90,7 @@ var TSExamplePluginService = (function () {
         this.serviceLocator.uiServices.createHierarchyContextMenuItemSource(ExamplePluginUILabel, { "Get name": ["tsexampleplugin hierarchy context"] });
         this.serviceLocator.uiServices.createHierarchyContextMenuItemSource(ExamplePluginUILabel, { "Get name": ["tsexampleplugin hierarchy context"] });
         this.serviceLocator.uiServices.createProjectContextMenuItemSource(ExamplePluginUILabel, { "Get name": ["tsexampleplugin project context"] });
         this.serviceLocator.uiServices.createProjectContextMenuItemSource(ExamplePluginUILabel, { "Get name": ["tsexampleplugin project context"] });
         this.totalUses = this.serviceLocator.projectServices.getUserPreference(this.name, "UsageCount", 0);
         this.totalUses = this.serviceLocator.projectServices.getUserPreference(this.name, "UsageCount", 0);
+        this.serviceLocator.uiServices.registerCustomEditor(customEditorBuilder);
     };
     };
     TSExamplePluginService.prototype.playerStarted = function () {
     TSExamplePluginService.prototype.playerStarted = function () {
         Atomic.print("TSExamplePluginService.playerStarted");
         Atomic.print("TSExamplePluginService.playerStarted");

+ 61 - 0
EditorPluginExample/Resources/EditorData/TSExample.plugin.ts

@@ -4,6 +4,65 @@ const ExamplePluginUILabel = "TS Example Plugin";
 const ExamplePluginTBPath = "EditorData/Example.tb.txt";
 const ExamplePluginTBPath = "EditorData/Example.tb.txt";
 const InfoboxTBPath = "EditorData/Infobox.tb.txt";
 const InfoboxTBPath = "EditorData/Infobox.tb.txt";
 
 
+class CustomEditorBuilder implements Editor.Extensions.ResourceEditorBuilder {
+
+        /**
+         * Returns true if this builder can generate an editor for this resource type
+         */
+        canHandleResource(resourcePath: string) {
+            return resourcePath.indexOf("custom.editor.json") > 0;
+        }
+
+        /**
+         * Full path is the fully qualified path from the root of the filesystem.  In order to take advantage
+         * of the resource caching system, let's trim it down to just the path inside the resources directory
+         * including the Resources directory so that the casing is correct
+         */
+        private getNormalizedPath(path: string) {
+            const RESOURCES_MARKER = "resources/";
+            return path.substring(path.toLowerCase().indexOf(RESOURCES_MARKER));
+        }
+
+        /**
+         * Generates a resource editor for the provided resource type
+         * @param  resourcePath
+         * @param  tabContainer
+         */
+        getEditor(resourceFrame: Atomic.UIWidget, resourcePath: string, tabContainer: Atomic.UITabContainer) : Editor.ResourceEditor {
+
+            // point to a custom page
+            const editorUrl = "atomic://" + ToolCore.toolSystem.project.resourcePath + "EditorData/customEditor.html";
+            const editor = new Editor.JSResourceEditor(resourcePath, tabContainer, editorUrl);
+
+            // one time subscriptions waiting for the web view to finish loading.  This event
+            // actually hits the editor instance before we can hook it, so listen to it on the
+            // frame and then unhook it
+            editor.subscribeToEvent("WebViewLoadEnd", (data) => {
+                editor.unsubscribeFromEvent("WebViewLoadEnd");
+
+                const webClient = editor.webView.webClient;
+                webClient.executeJavaScript(`HOST_loadCode("atomic://${this.getNormalizedPath(editor.fullPath)}");`);
+            });
+
+            editor.subscribeToEvent("DeleteResourceNotification", (data) => {
+                const webClient = editor.webView.webClient;
+                webClient.executeJavaScript(`HOST_resourceDeleted("atomic://${this.getNormalizedPath(data.path)}");`);
+            });
+
+            editor.subscribeToEvent("UserPreferencesChangedNotification", (data) => {
+                let prefsPath = ToolCore.toolSystem.project.userPrefsFullPath;
+                if (Atomic.fileSystem.fileExists(prefsPath)) {
+                    // Get a reference to the web client so we can call the load preferences method
+                    const webClient = editor.webView.webClient;
+                    webClient.executeJavaScript(`HOST_loadPreferences("atomic://${prefsPath}");`);
+                }
+            });
+
+            return editor;
+        }
+}
+const customEditorBuilder = new CustomEditorBuilder();
+
 class TSExamplePluginService implements Editor.HostExtensions.HostEditorService, Editor.HostExtensions.ProjectServicesEventListener, Editor.HostExtensions.UIServicesEventListener {
 class TSExamplePluginService implements Editor.HostExtensions.HostEditorService, Editor.HostExtensions.ProjectServicesEventListener, Editor.HostExtensions.UIServicesEventListener {
 
 
     name: string = "TSExampleService";
     name: string = "TSExampleService";
@@ -31,6 +90,7 @@ class TSExamplePluginService implements Editor.HostExtensions.HostEditorService,
         this.serviceLocator.uiServices.removeProjectContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removeProjectContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removeHierarchyContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removeHierarchyContextMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removePluginMenuItemSource(ExamplePluginUILabel);
         this.serviceLocator.uiServices.removePluginMenuItemSource(ExamplePluginUILabel);
+        this.serviceLocator.uiServices.unregisterCustomEditor(customEditorBuilder);
 
 
         Atomic.print("TSExamplePluginService.projectUnloaded");
         Atomic.print("TSExamplePluginService.projectUnloaded");
         if (this.serviceLocator) {
         if (this.serviceLocator) {
@@ -44,6 +104,7 @@ class TSExamplePluginService implements Editor.HostExtensions.HostEditorService,
         this.serviceLocator.uiServices.createHierarchyContextMenuItemSource(ExamplePluginUILabel, { "Get name" : ["tsexampleplugin hierarchy context"]});
         this.serviceLocator.uiServices.createHierarchyContextMenuItemSource(ExamplePluginUILabel, { "Get name" : ["tsexampleplugin hierarchy context"]});
         this.serviceLocator.uiServices.createProjectContextMenuItemSource(ExamplePluginUILabel, { "Get name" : ["tsexampleplugin project context"]});
         this.serviceLocator.uiServices.createProjectContextMenuItemSource(ExamplePluginUILabel, { "Get name" : ["tsexampleplugin project context"]});
         this.totalUses = this.serviceLocator.projectServices.getUserPreference(this.name, "UsageCount", 0);
         this.totalUses = this.serviceLocator.projectServices.getUserPreference(this.name, "UsageCount", 0);
+        this.serviceLocator.uiServices.registerCustomEditor(customEditorBuilder);
     }
     }
     playerStarted() {
     playerStarted() {
         Atomic.print("TSExamplePluginService.playerStarted");
         Atomic.print("TSExamplePluginService.playerStarted");

+ 169 - 0
EditorPluginExample/Resources/EditorData/customEditor.html

@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title>Editor</title>
+</head>
+
+<body>
+
+    <h1>Custom Editor Instance</h1>
+    <p>This is an example of creating a custom editor. You can edit the elements below and if you hit save, it will update the source file.</p>
+
+    <div>
+        <div style="float:left">
+            <select style="width:190px;height:400px" id="npc_list" size="10"></select>
+        </div>
+        <div style="margin-left:200px">
+            <p>Name
+                <br/>
+                <input type="text" id="npc_name" />
+            </p>
+            <p>Conversation Text
+                <br/>
+                <textarea id="convo_text" rows="10" cols="80"></textarea>
+            </p>
+
+            <div>
+                <label>
+                    <input type="checkbox" value="quest_giver" id="quest">&nbsp;Quest Giver?
+                </label>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        var filename;
+        var structure;
+        var currentObject;
+
+        //get a reference to the elements
+        var controls = {
+            list: document.getElementById("npc_list"),
+            npcName: document.getElementById("npc_name"),
+            convoText: document.getElementById("convo_text"),
+            questCheck: document.getElementById("quest")
+        };
+
+        controls.convoText.addEventListener("input", (event) => {
+            if (currentObject) {
+                atomicQueryPromise("editorChange");
+                currentObject.convo = controls.convoText.value;
+            }
+            event.stopPropagation();
+        });
+
+        controls.npcName.addEventListener("input", (event) => {
+            if (currentObject) {
+                atomicQueryPromise("editorChange");
+                currentObject.name = controls.npcName.value;
+                controls.list.options[controls.list.selectedIndex].text = currentObject.name;
+            }
+            event.stopPropagation();
+        });
+
+        controls.questCheck.addEventListener("click", (event) => {
+            if (currentObject) {
+                atomicQueryPromise("editorChange");
+                currentObject.quest_giver = event.currentTarget.checked;
+            }
+            event.stopPropagation();
+        });
+
+        controls.list.addEventListener("change", (event) => {
+            var selName = controls.list.options[controls.list.selectedIndex].text;
+
+            currentObject = structure.npclist.find((el) => el.name == selName);
+            if (currentObject) {
+                controls.convoText.value = currentObject.convo;
+                controls.npcName.value = currentObject.name;
+                controls.questCheck.checked = currentObject.quest_giver;
+            }
+            event.stopPropagation();
+
+        });
+
+        /**
+         * Promise version of atomic query
+         * @param  {string} message the query to use to pass to atomicQuery.  If there is no payload, this will be passed directly, otherwise it will be passed in a data object
+         * @param  {any} payload optional data to send
+         * @return {Promise}
+         */
+        function atomicQueryPromise(message) {
+            return new Promise(function(resolve, reject) {
+                var queryMessage = message;
+
+                // if message is coming in as an object then let's stringify it
+                if (typeof(message) != "string") {
+                    queryMessage = JSON.stringify(message);
+                }
+
+                window.atomicQuery({
+                    request: queryMessage,
+                    persistent: false,
+                    onSuccess: resolve,
+                    onFailure: (error_code, error_message) => reject({
+                        error_code: error_code,
+                        error_message: error_message
+                    })
+                });
+            });
+        }
+
+        /**
+         * Queries the host for a particular resource and returns it in a promise
+         * @param  {string} codeUrl
+         * @return {Promise}
+         */
+        function getResource(codeUrl) {
+            return new Promise(function(resolve, reject) {
+                var xmlHttp = new XMLHttpRequest();
+                xmlHttp.onreadystatechange = () => {
+                    if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
+                        resolve(xmlHttp.responseText);
+                    }
+                };
+                xmlHttp.open("GET", codeUrl, true); // true for asynchronous
+                xmlHttp.send(null);
+            });
+        }
+
+        // Functions exposed to the host editor.  These
+        // are hooked in here so that they are available immediately from the host
+        function HOST_loadCode(codeUrl) {
+            filename = codeUrl.replace("atomic://", "");
+            getResource(codeUrl).then((code) => {
+                structure = JSON.parse(code);
+                for (var i = 0; i < structure.npclist.length; i++) {
+                    var opt = document.createElement("OPTION");
+                    controls.list.options.add(opt);
+                    opt.text = structure.npclist[i].name;
+                }
+            });
+        }
+
+        function HOST_saveCode() {
+            atomicQueryPromise({
+                message: "editorSaveCode",
+                payload: JSON.stringify(structure, null, 2)
+            });
+        }
+
+        function HOST_resourceRenamed(path, newPath) {
+            alert("Resource Renamed: " + path + "->" + newPath);
+        }
+
+        function HOST_resourceDeleted(path) {
+            alert("Resource Deleted: " + path);
+        }
+
+        function HOST_loadPreferences(prefUrl) {
+            alert("Prefs Changed: " + prefUrl);
+        }
+    </script>
+
+</body>
+
+</html>

+ 19 - 0
EditorPluginExample/Resources/Scripts/custom.editor.json

@@ -0,0 +1,19 @@
+{
+  "npclist": [
+    {
+      "name": "Paul",
+      "quest_giver": false,
+      "convo": "Fear is the mind killer."
+    },
+    {
+      "name": "Jessica",
+      "quest_giver": true,
+      "convo": "My son lives!"
+    },
+    {
+      "name": "Chani",
+      "quest_giver": true,
+      "convo": "Tell me of the waters of your homeworld, Usul."
+    }
+  ]
+}

+ 5 - 0
EditorPluginExample/Resources/Scripts/custom.editor.json.asset

@@ -0,0 +1,5 @@
+{
+	"version": 1,
+	"guid": "cb8047ec5b5b44ad0351e4724d46c16d",
+	"JSONImporter": null
+}

+ 14 - 0
EditorPluginExample/Resources/Scripts/custom.editorjs.json

@@ -0,0 +1,14 @@
+{
+  "npclist": [
+    {
+      "name": "Vlad",
+      "quest_giver": false,
+      "convo": "You must squeeze them."
+    },
+    {
+      "name": "Feyd",
+      "quest_giver": false,
+      "convo": "Who is the little one, a pet perhaps? Will she deserve my special attentions?"
+    }
+  ]
+}

+ 5 - 0
EditorPluginExample/Resources/Scripts/custom.editorjs.json.asset

@@ -0,0 +1,5 @@
+{
+	"version": 1,
+	"guid": "fbd0ca31f7ad8ba64481d5d7071da56c",
+	"JSONImporter": null
+}

File diff suppressed because it is too large
+ 280 - 280
EditorPluginExample/typings/Atomic/Atomic.d.ts


Some files were not shown because too many files changed in this diff