diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index ca9ceb7bdd..8e20c283c0 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -39,7 +39,6 @@ define([ "./src/policies/EditableMovePolicy", "./src/policies/EditContextualActionPolicy", "./src/representers/EditRepresenter", - "./src/representers/EditToolbarRepresenter", "./src/capabilities/EditorCapability", "./src/capabilities/TransactionCapabilityDecorator", "./src/services/TransactionManager", @@ -78,7 +77,6 @@ define([ EditableMovePolicy, EditContextualActionPolicy, EditRepresenter, - EditToolbarRepresenter, EditorCapability, TransactionCapabilityDecorator, TransactionManager, @@ -381,12 +379,6 @@ define([ "depends": [ "$log" ] - }, - { - "implementation": EditToolbarRepresenter, - "depends": [ - "openmct" - ] } ], "constants": [ @@ -424,6 +416,17 @@ define([ "transactionService" ] } + ], + "runs": [ + { + depends: [ + "toolbars[]", + "openmct" + ], + implementation: function (toolbars, openmct) { + toolbars.forEach(openmct.toolbars.addProvider, openmct.toolbars); + } + } ] } }); diff --git a/platform/commonUI/edit/res/templates/edit-object.html b/platform/commonUI/edit/res/templates/edit-object.html index ffdc84e12a..cbfd73a577 100644 --- a/platform/commonUI/edit/res/templates/edit-object.html +++ b/platform/commonUI/edit/res/templates/edit-object.html @@ -24,7 +24,8 @@
+ class="flex-elem l-back"> + @@ -48,8 +49,8 @@
0) { - this.selection[0] = p; - } - return this.selection[0]; - }; - - /** - * Get an array containing all selections, including the - * selection proxy. It is generally not advisable to - * mutate this array directly. - * @returns {Array} all selections - */ - EditToolbarSelection.prototype.all = function () { - return this.selection; - }; - - return EditToolbarSelection; - } -); diff --git a/platform/commonUI/edit/test/representers/EditToolbarRepresenterSpec.js b/platform/commonUI/edit/test/representers/EditToolbarRepresenterSpec.js deleted file mode 100644 index 5a99bb5c8a..0000000000 --- a/platform/commonUI/edit/test/representers/EditToolbarRepresenterSpec.js +++ /dev/null @@ -1,156 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - ["../../src/representers/EditToolbarRepresenter"], - function (EditToolbarRepresenter) { - - describe("The Edit mode toolbar representer", function () { - var mockScope, - mockElement, - testAttrs, - mockUnwatch, - representer, - mockOpenMCT, - mockSelection; - - beforeEach(function () { - mockScope = jasmine.createSpyObj( - '$scope', - ['$on', '$watch', '$watchCollection', "commit", "$apply"] - ); - mockElement = {}; - testAttrs = { toolbar: 'testToolbar' }; - mockScope.$parent = jasmine.createSpyObj( - '$parent', - ['$watch', '$watchCollection'] - ); - mockUnwatch = jasmine.createSpy('unwatch'); - - mockScope.$parent.$watchCollection.andReturn(mockUnwatch); - - mockSelection = jasmine.createSpyObj("selection", [ - 'on', - 'off', - 'get' - ]); - mockSelection.get.andReturn([]); - mockOpenMCT = { - selection: mockSelection - }; - - representer = new EditToolbarRepresenter( - mockOpenMCT, - mockScope, - mockElement, - testAttrs - ); - }); - - it("exposes toolbar state under a attr-defined name", function () { - // A structure/state object should have been added to the - // parent scope under the name provided in the "toolbar" - // attribute - expect(mockScope.$parent.testToolbar).toBeDefined(); - }); - - it("is robust against lack of a toolbar definition", function () { - expect(function () { - representer.represent({}); - }).not.toThrow(); - }); - - it("watches for toolbar state changes", function () { - representer.represent({}); - expect(mockScope.$watchCollection).toHaveBeenCalledWith( - jasmine.any(Function), - jasmine.any(Function) - ); - expect(mockScope.$watchCollection.calls[0].args[0]()) - .toBe(mockScope.$parent.testToolbar.state); - }); - - it("removes state from parent scope on destroy", function () { - // Verify precondition - expect(mockScope.$parent.testToolbar).toBeDefined(); - // Destroy the representer - representer.destroy(); - // Should have removed toolbar state from view - expect(mockScope.$parent.testToolbar).toBeUndefined(); - }); - - // Verify a simple interaction between selection state and toolbar - // state; more complicated interactions are tested in EditToolbar. - it("conveys state changes", function () { - var testObject = { k: 123 }; - - // Provide a view which has a toolbar - representer.represent({ - toolbar: { sections: [{ items: [{ property: 'k' }] }] } - }); - - // Update the selection - mockScope.selection.select(testObject); - expect(mockScope.$watchCollection.mostRecentCall.args[0]) - .toEqual('selection.all()'); // Make sure we're using right watch - mockScope.$watchCollection.mostRecentCall.args[1]([testObject]); - - // Update the state - mockScope.$parent.testToolbar.state[0] = 456; - // Invoke the first watch (assumed to be for toolbar state) - mockScope.$watchCollection.calls[0].args[1]( - mockScope.$parent.testToolbar.state - ); - - // Should have updated the original object - expect(testObject.k).toEqual(456); - - // Should have committed the change - expect(mockScope.commit).toHaveBeenCalled(); - }); - - it("does not commit if nothing changed", function () { - var testObject = { k: 123 }; - - // Provide a view which has a toolbar - representer.represent({ - toolbar: { sections: [{ items: [{ property: 'k' }] }] } - }); - - // Update the selection - mockScope.selection.select(testObject); - expect(mockScope.$watchCollection.mostRecentCall.args[0]) - .toEqual('selection.all()'); // Make sure we're using right watch - mockScope.$watchCollection.mostRecentCall.args[1]([testObject]); - - // Invoke the first watch (assumed to be for toolbar state) - mockScope.$watchCollection.calls[0].args[1]( - mockScope.$parent.testToolbar.state - ); - - // Should have committed the change - expect(mockScope.commit).not.toHaveBeenCalled(); - }); - - }); - } -); diff --git a/platform/commonUI/edit/test/representers/EditToolbarSelectionSpec.js b/platform/commonUI/edit/test/representers/EditToolbarSelectionSpec.js deleted file mode 100644 index 7754744325..0000000000 --- a/platform/commonUI/edit/test/representers/EditToolbarSelectionSpec.js +++ /dev/null @@ -1,128 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2018, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - ['../../src/representers/EditToolbarSelection'], - function (EditToolbarSelection) { - - describe("The Edit mode selection manager", function () { - var testProxy, - testElement, - otherElement, - selection, - mockSelection, - mockOpenMCT, - mockScope; - - beforeEach(function () { - testProxy = { someKey: "some value" }; - testElement = { someOtherKey: "some other value" }; - otherElement = { yetAnotherKey: 42 }; - mockSelection = jasmine.createSpyObj("selection", [ - // 'select', - 'on', - 'off', - 'get' - ]); - mockSelection.get.andReturn([]); - mockOpenMCT = { - selection: mockSelection - }; - mockScope = jasmine.createSpyObj('$scope', [ - '$on', - '$apply' - ]); - - selection = new EditToolbarSelection(mockScope, mockOpenMCT); - selection.proxy(testProxy); - }); - - it("adds the proxy to the selection array", function () { - expect(selection.all()).toEqual([testProxy]); - }); - - it("exposes view proxy", function () { - expect(selection.proxy()).toBe(testProxy); - }); - - it("includes selected objects alongside the proxy", function () { - selection.select(testElement); - expect(selection.all()).toEqual([testProxy, testElement]); - }); - - it("allows elements to be deselected", function () { - selection.select(testElement); - selection.deselect(); - expect(selection.all()).toEqual([testProxy]); - }); - - it("replaces old selections with new ones", function () { - selection.select(testElement); - selection.select(otherElement); - expect(selection.all()).toEqual([testProxy, otherElement]); - }); - - it("allows retrieval of the current selection", function () { - selection.select(testElement); - expect(selection.get()).toBe(testElement); - selection.select(otherElement); - expect(selection.get()).toBe(otherElement); - }); - - it("can check if an element is selected", function () { - selection.select(testElement); - expect(selection.selected(testElement)).toBeTruthy(); - expect(selection.selected(otherElement)).toBeFalsy(); - selection.select(otherElement); - expect(selection.selected(testElement)).toBeFalsy(); - expect(selection.selected(otherElement)).toBeTruthy(); - }); - - it("considers the proxy to be selected", function () { - expect(selection.selected(testProxy)).toBeTruthy(); - selection.select(testElement); - // Even when something else is selected... - expect(selection.selected(testProxy)).toBeTruthy(); - }); - - it("treats selection of the proxy as a no-op", function () { - selection.select(testProxy); - expect(selection.all()).toEqual([testProxy]); - }); - - it("cleans up selection on scope destroy", function () { - expect(mockScope.$on).toHaveBeenCalledWith( - '$destroy', - jasmine.any(Function) - ); - - mockScope.$on.mostRecentCall.args[1](); - - expect(mockOpenMCT.selection.off).toHaveBeenCalledWith( - 'change', - jasmine.any(Function) - ); - }); - - }); - } -); diff --git a/platform/commonUI/edit/test/representers/EditToolbarSpec.js b/platform/commonUI/edit/test/representers/EditToolbarSpec.js index eac8c894b9..922567b192 100644 --- a/platform/commonUI/edit/test/representers/EditToolbarSpec.js +++ b/platform/commonUI/edit/test/representers/EditToolbarSpec.js @@ -25,7 +25,10 @@ define( function (EditToolbar) { describe("An Edit mode toolbar", function () { - var mockCommit, + var mockOpenMCT, + mockScope, + mockObjects, + mockDomainObject, testStructure, testAB, testABC, @@ -35,35 +38,30 @@ define( testM, toolbar; - function getVisibility(obj) { - return !obj.hidden; - } - beforeEach(function () { - mockCommit = jasmine.createSpy('commit'); - testStructure = { - sections: [ - { - items: [ - { name: "A", property: "a", exclusive: true }, - { name: "B", property: "b", exclusive: true }, - { name: "C", property: "c", exclusive: true } - ] - }, - { - items: [ - { name: "X", property: "x" }, - { name: "Y", property: "y", exclusive: true }, - { name: "Z", property: "z", exclusive: true } - ] - }, - { - items: [ - { name: "M", method: "m", exclusive: true } - ] - } - ] - }; + mockOpenMCT = jasmine.createSpy('openmct', ['objects']); + mockObjects = jasmine.createSpyObj('objects', ['observe']); + mockObjects.observe.andReturn(); + mockOpenMCT.objects = mockObjects; + mockScope = jasmine.createSpyObj("$scope", [ + "$watchCollection", + "$on" + ]); + mockScope.$watchCollection.andReturn(); + mockDomainObject = jasmine.createSpyObj("domainObject", [ + 'identifier' + ]); + + testStructure = [ + { name: "A", property: "a", domainObject: mockDomainObject }, + { name: "B", property: "b", domainObject: mockDomainObject }, + { name: "C", property: "c", domainObject: mockDomainObject }, + { name: "X", property: "x", domainObject: mockDomainObject }, + { name: "Y", property: "y", domainObject: mockDomainObject }, + { name: "Z", property: "z", domainObject: mockDomainObject }, + { name: "M", method: "m", domainObject: mockDomainObject } + ]; + testAB = { a: 0, b: 1 }; testABC = { a: 0, b: 1, c: 2 }; testABC2 = { a: 4, b: 1, c: 2 }; // For inconsistent-state checking @@ -71,151 +69,17 @@ define( testABCYZ = { a: 0, b: 1, c: 2, y: 'Y!', z: 'Z!' }; testM = { m: jasmine.createSpy("method") }; - toolbar = new EditToolbar(testStructure, mockCommit); - }); - - it("provides properties from the original structure", function () { - expect( - new EditToolbar(testStructure, [testABC]) - .getStructure() - .sections[0] - .items[1] - .name - ).toEqual("B"); - }); - - // This is needed by mct-toolbar - it("adds keys to form structure", function () { - expect( - new EditToolbar(testStructure, [testABC]) - .getStructure() - .sections[0] - .items[1] - .key - ).not.toBeUndefined(); - }); - - it("marks empty sections as hidden", function () { - // Verify that all sections are included when applicable... - toolbar.setSelection([testABCXYZ]); - expect(toolbar.getStructure().sections.map(getVisibility)) - .toEqual([true, true, false]); - - // ...but omitted when only some are applicable - toolbar.setSelection([testABC]); - expect(toolbar.getStructure().sections.map(getVisibility)) - .toEqual([true, false, false]); - }); - - it("reads properties from selections", function () { - var structure, state; - - toolbar.setSelection([testABC]); - - structure = toolbar.getStructure(); - state = toolbar.getState(); - - expect(state[structure.sections[0].items[0].key]) - .toEqual(testABC.a); - expect(state[structure.sections[0].items[1].key]) - .toEqual(testABC.b); - expect(state[structure.sections[0].items[2].key]) - .toEqual(testABC.c); - }); - - it("reads properties from getters", function () { - var structure, state; - - testABC.a = function () { - return "from a getter!"; - }; - - toolbar.setSelection([testABC]); - structure = toolbar.getStructure(); - state = toolbar.getState(); - - expect(state[structure.sections[0].items[0].key]) - .toEqual("from a getter!"); - }); - - it("sets properties on update", function () { - toolbar.setSelection([testABC]); - toolbar.updateState( - toolbar.getStructure().sections[0].items[0].key, - "new value" - ); - // Should have updated the underlying object - expect(testABC.a).toEqual("new value"); - }); - - it("invokes setters on update", function () { - var structure; - - testABC.a = jasmine.createSpy('a'); - - toolbar.setSelection([testABC]); - structure = toolbar.getStructure(); - - toolbar.updateState( - structure.sections[0].items[0].key, - "new value" - ); - // Should have updated the underlying object - expect(testABC.a).toHaveBeenCalledWith("new value"); - }); - - it("provides a return value describing update status", function () { - // Should return true if actually updated, otherwise false - var key; - toolbar.setSelection([testABC]); - key = toolbar.getStructure().sections[0].items[0].key; - expect(toolbar.updateState(key, testABC.a)).toBeFalsy(); - expect(toolbar.updateState(key, "new value")).toBeTruthy(); - }); - - it("removes inapplicable items", function () { - // First, verify with all items - toolbar.setSelection([testABC]); - expect(toolbar.getStructure().sections[0].items.map(getVisibility)) - .toEqual([true, true, true]); - // Then, try with some items omitted - toolbar.setSelection([testABC, testAB]); - expect(toolbar.getStructure().sections[0].items.map(getVisibility)) - .toEqual([true, true, false]); - }); - - it("removes inconsistent states", function () { - // Only two of three values match among these selections - toolbar.setSelection([testABC, testABC2]); - expect(toolbar.getStructure().sections[0].items.map(getVisibility)) - .toEqual([false, true, true]); - }); - - it("allows inclusive items", function () { - // One inclusive item is in the set, property 'x' of the - // second section; make sure items are pruned down - // when only some of the selection has x,y,z properties - toolbar.setSelection([testABC, testABCXYZ]); - expect(toolbar.getStructure().sections[1].items.map(getVisibility)) - .toEqual([true, false, false]); - }); - - it("removes inclusive items when there are no matches", function () { - toolbar.setSelection([testABCYZ]); - expect(toolbar.getStructure().sections[1].items.map(getVisibility)) - .toEqual([false, true, true]); + toolbar = new EditToolbar(mockScope, mockOpenMCT, testStructure); }); it("adds click functions when a method is specified", function () { - toolbar.setSelection([testM]); - // Verify precondition - expect(testM.m).not.toHaveBeenCalled(); - // Click! - toolbar.getStructure().sections[2].items[0].click(); - // Should have called the underlying function - expect(testM.m).toHaveBeenCalled(); - // Should also have committed the change - expect(mockCommit).toHaveBeenCalled(); + var structure = toolbar.getStructure(); + expect(structure[6].click).toBeDefined(); + }); + + it("adds key for controls that define a property", function () { + var structure = toolbar.getStructure(); + expect(structure[0].key).toEqual(0); }); }); } diff --git a/platform/commonUI/general/src/directives/MCTSelectable.js b/platform/commonUI/general/src/directives/MCTSelectable.js index 212b644a04..d9891f9378 100644 --- a/platform/commonUI/general/src/directives/MCTSelectable.js +++ b/platform/commonUI/general/src/directives/MCTSelectable.js @@ -28,6 +28,16 @@ define( * The mct-selectable directive allows selection functionality * (click) to be attached to specific elements. * + * Example of how to use the directive: + * + * mct-selectable="{ + * // item is an optional domain object. + * item: domainObject, + * // Can define other arbitrary properties. + * elementProxy: element, + * controller: fixedController + * }" + * * @memberof platform/commonUI/general * @constructor */ diff --git a/platform/features/fixed/bundle.js b/platform/features/fixed/bundle.js index 24ef20ca87..8901790fbf 100644 --- a/platform/features/fixed/bundle.js +++ b/platform/features/fixed/bundle.js @@ -39,240 +39,309 @@ define([ "cssClass": "icon-box-with-dashed-lines", "type": "telemetry.fixed", "template": fixedTemplate, - "uses": [ - "composition" - ], - "editable": true, - "toolbar": { - "sections": [ + "uses": [], + "editable": true + } + ], + "toolbars": [ + { + name: "Fixed Position Toolbar", + key: "fixed.position", + description: "Toolbar for the selected element inside a fixed position display.", + forSelection: function (selection) { + if (!selection) { + return; + } + + return ( + selection[0] && selection[0].context.elementProxy && + selection[1] && selection[1].context.item.type === 'telemetry.fixed' || + selection[0] && selection[0].context.item.type === 'telemetry.fixed' + ); + }, + toolbar: function (selection) { + var imageProperties = ["add", "remove", "order", "stroke", "useGrid", "x", "y", "height", "width", "url"]; + var boxProperties = ["add", "remove", "order", "stroke", "useGrid", "x", "y", "height", "width", "fill"]; + var textProperties = ["add", "remove", "order", "stroke", "useGrid", "x", "y", "height", "width", "fill", "color", "size", "text"]; + var lineProperties = ["add", "remove", "order", "stroke", "useGrid", "x", "y", "x2", "y2"]; + var telemetryProperties = ["add", "remove", "order", "stroke", "useGrid", "x", "y", "height", "width", "fill", "color", "size", "titled"]; + var fixedPageProperties = ["add"]; + + var properties = [], + fixedItem = selection[0] && selection[0].context.item, + elementProxy = selection[0] && selection[0].context.elementProxy, + domainObject = selection[1] && selection[1].context.item, + path; + + if (elementProxy) { + var type = elementProxy.element.type; + path = "configuration['fixed-display'].elements[" + elementProxy.index + "]"; + properties = + type === 'fixed.image' ? imageProperties : + type === 'fixed.text' ? textProperties : + type === 'fixed.box' ? boxProperties : + type === 'fixed.line' ? lineProperties : + type === 'fixed.telemetry' ? telemetryProperties : []; + } else if (fixedItem) { + properties = domainObject && domainObject.type === 'layout' ? [] : fixedPageProperties; + } + + return [ { - "items": [ + control: "menu-button", + domainObject: domainObject || selection[0].context.item, + method: function (value) { + selection[0].context.fixedController.add(value); + }, + key: "add", + cssClass: "icon-plus", + text: "Add", + options: [ { - "method": "add", - "cssClass": "icon-plus", - "control": "menu-button", - "text": "Add", - "options": [ - { - "name": "Box", - "cssClass": "icon-box", - "key": "fixed.box" - }, - { - "name": "Line", - "cssClass": "icon-line-horz", - "key": "fixed.line" - }, - { - "name": "Text", - "cssClass": "icon-T", - "key": "fixed.text" - }, - { - "name": "Image", - "cssClass": "icon-image", - "key": "fixed.image" - } - ] - } - ] - }, - { - "items": [ - { - "method": "order", - "cssClass": "icon-layers", - "control": "menu-button", - "title": "Layering", - "description": "Move the selected object above or below other objects", - "options": [ - { - "name": "Move to Top", - "cssClass": "icon-arrow-double-up", - "key": "top" - }, - { - "name": "Move Up", - "cssClass": "icon-arrow-up", - "key": "up" - }, - { - "name": "Move Down", - "cssClass": "icon-arrow-down", - "key": "down" - }, - { - "name": "Move to Bottom", - "cssClass": "icon-arrow-double-down", - "key": "bottom" - } - ] + "name": "Box", + "cssClass": "icon-box", + "key": "fixed.box" }, { - "property": "fill", - "cssClass": "icon-paint-bucket", - "title": "Fill color", - "description": "Set fill color", - "control": "color" - }, - { - "property": "stroke", + "name": "Line", "cssClass": "icon-line-horz", - "title": "Border color", - "description": "Set border color", - "control": "color" + "key": "fixed.line" }, { - "property": "url", - "cssClass": "icon-image", - "control": "dialog-button", - "title": "Image Properties", - "description": "Edit image properties", - "dialog": { - "control": "textfield", - "name": "Image URL", - "cssClass": "l-input-lg", - "required": true - } - } - ] - }, - { - "items": [ - { - "property": "color", + "name": "Text", "cssClass": "icon-T", - "title": "Text color", - "description": "Set text color", - "mandatory": true, - "control": "color" + "key": "fixed.text" }, { - "property": "size", - "title": "Text size", - "description": "Set text size", - "control": "select", - "options": [9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 30, 36, 48, 72, 96].map(function (size) { - return { "name": size + " px", "value": size + "px" }; - }) + "name": "Image", + "cssClass": "icon-image", + "key": "fixed.image" } ] }, { - "items": [ + control: "menu-button", + domainObject: domainObject, + method: function (value) { + selection[0].context.fixedController.order( + selection[0].context.elementProxy, + value + ); + }, + key: "order", + cssClass: "icon-layers", + title: "Layering", + description: "Move the selected object above or below other objects", + options: [ { - "property": "editX", - "text": "X", - "name": "X", - "cssClass": "l-input-sm", - "control": "numberfield", - "min": "0" + "name": "Move to Top", + "cssClass": "icon-arrow-double-up", + "key": "top" }, { - "property": "editY", - "text": "Y", - "name": "Y", - "cssClass": "l-input-sm", - "control": "numberfield", - "min": "0" + "name": "Move Up", + "cssClass": "icon-arrow-up", + "key": "up" }, { - "property": "editX1", - "text": "X1", - "name": "X1", - "cssClass": "l-input-sm", - "control" : "numberfield", - "min": "0" + "name": "Move Down", + "cssClass": "icon-arrow-down", + "key": "down" }, { - "property": "editY1", - "text": "Y1", - "name": "Y1", - "cssClass": "l-input-sm", - "control" : "numberfield", - "min": "0" - }, - { - "property": "editX2", - "text": "X2", - "name": "X2", - "cssClass": "l-input-sm", - "control" : "numberfield", - "min": "0" - }, - { - "property": "editY2", - "text": "Y2", - "name": "Y2", - "cssClass": "l-input-sm", - "control" : "numberfield", - "min": "0" - }, - { - "property": "editHeight", - "text": "H", - "name": "H", - "cssClass": "l-input-sm", - "control": "numberfield", - "description": "Resize object height", - "min": "1" - }, - { - "property": "editWidth", - "text": "W", - "name": "W", - "cssClass": "l-input-sm", - "control": "numberfield", - "description": "Resize object width", - "min": "1" - }, - { - "property": "useGrid", - "name": "Snap to Grid", - "control": "checkbox" + "name": "Move to Bottom", + "cssClass": "icon-arrow-double-down", + "key": "bottom" } ] }, { - "items": [ - { - "property": "text", - "cssClass": "icon-gear", - "control": "dialog-button", - "title": "Text Properties", - "description": "Edit text properties", - "dialog": { - "control": "textfield", - "name": "Text", - "required": true - } - }, - { - "method": "showTitle", - "cssClass": "icon-two-parts-both", - "control": "button", - "title": "Show title", - "description": "Show telemetry element title" - }, - { - "method": "hideTitle", - "cssClass": "icon-two-parts-one-only", - "control": "button", - "title": "Hide title", - "description": "Hide telemetry element title" - } - ] + control: "color", + domainObject: domainObject, + property: path + ".fill", + cssClass: "icon-paint-bucket", + title: "Fill color", + description: "Set fill color", + key: 'fill' }, { - "items": [ - { - "method": "remove", - "control": "button", - "cssClass": "icon-trash" - } - ] + control: "color", + domainObject: domainObject, + property: path + ".stroke", + cssClass: "icon-line-horz", + title: "Border color", + description: "Set border color", + key: 'stroke' + }, + { + control: "dialog-button", + domainObject: domainObject, + property: path + ".url", + cssClass: "icon-image", + title: "Image Properties", + description: "Edit image properties", + key: 'url', + dialog: { + control: "textfield", + name: "Image URL", + cssClass: "l-input-lg", + required: true + } + }, + { + control: "color", + domainObject: domainObject, + property: path + ".color", + cssClass: "icon-T", + title: "Text color", + mandatory: true, + description: "Set text color", + key: 'color' + }, + { + control: "select", + domainObject: domainObject, + property: path + ".size", + title: "Text size", + description: "Set text size", + "options": [9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 30, 36, 48, 72, 96].map(function (size) { + return { "name": size + " px", "value": size + "px" }; + }), + key: 'size' + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".x", + text: "X", + name: "X", + key: "x", + cssClass: "l-input-sm", + min: "0" + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".y", + text: "Y", + name: "Y", + key: "y", + cssClass: "l-input-sm", + min: "0" + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".x", + text: "X1", + name: "X1", + key: "x1", + cssClass: "l-input-sm", + min: "0" + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".y", + text: "Y1", + name: "Y1", + key: "y1", + cssClass: "l-input-sm", + min: "0" + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".x2", + text: "X2", + name: "X2", + key: "x2", + cssClass: "l-input-sm", + min: "0" + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".y2", + text: "Y2", + name: "Y2", + key: "y2", + cssClass: "l-input-sm", + min: "0" + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".height", + text: "H", + name: "H", + key: "height", + cssClass: "l-input-sm", + description: "Resize object height", + min: "1" + }, + { + control: "numberfield", + domainObject: domainObject, + property: path + ".width", + text: "W", + name: "W", + key: "width", + cssClass: "l-input-sm", + description: "Resize object width", + min: "1" + }, + { + control: "checkbox", + domainObject: domainObject, + property: path + ".useGrid", + name: "Snap to Grid", + key: "useGrid" + }, + { + control: "dialog-button", + domainObject: domainObject, + property: path + ".text", + cssClass: "icon-gear", + title: "Text Properties", + description: "Edit text properties", + key: "text", + dialog: { + control: "textfield", + name: "Text", + required: true + } + }, + { + control: "checkbox", + domainObject: domainObject, + property: path + ".titled", + name: "Show Title", + key: "titled" + }, + { + control: "button", + domainObject: domainObject, + method: function () { + selection[0].context.fixedController.remove( + selection[0].context.elementProxy + ); + }, + key: "remove", + cssClass: "icon-trash" } - ] + ].filter(function (item) { + var filtered; + + properties.forEach(function (property) { + if (item.property && item.key === property || + item.method && item.key === property) { + filtered = item; + } + }); + + return filtered; + }); } } ], diff --git a/platform/features/layout/bundle.js b/platform/features/layout/bundle.js index 589a77e817..b89cbd6459 100644 --- a/platform/features/layout/bundle.js +++ b/platform/features/layout/bundle.js @@ -62,29 +62,7 @@ define([ "type": "layout", "template": layoutTemplate, "editable": true, - "uses": [], - "toolbar": { - "sections": [ - { - "items": [ - { - "method": "showFrame", - "cssClass": "icon-frame-show", - "control": "button", - "title": "Show frame", - "description": "Show frame" - }, - { - "method": "hideFrame", - "cssClass": "icon-frame-hide", - "control": "button", - "title": "Hide frame", - "description": "Hide frame" - } - ] - } - ] - } + "uses": [] }, { "key": "fixed", @@ -305,6 +283,27 @@ define([ "implementation": LayoutCompositionPolicy } ], + "toolbars": [ + { + name: "Display Layout Toolbar", + key: "layout", + description: "A toolbar for objects inside a display layout.", + forSelection: function (selection) { + // Apply the layout toolbar if the selected object is inside a layout. + return (selection && selection[1] && selection[1].context.item.type === 'layout'); + }, + toolbar: function (selection) { + return [ + { + control: "checkbox", + name: "Show frame", + domainObject: selection[1].context.item, + property: "configuration.layout.panels[" + selection[0].context.oldItem.id + "].hasFrame" + } + ]; + } + } + ], "types": [ { "key": "layout", @@ -314,7 +313,14 @@ define([ "priority": 900, "features": "creation", "model": { - "composition": [] + "composition": [], + configuration: { + layout: { + panels: { + + } + } + } }, "properties": [ { diff --git a/platform/features/layout/res/templates/fixed.html b/platform/features/layout/res/templates/fixed.html index a25f36b7bf..e984f2d6cd 100644 --- a/platform/features/layout/res/templates/fixed.html +++ b/platform/features/layout/res/templates/fixed.html @@ -41,7 +41,7 @@ - +
diff --git a/platform/features/layout/res/templates/layout.html b/platform/features/layout/res/templates/layout.html index 4e7ba4e33b..e18c877006 100644 --- a/platform/features/layout/res/templates/layout.html +++ b/platform/features/layout/res/templates/layout.html @@ -41,7 +41,7 @@ ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }" ng-repeat="childObject in composition" ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)" - mct-selectable="controller.getContext(childObject, true)" + mct-selectable="controller.getContext(childObject)" ng-dblclick="controller.drill($event, childObject)" ng-style="controller.getFrameStyle(childObject.getId())"> diff --git a/platform/features/layout/src/FixedController.js b/platform/features/layout/src/FixedController.js index cf3be8910c..9a74dcf05d 100644 --- a/platform/features/layout/src/FixedController.js +++ b/platform/features/layout/src/FixedController.js @@ -38,6 +38,24 @@ define( var DEFAULT_DIMENSIONS = [2, 1]; + // Convert from element x/y/width/height to an + // appropriate ng-style argument, to position elements. + function convertPosition(elementProxy) { + if (elementProxy.getStyle) { + return elementProxy.getStyle(); + } + + var gridSize = elementProxy.getGridSize(); + + // Multiply position/dimensions by grid size + return { + left: (gridSize[0] * elementProxy.element.x) + 'px', + top: (gridSize[1] * elementProxy.element.y) + 'px', + width: (gridSize[0] * elementProxy.element.width) + 'px', + height: (gridSize[1] * elementProxy.element.height) + 'px' + }; + } + /** * The FixedController is responsible for supporting the * Fixed Position view. It arranges frames according to saved @@ -51,14 +69,14 @@ define( this.names = {}; // Cache names by ID this.values = {}; // Cache values by ID this.elementProxiesById = {}; - - this.telemetryObjects = []; - this.subscriptions = []; + this.telemetryObjects = {}; + this.subscriptions = {}; this.openmct = openmct; this.$element = $element; this.$scope = $scope; - - this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid; + this.dialogService = dialogService; + this.$q = $q; + this.newDomainObject = $scope.domainObject.useCapability('adapter'); this.fixedViewSelectable = false; var self = this; @@ -67,59 +85,13 @@ define( 'fetchHistoricalData', 'getTelemetry', 'setDisplayedValue', - 'subscribeToObjects', + 'subscribeToObject', 'unsubscribe', 'updateView' ].forEach(function (name) { self[name] = self[name].bind(self); }); - // Convert from element x/y/width/height to an - // appropriate ng-style argument, to position elements. - function convertPosition(elementProxy) { - var gridSize = elementProxy.getGridSize(); - // Multiply position/dimensions by grid size - return { - left: (gridSize[0] * elementProxy.x()) + 'px', - top: (gridSize[1] * elementProxy.y()) + 'px', - width: (gridSize[0] * elementProxy.width()) + 'px', - height: (gridSize[1] * elementProxy.height()) + 'px' - }; - } - - // Update the style for a selected element - function updateSelectionStyle() { - if (self.selectedElementProxy) { - self.selectedElementProxy.style = convertPosition(self.selectedElementProxy); - } - } - - // Generate a specific drag handle - function generateDragHandle(elementHandle) { - return new FixedDragHandle( - elementHandle, - self.gridSize, - updateSelectionStyle, - $scope.commit - ); - } - - // Generate drag handles for an element - function generateDragHandles(element) { - return element.handles().map(generateDragHandle); - } - - // Update element positions when grid size changes - function updateElementPositions(layoutGrid) { - // Update grid size from model - self.gridSize = layoutGrid; - - self.elementProxies.forEach(function (elementProxy) { - elementProxy.setGridSize(self.gridSize); - elementProxy.style = convertPosition(elementProxy); - }); - } - // Decorate an element for display function makeProxyElement(element, index, elements) { var ElementProxy = ElementProxies[element.type], @@ -137,14 +109,14 @@ define( // Decorate elements in the current configuration function refreshElements() { - var elements = (($scope.configuration || {}).elements || []); + var elements = (((self.newDomainObject.configuration || {})['fixed-display'] || {}).elements || []); // Create the new proxies... self.elementProxies = elements.map(makeProxyElement); - // If selection is not in array, select parent. - // Otherwise, set the element to select after refresh. if (self.selectedElementProxy) { + // If selection is not in array, select parent. + // Otherwise, set the element to select after refresh. var index = elements.indexOf(self.selectedElementProxy.element); if (index === -1) { self.$element[0].click(); @@ -168,79 +140,53 @@ define( }); } - function removeObjects(ids) { - var configuration = self.$scope.configuration; - - if (configuration && - configuration.elements) { - configuration.elements = configuration.elements.filter(function (proxy) { - return ids.indexOf(proxy.id) === -1; - }); - } - self.getTelemetry($scope.domainObject); - refreshElements(); - // Mark change as persistable - if (self.$scope.commit) { - self.$scope.commit("Objects removed."); - } - } - - // Handle changes in the object's composition - function updateComposition(composition, previousComposition) { - var removedIds = []; - // Resubscribe - objects in view have changed - if (composition !== previousComposition) { - //remove any elements no longer in the composition - removedIds = _.difference(previousComposition, composition); - if (removedIds.length > 0) { - removeObjects(removedIds); - } - } - } - // Trigger a new query for telemetry data function updateDisplayBounds(bounds, isTick) { if (!isTick) { //Reset values self.values = {}; refreshElements(); + //Fetch new data - self.fetchHistoricalData(self.telemetryObjects); + Object.values(self.telemetryObjects).forEach(function (object) { + self.fetchHistoricalData(object); + }); } } // Add an element to this view function addElement(element) { - // Ensure that configuration field is populated - $scope.configuration = $scope.configuration || {}; - // Make sure there is a "elements" field in the - // view configuration. - $scope.configuration.elements = - $scope.configuration.elements || []; - // Store the position of this element. - $scope.configuration.elements.push(element); + var index; + var elements = (((self.newDomainObject.configuration || {})['fixed-display'] || {}).elements || []); + elements.push(element); - self.elementToSelectAfterRefresh = element; - - // Refresh displayed elements - refreshElements(); - - // Mark change as persistable - if ($scope.commit) { - $scope.commit("Dropped an element."); + if (self.selectedElementProxy) { + index = elements.indexOf(self.selectedElementProxy.element); } + + self.mutate("configuration['fixed-display'].elements", elements); + elements = (self.newDomainObject.configuration)['fixed-display'].elements || []; + self.elementToSelectAfterRefresh = elements[elements.length - 1]; + + if (self.selectedElementProxy) { + // Update the selected element with the new + // value since newDomainOject is mutated. + self.selectedElementProxy.element = elements[index]; + } + refreshElements(); } // Position a panel after a drop event function handleDrop(e, id, position) { // Don't handle this event if it has already been handled - // color is set to "" to let the CSS theme determine the default color if (e.defaultPrevented) { return; } e.preventDefault(); + // Store the position of this element. + // color is set to "" to let the CSS theme determine the default color addElement({ type: "fixed.telemetry", x: Math.floor(position.x / self.gridSize[0]), @@ -254,71 +200,229 @@ define( useGrid: true }); - //Re-initialize objects, and subscribe to new object - self.getTelemetry($scope.domainObject); - } - - // Sets the selectable object in response to the selection change event. - function setSelection(selectable) { - var selection = selectable[0]; - - if (!selection) { - return; - } - - if (selection.context.elementProxy) { - self.selectedElementProxy = selection.context.elementProxy; - self.mvHandle = self.generateDragHandle(self.selectedElementProxy); - self.resizeHandles = self.generateDragHandles(self.selectedElementProxy); - } else { - // Make fixed view selectable if it's not already. - if (!self.fixedViewSelectable && selectable.length === 1) { - self.fixedViewSelectable = true; - selection.context.viewProxy = new FixedProxy(addElement, $q, dialogService); - self.openmct.selection.select(selection); - } - - self.resizeHandles = []; - self.mvHandle = undefined; - self.selectedElementProxy = undefined; - } + // Subscribe to the new object to get telemetry + self.openmct.objects.get(id).then(function (object) { + self.getTelemetry(object); + }); } this.elementProxies = []; - this.generateDragHandle = generateDragHandle; - this.generateDragHandles = generateDragHandles; - this.updateSelectionStyle = updateSelectionStyle; + this.addElement = addElement; + this.refreshElements = refreshElements; + this.fixedProxy = new FixedProxy(this.addElement, this.$q, this.dialogService); - // Detect changes to grid size - $scope.$watch("model.layoutGrid", updateElementPositions); + this.composition = this.openmct.composition.get(this.newDomainObject); + this.composition.on('add', this.onCompositionAdd, this); + this.composition.on('remove', this.onCompositionRemove, this); + this.composition.load(); // Position panes where they are dropped $scope.$on("mctDrop", handleDrop); - // Position panes when the model field changes - $scope.$watch("model.composition", updateComposition); - - // Refresh list of elements whenever model changes - $scope.$watch("model.modified", refreshElements); - - // Subscribe to telemetry when an object is available - $scope.$watch("domainObject", this.getTelemetry); - - // Free up subscription on destroy - $scope.$on("$destroy", function () { - self.unsubscribe(); - self.openmct.time.off("bounds", updateDisplayBounds); - self.openmct.selection.off("change", setSelection); - }); + $scope.$on("$destroy", this.destroy.bind(this)); // Respond to external bounds changes this.openmct.time.on("bounds", updateDisplayBounds); - this.openmct.selection.on('change', setSelection); - this.$element.on('click', this.bypassSelection.bind(this)); - setSelection(this.openmct.selection.get()); + this.openmct.selection.on('change', this.setSelection.bind(this)); + this.$element.on('click', this.bypassSelection.bind(this)); + this.unlisten = this.openmct.objects.observe(this.newDomainObject, '*', function (obj) { + this.newDomainObject = JSON.parse(JSON.stringify(obj)); + this.updateElementPositions(this.newDomainObject.layoutGrid); + }.bind(this)); + + this.updateElementPositions(this.newDomainObject.layoutGrid); + refreshElements(); } + FixedController.prototype.updateElementPositions = function (layoutGrid) { + this.gridSize = layoutGrid; + + this.elementProxies.forEach(function (elementProxy) { + elementProxy.setGridSize(this.gridSize); + elementProxy.style = convertPosition(elementProxy); + }.bind(this)); + }; + + FixedController.prototype.onCompositionAdd = function (object) { + this.getTelemetry(object); + }; + + FixedController.prototype.onCompositionRemove = function (identifier) { + // Defer mutation of newDomainObject to prevent mutating an + // outdated version since this is triggered by a composition change. + setTimeout(function () { + var id = objectUtils.makeKeyString(identifier); + var elements = this.newDomainObject.configuration['fixed-display'].elements || []; + var newElements = elements.filter(function (proxy) { + return proxy.id !== id; + }); + this.mutate("configuration['fixed-display'].elements", newElements); + + if (this.subscriptions[id]) { + this.subscriptions[id](); + delete this.subscriptions[id]; + } + + delete this.telemetryObjects[id]; + this.refreshElements(); + }.bind(this)); + }; + + /** + * Removes an element from the view. + * + * @param {Object} elementProxy the element proxy to remove. + */ + FixedController.prototype.remove = function (elementProxy) { + var element = elementProxy.element; + var elements = this.newDomainObject.configuration['fixed-display'].elements || []; + elements.splice(elements.indexOf(element), 1); + + if (element.type === 'fixed.telemetry') { + this.newDomainObject.composition = this.newDomainObject.composition.filter(function (identifier) { + return objectUtils.makeKeyString(identifier) !== element.id; + }); + } + + this.mutate("configuration['fixed-display'].elements", elements); + this.refreshElements(); + + }; + + /** + * Adds a new element to the view. + * + * @param {string} type the type of element to add. Supported types are: + * `fixed.image` + * `fixed.box` + * `fixed.text` + * `fixed.line` + */ + FixedController.prototype.add = function (type) { + this.fixedProxy.add(type); + }; + + /** + * Change the display order of the element proxy. + */ + FixedController.prototype.order = function (elementProxy, position) { + var elements = elementProxy.order(position); + + // Find the selected element index in the updated array. + var selectedElemenetIndex = elements.indexOf(this.selectedElementProxy.element); + + this.mutate("configuration['fixed-display'].elements", elements); + elements = (this.newDomainObject.configuration)['fixed-display'].elements || []; + + // Update the selected element with the new + // value since newDomainOject is mutated. + this.selectedElementProxy.element = elements[selectedElemenetIndex]; + this.refreshElements(); + }; + + FixedController.prototype.generateDragHandle = function (elementProxy, elementHandle) { + var index = this.elementProxies.indexOf(elementProxy); + + if (elementHandle) { + elementHandle.element = elementProxy.element; + elementProxy = elementHandle; + } + + return new FixedDragHandle( + elementProxy, + "configuration['fixed-display'].elements[" + index + "]", + this + ); + }; + + FixedController.prototype.generateDragHandles = function (elementProxy) { + return elementProxy.handles().map(function (handle) { + return this.generateDragHandle(elementProxy, handle); + }, this); + }; + + FixedController.prototype.updateSelectionStyle = function () { + this.selectedElementProxy.style = convertPosition(this.selectedElementProxy); + }; + + FixedController.prototype.setSelection = function (selectable) { + var selection = selectable[0]; + + if (this.selectionListeners) { + this.selectionListeners.forEach(function (l) { + l(); + }); + } + + this.selectionListeners = []; + + if (!selection) { + return; + } + + if (selection.context.elementProxy) { + this.selectedElementProxy = selection.context.elementProxy; + this.attachSelectionListeners(); + this.mvHandle = this.generateDragHandle(this.selectedElementProxy); + this.resizeHandles = this.generateDragHandles(this.selectedElementProxy); + } else { + // Make fixed view selectable if it's not already. + if (!this.fixedViewSelectable && selectable.length === 1) { + this.fixedViewSelectable = true; + selection.context.fixedController = this; + this.openmct.selection.select(selection); + } + + this.resizeHandles = []; + this.mvHandle = undefined; + this.selectedElementProxy = undefined; + } + }; + + FixedController.prototype.attachSelectionListeners = function () { + var index = this.elementProxies.indexOf(this.selectedElementProxy); + var path = "configuration['fixed-display'].elements[" + index + "]"; + + this.selectionListeners.push(this.openmct.objects.observe(this.newDomainObject, path + ".useGrid", function (newValue) { + if (this.selectedElementProxy.useGrid() !== newValue) { + this.selectedElementProxy.useGrid(newValue); + this.updateSelectionStyle(); + this.openmct.objects.mutate(this.newDomainObject, path, this.selectedElementProxy.element); + } + }.bind(this))); + [ + "width", + "height", + "stroke", + "fill", + "x", + "y", + "x1", + "y1", + "x2", + "y2", + "color", + "size", + "text", + "titled" + ].forEach(function (property) { + this.selectionListeners.push(this.openmct.objects.observe(this.newDomainObject, path + "." + property, function (newValue) { + this.selectedElementProxy.element[property] = newValue; + this.updateSelectionStyle(); + }.bind(this))); + }.bind(this)); + }; + + FixedController.prototype.destroy = function () { + this.unsubscribe(); + this.unlisten(); + this.openmct.time.off("bounds", this.updateDisplayBounds); + this.openmct.selection.off("change", this.setSelection); + this.composition.off('add', this.onCompositionAdd, this); + this.composition.off('remove', this.onCompositionRemove, this); + }; + /** * A rate-limited digest function. Caps digests at 60Hz * @private @@ -340,31 +444,30 @@ define( * @private */ FixedController.prototype.unsubscribe = function () { - this.subscriptions.forEach(function (unsubscribeFunc) { + Object.values(this.subscriptions).forEach(function (unsubscribeFunc) { unsubscribeFunc(); }); - this.subscriptions = []; - this.telemetryObjects = []; + this.subscriptions = {}; + this.telemetryObjects = {}; }; /** - * Subscribe to all given domain objects + * Subscribe to the given domain object * @private - * @param {object[]} objects Domain objects to subscribe to - * @returns {object[]} The provided objects, for chaining. + * @param {object} object Domain object to subscribe to + * @returns {object} The provided object, for chaining. */ - FixedController.prototype.subscribeToObjects = function (objects) { + FixedController.prototype.subscribeToObject = function (object) { var self = this; var timeAPI = this.openmct.time; + var id = objectUtils.makeKeyString(object.identifier); + this.subscriptions[id] = self.openmct.telemetry.subscribe(object, function (datum) { + if (timeAPI.clock() !== undefined) { + self.updateView(object, datum); + } + }, {}); - this.subscriptions = objects.map(function (object) { - return self.openmct.telemetry.subscribe(object, function (datum) { - if (timeAPI.clock() !== undefined) { - self.updateView(object, datum); - } - }, {}); - }); - return objects; + return object; }; /** @@ -416,23 +519,22 @@ define( }; /** - * Request the last historical data point for the given domain objects - * @param {object[]} objects - * @returns {object[]} the provided objects for chaining. + * Request the last historical data point for the given domain object + * @param {object} object + * @returns {object} the provided object for chaining. */ - FixedController.prototype.fetchHistoricalData = function (objects) { + FixedController.prototype.fetchHistoricalData = function (object) { var bounds = this.openmct.time.bounds(); var self = this; - objects.forEach(function (object) { - self.openmct.telemetry.request(object, {start: bounds.start, end: bounds.end, size: 1}) - .then(function (data) { - if (data.length > 0) { - self.updateView(object, data[data.length - 1]); - } - }); - }); - return objects; + self.openmct.telemetry.request(object, {start: bounds.start, end: bounds.end, size: 1}) + .then(function (data) { + if (data.length > 0) { + self.updateView(object, data[data.length - 1]); + } + }); + + return object; }; @@ -457,33 +559,25 @@ define( }; FixedController.prototype.getTelemetry = function (domainObject) { - var newObject = domainObject.useCapability('adapter'); - var self = this; + var id = objectUtils.makeKeyString(domainObject.identifier); - if (this.subscriptions.length > 0) { - this.unsubscribe(); + if (this.subscriptions[id]) { + this.subscriptions[id](); + delete this.subscriptions[id]; + } + delete this.telemetryObjects[id]; + + if (!this.openmct.telemetry.isTelemetryObject(domainObject)) { + return; } - function filterForTelemetryObjects(objects) { - return objects.filter(function (object) { - return self.openmct.telemetry.isTelemetryObject(object); - }); - } + // Initialize display + this.telemetryObjects[id] = domainObject; + this.setDisplayedValue(domainObject, ""); - function initializeDisplay(objects) { - self.telemetryObjects = objects; - objects.forEach(function (object) { - // Initialize values - self.setDisplayedValue(object, ""); - }); - return objects; - } - - return this.openmct.composition.get(newObject).load() - .then(filterForTelemetryObjects) - .then(initializeDisplay) + return Promise.resolve(domainObject) .then(this.fetchHistoricalData) - .then(this.subscribeToObjects); + .then(this.subscribeToObject); }; /** @@ -580,12 +674,12 @@ define( * Gets the selection context. * * @param elementProxy the element proxy - * @returns {object} the context object which includes elementProxy and toolbar + * @returns {object} the context object which includes elementProxy */ FixedController.prototype.getContext = function (elementProxy) { return { elementProxy: elementProxy, - toolbar: elementProxy + fixedController: this }; }; @@ -608,6 +702,10 @@ define( } }; + FixedController.prototype.mutate = function (path, value) { + this.openmct.objects.mutate(this.newDomainObject, path, value); + }; + return FixedController; } ); diff --git a/platform/features/layout/src/FixedDragHandle.js b/platform/features/layout/src/FixedDragHandle.js index ba230e2ec1..d371efcf20 100644 --- a/platform/features/layout/src/FixedDragHandle.js +++ b/platform/features/layout/src/FixedDragHandle.js @@ -24,30 +24,34 @@ define( [], function () { - // Drag handle dimensions var DRAG_HANDLE_SIZE = [6, 6]; /** * Template-displayable drag handle for an element in fixed * position mode. + * + * @param elementHandle the element handle + * @param configPath the configuration path of an element + * @param {Object} fixedControl the fixed controller * @memberof platform/features/layout * @constructor */ - function FixedDragHandle(elementHandle, gridSize, update, commit) { + function FixedDragHandle(elementHandle, configPath, fixedControl) { this.elementHandle = elementHandle; - this.gridSize = gridSize; - this.update = update; - this.commit = commit; + this.configPath = configPath; + this.fixedControl = fixedControl; } /** * Get a CSS style to position this drag handle. + * * @returns CSS style object (for `ng-style`) * @memberof platform/features/layout.FixedDragHandle# */ FixedDragHandle.prototype.style = function () { var gridSize = this.elementHandle.getGridSize(); + // Adjust from grid to pixel coordinates var x = this.elementHandle.x() * gridSize[0], y = this.elementHandle.y() * gridSize[1]; @@ -75,23 +79,20 @@ define( /** * Continue a drag gesture; update x/y positions. - * @param {number[]} delta x/y pixel difference since drag - * started + * + * @param {number[]} delta x/y pixel difference since drag started */ FixedDragHandle.prototype.continueDrag = function (delta) { var gridSize = this.elementHandle.getGridSize(); + if (this.dragging) { // Update x/y positions (snapping to grid) - this.elementHandle.x( - this.dragging.x + Math.round(delta[0] / gridSize[0]) - ); - this.elementHandle.y( - this.dragging.y + Math.round(delta[1] / gridSize[1]) - ); - // Invoke update callback - if (this.update) { - this.update(); - } + var newX = this.dragging.x + Math.round(delta[0] / gridSize[0]); + var newY = this.dragging.y + Math.round(delta[1] / gridSize[1]); + + this.elementHandle.x(Math.max(0, newX)); + this.elementHandle.y(Math.max(0, newY)); + this.fixedControl.updateSelectionStyle(); } }; @@ -100,12 +101,8 @@ define( * concludes to trigger commit of changes. */ FixedDragHandle.prototype.endDrag = function () { - // Clear cached state this.dragging = undefined; - // Mark change as complete - if (this.commit) { - this.commit("Dragged handle."); - } + this.fixedControl.mutate(this.configPath, this.elementHandle.element); }; return FixedDragHandle; diff --git a/platform/features/layout/src/LayoutController.js b/platform/features/layout/src/LayoutController.js index 9130f59c70..b157770ff8 100644 --- a/platform/features/layout/src/LayoutController.js +++ b/platform/features/layout/src/LayoutController.js @@ -78,29 +78,30 @@ define( } $scope.configuration = $scope.configuration || {}; - $scope.configuration.panels = - $scope.configuration.panels || {}; + $scope.configuration.panels = $scope.configuration.panels || {}; - $scope.configuration.panels[id] = { - position: [ - Math.floor(position.x / self.gridSize[0]), - Math.floor(position.y / self.gridSize[1]) - ], - dimensions: self.defaultDimensions() - }; + self.openmct.objects.get(id).then(function (object) { + $scope.configuration.panels[id] = { + position: [ + Math.floor(position.x / self.gridSize[0]), + Math.floor(position.y / self.gridSize[1]) + ], + dimensions: self.defaultDimensions(), + hasFrame: self.getDefaultFrame(object.type) + }; - // Store the id so that the newly-dropped object - // gets selected during refresh composition - self.droppedIdToSelectAfterRefresh = id; + // Store the id so that the newly-dropped object + // gets selected during refresh composition + self.droppedIdToSelectAfterRefresh = id; + + self.commit(); + + // Populate template-facing position for this id + self.rawPositions[id] = $scope.configuration.panels[id]; + self.populatePosition(id); + refreshComposition(); + }); - // Mark change as persistable - if ($scope.commit) { - $scope.commit("Dropped a frame."); - } - // Populate template-facing position for this id - self.rawPositions[id] = - $scope.configuration.panels[id]; - self.populatePosition(id); // Layout may contain embedded views which will // listen for drops, so call preventDefault() so // that they can recognize that this event is handled. @@ -157,10 +158,7 @@ define( $scope.configuration.panels[self.activeDragId].dimensions = self.rawPositions[self.activeDragId].dimensions; - // Mark this object as dirty to encourage persistence - if ($scope.commit) { - $scope.commit("Moved frame."); - } + self.commit(); }; // Sets the selectable object in response to the selection change event. @@ -194,9 +192,22 @@ define( $scope.$on("$destroy", function () { openmct.selection.off("change", setSelection); + self.unlisten(); }); $scope.$on("mctDrop", handleDrop); + + self.unlisten = self.$scope.domainObject.getCapability('mutation').listen(function (model) { + $scope.configuration = model.configuration.layout; + $scope.model = model; + var panels = $scope.configuration.panels; + + Object.keys(panels).forEach(function (key) { + if (self.frames && self.frames.hasOwnProperty(key)) { + self.frames[key] = panels[key].hasFrame; + } + }); + }); } // Utility function to copy raw positions from configuration, @@ -220,7 +231,6 @@ define( */ LayoutController.prototype.setFrames = function (ids) { var panels = shallowCopy(this.$scope.configuration.panels || {}, ids); - this.frames = {}; this.$scope.composition.forEach(function (object) { @@ -230,11 +240,22 @@ define( if (panels[id].hasOwnProperty('hasFrame')) { this.frames[id] = panels[id].hasFrame; } else { - this.frames[id] = DEFAULT_HIDDEN_FRAME_TYPES.indexOf(object.getModel().type) === -1; + this.frames[id] = this.getDefaultFrame(object.getModel().type); } }, this); }; + /** + * Gets the default value for frame. + * + * @param type the domain object type + * @return {boolean} true if the object should have + * frame by default, false, otherwise + */ + LayoutController.prototype.getDefaultFrame = function (type) { + return DEFAULT_HIDDEN_FRAME_TYPES.indexOf(type) === -1; + }; + // Convert from { positions: ..., dimensions: ... } to an // appropriate ng-style argument, to position frames. LayoutController.prototype.convertPosition = function (raw) { @@ -389,40 +410,6 @@ define( return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false; }; - /** - * Callback to show/hide the object frame. - * - * @param {string} id the object id - * @private - */ - LayoutController.prototype.toggleFrame = function (id, domainObject) { - var configuration = this.$scope.configuration; - - if (!configuration.panels[id]) { - configuration.panels[id] = {}; - } - - this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id]; - - var selection = this.openmct.selection.get(); - selection[0].context.toolbar = this.getToolbar(id, domainObject); - this.openmct.selection.select(selection); // reselect so toolbar updates - }; - - /** - * Gets the toolbar object for the given domain object. - * - * @param id the domain object id - * @param domainObject the domain object - * @returns {object} - * @private - */ - LayoutController.prototype.getToolbar = function (id, domainObject) { - var toolbarObj = {}; - toolbarObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id, domainObject); - return toolbarObj; - }; - /** * Bypasses selection if drag is in progress. * @@ -497,17 +484,25 @@ define( * Gets the selection context. * * @param domainObject the domain object - * @returns {object} the context object which includes - * item, oldItem and toolbar + * @returns {object} the context object which includes item and oldItem */ - LayoutController.prototype.getContext = function (domainObject, toolbar) { + LayoutController.prototype.getContext = function (domainObject) { return { item: domainObject.useCapability('adapter'), - oldItem: domainObject, - toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined + oldItem: domainObject }; }; + LayoutController.prototype.commit = function () { + var model = this.$scope.model; + model.configuration = model.configuration || {}; + model.configuration.layout = this.$scope.configuration; + + this.$scope.domainObject.useCapability('mutation', function () { + return model; + }); + }; + /** * Selects a newly-dropped object. * diff --git a/platform/features/layout/src/elements/BoxProxy.js b/platform/features/layout/src/elements/BoxProxy.js index a426eb99d1..26e394090a 100644 --- a/platform/features/layout/src/elements/BoxProxy.js +++ b/platform/features/layout/src/elements/BoxProxy.js @@ -53,12 +53,6 @@ define( */ proxy.fill = new AccessorMutator(element, 'fill'); - //Expose x,y, width and height for editing - proxy.editWidth = new AccessorMutator(element, 'width'); - proxy.editHeight = new AccessorMutator(element, 'height'); - proxy.editX = new AccessorMutator(element, 'x'); - proxy.editY = new AccessorMutator(element, 'y'); - return proxy; } diff --git a/platform/features/layout/src/elements/ElementProxy.js b/platform/features/layout/src/elements/ElementProxy.js index 7df0cc16ae..84114a1ae7 100644 --- a/platform/features/layout/src/elements/ElementProxy.js +++ b/platform/features/layout/src/elements/ElementProxy.js @@ -71,13 +71,6 @@ define( */ this.gridSize = gridSize || [1,1]; //Ensure a reasonable default - this.resizeHandles = [new ResizeHandle( - this.element, - this.getMinWidth(), - this.getMinHeight(), - this.getGridSize() - )]; - /** * Get and/or set the x position of this element. * Units are in fixed position grid space. @@ -123,15 +116,16 @@ define( this.height = new AccessorMutator(element, 'height'); this.useGrid = new UnitAccessorMutator(this); - this.index = index; this.elements = elements; + this.resizeHandles = [new ResizeHandle(this, this.element)]; } /** * Change the display order of this element. * @param {string} o where to move this element; * one of "top", "up", "down", or "bottom" + * @return {Array} the full array of elements */ ElementProxy.prototype.order = function (o) { var index = this.index, @@ -152,16 +146,8 @@ define( // anyway, but be consistent) this.index = desired; } - }; - /** - * Remove this element from the fixed position view. - */ - ElementProxy.prototype.remove = function () { - var index = this.index; - if (this.elements[index] === this.element) { - this.elements.splice(index, 1); - } + return elements; }; /** @@ -208,7 +194,6 @@ define( */ ElementProxy.prototype.getMinWidth = function () { return Math.ceil(MIN_WIDTH / this.getGridSize()[0]); - }; /** diff --git a/platform/features/layout/src/elements/ImageProxy.js b/platform/features/layout/src/elements/ImageProxy.js index 4e7fe04001..e3a2c42db0 100644 --- a/platform/features/layout/src/elements/ImageProxy.js +++ b/platform/features/layout/src/elements/ImageProxy.js @@ -50,12 +50,6 @@ define( */ proxy.url = new AccessorMutator(element, 'url'); - //Expose x,y, width and height properties for editing - proxy.editWidth = new AccessorMutator(element, 'width'); - proxy.editHeight = new AccessorMutator(element, 'height'); - proxy.editX = new AccessorMutator(element, 'x'); - proxy.editY = new AccessorMutator(element, 'y'); - return proxy; } diff --git a/platform/features/layout/src/elements/LineHandle.js b/platform/features/layout/src/elements/LineHandle.js index 6d2548fdee..eff2933b5a 100644 --- a/platform/features/layout/src/elements/LineHandle.js +++ b/platform/features/layout/src/elements/LineHandle.js @@ -32,19 +32,18 @@ define( * @constructor * @param element the line element * @param {string} xProperty field which stores x position - * @param {string} yProperty field which stores x position + * @param {string} yProperty field which stores y position * @param {string} xOther field which stores x of other end * @param {string} yOther field which stores y of other end - * @param {number[]} gridSize the current layout grid size in [x,y] from * @implements {platform/features/layout.ElementHandle} */ - function LineHandle(element, xProperty, yProperty, xOther, yOther, gridSize) { + function LineHandle(element, elementProxy, xProperty, yProperty, xOther, yOther) { + this.elementProxy = elementProxy; this.element = element; this.xProperty = xProperty; this.yProperty = yProperty; this.xOther = xOther; this.yOther = yOther; - this.gridSize = gridSize; } LineHandle.prototype.x = function (value) { @@ -86,7 +85,7 @@ define( }; LineHandle.prototype.getGridSize = function () { - return this.gridSize; + return this.elementProxy.getGridSize(); }; return LineHandle; diff --git a/platform/features/layout/src/elements/LineProxy.js b/platform/features/layout/src/elements/LineProxy.js index dfa8a8ba48..ea042c35a1 100644 --- a/platform/features/layout/src/elements/LineProxy.js +++ b/platform/features/layout/src/elements/LineProxy.js @@ -39,10 +39,24 @@ define( function LineProxy(element, index, elements, gridSize) { var proxy = new ElementProxy(element, index, elements, gridSize), handles = [ - new LineHandle(element, 'x', 'y', 'x2', 'y2', proxy.getGridSize()), - new LineHandle(element, 'x2', 'y2', 'x', 'y', proxy.getGridSize()) + new LineHandle(element, proxy, 'x', 'y', 'x2', 'y2'), + new LineHandle(element, proxy, 'x2', 'y2', 'x', 'y') ]; + /** + * Gets style specific to line proxy. + */ + proxy.getStyle = function () { + var layoutGridSize = proxy.getGridSize(); + + return { + left: (layoutGridSize[0] * proxy.x()) + 'px', + top: (layoutGridSize[1] * proxy.y()) + 'px', + width: (layoutGridSize[0] * proxy.width()) + 'px', + height: (layoutGridSize[1] * proxy.height()) + 'px' + }; + }; + /** * Get the top-left x coordinate, in grid space, of * this line's bounding box. @@ -149,12 +163,6 @@ define( return handles; }; - // Expose endpoint coordinates for editing - proxy.editX1 = new AccessorMutator(element, 'x'); - proxy.editY1 = new AccessorMutator(element, 'y'); - proxy.editX2 = new AccessorMutator(element, 'x2'); - proxy.editY2 = new AccessorMutator(element, 'y2'); - return proxy; } diff --git a/platform/features/layout/src/elements/ResizeHandle.js b/platform/features/layout/src/elements/ResizeHandle.js index 07d526024a..14ee70d165 100644 --- a/platform/features/layout/src/elements/ResizeHandle.js +++ b/platform/features/layout/src/elements/ResizeHandle.js @@ -35,21 +35,16 @@ define( * @memberof platform/features/layout * @constructor */ - function ResizeHandle(element, minWidth, minHeight, gridSize) { + function ResizeHandle(elementProxy, element) { + this.elementProxy = elementProxy; this.element = element; - - // Ensure reasonable defaults - this.minWidth = minWidth || 0; - this.minHeight = minHeight || 0; - - this.gridSize = gridSize; } ResizeHandle.prototype.x = function (value) { var element = this.element; if (arguments.length > 0) { element.width = Math.max( - this.minWidth, + this.elementProxy.getMinWidth(), value - element.x ); } @@ -60,7 +55,7 @@ define( var element = this.element; if (arguments.length > 0) { element.height = Math.max( - this.minHeight, + this.elementProxy.getMinHeight(), value - element.y ); } @@ -68,7 +63,7 @@ define( }; ResizeHandle.prototype.getGridSize = function () { - return this.gridSize; + return this.elementProxy.getGridSize(); }; return ResizeHandle; diff --git a/platform/features/layout/src/elements/TelemetryProxy.js b/platform/features/layout/src/elements/TelemetryProxy.js index a2969408d3..574e2b1b8a 100644 --- a/platform/features/layout/src/elements/TelemetryProxy.js +++ b/platform/features/layout/src/elements/TelemetryProxy.js @@ -24,9 +24,6 @@ define( ['./TextProxy'], function (TextProxy) { - // Method names to expose from this proxy - var HIDE = 'hideTitle', SHOW = 'showTitle'; - /** * Selection proxy for telemetry elements in a fixed position view. * @@ -45,24 +42,9 @@ define( function TelemetryProxy(element, index, elements, gridSize) { var proxy = new TextProxy(element, index, elements, gridSize); - // Toggle the visibility of the title - function toggle() { - // Toggle the state - element.titled = !element.titled; - - // Change which method is exposed, to influence - // which button is shown in the toolbar - delete proxy[SHOW]; - delete proxy[HIDE]; - proxy[element.titled ? HIDE : SHOW] = toggle; - } - // Expose the domain object identifier proxy.id = element.id; - // Expose initial toggle - proxy[element.titled ? HIDE : SHOW] = toggle; - // Don't expose text configuration delete proxy.text; diff --git a/platform/features/layout/test/FixedControllerSpec.js b/platform/features/layout/test/FixedControllerSpec.js index 52f14fdfe9..5c262195ad 100644 --- a/platform/features/layout/test/FixedControllerSpec.js +++ b/platform/features/layout/test/FixedControllerSpec.js @@ -53,22 +53,14 @@ define( mockTimeSystem, mockLimitEvaluator, mockSelection, + mockObjects, + mockNewDomainObject, + unlistenFunc, $element = [], selectable = [], controller; - // Utility function; find a watch for a given expression - function findWatch(expr) { - var watch; - mockScope.$watch.calls.forEach(function (call) { - if (call.args[0] === expr) { - watch = call.args[1]; - } - }); - return watch; - } - - // As above, but for $on calls + // Utility function; find a $on calls for a given expression. function findOn(expr) { var on; mockScope.$on.calls.forEach(function (call) { @@ -82,7 +74,8 @@ define( function makeMockDomainObject(id) { return { identifier: { - key: "domainObject-" + id + key: "domainObject-" + id, + namespace: "" }, name: "Point " + id }; @@ -110,11 +103,6 @@ define( return "Formatted " + valueMetadata.value; }); - mockDomainObject = jasmine.createSpyObj( - 'domainObject', - ['getId', 'getModel', 'getCapability', 'useCapability'] - ); - mockHandle = jasmine.createSpyObj( 'subscription', [ @@ -172,16 +160,14 @@ define( ]}; mockChildren = testModel.composition.map(makeMockDomainObject); - mockCompositionCollection = jasmine.createSpyObj('compositionCollection', - [ - 'load' - ] - ); - mockCompositionAPI = jasmine.createSpyObj('composition', - [ - 'get' - ] - ); + mockCompositionCollection = jasmine.createSpyObj('compositionCollection', [ + 'load', + 'on', + 'off' + ]); + mockCompositionAPI = jasmine.createSpyObj('composition', [ + 'get' + ]); mockCompositionAPI.get.andReturn(mockCompositionCollection); mockCompositionCollection.load.andReturn( Promise.resolve(mockChildren) @@ -190,6 +176,24 @@ define( mockScope.model = testModel; mockScope.configuration = testConfiguration; + mockNewDomainObject = jasmine.createSpyObj("newDomainObject", [ + 'layoutGrid', + 'configuration', + 'composition' + ]); + mockNewDomainObject.layoutGrid = testGrid; + mockNewDomainObject.configuration = { + 'fixed-display': testConfiguration + }; + mockNewDomainObject.composition = ['a', 'b', 'c']; + + mockDomainObject = jasmine.createSpyObj( + 'domainObject', + ['getId', 'getModel', 'getCapability', 'useCapability'] + ); + mockDomainObject.useCapability.andReturn(mockNewDomainObject); + mockScope.domainObject = mockDomainObject; + selectable[0] = { context: { oldItem: mockDomainObject @@ -203,11 +207,19 @@ define( ]); mockSelection.get.andReturn([]); + unlistenFunc = jasmine.createSpy("unlisten"); + mockObjects = jasmine.createSpyObj('objects', [ + 'observe', + 'get' + ]); + mockObjects.observe.andReturn(unlistenFunc); + mockOpenMCT = { time: mockConductor, telemetry: mockTelemetryAPI, composition: mockCompositionAPI, - selection: mockSelection + selection: mockSelection, + objects: mockObjects }; $element = $('
'); @@ -251,76 +263,60 @@ define( mockOpenMCT, $element ); - - findWatch("model.layoutGrid")(testModel.layoutGrid); + spyOn(controller, "mutate"); }); - it("subscribes when a domain object is available", function () { - var dunzo = false; + it("subscribes a domain object", function () { + var object = makeMockDomainObject("mock"); + var done = false; - mockScope.domainObject = mockDomainObject; - findWatch("domainObject")(mockDomainObject).then(function () { - dunzo = true; + controller.getTelemetry(object).then(function () { + done = true; }); waitsFor(function () { - return dunzo; - }, "Telemetry fetched", 200); + return done; + }); runs(function () { - mockChildren.forEach(function (child) { - expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith( - child, - jasmine.any(Function), - jasmine.any(Object) - ); - }); + expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith( + object, + jasmine.any(Function), + jasmine.any(Object) + ); }); }); - it("releases subscriptions when domain objects change", function () { - var dunzo = false; + it("releases subscription when a domain objects is removed", function () { + var done = false; var unsubscribe = jasmine.createSpy('unsubscribe'); + var object = makeMockDomainObject("mock"); mockTelemetryAPI.subscribe.andReturn(unsubscribe); - - mockScope.domainObject = mockDomainObject; - findWatch("domainObject")(mockDomainObject).then(function () { - dunzo = true; + controller.getTelemetry(object).then(function () { + done = true; }); waitsFor(function () { - return dunzo; - }, "Telemetry fetched", 200); + return done; + }); runs(function () { - expect(unsubscribe).not.toHaveBeenCalled(); - - dunzo = false; - - findWatch("domainObject")(mockDomainObject).then(function () { - dunzo = true; - }); + controller.onCompositionRemove(object.identifier); waitsFor(function () { - return dunzo; - }, "Telemetry fetched", 200); - - runs(function () { - expect(unsubscribe.calls.length).toBe(mockChildren.length); + return unsubscribe.calls.length > 0; }); + runs(function () { + expect(unsubscribe).toHaveBeenCalled(); + }); }); }); it("exposes visible elements based on configuration", function () { - var elements; + var elements = controller.getElements(); - mockScope.model = testModel; - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - - elements = controller.getElements(); expect(elements.length).toEqual(3); expect(elements[0].id).toEqual('a'); expect(elements[1].id).toEqual('b'); @@ -328,9 +324,6 @@ define( }); it("allows elements to be selected", function () { - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - selectable[0].context.elementProxy = controller.getElements()[1]; mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); @@ -338,12 +331,7 @@ define( }); it("allows selection retrieval", function () { - var elements; - - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - - elements = controller.getElements(); + var elements = controller.getElements(); selectable[0].context.elementProxy = elements[1]; mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); @@ -351,16 +339,10 @@ define( }); it("selects the parent view when selected element is removed", function () { - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - var elements = controller.getElements(); selectable[0].context.elementProxy = elements[1]; mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); - - elements[1].remove(); - testModel.modified = 2; - findWatch("model.modified")(testModel.modified); + controller.remove(elements[1]); expect($element[0].click).toHaveBeenCalled(); }); @@ -368,21 +350,13 @@ define( it("retains selections during refresh", function () { // Get elements; remove one of them; trigger refresh. // Same element (at least by index) should still be selected. - var elements; - - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - - elements = controller.getElements(); + var elements = controller.getElements(); selectable[0].context.elementProxy = elements[1]; mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); expect(controller.getSelectedElement()).toEqual(elements[1]); - elements[2].remove(); - testModel.modified = 2; - findWatch("model.modified")(testModel.modified); - + controller.remove(elements[2]); elements = controller.getElements(); // Verify removal, as test assumes this @@ -408,7 +382,7 @@ define( controller.elementProxiesById['12345'] = [testElement]; controller.elementProxies = [testElement]; - controller.subscribeToObjects([telemetryObject]); + controller.subscribeToObject(telemetryObject); mockTelemetryAPI.subscribe.mostRecentCall.args[1](mockTelemetry); waitsFor(function () { @@ -426,18 +400,13 @@ define( }); it("updates elements styles when grid size changes", function () { - var originalLeft; + // Grid size is initially set to testGrid which is [123, 456] + var originalLeft = controller.getElements()[0].style.left; - mockScope.domainObject = mockDomainObject; - mockScope.model = testModel; - findWatch("domainObject")(mockDomainObject); - findWatch("model.modified")(1); - findWatch("model.composition")(mockScope.model.composition); - findWatch("model.layoutGrid")([10, 10]); - originalLeft = controller.getElements()[0].style.left; - findWatch("model.layoutGrid")([20, 20]); - expect(controller.getElements()[0].style.left) - .not.toEqual(originalLeft); + // Change the grid size + controller.updateElementPositions([20, 20]); + + expect(controller.getElements()[0].style.left).not.toEqual(originalLeft); }); it("listens for drop events", function () { @@ -457,6 +426,9 @@ define( // Notify that a drop occurred testModel.composition.push('d'); + + mockObjects.get.andReturn(Promise.resolve([])); + findOn('mctDrop')( mockEvent, 'd', @@ -468,11 +440,6 @@ define( // ...and prevented default... expect(mockEvent.preventDefault).toHaveBeenCalled(); - - // Should have triggered commit (provided by - // EditRepresenter) with some message. - expect(mockScope.commit) - .toHaveBeenCalledWith(jasmine.any(String)); }); it("ignores drops when default has been prevented", function () { @@ -492,52 +459,35 @@ define( }); it("unsubscribes when destroyed", function () { - - var dunzo = false; + var done = false; var unsubscribe = jasmine.createSpy('unsubscribe'); + var object = makeMockDomainObject("mock"); mockTelemetryAPI.subscribe.andReturn(unsubscribe); - mockScope.domainObject = mockDomainObject; - findWatch("domainObject")(mockDomainObject).then(function () { - dunzo = true; + controller.getTelemetry(object).then(function () { + done = true; }); waitsFor(function () { - return dunzo; - }, "Telemetry fetched", 200); + return done; + }); runs(function () { expect(unsubscribe).not.toHaveBeenCalled(); // Destroy the scope findOn('$destroy')(); - //Check that the same unsubscribe function returned by the - expect(unsubscribe.calls.length).toBe(mockChildren.length); + expect(unsubscribe).toHaveBeenCalled(); }); }); it("exposes its grid size", function () { - findWatch('model.layoutGrid')(testGrid); - // Template needs to be able to pass this into line - // elements to size SVGs appropriately expect(controller.getGridSize()).toEqual(testGrid); }); - it("exposes a view-level selection proxy", function () { - mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); - var selection = mockOpenMCT.selection.select.mostRecentCall.args[0]; - - expect(mockOpenMCT.selection.select).toHaveBeenCalled(); - expect(selection.context.viewProxy).toBeDefined(); - }); - it("exposes drag handles", function () { var handles; - - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - selectable[0].context.elementProxy = controller.getElements()[1]; mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); @@ -556,9 +506,6 @@ define( }); it("exposes a move handle", function () { - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - selectable[0].context.elementProxy = controller.getElements()[1]; mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); @@ -573,10 +520,6 @@ define( it("updates selection style during drag", function () { var oldStyle; - - testModel.modified = 1; - findWatch("model.modified")(testModel.modified); - selectable[0].context.elementProxy = controller.getElements()[1]; mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); @@ -677,7 +620,7 @@ define( value: testValue }])); - controller.fetchHistoricalData([mockTelemetryObject]); + controller.fetchHistoricalData(mockTelemetryObject); waitsFor(function () { return controller.digesting === false; diff --git a/platform/features/layout/test/FixedDragHandleSpec.js b/platform/features/layout/test/FixedDragHandleSpec.js index 3feba5d50a..1d7d88f621 100644 --- a/platform/features/layout/test/FixedDragHandleSpec.js +++ b/platform/features/layout/test/FixedDragHandleSpec.js @@ -28,8 +28,8 @@ define( describe("A fixed position drag handle", function () { var mockElementHandle, - mockUpdate, - mockCommit, + mockConfigPath, + mockFixedControl, handle; beforeEach(function () { @@ -37,18 +37,23 @@ define( 'elementHandle', ['x', 'y','getGridSize'] ); - mockUpdate = jasmine.createSpy('update'); - mockCommit = jasmine.createSpy('commit'); - mockElementHandle.x.andReturn(6); mockElementHandle.y.andReturn(8); mockElementHandle.getGridSize.andReturn(TEST_GRID_SIZE); + mockFixedControl = jasmine.createSpyObj( + 'fixedControl', + ['updateSelectionStyle', 'mutate'] + ); + mockFixedControl.updateSelectionStyle.andReturn(); + mockFixedControl.mutate.andReturn(); + + mockConfigPath = jasmine.createSpy('configPath'); + handle = new FixedDragHandle( mockElementHandle, - TEST_GRID_SIZE, - mockUpdate, - mockCommit + mockConfigPath, + mockFixedControl ); }); @@ -74,13 +79,12 @@ define( expect(mockElementHandle.x).toHaveBeenCalledWith(5); expect(mockElementHandle.y).toHaveBeenCalledWith(7); - // Should have called update once per continueDrag - expect(mockUpdate.calls.length).toEqual(2); + // Should have called updateSelectionStyle once per continueDrag + expect(mockFixedControl.updateSelectionStyle.calls.length).toEqual(2); - // Finally, ending drag should commit - expect(mockCommit).not.toHaveBeenCalled(); + // Finally, ending drag should mutate handle.endDrag(); - expect(mockCommit).toHaveBeenCalled(); + expect(mockFixedControl.mutate).toHaveBeenCalled(); }); }); diff --git a/platform/features/layout/test/LayoutControllerSpec.js b/platform/features/layout/test/LayoutControllerSpec.js index 84b54b742b..55d575bf1e 100644 --- a/platform/features/layout/test/LayoutControllerSpec.js +++ b/platform/features/layout/test/LayoutControllerSpec.js @@ -42,6 +42,8 @@ define( mockOpenMCT, mockSelection, mockDomainObjectCapability, + mockObjects, + unlistenFunc, $element = [], selectable = []; @@ -77,14 +79,15 @@ define( if (param === 'composition') { return id !== 'b'; } - } + }, + type: "testType" }; } beforeEach(function () { mockScope = jasmine.createSpyObj( "$scope", - ["$watch", "$watchCollection", "$on", "commit"] + ["$watch", "$watchCollection", "$on"] ); mockEvent = jasmine.createSpyObj( 'event', @@ -104,9 +107,13 @@ define( } } }; + + unlistenFunc = jasmine.createSpy("unlisten"); mockDomainObjectCapability = jasmine.createSpyObj('capability', - ['inEditContext'] + ['inEditContext', 'listen'] ); + mockDomainObjectCapability.listen.andReturn(unlistenFunc); + mockCompositionCapability = mockPromise(mockCompositionObjects); mockScope.domainObject = mockDomainObject("mockDomainObject"); @@ -126,8 +133,14 @@ define( 'get' ]); mockSelection.get.andReturn(selectable); + + mockObjects = jasmine.createSpyObj('objects', [ + 'get' + ]); + mockObjects.get.andReturn(mockPromise(mockDomainObject("mockObject"))); mockOpenMCT = { - selection: mockSelection + selection: mockSelection, + objects: mockObjects }; $element = $('
'); @@ -138,6 +151,7 @@ define( controller = new LayoutController(mockScope, $element, mockOpenMCT); spyOn(controller, "layoutPanels").andCallThrough(); + spyOn(controller, "commit"); jasmine.Clock.useMock(); }); @@ -270,10 +284,7 @@ define( controller.continueDrag([100, 100]); controller.endDrag(); - // Should have triggered commit (provided by - // EditRepresenter) with some message. - expect(mockScope.commit) - .toHaveBeenCalledWith(jasmine.any(String)); + expect(controller.commit).toHaveBeenCalled(); }); it("listens for drop events", function () { @@ -296,11 +307,7 @@ define( ); expect(testConfiguration.panels.d).toBeDefined(); expect(mockEvent.preventDefault).toHaveBeenCalled(); - - // Should have triggered commit (provided by - // EditRepresenter) with some message. - expect(mockScope.commit) - .toHaveBeenCalledWith(jasmine.any(String)); + expect(controller.commit).toHaveBeenCalled(); }); it("ignores drops when default has been prevented", function () { @@ -340,13 +347,17 @@ define( testModel.layoutGrid = [1, 1]; mockScope.$watch.calls[0].args[1](testModel.layoutGrid); + // Add a new object to the composition + mockComposition = ["a", "b", "c", "d"]; + mockCompositionObjects = mockComposition.map(mockDomainObject); + mockCompositionCapability = mockPromise(mockCompositionObjects); + // Notify that a drop occurred mockScope.$on.mostRecentCall.args[1]( mockEvent, 'd', { x: 300, y: 100 } ); - mockScope.$watch.calls[0].args[1](['d']); style = controller.getFrameStyle("d"); @@ -415,30 +426,6 @@ define( expect(controller.hasFrame(mockCompositionObjects[1])).toBe(false); }); - it("hides frame when selected object has frame ", function () { - mockScope.$watchCollection.mostRecentCall.args[1](); - var childObj = mockCompositionObjects[0]; - selectable[0].context.oldItem = childObj; - mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); - var toolbarObj = controller.getToolbar(childObj.getId(), childObj); - - expect(controller.hasFrame(childObj)).toBe(true); - expect(toolbarObj.hideFrame).toBeDefined(); - expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function)); - }); - - it("shows frame when selected object has no frame", function () { - mockScope.$watchCollection.mostRecentCall.args[1](); - var childObj = mockCompositionObjects[1]; - selectable[0].context.oldItem = childObj; - mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); - var toolbarObj = controller.getToolbar(childObj.getId(), childObj); - - expect(controller.hasFrame(childObj)).toBe(false); - expect(toolbarObj.showFrame).toBeDefined(); - expect(toolbarObj.showFrame).toEqual(jasmine.any(Function)); - }); - it("selects the parent object when selected object is removed", function () { mockScope.$watchCollection.mostRecentCall.args[1](); var childObj = mockCompositionObjects[0]; diff --git a/platform/features/layout/test/elements/ElementProxySpec.js b/platform/features/layout/test/elements/ElementProxySpec.js index ce38b19270..dcb6786c72 100644 --- a/platform/features/layout/test/elements/ElementProxySpec.js +++ b/platform/features/layout/test/elements/ElementProxySpec.js @@ -53,11 +53,6 @@ define( }); }); - it("allows elements to be removed", function () { - proxy.remove(); - expect(testElements).toEqual([{}, {}, {}]); - }); - it("allows order to be changed", function () { proxy.order("down"); expect(testElements).toEqual([{}, testElement, {}, {}]); diff --git a/platform/features/layout/test/elements/LineHandleSpec.js b/platform/features/layout/test/elements/LineHandleSpec.js index f01ca36e95..d1465f8f89 100644 --- a/platform/features/layout/test/elements/LineHandleSpec.js +++ b/platform/features/layout/test/elements/LineHandleSpec.js @@ -26,7 +26,9 @@ define( describe("A fixed position drag handle", function () { var testElement, - handle; + mockElementProxy, + handle, + TEST_GRID_SIZE = [45, 21]; beforeEach(function () { testElement = { @@ -36,8 +38,10 @@ define( y2: 11, useGrid: true }; + mockElementProxy = jasmine.createSpyObj('elementProxy', ['getGridSize']); + mockElementProxy.getGridSize.andReturn(TEST_GRID_SIZE); - handle = new LineHandle(testElement, 'x', 'y', 'x2', 'y2', [45,21]); + handle = new LineHandle(testElement, mockElementProxy, 'x', 'y', 'x2', 'y2'); }); it("provides x/y grid coordinates for its corner", function () { @@ -69,7 +73,7 @@ define( }); it("returns the correct grid size", function () { - expect(handle.getGridSize()).toEqual([45,21]); + expect(handle.getGridSize()).toEqual(TEST_GRID_SIZE); }); }); diff --git a/platform/features/layout/test/elements/LineProxySpec.js b/platform/features/layout/test/elements/LineProxySpec.js index f2b3fbae9b..9b45396ee3 100644 --- a/platform/features/layout/test/elements/LineProxySpec.js +++ b/platform/features/layout/test/elements/LineProxySpec.js @@ -63,13 +63,13 @@ define( it("adjusts both ends when mutating x", function () { var proxy = new LineProxy(diagonal); proxy.x(6); - expect(diagonal).toEqual({ x: 6, y: 8, x2: 8, y2: 11, useGrid: true }); + expect(diagonal).toEqual({ x: 6, y: 8, x2: 8, y2: 11}); }); it("adjusts both ends when mutating y", function () { var proxy = new LineProxy(diagonal); proxy.y(6); - expect(diagonal).toEqual({ x: 3, y: 6, x2: 5, y2: 9, useGrid: true }); + expect(diagonal).toEqual({ x: 3, y: 6, x2: 5, y2: 9}); }); it("provides internal positions for SVG lines", function () { diff --git a/platform/features/layout/test/elements/ResizeHandleSpec.js b/platform/features/layout/test/elements/ResizeHandleSpec.js index 5e113adc9b..526df95813 100644 --- a/platform/features/layout/test/elements/ResizeHandleSpec.js +++ b/platform/features/layout/test/elements/ResizeHandleSpec.js @@ -25,10 +25,12 @@ define( function (ResizeHandle) { var TEST_MIN_WIDTH = 4, - TEST_MIN_HEIGHT = 2; + TEST_MIN_HEIGHT = 2, + TEST_GRID_SIZE = [34, 81]; describe("A fixed position drag handle", function () { var testElement, + mockElementProxy, handle; beforeEach(function () { @@ -39,12 +41,18 @@ define( height: 36, useGrid: true }; + mockElementProxy = jasmine.createSpyObj('elementProxy', [ + 'getGridSize', + 'getMinWidth', + 'getMinHeight' + ]); + mockElementProxy.getGridSize.andReturn(TEST_GRID_SIZE); + mockElementProxy.getMinWidth.andReturn(TEST_MIN_WIDTH); + mockElementProxy.getMinHeight.andReturn(TEST_MIN_HEIGHT); handle = new ResizeHandle( - testElement, - TEST_MIN_WIDTH, - TEST_MIN_HEIGHT, - [34,81] + mockElementProxy, + testElement ); }); @@ -77,7 +85,7 @@ define( }); it("returns the correct grid size", function () { - expect(handle.getGridSize()).toEqual([34,81]); + expect(handle.getGridSize()).toEqual(TEST_GRID_SIZE); }); }); diff --git a/platform/features/layout/test/elements/TelemetryProxySpec.js b/platform/features/layout/test/elements/TelemetryProxySpec.js index f6945da9c8..22bcfbf69b 100644 --- a/platform/features/layout/test/elements/TelemetryProxySpec.js +++ b/platform/features/layout/test/elements/TelemetryProxySpec.js @@ -49,27 +49,6 @@ define( it("exposes the element's id", function () { expect(proxy.id).toEqual('test-id'); }); - - it("allows title to be shown/hidden", function () { - // Initially, only showTitle and hideTitle are available - expect(proxy.hideTitle).toBeUndefined(); - proxy.showTitle(); - - // Should have set titled state - expect(testElement.titled).toBeTruthy(); - - // Should also have changed methods available - expect(proxy.showTitle).toBeUndefined(); - proxy.hideTitle(); - - // Should have cleared titled state - expect(testElement.titled).toBeFalsy(); - - // Available methods should have changed again - expect(proxy.hideTitle).toBeUndefined(); - proxy.showTitle(); - }); - }); } ); diff --git a/platform/forms/res/templates/toolbar.html b/platform/forms/res/templates/toolbar.html index 63217352a8..e636bb612d 100644 --- a/platform/forms/res/templates/toolbar.html +++ b/platform/forms/res/templates/toolbar.html @@ -21,30 +21,29 @@ -->
- - + + + + - - - - + + + +
diff --git a/platform/forms/src/MCTToolbar.js b/platform/forms/src/MCTToolbar.js index 0a6650d50f..5a7ce38da9 100644 --- a/platform/forms/src/MCTToolbar.js +++ b/platform/forms/src/MCTToolbar.js @@ -24,8 +24,16 @@ * Module defining MCTForm. Created by vwoeltje on 11/10/14. */ define( - ["./MCTForm", "text!../res/templates/toolbar.html"], - function (MCTForm, toolbarTemplate) { + [ + "./MCTForm", + "text!../res/templates/toolbar.html", + "./controllers/ToolbarController" + ], + function ( + MCTForm, + toolbarTemplate, + ToolbarController + ) { /** * The mct-toolbar directive allows generation of displayable @@ -35,7 +43,7 @@ define( * This directive accepts three attributes: * * * `ng-model`: The model for the form; where user input - * where be stored. + * will be stored. * * `structure`: The declarative structure of the toolbar. * Describes what controls should be shown and where * their values should be read/written in the model. @@ -49,9 +57,10 @@ define( */ function MCTToolbar() { // Use Directive Definition Object from mct-form, - // but use the toolbar's template instead. + // but use the toolbar's template and controller instead. var ddo = new MCTForm(); ddo.template = toolbarTemplate; + ddo.controller = ['$scope', 'openmct', ToolbarController]; return ddo; } diff --git a/platform/forms/src/controllers/ToolbarController.js b/platform/forms/src/controllers/ToolbarController.js new file mode 100644 index 0000000000..3a8b263e17 --- /dev/null +++ b/platform/forms/src/controllers/ToolbarController.js @@ -0,0 +1,84 @@ +define( + [ + '../../../commonUI/edit/src/representers/EditToolbar' + ], + function (EditToolbar) { + + // Default ng-pattern; any non whitespace + var NON_WHITESPACE = /\S/; + + /** + * Controller for mct-toolbar directive. + * + * @memberof platform/forms + * @constructor + */ + function ToolbarController($scope, openmct) { + var regexps = []; + + // ng-pattern seems to want a RegExp, and not a + // string (despite what documentation says) but + // we want toolbar structure to be JSON-expressible, + // so we make RegExp's from strings as-needed + function getRegExp(pattern) { + // If undefined, don't apply a pattern + if (!pattern) { + return NON_WHITESPACE; + } + + // Just echo if it's already a regexp + if (pattern instanceof RegExp) { + return pattern; + } + + // Otherwise, assume a string + // Cache for easy lookup later (so we don't + // creat a new RegExp every digest cycle) + if (!regexps[pattern]) { + regexps[pattern] = new RegExp(pattern); + } + + return regexps[pattern]; + } + + this.openmct = openmct; + this.$scope = $scope; + $scope.editToolbar = {}; + $scope.getRegExp = getRegExp; + + $scope.$on("$destroy", this.destroy.bind(this)); + openmct.selection.on('change', this.handleSelection.bind(this)); + } + + ToolbarController.prototype.handleSelection = function (selection) { + var domainObject = selection[0].context.oldItem; + var element = selection[0].context.elementProxy; + + if ((domainObject && domainObject === this.selectedObject) || (element && element === this.selectedObject)) { + return; + } + + this.selectedObject = domainObject || element; + + if (this.editToolbar) { + this.editToolbar.destroy(); + } + + var structure = this.openmct.toolbars.get(selection) || []; + this.editToolbar = new EditToolbar(this.$scope, this.openmct, structure); + this.$scope.$parent.editToolbar = this.editToolbar; + this.$scope.$parent.editToolbar.structure = this.editToolbar.getStructure(); + this.$scope.$parent.editToolbar.state = this.editToolbar.getState(); + + setTimeout(function () { + this.$scope.$apply(); + }.bind(this)); + }; + + ToolbarController.prototype.destroy = function () { + this.openmct.selection.off("change", this.handleSelection); + }; + + return ToolbarController; + } +); diff --git a/platform/forms/test/MCTToolbarSpec.js b/platform/forms/test/MCTToolbarSpec.js index 58f8eb9020..ac8f20e9e1 100644 --- a/platform/forms/test/MCTToolbarSpec.js +++ b/platform/forms/test/MCTToolbarSpec.js @@ -26,16 +26,28 @@ define( describe("The mct-toolbar directive", function () { var mockScope, + mockOpenMCT, + mockSelection, mctToolbar; function installController() { - var Controller = mctToolbar.controller[1]; - return new Controller(mockScope); + var Controller = mctToolbar.controller[2]; + return new Controller(mockScope, mockOpenMCT); } beforeEach(function () { - mockScope = jasmine.createSpyObj("$scope", ["$watch"]); + mockScope = jasmine.createSpyObj("$scope", [ + "$watch", + "$on" + ]); mockScope.$parent = {}; + mockSelection = jasmine.createSpyObj("selection", [ + 'on', + 'off' + ]); + mockOpenMCT = { + selection: mockSelection + }; mctToolbar = new MCTToolbar(); }); @@ -43,29 +55,15 @@ define( expect(mctToolbar.restrict).toEqual("E"); }); - it("watches for changes in form by name", function () { - // mct-form needs to watch for the form by name - // in order to convey changes in $valid, $dirty, etc - // up to the parent scope. + it("listens for selection change event", function () { installController(); - expect(mockScope.$watch).toHaveBeenCalledWith( - "mctForm", + expect(mockOpenMCT.selection.on).toHaveBeenCalledWith( + "change", jasmine.any(Function) ); }); - it("conveys form status to parent scope", function () { - var someState = { someKey: "some value" }; - mockScope.name = "someName"; - - installController(); - - mockScope.$watch.mostRecentCall.args[1](someState); - - expect(mockScope.$parent.someName).toBe(someState); - }); - it("allows strings to be converted to RegExps", function () { // This is needed to support ng-pattern in the template installController(); diff --git a/src/MCT.js b/src/MCT.js index f41f25ce0a..2519752569 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -29,7 +29,8 @@ define([ './api/objects/object-utils', './plugins/plugins', './ui/ViewRegistry', - './ui/InspectorViewRegistry' + './ui/InspectorViewRegistry', + './ui/ToolbarRegistry' ], function ( EventEmitter, legacyRegistry, @@ -39,7 +40,8 @@ define([ objectUtils, plugins, ViewRegistry, - InspectorViewRegistry + InspectorViewRegistry, + ToolbarRegistry ) { /** * Open MCT is an extensible web application for building mission @@ -76,7 +78,7 @@ define([ * Tracks current selection state of the application. * @private */ - this.selection = new Selection(); + this.selection = new Selection(this); /** * MCT's time conductor, which may be used to synchronize view contents @@ -143,17 +145,13 @@ define([ /** * Registry for views which should appear in the toolbar area while - * editing. + * editing. These views will be chosen based on the selection state. * - * These views will be chosen based on selection state, so - * providers should be prepared to test arbitrary objects for - * viewability. - * - * @type {module:openmct.ViewRegistry} + * @type {module:openmct.ToolbarRegistry} * @memberof module:openmct.MCT# * @name toolbars */ - this.toolbars = new ViewRegistry(); + this.toolbars = new ToolbarRegistry(); /** * Registry for domain object types which may exist within this diff --git a/src/api/objects/MutableObject.js b/src/api/objects/MutableObject.js index 6175eb6a73..dd718d7975 100644 --- a/src/api/objects/MutableObject.js +++ b/src/api/objects/MutableObject.js @@ -76,12 +76,20 @@ define([ * @memberof module:openmct.MutableObject# */ MutableObject.prototype.set = function (path, value) { - _.set(this.object, path, value); _.set(this.object, 'modified', Date.now()); + var handleRecursiveMutation = function (newObject) { + this.object = newObject; + }.bind(this); + + this.eventEmitter.on(qualifiedEventName(this.object, '*'), handleRecursiveMutation); + //Emit event specific to property this.eventEmitter.emit(qualifiedEventName(this.object, path), value); + + this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation); + //Emit wildcare event this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object); diff --git a/src/selection/Selection.js b/src/selection/Selection.js index d48f58b31c..6f294c93b9 100644 --- a/src/selection/Selection.js +++ b/src/selection/Selection.js @@ -26,8 +26,10 @@ define(['EventEmitter'], function (EventEmitter) { * Manages selection state for Open MCT * @private */ - function Selection() { + function Selection(openmct) { EventEmitter.call(this); + + this.openmct = openmct; this.selected = []; } @@ -99,7 +101,12 @@ define(['EventEmitter'], function (EventEmitter) { * Attaches the click handlers to the element. * * @param element an html element - * @param context object with oldItem, item and toolbar properties + * @param context object which defines item or other arbitrary properties. + * e.g. { + * item: domainObject, + * elementProxy: element, + * controller: fixedController + * } * @param select a flag to select the element if true * @returns a function that removes the click handlers from the element * @public @@ -114,6 +121,12 @@ define(['EventEmitter'], function (EventEmitter) { element.addEventListener('click', capture, true); element.addEventListener('click', selectCapture); + if (context.item) { + var unlisten = this.openmct.objects.observe(context.item, "*", function (newItem) { + context.item = newItem; + }); + } + if (select) { element.click(); } @@ -121,6 +134,10 @@ define(['EventEmitter'], function (EventEmitter) { return function () { element.removeEventListener('click', capture); element.removeEventListener('click', selectCapture); + + if (unlisten) { + unlisten(); + } }; }; diff --git a/src/ui/ToolbarRegistry.js b/src/ui/ToolbarRegistry.js new file mode 100644 index 0000000000..7d34421a9f --- /dev/null +++ b/src/ui/ToolbarRegistry.js @@ -0,0 +1,125 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + /*global console */ + +define([], function () { + + /** + * A ToolbarRegistry maintains the definitions for toolbars. + * + * @interface ToolbarRegistry + * @memberof module:openmct + */ + function ToolbarRegistry() { + this.providers = {}; + } + + /** + * Gets toolbar controls from providers which can provide a toolbar for this selection. + * + * @param {object} selection the selection object + * @returns {Object[]} an array of objects defining controls for the toolbar + * @private for platform-internal use + */ + ToolbarRegistry.prototype.get = function (selection) { + var providers = this.getAllProviders().filter(function (provider) { + return provider.forSelection(selection); + }); + + var structure = []; + + providers.map(function (provider) { + provider.toolbar(selection).forEach(function (item) { + structure.push(item); + }); + }); + + return structure; + }; + + /** + * @private + */ + ToolbarRegistry.prototype.getAllProviders = function () { + return Object.values(this.providers); + }; + + /** + * @private + */ + ToolbarRegistry.prototype.getByProviderKey = function (key) { + return this.providers[key]; + }; + + /** + * Registers a new type of toolbar. + * + * @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar + * @method addProvider + * @memberof module:openmct.ToolbarRegistry# + */ + ToolbarRegistry.prototype.addProvider = function (provider) { + var key = provider.key; + + if (key === undefined) { + throw "Toolbar providers must have a unique 'key' property defined."; + } + + if (this.providers[key] !== undefined) { + console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); + } + + this.providers[key] = provider; + }; + + /** + * Exposes types of toolbars in Open MCT. + * + * @interface ToolbarProvider + * @property {string} key a unique identifier for this toolbar + * @property {string} name the human-readable name of this toolbar + * @property {string} [description] a longer-form description (typically + * a single sentence or short paragraph) of this kind of toolbar + * @memberof module:openmct + */ + + /** + * Checks if this provider can supply toolbar for a selection. + * + * @method forSelection + * @memberof module:openmct.ToolbarProvider# + * @param {module:openmct.selection} selection + * @returns {boolean} 'true' if the toolbar applies to the provided selection, + * otherwise 'false'. + */ + + /** + * Provides controls that comprise a toolbar. + * + * @method toolbar + * @memberof module:openmct.ToolbarProvider# + * @param {object} selection the selection object + * @returns {Object[]} an array of objects defining controls for the toolbar. + */ + + return ToolbarRegistry; +});