Merge remote-tracking branch 'origin/open880' into open-master

This commit is contained in:
bwyu 2015-03-03 10:52:27 -08:00
commit 0b29e05a31
20 changed files with 594 additions and 37 deletions

View File

@ -26,9 +26,32 @@
"items": [
{
"method": "add",
"control": "button",
"glyph": "+",
"control": "menu-button",
"text": "Add",
"inclusive": true
"inclusive": true,
"options": [
{
"name": "Box",
"glyph": "\u2610",
"key": "fixed.box"
},
{
"name": "Line",
"glyph": "-",
"key": "fixed.line"
},
{
"name": "Text",
"glyph": "\u1D1B",
"key": "fixed.text"
},
{
"name": "Image",
"glyph": "\u2353",
"key": "fixed.image"
}
]
}
]
},
@ -61,13 +84,35 @@
{
"key": "FixedController",
"implementation": "FixedController.js",
"depends": [ "$scope", "telemetrySubscriber", "telemetryFormatter" ]
"depends": [
"$scope",
"$q",
"dialogService",
"telemetrySubscriber",
"telemetryFormatter"
]
}
],
"templates": [
{
"key": "fixed.telemetry",
"templateUrl": "templates/elements/telemetry.html"
},
{
"key": "fixed.box",
"templateUrl": "templates/elements/box.html"
},
{
"key": "fixed.line",
"templateUrl": "templates/elements/line.html"
},
{
"key": "fixed.text",
"templateUrl": "templates/elements/text.html"
},
{
"key": "fixed.image",
"templateUrl": "templates/elements/image.html"
}
],
"types": [

View File

@ -0,0 +1,3 @@
<div ng-style="{ background: ngModel.element.fill }"
style="width: 100%; height: 100%;">
</div>

View File

@ -0,0 +1,3 @@
<div ng-style="{ 'background-image': 'url(' + ngModel.element.url + ')'}"
style="width: 100%; height: 100%; background-size: contain; background-repeat: no-repeat; background-position: center;">
</div>

View File

@ -0,0 +1,10 @@
<svg ng-attr-width="{{parameters.gridSize[0] * ngModel.width()}}"
ng-attr-height="{{parameters.gridSize[1] * ngModel.height()}}">
<line ng-attr-x1="{{parameters.gridSize[0] * ngModel.x1() + 1}}"
ng-attr-y1="{{parameters.gridSize[1] * ngModel.y1() + 1}}"
ng-attr-x2="{{parameters.gridSize[0] * ngModel.x2() + 1}}"
ng-attr-y2="{{parameters.gridSize[1] * ngModel.y2() + 1}}"
stroke="lightgray"
stroke-width="2">
</line>
</svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1,4 @@
<div ng-style="{ background: ngModel.element.fill }"
style="width: 100%; height: 100%; overflow: hidden;">
{{ngModel.element.text}}
</div>

View File

@ -14,6 +14,7 @@
<mct-include ng-repeat="element in controller.getElements()"
style="position: absolute;"
key="element.template"
parameters="{ gridSize: controller.getGridSize() }"
ng-class="{ test: controller.selected(element) }"
ng-style="element.style"
ng-click="controller.select(element)"

View File

@ -17,7 +17,7 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function FixedController($scope, telemetrySubscriber, telemetryFormatter) {
function FixedController($scope, $q, dialogService, telemetrySubscriber, telemetryFormatter) {
var gridSize = DEFAULT_GRID_SIZE,
gridExtent = DEFAULT_GRID_EXTENT,
dragging,
@ -52,13 +52,13 @@ define(
// Convert from element x/y/width/height to an
// apropriate ng-style argument, to position elements.
function convertPosition(raw) {
function convertPosition(elementProxy) {
// Multiply position/dimensions by grid size
return {
left: (gridSize[0] * raw.x) + 'px',
top: (gridSize[1] * raw.y) + 'px',
width: (gridSize[0] * raw.width) + 'px',
height: (gridSize[1] * raw.height) + 'px'
left: (gridSize[0] * elementProxy.x()) + 'px',
top: (gridSize[1] * elementProxy.y()) + 'px',
width: (gridSize[0] * elementProxy.width()) + 'px',
height: (gridSize[1] * elementProxy.height()) + 'px'
};
}
@ -89,7 +89,7 @@ define(
if (e) {
// Provide a displayable position (convert from grid to px)
e.style = convertPosition(element);
e.style = convertPosition(e);
// Template names are same as type names, presently
e.template = element.type;
}
@ -163,8 +163,8 @@ define(
subscribe($scope.domainObject);
}
// Position a panel after a drop event
function handleDrop(e, id, position) {
// Add an element to this view
function addElement(element) {
// Ensure that configuration field is populated
$scope.configuration = $scope.configuration || {};
// Make sure there is a "elements" field in the
@ -172,7 +172,23 @@ define(
$scope.configuration.elements =
$scope.configuration.elements || [];
// Store the position of this element.
$scope.configuration.elements.push({
$scope.configuration.elements.push(element);
// Refresh displayed elements
refreshElements();
// Select the newly-added element
if (selection) {
selection.select(elementProxies[elementProxies.length - 1]);
}
// Mark change as persistable
if ($scope.commit) {
$scope.commit("Dropped an element.");
}
}
// Position a panel after a drop event
function handleDrop(e, id, position) {
// Store the position of this element.
addElement({
type: "fixed.telemetry",
x: Math.floor(position.x / gridSize[0]),
y: Math.floor(position.y / gridSize[1]),
@ -180,17 +196,14 @@ define(
width: DEFAULT_DIMENSIONS[0],
height: DEFAULT_DIMENSIONS[1]
});
// Mark change as persistable
if ($scope.commit) {
$scope.commit("Dropped an element.");
}
}
// Track current selection state
if (Array.isArray($scope.selection)) {
selection = new LayoutSelection(
$scope.selection,
new FixedProxy($scope.configuration)
new FixedProxy(addElement, $q, dialogService)
);
}
@ -222,6 +235,14 @@ define(
getCellStyles: function () {
return cellStyles;
},
/**
* Get the size of the grid, in pixels. The returned array
* is in the form `[x, y]`.
* @returns {number[]} the grid size
*/
getGridSize: function () {
return gridSize;
},
/**
* Set the size of the viewable fixed position area.
* @memberof FixedController#
@ -303,9 +324,11 @@ define(
*/
continueDrag: function (delta) {
if (dragging) {
// Update x/y values
dragging.element.x(dragging.x + Math.round(delta[0] / gridSize[0]));
dragging.element.y(dragging.y + Math.round(delta[1] / gridSize[1]));
dragging.element.style = convertPosition(dragging.element.element);
// Update display position
dragging.element.style = convertPosition(dragging.element);
}
},
/**

View File

@ -1,22 +1,42 @@
/*global define,window*/
define(
[],
function () {
['./elements/ElementFactory'],
function (ElementFactory) {
"use strict";
/**
* Proxy for configuring a fixed position view via the toolbar.
* @constructor
* @param configuration the view configuration object
* @param {Function} addElementCallback callback to invoke when
* elements are created
* @param $q Angular's $q, for promise-handling
* @param {DialogService} dialogService dialog service to use
* when adding a new element will require user input
*/
function FixedProxy(configuration) {
function FixedProxy(addElementCallback, $q, dialogService) {
var factory = new ElementFactory(dialogService);
return {
/**
* Add a new visual element to this view.
*/
add: function (type) {
window.alert("Placeholder. Should add a " + type + ".");
// Place a configured element into the view configuration
function addElement(element) {
// Configure common properties of the element
element.x = element.x || 0;
element.y = element.y || 0;
element.width = element.width || 1;
element.height = element.height || 1;
element.type = type;
// Finally, add it to the view's configuration
addElementCallback(element);
}
// Defer creation to the factory
$q.when(factory.createElement(type)).then(addElement);
}
};
}

View File

@ -0,0 +1,89 @@
/*global define*/
define(
[],
function () {
"use strict";
var INITIAL_STATES = {
"fixed.image": {},
"fixed.box": {
fill: "#888",
border: "transparent"
},
"fixed.line": {
x: 5,
y: 9,
x2: 6,
y2: 6
},
"fixed.text": {
fill: "transparent",
border: "transparent"
}
},
DIALOGS = {
"fixed.image": {
name: "Image Properties",
sections: [
{
rows: [
{
key: "url",
control: "textfield",
name: "Image URL",
required: true
}
]
}
]
},
"fixed.text": {
name: "Text Element Properties",
sections: [
{
rows: [
{
key: "text",
control: "textfield",
name: "Text",
required: true
}
]
}
]
}
};
/**
* The ElementFactory creates new instances of elements for the
* fixed position view, prompting for user input where necessary.
* @param {DialogService} dialogService service to request user input
* @constructor
*/
function ElementFactory(dialogService) {
return {
/**
* Create a new element for the fixed position view.
* @param {string} type the type of element to create
* @returns {Promise|object} the created element, or a promise
* for that element
*/
createElement: function (type) {
var initialState = INITIAL_STATES[type] || {};
// Clone that state
initialState = JSON.parse(JSON.stringify(initialState));
// Show a dialog to configure initial state, if appropriate
return DIALOGS[type] ? dialogService.getUserInput(
DIALOGS[type],
initialState
) : initialState;
}
};
}
return ElementFactory;
}
);

View File

@ -1,12 +1,16 @@
/*global define*/
define(
['./TelemetryProxy'],
function (TelemetryProxy) {
['./TelemetryProxy', './ElementProxy', './LineProxy'],
function (TelemetryProxy, ElementProxy, LineProxy) {
"use strict";
return {
"fixed.telemetry": TelemetryProxy
"fixed.telemetry": TelemetryProxy,
"fixed.line": LineProxy,
"fixed.box": ElementProxy,
"fixed.image": ElementProxy,
"fixed.text": ElementProxy
};
}
);

View File

@ -11,18 +11,54 @@ define(
* This handles the generic operations (e.g. remove) so that
* subclasses only need to implement element-specific behaviors.
* @constructor
* @param element the telemetry element
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
*/
function ElementProxy(element, index, elements) {
return {
/**
* The element as stored in the view configuration.
*/
element: element,
/**
* Get and/or set the x position of this element.
* Units are in fixed position grid space.
* @param {number} [x] the new x position (if setting)
* @returns {number} the x position
*/
x: new AccessorMutator(element, 'x'),
/**
* Get and/or set the y position of this element.
* Units are in fixed position grid space.
* @param {number} [y] the new y position (if setting)
* @returns {number} the y position
*/
y: new AccessorMutator(element, 'y'),
/**
* Get and/or set the z index of this element.
* @param {number} [z] the new z index (if setting)
* @returns {number} the z index
*/
z: new AccessorMutator(element, 'z'),
/**
* Get and/or set the width of this element.
* Units are in fixed position grid space.
* @param {number} [w] the new width (if setting)
* @returns {number} the width
*/
width: new AccessorMutator(element, 'width'),
/**
* Get and/or set the height of this element.
* Units are in fixed position grid space.
* @param {number} [h] the new height (if setting)
* @returns {number} the height
*/
height: new AccessorMutator(element, 'height'),
/**
* Remove this element from the fixed position view.
*/
remove: function () {
if (elements[index] === element) {
elements.splice(index, 1);

View File

@ -0,0 +1,113 @@
/*global define*/
define(
['./ElementProxy'],
function (ElementProxy) {
'use strict';
/**
* Selection/diplay proxy for line elements of a fixed position
* view.
* @constructor
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
*/
function LineProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements);
/**
* Get the top-left x coordinate, in grid space, of
* this line's bounding box.
* @returns {number} the x coordinate
*/
proxy.x = function (v) {
var x = Math.min(element.x, element.x2),
delta = v - x;
if (arguments.length > 0 && delta) {
element.x += delta;
element.x2 += delta;
}
return x;
};
/**
* Get the top-left y coordinate, in grid space, of
* this line's bounding box.
* @returns {number} the y coordinate
*/
proxy.y = function (v) {
var y = Math.min(element.y, element.y2),
delta = v - y;
if (arguments.length > 0 && delta) {
element.y += delta;
element.y2 += delta;
}
return y;
};
/**
* Get the width, in grid space, of
* this line's bounding box.
* @returns {number} the width
*/
proxy.width = function () {
return Math.max(Math.abs(element.x - element.x2), 1);
};
/**
* Get the height, in grid space, of
* this line's bounding box.
* @returns {number} the height
*/
proxy.height = function () {
return Math.max(Math.abs(element.y - element.y2), 1);
};
/**
* Get the x position, in grid units relative to
* the top-left corner, of the first point in this line
* segment.
* @returns {number} the x position of the first point
*/
proxy.x1 = function () {
return element.x - proxy.x();
};
/**
* Get the y position, in grid units relative to
* the top-left corner, of the first point in this line
* segment.
* @returns {number} the y position of the first point
*/
proxy.y1 = function () {
return element.y - proxy.y();
};
/**
* Get the x position, in grid units relative to
* the top-left corner, of the second point in this line
* segment.
* @returns {number} the x position of the second point
*/
proxy.x2 = function () {
return element.x2 - proxy.x();
};
/**
* Get the y position, in grid units relative to
* the top-left corner, of the second point in this line
* segment.
* @returns {number} the y position of the second point
*/
proxy.y2 = function () {
return element.y2 - proxy.y();
};
return proxy;
}
return LineProxy;
}
);

View File

@ -7,6 +7,8 @@ define(
describe("The Fixed Position controller", function () {
var mockScope,
mockQ,
mockDialogService,
mockSubscriber,
mockFormatter,
mockDomainObject,
@ -58,6 +60,11 @@ define(
'telemetrySubscriber',
[ 'subscribe' ]
);
mockQ = jasmine.createSpyObj('$q', ['when']);
mockDialogService = jasmine.createSpyObj(
'dialogService',
['getUserInput']
);
mockFormatter = jasmine.createSpyObj(
'telemetryFormatter',
[ 'formatDomainValue', 'formatRangeValue' ]
@ -99,6 +106,8 @@ define(
controller = new FixedController(
mockScope,
mockQ,
mockDialogService,
mockSubscriber,
mockFormatter
);
@ -263,8 +272,6 @@ define(
.toHaveBeenCalledWith(jasmine.any(String));
});
it("unsubscribes when destroyed", function () {
// Make an object available
findWatch('domainObject')(mockDomainObject);
@ -275,6 +282,12 @@ define(
// Should have unsubscribed
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
});
it("exposes its grid size", function () {
// Template needs to be able to pass this into line
// elements to size SVGs appropriately
expect(controller.getGridSize()).toEqual(testGrid);
});
});
}
);

View File

@ -6,13 +6,46 @@ define(
"use strict";
describe("Fixed Position view's selection proxy", function () {
it("has a placeholder message when clicked", function () {
var oldAlert = window.alert;
window.alert = jasmine.createSpy('alert');
new FixedProxy({}).add('');
expect(window.alert).toHaveBeenCalledWith(jasmine.any(String));
window.alert = oldAlert;
var mockCallback,
mockQ,
mockDialogService,
mockPromise,
proxy;
beforeEach(function () {
mockCallback = jasmine.createSpy('callback');
mockQ = jasmine.createSpyObj('$q', ['when']);
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserInput']);
mockPromise = jasmine.createSpyObj('promise', ['then']);
mockQ.when.andReturn(mockPromise);
proxy = new FixedProxy(mockCallback, mockQ, mockDialogService);
});
it("handles promised element creation", function () {
// The element factory may return promises (e.g. if
// user input is required) so make sure proxy is wrapping these
proxy.add("fixed.box");
expect(mockQ.when).toHaveBeenCalled();
});
it("notifies its callback when an element is created", function () {
proxy.add("fixed.box");
// Callback should not have been invoked yet
expect(mockCallback).not.toHaveBeenCalled();
// Resolve the promise
mockPromise.then.mostRecentCall.args[0]({});
// Should have fired the callback
expect(mockCallback).toHaveBeenCalledWith({
type: "fixed.box",
x: 0,
y: 0,
width: 1,
height: 1
});
});
});
}
);

View File

@ -0,0 +1,50 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/ElementFactory'],
function (ElementFactory) {
"use strict";
var DIALOG_ELEMENTS = [ 'image', 'text' ],
NON_DIALOG_ELEMENTS = [ 'box', 'line' ];
describe("The fixed position element factory", function () {
var mockDialogService,
mockPromise,
factory;
beforeEach(function () {
mockDialogService = jasmine.createSpyObj(
'dialogService',
[ 'getUserInput' ]
);
mockPromise = jasmine.createSpyObj(
'promise',
[ 'then' ]
);
mockDialogService.getUserInput.andReturn(mockPromise);
mockPromise.then.andReturn(mockPromise);
factory = new ElementFactory(mockDialogService);
});
DIALOG_ELEMENTS.forEach(function (type) {
it("shows a dialog for " + type + " elements", function () {
expect(factory.createElement('fixed.' + type))
.toEqual(mockPromise);
expect(mockDialogService.getUserInput).toHaveBeenCalled();
});
});
NON_DIALOG_ELEMENTS.forEach(function (type) {
it("immediately provides " + type + " elements", function () {
var result = factory.createElement('fixed.' + type);
expect(result).toBeDefined();
expect(result).not.toEqual(mockPromise);
expect(mockDialogService.getUserInput).not.toHaveBeenCalled();
});
});
});
}
);

View File

@ -5,8 +5,13 @@ define(
function (ElementProxies) {
"use strict";
// Expect these element types to have proxies
var ELEMENT_TYPES = [
"fixed.telemetry"
"fixed.telemetry",
"fixed.line",
"fixed.box",
"fixed.text",
"fixed.image"
];
// Verify that the set of proxies exposed matches the specific

View File

@ -0,0 +1,72 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/LineProxy'],
function (LineProxy) {
"use strict";
describe("A fixed position line proxy", function () {
var vertical, horizontal, diagonal, reversed;
beforeEach(function () {
vertical = { x: 1, y: 4, x2: 1, y2: 8 };
horizontal = { x: 3, y: 3, x2: 12, y2: 3 };
diagonal = { x: 3, y: 8, x2: 5, y2: 11 };
reversed = { x2: 3, y2: 8, x: 5, y: 11 };
});
it("ensures visible width for vertical lines", function () {
expect(new LineProxy(vertical).width()).toEqual(1);
});
it("ensures visible height for horizontal lines", function () {
expect(new LineProxy(horizontal).height()).toEqual(1);
});
it("provides a bounding box for lines", function () {
var proxy = new LineProxy(diagonal);
expect(proxy.x()).toEqual(3);
expect(proxy.y()).toEqual(8);
expect(proxy.width()).toEqual(2);
expect(proxy.height()).toEqual(3);
});
it("bounds lines identically regardless of point order", function () {
// That is, x(), width(), y(), and height() should always give
// the same results for the same line segments, regardless of
// which point is x,y and which is x2,y2
['x', 'y', 'width', 'height'].forEach(function (method) {
expect(new LineProxy(diagonal)[method]())
.toEqual(new LineProxy(reversed)[method]());
});
});
it("adjusts both ends when mutating x", function () {
var proxy = new LineProxy(diagonal);
proxy.x(6);
expect(diagonal).toEqual({ x: 6, y: 8, x2: 8, y2: 11 });
});
it("adjusts both ends when mutating y", function () {
var proxy = new LineProxy(diagonal);
proxy.y(6);
expect(diagonal).toEqual({ x: 3, y: 6, x2: 5, y2: 9 });
});
it("provides internal positions for SVG lines", function () {
var proxy;
proxy = new LineProxy(diagonal);
expect(proxy.x1()).toEqual(0);
expect(proxy.y1()).toEqual(0);
expect(proxy.x2()).toEqual(2);
expect(proxy.y2()).toEqual(3);
proxy = new LineProxy(reversed);
expect(proxy.x1()).toEqual(2);
expect(proxy.y1()).toEqual(3);
expect(proxy.x2()).toEqual(0);
expect(proxy.y2()).toEqual(0);
});
});
}
);

View File

@ -5,7 +5,9 @@
"LayoutDrag",
"LayoutSelection",
"elements/AccessorMutator",
"elements/ElementFactory",
"elements/ElementProxies",
"elements/ElementProxy",
"elements/LineProxy",
"elements/TelemetryProxy"
]

View File

@ -45,6 +45,10 @@
{
"key": "composite",
"templateUrl": "templates/controls/composite.html"
},
{
"key": "menu-button",
"templateUrl": "templates/controls/menu-button.html"
}
],
"controllers": [

View File

@ -0,0 +1,27 @@
<div class="s-btn s-icon-btn s-very-subtle btn-menu menu-element dropdown click-invoke"
ng-controller="ClickAwayController as toggle">
<span ng-click="toggle.toggle()">
<span class="ui-symbol icon">{{structure.glyph}}</span>
<span class="title-label" ng-if="structure.text">
{{structure.text}}
</span>
<span class='ui-symbol icon invoke-menu'
ng-if="!structure.text">
v
</span>
</span>
<div class="menu dropdown" ng-show="toggle.isActive()">
<ul>
<li ng-repeat="option in structure.options">
<a href="" ng-click="structure.click(option.key); toggle.setState(false)">
<span class="ui-symbol type-icon icon">
{{option.glyph}}
</span>
{{option.name}}
</a>
</li>
</ul>
</div>
</div>