[Toolbar] Implement a public API for adding toolbars (#1908)

* [API] Implement a toolbar registry and a plugin to allow providing a toolbar for a selected object.
* Modify the mct-toolbar directive to get the toolbar structure from a provider based on selection.
* Implements the layout toolbar in the layout bundle
This commit is contained in:
Pegah Sarram 2018-06-27 13:30:01 -07:00 committed by Andrew Henry
parent de8f8d174d
commit 73e38f1955
39 changed files with 1400 additions and 1844 deletions

View File

@ -39,7 +39,6 @@ define([
"./src/policies/EditableMovePolicy", "./src/policies/EditableMovePolicy",
"./src/policies/EditContextualActionPolicy", "./src/policies/EditContextualActionPolicy",
"./src/representers/EditRepresenter", "./src/representers/EditRepresenter",
"./src/representers/EditToolbarRepresenter",
"./src/capabilities/EditorCapability", "./src/capabilities/EditorCapability",
"./src/capabilities/TransactionCapabilityDecorator", "./src/capabilities/TransactionCapabilityDecorator",
"./src/services/TransactionManager", "./src/services/TransactionManager",
@ -78,7 +77,6 @@ define([
EditableMovePolicy, EditableMovePolicy,
EditContextualActionPolicy, EditContextualActionPolicy,
EditRepresenter, EditRepresenter,
EditToolbarRepresenter,
EditorCapability, EditorCapability,
TransactionCapabilityDecorator, TransactionCapabilityDecorator,
TransactionManager, TransactionManager,
@ -381,12 +379,6 @@ define([
"depends": [ "depends": [
"$log" "$log"
] ]
},
{
"implementation": EditToolbarRepresenter,
"depends": [
"openmct"
]
} }
], ],
"constants": [ "constants": [
@ -424,6 +416,17 @@ define([
"transactionService" "transactionService"
] ]
} }
],
"runs": [
{
depends: [
"toolbars[]",
"openmct"
],
implementation: function (toolbars, openmct) {
toolbars.forEach(openmct.toolbars.addProvider, openmct.toolbars);
}
}
] ]
} }
}); });

View File

@ -24,7 +24,8 @@
<div class="items-select left flex-elem l-flex-row grows"> <div class="items-select left flex-elem l-flex-row grows">
<mct-representation key="'back-arrow'" <mct-representation key="'back-arrow'"
mct-object="domainObject" mct-object="domainObject"
class="flex-elem l-back"></mct-representation> class="flex-elem l-back">
</mct-representation>
<mct-representation key="'object-header'" <mct-representation key="'object-header'"
mct-object="domainObject" mct-object="domainObject"
class="l-flex-row flex-elem grows object-header"> class="l-flex-row flex-elem grows object-header">
@ -48,8 +49,8 @@
<!-- Toolbar and Save/Cancel buttons --> <!-- Toolbar and Save/Cancel buttons -->
<div class="l-edit-controls flex-elem l-flex-row flex-align-end"> <div class="l-edit-controls flex-elem l-flex-row flex-align-end">
<mct-toolbar name="mctToolbar" <mct-toolbar name="mctToolbar"
structure="toolbar.structure" structure="editToolbar.structure"
ng-model="toolbar.state" ng-model="editToolbar.state"
class="flex-elem grows"> class="flex-elem grows">
</mct-toolbar> </mct-toolbar>
<mct-representation key="'edit-action-buttons'" <mct-representation key="'edit-action-buttons'"
@ -61,7 +62,6 @@
<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"
mct-selectable="{ mct-selectable="{
item: domainObject.useCapability('adapter'), item: domainObject.useCapability('adapter'),
oldItem: domainObject oldItem: domainObject

View File

@ -20,192 +20,110 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define( define(
[], [
function () { '../../../../../src/api/objects/object-utils',
'lodash'
// Utility functions for reducing truth arrays ],
function and(a, b) { function (
return a && b; objectUtils,
} _
function or(a, b) { ) {
return a || b;
}
/** /**
* Provides initial structure and state (as suitable for provision * Provides initial structure and state (as suitable for provision
* to the `mct-toolbar` directive) for a view's tool bar, based on * to the `mct-toolbar` directive) for a view's toolbar, based on
* that view's declaration of what belongs in its tool bar and on * that view's declaration of what belongs in its toolbar and on
* the current selection. * the current selection.
* *
* @param structure toolbar structure, as provided by view definition * @param $scope the Angular scope
* @param {Function} commit callback to invoke after changes * @param {Object} openmct the openmct object
* @param structure the toolbar structure
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
*/ */
function EditToolbar(structure, commit) { function EditToolbar($scope, openmct, structure) {
this.toolbarStructure = [];
this.properties = [];
this.toolbarState = [];
this.openmct = openmct;
this.domainObjectsById = {};
this.unobserveObjects = [];
this.stateTracker = [];
$scope.$watchCollection(this.getState.bind(this), this.handleStateChanges.bind(this));
$scope.$on("$destroy", this.destroy.bind(this));
this.updateToolbar(structure);
this.registerListeners(structure);
}
/**
* Updates the toolbar with a new structure.
*
* @param {Array} structure the toolbar structure
*/
EditToolbar.prototype.updateToolbar = function (structure) {
var self = this; var self = this;
// Generate a new key for an item's property function addKey(item) {
function addKey(property) { self.stateTracker.push({
self.properties.push(property); id: objectUtils.makeKeyString(item.domainObject.identifier),
domainObject: item.domainObject,
property: item.property
});
self.properties.push(item.property);
return self.properties.length - 1; // Return index of property return self.properties.length - 1; // Return index of property
} }
// Invoke all functions in selections with the given name
function invoke(method, value) {
if (method) {
// Make the change in the selection
self.selection.forEach(function (selected) {
if (typeof selected[method] === 'function') {
selected[method](value);
}
});
// ...and commit!
commit();
}
}
// Prepare a toolbar item based on current selection
function convertItem(item) { function convertItem(item) {
var converted = Object.create(item || {}); var converted = Object.create(item || {});
if (item.property) { if (item.property) {
converted.key = addKey(item.property); converted.key = addKey(item);
} }
if (item.method) { if (item.method) {
converted.click = function (v) { converted.click = function (value) {
invoke(item.method, v); item.method(value);
}; };
} }
return converted; return converted;
} }
// Prepare a toolbar section
function convertSection(section) {
var converted = Object.create(section || {});
converted.items =
((section || {}).items || [])
.map(convertItem);
return converted;
}
this.toolbarState = [];
this.selection = undefined;
this.properties = [];
this.toolbarStructure = Object.create(structure || {});
this.toolbarStructure.sections =
((structure || {}).sections || []).map(convertSection);
}
// Check if all elements of the selection which have this
// property have the same value for this property.
EditToolbar.prototype.isConsistent = function (property) {
var self = this,
consistent = true,
observed = false,
state;
// Check if a given element of the selection is consistent
// with previously-observed elements for this property.
function checkConsistency(selected) {
var next;
// Ignore selections which don't have this property
if (selected[property] !== undefined) {
// Look up state of this element in the selection
next = self.lookupState(property, selected);
// Detect inconsistency
if (observed) {
consistent = consistent && (next === state);
}
// Track state for next iteration
state = next;
observed = true;
}
}
// Iterate through selections
self.selection.forEach(checkConsistency);
return consistent;
};
// Used to filter out items which are applicable (or not)
// to the current selection.
EditToolbar.prototype.isApplicable = function (item) {
var property = (item || {}).property,
method = (item || {}).method,
exclusive = !!(item || {}).exclusive;
// Check if a selected item defines this property
function hasProperty(selected) {
return (property && (selected[property] !== undefined)) ||
(method && (typeof selected[method] === 'function'));
}
return this.selection.map(hasProperty).reduce(
exclusive ? and : or,
exclusive
) && this.isConsistent(property);
};
// Look up the current value associated with a property
EditToolbar.prototype.lookupState = function (property, selected) {
var value = selected[property];
return (typeof value === 'function') ? value() : value;
};
/**
* Set the current selection. Visibility of sections
* and items in the toolbar will be updated to match this.
* @param {Array} s the new selection
*/
EditToolbar.prototype.setSelection = function (s) {
var self = this;
// Show/hide controls in this section per applicability
function refreshSectionApplicability(section) {
var count = 0;
// Show/hide each item
(section.items || []).forEach(function (item) {
item.hidden = !self.isApplicable(item);
count += item.hidden ? 0 : 1;
});
// Hide this section if there are no applicable items
section.hidden = !count;
}
// Get initial value for a given property // Get initial value for a given property
function initializeState(property) { function initializeState(property) {
var result; var result;
// Look through all selections for this property; structure.forEach(function (item) {
// values should all match by the time we perform if (item.property === property) {
// this lookup anyway. result = _.get(item.domainObject, item.property);
self.selection.forEach(function (selected) { }
result = (selected[property] !== undefined) ?
self.lookupState(property, selected) :
result;
}); });
return result; return result;
} }
this.selection = s; // Tracks the domain object and property for every element in the state array
this.toolbarStructure.sections.forEach(refreshSectionApplicability); this.stateTracker = [];
this.toolbarStructure = structure.map(convertItem);
this.toolbarState = this.properties.map(initializeState); this.toolbarState = this.properties.map(initializeState);
}; };
/** /**
* Get the structure of the toolbar, as appropriate to * Gets the structure of the toolbar, as appropriate to
* pass to `mct-toolbar`. * pass to `mct-toolbar`.
* @returns the toolbar structure *
* @returns {Array} the toolbar structure
*/ */
EditToolbar.prototype.getStructure = function () { EditToolbar.prototype.getStructure = function () {
return this.toolbarStructure; return this.toolbarStructure;
}; };
/** /**
* Get the current state of the toolbar, as appropriate * Gets the current state of the toolbar, as appropriate
* to two-way bind to the state handled by `mct-toolbar`. * to two-way bind to the state handled by `mct-toolbar`.
*
* @returns {Array} state of the toolbar * @returns {Array} state of the toolbar
*/ */
EditToolbar.prototype.getState = function () { EditToolbar.prototype.getState = function () {
@ -213,48 +131,124 @@ define(
}; };
/** /**
* Update state within the current selection. * Mutates the domain object's property with a new value.
*
* @param {Object} dominObject the domain object
* @param {string} property the domain object's property to update
* @param value the property's new value
*/
EditToolbar.prototype.updateDomainObject = function (domainObject, property, value) {
this.openmct.objects.mutate(domainObject, property, value);
};
/**
* Updates state with the new value.
*
* @param {number} index the index of the corresponding * @param {number} index the index of the corresponding
* element in the state array * element in the state array
* @param value the new value to convey to the selection * @param value the new value to update the state array with
*/ */
EditToolbar.prototype.updateState = function (index, value) { EditToolbar.prototype.updateState = function (index, value) {
this.toolbarState[index] = value;
};
/**
* Register listeners for domain objects to watch for updates.
*
* @param {Array} the toolbar structure
*/
EditToolbar.prototype.registerListeners = function (structure) {
var self = this; var self = this;
// Update value for this property in all elements of the function observeObject(domainObject, id) {
// selection which have this property. var unobserveObject = self.openmct.objects.observe(domainObject, '*', function (newObject) {
function updateProperties(property, val) { self.domainObjectsById[id].newObject = JSON.parse(JSON.stringify(newObject));
var changed = false; self.scheduleStateUpdate();
});
// Update property in a selected element self.unobserveObjects.push(unobserveObject);
function updateProperty(selected) {
// Ignore selected elements which don't have this property
if (selected[property] !== undefined) {
// Check if this is a setter, or just assignable
if (typeof selected[property] === 'function') {
changed =
changed || (selected[property]() !== val);
selected[property](val);
} else {
changed =
changed || (selected[property] !== val);
selected[property] = val;
}
}
}
// Update property in all selected elements
self.selection.forEach(updateProperty);
// Return whether or not anything changed
return changed;
} }
return updateProperties(this.properties[index], value); structure.forEach(function (item) {
var domainObject = item.domainObject;
var id = objectUtils.makeKeyString(domainObject.identifier);
if (!self.domainObjectsById[id]) {
self.domainObjectsById[id] = {
domainObject: domainObject,
properties: []
};
observeObject(domainObject, id);
}
self.domainObjectsById[id].properties.push(item.property);
});
};
/**
* Delays updating the state.
*/
EditToolbar.prototype.scheduleStateUpdate = function () {
if (this.stateUpdateScheduled) {
return;
}
this.stateUpdateScheduled = true;
setTimeout(this.updateStateAfterMutation.bind(this));
};
EditToolbar.prototype.updateStateAfterMutation = function () {
this.stateTracker.forEach(function (state, index) {
if (!this.domainObjectsById[state.id].newObject) {
return;
}
var domainObject = this.domainObjectsById[state.id].domainObject;
var newObject = this.domainObjectsById[state.id].newObject;
var currentValue = _.get(domainObject, state.property);
var newValue = _.get(newObject, state.property);
state.domainObject = newObject;
if (currentValue !== newValue) {
this.updateState(index, newValue);
}
}, this);
Object.values(this.domainObjectsById).forEach(function (tracker) {
if (tracker.newObject) {
tracker.domainObject = tracker.newObject;
}
delete tracker.newObject;
});
this.stateUpdateScheduled = false;
};
/**
* Removes the listeners.
*/
EditToolbar.prototype.deregisterListeners = function () {
this.unobserveObjects.forEach(function (unobserveObject) {
unobserveObject();
});
this.unobserveObjects = [];
};
EditToolbar.prototype.handleStateChanges = function (state) {
(state || []).map(function (newValue, index) {
var domainObject = this.stateTracker[index].domainObject;
var property = this.stateTracker[index].property;
var currentValue = _.get(domainObject, property);
if (currentValue !== newValue) {
this.updateDomainObject(domainObject, property, newValue);
}
}, this);
};
EditToolbar.prototype.destroy = function () {
this.deregisterListeners();
}; };
return EditToolbar; return EditToolbar;
} }
); );

