[Layout] Support sub-object selection in layout (#1811)

Updates to sub object selection, first cut of selection APIs.

* [API] Add inspector view registry to register inspector view providers and show a view in the inspector.

[API] Modify the selection API to register the click event and handle the event. The API will add a class to the selected object and the immediate parent of the selected object.

[Directive] Implemenet mct-selectable directive for making an element selectable.

[Layout] Update the layout controller to use the Selection API. Also, add double click gesture to allow drilling into a selected object.

Populate the Elements pool with contained elements of the selected object. Update toolbar and inspector to listen for the changes in selection.

* [Frontend] Mods to markup and CSS for sub-object selection

* MCTSelectable allows selection in initialization, use to select on navigation

[Frontend] Show grid in first nested layout, hide from deeper nesting. Only show grids when applicable to relative selection.

* Fix checkstyle and lint errors

* Bring back the change that made mct-init-select work

* [Inspector] Make sure the right content is displayed based on whether a view provider exists or not.

* Only show table options when editing

* Make reviewers' requested changes

* Fix broken tests

* [Frontend] Cleanups and tweaks

Fixes #1811
- Cleanups between frame, editor and selecting.scss;
- Hover and selected borders visually pumped up a bit;
- Solid borders on hover and selecting when browsing;
- Dashed borders for layouts when editing;
- Fixed cursor to only show move capability when
element is selected;

* [Frontend] Tweaks to frame.no-frame layout

Fixes #1811
- Margin set to 0;
- Overflow set to hidden;

* [Frontend] Fixed position items border width fixed

Fixes #1811
- Set to 1px;

* Add tests for inspector controller and fix broken tests. Clean up code.

* [Fixed Position] Stop event propagation on click handlers in fixed position to avoid the event reaching the selection click handlers which caused issues with toolbar and selection."

* Fix tests

* Add tests

* Add test

* Remove element from document
This commit is contained in:
Pegah Sarram 2017-12-07 13:04:46 -08:00 committed by Pete Richards
parent 50b4d5cb28
commit 425655bae0
34 changed files with 944 additions and 360 deletions

View File

@ -57,7 +57,12 @@
</div> </div>
<mct-representation key="representation.selected.key" <mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject" mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"> class="abs flex-elem grows object-holder-main scroll"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
</mct-representation> </mct-representation>
</div> </div>
</div> </div>

View File

@ -19,12 +19,21 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div ng-controller="InspectorController"> <div ng-controller="InspectorController as controller">
<div ng-repeat="region in regions">
<mct-representation <mct-representation
key="region.content.key" key="'object-properties'"
mct-object="domainObject" mct-object="controller.selectedItem()"
ng-model="ngModel">
</mct-representation>
<div ng-if="!controller.hasProviderView()">
<mct-representation
key="inspectorKey"
mct-object="controller.selectedItem()"
ng-model="ngModel"> ng-model="ngModel">
</mct-representation> </mct-representation>
</div> </div>
<div class='inspector-provider-view'>
</div>
</div> </div>

View File

@ -38,8 +38,6 @@
ng-class="{ last:($index + 1) === contextualParents.length }"> ng-class="{ last:($index + 1) === contextualParents.length }">
<mct-representation key="'label'" <mct-representation key="'label'"
mct-object="parent" mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item"> class="location-item">
</mct-representation> </mct-representation>
</span> </span>
@ -51,8 +49,6 @@
ng-class="{ last:($index + 1) === primaryParents.length }"> ng-class="{ last:($index + 1) === primaryParents.length }">
<mct-representation key="'label'" <mct-representation key="'label'"
mct-object="parent" mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item"> class="location-item">
</mct-representation> </mct-representation>
</span> </span>

View File

@ -121,7 +121,8 @@ define([
"key": "ElementsController", "key": "ElementsController",
"implementation": ElementsController, "implementation": ElementsController,
"depends": [ "depends": [
"$scope" "$scope",
"openmct"
] ]
}, },
{ {
@ -299,9 +300,6 @@ define([
{ {
"key": "edit-elements", "key": "edit-elements",
"template": elementsTemplate, "template": elementsTemplate,
"uses": [
"composition"
],
"gestures": [ "gestures": [
"drop" "drop"
] ]
@ -385,7 +383,10 @@ define([
] ]
}, },
{ {
"implementation": EditToolbarRepresenter "implementation": EditToolbarRepresenter,
"depends": [
"openmct"
]
} }
], ],
"constants": [ "constants": [

View File

@ -61,7 +61,12 @@
<mct-representation key="representation.selected.key" <mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject" mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll" class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar"> toolbar="toolbar"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
</mct-representation> </mct-representation>
</div><!--/ l-object-wrapper-inner --> </div><!--/ l-object-wrapper-inner -->
</div> </div>

View File

@ -25,7 +25,7 @@
ng-model="filterBy"> ng-model="filterBy">
</mct-include> </mct-include>
<div class="flex-elem grows vscroll"> <div class="flex-elem grows vscroll">
<ul class="tree"> <ul class="tree" ng-if="composition.length > 0">
<li ng-repeat="containedObject in composition | filter:searchElements"> <li ng-repeat="containedObject in composition | filter:searchElements">
<span class="tree-item"> <span class="tree-item">
<mct-representation <mct-representation
@ -36,5 +36,6 @@
</span> </span>
</li> </li>
</ul> </ul>
<div ng-if="composition.length === 0">No contained elements</div>
</div> </div>
</div> </div>

View File

@ -29,7 +29,11 @@ define(
* *
* @constructor * @constructor
*/ */
function ElementsController($scope) { function ElementsController($scope, openmct) {
this.scope = $scope;
this.scope.composition = [];
var self = this;
function filterBy(text) { function filterBy(text) {
if (typeof text === 'undefined') { if (typeof text === 'undefined') {
return $scope.searchText; return $scope.searchText;
@ -47,10 +51,44 @@ define(
} }
} }
function setSelection(selection) {
self.scope.selection = selection;
self.refreshComposition(selection);
}
$scope.filterBy = filterBy; $scope.filterBy = filterBy;
$scope.searchElements = searchElements; $scope.searchElements = searchElements;
openmct.selection.on('change', setSelection);
setSelection(openmct.selection.get());
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
} }
/**
* Gets the composition for the selected object and populates the scope with it.
*
* @param selection the selection object
* @private
*/
ElementsController.prototype.refreshComposition = function (selection) {
if (!selection[0]) {
return;
}
var selectedObjectComposition = selection[0].context.oldItem.useCapability('composition');
if (selectedObjectComposition) {
selectedObjectComposition.then(function (composition) {
this.scope.composition = composition;
}.bind(this));
} else {
this.scope.composition = [];
}
};
return ElementsController; return ElementsController;
} }
); );

View File

@ -38,7 +38,7 @@ define(
* @constructor * @constructor
* @implements {Representer} * @implements {Representer}
*/ */
function EditToolbarRepresenter(scope, element, attrs) { function EditToolbarRepresenter(openmct, scope, element, attrs) {
var self = this; var self = this;
// Mark changes as ready to persist // Mark changes as ready to persist
@ -109,6 +109,7 @@ define(
this.updateSelection = updateSelection; this.updateSelection = updateSelection;
this.toolbar = undefined; this.toolbar = undefined;
this.toolbarObject = {}; this.toolbarObject = {};
this.openmct = openmct;
// If this representation exposes a toolbar, set up watches // If this representation exposes a toolbar, set up watches
// to synchronize with it. // to synchronize with it.
@ -146,7 +147,7 @@ define(
// Expose the toolbar object to the parent scope // Expose the toolbar object to the parent scope
initialize(definition); initialize(definition);
// Create a selection scope // Create a selection scope
this.setSelection(new EditToolbarSelection()); this.setSelection(new EditToolbarSelection(this.openmct));
// Initialize toolbar to an empty selection // Initialize toolbar to an empty selection
this.updateSelection([]); this.updateSelection([]);
}; };

View File

@ -38,10 +38,18 @@ define(
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
*/ */
function EditToolbarSelection() { function EditToolbarSelection(openmct) {
this.selection = [{}]; this.selection = [{}];
this.selecting = false; this.selecting = false;
this.selectedObj = undefined; this.selectedObj = undefined;
openmct.selection.on('change', function (selection) {
if (selection[0] && selection[0].context.toolbar) {
this.select(selection[0].context.toolbar);
} else {
this.deselect();
}
}.bind(this));
} }
/** /**

View File

@ -27,11 +27,23 @@ define(
describe("The Elements Pane controller", function () { describe("The Elements Pane controller", function () {
var mockScope, var mockScope,
mockOpenMCT,
mockSelection,
controller; controller;
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpy("$scope"); mockScope = jasmine.createSpyObj("$scope", ['$on']);
controller = new ElementsController(mockScope); mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
controller = new ElementsController(mockScope, mockOpenMCT);
}); });
function getModel(model) { function getModel(model) {

View File

@ -29,7 +29,9 @@ define(
mockElement, mockElement,
testAttrs, testAttrs,
mockUnwatch, mockUnwatch,
representer; representer,
mockOpenMCT,
mockSelection;
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
@ -46,7 +48,18 @@ define(
mockScope.$parent.$watchCollection.andReturn(mockUnwatch); mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
representer = new EditToolbarRepresenter( representer = new EditToolbarRepresenter(
mockOpenMCT,
mockScope, mockScope,
mockElement, mockElement,
testAttrs testAttrs

View File

@ -28,13 +28,25 @@ define(
var testProxy, var testProxy,
testElement, testElement,
otherElement, otherElement,
selection; selection,
mockSelection,
mockOpenMCT;
beforeEach(function () { beforeEach(function () {
testProxy = { someKey: "some value" }; testProxy = { someKey: "some value" };
testElement = { someOtherKey: "some other value" }; testElement = { someOtherKey: "some other value" };
otherElement = { yetAnotherKey: 42 }; otherElement = { yetAnotherKey: 42 };
selection = new EditToolbarSelection(); mockSelection = jasmine.createSpyObj("selection", [
// 'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
selection = new EditToolbarSelection(mockOpenMCT);
selection.proxy(testProxy); selection.proxy(testProxy);
}); });

View File

@ -41,6 +41,7 @@ define([
"./src/controllers/BannerController", "./src/controllers/BannerController",
"./src/directives/MCTContainer", "./src/directives/MCTContainer",
"./src/directives/MCTDrag", "./src/directives/MCTDrag",
"./src/directives/MCTSelectable",
"./src/directives/MCTClickElsewhere", "./src/directives/MCTClickElsewhere",
"./src/directives/MCTResize", "./src/directives/MCTResize",
"./src/directives/MCTPopup", "./src/directives/MCTPopup",
@ -90,6 +91,7 @@ define([
BannerController, BannerController,
MCTContainer, MCTContainer,
MCTDrag, MCTDrag,
MCTSelectable,
MCTClickElsewhere, MCTClickElsewhere,
MCTResize, MCTResize,
MCTPopup, MCTPopup,
@ -328,6 +330,13 @@ define([
"$document" "$document"
] ]
}, },
{
"key": "mctSelectable",
"implementation": MCTSelectable,
"depends": [
"openmct"
]
},
{ {
"key": "mctClickElsewhere", "key": "mctClickElsewhere",
"implementation": MCTClickElsewhere, "implementation": MCTClickElsewhere,

View File

@ -25,6 +25,7 @@
} }
.l-fixed-position-item { .l-fixed-position-item {
border-width: 1px;
position: absolute; position: absolute;
&.s-not-selected { &.s-not-selected {
opacity: 0.8; opacity: 0.8;

View File

@ -80,23 +80,32 @@
// Editing Grids // Editing Grids
.l-grid-holder { .l-grid-holder {
display: block;
.l-grid { .l-grid {
&.l-grid-x { @include bgTicks($colorGridLines, 'x'); } &.l-grid-x { @include bgTicks($colorGridLines, 'x'); }
&.l-grid-y { @include bgTicks($colorGridLines, 'y'); } &.l-grid-y { @include bgTicks($colorGridLines, 'y'); }
} }
} }
// Prevent nested frames from showing their grids // Display grid when selected or selection parent.
.t-frame-outer .l-grid-holder { display: none !important; } .s-selected .l-grid-holder,
.s-selected-parent .l-grid-holder {
// Prevent nested elements from showing s-hover-border display: block;
.t-frame-outer .s-hover-border {
border: none !important;
} }
// Prevent nested frames from being selectable until we have proper sub-object editing // Display in nested frames...
.t-frame-outer .t-frame-outer { .t-frame-outer {
pointer-events: none; // ...when drilled in or selection parent...
&.s-drilled-in, &.s-selected-parent {
.l-grid-holder {
display: block;
}
.t-frame-outer:not(.s-drilled-in) .l-grid-holder {
display: none;
}
}
// ...but hide otherwise.
.l-grid-holder {
display: none;
}
} }
} }

View File

@ -23,15 +23,14 @@
$ohH: $btnFrameH; $ohH: $btnFrameH;
$bc: $colorInteriorBorder; $bc: $colorInteriorBorder;
&.child-frame.panel { &.child-frame.panel {
border: 1px solid transparent;
z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above
&:not(.no-frame) { &:not(.no-frame) {
background: $colorBodyBg; background: $colorBodyBg;
border: 1px solid $bc; border-color: $bc;
&:hover {
border-color: lighten($bc, 10%);
}
} }
} }
.object-browse-bar { .object-browse-bar {
font-size: 0.75em; font-size: 0.75em;
height: $ohH; height: $ohH;
@ -92,9 +91,9 @@
&.no-frame { &.no-frame {
background: transparent !important; background: transparent !important;
border: none !important; border: none;
.object-browse-bar .right { .object-browse-bar .right {
$m: 0; // $interiorMarginSm; $m: 0;
background: rgba(black, 0.3); background: rgba(black, 0.3);
border-radius: $basicCr; border-radius: $basicCr;
padding: $interiorMarginSm; padding: $interiorMarginSm;
@ -104,7 +103,7 @@
} }
&.t-frame-outer > .t-rep-frame { &.t-frame-outer > .t-rep-frame {
&.contents { &.contents {
$m: 2px; $m: 0px;
top: $m; top: $m;
right: $m; right: $m;
bottom: $m; bottom: $m;
@ -115,6 +114,7 @@
display: none; display: none;
} }
> .object-holder.abs { > .object-holder.abs {
overflow: hidden;
top: 0 !important; top: 0 !important;
} }
} }

View File

@ -20,16 +20,35 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
.s-hover-border { .s-hover-border {
border: 1px dotted transparent; &:hover {
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
}
} }
.s-status-editing { .s-status-editing {
// Limit to editing mode until we have sub-object selection // Limit to editing mode
$o: 0.5;
$oHover: 0.8;
$bc: $colorSelectableSelectedPrimary;
.s-hover-border { .s-hover-border {
// Show a border by default so user can see object bounds and empty objects // Show a border by default so user can see object bounds and empty objects
border: 1px dotted rgba($colorSelectableSelectedPrimary, 0.3) !important; border-color: rgba($bc, $o) !important;
border-style: dotted !important;
&:hover { &:hover {
border-color: rgba($colorSelectableSelectedPrimary, 0.7) !important; border-color: rgba($bc, $oHover) !important;
}
&.t-object-type-layout {
border-style: dashed !important;
}
}
.s-selected {
&.s-moveable {
&:not(.s-drilled-in) {
cursor: move;
}
}
} }
} }
@ -47,8 +66,5 @@
} }
} }
.s-selected > .s-moveable,
.s-selected.s-moveable {
cursor: move;
}
}

View File

@ -40,7 +40,7 @@ define(
// Gets an array of the contextual parents/ancestors of the selected object // Gets an array of the contextual parents/ancestors of the selected object
function getContextualPath() { function getContextualPath() {
var currentObj = $scope.ngModel.selectedObject, var currentObj = $scope.domainObject,
currentParent, currentParent,
parents = []; parents = [];
@ -68,7 +68,7 @@ define(
// If this the the initial call of this recursive function // If this the the initial call of this recursive function
if (!current) { if (!current) {
current = $scope.ngModel.selectedObject; current = $scope.domainObject;
$scope.primaryParents = []; $scope.primaryParents = [];
} }
@ -87,16 +87,16 @@ define(
// Gets the metadata for the selected object // Gets the metadata for the selected object
function getMetadata() { function getMetadata() {
$scope.metadata = $scope.ngModel.selectedObject && $scope.metadata = $scope.domainObject &&
$scope.ngModel.selectedObject.hasCapability('metadata') && $scope.domainObject.hasCapability('metadata') &&
$scope.ngModel.selectedObject.useCapability('metadata'); $scope.domainObject.useCapability('metadata');
} }
// Set scope variables when the selected object changes // Set scope variables when the selected object changes
$scope.$watch('ngModel.selectedObject', function () { $scope.$watch('domainObject', function () {
$scope.isLink = $scope.ngModel.selectedObject && $scope.isLink = $scope.domainObject &&
$scope.ngModel.selectedObject.hasCapability('location') && $scope.domainObject.hasCapability('location') &&
$scope.ngModel.selectedObject.getCapability('location').isLink(); $scope.domainObject.getCapability('location').isLink();
if ($scope.isLink) { if ($scope.isLink) {
getPrimaryPath(); getPrimaryPath();
@ -109,7 +109,7 @@ define(
getMetadata(); getMetadata();
}); });
var mutation = $scope.ngModel.selectedObject.getCapability('mutation'); var mutation = $scope.domainObject.getCapability('mutation');
var unlisten = mutation.listen(getMetadata); var unlisten = mutation.listen(getMetadata);
$scope.$on('$destroy', unlisten); $scope.$on('$destroy', unlisten);
} }

View File

@ -0,0 +1,60 @@
/*****************************************************************************
* 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.
*****************************************************************************/
define(
[],
function () {
/**
* The mct-selectable directive allows selection functionality
* (click) to be attached to specific elements.
*
* @memberof platform/commonUI/general
* @constructor
*/
function MCTSelectable(openmct) {
// Link; install event handlers.
function link(scope, element, attrs) {
var removeSelectable = openmct.selection.selectable(
element[0],
scope.$eval(attrs.mctSelectable),
attrs.hasOwnProperty('mctInitSelect') && scope.$eval(attrs.mctInitSelect) !== false
);
scope.$on("$destroy", function () {
removeSelectable();
});
}
return {
// mct-selectable only makes sense as an attribute
restrict: "A",
// Link function, to install event handlers
link: link
};
}
return MCTSelectable;
}
);

View File

@ -41,16 +41,6 @@ define(
"$scope", "$scope",
["$watch", "$on"] ["$watch", "$on"]
); );
mockScope.ngModel = {};
mockScope.ngModel.selectedObject = {
getCapability: function () {
return {
listen: function () {
return true;
}
};
}
};
mockObjectService = jasmine.createSpyObj( mockObjectService = jasmine.createSpyObj(
"objectService", "objectService",
@ -77,22 +67,27 @@ define(
"location capability", "location capability",
["isLink"] ["isLink"]
); );
mockDomainObject.getCapability.andCallFake(function (param) { mockDomainObject.getCapability.andCallFake(function (param) {
if (param === 'location') { if (param === 'location') {
return mockLocationCapability; return mockLocationCapability;
} else if (param === 'context') { } else if (param === 'context') {
return mockContextCapability; return mockContextCapability;
} else if (param === 'mutation') {
return {
listen: function () {
return true;
}
};
} }
}); });
mockScope.domainObject = mockDomainObject;
controller = new ObjectInspectorController(mockScope, mockObjectService); controller = new ObjectInspectorController(mockScope, mockObjectService);
// Change the selected object to trigger the watch call
mockScope.ngModel.selectedObject = mockDomainObject;
}); });
it("watches for changes to the selected object", function () { it("watches for changes to the selected object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith('ngModel.selectedObject', jasmine.any(Function)); expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
}); });
it("looks for contextual parent objects", function () { it("looks for contextual parent objects", function () {

View File

@ -38,7 +38,8 @@ define([
"implementation": InspectorController, "implementation": InspectorController,
"depends": [ "depends": [
"$scope", "$scope",
"policyService" "openmct",
"$document"
] ]
} }
], ],

View File

@ -21,44 +21,69 @@
*****************************************************************************/ *****************************************************************************/
define( define(
['../../browse/src/InspectorRegion'], [],
function (InspectorRegion) { function () {
/** /**
* The InspectorController adds region data for a domain object's type * The InspectorController listens for the selection changes and adds the selection
* to the scope. * object to the scope.
* *
* @constructor * @constructor
*/ */
function InspectorController($scope, policyService) { function InspectorController($scope, openmct, $document) {
var domainObject = $scope.domainObject, var self = this;
typeCapability = domainObject.getCapability('type'), self.$scope = $scope;
statusListener;
/** /**
* Filters region parts to only those allowed by region policies * Callback handler for the selection change event.
* @param regions * Adds the selection object to the scope. If the selected item has an inspector view,
* @returns {{}} * it puts the key in the scope. If provider view exists, it shows the view.
*/ */
function filterRegions(inspector) { function setSelection(selection) {
//Dupe so we're not modifying the type definition. if (selection[0]) {
return inspector.regions && inspector.regions.filter(function (region) { var view = openmct.inspectorViews.get(selection);
return policyService.allow('region', region, domainObject); var container = $document[0].querySelectorAll('.inspector-provider-view')[0];
}); container.innerHTML = "";
if (view) {
self.providerView = true;
view.show(container);
} else {
self.providerView = false;
$scope.inspectorKey = selection[0].context.oldItem.getCapability("type").typeDef.inspector;
}
} }
function setRegions() { self.$scope.selection = selection;
$scope.regions = filterRegions(typeCapability.getDefinition().inspector || new InspectorRegion());
} }
statusListener = domainObject.getCapability("status").listen(setRegions); openmct.selection.on("change", setSelection);
setSelection(openmct.selection.get());
$scope.$on("$destroy", function () { $scope.$on("$destroy", function () {
statusListener(); openmct.selection.off("change", setSelection);
}); });
setRegions();
} }
/**
* Gets the selected item.
*
* @returns a domain object
*/
InspectorController.prototype.selectedItem = function () {
return this.$scope.selection[0].context.oldItem;
};
/**
* Checks if a provider view exists.
*
* @returns 'true' if provider view exists, 'false' otherwise
*/
InspectorController.prototype.hasProviderView = function () {
return this.providerView;
};
return InspectorController; return InspectorController;
} }
); );

View File

@ -27,82 +27,93 @@ define(
describe("The inspector controller ", function () { describe("The inspector controller ", function () {
var mockScope, var mockScope,
mockDomainObject, mockDomainObject,
mockTypeCapability, mockOpenMCT,
mockTypeDefinition, mockSelection,
mockPolicyService, mockInspectorViews,
mockStatusCapability, mockTypeDef,
capabilities = {}, controller,
controller; container,
$document = [],
selectable = [];
beforeEach(function () { beforeEach(function () {
mockTypeDefinition = { mockTypeDef = {
inspector: typeDef: {
{ inspector: "some-key"
'regions': [
{'name': 'Part One'},
{'name': 'Part Two'}
]
} }
}; };
mockTypeCapability = jasmine.createSpyObj('typeCapability', [
'getDefinition'
]);
mockTypeCapability.getDefinition.andReturn(mockTypeDefinition);
capabilities.type = mockTypeCapability;
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
'listen'
]);
capabilities.status = mockStatusCapability;
mockDomainObject = jasmine.createSpyObj('domainObject', [ mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability' 'getCapability'
]); ]);
mockDomainObject.getCapability.andCallFake(function (name) { mockDomainObject.getCapability.andReturn(mockTypeDef);
return capabilities[name];
});
mockPolicyService = jasmine.createSpyObj('policyService', [
'allow'
]);
mockScope = jasmine.createSpyObj('$scope', mockScope = jasmine.createSpyObj('$scope',
['$on'] ['$on', 'selection']
); );
mockScope.domainObject = mockDomainObject; selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockInspectorViews = jasmine.createSpyObj('inspectorViews', ['get']);
mockOpenMCT = {
selection: mockSelection,
inspectorViews: mockInspectorViews
};
container = jasmine.createSpy('container', ['innerHTML']);
$document[0] = jasmine.createSpyObj("$document", ['querySelectorAll']);
$document[0].querySelectorAll.andReturn([container]);
controller = new InspectorController(mockScope, mockOpenMCT, $document);
}); });
it("filters out regions disallowed by region policy", function () { it("listens for selection change event", function () {
mockPolicyService.allow.andReturn(false); expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
controller = new InspectorController(mockScope, mockPolicyService); 'change',
expect(mockScope.regions.length).toBe(0); jasmine.any(Function)
);
expect(controller.selectedItem()).toEqual(mockDomainObject);
var mockItem = jasmine.createSpyObj('domainObject', [
'getCapability'
]);
mockItem.getCapability.andReturn(mockTypeDef);
selectable[0].context.oldItem = mockItem;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.selectedItem()).toEqual(mockItem);
}); });
it("does not filter out regions allowed by region policy", function () { it("cleans up on scope destroy", function () {
mockPolicyService.allow.andReturn(true); expect(mockScope.$on).toHaveBeenCalledWith(
controller = new InspectorController(mockScope, mockPolicyService); '$destroy',
expect(mockScope.regions.length).toBe(2); jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
}); });
it("Responds to status changes", function () { it("adds selection object to scope", function () {
mockPolicyService.allow.andReturn(true); expect(mockScope.selection).toEqual(selectable);
controller = new InspectorController(mockScope, mockPolicyService); expect(controller.selectedItem()).toEqual(mockDomainObject);
expect(mockScope.regions.length).toBe(2);
expect(mockStatusCapability.listen).toHaveBeenCalled();
mockPolicyService.allow.andReturn(false);
mockStatusCapability.listen.mostRecentCall.args[0]();
expect(mockScope.regions.length).toBe(0);
});
it("Unregisters status listener", function () {
var mockListener = jasmine.createSpy('listener');
mockStatusCapability.listen.andReturn(mockListener);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(mockListener).toHaveBeenCalled();
}); });
}); });
} }

