Browse Source

Fleshed out the per-project extensions
- Migrated the mechanism to search for and load per-project extensions into an extension itself that listens for project loaded events
- added a new uiServices registry that gives extensions access to UI
- Updated the main frame to inject the exposed menu services into the ui extensions registry so that extensions can create menus. Additional ui features should be injected the same way
- needed to override the way duktape searches for requires in order for it to pull in project extensions correctly on OSX (possibly Linux and Windows too)

Shaddock Heath 9 years ago
parent
commit
af17f951a7

+ 0 - 27
Script/AtomicEditor/editor/Editor.ts

@@ -28,9 +28,6 @@ import EditorEvents = require("./EditorEvents");
 import Preferences = require("./Preferences");
 import Preferences = require("./Preferences");
 import ServiceLocator from "../hostExtensions/ServiceLocator";
 import ServiceLocator from "../hostExtensions/ServiceLocator";
 
 
-// Duktape require isn't recognized as a function, but can be used as one
-declare function require(filename : string) : any;
-
 class Editor extends Atomic.ScriptObject {
 class Editor extends Atomic.ScriptObject {
 
 
     project: ToolCore.Project;
     project: ToolCore.Project;
@@ -155,35 +152,11 @@ class Editor extends Atomic.ScriptObject {
         }
         }
         const loaded = system.loadProject(event.path);
         const loaded = system.loadProject(event.path);
         if (loaded) {
         if (loaded) {
-            this.loadProjectExtensions();
             this.sendEvent(EditorEvents.LoadProjectNotification, event);
             this.sendEvent(EditorEvents.LoadProjectNotification, event);
         }
         }
         return loaded;
         return loaded;
     }
     }
 
 
-    loadProjectExtensions() {
-        var system = ToolCore.getToolSystem();
-        if (system.project) {
-            var fileSystem = Atomic.getFileSystem();
-            var editorScriptsPath = system.project.resourcePath + "../Editor/";
-            if (fileSystem.dirExists(editorScriptsPath)) {
-                var filenames = fileSystem.scanDir(editorScriptsPath, "*.js", Atomic.SCAN_FILES, true);
-                for (var index in filenames) {
-                    var filename = filenames[index];
-                    // Filtered search in Atomic doesn't due true wildcarding, only handles extension filters
-                    if (filename.lastIndexOf(".Service.js") >= 0) {
-                        var extensionPath = editorScriptsPath + filename;
-                        extensionPath = extensionPath.substring(0, extensionPath.length - 3);
-                        // Note: duktape does not yet support unloading modules,
-                        // but will return the same object when passed a path the second time.
-                        var resourceService = <Editor.HostExtensions.HostEditorService> require(extensionPath).default;
-                        ServiceLocator.loadService(resourceService);
-                    }
-                }
-            }
-        }
-    }
-
     closeAllResourceEditors() {
     closeAllResourceEditors() {
         var editor = EditorUI.getCurrentResourceEditor();
         var editor = EditorUI.getCurrentResourceEditor();
         if (!editor) {
         if (!editor) {

+ 107 - 14
Script/AtomicEditor/hostExtensions/HostExtensionServices.ts

@@ -22,7 +22,7 @@
 
 
 import * as EditorEvents from "../editor/EditorEvents";
 import * as EditorEvents from "../editor/EditorEvents";
 import * as EditorUI from "../ui/EditorUI";
 import * as EditorUI from "../ui/EditorUI";
-
+import MainFramMenu = require("../ui/frames/menus/MainFrameMenu");
 
 
 /**
 /**
  * Generic registry for storing Editor Extension Services
  * Generic registry for storing Editor Extension Services
@@ -41,8 +41,8 @@ export class ServiceRegistry<T extends Editor.Extensions.EditorService> implemen
     unregister(service: T) {
     unregister(service: T) {
         var index = this.registeredServices.indexOf(service, 0);
         var index = this.registeredServices.indexOf(service, 0);
         if (index > -1) {
         if (index > -1) {
-           this.registeredServices.splice(index, 1);
-        }        
+            this.registeredServices.splice(index, 1);
+        }
     }
     }
 }
 }
 
 
@@ -57,7 +57,7 @@ export interface ServiceEventSubscriber {
 /**
 /**
  * Registry for service extensions that are concerned about project events
  * 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() {
     constructor() {
         super();
         super();
     }
     }
@@ -77,16 +77,18 @@ export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtension
      * @param  {[type]} data Event info from the project unloaded event
      * @param  {[type]} data Event info from the project unloaded event
      */
      */
     projectUnloaded(data) {
     projectUnloaded(data) {
-        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
+        for (let i = 0; i < this.registeredServices.length; i++) {
+            let service = this.registeredServices[i];
             // Notify services that the project has been unloaded
             // Notify services that the project has been unloaded
             try {
             try {
                 if (service.projectUnloaded) {
                 if (service.projectUnloaded) {
                     service.projectUnloaded();
                     service.projectUnloaded();
                 }
                 }
             } catch (e) {
             } 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}`);
             }
             }
-        });
+        };
     }
     }
 
 
     /**
     /**
@@ -94,16 +96,18 @@ export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtension
      * @param  {[type]} data Event info from the project unloaded event
      * @param  {[type]} data Event info from the project unloaded event
      */
      */
     projectLoaded(ev: Editor.EditorEvents.LoadProjectEvent) {
     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
+        for (let i = 0; i < this.registeredServices.length; i++) {
+            let service = this.registeredServices[i];
             try {
             try {
                 // Notify services that the project has just been loaded
                 // Notify services that the project has just been loaded
                 if (service.projectLoaded) {
                 if (service.projectLoaded) {
                     service.projectLoaded(ev);
                     service.projectLoaded(ev);
                 }
                 }
             } catch (e) {
             } 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() {
     playerStarted() {
@@ -123,7 +127,7 @@ export class ProjectServiceRegistry extends ServiceRegistry<Editor.HostExtension
 /**
 /**
  * Registry for service extensions that are concerned about Resources
  * 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() {
     constructor() {
         super();
         super();
     }
     }
@@ -151,7 +155,7 @@ export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensio
                     service.save(ev);
                     service.save(ev);
                 }
                 }
             } catch (e) {
             } 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}`);
             }
             }
         });
         });
     }
     }