View File

@ -1,154 +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(
['./EditToolbar', './EditToolbarSelection'],
function (EditToolbar, EditToolbarSelection) {
// No operation
var NOOP_REPRESENTER = {
represent: function () {},
destroy: function () {}
};
/**
* The EditToolbarRepresenter populates the toolbar in Edit mode
* based on a view's definition.
* @param {Scope} scope the Angular scope of the representation
* @memberof platform/commonUI/edit
* @constructor
* @implements {Representer}
*/
function EditToolbarRepresenter(openmct, scope, element, attrs) {
var self = this;
// Mark changes as ready to persist
function commit(message) {
if (scope.commit) {
scope.commit(message);
}
}
// Handle changes to the current selection
function updateSelection(selection) {
// Only update if there is a toolbar to update
if (self.toolbar) {
// Make sure selection is array-like
selection = Array.isArray(selection) ?
selection :
(selection ? [selection] : []);
// Update the toolbar's selection
self.toolbar.setSelection(selection);
// ...and expose its structure/state
self.toolbarObject.structure =
self.toolbar.getStructure();
self.toolbarObject.state =
self.toolbar.getState();
}
}
// Get state (to watch it)
function getState() {
return self.toolbarObject.state;
}
// Update selection models to match changed toolbar state
function updateState(state) {
// Update underlying state based on toolbar changes
var changed = (state || []).map(function (value, index) {
return self.toolbar.updateState(index, value);
}).reduce(function (a, b) {
return a || b;
}, false);
// Only commit if something actually changed
if (changed) {
// Commit the changes.
commit("Changes from toolbar.");
}
}
this.clearExposedToolbar = function () {
// Clear exposed toolbar state (if any)
if (attrs.toolbar) {
delete scope.$parent[attrs.toolbar];
}
};
this.exposeToolbar = function () {
scope.$parent[self.attrs.toolbar] = self.toolbarObject;
};
this.commit = commit;
this.attrs = attrs;
this.updateSelection = updateSelection;
this.toolbar = undefined;
this.toolbarObject = {};
this.openmct = openmct;
this.scope = scope;
// If this representation exposes a toolbar, set up watches
// to synchronize with it.
if (attrs && attrs.toolbar) {
// Detect and handle changes to state from the toolbar
scope.$watchCollection(getState, updateState);
// Watch for changes in the current selection state
scope.$watchCollection("selection.all()", updateSelection);
// Expose toolbar state under that name
scope.$parent[attrs.toolbar] = this.toolbarObject;
} else {
// No toolbar declared, so do nothing.
return NOOP_REPRESENTER;
}
}
// Represent a domain object using this definition
EditToolbarRepresenter.prototype.represent = function (representation) {
// Get the newest toolbar definition from the view
var definition = (representation || {}).toolbar || {};
// If we have been asked to expose toolbar state...
if (this.attrs.toolbar) {
// Initialize toolbar object
this.toolbar = new EditToolbar(definition, this.commit);
// Ensure toolbar state is exposed
this.exposeToolbar();
}
// Add toolbar selection to scope.
this.scope.selection = new EditToolbarSelection(
this.scope,
this.openmct
);
// Initialize toolbar to current selection
this.updateSelection(this.scope.selection.all());
};
// Destroy; remove toolbar object from parent scope
EditToolbarRepresenter.prototype.destroy = function () {
this.clearExposedToolbar();
};
return EditToolbarRepresenter;
}
);

View File

@ -1,157 +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(
[],
function () {
/**
* Tracks selection state for editable views. Selection is
* implemented such that (from the toolbar's perspective)
* up to two objects can be "selected" at any given time:
*
* * The view proxy (see the `proxy` method), which provides
* an interface for interacting with the view itself (e.g.
* for buttons like "Add")
* * The selection, for single selected elements within the
* view.
*
* @memberof platform/commonUI/edit
* @constructor
*/
function EditToolbarSelection($scope, openmct) {
this.selection = [{}];
this.selecting = false;
this.selectedObj = undefined;
this.openmct = openmct;
var self = this;
function setSelection(selection) {
var selected = selection[0];
if (selected && selected.context.toolbar) {
self.select(selected.context.toolbar);
} else {
self.deselect();
}
if (selected && selected.context.viewProxy) {
self.proxy(selected.context.viewProxy);
}
setTimeout(function () {
$scope.$apply();
});
}
$scope.$on("$destroy", function () {
self.openmct.selection.off('change', setSelection);
});
this.openmct.selection.on('change', setSelection);
setSelection(this.openmct.selection.get());
}
/**
* Check if an object is currently selected.
* @param {*} obj the object to check for selection
* @returns {boolean} true if selected, otherwise false
*/
EditToolbarSelection.prototype.selected = function (obj) {
return (obj === this.selectedObj) || (obj === this.selection[0]);
};
/**
* Select an object.
* @param obj the object to select
* @returns {boolean} true if selection changed
*/
EditToolbarSelection.prototype.select = function (obj) {
// Proxy is always selected
if (obj === this.selection[0]) {
return false;
}
// Clear any existing selection
this.deselect();
// Note the current selection state
this.selectedObj = obj;
this.selecting = true;
// Add the selection
this.selection.push(obj);
};
/**
* Clear the current selection.
* @returns {boolean} true if selection changed
*/
EditToolbarSelection.prototype.deselect = function () {
// Nothing to do if we don't have a selected object
if (this.selecting) {
// Clear state tracking
this.selecting = false;
this.selectedObj = undefined;
// Remove the selection
this.selection.pop();
return true;
}
return false;
};
/**
* Get the currently-selected object.
* @returns the currently selected object
*/
EditToolbarSelection.prototype.get = function () {
return this.selectedObj;
};
/**
* Get/set the view proxy (for toolbar actions taken upon
* the view itself.)
* @param [proxy] the view proxy (if setting)
* @returns the current view proxy
*/
EditToolbarSelection.prototype.proxy = function (p) {
if (arguments.length > 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;
}
);

View File

@ -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();
});
});
}
);