View File

@ -260,7 +260,9 @@ define([
"key": "LayoutController", "key": "LayoutController",
"implementation": LayoutController, "implementation": LayoutController,
"depends": [ "depends": [
"$scope" "$scope",
"$element",
"openmct"
] ]
}, },
{ {

View File

@ -40,7 +40,7 @@
's-selected': controller.selected(element) 's-selected': controller.selected(element)
}" }"
ng-style="element.style" ng-style="element.style"
ng-click="controller.select(element)"> ng-click="controller.select(element, $event)">
<mct-include key="element.template" <mct-include key="element.template"
parameters="{ gridSize: controller.getGridSize() }" parameters="{ gridSize: controller.getGridSize() }"
ng-model="element"> ng-model="element">
@ -53,14 +53,16 @@
mct-drag-down="controller.moveHandle().startDrag(controller.selected())" mct-drag-down="controller.moveHandle().startDrag(controller.selected())"
mct-drag="controller.moveHandle().continueDrag(delta)" mct-drag="controller.moveHandle().continueDrag(delta)"
mct-drag-up="controller.moveHandle().endDrag()" mct-drag-up="controller.moveHandle().endDrag()"
ng-style="controller.selected().style"> ng-style="controller.selected().style"
ng-click="$event.stopPropagation()">
</div> </div>
<div ng-repeat="handle in controller.handles()" <div ng-repeat="handle in controller.handles()"
class="l-fixed-position-item-handle edit-corner" class="l-fixed-position-item-handle edit-corner"
ng-style="handle.style()" ng-style="handle.style()"
mct-drag-down="handle.startDrag()" mct-drag-down="handle.startDrag()"
mct-drag="handle.continueDrag(delta)" mct-drag="handle.continueDrag(delta)"
mct-drag-up="handle.endDrag()"> mct-drag-up="handle.endDrag()"
ng-click="$event.stopPropagation()">
</div> </div>
</span> </span>

View File

@ -22,10 +22,12 @@
<div class="abs l-layout" <div class="abs l-layout"
ng-controller="LayoutController as controller" ng-controller="LayoutController as controller"
ng-click="controller.clearSelection()"> ng-click="controller.bypassSelection($event)">
<!-- Background grid --> <!-- Background grid -->
<div class="l-grid-holder" ng-click="controller.clearSelection()"> <div class="l-grid-holder"
ng-show="!controller.drilledIn"
ng-click="controller.bypassSelection($event)">
<div class="l-grid l-grid-x" <div class="l-grid l-grid-x"
ng-if="!controller.getGridSize()[0] < 3" ng-if="!controller.getGridSize()[0] < 3"
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div> ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
@ -34,10 +36,12 @@
ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div> ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div>
</div> </div>
<div class='abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border' <div class="abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border {{childObject.getId() + '-' + $id}} t-object-type-{{ childObject.getModel().type }}"
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-selected':controller.selected(childObject) }" ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }"
ng-repeat="childObject in composition" ng-repeat="childObject in composition"
ng-click="controller.select($event, childObject.getId())" ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)"
mct-selectable="controller.getContext(childObject, true)"
ng-dblclick="controller.drill($event, childObject)"
ng-style="controller.getFrameStyle(childObject.getId())"> ng-style="controller.getFrameStyle(childObject.getId())">
<mct-representation key="'frame'" <mct-representation key="'frame'"
@ -45,7 +49,7 @@
mct-object="childObject"> mct-object="childObject">
</mct-representation> </mct-representation>
<!-- Drag handles --> <!-- Drag handles -->
<span class="abs t-edit-handle-holder s-hover-border" ng-if="controller.selected(childObject)"> <span class="abs t-edit-handle-holder" ng-if="controller.selected(childObject) && !controller.isDrilledIn(childObject)">
<span class="edit-handle edit-move" <span class="edit-handle edit-move"
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])" mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])"
mct-drag="controller.continueDrag(delta)" mct-drag="controller.continueDrag(delta)"
@ -73,7 +77,6 @@
mct-drag-up="controller.endDrag()"> mct-drag-up="controller.endDrag()">
</span> </span>
</span> </span>
</div> </div>
</div> </div>

