Browse Source

Merge pull request #750 from LumaDigital/PerProjectExtensions

Per project extensions, authors update
JoshEngebretson 9 years ago
parent
commit
264cc5a4aa

+ 4 - 0
AUTHORS.md

@@ -27,6 +27,10 @@
 
 - Gareth Fouche (https://github.com/GarethNN)
 
+- Wynand van Vuuren (https://github.com/Vlamboljant)
+
+- Johnny Wahib (https://github.com/JohnnyWahib)
+
 ### Contribution Copyright and Licensing
 
 Atomic Game Engine contribution copyrights are held by their authors.  Each author retains the copyright to their contribution and agrees to irrevocably license the contribution under the Atomic Game Engine Contribution License `CONTRIBUTION_LICENSE.md`.  Please see `CONTRIBUTING.md` for more details.

+ 126 - 15
Script/AtomicEditor/hostExtensions/HostExtensionServices.ts

@@ -22,12 +22,12 @@
 
 import * as EditorEvents from "../editor/EditorEvents";
 import * as EditorUI from "../ui/EditorUI";
-
-
+import MainFramMenu = require("../ui/frames/menus/MainFrameMenu");
+import ModalOps = require("../ui/modal/ModalOps");
 /**
  * Generic registry for storing Editor Extension Services
  */
-class ServiceRegistry<T extends Editor.Extensions.EditorService> implements Editor.Extensions.ServiceRegistry<T> {
+export class ServiceRegistry<T extends Editor.Extensions.EditorService> implements Editor.Extensions.ServiceRegistry<T> {
     registeredServices: T[] = [];
 
     /**
@@ -38,9 +38,15 @@ class ServiceRegistry<T extends Editor.Extensions.EditorService> implements Edit
         this.registeredServices.push(service);
     }
 
+    unregister(service: T) {
+        var index = this.registeredServices.indexOf(service, 0);
+        if (index > -1) {
+            this.registeredServices.splice(index, 1);
+        }
+    }
 }
 
-interface ServiceEventSubscriber {
+export interface ServiceEventSubscriber {
     /**
      * Allow this service registry to subscribe to events that it is interested in
      * @param  {Atomic.UIWidget} topLevelWindow The top level window that will be receiving these events
@@ -51,7 +57,7 @@ interface ServiceEventSubscriber {
 /**
  * Registry for service extensions that are concerned about project events
  */
-export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtensions.ProjectService> implements ServiceEventSubscriber {
+export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtensions.ProjectService> implements Editor.HostExtensions.ProjectServiceRegistry {
     constructor() {
         super();
     }
@@ -71,16 +77,18 @@ export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtension
      * @param  {[type]} data Event info from the project unloaded event
      */
     projectUnloaded(data) {
-        this.registeredServices.forEach((service) => {
+        // Need to use a for loop for length down to 0 because extensions *could* delete themselves from the list on projectUnloaded
+        for (let i = this.registeredServices.length - 1; i >= 0; i--) {
+            let service = this.registeredServices[i];
             // Notify services that the project has been unloaded
             try {
                 if (service.projectUnloaded) {
                     service.projectUnloaded();
                 }
             } catch (e) {
-                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}\n \n ${e.stack}`);
+                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e} \n\n ${e.stack}`);
             }
-        });
+        };
     }
 
     /**
@@ -88,16 +96,18 @@ export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtension
      * @param  {[type]} data Event info from the project unloaded event
      */
     projectLoaded(ev: Editor.EditorEvents.LoadProjectEvent) {
-        this.registeredServices.forEach((service) => {
+        // Need to use a for loop and don't cache the length because the list of services *may* change while processing.  Extensions could be appended to the end
+        for (let i = 0; i < this.registeredServices.length; i++) {
+            let service = this.registeredServices[i];
             try {
                 // Notify services that the project has just been loaded
                 if (service.projectLoaded) {
                     service.projectLoaded(ev);
                 }
             } catch (e) {
-                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}\n \n ${e.stack}`);
+                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e}\n\n ${e.stack}`);
             }
-        });
+        };
     }
 
     playerStarted() {
@@ -117,7 +127,7 @@ export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtension
 /**
  * Registry for service extensions that are concerned about Resources
  */
-export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensions.ResourceService> {
+export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensions.ResourceService> implements Editor.HostExtensions.ResourceServiceRegistry {
     constructor() {
         super();
     }
@@ -145,7 +155,7 @@ export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensio
                     service.save(ev);
                 }
             } catch (e) {
-                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}\n \n ${e.stack}`);
+                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e}\n\n ${e.stack}`);
             }
         });
     }
