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