blob: 5ef4369931a5968722c5e32e9589bccbc8ec04b9 [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Layer -- Quick Help Service
Provides a mechanism to display key bindings and mouse gesture notes.
*/
(function () {
'use strict';
// injected references
var $log, fs, sus, ls;
// configuration
var defaultSettings = {
fade: 500
},
w = '100%',
h = '80%',
vbox = '-200 0 400 400',
pad = 10,
offy = 45,
sepYDelta = 20,
colXDelta = 16,
yTextSpc = 12,
offDesc = 8;
// internal state
var settings,
data = [],
yCount;
// DOM elements
var qhdiv, svg, pane, rect, items;
// key-logical-name to key-display lookup..
var keyDisp = {
equals: '=',
slash: '/',
backSlash: '\\',
backQuote: '`',
leftArrow: 'L-arrow',
upArrow: 'U-arrow',
rightArrow: 'R-arrow',
downArrow: 'D-arrow'
};
// list of needed bindings to use in aggregateData
var neededBindings = [
'globalKeys', 'globalFormat', 'viewKeys', 'viewGestures'
];
// ===========================================
// === Function Definitions ===
function mkKeyDisp(id) {
var v = keyDisp[id] || id;
return fs.cap(v);
}
function addSeparator(el, i) {
var y = sepYDelta/2 - 5;
el.append('line')
.attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y });
}
function addContent(el, data, ri) {
var xCount = 0,
clsPfx = 'qh-r' + ri + '-c';
function addColumn(el, c, i) {
var cls = clsPfx + i,
oy = 0,
aggKey = el.append('g').attr('visibility', 'hidden'),
gcol = el.append('g').attr({
'class': cls,
transform: sus.translate(xCount, 0)
});
c.forEach(function (j) {
var k = j[0],
v = j[1];
if (k !== '-') {
aggKey.append('text').text(k);
gcol.append('text').text(k)
.attr({
'class': 'key',
y: oy
});
gcol.append('text').text(v)
.attr({
'class': 'desc',
y: oy
});
}
oy += yTextSpc;
});
// adjust position of descriptions, based on widest key
var kbox = aggKey.node().getBBox(),
ox = kbox.width + offDesc;
gcol.selectAll('.desc').attr('x', ox);
aggKey.remove();
// now update x-offset for next column
var bbox = gcol.node().getBBox();
xCount += bbox.width + colXDelta;
}
data.forEach(function (d, i) {
addColumn(el, d, i);
});
// finally, return the height of the row..
return el.node().getBBox().height;
}
function updateKeyItems() {
var rows = items.selectAll('.qhRow').data(data);
yCount = offy;
var entering = rows.enter()
.append('g')
.attr({
'class': 'qhrow'
});
entering.each(function (r, i) {
var el = d3.select(this),
sep = r.type === 'sep',
dy;
el.attr('transform', sus.translate(0, yCount));
if (sep) {
addSeparator(el, i);
yCount += sepYDelta;
} else {
dy = addContent(el, r.data, i);
yCount += dy;
}
});
// size the backing rectangle
var ibox = items.node().getBBox(),
paneW = ibox.width + pad * 2,
paneH = ibox.height + offy;
items.selectAll('.qhrowsep').attr('x2', ibox.width);
items.attr('transform', sus.translate(-paneW/2, -pad));
rect.attr({
width: paneW,
height: paneH,
transform: sus.translate(-paneW/2-pad, 0)
});
}
function checkFmt(fmt) {
// should be a single array of keys,
// or array of arrays of keys (one per column).
// return null if there is a problem.
var a = fs.isA(fmt),
n = a && a.length,
ns = 0,
na = 0;
if (n) {
// it is an array which has some content
a.forEach(function (d) {
fs.isA(d) && na++;
fs.isS(d) && ns++;
});
if (na === n || ns === n) {
// all arrays or all strings...
return a;
}
}
return null;
}
function buildBlock(map, fmt) {
var b = [];
fmt.forEach(function (k) {
var v = map.get(k),
a = fs.isA(v),
d = (a && a[1]),
dfn = fs.isF(d),
dval = (dfn && dfn()) || d;
// '-' marks a separator; d is the description
if (k === '-' || dval) {
b.push([mkKeyDisp(k), dval]);
}
});
return b;
}
function emptyRow() {
return { type: 'row', data: [] };
}
function mkArrRow(fmt) {
var d = emptyRow();
d.data.push(fmt);
return d;
}
function mkColumnarRow(map, fmt) {
var d = emptyRow();
fmt.forEach(function (a) {
d.data.push(buildBlock(map, a));
});
return d;
}
function mkMapRow(map, fmt) {
var d = emptyRow();
d.data.push(buildBlock(map, fmt));
return d;
}
function addRow(row) {
var d = row || { type: 'sep' };
data.push(d);
}
function aggregateData(bindings) {
var hf = '_helpFormat',
gmap = d3.map(bindings.globalKeys),
gfmt = bindings.globalFormat,
vmap = d3.map(bindings.viewKeys),
vgest = bindings.viewGestures,
vfmt, vkeys;
// filter out help format entry
vfmt = checkFmt(vmap.get(hf));
vmap.remove(hf);
// if bad (or no) format, fallback to sorted keys
if (!vfmt) {
vkeys = vmap.keys();
vfmt = vkeys.sort();
}
data = [];
addRow(mkMapRow(gmap, gfmt));
addRow();
addRow(fs.isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt));
addRow();
addRow(mkArrRow(vgest));
}
function qhlion_title() {
var lion = ls.bundle('core.fw.QuickHelp');
return lion('qh_title');
}
function popBind(bindings) {
pane = svg.append('g')
.attr({
class: 'help',
opacity: 0
});
rect = pane.append('rect')
.attr('rx', 8);
pane.append('text')
.text(qhlion_title())
.attr({
class: 'title',
dy: '1.2em',
transform: sus.translate(-pad,0)
});
items = pane.append('g');
aggregateData(bindings);
updateKeyItems();
_fade(1);
}
function fadeBindings() {
_fade(0);
}
function _fade(o) {
svg.selectAll('g.help')
.transition()
.duration(settings.fade)
.attr('opacity', o);
}
function addSvg() {
svg = qhdiv.append('svg')
.attr({
width: w,
height: h,
viewBox: vbox
});
}
function removeSvg() {
svg.transition()
.delay(settings.fade + 20)
.remove();
}
function goodBindings(bindings) {
var warnPrefix = 'Quickhelp Service: showQuickHelp(), ';
if (!bindings || !fs.isO(bindings) || fs.isEmptyObject(bindings)) {
$log.warn(warnPrefix + 'invalid bindings object');
return false;
}
if (!(neededBindings.every(function (key) { return key in bindings; }))) {
$log.warn(
warnPrefix +
'needed bindings for help panel not provided:',
neededBindings
);
return false
}
return true;
}
// ===========================================
// === Module Definition ===
angular.module('onosLayer')
.factory('QuickHelpService',
['$log', 'FnService', 'SvgUtilService', 'LionService',
function (_$log_, _fs_, _sus_, _ls_) {
$log = _$log_;
fs = _fs_;
sus = _sus_;
ls = _ls_;
function initQuickHelp(opts) {
settings = angular.extend({}, defaultSettings, fs.isO(opts));
qhdiv = d3.select('#quickhelp');
}
function showQuickHelp(bindings) {
svg = qhdiv.select('svg');
if (!goodBindings(bindings)) {
return null;
}
if (svg.empty()) {
addSvg();
popBind(bindings);
} else {
hideQuickHelp();
}
}
function hideQuickHelp() {
svg = qhdiv.select('svg');
if (!svg.empty()) {
fadeBindings();
removeSvg();
return true;
}
return false;
}
return {
initQuickHelp: initQuickHelp,
showQuickHelp: showQuickHelp,
hideQuickHelp: hideQuickHelp
};
}]);
}());