View File

@ -506,7 +506,11 @@ define(
* Set the active user selection in this view. * Set the active user selection in this view.
* @param element the element to select * @param element the element to select
*/ */
FixedController.prototype.select = function select(element) { FixedController.prototype.select = function select(element, event) {
if (event) {
event.stopPropagation();
}
if (this.selection) { if (this.selection) {
// Update selection... // Update selection...
this.selection.select(element); this.selection.select(element);

View File

@ -27,9 +27,11 @@
*/ */
define( define(
[ [
'zepto',
'./LayoutDrag' './LayoutDrag'
], ],
function ( function (
$,
LayoutDrag LayoutDrag
) { ) {
@ -50,10 +52,12 @@ define(
* @constructor * @constructor
* @param {Scope} $scope the controller's Angular scope * @param {Scope} $scope the controller's Angular scope
*/ */
function LayoutController($scope) { function LayoutController($scope, $element, openmct) {
var self = this, var self = this,
callbackCount = 0; callbackCount = 0;
this.$element = $element;
// Update grid size when it changed // Update grid size when it changed
function updateGridSize(layoutGrid) { function updateGridSize(layoutGrid) {
var oldSize = self.gridSize; var oldSize = self.gridSize;
@ -123,12 +127,11 @@ define(
self.layoutPanels(ids); self.layoutPanels(ids);
self.setFrames(ids); self.setFrames(ids);
// If there is a newly-dropped object, select it. if (self.selectedId &&
if (self.droppedIdToSelectAfterRefresh) { self.selectedId !== $scope.domainObject.getId() &&
self.select(null, self.droppedIdToSelectAfterRefresh); composition.indexOf(self.selectedId) === -1) {
delete self.droppedIdToSelectAfterRefresh; // Click triggers selection of layout parent.
} else if (composition.indexOf(self.selectedId) === -1) { self.$element[0].click();
self.clearSelection();
} }
} }
}); });
@ -160,22 +163,39 @@ define(
} }
}; };
// Sets the selectable object in response to the selection change event.
function setSelection(selectable) {
var selection = selectable[0];
if (!selection) {
delete self.selectedId;
return;
}
self.selectedId = selection.context.oldItem.getId();
self.drilledIn = undefined;
self.selectable = selectable;
}
this.positions = {}; this.positions = {};
this.rawPositions = {}; this.rawPositions = {};
this.gridSize = DEFAULT_GRID_SIZE; this.gridSize = DEFAULT_GRID_SIZE;
this.$scope = $scope; this.$scope = $scope;
this.drilledIn = undefined;
this.openmct = openmct;
// Watch for changes to the grid size in the model // Watch for changes to the grid size in the model
$scope.$watch("model.layoutGrid", updateGridSize); $scope.$watch("model.layoutGrid", updateGridSize);
$scope.$watch("selection", function (selection) {
this.selection = selection;
}.bind(this));
// Update composed objects on screen, and position panes // Update composed objects on screen, and position panes
$scope.$watchCollection("model.composition", refreshComposition); $scope.$watchCollection("model.composition", refreshComposition);
// Position panes where they are dropped openmct.selection.on('change', setSelection);
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
$scope.$on("mctDrop", handleDrop); $scope.$on("mctDrop", handleDrop);
} }
@ -357,37 +377,14 @@ define(
}; };
/** /**
* Check if the object is currently selected. * Checks if the object is currently selected.
* *
* @param {string} obj the object to check for selection * @param {string} obj the object to check for selection
* @returns {boolean} true if selected, otherwise false * @returns {boolean} true if selected, otherwise false
*/ */
LayoutController.prototype.selected = function (obj) { LayoutController.prototype.selected = function (obj) {
return !!this.selectedId && this.selectedId === obj.getId(); var sobj = this.openmct.selection.get()[0];
}; return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false;
/**
* Set the active user selection in this view.
*
* @param event the mouse event
* @param {string} id the object id
*/
LayoutController.prototype.select = function (event, id) {
if (event) {
event.stopPropagation();
if (this.selection) {
event.preventDefault();
}
}
this.selectedId = id;
var selectedObj = {};
selectedObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id);
if (this.selection) {
this.selection.select(selectedObj);
}
}; };
/** /**
@ -396,7 +393,7 @@ define(
* @param {string} id the object id * @param {string} id the object id
* @private * @private
*/ */
LayoutController.prototype.toggleFrame = function (id) { LayoutController.prototype.toggleFrame = function (id, domainObject) {
var configuration = this.$scope.configuration; var configuration = this.$scope.configuration;
if (!configuration.panels[id]) { if (!configuration.panels[id]) {
@ -404,21 +401,75 @@ define(
} }
this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id]; this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id];
this.select(undefined, id); // reselect so toolbar updates
var selection = this.openmct.selection.get();
selection[0].context.toolbar = this.getToolbar(id, domainObject);
this.openmct.selection.select(selection); // reselect so toolbar updates
}; };
/** /**
* Clear the current user selection. * 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.clearSelection = function () { 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.
*
* @param event the angular event object
*/
LayoutController.prototype.bypassSelection = function (event) {
if (this.dragInProgress) { if (this.dragInProgress) {
if (event) {
event.stopPropagation();
}
return;
}
};
/**
* Checks if the domain object is drilled in.
*
* @param domainObject the domain object
* @return true if the object is drilled in, false otherwise
*/
LayoutController.prototype.isDrilledIn = function (domainObject) {
return this.drilledIn === domainObject.getId();
};
/**
* Puts the given object in the drilled-in mode.
*
* @param event the angular event object
* @param domainObject the domain object
*/
LayoutController.prototype.drill = function (event, domainObject) {
if (event) {
event.stopPropagation();
}
if (!domainObject.getCapability('editor').inEditContext()) {
return; return;
} }
if (this.selection) { if (!domainObject.hasCapability('composition')) {
this.selection.deselect(); return;
delete this.selectedId;
} }
// Disable since fixed position doesn't use the selection API yet
if (domainObject.getModel().type === 'telemetry.fixed') {
return;
}
this.drilledIn = domainObject.getId();
}; };
/** /**
@ -440,6 +491,36 @@ define(
return this.gridSize; return this.gridSize;
}; };
/**
* Gets the selection context.
*
* @param domainObject the domain object
* @returns {object} the context object which includes
* item, oldItem and toolbar
*/
LayoutController.prototype.getContext = function (domainObject, toolbar) {
return {
item: domainObject.useCapability('adapter'),
oldItem: domainObject,
toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined
};
};
/**
* Selects a newly-dropped object.
*
* @param classSelector the css class selector
* @param domainObject the domain object
*/
LayoutController.prototype.selectIfNew = function (selector, domainObject) {
if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) {
setTimeout(function () {
$('.' + selector)[0].click();
delete this.droppedIdToSelectAfterRefresh;
}.bind(this), 0);
}
};
return LayoutController; return LayoutController;
} }
); );