@@ -161,7 +171,7 @@ export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensio
                     service.delete(ev);
                 }
             } catch (e) {
-                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}\n ${e}\n ${e.stack}`);
+                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e}\n\n ${e.stack}`);
             }
         });
     }
@@ -178,9 +188,110 @@ export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensio
                     service.rename(ev);
                 }
             } catch (e) {
-                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}\n \n ${e.stack}`);
+                EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e}\n\n ${e.stack}`);
             }
         });
     }
 
 }
+
+/**
+ * Registry for service extensions that are concerned about and need access to parts of the editor user interface
+ * Note: we may want to move this out into it's own file since it has a bunch of editor dependencies
+ */
+export class UIServiceRegistry extends ServiceRegistry<Editor.HostExtensions.UIService> implements Editor.HostExtensions.UIServiceRegistry {
+    constructor() {
+        super();
+    }
+
+    private mainFrameMenu: MainFramMenu = null;
+    private modalOps: ModalOps;
+
+    init(menu: MainFramMenu, modalOps: ModalOps) {
+        // Only set these once
+        if (this.mainFrameMenu == null) {
+            this.mainFrameMenu = menu;
+        }
+        if (this.modalOps == null) {
+            this.modalOps = modalOps;
+        }
+    }
+
+    /**
+     * Adds a new menu to the plugin menu
+     * @param  {string} id
+     * @param  {any} items
+     * @return {Atomic.UIMenuItemSource}
+     */
+    createPluginMenuItemSource(id: string, items: any): Atomic.UIMenuItemSource {
+        return this.mainFrameMenu.createPluginMenuItemSource(id, items);
+    }
+
+    /**
+     * Removes a previously added menu from the plugin menu
+     * @param  {string} id
+     */
+    removePluginMenuItemSource(id: string) {
+        this.mainFrameMenu.removePluginMenuItemSource(id);
+    }
+
+    /**
+     * Disaplays a modal window
+     * @param  {Editor.Modal.ModalWindow} window
+     */
+    showModalWindow(windowText: string, uifilename: string, handleWidgetEventCB: (ev: Atomic.UIWidgetEvent) => void): Editor.Modal.ExtensionWindow {
+        return this.modalOps.showExtensionWindow(windowText, uifilename, handleWidgetEventCB);
+    }
+
+    /**
+     * Called when a menu item has been clicked
+     * @param  {string} refId
+     * @type {boolean} return true if handled
+     */
+    menuItemClicked(refId: string): boolean {
+
+        // run through and find any services that can handle this.
+        let holdResult = false;
+        this.registeredServices.forEach((service) => {
+            try {
+                // Verify that the service contains the appropriate methods and that it can handle it
+                if (service.menuItemClicked) {
+                    if (service.menuItemClicked(refId)) {
+                        holdResult = true;
+                    }
+                }
+            } catch (e) {
+               EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e}\n\n ${e.stack}`);
+            }
+        });
+        return holdResult;
+    }
+
+    /**
+     * Allow this service registry to subscribe to events that it is interested in
+     * @param  {Atomic.UIWidget} topLevelWindow The top level window that will be receiving these events
+     */
+    subscribeToEvents(eventDispatcher: Editor.Extensions.EventDispatcher) {
+        // Placeholder
+        //eventDispatcher.subscribeToEvent(EditorEvents.SaveResourceNotification, (ev) => this.doSomeUiMessage(ev));
+    }
+
+    /**
+     * Called after a resource has been saved
+     * @param  {Editor.EditorEvents.SaveResourceEvent} ev
+     */
+    doSomeUiMessage(ev: Editor.EditorEvents.SaveResourceEvent) {
+        // PLACEHOLDER
+        // run through and find any services that can handle this.
+        this.registeredServices.forEach((service) => {
+            // try {
+            //     // Verify that the service contains the appropriate methods and that it can save
+            //     if (service.save) {
+            //         service.save(ev);
+            //     }
+            // } catch (e) {
+            //    EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e}\n\n ${e.stack}`);
+            // }
+        });
+    }
+}