@@ -167,7 +171,7 @@ export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensio
                     service.delete(ev);
                     service.delete(ev);
                 }
                 }
             } catch (e) {
             } 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}`);
             }
             }
         });
         });
     }
     }
@@ -184,9 +188,98 @@ export class ResourceServiceRegistry extends ServiceRegistry<Editor.HostExtensio
                     service.rename(ev);
                     service.rename(ev);
                 }
                 }
             } catch (e) {
             } 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;
+
+    setMainFrameMenu(menu: MainFramMenu) {
+        // Only set this once
+        if (this.mainFrameMenu == null) {
+            this.mainFrameMenu = menu;
+        }
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * 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 HostExtensionServices from "./HostExtensionServices";
 import * as EditorUI from "../ui/EditorUI";
 import * as EditorUI from "../ui/EditorUI";
+import ProjectBasedExtensionLoader from "./coreExtensions/ProjectBasedExtensionLoader";
 import TypescriptLanguageExtension from "./languageExtensions/TypscriptLanguageExtension";
 import TypescriptLanguageExtension from "./languageExtensions/TypscriptLanguageExtension";
 
 
 /**
 /**
@@ -33,18 +34,20 @@ export class ServiceLocatorType implements Editor.HostExtensions.HostServiceLoca
     constructor() {
     constructor() {
         this.resourceServices = new HostExtensionServices.ResourceServiceRegistry();
         this.resourceServices = new HostExtensionServices.ResourceServiceRegistry();
         this.projectServices = new HostExtensionServices.ProjectServiceRegistry();
         this.projectServices = new HostExtensionServices.ProjectServiceRegistry();
+        this.uiServices = new HostExtensionServices.UIServiceRegistry();
     }
     }
 
 
     private eventDispatcher: Atomic.UIWidget = null;
     private eventDispatcher: Atomic.UIWidget = null;
 
 
     resourceServices: HostExtensionServices.ResourceServiceRegistry;
     resourceServices: HostExtensionServices.ResourceServiceRegistry;
     projectServices: HostExtensionServices.ProjectServiceRegistry;
     projectServices: HostExtensionServices.ProjectServiceRegistry;
+    uiServices: HostExtensionServices.UIServiceRegistry;
 
 
     loadService(service: Editor.HostExtensions.HostEditorService) {
     loadService(service: Editor.HostExtensions.HostEditorService) {
         try {
         try {
             service.initialize(this);
             service.initialize(this);
         } catch (e) {
         } 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.eventDispatcher = frame;
         this.resourceServices.subscribeToEvents(this);
         this.resourceServices.subscribeToEvents(this);
         this.projectServices.subscribeToEvents(this);
         this.projectServices.subscribeToEvents(this);
+        this.uiServices.subscribeToEvents(this);
     }
     }
 
 
     /**
     /**
@@ -85,4 +89,5 @@ const serviceLocator = new ServiceLocatorType();
 export default serviceLocator;
 export default serviceLocator;
 
 
 // Load up all the internal services
 // Load up all the internal services
+serviceLocator.loadService(new ProjectBasedExtensionLoader());
 serviceLocator.loadService(new TypescriptLanguageExtension());
 serviceLocator.loadService(new TypescriptLanguageExtension());

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

@@ -0,0 +1,136 @@
+//
+// 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 * as EditorEvents from "../../editor/EditorEvents";
+
+// Duktape require isn't recognized as a function, but can be used as one
+declare function require(filename: string): any;
+
+/**
+ * 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.projectPath) + "Editor/";
+            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(".service.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);
+                    }
+                });
+            }
+        }
+    }
+}

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

@@ -76,6 +76,7 @@ class MainFrame extends ScriptWidget {
         });
         });
 
 
         // Allow the service locator to hook into the event system
         // Allow the service locator to hook into the event system
+        ServiceLocator.uiServices.setMainFrameMenu(this.menu);
         ServiceLocator.subscribeToEvents(this);
         ServiceLocator.subscribeToEvents(this);
 
 
         this.showWelcomeFrame(true);
         this.showWelcomeFrame(true);

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

@@ -25,6 +25,7 @@ import EditorEvents = require("../../../editor/EditorEvents");
 import EditorUI = require("../../EditorUI");
 import EditorUI = require("../../EditorUI");
 import MenuItemSources = require("./MenuItemSources");
 import MenuItemSources = require("./MenuItemSources");
 import Preferences = require("editor/Preferences");
 import Preferences = require("editor/Preferences");
+import ServiceLocator from "../../../hostExtensions/ServiceLocator";
 
 
 class MainFrameMenu extends Atomic.ScriptObject {
 class MainFrameMenu extends Atomic.ScriptObject {
 
 
@@ -241,12 +242,14 @@ class MainFrameMenu extends Atomic.ScriptObject {
             if (refid == "developer assetdatabase scan") {
             if (refid == "developer assetdatabase scan") {
 
 
               ToolCore.assetDatabase.scan();
               ToolCore.assetDatabase.scan();
+              return true;
 
 
             }
             }
 
 
             if (refid == "developer assetdatabase force") {
             if (refid == "developer assetdatabase force") {
 
 
               ToolCore.assetDatabase.reimportAllAssets();
               ToolCore.assetDatabase.reimportAllAssets();
+              return true;
 
 
             }
             }
 
 
@@ -257,8 +260,12 @@ class MainFrameMenu extends Atomic.ScriptObject {
                 myPrefs.saveEditorWindowData(myPrefs.editorWindow);
                 myPrefs.saveEditorWindowData(myPrefs.editorWindow);
                 myPrefs.savePlayerWindowData(myPrefs.playerWindow);
                 myPrefs.savePlayerWindowData(myPrefs.playerWindow);
                 Atomic.getEngine().exit();
                 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") {
         } else if (target.id == "menu tools popup") {
 
 
             if (refid == "tools toggle profiler") {
             if (refid == "tools toggle profiler") {
@@ -309,6 +316,8 @@ class MainFrameMenu extends Atomic.ScriptObject {
                 return true;
                 return true;
             }
             }
 
 
+        } else {
+            console.log("Menu: " + target.id + " clicked");
         }
         }
 
 
     }
     }

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

@@ -64,6 +64,13 @@ class ServiceRegistry<T extends Editor.Extensions.EditorService> implements Edit
     register(service: T) {
     register(service: T) {
         this.registeredServices.push(service);
         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> {
 export class ExtensionServiceRegistry extends ServiceRegistry<Editor.ClientExtensions.WebViewService> {

+ 1 - 0
Script/AtomicWebViewEditor/tsconfig.json

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

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

@@ -210,8 +210,9 @@ declare module Editor.HostExtensions {
      * or by the editor itself.
      * or by the editor itself.
      */
      */
     export interface HostServiceLocator extends Editor.Extensions.ServiceLoader {
     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 {
     export interface HostEditorService extends Editor.Extensions.EditorService {
@@ -226,12 +227,23 @@ declare module Editor.HostExtensions {
         delete?(ev: EditorEvents.DeleteResourceEvent);
         delete?(ev: EditorEvents.DeleteResourceEvent);
         rename?(ev: EditorEvents.RenameResourceEvent);
         rename?(ev: EditorEvents.RenameResourceEvent);
     }
     }
+    export interface ResourceServiceRegistry extends Editor.Extensions.ServiceRegistry<ResourceService> { }
 
 
     export interface ProjectService extends Editor.Extensions.EditorService {
     export interface ProjectService extends Editor.Extensions.EditorService {
         projectUnloaded?();
         projectUnloaded?();
         projectLoaded?(ev: EditorEvents.LoadProjectEvent);
         projectLoaded?(ev: EditorEvents.LoadProjectEvent);
         playerStarted?();
         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);
+        menuItemClicked(refId: string): boolean;
+    }
 }
 }
 
 
 /**
 /**

+ 2 - 0
Script/tsconfig.json

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