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