View File

@ -21,8 +21,14 @@
*****************************************************************************/ *****************************************************************************/
define( define(
["../src/LayoutController"], [
function (LayoutController) { "../src/LayoutController",
"zepto"
],
function (
LayoutController,
$
) {
describe("The Layout controller", function () { describe("The Layout controller", function () {
var mockScope, var mockScope,
@ -32,7 +38,12 @@ define(
controller, controller,
mockCompositionCapability, mockCompositionCapability,
mockComposition, mockComposition,
mockCompositionObjects; mockCompositionObjects,
mockOpenMCT,
mockSelection,
mockDomainObjectCapability,
$element = [],
selectable = [];
function mockPromise(value) { function mockPromise(value) {
return { return {
@ -58,21 +69,18 @@ define(
} else { } else {
return {}; return {};
} }
},
getCapability: function () {
return mockDomainObjectCapability;
},
hasCapability: function (param) {
if (param === 'composition') {
return id !== 'b';
}
} }
}; };
} }
// Utility function to 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;
}
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
"$scope", "$scope",
@ -88,7 +96,6 @@ define(
mockComposition = ["a", "b", "c"]; mockComposition = ["a", "b", "c"];
mockCompositionObjects = mockComposition.map(mockDomainObject); mockCompositionObjects = mockComposition.map(mockDomainObject);
testConfiguration = { testConfiguration = {
panels: { panels: {
a: { a: {
@ -97,27 +104,70 @@ define(
} }
} }
}; };
mockDomainObjectCapability = jasmine.createSpyObj('capability',
['inEditContext']
);
mockCompositionCapability = mockPromise(mockCompositionObjects); mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject"); mockScope.domainObject = mockDomainObject("mockDomainObject");
mockScope.model = testModel; mockScope.model = testModel;
mockScope.configuration = testConfiguration; mockScope.configuration = testConfiguration;
mockScope.selection = jasmine.createSpyObj(
'selection', selectable[0] = {
['select', 'get', 'selected', 'deselect'] context: {
); oldItem: mockScope.domainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockOpenMCT = {
selection: mockSelection
};
$element = $('<div></div>');
$(document).find('body').append($element);
spyOn($element[0], 'click');
spyOn(mockScope.domainObject, "useCapability").andCallThrough(); spyOn(mockScope.domainObject, "useCapability").andCallThrough();
controller = new LayoutController(mockScope); controller = new LayoutController(mockScope, $element, mockOpenMCT);
spyOn(controller, "layoutPanels").andCallThrough(); spyOn(controller, "layoutPanels").andCallThrough();
findWatch("selection")(mockScope.selection);
jasmine.Clock.useMock(); jasmine.Clock.useMock();
}); });
afterEach(function () {
$element.remove();
});
it("listens for selection change events", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
// Model changes will indicate that panel positions // Model changes will indicate that panel positions
// may have changed, for instance. // may have changed, for instance.
it("watches for changes to composition", function () { it("watches for changes to composition", function () {
@ -320,67 +370,35 @@ define(
.not.toEqual(oldStyle); .not.toEqual(oldStyle);
}); });
it("allows panels to be selected", function () { it("allows objects to be selected", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.selected(childObj)).toBe(true);
});
it("prevents event bubbling while drag is in progress", function () {
mockScope.$watchCollection.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0]; var childObj = mockCompositionObjects[0];
controller.select(mockEvent, childObj.getId()); // Do a drag
controller.startDrag(childObj.getId(), [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// Because mouse position could cause the parent object to be selected, this should be ignored.
controller.bypassSelection(mockEvent);
expect(mockEvent.stopPropagation).toHaveBeenCalled(); expect(mockEvent.stopPropagation).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(true); // Shoud be able to select another object when dragging is done.
});
it("allows selection to be cleared", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(null, childObj.getId());
controller.clearSelection();
expect(controller.selected(childObj)).toBeFalsy();
});
it("prevents clearing selection while drag is in progress", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// Because mouse position could cause clearSelection to be called, this should be ignored.
controller.clearSelection();
expect(controller.selected(childObj)).toBe(true);
// Shoud be able to clear the selection after dragging is done.
jasmine.Clock.tick(0); jasmine.Clock.tick(0);
controller.clearSelection(); mockEvent.stopPropagation.reset();
controller.bypassSelection(mockEvent);
expect(controller.selected(childObj)).toBe(false); expect(mockEvent.stopPropagation).not.toHaveBeenCalled();
});
it("clears selection after moving/resizing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
jasmine.Clock.tick(0);
controller.clearSelection();
expect(controller.selected(childObj)).toBe(false);
}); });
it("shows frames by default", function () { it("shows frames by default", function () {
@ -398,43 +416,74 @@ define(
it("hides frame when selected object has frame ", function () { it("hides frame when selected object has frame ", function () {
mockScope.$watchCollection.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0]; var childObj = mockCompositionObjects[0];
controller.select(mockEvent, childObj.getId()); selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockScope.selection.select).toHaveBeenCalled(); var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
expect(controller.hasFrame(childObj)).toBe(true); expect(controller.hasFrame(childObj)).toBe(true);
expect(selectedObj.hideFrame).toBeDefined(); expect(toolbarObj.hideFrame).toBeDefined();
expect(selectedObj.hideFrame).toEqual(jasmine.any(Function)); expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function));
}); });
it("shows frame when selected object has no frame", function () { it("shows frame when selected object has no frame", function () {
mockScope.$watchCollection.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1]; var childObj = mockCompositionObjects[1];
controller.select(mockEvent, childObj.getId()); selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockScope.selection.select).toHaveBeenCalled(); var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
expect(controller.hasFrame(childObj)).toBe(false); expect(controller.hasFrame(childObj)).toBe(false);
expect(selectedObj.showFrame).toBeDefined(); expect(toolbarObj.showFrame).toBeDefined();
expect(selectedObj.showFrame).toEqual(jasmine.any(Function)); expect(toolbarObj.showFrame).toEqual(jasmine.any(Function));
}); });
it("deselects the object that is no longer in the composition", function () { it("selects the parent object when selected object is removed", function () {
mockScope.$watchCollection.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0]; var childObj = mockCompositionObjects[0];
controller.select(mockEvent, childObj.getId()); selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var composition = ["b", "c"]; var composition = ["b", "c"];
mockScope.$watchCollection.mostRecentCall.args[1](composition); mockScope.$watchCollection.mostRecentCall.args[1](composition);
expect(controller.selected(childObj)).toBe(false); expect($element[0].click).toHaveBeenCalled();
}); });
it("allows objects to be drilled-in only when editing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
childObj.getCapability().inEditContext.andReturn(false);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("allows objects to be drilled-in only if it has sub objects", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
childObj.getCapability().inEditContext.andReturn(true);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("selects a newly-dropped object", function () {
mockScope.$on.mostRecentCall.args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
var childObj = mockDomainObject("d");
var testElement = $("<div class='some-class'></div>");
$element.append(testElement);
spyOn(testElement[0], 'click');
controller.selectIfNew('some-class', childObj);
jasmine.Clock.tick(0);
expect(testElement[0].click).toHaveBeenCalled();
});
}); });
} }
); );

