/* * 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: '=', 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]); // '-' 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(); } 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', function (_$log_, _fs_, _sus_) { $log = _$log_; fs = _fs_; sus = _sus_; 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 }; }]); }());