blob: a25cf68d65ed6f2b5ab635694f58fe2cc50a11c3 [file] [log] [blame]
Simon Hunt639dc662015-02-18 14:19:20 -08001/*
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: '=',
Simon Hunt639dc662015-02-18 14:19:20 -080053 slash: '/',
54 backSlash: '\\',
55 backQuote: '`',
56 leftArrow: 'L-arrow',
57 upArrow: 'U-arrow',
58 rightArrow: 'R-arrow',
59 downArrow: 'D-arrow'
60 };
61
Bri Prebilic Cole2efc7152015-04-29 15:47:06 -070062 // list of needed bindings to use in aggregateData
63 var neededBindings = [
64 'globalKeys', 'globalFormat', 'viewKeys', 'viewGestures'
65 ];
66
Simon Hunt639dc662015-02-18 14:19:20 -080067 // ===========================================
68 // === Function Definitions ===
69
Simon Hunt639dc662015-02-18 14:19:20 -080070 function mkKeyDisp(id) {
71 var v = keyDisp[id] || id;
Simon Hunt27a5cc82015-02-19 14:49:55 -080072 return fs.cap(v);
Simon Hunt639dc662015-02-18 14:19:20 -080073 }
74
75 function addSeparator(el, i) {
76 var y = sepYDelta/2 - 5;
77 el.append('line')
78 .attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y });
79 }
80
81 function addContent(el, data, ri) {
82 var xCount = 0,
83 clsPfx = 'qh-r' + ri + '-c';
84
85 function addColumn(el, c, i) {
86 var cls = clsPfx + i,
87 oy = 0,
88 aggKey = el.append('g').attr('visibility', 'hidden'),
89 gcol = el.append('g').attr({
90 'class': cls,
91 transform: sus.translate(xCount, 0)
92 });
93
94 c.forEach(function (j) {
95 var k = j[0],
96 v = j[1];
97
98 if (k !== '-') {
99 aggKey.append('text').text(k);
100
101 gcol.append('text').text(k)
102 .attr({
103 'class': 'key',
104 y: oy
105 });
106 gcol.append('text').text(v)
107 .attr({
108 'class': 'desc',
109 y: oy
110 });
111 }
112
113 oy += yTextSpc;
114 });
115
116 // adjust position of descriptions, based on widest key
117 var kbox = aggKey.node().getBBox(),
118 ox = kbox.width + offDesc;
119 gcol.selectAll('.desc').attr('x', ox);
120 aggKey.remove();
121
122 // now update x-offset for next column
123 var bbox = gcol.node().getBBox();
124 xCount += bbox.width + colXDelta;
125 }
126
127 data.forEach(function (d, i) {
128 addColumn(el, d, i);
129 });
130
131 // finally, return the height of the row..
132 return el.node().getBBox().height;
133 }
134
135 function updateKeyItems() {
136 var rows = items.selectAll('.qhRow').data(data);
137
138 yCount = offy;
139
140 var entering = rows.enter()
141 .append('g')
142 .attr({
143 'class': 'qhrow'
144 });
145
146 entering.each(function (r, i) {
147 var el = d3.select(this),
148 sep = r.type === 'sep',
149 dy;
150
151 el.attr('transform', sus.translate(0, yCount));
152
153 if (sep) {
154 addSeparator(el, i);
155 yCount += sepYDelta;
156 } else {
157 dy = addContent(el, r.data, i);
158 yCount += dy;
159 }
160 });
161
162 // size the backing rectangle
163 var ibox = items.node().getBBox(),
164 paneW = ibox.width + pad * 2,
165 paneH = ibox.height + offy;
166
167 items.selectAll('.qhrowsep').attr('x2', ibox.width);
168 items.attr('transform', sus.translate(-paneW/2, -pad));
169 rect.attr({
170 width: paneW,
171 height: paneH,
172 transform: sus.translate(-paneW/2-pad, 0)
173 });
174
175 }
176
177 function checkFmt(fmt) {
178 // should be a single array of keys,
179 // or array of arrays of keys (one per column).
180 // return null if there is a problem.
181 var a = fs.isA(fmt),
182 n = a && a.length,
183 ns = 0,
184 na = 0;
185
186 if (n) {
187 // it is an array which has some content
188 a.forEach(function (d) {
189 fs.isA(d) && na++;
190 fs.isS(d) && ns++;
191 });
192 if (na === n || ns === n) {
193 // all arrays or all strings...
194 return a;
195 }
196 }
197 return null;
198 }
199
200 function buildBlock(map, fmt) {
201 var b = [];
202 fmt.forEach(function (k) {
203 var v = map.get(k),
204 a = fs.isA(v),
205 d = (a && a[1]);
206
207 // '-' marks a separator; d is the description
208 if (k === '-' || d) {
209 b.push([mkKeyDisp(k), d]);
210 }
211 });
212 return b;
213 }
214
215 function emptyRow() {
216 return { type: 'row', data: [] };
217 }
218
219 function mkArrRow(fmt) {
220 var d = emptyRow();
221 d.data.push(fmt);
222 return d;
223 }
224
225 function mkColumnarRow(map, fmt) {
226 var d = emptyRow();
227 fmt.forEach(function (a) {
228 d.data.push(buildBlock(map, a));
229 });
230 return d;
231 }
232
233 function mkMapRow(map, fmt) {
234 var d = emptyRow();
235 d.data.push(buildBlock(map, fmt));
236 return d;
237 }
238
239 function addRow(row) {
240 var d = row || { type: 'sep' };
241 data.push(d);
242 }
243
244 function aggregateData(bindings) {
245 var hf = '_helpFormat',
246 gmap = d3.map(bindings.globalKeys),
247 gfmt = bindings.globalFormat,
248 vmap = d3.map(bindings.viewKeys),
249 vgest = bindings.viewGestures,
250 vfmt, vkeys;
251
252 // filter out help format entry
253 vfmt = checkFmt(vmap.get(hf));
254 vmap.remove(hf);
255
256 // if bad (or no) format, fallback to sorted keys
257 if (!vfmt) {
258 vkeys = vmap.keys();
259 vfmt = vkeys.sort();
260 }
261
262 data = [];
263
264 addRow(mkMapRow(gmap, gfmt));
265 addRow();
266 addRow(fs.isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt));
267 addRow();
268 addRow(mkArrRow(vgest));
269 }
270
271
272 function popBind(bindings) {
273 pane = svg.append('g')
274 .attr({
275 class: 'help',
276 opacity: 0
277 });
278
279 rect = pane.append('rect')
280 .attr('rx', 8);
281
282 pane.append('text')
283 .text('Quick Help')
284 .attr({
285 class: 'title',
286 dy: '1.2em',
287 transform: sus.translate(-pad,0)
288 });
289
290 items = pane.append('g');
291 aggregateData(bindings);
292 updateKeyItems();
293
294 _fade(1);
295 }
296
297 function fadeBindings() {
298 _fade(0);
299 }
300
301 function _fade(o) {
302 svg.selectAll('g.help')
303 .transition()
304 .duration(settings.fade)
305 .attr('opacity', o);
306 }
307
308 function addSvg() {
309 svg = qhdiv.append('svg')
310 .attr({
311 width: w,
312 height: h,
313 viewBox: vbox
314 });
315 }
316
317 function removeSvg() {
318 svg.transition()
319 .delay(settings.fade + 20)
320 .remove();
321 }
322
Bri Prebilic Cole2efc7152015-04-29 15:47:06 -0700323 function goodBindings(bindings) {
324 var warnPrefix = 'Quickhelp Service: showQuickHelp(), ';
325 if (!bindings || !fs.isO(bindings) || fs.isEmptyObject(bindings)) {
326 $log.warn(warnPrefix + 'invalid bindings object');
327 return false;
328 }
329 if (!(neededBindings.every(function (key) { return key in bindings; }))) {
330 $log.warn(
331 warnPrefix +
332 'needed bindings for help panel not provided:',
333 neededBindings
334 );
335 return false
336 }
337 return true;
338 }
Simon Hunt639dc662015-02-18 14:19:20 -0800339
340 // ===========================================
341 // === Module Definition ===
342
343 angular.module('onosLayer')
344 .factory('QuickHelpService',
345 ['$log', 'FnService', 'SvgUtilService',
346
347 function (_$log_, _fs_, _sus_) {
348 $log = _$log_;
349 fs = _fs_;
350 sus = _sus_;
351
352 function initQuickHelp(opts) {
Bri Prebilic Cole2efc7152015-04-29 15:47:06 -0700353 settings = angular.extend({}, defaultSettings, fs.isO(opts));
Simon Hunt639dc662015-02-18 14:19:20 -0800354 qhdiv = d3.select('#quickhelp');
355 }
356
357 function showQuickHelp(bindings) {
358 svg = qhdiv.select('svg');
Bri Prebilic Cole2efc7152015-04-29 15:47:06 -0700359 if (!goodBindings(bindings)) {
360 return null;
361 }
Simon Hunt639dc662015-02-18 14:19:20 -0800362 if (svg.empty()) {
363 addSvg();
364 popBind(bindings);
365 } else {
366 hideQuickHelp();
367 }
368 }
369
370 function hideQuickHelp() {
371 svg = qhdiv.select('svg');
372 if (!svg.empty()) {
373 fadeBindings();
374 removeSvg();
375 return true;
376 }
377 return false;
378 }
379
380 return {
381 initQuickHelp: initQuickHelp,
382 showQuickHelp: showQuickHelp,
383 hideQuickHelp: hideQuickHelp
384 };
385 }]);
386
387}());