Simon Hunt

GUI -- TopoView - re-implemented Quick Help panel.

Change-Id: I92edeb570a97eff87a5f9b08373ff0517849bf24
1 +/*
2 + * Copyright 2014,2015 Open Networking Laboratory
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + */
16 +
17 +/*
18 + ONOS GUI -- Quick Help Service -- CSS file
19 + */
20 +
21 +#quickhelp {
22 + z-index: 1300;
23 +}
24 +
25 +#quickhelp svg {
26 + position: absolute;
27 + top: 180px;
28 + opacity: 1;
29 +}
30 +
31 +#quickhelp svg g.help rect {
32 + fill: black;
33 + opacity: 0.7;
34 +}
35 +
36 +#quickhelp svg text.title {
37 + font-size: 10pt;
38 + font-style: italic;
39 + text-anchor: middle;
40 + fill: #999;
41 +}
42 +
43 +#quickhelp svg g.keyItem {
44 + fill: white;
45 +}
46 +
47 +#quickhelp svg g line.qhrowsep {
48 + stroke: #888;
49 + stroke-dasharray: 2 2;
50 +}
51 +
52 +#quickhelp svg text {
53 + font-size: 7pt;
54 + alignment-baseline: middle;
55 +}
56 +
57 +#quickhelp svg text.key {
58 + fill: #add;
59 +}
60 +
61 +#quickhelp svg text.desc {
62 + fill: #ddd;
63 +}
64 +
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + */
16 +
17 +/*
18 + ONOS GUI -- Layer -- Quick Help Service
19 +
20 + Provides a mechanism to display key bindings and mouse gesture notes.
21 + */
22 +(function () {
23 + 'use strict';
24 +
25 + // injected references
26 + var $log, fs, sus;
27 +
28 + // configuration
29 + var defaultSettings = {
30 + fade: 500
31 + },
32 + w = '100%',
33 + h = '80%',
34 + vbox = '-200 0 400 400',
35 + pad = 10,
36 + offy = 45,
37 + sepYDelta = 20,
38 + colXDelta = 16,
39 + yTextSpc = 12,
40 + offDesc = 8;
41 +
42 + // internal state
43 + var settings,
44 + data = [],
45 + yCount;
46 +
47 + // DOM elements
48 + var qhdiv, svg, pane, rect, items;
49 +
50 + // key-logical-name to key-display lookup..
51 + var keyDisp = {
52 + equals: '=',
53 + dash: '-',
54 + slash: '/',
55 + backSlash: '\\',
56 + backQuote: '`',
57 + leftArrow: 'L-arrow',
58 + upArrow: 'U-arrow',
59 + rightArrow: 'R-arrow',
60 + downArrow: 'D-arrow'
61 + };
62 +
63 + // ===========================================
64 + // === Function Definitions ===
65 +
66 +
67 + // TODO: move this to FnService.
68 + function cap(s) {
69 + return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); });
70 + }
71 +
72 +
73 + function mkKeyDisp(id) {
74 + var v = keyDisp[id] || id;
75 + return cap(v);
76 + }
77 +
78 + function addSeparator(el, i) {
79 + var y = sepYDelta/2 - 5;
80 + el.append('line')
81 + .attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y });
82 + }
83 +
84 + function addContent(el, data, ri) {
85 + var xCount = 0,
86 + clsPfx = 'qh-r' + ri + '-c';
87 +
88 + function addColumn(el, c, i) {
89 + var cls = clsPfx + i,
90 + oy = 0,
91 + aggKey = el.append('g').attr('visibility', 'hidden'),
92 + gcol = el.append('g').attr({
93 + 'class': cls,
94 + transform: sus.translate(xCount, 0)
95 + });
96 +
97 + c.forEach(function (j) {
98 + var k = j[0],
99 + v = j[1];
100 +
101 + if (k !== '-') {
102 + aggKey.append('text').text(k);
103 +
104 + gcol.append('text').text(k)
105 + .attr({
106 + 'class': 'key',
107 + y: oy
108 + });
109 + gcol.append('text').text(v)
110 + .attr({
111 + 'class': 'desc',
112 + y: oy
113 + });
114 + }
115 +
116 + oy += yTextSpc;
117 + });
118 +
119 + // adjust position of descriptions, based on widest key
120 + var kbox = aggKey.node().getBBox(),
121 + ox = kbox.width + offDesc;
122 + gcol.selectAll('.desc').attr('x', ox);
123 + aggKey.remove();
124 +
125 + // now update x-offset for next column
126 + var bbox = gcol.node().getBBox();
127 + xCount += bbox.width + colXDelta;
128 + }
129 +
130 + data.forEach(function (d, i) {
131 + addColumn(el, d, i);
132 + });
133 +
134 + // finally, return the height of the row..
135 + return el.node().getBBox().height;
136 + }
137 +
138 + function updateKeyItems() {
139 + var rows = items.selectAll('.qhRow').data(data);
140 +
141 + yCount = offy;
142 +
143 + var entering = rows.enter()
144 + .append('g')
145 + .attr({
146 + 'class': 'qhrow'
147 + });
148 +
149 + entering.each(function (r, i) {
150 + var el = d3.select(this),
151 + sep = r.type === 'sep',
152 + dy;
153 +
154 + el.attr('transform', sus.translate(0, yCount));
155 +
156 + if (sep) {
157 + addSeparator(el, i);
158 + yCount += sepYDelta;
159 + } else {
160 + dy = addContent(el, r.data, i);
161 + yCount += dy;
162 + }
163 + });
164 +
165 + // size the backing rectangle
166 + var ibox = items.node().getBBox(),
167 + paneW = ibox.width + pad * 2,
168 + paneH = ibox.height + offy;
169 +
170 + items.selectAll('.qhrowsep').attr('x2', ibox.width);
171 + items.attr('transform', sus.translate(-paneW/2, -pad));
172 + rect.attr({
173 + width: paneW,
174 + height: paneH,
175 + transform: sus.translate(-paneW/2-pad, 0)
176 + });
177 +
178 + }
179 +
180 + function checkFmt(fmt) {
181 + // should be a single array of keys,
182 + // or array of arrays of keys (one per column).
183 + // return null if there is a problem.
184 + var a = fs.isA(fmt),
185 + n = a && a.length,
186 + ns = 0,
187 + na = 0;
188 +
189 + if (n) {
190 + // it is an array which has some content
191 + a.forEach(function (d) {
192 + fs.isA(d) && na++;
193 + fs.isS(d) && ns++;
194 + });
195 + if (na === n || ns === n) {
196 + // all arrays or all strings...
197 + return a;
198 + }
199 + }
200 + return null;
201 + }
202 +
203 + function buildBlock(map, fmt) {
204 + var b = [];
205 + fmt.forEach(function (k) {
206 + var v = map.get(k),
207 + a = fs.isA(v),
208 + d = (a && a[1]);
209 +
210 + // '-' marks a separator; d is the description
211 + if (k === '-' || d) {
212 + b.push([mkKeyDisp(k), d]);
213 + }
214 + });
215 + return b;
216 + }
217 +
218 + function emptyRow() {
219 + return { type: 'row', data: [] };
220 + }
221 +
222 + function mkArrRow(fmt) {
223 + var d = emptyRow();
224 + d.data.push(fmt);
225 + return d;
226 + }
227 +
228 + function mkColumnarRow(map, fmt) {
229 + var d = emptyRow();
230 + fmt.forEach(function (a) {
231 + d.data.push(buildBlock(map, a));
232 + });
233 + return d;
234 + }
235 +
236 + function mkMapRow(map, fmt) {
237 + var d = emptyRow();
238 + d.data.push(buildBlock(map, fmt));
239 + return d;
240 + }
241 +
242 + function addRow(row) {
243 + var d = row || { type: 'sep' };
244 + data.push(d);
245 + }
246 +
247 + function aggregateData(bindings) {
248 + var hf = '_helpFormat',
249 + gmap = d3.map(bindings.globalKeys),
250 + gfmt = bindings.globalFormat,
251 + vmap = d3.map(bindings.viewKeys),
252 + vgest = bindings.viewGestures,
253 + vfmt, vkeys;
254 +
255 + // filter out help format entry
256 + vfmt = checkFmt(vmap.get(hf));
257 + vmap.remove(hf);
258 +
259 + // if bad (or no) format, fallback to sorted keys
260 + if (!vfmt) {
261 + vkeys = vmap.keys();
262 + vfmt = vkeys.sort();
263 + }
264 +
265 + data = [];
266 +
267 + addRow(mkMapRow(gmap, gfmt));
268 + addRow();
269 + addRow(fs.isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt));
270 + addRow();
271 + addRow(mkArrRow(vgest));
272 + }
273 +
274 +
275 + function popBind(bindings) {
276 + pane = svg.append('g')
277 + .attr({
278 + class: 'help',
279 + opacity: 0
280 + });
281 +
282 + rect = pane.append('rect')
283 + .attr('rx', 8);
284 +
285 + pane.append('text')
286 + .text('Quick Help')
287 + .attr({
288 + class: 'title',
289 + dy: '1.2em',
290 + transform: sus.translate(-pad,0)
291 + });
292 +
293 + items = pane.append('g');
294 + aggregateData(bindings);
295 + updateKeyItems();
296 +
297 + _fade(1);
298 + }
299 +
300 + function fadeBindings() {
301 + _fade(0);
302 + }
303 +
304 + function _fade(o) {
305 + svg.selectAll('g.help')
306 + .transition()
307 + .duration(settings.fade)
308 + .attr('opacity', o);
309 + }
310 +
311 + function addSvg() {
312 + svg = qhdiv.append('svg')
313 + .attr({
314 + width: w,
315 + height: h,
316 + viewBox: vbox
317 + });
318 + }
319 +
320 + function removeSvg() {
321 + svg.transition()
322 + .delay(settings.fade + 20)
323 + .remove();
324 + }
325 +
326 +
327 + // ===========================================
328 + // === Module Definition ===
329 +
330 + angular.module('onosLayer')
331 + .factory('QuickHelpService',
332 + ['$log', 'FnService', 'SvgUtilService',
333 +
334 + function (_$log_, _fs_, _sus_) {
335 + $log = _$log_;
336 + fs = _fs_;
337 + sus = _sus_;
338 +
339 + function initQuickHelp(opts) {
340 + settings = angular.extend({}, defaultSettings, opts);
341 + qhdiv = d3.select('#quickhelp');
342 + }
343 +
344 + function showQuickHelp(bindings) {
345 + svg = qhdiv.select('svg');
346 + if (svg.empty()) {
347 + addSvg();
348 + popBind(bindings);
349 + } else {
350 + hideQuickHelp();
351 + }
352 + }
353 +
354 + function hideQuickHelp() {
355 + svg = qhdiv.select('svg');
356 + if (!svg.empty()) {
357 + fadeBindings();
358 + removeSvg();
359 + return true;
360 + }
361 + return false;
362 + }
363 +
364 + return {
365 + initQuickHelp: initQuickHelp,
366 + showQuickHelp: showQuickHelp,
367 + hideQuickHelp: hideQuickHelp
368 + };
369 + }]);
370 +
371 +}());
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
21 'use strict'; 21 'use strict';
22 22
23 // references to injected services 23 // references to injected services
24 - var $log, fs, ts; 24 + var $log, fs, ts, qhs;
25 25
26 // internal state 26 // internal state
27 var enabled = true, 27 var enabled = true,
...@@ -115,22 +115,13 @@ ...@@ -115,22 +115,13 @@
115 } 115 }
116 116
117 function quickHelp(view, key, code, ev) { 117 function quickHelp(view, key, code, ev) {
118 - // TODO: show quick help 118 + qhs.showQuickHelp(keyHandler);
119 - // delegate to QuickHelp service.
120 - //libApi.quickHelp.show(keyHandler);
121 - console.log('QUICK-HELP');
122 return true; 119 return true;
123 } 120 }
124 121
125 // returns true if we 'consumed' the ESC keypress, false otherwise 122 // returns true if we 'consumed' the ESC keypress, false otherwise
126 function escapeKey(view, key, code, ev) { 123 function escapeKey(view, key, code, ev) {
127 - // TODO: plumb in handling of quick help dismissal 124 + return qhs.hideQuickHelp();
128 -/*
129 - if (qh.hide()) {
130 - return true;
131 - }
132 -*/
133 - return false;
134 } 125 }
135 126
136 function toggleTheme(view, key, code, ev) { 127 function toggleTheme(view, key, code, ev) {
...@@ -176,13 +167,18 @@ ...@@ -176,13 +167,18 @@
176 } 167 }
177 168
178 angular.module('onosUtil') 169 angular.module('onosUtil')
179 - .factory('KeyService', ['$log', 'FnService', 'ThemeService', 170 + .factory('KeyService',
171 + ['$log', 'FnService', 'ThemeService',
172 +
180 function (_$log_, _fs_, _ts_) { 173 function (_$log_, _fs_, _ts_) {
181 $log = _$log_; 174 $log = _$log_;
182 fs = _fs_; 175 fs = _fs_;
183 ts = _ts_; 176 ts = _ts_;
184 177
185 return { 178 return {
179 + bindQhs: function (_qhs_) {
180 + qhs = _qhs_;
181 + },
186 installOn: function (elem) { 182 installOn: function (elem) {
187 elem.on('keydown', keyIn); 183 elem.on('keydown', keyIn);
188 setupGlobalKeys(); 184 setupGlobalKeys();
......
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
63 <script src="fw/layer/layer.js"></script> 63 <script src="fw/layer/layer.js"></script>
64 <script src="fw/layer/panel.js"></script> 64 <script src="fw/layer/panel.js"></script>
65 <script src="fw/layer/flash.js"></script> 65 <script src="fw/layer/flash.js"></script>
66 + <script src="fw/layer/quickhelp.js"></script>
66 <script src="fw/layer/veil.js"></script> 67 <script src="fw/layer/veil.js"></script>
67 68
68 <!-- Framework and library stylesheets included here --> 69 <!-- Framework and library stylesheets included here -->
...@@ -74,6 +75,7 @@ ...@@ -74,6 +75,7 @@
74 <link rel="stylesheet" href="fw/svg/icon.css"> 75 <link rel="stylesheet" href="fw/svg/icon.css">
75 <link rel="stylesheet" href="fw/layer/panel.css"> 76 <link rel="stylesheet" href="fw/layer/panel.css">
76 <link rel="stylesheet" href="fw/layer/flash.css"> 77 <link rel="stylesheet" href="fw/layer/flash.css">
78 + <link rel="stylesheet" href="fw/layer/quickhelp.css">
77 <link rel="stylesheet" href="fw/layer/veil.css"> 79 <link rel="stylesheet" href="fw/layer/veil.css">
78 <link rel="stylesheet" href="fw/nav/nav.css"> 80 <link rel="stylesheet" href="fw/nav/nav.css">
79 81
......
...@@ -65,9 +65,10 @@ ...@@ -65,9 +65,10 @@
65 .controller('OnosCtrl', [ 65 .controller('OnosCtrl', [
66 '$log', '$route', '$routeParams', '$location', 66 '$log', '$route', '$routeParams', '$location',
67 'KeyService', 'ThemeService', 'GlyphService', 'PanelService', 67 'KeyService', 'ThemeService', 'GlyphService', 'PanelService',
68 - 'FlashService', 68 + 'FlashService', 'QuickHelpService',
69 69
70 - function ($log, $route, $routeParams, $location, ks, ts, gs, ps, flash) { 70 + function ($log, $route, $routeParams, $location,
71 + ks, ts, gs, ps, flash, qhs) {
71 var self = this; 72 var self = this;
72 73
73 self.$route = $route; 74 self.$route = $route;
...@@ -78,9 +79,11 @@ ...@@ -78,9 +79,11 @@
78 // initialize services... 79 // initialize services...
79 ts.init(); 80 ts.init();
80 ks.installOn(d3.select('body')); 81 ks.installOn(d3.select('body'));
82 + ks.bindQhs(qhs);
81 gs.init(); 83 gs.init();
82 ps.init(); 84 ps.init();
83 flash.initFlash(); 85 flash.initFlash();
86 + qhs.initQuickHelp();
84 87
85 $log.log('OnosCtrl has been created'); 88 $log.log('OnosCtrl has been created');
86 89
......
...@@ -75,14 +75,13 @@ ...@@ -75,14 +75,13 @@
75 ] 75 ]
76 }); 76 });
77 77
78 - // TODO: // mouse gestures 78 + ks.gestureNotes([
79 - var gestures = [
80 ['click', 'Select the item and show details'], 79 ['click', 'Select the item and show details'],
81 ['shift-click', 'Toggle selection state'], 80 ['shift-click', 'Toggle selection state'],
82 ['drag', 'Reposition (and pin) device / host'], 81 ['drag', 'Reposition (and pin) device / host'],
83 ['cmd-scroll', 'Zoom in / out'], 82 ['cmd-scroll', 'Zoom in / out'],
84 ['cmd-drag', 'Pan'] 83 ['cmd-drag', 'Pan']
85 - ]; 84 + ]);
86 } 85 }
87 86
88 // --- Keystroke functions ------------------------------------------- 87 // --- Keystroke functions -------------------------------------------
......
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + */
16 +
17 +/*
18 + ONOS GUI -- Layer -- Flash Service - Unit Tests
19 + */
20 +describe('factory: fw/layer/quickhelp.js', function () {
21 + var $log, $timeout, fs, qhs, d3Elem;
22 +
23 + beforeEach(module('onosUtil', 'onosSvg', 'onosLayer'));
24 +
25 + beforeEach(inject(function (_$log_, _$timeout_, FnService, QuickHelpService) {
26 + $log = _$log_;
27 + //$timeout = _$timeout_;
28 + fs = FnService;
29 + qhs = QuickHelpService;
30 + //jasmine.clock().install();
31 + d3Elem = d3.select('body').append('div').attr('id', 'myqhdiv');
32 + }));
33 +
34 + afterEach(function () {
35 + //jasmine.clock().uninstall();
36 + d3.select('#myqhdiv').remove();
37 + });
38 +
39 + function helpItemSelection() {
40 + return d3Elem.selectAll('.help');
41 + }
42 +
43 + it('should define QuickHelpService', function () {
44 + expect(qhs).toBeDefined();
45 + });
46 +
47 + it('should define api functions', function () {
48 + expect(fs.areFunctions(qhs, [
49 + 'initQuickHelp', 'showQuickHelp', 'hideQuickHelp'
50 + ])).toBeTruthy();
51 + });
52 +
53 + it('should have no items to start', function () {
54 + expect(helpItemSelection().size()).toBe(0);
55 + });
56 +
57 + // TODO: check that the help stuff appears
58 +/*
59 + it('should show help items', function () {
60 + var item, rect, text;
61 + flash.flash('foo');
62 + //jasmine.clock().tick(101);
63 + setTimeout(function () {
64 + item = flashItemSelection();
65 + expect(item.size()).toEqual(1);
66 + expect(item.classed('flashItem')).toBeTruthy();
67 + expect(item.select('rect').size()).toEqual(1);
68 + text = item.select('text');
69 + expect(text.size()).toEqual(1);
70 + expect(text.text()).toEqual('foo');
71 + }, 100);
72 + });
73 +*/
74 +});
75 +
...@@ -18,19 +18,21 @@ ...@@ -18,19 +18,21 @@
18 ONOS GUI -- Key Handler Service - Unit Tests 18 ONOS GUI -- Key Handler Service - Unit Tests
19 */ 19 */
20 describe('factory: fw/util/keys.js', function() { 20 describe('factory: fw/util/keys.js', function() {
21 - var $log, ks, fs, 21 + var $log, ks, fs, qhs,
22 d3Elem, elem, last; 22 d3Elem, elem, last;
23 23
24 24
25 - beforeEach(module('onosUtil')); 25 + beforeEach(module('onosUtil', 'onosSvg', 'onosLayer'));
26 26
27 - beforeEach(inject(function (_$log_, KeyService, FnService) { 27 + beforeEach(inject(function (_$log_, KeyService, FnService, QuickHelpService) {
28 $log = _$log_; 28 $log = _$log_;
29 ks = KeyService; 29 ks = KeyService;
30 fs = FnService; 30 fs = FnService;
31 + qhs = QuickHelpService;
31 d3Elem = d3.select('body').append('p').attr('id', 'ptest'); 32 d3Elem = d3.select('body').append('p').attr('id', 'ptest');
32 elem = d3Elem.node(); 33 elem = d3Elem.node();
33 ks.installOn(d3Elem); 34 ks.installOn(d3Elem);
35 + ks.bindQhs(qhs);
34 last = { 36 last = {
35 view: null, 37 view: null,
36 key: null, 38 key: null,
...@@ -49,7 +51,7 @@ describe('factory: fw/util/keys.js', function() { ...@@ -49,7 +51,7 @@ describe('factory: fw/util/keys.js', function() {
49 51
50 it('should define api functions', function () { 52 it('should define api functions', function () {
51 expect(fs.areFunctions(ks, [ 53 expect(fs.areFunctions(ks, [
52 - 'installOn', 'keyBindings', 'gestureNotes', 'enableKeys' 54 + 'bindQhs', 'installOn', 'keyBindings', 'gestureNotes', 'enableKeys'
53 ])).toBeTruthy(); 55 ])).toBeTruthy();
54 }); 56 });
55 57
......