diff --git a/platform/features/layout/bundle.json b/platform/features/layout/bundle.json index 8ded0c2f6f..d7fb512219 100644 --- a/platform/features/layout/bundle.json +++ b/platform/features/layout/bundle.json @@ -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": [ diff --git a/platform/features/layout/res/templates/elements/box.html b/platform/features/layout/res/templates/elements/box.html new file mode 100644 index 0000000000..58ecf860d7 --- /dev/null +++ b/platform/features/layout/res/templates/elements/box.html @@ -0,0 +1,3 @@ +
+
\ No newline at end of file diff --git a/platform/features/layout/res/templates/elements/image.html b/platform/features/layout/res/templates/elements/image.html new file mode 100644 index 0000000000..041147df27 --- /dev/null +++ b/platform/features/layout/res/templates/elements/image.html @@ -0,0 +1,3 @@ +
+
\ No newline at end of file diff --git a/platform/features/layout/res/templates/elements/line.html b/platform/features/layout/res/templates/elements/line.html new file mode 100644 index 0000000000..524cd451ca --- /dev/null +++ b/platform/features/layout/res/templates/elements/line.html @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/platform/features/layout/res/templates/elements/text.html b/platform/features/layout/res/templates/elements/text.html new file mode 100644 index 0000000000..2b52f04598 --- /dev/null +++ b/platform/features/layout/res/templates/elements/text.html @@ -0,0 +1,4 @@ +
+ {{ngModel.element.text}} +
\ No newline at end of file diff --git a/platform/features/layout/res/templates/fixed.html b/platform/features/layout/res/templates/fixed.html index 0b7e1ef056..9d6d741a62 100644 --- a/platform/features/layout/res/templates/fixed.html +++ b/platform/features/layout/res/templates/fixed.html @@ -14,6 +14,7 @@ 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; + } +); \ No newline at end of file diff --git a/platform/features/layout/test/FixedControllerSpec.js b/platform/features/layout/test/FixedControllerSpec.js index 460228e362..c8b943cc62 100644 --- a/platform/features/layout/test/FixedControllerSpec.js +++ b/platform/features/layout/test/FixedControllerSpec.js @@ -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); + }); }); } ); \ No newline at end of file diff --git a/platform/features/layout/test/FixedProxySpec.js b/platform/features/layout/test/FixedProxySpec.js index 224b19f022..dde97fa142 100644 --- a/platform/features/layout/test/FixedProxySpec.js +++ b/platform/features/layout/test/FixedProxySpec.js @@ -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 + }); + }); + }); } ); diff --git a/platform/features/layout/test/elements/ElementFactorySpec.js b/platform/features/layout/test/elements/ElementFactorySpec.js new file mode 100644 index 0000000000..4dec7a4357 --- /dev/null +++ b/platform/features/layout/test/elements/ElementFactorySpec.js @@ -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(); + }); + }); + }); + } +); \ No newline at end of file diff --git a/platform/features/layout/test/elements/ElementProxiesSpec.js b/platform/features/layout/test/elements/ElementProxiesSpec.js index cf771a043a..1e58fb8826 100644 --- a/platform/features/layout/test/elements/ElementProxiesSpec.js +++ b/platform/features/layout/test/elements/ElementProxiesSpec.js @@ -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 diff --git a/platform/features/layout/test/elements/LineProxySpec.js b/platform/features/layout/test/elements/LineProxySpec.js new file mode 100644 index 0000000000..fe38c7c3c9 --- /dev/null +++ b/platform/features/layout/test/elements/LineProxySpec.js @@ -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); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/features/layout/test/suite.json b/platform/features/layout/test/suite.json index af82513e01..c895900595 100644 --- a/platform/features/layout/test/suite.json +++ b/platform/features/layout/test/suite.json @@ -5,7 +5,9 @@ "LayoutDrag", "LayoutSelection", "elements/AccessorMutator", + "elements/ElementFactory", "elements/ElementProxies", "elements/ElementProxy", + "elements/LineProxy", "elements/TelemetryProxy" ] \ No newline at end of file diff --git a/platform/forms/bundle.json b/platform/forms/bundle.json index d8e3730eca..eb63e22432 100644 --- a/platform/forms/bundle.json +++ b/platform/forms/bundle.json @@ -45,6 +45,10 @@ { "key": "composite", "templateUrl": "templates/controls/composite.html" + }, + { + "key": "menu-button", + "templateUrl": "templates/controls/menu-button.html" } ], "controllers": [ diff --git a/platform/forms/res/templates/controls/menu-button.html b/platform/forms/res/templates/controls/menu-button.html new file mode 100644 index 0000000000..cb6b7e565b --- /dev/null +++ b/platform/forms/res/templates/controls/menu-button.html @@ -0,0 +1,27 @@ + \ No newline at end of file