openmct/platform/features/layout/src/LayoutController.js

309 lines
12 KiB
JavaScript

/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 define*/
/**
* This bundle implements object types and associated views for
* display-building.
* @namespace platform/features/layout
*/
define(
['./LayoutDrag'],
function (LayoutDrag) {
"use strict";
var DEFAULT_DIMENSIONS = [ 12, 8 ],
DEFAULT_GRID_SIZE = [ 32, 32 ],
MINIMUM_FRAME_SIZE = [ 320, 180 ];
/**
* The LayoutController is responsible for supporting the
* Layout view. It arranges frames according to saved configuration
* and provides methods for updating these based on mouse
* movement.
* @memberof platform/features/layout
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function LayoutController($scope) {
var self = this,
callbackCount = 0;
// Update grid size when it changed
function updateGridSize(layoutGrid) {
var oldSize = self.gridSize;
self.gridSize = layoutGrid || DEFAULT_GRID_SIZE;
// Only update panel positions if this actually changed things
if (self.gridSize[0] !== oldSize[0] ||
self.gridSize[1] !== oldSize[1]) {
self.layoutPanels(Object.keys(self.positions));
}
}
// Position a panel after a drop event
//An editableDomainObject is provided, as the drop may have
// triggered a transition to edit mode.
function handleDrop(e, id, position, editableDomainObject) {
if (e.defaultPrevented) {
return;
}
if (editableDomainObject){
$scope.setEditable(editableDomainObject);
}
// Ensure that configuration field is populated
$scope.configuration = $scope.configuration || {};
// Make sure there is a "panels" field in the
// view configuration.
$scope.configuration.panels =
$scope.configuration.panels || {};
// Store the position of this panel.
$scope.configuration.panels[id] = {
position: [
Math.floor(position.x / self.gridSize[0]),
Math.floor(position.y / self.gridSize[1])
],
dimensions: self.defaultDimensions()
};
// Mark change as persistable
if ($scope.commit) {
$scope.commit("Dropped a frame.");
}
// Populate template-facing position for this id
self.rawPositions[id] =
$scope.configuration.panels[id];
self.populatePosition(id);
// Layout may contain embedded views which will
// listen for drops, so call preventDefault() so
// that they can recognize that this event is handled.
e.preventDefault();
}
//Will fetch fully contextualized composed objects, and populate
// scope with them.
function refreshComposition() {
//Keep a track of how many composition callbacks have been made
var thisCount = ++callbackCount;
$scope.domainObject.useCapability('composition').then(function(composition){
var ids;
//Is this callback for the most recent composition
// request? If not, discard it. Prevents race condition
if (thisCount === callbackCount){
ids = composition.map(function (object) {
return object.getId();
}) || [];
$scope.composition = composition;
self.layoutPanels(ids);
}
});
}
// End drag; we don't want to put $scope into this
// because it triggers "cpws" (copy window or scope)
// errors in Angular.
this.endDragInScope = function () {
// Write to configuration; this is watched and
// saved by the EditRepresenter.
$scope.configuration =
$scope.configuration || {};
// Make sure there is a "panels" field in the
// view configuration.
$scope.configuration.panels =
$scope.configuration.panels || {};
// Store the position of this panel.
$scope.configuration.panels[self.activeDragId] =
self.rawPositions[self.activeDragId];
// Mark this object as dirty to encourage persistence
if ($scope.commit) {
$scope.commit("Moved frame.");
}
};
this.positions = {};
this.rawPositions = {};
this.gridSize = DEFAULT_GRID_SIZE;
this.$scope = $scope;
// Watch for changes to the grid size in the model
$scope.$watch("model.layoutGrid", updateGridSize);
// Update composed objects on screen, and position panes
$scope.$watchCollection("model.composition", refreshComposition);
// Position panes where they are dropped
$scope.$on("mctDrop", handleDrop);
}
// Convert from { positions: ..., dimensions: ... } to an
// apropriate ng-style argument, to position frames.
LayoutController.prototype.convertPosition = function (raw) {
var gridSize = this.gridSize;
// Multiply position/dimensions by grid size
return {
left: (gridSize[0] * raw.position[0]) + 'px',
top: (gridSize[1] * raw.position[1]) + 'px',
width: (gridSize[0] * raw.dimensions[0]) + 'px',
height: (gridSize[1] * raw.dimensions[1]) + 'px'
};
};
// Generate default positions for a new panel
LayoutController.prototype.defaultDimensions = function () {
var gridSize = this.gridSize;
return MINIMUM_FRAME_SIZE.map(function (min, i) {
return Math.max(
Math.ceil(min / gridSize[i]),
DEFAULT_DIMENSIONS[i]
);
});
};
// Generate a default position (in its raw format) for a frame.
// Use an index to ensure that default positions are unique.
LayoutController.prototype.defaultPosition = function (index) {
return {
position: [index, index],
dimensions: this.defaultDimensions()
};
};
// Store a computed position for a contained frame by its
// domain object id. Called in a forEach loop, so arguments
// are as expected there.
LayoutController.prototype.populatePosition = function (id, index) {
this.rawPositions[id] =
this.rawPositions[id] || this.defaultPosition(index || 0);
this.positions[id] =
this.convertPosition(this.rawPositions[id]);
};
/**
* Get a style object for a frame with the specified domain
* object identifier, suitable for use in an `ng-style`
* directive to position a frame as configured for this layout.
* @param {string} id the object identifier
* @returns {Object.<string, string>} an object with
* appropriate left, width, etc fields for positioning
*/
LayoutController.prototype.getFrameStyle = function (id) {
// Called in a loop, so just look up; the "positions"
// object is kept up to date by a watch.
return this.positions[id];
};
/**
* Start a drag gesture to move/resize a frame.
*
* The provided position and dimensions factors will determine
* whether this is a move or a resize, and what type it
* will be. For instance, a position factor of [1, 1]
* will move a frame along with the mouse as the drag
* proceeds, while a dimension factor of [0, 0] will leave
* dimensions unchanged. Combining these in different
* ways results in different handles; a position factor of
* [1, 0] and a dimensions factor of [-1, 0] will implement
* a left-edge resize, as the horizontal position will move
* with the mouse while the horizontal dimensions shrink in
* kind (and vertical properties remain unmodified.)
*
* @param {string} id the identifier of the domain object
* in the frame being manipulated
* @param {number[]} posFactor the position factor
* @param {number[]} dimFactor the dimensions factor
*/
LayoutController.prototype.startDrag = function (id, posFactor, dimFactor) {
this.activeDragId = id;
this.activeDrag = new LayoutDrag(
this.rawPositions[id],
posFactor,
dimFactor,
this.gridSize
);
};
/**
* Continue an active drag gesture.
* @param {number[]} delta the offset, in pixels,
* of the current pointer position, relative
* to its position when the drag started
*/
LayoutController.prototype.continueDrag = function (delta) {
if (this.activeDrag) {
this.rawPositions[this.activeDragId] =
this.activeDrag.getAdjustedPosition(delta);
this.populatePosition(this.activeDragId);
}
};
// Utility function to copy raw positions from configuration,
// without writing directly to configuration (to avoid triggering
// persistence from watchers during drags).
function shallowCopy(obj, keys) {
var copy = {};
keys.forEach(function (k) {
copy[k] = obj[k];
});
return copy;
}
/**
* Compute panel positions based on the layout's object model.
* Defined as member function to facilitate testing.
* @private
*/
LayoutController.prototype.layoutPanels = function (ids) {
var configuration = this.$scope.configuration || {},
self = this;
// Pull panel positions from configuration
this.rawPositions =
shallowCopy(configuration.panels || {}, ids);
// Clear prior computed positions
this.positions = {};
// Update width/height that we are tracking
this.gridSize =
(this.$scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE;
// Compute positions and add defaults where needed
ids.forEach(function (id, index) {
self.populatePosition(id, index);
});
};
/**
* End the active drag gesture. This will update the
* view configuration.
*/
LayoutController.prototype.endDrag = function () {
this.endDragInScope();
};
return LayoutController;
}
);