View File

@ -69,7 +69,7 @@ define([
"delegates": [ "delegates": [
"telemetry" "telemetry"
], ],
"inspector": tableInspector, "inspector": "table-options-edit",
"contains": [ "contains": [
{ {
"has": "telemetry" "has": "telemetry"

View File

@ -19,7 +19,10 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div ng-controller="TableOptionsController" class="l-controls-first flex-elem grows l-inspector-part">
<div ng-if="domainObject.getCapability('editor').inEditContext()"
ng-controller="TableOptionsController"
class="l-controls-first flex-elem grows l-inspector-part">
<em class="t-inspector-part-header" title="Display properties for this object">Table Options</em> <em class="t-inspector-part-header" title="Display properties for this object">Table Options</em>
<mct-form <mct-form
ng-model="configuration.table.columns" ng-model="configuration.table.columns"

View File

@ -28,7 +28,8 @@ define([
'./selection/Selection', './selection/Selection',
'./api/objects/object-utils', './api/objects/object-utils',
'./plugins/plugins', './plugins/plugins',
'./ui/ViewRegistry' './ui/ViewRegistry',
'./ui/InspectorViewRegistry'
], function ( ], function (
EventEmitter, EventEmitter,
legacyRegistry, legacyRegistry,
@ -37,7 +38,8 @@ define([
Selection, Selection,
objectUtils, objectUtils,
plugins, plugins,
ViewRegistry ViewRegistry,
InspectorViewRegistry
) { ) {
/** /**
* Open MCT is an extensible web application for building mission * Open MCT is an extensible web application for building mission
@ -112,15 +114,13 @@ define([
/** /**
* Registry for views which should appear in the Inspector area. * Registry for views which should appear in the Inspector area.
* These views will be chosen based on selection state, so * These views will be chosen based on the selection state.
* providers should be prepared to test arbitrary objects for
* viewability.
* *
* @type {module:openmct.ViewRegistry} * @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name inspectors * @name inspectorViews
*/ */
this.inspectors = new ViewRegistry(); this.inspectorViews = new InspectorViewRegistry();
/** /**
* Registry for views which should appear in Edit Properties * Registry for views which should appear in Edit Properties
@ -196,7 +196,6 @@ define([
this.Dialog = api.Dialog; this.Dialog = api.Dialog;
this.on('navigation', this.selection.clear.bind(this.selection));
} }
MCT.prototype = Object.create(EventEmitter.prototype); MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -33,37 +33,96 @@ define(['EventEmitter'], function (EventEmitter) {
Selection.prototype = Object.create(EventEmitter.prototype); Selection.prototype = Object.create(EventEmitter.prototype);
Selection.prototype.add = function (context) { /**
this.clear(); // Only allow single select as initial simplification * Gets the selected object.
this.selected.push(context); * @public
this.emit('change'); */
}; Selection.prototype.get = function () {
Selection.prototype.remove = function (path) {
this.selected = this.selected.filter(function (otherPath) {
return !path.matches(otherPath);
});
this.emit('change');
};
Selection.prototype.contains = function (path) {
return this.selected.some(function (otherPath) {
return path.matches(otherPath);
});
};
Selection.prototype.clear = function () {
this.selected = [];
this.emit('change');
};
Selection.prototype.primary = function () {
return this.selected[this.selected.length - 1];
};
Selection.prototype.all = function () {
return this.selected; return this.selected;
}; };
/**
* Selects the selectable object and emits the 'change' event.
*
* @param {object} selectable an object with element and context properties
* @private
*/
Selection.prototype.select = function (selectable) {
if (!Array.isArray(selectable)) {
selectable = [selectable];
}
if (this.selected[0] && this.selected[0].element) {
this.selected[0].element.classList.remove('s-selected');
}
if (this.selected[1]) {
this.selected[1].element.classList.remove('s-selected-parent');
}
if (selectable[0] && selectable[0].element) {
selectable[0].element.classList.add('s-selected');
}
if (selectable[1]) {
selectable[1].element.classList.add('s-selected-parent');
}
this.selected = selectable;
this.emit('change', this.selected);
};
/**
* @private
*/
Selection.prototype.capture = function (selectable) {
if (!this.capturing) {
this.capturing = [];
}
this.capturing.push(selectable);
};
/**
* @private
*/
Selection.prototype.selectCapture = function (selectable) {
if (!this.capturing) {
return;
}
this.select(this.capturing.reverse());
delete this.capturing;
};
/**
* Attaches the click handlers to the element.
*
* @param element an html element
* @param context object with oldItem, item and toolbar properties
* @param select a flag to select the element if true
* @returns a function that removes the click handlers from the element
* @public
*/
Selection.prototype.selectable = function (element, context, select) {
var selectable = {
context: context,
element: element
};
var capture = this.capture.bind(this, selectable);
var selectCapture = this.selectCapture.bind(this, selectable);
element.addEventListener('click', capture, true);
element.addEventListener('click', selectCapture);
if (select) {
this.select(selectable);
}
return function () {
element.removeEventListener('click', capture);
element.removeEventListener('click', selectCapture);
};
};
return Selection; return Selection;
}); });

View File

@ -0,0 +1,154 @@
/*****************************************************************************
* 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 InspectorViewRegistry maintains the definitions for views
* that may occur in the inspector.
*
* @interface InspectorViewRegistry
* @memberof module:openmct
*/
function InspectorViewRegistry() {
this.providers = {};
}
/**
*
* @param {object} selection the object to be viewed
* @returns {module:openmct.InspectorViewRegistry[]} any providers
* which can provide views of this object
* @private for platform-internal use
*/
InspectorViewRegistry.prototype.get = function (selection) {
var providers = this.getAllProviders().filter(function (provider) {
return provider.canView(selection);
});
if (providers && providers.length > 0) {
return providers[0].view(selection);
}
};
/**
* @private
*/
InspectorViewRegistry.prototype.getAllProviders = function () {
return Object.values(this.providers);
};
/**
* Registers a new type of view.
*
* @param {module:openmct.InspectorViewRegistry} provider the provider for this view
* @method addProvider
* @memberof module:openmct.InspectorViewRegistry#
*/
InspectorViewRegistry.prototype.addProvider = function (provider) {
var key = provider.key;
if (key === undefined) {
throw "View 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;
};
/**
* @private
*/
InspectorViewRegistry.prototype.getByProviderKey = function (key) {
return this.providers[key];
};
/**
* A View is used to provide displayable content, and to react to
* associated life cycle events.
*
* @name View
* @interface
* @memberof module:openmct
*/
/**
* Populate the supplied DOM element with the contents of this view.
*
* View implementations should use this method to attach any
* listeners or acquire other resources that are necessary to keep
* the contents of this view up-to-date.
*
* @param {HTMLElement} container the DOM element to populate
* @method show
* @memberof module:openmct.View#
*/
/**
* Release any resources associated with this view.
*
* View implementations should use this method to detach any
* listeners or release other resources that are no longer necessary
* once a view is no longer used.
*
* @method destroy
* @memberof module:openmct.View#
*/
/**
* Exposes types of views in inspector.
*
* @interface InspectorViewProvider
* @property {string} key a unique identifier for this view
* @property {string} name the human-readable name of this view
* @property {string} [description] a longer-form description (typically
* a single sentence or short paragraph) of this kind of view
* @property {string} [cssClass] the CSS class to apply to labels for this
* view (to add icons, for instance)
* @memberof module:openmct
*/
/**
* Checks if this provider can supply views for a selection.
*
* @method canView
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection
* @returns {boolean} 'true' if the view applies to the provided selection,
* otherwise 'false'.
*/
/**
* Provides a view of the selection object in the inspector.
*
* @method view
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection the selection object
* @returns {module:openmct.View} a view of this selection
*/
return InspectorViewRegistry;
});