+ 6 - 1
Script/AtomicEditor/hostExtensions/ServiceLocator.ts

@@ -22,6 +22,7 @@
 
 import * as HostExtensionServices from "./HostExtensionServices";
 import * as EditorUI from "../ui/EditorUI";
+import ProjectBasedExtensionLoader from "./coreExtensions/ProjectBasedExtensionLoader";
 import TypescriptLanguageExtension from "./languageExtensions/TypscriptLanguageExtension";
 
 /**
@@ -33,18 +34,20 @@ export class ServiceLocatorType implements Editor.HostExtensions.HostServiceLoca
     constructor() {
         this.resourceServices = new HostExtensionServices.ResourceServiceRegistry();
         this.projectServices = new HostExtensionServices.ProjectServiceRegistry();
+        this.uiServices = new HostExtensionServices.UIServiceRegistry();
     }
 
     private eventDispatcher: Atomic.UIWidget = null;
 
     resourceServices: HostExtensionServices.ResourceServiceRegistry;
     projectServices: HostExtensionServices.ProjectServiceRegistry;
+    uiServices: HostExtensionServices.UIServiceRegistry;
 
     loadService(service: Editor.HostExtensions.HostEditorService) {
         try {
             service.initialize(this);
         } catch (e) {
-            EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}\n \n ${e.stack}`);
+            EditorUI.showModalError("Extension Error", `Error detected in extension ${service.name}:\n${e}\n\n ${e.stack}`);
         }
     }
 
@@ -56,6 +59,7 @@ export class ServiceLocatorType implements Editor.HostExtensions.HostServiceLoca
         this.eventDispatcher = frame;
         this.resourceServices.subscribeToEvents(this);
         this.projectServices.subscribeToEvents(this);
+        this.uiServices.subscribeToEvents(this);
     }
 
     /**
@@ -85,4 +89,5 @@ const serviceLocator = new ServiceLocatorType();
 export default serviceLocator;
 
 // Load up all the internal services
+serviceLocator.loadService(new ProjectBasedExtensionLoader());
 serviceLocator.loadService(new TypescriptLanguageExtension());

+ 135 - 0
Script/AtomicEditor/hostExtensions/coreExtensions/ProjectBasedExtensionLoader.ts

@@ -0,0 +1,135 @@
+//
+// Copyright (c) 2014-2016 THUNDERBEAST GAMES LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+/// <reference path="../../../TypeScript/duktape.d.ts" />
+
+import * as EditorEvents from "../../editor/EditorEvents";
+
+/**
+ * Resource extension that supports the web view typescript extension
+ */
+export default class ProjectBasedExtensionLoader implements Editor.HostExtensions.ProjectService {
+    name: string = "ProjectBasedExtensionLoader";
+    description: string = "This service supports loading extensions that reside in the project under {ProjectRoot}/Editor and named '*.Service.js'.";
+
+    private serviceRegistry: Editor.HostExtensions.HostServiceLocator = null;
+    private modSearchRewritten = false;
+
+    /**
+     * Prefix to use to detect "special" require paths
+     * @type {String}
+     */
+    private static duktapeRequirePrefix = "project:";
+
+    /**
+     * Inject this language service into the registry
+     * @return {[type]}             True if successful
+     */
+    initialize(serviceRegistry: Editor.HostExtensions.HostServiceLocator) {
+
+        // Let's rewrite the mod search
+        this.rewriteModSearch();
+
+        // We care project events
+        serviceRegistry.projectServices.register(this);
+        this.serviceRegistry = serviceRegistry;
+    }
+
+    /**
+     * Rewrite the duktape modSearch routine so that we can intercept any
+     * require calls with a "project:" prefix.  Duktape will fail if it receives
+     * a require call with a fully qualified path starting with a "/" (at least on OSX and Linux),
+     * so we will need to detect any of these project level requires and allow Atomic to go to the
+     * file system and manually pull these in to provide to duktape
+     */
+    private rewriteModSearch() {
+        Duktape.modSearch = (function(origModSearch) {
+            return function(id: string, require, exports, module) {
+                let system = ToolCore.getToolSystem();
+                if (id.indexOf(ProjectBasedExtensionLoader.duktapeRequirePrefix) == 0) {
+                    let path = id.substr(ProjectBasedExtensionLoader.duktapeRequirePrefix.length) + ".js";
+
+                    // For safety, only allow bringing modules in from the project directory.  This could be
+                    // extended to look for some global extension directory to pull extensions from such as
+                    // ~/.atomicExtensions/...
+                    if (system.project && path.indexOf(system.project.projectPath) == 0) {
+                        console.log(`Searching for project based include: ${path}`);
+                        // we have a project based require
+                        if (Atomic.fileSystem.fileExists(path)) {
+                            let include = new Atomic.File(path, Atomic.FILE_READ);
+                            try {
+                                return include.readText();
+                            } finally {
+                                include.close();
+                            }
+                        } else {
+                            throw new Error(`Cannot find project module: ${path}`);
+                        }
+                    } else {
+                        throw new Error(`Extension at ${path} does not reside in the project directory ${system.project.projectPath}`);
+                    }
+                } else {
+                    return origModSearch(id, require, exports, module);
+                }
+            };
+        })(Duktape.modSearch);
+    }
+    /**
+     * Called when the project is being loaded to allow the typscript language service to reset and
+     * possibly compile
+     */
+    projectLoaded(ev: Editor.EditorEvents.LoadProjectEvent) {
+        // got a load, we need to reset the language service
+        console.log(`${this.name}: received a project loaded event for project at ${ev.path}`);
+        let system = ToolCore.getToolSystem();
+        if (system.project) {
+            let fileSystem = Atomic.getFileSystem();
+            let editorScriptsPath = Atomic.addTrailingSlash(system.project.resourcePath) + "EditorData/";
+            if (fileSystem.dirExists(editorScriptsPath)) {
+                let filenames = fileSystem.scanDir(editorScriptsPath, "*.js", Atomic.SCAN_FILES, true);
+                filenames.forEach((filename) => {
+                    // Filtered search in Atomic doesn't due true wildcarding, only handles extension filters
+                    // in the future this may be better handled with some kind of manifest file
+                    if (filename.toLowerCase().lastIndexOf(".plugin.js") >= 0) {
+                        var extensionPath = editorScriptsPath + filename;
+                        extensionPath = extensionPath.substring(0, extensionPath.length - 3);
+
+                        console.log(`Detected project extension at: ${extensionPath} `);
+                        // Note: duktape does not yet support unloading modules,
+                        // but will return the same object when passed a path the second time.
+                        let resourceServiceModule = require(ProjectBasedExtensionLoader.duktapeRequirePrefix + extensionPath);
+
+                        // Handle situation where the service is either exposed by a typescript default export
+                        // or as the module.export (depends on if it is being written in typescript, javascript, es6, etc.)
+                        let resourceService: Editor.HostExtensions.HostEditorService = null;
+                        if (resourceServiceModule.default) {
+                            resourceService = resourceServiceModule.default;
+                        } else {
+                            resourceService = resourceServiceModule;
+                        }
+                        this.serviceRegistry.loadService(resourceService);
+                    }
+                });
+            }
+        }
+    }
+}

