Simon Hunt

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

Change-Id: I92edeb570a97eff87a5f9b08373ff0517849bf24
/*
* Copyright 2014,2015 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 -- Quick Help Service -- CSS file
*/
#quickhelp {
z-index: 1300;
}
#quickhelp svg {
position: absolute;
top: 180px;
opacity: 1;
}
#quickhelp svg g.help rect {
fill: black;
opacity: 0.7;
}
#quickhelp svg text.title {
font-size: 10pt;
font-style: italic;
text-anchor: middle;
fill: #999;
}
#quickhelp svg g.keyItem {
fill: white;
}
#quickhelp svg g line.qhrowsep {
stroke: #888;
stroke-dasharray: 2 2;
}
#quickhelp svg text {
font-size: 7pt;
alignment-baseline: middle;
}
#quickhelp svg text.key {
fill: #add;
}
#quickhelp svg text.desc {
fill: #ddd;
}
/*
* Copyright 2015 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;
// 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: '=',
dash: '-',
slash: '/',
backSlash: '\\',
backQuote: '`',
leftArrow: 'L-arrow',
upArrow: 'U-arrow',
rightArrow: 'R-arrow',
downArrow: 'D-arrow'
};
// ===========================================
// === Function Definitions ===
// TODO: move this to FnService.
function cap(s) {
return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); });
}
function mkKeyDisp(id) {
var v = keyDisp[id] || id;
return 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]);
// '-' marks a separator; d is the description
if (k === '-' || d) {
b.push([mkKeyDisp(k), d]);
}
});
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 popBind(bindings) {
pane = svg.append('g')
.attr({
class: 'help',
opacity: 0
});
rect = pane.append('rect')
.attr('rx', 8);
pane.append('text')
.text('Quick Help')
.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();
}
// ===========================================
// === Module Definition ===
angular.module('onosLayer')
.factory('QuickHelpService',
['$log', 'FnService', 'SvgUtilService',
function (_$log_, _fs_, _sus_) {
$log = _$log_;
fs = _fs_;
sus = _sus_;
function initQuickHelp(opts) {
settings = angular.extend({}, defaultSettings, opts);
qhdiv = d3.select('#quickhelp');
}
function showQuickHelp(bindings) {
svg = qhdiv.select('svg');
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
};
}]);
}());
......@@ -21,7 +21,7 @@
'use strict';
// references to injected services
var $log, fs, ts;
var $log, fs, ts, qhs;
// internal state
var enabled = true,
......@@ -115,22 +115,13 @@
}
function quickHelp(view, key, code, ev) {
// TODO: show quick help
// delegate to QuickHelp service.
//libApi.quickHelp.show(keyHandler);
console.log('QUICK-HELP');
qhs.showQuickHelp(keyHandler);
return true;
}
// returns true if we 'consumed' the ESC keypress, false otherwise
function escapeKey(view, key, code, ev) {
// TODO: plumb in handling of quick help dismissal
/*
if (qh.hide()) {
return true;
}
*/
return false;
return qhs.hideQuickHelp();
}
function toggleTheme(view, key, code, ev) {
......@@ -176,13 +167,18 @@
}
angular.module('onosUtil')
.factory('KeyService', ['$log', 'FnService', 'ThemeService',
.factory('KeyService',
['$log', 'FnService', 'ThemeService',
function (_$log_, _fs_, _ts_) {
$log = _$log_;
fs = _fs_;
ts = _ts_;
return {
bindQhs: function (_qhs_) {
qhs = _qhs_;
},
installOn: function (elem) {
elem.on('keydown', keyIn);
setupGlobalKeys();
......
......@@ -63,6 +63,7 @@
<script src="fw/layer/layer.js"></script>
<script src="fw/layer/panel.js"></script>
<script src="fw/layer/flash.js"></script>
<script src="fw/layer/quickhelp.js"></script>
<script src="fw/layer/veil.js"></script>
<!-- Framework and library stylesheets included here -->
......@@ -74,6 +75,7 @@
<link rel="stylesheet" href="fw/svg/icon.css">
<link rel="stylesheet" href="fw/layer/panel.css">
<link rel="stylesheet" href="fw/layer/flash.css">
<link rel="stylesheet" href="fw/layer/quickhelp.css">
<link rel="stylesheet" href="fw/layer/veil.css">
<link rel="stylesheet" href="fw/nav/nav.css">
......
......@@ -65,9 +65,10 @@
.controller('OnosCtrl', [
'$log', '$route', '$routeParams', '$location',
'KeyService', 'ThemeService', 'GlyphService', 'PanelService',
'FlashService',
'FlashService', 'QuickHelpService',
function ($log, $route, $routeParams, $location, ks, ts, gs, ps, flash) {
function ($log, $route, $routeParams, $location,
ks, ts, gs, ps, flash, qhs) {
var self = this;
self.$route = $route;
......@@ -78,9 +79,11 @@
// initialize services...
ts.init();
ks.installOn(d3.select('body'));
ks.bindQhs(qhs);
gs.init();
ps.init();
flash.initFlash();
qhs.initQuickHelp();
$log.log('OnosCtrl has been created');
......
......@@ -75,14 +75,13 @@
]
});
// TODO: // mouse gestures
var gestures = [
ks.gestureNotes([
['click', 'Select the item and show details'],
['shift-click', 'Toggle selection state'],
['drag', 'Reposition (and pin) device / host'],
['cmd-scroll', 'Zoom in / out'],
['cmd-drag', 'Pan']
];
]);
}
// --- Keystroke functions -------------------------------------------
......
/*
* Copyright 2015 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 -- Flash Service - Unit Tests
*/
describe('factory: fw/layer/quickhelp.js', function () {
var $log, $timeout, fs, qhs, d3Elem;
beforeEach(module('onosUtil', 'onosSvg', 'onosLayer'));
beforeEach(inject(function (_$log_, _$timeout_, FnService, QuickHelpService) {
$log = _$log_;
//$timeout = _$timeout_;
fs = FnService;
qhs = QuickHelpService;
//jasmine.clock().install();
d3Elem = d3.select('body').append('div').attr('id', 'myqhdiv');
}));
afterEach(function () {
//jasmine.clock().uninstall();
d3.select('#myqhdiv').remove();
});
function helpItemSelection() {
return d3Elem.selectAll('.help');
}
it('should define QuickHelpService', function () {
expect(qhs).toBeDefined();
});
it('should define api functions', function () {
expect(fs.areFunctions(qhs, [
'initQuickHelp', 'showQuickHelp', 'hideQuickHelp'
])).toBeTruthy();
});
it('should have no items to start', function () {
expect(helpItemSelection().size()).toBe(0);
});
// TODO: check that the help stuff appears
/*
it('should show help items', function () {
var item, rect, text;
flash.flash('foo');
//jasmine.clock().tick(101);
setTimeout(function () {
item = flashItemSelection();
expect(item.size()).toEqual(1);
expect(item.classed('flashItem')).toBeTruthy();
expect(item.select('rect').size()).toEqual(1);
text = item.select('text');
expect(text.size()).toEqual(1);
expect(text.text()).toEqual('foo');
}, 100);
});
*/
});
......@@ -18,19 +18,21 @@
ONOS GUI -- Key Handler Service - Unit Tests
*/
describe('factory: fw/util/keys.js', function() {
var $log, ks, fs,
var $log, ks, fs, qhs,
d3Elem, elem, last;
beforeEach(module('onosUtil'));
beforeEach(module('onosUtil', 'onosSvg', 'onosLayer'));
beforeEach(inject(function (_$log_, KeyService, FnService) {
beforeEach(inject(function (_$log_, KeyService, FnService, QuickHelpService) {
$log = _$log_;
ks = KeyService;
fs = FnService;
qhs = QuickHelpService;
d3Elem = d3.select('body').append('p').attr('id', 'ptest');
elem = d3Elem.node();
ks.installOn(d3Elem);
ks.bindQhs(qhs);
last = {
view: null,
key: null,
......@@ -49,7 +51,7 @@ describe('factory: fw/util/keys.js', function() {
it('should define api functions', function () {
expect(fs.areFunctions(ks, [
'installOn', 'keyBindings', 'gestureNotes', 'enableKeys'
'bindQhs', 'installOn', 'keyBindings', 'gestureNotes', 'enableKeys'
])).toBeTruthy();
});
......