View File

@ -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)
);
});
});
}
);

View File

@ -25,7 +25,10 @@ define(
function (EditToolbar) { function (EditToolbar) {
describe("An Edit mode toolbar", function () { describe("An Edit mode toolbar", function () {
var mockCommit, var mockOpenMCT,
mockScope,
mockObjects,
mockDomainObject,
testStructure, testStructure,
testAB, testAB,
testABC, testABC,
@ -35,35 +38,30 @@ define(
testM, testM,
toolbar; toolbar;
function getVisibility(obj) {
return !obj.hidden;
}
beforeEach(function () { beforeEach(function () {
mockCommit = jasmine.createSpy('commit'); mockOpenMCT = jasmine.createSpy('openmct', ['objects']);
testStructure = { mockObjects = jasmine.createSpyObj('objects', ['observe']);
sections: [ mockObjects.observe.andReturn();
{ mockOpenMCT.objects = mockObjects;
items: [ mockScope = jasmine.createSpyObj("$scope", [
{ name: "A", property: "a", exclusive: true }, "$watchCollection",
{ name: "B", property: "b", exclusive: true }, "$on"
{ name: "C", property: "c", exclusive: true } ]);
] mockScope.$watchCollection.andReturn();
}, mockDomainObject = jasmine.createSpyObj("domainObject", [
{ 'identifier'
items: [ ]);
{ name: "X", property: "x" },
{ name: "Y", property: "y", exclusive: true }, testStructure = [
{ name: "Z", property: "z", exclusive: true } { name: "A", property: "a", domainObject: mockDomainObject },
] { name: "B", property: "b", domainObject: mockDomainObject },
}, { name: "C", property: "c", domainObject: mockDomainObject },
{ { name: "X", property: "x", domainObject: mockDomainObject },
items: [ { name: "Y", property: "y", domainObject: mockDomainObject },
{ name: "M", method: "m", exclusive: true } { name: "Z", property: "z", domainObject: mockDomainObject },
] { name: "M", method: "m", domainObject: mockDomainObject }
} ];
]
};
testAB = { a: 0, b: 1 }; testAB = { a: 0, b: 1 };
testABC = { a: 0, b: 1, c: 2 }; testABC = { a: 0, b: 1, c: 2 };
testABC2 = { a: 4, b: 1, c: 2 }; // For inconsistent-state checking 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!' }; testABCYZ = { a: 0, b: 1, c: 2, y: 'Y!', z: 'Z!' };
testM = { m: jasmine.createSpy("method") }; testM = { m: jasmine.createSpy("method") };
toolbar = new EditToolbar(testStructure, mockCommit); toolbar = new EditToolbar(mockScope, mockOpenMCT, testStructure);
});
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]);
}); });
it("adds click functions when a method is specified", function () { it("adds click functions when a method is specified", function () {
toolbar.setSelection([testM]); var structure = toolbar.getStructure();
// Verify precondition expect(structure[6].click).toBeDefined();
expect(testM.m).not.toHaveBeenCalled(); });
// Click!
toolbar.getStructure().sections[2].items[0].click(); it("adds key for controls that define a property", function () {
// Should have called the underlying function var structure = toolbar.getStructure();
expect(testM.m).toHaveBeenCalled(); expect(structure[0].key).toEqual(0);
// Should also have committed the change
expect(mockCommit).toHaveBeenCalled();
}); });
}); });
} }

View File

@ -28,6 +28,16 @@ define(
* The mct-selectable directive allows selection functionality * The mct-selectable directive allows selection functionality
* (click) to be attached to specific elements. * (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 * @memberof platform/commonUI/general
* @constructor * @constructor
*/ */

View File

@ -39,240 +39,309 @@ define([
"cssClass": "icon-box-with-dashed-lines", "cssClass": "icon-box-with-dashed-lines",
"type": "telemetry.fixed", "type": "telemetry.fixed",
"template": fixedTemplate, "template": fixedTemplate,
"uses": [ "uses": [],
"composition" "editable": true
], }
"editable": true, ],
"toolbar": { "toolbars": [
"sections": [ {
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", "name": "Box",
"cssClass": "icon-plus", "cssClass": "icon-box",
"control": "menu-button", "key": "fixed.box"
"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"
}
]
}, },
{ {
"property": "fill", "name": "Line",
"cssClass": "icon-paint-bucket",
"title": "Fill color",
"description": "Set fill color",
"control": "color"
},
{
"property": "stroke",
"cssClass": "icon-line-horz", "cssClass": "icon-line-horz",
"title": "Border color", "key": "fixed.line"
"description": "Set border color",
"control": "color"
}, },
{ {
"property": "url", "name": "Text",
"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",
"cssClass": "icon-T", "cssClass": "icon-T",
"title": "Text color", "key": "fixed.text"
"description": "Set text color",
"mandatory": true,
"control": "color"
}, },
{ {
"property": "size", "name": "Image",
"title": "Text size", "cssClass": "icon-image",
"description": "Set text size", "key": "fixed.image"
"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" };
})
} }
] ]
}, },
{ {
"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", "name": "Move to Top",
"text": "X", "cssClass": "icon-arrow-double-up",
"name": "X", "key": "top"
"cssClass": "l-input-sm",
"control": "numberfield",
"min": "0"
}, },
{ {
"property": "editY", "name": "Move Up",
"text": "Y", "cssClass": "icon-arrow-up",
"name": "Y", "key": "up"
"cssClass": "l-input-sm",
"control": "numberfield",
"min": "0"
}, },
{ {
"property": "editX1", "name": "Move Down",
"text": "X1", "cssClass": "icon-arrow-down",
"name": "X1", "key": "down"
"cssClass": "l-input-sm",
"control" : "numberfield",
"min": "0"
}, },
{ {
"property": "editY1", "name": "Move to Bottom",
"text": "Y1", "cssClass": "icon-arrow-double-down",
"name": "Y1", "key": "bottom"
"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"
} }
] ]
}, },
{ {
"items": [ control: "color",
{ domainObject: domainObject,
"property": "text", property: path + ".fill",
"cssClass": "icon-gear", cssClass: "icon-paint-bucket",
"control": "dialog-button", title: "Fill color",
"title": "Text Properties", description: "Set fill color",
"description": "Edit text properties", key: 'fill'
"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"
}
]
}, },
{ {
"items": [ control: "color",
{ domainObject: domainObject,
"method": "remove", property: path + ".stroke",
"control": "button", cssClass: "icon-line-horz",
"cssClass": "icon-trash" 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;
});
} }
} }
], ],

View File