+ 5 - 0
Script/AtomicEditor/ui/EditorUI.ts

@@ -24,6 +24,7 @@ import EditorEvents = require("editor/EditorEvents");
 import MainFrame = require("./frames/MainFrame");
 import ModalOps = require("./modal/ModalOps");
 import Shortcuts = require("./Shortcuts");
+import ServiceLocator from "../hostExtensions/ServiceLocator";
 
 // this is designed with public get functions to solve
 // circular dependency issues in TS
@@ -94,6 +95,10 @@ class EditorUI extends Atomic.ScriptObject {
     this.modalOps = new ModalOps();
     this.shortcuts = new Shortcuts();
 
+    // Hook the service locator into the event system and give it the ui objects it needs
+    ServiceLocator.uiServices.init(this.mainframe.menu, this.modalOps);
+    ServiceLocator.subscribeToEvents(this.mainframe);
+
     this.subscribeToEvent(EditorEvents.ModalError, (event:EditorEvents.ModalErrorEvent) => {
       this.showModalError(event.title, event.message);
     });

+ 0 - 4
Script/AtomicEditor/ui/frames/MainFrame.ts

@@ -33,7 +33,6 @@ import ScriptWidget = require("ui/ScriptWidget");
 import MainFrameMenu = require("./menus/MainFrameMenu");
 
 import MenuItemSources = require("./menus/MenuItemSources");
-import ServiceLocator from "../../hostExtensions/ServiceLocator";
 import * as EditorEvents from "../../editor/EditorEvents";
 
 class MainFrame extends ScriptWidget {
@@ -75,9 +74,6 @@ class MainFrame extends ScriptWidget {
             this.disableProjectMenus();
         });
 
-        // Allow the service locator to hook into the event system
-        ServiceLocator.subscribeToEvents(this);
-
         this.showWelcomeFrame(true);
 
     }