@ -62,29 +62,7 @@ define([
"type": "layout", "type": "layout",
"template": layoutTemplate, "template": layoutTemplate,
"editable": true, "editable": true,
"uses": [], "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"
}
]
}
]
}
}, },
{ {
"key": "fixed", "key": "fixed",
@ -305,6 +283,27 @@ define([
"implementation": LayoutCompositionPolicy "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": [ "types": [
{ {
"key": "layout", "key": "layout",
@ -314,7 +313,14 @@ define([
"priority": 900, "priority": 900,
"features": "creation", "features": "creation",
"model": { "model": {
"composition": [] "composition": [],
configuration: {
layout: {
panels: {
}
}
}
}, },
"properties": [ "properties": [
{ {

View File

@ -41,7 +41,7 @@
<mct-include key="element.template" <mct-include key="element.template"
parameters="{ gridSize: controller.getGridSize() }" parameters="{ gridSize: controller.getGridSize() }"
ng-model="element"> ng-model="element">
</mct-include> </mct-include>
</div> </div>
<!-- Selection highlight, handles --> <!-- Selection highlight, handles -->
<span class="s-selected s-moveable" ng-if="controller.isElementSelected()"> <span class="s-selected s-moveable" ng-if="controller.isElementSelected()">

View File

@ -41,7 +41,7 @@
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(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-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)" 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-dblclick="controller.drill($event, childObject)"
ng-style="controller.getFrameStyle(childObject.getId())"> ng-style="controller.getFrameStyle(childObject.getId())">

View File

@ -38,6 +38,24 @@ define(
var DEFAULT_DIMENSIONS = [2, 1]; 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 * The FixedController is responsible for supporting the
* Fixed Position view. It arranges frames according to saved * Fixed Position view. It arranges frames according to saved
@ -51,14 +69,14 @@ define(
this.names = {}; // Cache names by ID this.names = {}; // Cache names by ID
this.values = {}; // Cache values by ID this.values = {}; // Cache values by ID
this.elementProxiesById = {}; this.elementProxiesById = {};
this.telemetryObjects = {};
this.telemetryObjects = []; this.subscriptions = {};
this.subscriptions = [];
this.openmct = openmct; this.openmct = openmct;
this.$element = $element; this.$element = $element;
this.$scope = $scope; this.$scope = $scope;
this.dialogService = dialogService;
this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid; this.$q = $q;
this.newDomainObject = $scope.domainObject.useCapability('adapter');
this.fixedViewSelectable = false; this.fixedViewSelectable = false;
var self = this; var self = this;
@ -67,59 +85,13 @@ define(
'fetchHistoricalData', 'fetchHistoricalData',
'getTelemetry', 'getTelemetry',
'setDisplayedValue', 'setDisplayedValue',
'subscribeToObjects', 'subscribeToObject',
'unsubscribe', 'unsubscribe',
'updateView' 'updateView'
].forEach(function (name) { ].forEach(function (name) {
self[name] = self[name].bind(self); 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 // Decorate an element for display
function makeProxyElement(element, index, elements) { function makeProxyElement(element, index, elements) {
var ElementProxy = ElementProxies[element.type], var ElementProxy = ElementProxies[element.type],
@ -137,14 +109,14 @@ define(
// Decorate elements in the current configuration // Decorate elements in the current configuration
function refreshElements() { function refreshElements() {
var elements = (($scope.configuration || {}).elements || []); var elements = (((self.newDomainObject.configuration || {})['fixed-display'] || {}).elements || []);
// Create the new proxies... // Create the new proxies...
self.elementProxies = elements.map(makeProxyElement); 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 (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); var index = elements.indexOf(self.selectedElementProxy.element);
if (index === -1) { if (index === -1) {
self.$element[0].click(); 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 // Trigger a new query for telemetry data
function updateDisplayBounds(bounds, isTick) { function updateDisplayBounds(bounds, isTick) {
if (!isTick) { if (!isTick) {
//Reset values //Reset values
self.values = {}; self.values = {};
refreshElements(); refreshElements();
//Fetch new data //Fetch new data
self.fetchHistoricalData(self.telemetryObjects); Object.values(self.telemetryObjects).forEach(function (object) {
self.fetchHistoricalData(object);
});
} }
} }
// Add an element to this view // Add an element to this view
function addElement(element) { function addElement(element) {
// Ensure that configuration field is populated var index;
$scope.configuration = $scope.configuration || {}; var elements = (((self.newDomainObject.configuration || {})['fixed-display'] || {}).elements || []);
// Make sure there is a "elements" field in the elements.push(element);
// view configuration.
$scope.configuration.elements =
$scope.configuration.elements || [];
// Store the position of this element.
$scope.configuration.elements.push(element);
self.elementToSelectAfterRefresh = element; if (self.selectedElementProxy) {
index = elements.indexOf(self.selectedElementProxy.element);
// Refresh displayed elements
refreshElements();
// Mark change as persistable
if ($scope.commit) {
$scope.commit("Dropped an 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 // Position a panel after a drop event
function handleDrop(e, id, position) { function handleDrop(e, id, position) {
// Don't handle this event if it has already been handled // 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) { if (e.defaultPrevented) {
return; return;
} }
e.preventDefault(); e.preventDefault();
// Store the position of this element. // Store the position of this element.
// color is set to "" to let the CSS theme determine the default color
addElement({ addElement({
type: "fixed.telemetry", type: "fixed.telemetry",
x: Math.floor(position.x / self.gridSize[0]), x: Math.floor(position.x / self.gridSize[0]),
@ -254,71 +200,229 @@ define(
useGrid: true useGrid: true
}); });
//Re-initialize objects, and subscribe to new object // Subscribe to the new object to get telemetry
self.getTelemetry($scope.domainObject); self.openmct.objects.get(id).then(function (object) {
} self.getTelemetry(object);
});
// 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;
}
} }
this.elementProxies = []; this.elementProxies = [];
this.generateDragHandle = generateDragHandle; this.addElement = addElement;
this.generateDragHandles = generateDragHandles; this.refreshElements = refreshElements;
this.updateSelectionStyle = updateSelectionStyle; this.fixedProxy = new FixedProxy(this.addElement, this.$q, this.dialogService);
// Detect changes to grid size this.composition = this.openmct.composition.get(this.newDomainObject);
$scope.$watch("model.layoutGrid", updateElementPositions); this.composition.on('add', this.onCompositionAdd, this);
this.composition.on('remove', this.onCompositionRemove, this);
this.composition.load();
// Position panes where they are dropped // Position panes where they are dropped
$scope.$on("mctDrop", handleDrop); $scope.$on("mctDrop", handleDrop);
// Position panes when the model field changes $scope.$on("$destroy", this.destroy.bind(this));
$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);
});
// Respond to external bounds changes // Respond to external bounds changes
this.openmct.time.on("bounds", updateDisplayBounds); 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 * A rate-limited digest function. Caps digests at 60Hz
* @private * @private
@ -340,31 +444,30 @@ define(
* @private * @private
*/ */
FixedController.prototype.unsubscribe = function () { FixedController.prototype.unsubscribe = function () {
this.subscriptions.forEach(function (unsubscribeFunc) { Object.values(this.subscriptions).forEach(function (unsubscribeFunc) {
unsubscribeFunc(); unsubscribeFunc();
}); });
this.subscriptions = []; this.subscriptions = {};
this.telemetryObjects = []; this.telemetryObjects = {};
}; };
/** /**
* Subscribe to all given domain objects * Subscribe to the given domain object
* @private * @private
* @param {object[]} objects Domain objects to subscribe to * @param {object} object Domain object to subscribe to
* @returns {object[]} The provided objects, for chaining. * @returns {object} The provided object, for chaining.
*/ */
FixedController.prototype.subscribeToObjects = function (objects) { FixedController.prototype.subscribeToObject = function (object) {
var self = this; var self = this;
var timeAPI = this.openmct.time; 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 object;
return self.openmct.telemetry.subscribe(object, function (datum) {
if (timeAPI.clock() !== undefined) {
self.updateView(object, datum);
}
}, {});
});
return objects;
}; };
/** /**
@ -416,23 +519,22 @@ define(
}; };
/** /**
* Request the last historical data point for the given domain objects * Request the last historical data point for the given domain object
* @param {object[]} objects * @param {object} object
* @returns {object[]} the provided objects for chaining. * @returns {object} the provided object for chaining.
*/ */
FixedController.prototype.fetchHistoricalData = function (objects) { FixedController.prototype.fetchHistoricalData = function (object) {
var bounds = this.openmct.time.bounds(); var bounds = this.openmct.time.bounds();
var self = this; var self = this;
objects.forEach(function (object) { self.openmct.telemetry.request(object, {start: bounds.start, end: bounds.end, size: 1})
self.openmct.telemetry.request(object, {start: bounds.start, end: bounds.end, size: 1}) .then(function (data) {
.then(function (data) { if (data.length > 0) {
if (data.length > 0) { self.updateView(object, data[data.length - 1]);
self.updateView(object, data[data.length - 1]); }
} });
});
}); return object;
return objects;
}; };
@ -457,33 +559,25 @@ define(
}; };
FixedController.prototype.getTelemetry = function (domainObject) { FixedController.prototype.getTelemetry = function (domainObject) {
var newObject = domainObject.useCapability('adapter'); var id = objectUtils.makeKeyString(domainObject.identifier);
var self = this;
if (this.subscriptions.length > 0) { if (this.subscriptions[id]) {
this.unsubscribe(); this.subscriptions[id]();
delete this.subscriptions[id];
}
delete this.telemetryObjects[id];
if (!this.openmct.telemetry.isTelemetryObject(domainObject)) {
return;
} }
function filterForTelemetryObjects(objects) { // Initialize display
return objects.filter(function (object) { this.telemetryObjects[id] = domainObject;
return self.openmct.telemetry.isTelemetryObject(object); this.setDisplayedValue(domainObject, "");
});
}
function initializeDisplay(objects) { return Promise.resolve(domainObject)
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)
.then(this.fetchHistoricalData) .then(this.fetchHistoricalData)
.then(this.subscribeToObjects); .then(this.subscribeToObject);
}; };
/** /**
@ -580,12 +674,12 @@ define(
* Gets the selection context. * Gets the selection context.
* *
* @param elementProxy the element proxy * @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) { FixedController.prototype.getContext = function (elementProxy) {
return { return {
elementProxy: elementProxy, 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; return FixedController;
} }
); );

View File

@ -24,30 +24,34 @@ define(
[], [],
function () { function () {
// Drag handle dimensions // Drag handle dimensions
var DRAG_HANDLE_SIZE = [6, 6]; var DRAG_HANDLE_SIZE = [6, 6];
/** /**
* Template-displayable drag handle for an element in fixed * Template-displayable drag handle for an element in fixed
* position mode. * 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 * @memberof platform/features/layout
* @constructor * @constructor
*/ */
function FixedDragHandle(elementHandle, gridSize, update, commit) { function FixedDragHandle(elementHandle, configPath, fixedControl) {
this.elementHandle = elementHandle; this.elementHandle = elementHandle;
this.gridSize = gridSize; this.configPath = configPath;
this.update = update; this.fixedControl = fixedControl;
this.commit = commit;
} }
/** /**
* Get a CSS style to position this drag handle. * Get a CSS style to position this drag handle.
*
* @returns CSS style object (for `ng-style`) * @returns CSS style object (for `ng-style`)
* @memberof platform/features/layout.FixedDragHandle# * @memberof platform/features/layout.FixedDragHandle#
*/ */
FixedDragHandle.prototype.style = function () { FixedDragHandle.prototype.style = function () {
var gridSize = this.elementHandle.getGridSize(); var gridSize = this.elementHandle.getGridSize();
// Adjust from grid to pixel coordinates // Adjust from grid to pixel coordinates
var x = this.elementHandle.x() * gridSize[0], var x = this.elementHandle.x() * gridSize[0],
y = this.elementHandle.y() * gridSize[1]; y = this.elementHandle.y() * gridSize[1];
@ -75,23 +79,20 @@ define(
/** /**
* Continue a drag gesture; update x/y positions. * 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) { FixedDragHandle.prototype.continueDrag = function (delta) {
var gridSize = this.elementHandle.getGridSize(); var gridSize = this.elementHandle.getGridSize();
if (this.dragging) { if (this.dragging) {
// Update x/y positions (snapping to grid) // Update x/y positions (snapping to grid)
this.elementHandle.x( var newX = this.dragging.x + Math.round(delta[0] / gridSize[0]);
this.dragging.x + Math.round(delta[0] / gridSize[0]) var newY = this.dragging.y + Math.round(delta[1] / gridSize[1]);
);
this.elementHandle.y( this.elementHandle.x(Math.max(0, newX));
this.dragging.y + Math.round(delta[1] / gridSize[1]) this.elementHandle.y(Math.max(0, newY));
); this.fixedControl.updateSelectionStyle();
// Invoke update callback
if (this.update) {
this.update();
}
} }
}; };
@ -100,12 +101,8 @@ define(
* concludes to trigger commit of changes. * concludes to trigger commit of changes.
*/ */
FixedDragHandle.prototype.endDrag = function () { FixedDragHandle.prototype.endDrag = function () {
// Clear cached state
this.dragging = undefined; this.dragging = undefined;
// Mark change as complete this.fixedControl.mutate(this.configPath, this.elementHandle.element);
if (this.commit) {
this.commit("Dragged handle.");
}
}; };
return FixedDragHandle; return FixedDragHandle;

View File

@ -78,29 +78,30 @@ define(
} }
$scope.configuration = $scope.configuration || {}; $scope.configuration = $scope.configuration || {};
$scope.configuration.panels = $scope.configuration.panels = $scope.configuration.panels || {};
$scope.configuration.panels || {};
$scope.configuration.panels[id] = { self.openmct.objects.get(id).then(function (object) {
position: [ $scope.configuration.panels[id] = {
Math.floor(position.x / self.gridSize[0]), position: [
Math.floor(position.y / self.gridSize[1]) Math.floor(position.x / self.gridSize[0]),
], Math.floor(position.y / self.gridSize[1])
dimensions: self.defaultDimensions() ],
}; dimensions: self.defaultDimensions(),
hasFrame: self.getDefaultFrame(object.type)
};
// Store the id so that the newly-dropped object // Store the id so that the newly-dropped object
// gets selected during refresh composition // gets selected during refresh composition
self.droppedIdToSelectAfterRefresh = id; 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 // Layout may contain embedded views which will
// listen for drops, so call preventDefault() so // listen for drops, so call preventDefault() so
// that they can recognize that this event is handled. // that they can recognize that this event is handled.
@ -157,10 +158,7 @@ define(
$scope.configuration.panels[self.activeDragId].dimensions = $scope.configuration.panels[self.activeDragId].dimensions =
self.rawPositions[self.activeDragId].dimensions; self.rawPositions[self.activeDragId].dimensions;
// Mark this object as dirty to encourage persistence self.commit();
if ($scope.commit) {
$scope.commit("Moved frame.");
}
}; };
// Sets the selectable object in response to the selection change event. // Sets the selectable object in response to the selection change event.
@ -194,9 +192,22 @@ define(
$scope.$on("$destroy", function () { $scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection); openmct.selection.off("change", setSelection);
self.unlisten();
}); });
$scope.$on("mctDrop", handleDrop); $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, // Utility function to copy raw positions from configuration,
@ -220,7 +231,6 @@ define(
*/ */
LayoutController.prototype.setFrames = function (ids) { LayoutController.prototype.setFrames = function (ids) {
var panels = shallowCopy(this.$scope.configuration.panels || {}, ids); var panels = shallowCopy(this.$scope.configuration.panels || {}, ids);
this.frames = {}; this.frames = {};
this.$scope.composition.forEach(function (object) { this.$scope.composition.forEach(function (object) {
@ -230,11 +240,22 @@ define(
if (panels[id].hasOwnProperty('hasFrame')) { if (panels[id].hasOwnProperty('hasFrame')) {
this.frames[id] = panels[id].hasFrame; this.frames[id] = panels[id].hasFrame;
} else { } else {
this.frames[id] = DEFAULT_HIDDEN_FRAME_TYPES.indexOf(object.getModel().type) === -1; this.frames[id] = this.getDefaultFrame(object.getModel().type);
} }
}, this); }, 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 // Convert from { positions: ..., dimensions: ... } to an
// appropriate ng-style argument, to position frames. // appropriate ng-style argument, to position frames.
LayoutController.prototype.convertPosition = function (raw) { LayoutController.prototype.convertPosition = function (raw) {
@ -389,40 +410,6 @@ define(
return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false; 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. * Bypasses selection if drag is in progress.
* *
@ -497,17 +484,25 @@ define(
* Gets the selection context. * Gets the selection context.
* *
* @param domainObject the domain object * @param domainObject the domain object
* @returns {object} the context object which includes * @returns {object} the context object which includes item and oldItem
* item, oldItem and toolbar
*/ */
LayoutController.prototype.getContext = function (domainObject, toolbar) { LayoutController.prototype.getContext = function (domainObject) {
return { return {
item: domainObject.useCapability('adapter'), item: domainObject.useCapability('adapter'),
oldItem: domainObject, oldItem: domainObject
toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined
}; };
}; };
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. * Selects a newly-dropped object.
* *

View File

@ -53,12 +53,6 @@ define(
*/ */
proxy.fill = new AccessorMutator(element, 'fill'); 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; return proxy;
} }

View File

@ -71,13 +71,6 @@ define(
*/ */
this.gridSize = gridSize || [1,1]; //Ensure a reasonable default 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. * Get and/or set the x position of this element.
* Units are in fixed position grid space. * Units are in fixed position grid space.
@ -123,15 +116,16 @@ define(
this.height = new AccessorMutator(element, 'height'); this.height = new AccessorMutator(element, 'height');
this.useGrid = new UnitAccessorMutator(this); this.useGrid = new UnitAccessorMutator(this);
this.index = index; this.index = index;
this.elements = elements; this.elements = elements;
this.resizeHandles = [new ResizeHandle(this, this.element)];
} }
/** /**
* Change the display order of this element. * Change the display order of this element.
* @param {string} o where to move this element; * @param {string} o where to move this element;
* one of "top", "up", "down", or "bottom" * one of "top", "up", "down", or "bottom"
* @return {Array} the full array of elements
*/ */
ElementProxy.prototype.order = function (o) { ElementProxy.prototype.order = function (o) {
var index = this.index, var index = this.index,
@ -152,16 +146,8 @@ define(
// anyway, but be consistent) // anyway, but be consistent)
this.index = desired; this.index = desired;
} }
};
/** return elements;
* 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);
}
}; };
/** /**
@ -208,7 +194,6 @@ define(
*/ */
ElementProxy.prototype.getMinWidth = function () { ElementProxy.prototype.getMinWidth = function () {
return Math.ceil(MIN_WIDTH / this.getGridSize()[0]); return Math.ceil(MIN_WIDTH / this.getGridSize()[0]);
}; };
/** /**

View File

@ -50,12 +50,6 @@ define(
*/ */
proxy.url = new AccessorMutator(element, 'url'); 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; return proxy;
} }

View File

@ -32,19 +32,18 @@ define(
* @constructor * @constructor
* @param element the line element * @param element the line element
* @param {string} xProperty field which stores x position * @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} xOther field which stores x of other end
* @param {string} yOther field which stores y 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} * @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.element = element;
this.xProperty = xProperty; this.xProperty = xProperty;
this.yProperty = yProperty; this.yProperty = yProperty;
this.xOther = xOther; this.xOther = xOther;
this.yOther = yOther; this.yOther = yOther;
this.gridSize = gridSize;
} }
LineHandle.prototype.x = function (value) { LineHandle.prototype.x = function (value) {
@ -86,7 +85,7 @@ define(
}; };
LineHandle.prototype.getGridSize = function () { LineHandle.prototype.getGridSize = function () {
return this.gridSize; return this.elementProxy.getGridSize();
}; };
return LineHandle; return LineHandle;

View File

@ -39,10 +39,24 @@ define(
function LineProxy(element, index, elements, gridSize) { function LineProxy(element, index, elements, gridSize) {
var proxy = new ElementProxy(element, index, elements, gridSize), var proxy = new ElementProxy(element, index, elements, gridSize),
handles = [ handles = [
new LineHandle(element, 'x', 'y', 'x2', 'y2', proxy.getGridSize()), new LineHandle(element, proxy, 'x', 'y', 'x2', 'y2'),
new LineHandle(element, 'x2', 'y2', 'x', 'y', proxy.getGridSize()) 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 * Get the top-left x coordinate, in grid space, of
* this line's bounding box. * this line's bounding box.
@ -149,12 +163,6 @@ define(
return handles; 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; return proxy;
} }

View File

@ -35,21 +35,16 @@ define(
* @memberof platform/features/layout * @memberof platform/features/layout
* @constructor * @constructor
*/ */
function ResizeHandle(element, minWidth, minHeight, gridSize) { function ResizeHandle(elementProxy, element) {
this.elementProxy = elementProxy;
this.element = element; this.element = element;
// Ensure reasonable defaults
this.minWidth = minWidth || 0;
this.minHeight = minHeight || 0;
this.gridSize = gridSize;
} }
ResizeHandle.prototype.x = function (value) { ResizeHandle.prototype.x = function (value) {
var element = this.element; var element = this.element;
if (arguments.length > 0) { if (arguments.length > 0) {
element.width = Math.max( element.width = Math.max(
this.minWidth, this.elementProxy.getMinWidth(),
value - element.x value - element.x
); );
} }
@ -60,7 +55,7 @@ define(
var element = this.element; var element = this.element;
if (arguments.length > 0) { if (arguments.length > 0) {
element.height = Math.max( element.height = Math.max(
this.minHeight, this.elementProxy.getMinHeight(),
value - element.y value - element.y
); );
} }
@ -68,7 +63,7 @@ define(
}; };
ResizeHandle.prototype.getGridSize = function () { ResizeHandle.prototype.getGridSize = function () {
return this.gridSize; return this.elementProxy.getGridSize();
}; };
return ResizeHandle; return ResizeHandle;

View File

@ -24,9 +24,6 @@ define(
['./TextProxy'], ['./TextProxy'],
function (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. * Selection proxy for telemetry elements in a fixed position view.
* *
@ -45,24 +42,9 @@ define(
function TelemetryProxy(element, index, elements, gridSize) { function TelemetryProxy(element, index, elements, gridSize) {
var proxy = new TextProxy(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 // Expose the domain object identifier
proxy.id = element.id; proxy.id = element.id;
// Expose initial toggle
proxy[element.titled ? HIDE : SHOW] = toggle;
// Don't expose text configuration // Don't expose text configuration
delete proxy.text; delete proxy.text;

View File

@ -53,22 +53,14 @@ define(
mockTimeSystem, mockTimeSystem,
mockLimitEvaluator, mockLimitEvaluator,
mockSelection, mockSelection,
mockObjects,
mockNewDomainObject,
unlistenFunc,
$element = [], $element = [],
selectable = [], selectable = [],
controller; controller;
// Utility function; find a watch for a given expression // Utility function; find a $on calls 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
function findOn(expr) { function findOn(expr) {
var on; var on;
mockScope.$on.calls.forEach(function (call) { mockScope.$on.calls.forEach(function (call) {
@ -82,7 +74,8 @@ define(
function makeMockDomainObject(id) { function makeMockDomainObject(id) {
return { return {
identifier: { identifier: {
key: "domainObject-" + id key: "domainObject-" + id,
namespace: ""
}, },
name: "Point " + id name: "Point " + id
}; };
@ -110,11 +103,6 @@ define(
return "Formatted " + valueMetadata.value; return "Formatted " + valueMetadata.value;
}); });
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getId', 'getModel', 'getCapability', 'useCapability']
);
mockHandle = jasmine.createSpyObj( mockHandle = jasmine.createSpyObj(
'subscription', 'subscription',
[ [
@ -172,16 +160,14 @@ define(
]}; ]};
mockChildren = testModel.composition.map(makeMockDomainObject); mockChildren = testModel.composition.map(makeMockDomainObject);
mockCompositionCollection = jasmine.createSpyObj('compositionCollection', mockCompositionCollection = jasmine.createSpyObj('compositionCollection', [
[ 'load',
'load' 'on',
] 'off'
); ]);
mockCompositionAPI = jasmine.createSpyObj('composition', mockCompositionAPI = jasmine.createSpyObj('composition', [
[ 'get'
'get' ]);
]
);
mockCompositionAPI.get.andReturn(mockCompositionCollection); mockCompositionAPI.get.andReturn(mockCompositionCollection);
mockCompositionCollection.load.andReturn( mockCompositionCollection.load.andReturn(
Promise.resolve(mockChildren) Promise.resolve(mockChildren)
@ -190,6 +176,24 @@ define(
mockScope.model = testModel; mockScope.model = testModel;
mockScope.configuration = testConfiguration; 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] = { selectable[0] = {
context: { context: {
oldItem: mockDomainObject oldItem: mockDomainObject
@ -203,11 +207,19 @@ define(
]); ]);
mockSelection.get.andReturn([]); mockSelection.get.andReturn([]);
unlistenFunc = jasmine.createSpy("unlisten");
mockObjects = jasmine.createSpyObj('objects', [
'observe',
'get'
]);
mockObjects.observe.andReturn(unlistenFunc);
mockOpenMCT = { mockOpenMCT = {
time: mockConductor, time: mockConductor,
telemetry: mockTelemetryAPI, telemetry: mockTelemetryAPI,
composition: mockCompositionAPI, composition: mockCompositionAPI,
selection: mockSelection selection: mockSelection,
objects: mockObjects
}; };
$element = $('<div></div>'); $element = $('<div></div>');
@ -251,76 +263,60 @@ define(
mockOpenMCT, mockOpenMCT,
$element $element
); );
spyOn(controller, "mutate");
findWatch("model.layoutGrid")(testModel.layoutGrid);
}); });
it("subscribes when a domain object is available", function () { it("subscribes a domain object", function () {
var dunzo = false; var object = makeMockDomainObject("mock");
var done = false;
mockScope.domainObject = mockDomainObject; controller.getTelemetry(object).then(function () {
findWatch("domainObject")(mockDomainObject).then(function () { done = true;
dunzo = true;
}); });
waitsFor(function () { waitsFor(function () {
return dunzo; return done;
}, "Telemetry fetched", 200); });
runs(function () { runs(function () {
mockChildren.forEach(function (child) { expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith( object,
child, jasmine.any(Function),
jasmine.any(Function), jasmine.any(Object)
jasmine.any(Object) );
);
});
}); });
}); });
it("releases subscriptions when domain objects change", function () { it("releases subscription when a domain objects is removed", function () {
var dunzo = false; var done = false;
var unsubscribe = jasmine.createSpy('unsubscribe'); var unsubscribe = jasmine.createSpy('unsubscribe');
var object = makeMockDomainObject("mock");
mockTelemetryAPI.subscribe.andReturn(unsubscribe); mockTelemetryAPI.subscribe.andReturn(unsubscribe);
controller.getTelemetry(object).then(function () {
mockScope.domainObject = mockDomainObject; done = true;
findWatch("domainObject")(mockDomainObject).then(function () {
dunzo = true;
}); });
waitsFor(function () { waitsFor(function () {
return dunzo; return done;
}, "Telemetry fetched", 200); });
runs(function () { runs(function () {
expect(unsubscribe).not.toHaveBeenCalled(); controller.onCompositionRemove(object.identifier);
dunzo = false;
findWatch("domainObject")(mockDomainObject).then(function () {
dunzo = true;
});
waitsFor(function () { waitsFor(function () {
return dunzo; return unsubscribe.calls.length > 0;
}, "Telemetry fetched", 200);
runs(function () {
expect(unsubscribe.calls.length).toBe(mockChildren.length);
}); });
runs(function () {
expect(unsubscribe).toHaveBeenCalled();
});
}); });
}); });
it("exposes visible elements based on configuration", function () { 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.length).toEqual(3);
expect(elements[0].id).toEqual('a'); expect(elements[0].id).toEqual('a');
expect(elements[1].id).toEqual('b'); expect(elements[1].id).toEqual('b');
@ -328,9 +324,6 @@ define(
}); });
it("allows elements to be selected", function () { it("allows elements to be selected", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1]; selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@ -338,12 +331,7 @@ define(
}); });
it("allows selection retrieval", function () { it("allows selection retrieval", function () {
var elements; var elements = controller.getElements();
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
selectable[0].context.elementProxy = elements[1]; selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@ -351,16 +339,10 @@ define(
}); });
it("selects the parent view when selected element is removed", function () { it("selects the parent view when selected element is removed", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
var elements = controller.getElements(); var elements = controller.getElements();
selectable[0].context.elementProxy = elements[1]; selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.remove(elements[1]);
elements[1].remove();
testModel.modified = 2;
findWatch("model.modified")(testModel.modified);
expect($element[0].click).toHaveBeenCalled(); expect($element[0].click).toHaveBeenCalled();
}); });
@ -368,21 +350,13 @@ define(
it("retains selections during refresh", function () { it("retains selections during refresh", function () {
// Get elements; remove one of them; trigger refresh. // Get elements; remove one of them; trigger refresh.
// Same element (at least by index) should still be selected. // Same element (at least by index) should still be selected.
var elements; var elements = controller.getElements();
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
selectable[0].context.elementProxy = elements[1]; selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.getSelectedElement()).toEqual(elements[1]); expect(controller.getSelectedElement()).toEqual(elements[1]);
elements[2].remove(); controller.remove(elements[2]);
testModel.modified = 2;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements(); elements = controller.getElements();
// Verify removal, as test assumes this // Verify removal, as test assumes this
@ -408,7 +382,7 @@ define(
controller.elementProxiesById['12345'] = [testElement]; controller.elementProxiesById['12345'] = [testElement];
controller.elementProxies = [testElement]; controller.elementProxies = [testElement];
controller.subscribeToObjects([telemetryObject]); controller.subscribeToObject(telemetryObject);
mockTelemetryAPI.subscribe.mostRecentCall.args[1](mockTelemetry); mockTelemetryAPI.subscribe.mostRecentCall.args[1](mockTelemetry);
waitsFor(function () { waitsFor(function () {
@ -426,18 +400,13 @@ define(
}); });
it("updates elements styles when grid size changes", function () { 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; // Change the grid size
mockScope.model = testModel; controller.updateElementPositions([20, 20]);
findWatch("domainObject")(mockDomainObject);
findWatch("model.modified")(1); expect(controller.getElements()[0].style.left).not.toEqual(originalLeft);
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);
}); });
it("listens for drop events", function () { it("listens for drop events", function () {
@ -457,6 +426,9 @@ define(
// Notify that a drop occurred // Notify that a drop occurred
testModel.composition.push('d'); testModel.composition.push('d');
mockObjects.get.andReturn(Promise.resolve([]));
findOn('mctDrop')( findOn('mctDrop')(
mockEvent, mockEvent,
'd', 'd',
@ -468,11 +440,6 @@ define(
// ...and prevented default... // ...and prevented default...
expect(mockEvent.preventDefault).toHaveBeenCalled(); 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 () { it("ignores drops when default has been prevented", function () {
@ -492,52 +459,35 @@ define(
}); });
it("unsubscribes when destroyed", function () { it("unsubscribes when destroyed", function () {
var done = false;
var dunzo = false;
var unsubscribe = jasmine.createSpy('unsubscribe'); var unsubscribe = jasmine.createSpy('unsubscribe');
var object = makeMockDomainObject("mock");
mockTelemetryAPI.subscribe.andReturn(unsubscribe); mockTelemetryAPI.subscribe.andReturn(unsubscribe);
mockScope.domainObject = mockDomainObject; controller.getTelemetry(object).then(function () {
findWatch("domainObject")(mockDomainObject).then(function () { done = true;
dunzo = true;
}); });
waitsFor(function () { waitsFor(function () {
return dunzo; return done;
}, "Telemetry fetched", 200); });
runs(function () { runs(function () {
expect(unsubscribe).not.toHaveBeenCalled(); expect(unsubscribe).not.toHaveBeenCalled();
// Destroy the scope // Destroy the scope
findOn('$destroy')(); findOn('$destroy')();
//Check that the same unsubscribe function returned by the expect(unsubscribe).toHaveBeenCalled();
expect(unsubscribe.calls.length).toBe(mockChildren.length);
}); });
}); });
it("exposes its grid size", function () { 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); 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 () { it("exposes drag handles", function () {
var handles; var handles;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1]; selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@ -556,9 +506,6 @@ define(
}); });
it("exposes a move handle", function () { it("exposes a move handle", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1]; selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@ -573,10 +520,6 @@ define(
it("updates selection style during drag", function () { it("updates selection style during drag", function () {
var oldStyle; var oldStyle;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1]; selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@ -677,7 +620,7 @@ define(
value: testValue value: testValue
}])); }]));
controller.fetchHistoricalData([mockTelemetryObject]); controller.fetchHistoricalData(mockTelemetryObject);
waitsFor(function () { waitsFor(function () {
return controller.digesting === false; return controller.digesting === false;

View File

@ -28,8 +28,8 @@ define(
describe("A fixed position drag handle", function () { describe("A fixed position drag handle", function () {
var mockElementHandle, var mockElementHandle,
mockUpdate, mockConfigPath,
mockCommit, mockFixedControl,
handle; handle;
beforeEach(function () { beforeEach(function () {
@ -37,18 +37,23 @@ define(
'elementHandle', 'elementHandle',
['x', 'y','getGridSize'] ['x', 'y','getGridSize']
); );
mockUpdate = jasmine.createSpy('update');
mockCommit = jasmine.createSpy('commit');
mockElementHandle.x.andReturn(6); mockElementHandle.x.andReturn(6);
mockElementHandle.y.andReturn(8); mockElementHandle.y.andReturn(8);
mockElementHandle.getGridSize.andReturn(TEST_GRID_SIZE); 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( handle = new FixedDragHandle(
mockElementHandle, mockElementHandle,
TEST_GRID_SIZE, mockConfigPath,
mockUpdate, mockFixedControl
mockCommit
); );
}); });
@ -74,13 +79,12 @@ define(
expect(mockElementHandle.x).toHaveBeenCalledWith(5); expect(mockElementHandle.x).toHaveBeenCalledWith(5);
expect(mockElementHandle.y).toHaveBeenCalledWith(7); expect(mockElementHandle.y).toHaveBeenCalledWith(7);
// Should have called update once per continueDrag // Should have called updateSelectionStyle once per continueDrag
expect(mockUpdate.calls.length).toEqual(2); expect(mockFixedControl.updateSelectionStyle.calls.length).toEqual(2);
// Finally, ending drag should commit // Finally, ending drag should mutate
expect(mockCommit).not.toHaveBeenCalled();
handle.endDrag(); handle.endDrag();
expect(mockCommit).toHaveBeenCalled(); expect(mockFixedControl.mutate).toHaveBeenCalled();
}); });
}); });

View File

@ -42,6 +42,8 @@ define(
mockOpenMCT, mockOpenMCT,
mockSelection, mockSelection,
mockDomainObjectCapability, mockDomainObjectCapability,
mockObjects,
unlistenFunc,
$element = [], $element = [],
selectable = []; selectable = [];
@ -77,14 +79,15 @@ define(
if (param === 'composition') { if (param === 'composition') {
return id !== 'b'; return id !== 'b';
} }
} },
type: "testType"
}; };
} }
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
"$scope", "$scope",
["$watch", "$watchCollection", "$on", "commit"] ["$watch", "$watchCollection", "$on"]
); );
mockEvent = jasmine.createSpyObj( mockEvent = jasmine.createSpyObj(
'event', 'event',
@ -104,9 +107,13 @@ define(
} }
} }
}; };
unlistenFunc = jasmine.createSpy("unlisten");
mockDomainObjectCapability = jasmine.createSpyObj('capability', mockDomainObjectCapability = jasmine.createSpyObj('capability',
['inEditContext'] ['inEditContext', 'listen']
); );
mockDomainObjectCapability.listen.andReturn(unlistenFunc);
mockCompositionCapability = mockPromise(mockCompositionObjects); mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject"); mockScope.domainObject = mockDomainObject("mockDomainObject");
@ -126,8 +133,14 @@ define(
'get' 'get'
]); ]);
mockSelection.get.andReturn(selectable); mockSelection.get.andReturn(selectable);
mockObjects = jasmine.createSpyObj('objects', [
'get'
]);
mockObjects.get.andReturn(mockPromise(mockDomainObject("mockObject")));
mockOpenMCT = { mockOpenMCT = {
selection: mockSelection selection: mockSelection,
objects: mockObjects
}; };
$element = $('<div></div>'); $element = $('<div></div>');
@ -138,6 +151,7 @@ define(
controller = new LayoutController(mockScope, $element, mockOpenMCT); controller = new LayoutController(mockScope, $element, mockOpenMCT);
spyOn(controller, "layoutPanels").andCallThrough(); spyOn(controller, "layoutPanels").andCallThrough();
spyOn(controller, "commit");
jasmine.Clock.useMock(); jasmine.Clock.useMock();
}); });
@ -270,10 +284,7 @@ define(
controller.continueDrag([100, 100]); controller.continueDrag([100, 100]);
controller.endDrag(); controller.endDrag();
// Should have triggered commit (provided by expect(controller.commit).toHaveBeenCalled();
// EditRepresenter) with some message.
expect(mockScope.commit)
.toHaveBeenCalledWith(jasmine.any(String));
}); });
it("listens for drop events", function () { it("listens for drop events", function () {
@ -296,11 +307,7 @@ define(
); );
expect(testConfiguration.panels.d).toBeDefined(); expect(testConfiguration.panels.d).toBeDefined();
expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(controller.commit).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 () { it("ignores drops when default has been prevented", function () {
@ -340,13 +347,17 @@ define(
testModel.layoutGrid = [1, 1]; testModel.layoutGrid = [1, 1];
mockScope.$watch.calls[0].args[1](testModel.layoutGrid); 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 // Notify that a drop occurred
mockScope.$on.mostRecentCall.args[1]( mockScope.$on.mostRecentCall.args[1](
mockEvent, mockEvent,
'd', 'd',
{ x: 300, y: 100 } { x: 300, y: 100 }
); );
mockScope.$watch.calls[0].args[1](['d']);
style = controller.getFrameStyle("d"); style = controller.getFrameStyle("d");
@ -415,30 +426,6 @@ define(
expect(controller.hasFrame(mockCompositionObjects[1])).toBe(false); 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 () { 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];

View File

@ -53,11 +53,6 @@ define(
}); });
}); });
it("allows elements to be removed", function () {
proxy.remove();
expect(testElements).toEqual([{}, {}, {}]);
});
it("allows order to be changed", function () { it("allows order to be changed", function () {
proxy.order("down"); proxy.order("down");
expect(testElements).toEqual([{}, testElement, {}, {}]); expect(testElements).toEqual([{}, testElement, {}, {}]);

View File

@ -26,7 +26,9 @@ define(
describe("A fixed position drag handle", function () { describe("A fixed position drag handle", function () {
var testElement, var testElement,
handle; mockElementProxy,
handle,
TEST_GRID_SIZE = [45, 21];
beforeEach(function () { beforeEach(function () {
testElement = { testElement = {
@ -36,8 +38,10 @@ define(
y2: 11, y2: 11,
useGrid: true 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 () { it("provides x/y grid coordinates for its corner", function () {
@ -69,7 +73,7 @@ define(
}); });
it("returns the correct grid size", function () { it("returns the correct grid size", function () {
expect(handle.getGridSize()).toEqual([45,21]); expect(handle.getGridSize()).toEqual(TEST_GRID_SIZE);
}); });
}); });

View File

@ -63,13 +63,13 @@ define(
it("adjusts both ends when mutating x", function () { it("adjusts both ends when mutating x", function () {
var proxy = new LineProxy(diagonal); var proxy = new LineProxy(diagonal);
proxy.x(6); 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 () { it("adjusts both ends when mutating y", function () {
var proxy = new LineProxy(diagonal); var proxy = new LineProxy(diagonal);
proxy.y(6); 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 () { it("provides internal positions for SVG lines", function () {

View File

@ -25,10 +25,12 @@ define(
function (ResizeHandle) { function (ResizeHandle) {
var TEST_MIN_WIDTH = 4, 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 () { describe("A fixed position drag handle", function () {
var testElement, var testElement,
mockElementProxy,
handle; handle;
beforeEach(function () { beforeEach(function () {
@ -39,12 +41,18 @@ define(
height: 36, height: 36,
useGrid: true 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( handle = new ResizeHandle(
testElement, mockElementProxy,
TEST_MIN_WIDTH, testElement
TEST_MIN_HEIGHT,
[34,81]
); );
}); });
@ -77,7 +85,7 @@ define(
}); });
it("returns the correct grid size", function () { it("returns the correct grid size", function () {
expect(handle.getGridSize()).toEqual([34,81]); expect(handle.getGridSize()).toEqual(TEST_GRID_SIZE);
}); });
}); });

View File

@ -49,27 +49,6 @@ define(
it("exposes the element's id", function () { it("exposes the element's id", function () {
expect(proxy.id).toEqual('test-id'); 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();
});
}); });
} }
); );

View File

@ -21,30 +21,29 @@
--> -->
<form novalidate> <form novalidate>
<div class="tool-bar btn-bar contents abs"> <div class="tool-bar btn-bar contents abs">
<span ng-repeat="section in structure.sections" <span ng-repeat="item in structure">
class="l-control-group" <span ng-if="item.control === 'divider'" class="l-control-group">
ng-if="!section.hidden" </span>
title="{{section.description}}"> <ng-form ng-class="{ 'input-labeled': item.name }"
<ng-form ng-repeat="item in section.items" ng-hide="item.hidden"
ng-class="{ 'input-labeled': item.name }" ng-if="item.control !== 'divider'"
ng-hide="item.hidden" class="inline"
class="inline" title="{{item.description}}"
title="{{item.description}}" name="mctFormInner">
name="mctFormInner">
<label ng-if="item.name"> <label ng-if="item.name">
{{item.name}}: {{item.name}}:
</label> </label>
<mct-control key="item.control" <mct-control key="item.control"
ng-class="{ disabled: item.disabled }" ng-class="{ disabled: item.disabled }"
ng-model="ngModel" ng-model="ngModel"
ng-required="item.required" ng-required="item.required"
ng-pattern="getRegExp(item.pattern)" ng-pattern="getRegExp(item.pattern)"
options="item.options" options="item.options"
structure="item" structure="item"
field="item.key"> field="item.key">
</mct-control> </mct-control>
</ng-form> </ng-form>
</span> </span>
</div> </div>
</form> </form>

View File

@ -24,8 +24,16 @@
* Module defining MCTForm. Created by vwoeltje on 11/10/14. * Module defining MCTForm. Created by vwoeltje on 11/10/14.
*/ */
define( 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 * The mct-toolbar directive allows generation of displayable
@ -35,7 +43,7 @@ define(
* This directive accepts three attributes: * This directive accepts three attributes:
* *
* * `ng-model`: The model for the form; where user input * * `ng-model`: The model for the form; where user input
* where be stored. * will be stored.
* * `structure`: The declarative structure of the toolbar. * * `structure`: The declarative structure of the toolbar.
* Describes what controls should be shown and where * Describes what controls should be shown and where
* their values should be read/written in the model. * their values should be read/written in the model.
@ -49,9 +57,10 @@ define(
*/ */
function MCTToolbar() { function MCTToolbar() {
// Use Directive Definition Object from mct-form, // 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(); var ddo = new MCTForm();
ddo.template = toolbarTemplate; ddo.template = toolbarTemplate;
ddo.controller = ['$scope', 'openmct', ToolbarController];
return ddo; return ddo;
} }

View File

@ -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;
}
);

View File

@ -26,16 +26,28 @@ define(
describe("The mct-toolbar directive", function () { describe("The mct-toolbar directive", function () {
var mockScope, var mockScope,
mockOpenMCT,
mockSelection,
mctToolbar; mctToolbar;
function installController() { function installController() {
var Controller = mctToolbar.controller[1]; var Controller = mctToolbar.controller[2];
return new Controller(mockScope); return new Controller(mockScope, mockOpenMCT);
} }
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", ["$watch"]); mockScope = jasmine.createSpyObj("$scope", [
"$watch",
"$on"
]);
mockScope.$parent = {}; mockScope.$parent = {};
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off'
]);
mockOpenMCT = {
selection: mockSelection
};
mctToolbar = new MCTToolbar(); mctToolbar = new MCTToolbar();
}); });
@ -43,29 +55,15 @@ define(
expect(mctToolbar.restrict).toEqual("E"); expect(mctToolbar.restrict).toEqual("E");
}); });
it("watches for changes in form by name", function () { it("listens for selection change event", 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.
installController(); installController();
expect(mockScope.$watch).toHaveBeenCalledWith( expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
"mctForm", "change",
jasmine.any(Function) 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 () { it("allows strings to be converted to RegExps", function () {
// This is needed to support ng-pattern in the template // This is needed to support ng-pattern in the template
installController(); installController();

View File

@ -29,7 +29,8 @@ define([
'./api/objects/object-utils', './api/objects/object-utils',
'./plugins/plugins', './plugins/plugins',
'./ui/ViewRegistry', './ui/ViewRegistry',
'./ui/InspectorViewRegistry' './ui/InspectorViewRegistry',
'./ui/ToolbarRegistry'
], function ( ], function (
EventEmitter, EventEmitter,
legacyRegistry, legacyRegistry,
@ -39,7 +40,8 @@ define([
objectUtils, objectUtils,
plugins, plugins,
ViewRegistry, ViewRegistry,
InspectorViewRegistry InspectorViewRegistry,
ToolbarRegistry
) { ) {
/** /**
* Open MCT is an extensible web application for building mission * Open MCT is an extensible web application for building mission
@ -76,7 +78,7 @@ define([
* Tracks current selection state of the application. * Tracks current selection state of the application.
* @private * @private
*/ */
this.selection = new Selection(); this.selection = new Selection(this);
/** /**
* MCT's time conductor, which may be used to synchronize view contents * 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 * 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 * @type {module:openmct.ToolbarRegistry}
* providers should be prepared to test arbitrary objects for
* viewability.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name toolbars * @name toolbars
*/ */
this.toolbars = new ViewRegistry(); this.toolbars = new ToolbarRegistry();
/** /**
* Registry for domain object types which may exist within this * Registry for domain object types which may exist within this

View File

@ -76,12 +76,20 @@ define([
* @memberof module:openmct.MutableObject# * @memberof module:openmct.MutableObject#
*/ */
MutableObject.prototype.set = function (path, value) { MutableObject.prototype.set = function (path, value) {
_.set(this.object, path, value); _.set(this.object, path, value);
_.set(this.object, 'modified', Date.now()); _.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 //Emit event specific to property
this.eventEmitter.emit(qualifiedEventName(this.object, path), value); this.eventEmitter.emit(qualifiedEventName(this.object, path), value);
this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
//Emit wildcare event //Emit wildcare event
this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object); this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);

View File

@ -26,8 +26,10 @@ define(['EventEmitter'], function (EventEmitter) {
* Manages selection state for Open MCT * Manages selection state for Open MCT
* @private * @private
*/ */
function Selection() { function Selection(openmct) {
EventEmitter.call(this); EventEmitter.call(this);
this.openmct = openmct;
this.selected = []; this.selected = [];
} }
@ -99,7 +101,12 @@ define(['EventEmitter'], function (EventEmitter) {
* Attaches the click handlers to the element. * Attaches the click handlers to the element.
* *
* @param element an html 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 * @param select a flag to select the element if true
* @returns a function that removes the click handlers from the element * @returns a function that removes the click handlers from the element
* @public * @public
@ -114,6 +121,12 @@ define(['EventEmitter'], function (EventEmitter) {
element.addEventListener('click', capture, true); element.addEventListener('click', capture, true);
element.addEventListener('click', selectCapture); element.addEventListener('click', selectCapture);
if (context.item) {
var unlisten = this.openmct.objects.observe(context.item, "*", function (newItem) {
context.item = newItem;
});
}
if (select) { if (select) {
element.click(); element.click();
} }
@ -121,6 +134,10 @@ define(['EventEmitter'], function (EventEmitter) {
return function () { return function () {
element.removeEventListener('click', capture); element.removeEventListener('click', capture);
element.removeEventListener('click', selectCapture); element.removeEventListener('click', selectCapture);
if (unlisten) {
unlisten();
}
}; };
}; };

125
src/ui/ToolbarRegistry.js Normal file
View File

@ -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;
});