+ 32 - 0
Script/AtomicEditor/ui/frames/menus/MainFrameMenu.ts

@@ -25,9 +25,12 @@ import EditorEvents = require("../../../editor/EditorEvents");
 import EditorUI = require("../../EditorUI");
 import MenuItemSources = require("./MenuItemSources");
 import Preferences = require("editor/Preferences");
+import ServiceLocator from "../../../hostExtensions/ServiceLocator";
 
 class MainFrameMenu extends Atomic.ScriptObject {
 
+    private pluginMenuItemSource: Atomic.UIMenuItemSource;
+
     constructor() {
 
         super();
@@ -41,6 +44,27 @@ class MainFrameMenu extends Atomic.ScriptObject {
 
     }
 
+    createPluginMenuItemSource(id: string, items: any): Atomic.UIMenuItemSource {
+        if (!this.pluginMenuItemSource) {
+            var developerMenuItemSource = MenuItemSources.getMenuItemSource("menu developer");
+            this.pluginMenuItemSource = MenuItemSources.createSubMenuItemSource(developerMenuItemSource ,"Plugins", {});
+        }
+
+        return MenuItemSources.createSubMenuItemSource(this.pluginMenuItemSource , id, items);
+
+    }
+
+    removePluginMenuItemSource(id: string) {
+        if (this.pluginMenuItemSource) {
+            this.pluginMenuItemSource.removeItemWithStr(id);
+            if (0 == this.pluginMenuItemSource.itemCount) {
+                var developerMenuItemSource = MenuItemSources.getMenuItemSource("menu developer");
+                developerMenuItemSource.removeItemWithStr("Plugins");
+                this.pluginMenuItemSource = null;
+            }
+        }
+    }
+
     handlePopupMenu(target: Atomic.UIWidget, refid: string): boolean {
 
         if (target.id == "menu edit popup") {
@@ -218,12 +242,14 @@ class MainFrameMenu extends Atomic.ScriptObject {
             if (refid == "developer assetdatabase scan") {
 
               ToolCore.assetDatabase.scan();
+              return true;
 
             }
 
             if (refid == "developer assetdatabase force") {
 
               ToolCore.assetDatabase.reimportAllAssets();
+              return true;
 
             }
 
@@ -234,8 +260,12 @@ class MainFrameMenu extends Atomic.ScriptObject {
                 myPrefs.saveEditorWindowData(myPrefs.editorWindow);
                 myPrefs.savePlayerWindowData(myPrefs.playerWindow);
                 Atomic.getEngine().exit();
+                return true;
             }
 
+            // If we got here, then we may have been injected by a plugin.  Notify the plugins
+            return ServiceLocator.uiServices.menuItemClicked(refid);
+
         } else if (target.id == "menu tools popup") {
 
             if (refid == "tools toggle profiler") {
@@ -286,6 +316,8 @@ class MainFrameMenu extends Atomic.ScriptObject {
                 return true;
             }
 
+        } else {
+            console.log("Menu: " + target.id + " clicked");
         }
 
     }

+ 17 - 6
Script/AtomicEditor/ui/frames/menus/MenuItemSources.ts

@@ -77,12 +77,7 @@ function createMenuItemSourceRecursive(items: any): Atomic.UIMenuItemSource {
 
             }
             else if (typeof value === "object") {
-
-                var subsrc = createMenuItemSourceRecursive(value);
-
-                var item = new Atomic.UIMenuItem(key);
-                item.subSource = subsrc;
-                src.addItem(item);
+                createSubMenuItemSource(src, key, value);
 
             }
 
@@ -95,6 +90,16 @@ function createMenuItemSourceRecursive(items: any): Atomic.UIMenuItemSource {
 
 }
 
+export function createSubMenuItemSource(src: Atomic.UIMenuItemSource, id: string, items: any): Atomic.UIMenuItemSource {
+    var subsrc = createMenuItemSourceRecursive(items);
+
+    var item = new Atomic.UIMenuItem(id);
+    item.subSource = subsrc;
+    src.addItem(item);
+
+    return subsrc;
+}
+
 export function createMenuItemSource(id: string, items: any): Atomic.UIMenuItemSource {
 
     srcLookup[id] = createMenuItemSourceRecursive(items);
@@ -102,3 +107,9 @@ export function createMenuItemSource(id: string, items: any): Atomic.UIMenuItemS
     return srcLookup[id];
 
 }
+
+export function deleteMenuItemSource(id: string) {
+    if (srcLookup[id]) {
+        delete srcLookup[id];
+    }
+}

+ 45 - 0
Script/AtomicEditor/ui/modal/ExtensionWindow.ts

@@ -0,0 +1,45 @@
+//
+// Copyright (c) 2014-2016 THUNDERBEAST GAMES LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+import EditorUI = require("../EditorUI");
+import ModalWindow = require("./ModalWindow");
+
+class ExtensionWindow extends ModalWindow {
+
+    private handleWidgetEventCB: (ev: Atomic.UIWidgetEvent) => void;
+
+    constructor(windowText: string, uifilename: string, handleWidgetEventCB: (ev: Atomic.UIWidgetEvent) => void) {
+
+        super();
+
+        this.init(windowText, uifilename);
+
+        this.handleWidgetEventCB = handleWidgetEventCB;
+    }
+
+    handleWidgetEvent(ev: Atomic.UIWidgetEvent) {
+        if (this.handleWidgetEventCB)
+            this.handleWidgetEventCB(ev);
+    }
+}
+
+export = ExtensionWindow;

+ 10 - 0
Script/AtomicEditor/ui/modal/ModalOps.ts

@@ -43,6 +43,8 @@ import UIResourceOps = require("./UIResourceOps");
 
 import SnapSettingsWindow = require("./SnapSettingsWindow");
 
+import ExtensionWindow = require("./ExtensionWindow");
+
 import ProjectTemplates = require("../../resources/ProjectTemplates");
 
 
@@ -280,6 +282,14 @@ class ModalOps extends Atomic.ScriptObject {
 
     }
 
+    showExtensionWindow(windowText: string, uifilename: string, handleWidgetEventCB: (ev: Atomic.UIWidgetEvent) => void): Editor.Modal.ExtensionWindow {
+        if (this.show()) {
+
+            this.opWindow = new ExtensionWindow(windowText, uifilename, handleWidgetEventCB);
+            return this.opWindow;
+        }
+    }
+
     private show(): boolean {
 
         if (this.dimmer.parent) {

+ 7 - 0
Script/AtomicWebViewEditor/clientExtensions/ClientExtensionServices.ts

@@ -64,6 +64,13 @@ class ServiceRegistry<T extends Editor.Extensions.EditorService> implements Edit
     register(service: T) {
         this.registeredServices.push(service);
     }
+
+    unregister(service: T) {
+        var index = this.registeredServices.indexOf(service, 0);
+        if (index > -1) {
+            this.registeredServices.splice(index, 1);
+        }
+    }
 }
 
 export class ExtensionServiceRegistry extends ServiceRegistry<Editor.ClientExtensions.WebViewService> {

+ 1 - 0
Script/AtomicWebViewEditor/tsconfig.json

@@ -38,6 +38,7 @@
         "../TypeScript/AtomicNET.d.ts",
         "../TypeScript/AtomicPlayer.d.ts",
         "../TypeScript/AtomicWork.d.ts",
+        "../TypeScript/duktape.d.ts",
         "../TypeScript/Editor.d.ts",
         "../TypeScript/EditorWork.d.ts",
         "../TypeScript/ToolCore.d.ts",

+ 1 - 1
Script/TypeScript/AtomicWork.d.ts

@@ -2,7 +2,7 @@
 /// <reference path="ToolCore.d.ts" />
 /// <reference path="Editor.d.ts" />
 /// <reference path="AtomicPlayer.d.ts" />
-
+/// <reference path="AtomicNET.d.ts" />
 
 declare module Atomic {
 

+ 27 - 2
Script/TypeScript/EditorWork.d.ts

@@ -5,6 +5,7 @@
 // license information: https://github.com/AtomicGameEngine/AtomicGameEngine
 //
 
+/// <reference path="Atomic.d.ts" />
 /// <reference path="Editor.d.ts" />
 
 declare module Editor.EditorEvents {
@@ -199,6 +200,17 @@ declare module Editor.Extensions {
          * @param  {T}      service the service to register
          */
         register(service: T);
+        /**
+         * Removes a service from the registered services list for this type of service
+         * @param  {T}      service the service to unregister
+         */
+        unregister(service: T);
+    }
+}
+
+declare module Editor.Modal {
+    export interface ExtensionWindow extends Atomic.UIWindow {
+        hide();
     }
 }
 
@@ -209,8 +221,9 @@ declare module Editor.HostExtensions {
      * or by the editor itself.
      */
     export interface HostServiceLocator extends Editor.Extensions.ServiceLoader {
-        resourceServices: Editor.Extensions.ServiceRegistry<ResourceService>;
-        projectServices: Editor.Extensions.ServiceRegistry<ProjectService>;
+        resourceServices: ResourceServiceRegistry;
+        projectServices: ProjectServiceRegistry;
+        uiServices: UIServiceRegistry;
     }
 
     export interface HostEditorService extends Editor.Extensions.EditorService {
@@ -225,12 +238,24 @@ declare module Editor.HostExtensions {
         delete?(ev: EditorEvents.DeleteResourceEvent);
         rename?(ev: EditorEvents.RenameResourceEvent);
     }
+    export interface ResourceServiceRegistry extends Editor.Extensions.ServiceRegistry<ResourceService> { }
 
     export interface ProjectService extends Editor.Extensions.EditorService {
         projectUnloaded?();
         projectLoaded?(ev: EditorEvents.LoadProjectEvent);
         playerStarted?();
     }
+    export interface ProjectServiceRegistry extends Editor.Extensions.ServiceRegistry<ProjectService> { }
+
+    export interface UIService extends Editor.Extensions.EditorService {
+        menuItemClicked?(refId: string): boolean;
+    }
+    export interface UIServiceRegistry extends Editor.Extensions.ServiceRegistry<UIService> {
+        createPluginMenuItemSource(id: string, items: any): Atomic.UIMenuItemSource;
+        removePluginMenuItemSource(id: string);
+        showModalWindow(windowText: string, uifilename: string, handleWidgetEventCB: (ev: Atomic.UIWidgetEvent) => void): Editor.Modal.ExtensionWindow;
+        menuItemClicked(refId: string): boolean;
+    }
 }
 
 /**

+ 17 - 0
Script/TypeScript/duktape.d.ts

@@ -0,0 +1,17 @@
+// Duktape built-ins
+
+// extracted from lib.d.ts
+declare interface Console {
+    log(message?: any, ...optionalParams: any[]): void;
+}
+
+declare var console: Console;
+
+// Duktape require isn't recognized as a function, but can be used as one
+declare function require(filename: string): any;
+
+declare interface DuktapeModule {
+    modSearch(id: string, require, exports, module);
+}
+
+declare var Duktape: DuktapeModule;

+ 2 - 0
Script/tsconfig.json

@@ -25,6 +25,7 @@
         "./AtomicEditor/editor/EditorEvents.ts",
         "./AtomicEditor/editor/EditorLicense.ts",
         "./AtomicEditor/editor/Preferences.ts",
+        "./AtomicEditor/hostExtensions/coreExtensions/ProjectBasedExtensionLoader.ts",
         "./AtomicEditor/hostExtensions/HostExtensionServices.ts",
         "./AtomicEditor/hostExtensions/languageExtensions/TypscriptLanguageExtension.ts",
         "./AtomicEditor/hostExtensions/ServiceLocator.ts",
@@ -98,6 +99,7 @@
         "./TypeScript/AtomicNET.d.ts",
         "./TypeScript/AtomicPlayer.d.ts",
         "./TypeScript/AtomicWork.d.ts",
+        "./TypeScript/duktape.d.ts",
         "./TypeScript/Editor.d.ts",
         "./TypeScript/EditorWork.d.ts",
         "./TypeScript/ToolCore.d.ts",

+ 23 - 0
Source/Atomic/UI/UISelectItem.cpp

@@ -84,6 +84,29 @@ UISelectItemSource::~UISelectItemSource()
 
 }
 
+void UISelectItemSource::RemoveItemWithId(const String& id)
+{
+    tb::TBID test = TBID(id.CString());
+    for (List<SharedPtr<UISelectItem> >::Iterator itr = items_.Begin(); itr != items_.End(); itr++)
+    {
+        if ((*itr)->GetID() == test) {
+            items_.Erase(itr);
+            break;
+        }
+    }
+}
+
+void UISelectItemSource::RemoveItemWithStr(const String& str)
+{
+    for (List<SharedPtr<UISelectItem> >::Iterator itr = items_.Begin(); itr != items_.End(); itr++)
+    {
+        if ((*itr)->GetStr() == str) {
+            items_.Erase(itr);
+            break;
+        }
+    }
+}
+
 TBSelectItemSource *UISelectItemSource::GetTBItemSource()
 {
     // caller's responsibility to clean up

+ 5 - 0
Source/Atomic/UI/UISelectItem.h

@@ -44,6 +44,8 @@ public:
 
     void SetString(const String& str) { str_ = str; }
     void SetID(const String& id);
+    const String& GetStr() { return str_; }
+    tb::TBID GetID() { return id_; }
     void SetSkinImage(const String& skinImage);
     void SetSubSource(UISelectItemSource *subSource);
 
@@ -72,6 +74,9 @@ public:
     virtual ~UISelectItemSource();
 
     void AddItem(UISelectItem* item) { items_.Push(SharedPtr<UISelectItem>(item)); }
+    void RemoveItemWithId(const String& id);
+    void RemoveItemWithStr(const String& str);
+    int GetItemCount() { return items_.Size(); }
 
     void Clear() { items_.Clear(); }