From a1a00c247e5ff9f91430afbea87da861e40f026c Mon Sep 17 00:00:00 2001 From: Simon Hunt Date: Thu, 4 Dec 2014 16:10:40 -0800 Subject: [PATCH] GUI - Rebuilt Quick Help layout to allow multi-column key binding section. Change-Id: Icada50c695ce60c8cbedb38d86434a842d935e77 --- web/gui/src/main/webapp/onos.js | 30 ++- web/gui/src/main/webapp/onosQuickHelp.css | 10 +- web/gui/src/main/webapp/onosQuickHelp.js | 288 ++++++++++++++-------- 3 files changed, 201 insertions(+), 127 deletions(-) diff --git a/web/gui/src/main/webapp/onos.js b/web/gui/src/main/webapp/onos.js index 89768db817..803607c1c9 100644 --- a/web/gui/src/main/webapp/onos.js +++ b/web/gui/src/main/webapp/onos.js @@ -417,19 +417,23 @@ } function setupGlobalKeys() { - keyHandler.globalKeys = { - slash: [quickHelp, 'Show / hide Quick Help'], - backSlash: [quickHelp, 'Show / hide Quick Help'], - esc: [escapeKey, 'Dismiss dialog or cancel selections'], - T: [toggleTheme, "Toggle theme"] - }; - // Masked keys are global key handlers that always return true. - // That is, the view will never see the event for that key. - keyHandler.maskedKeys = { - slash: true, - backSlash: true, - T: true - }; + $.extend(keyHandler, { + globalKeys: { + backSlash: [quickHelp, 'Show / hide Quick Help'], + slash: [quickHelp, 'Show / hide Quick Help'], + esc: [escapeKey, 'Dismiss dialog or cancel selections'], + T: [toggleTheme, "Toggle theme"] + }, + globalFormat: ['backSlash', 'slash', 'esc', 'T'], + + // Masked keys are global key handlers that always return true. + // That is, the view will never see the event for that key. + maskedKeys: { + slash: true, + backSlash: true, + T: true + } + }); } function quickHelp(view, key, code, ev) { diff --git a/web/gui/src/main/webapp/onosQuickHelp.css b/web/gui/src/main/webapp/onosQuickHelp.css index eb400cf57d..3f985e0916 100644 --- a/web/gui/src/main/webapp/onosQuickHelp.css +++ b/web/gui/src/main/webapp/onosQuickHelp.css @@ -26,7 +26,7 @@ #quickhelp svg { position: absolute; - bottom: 40px; + top: 180px; opacity: 1; } @@ -36,7 +36,7 @@ } #quickhelp svg text.title { - font-size: 8pt; + font-size: 10pt; font-style: italic; text-anchor: middle; fill: #999; @@ -46,23 +46,21 @@ fill: white; } -#quickhelp svg g.keyItem line { +#quickhelp svg g line.qhrowsep { stroke: #888; stroke-dasharray: 2 2; } #quickhelp svg text { - font-size: 5pt; + font-size: 7pt; alignment-baseline: middle; } #quickhelp svg text.key { - font-size: 5pt; fill: #add; } #quickhelp svg text.desc { - font-size: 5pt; fill: #ddd; } diff --git a/web/gui/src/main/webapp/onosQuickHelp.js b/web/gui/src/main/webapp/onosQuickHelp.js index 28c488b2b0..a0da6aaaa5 100644 --- a/web/gui/src/main/webapp/onosQuickHelp.js +++ b/web/gui/src/main/webapp/onosQuickHelp.js @@ -26,29 +26,35 @@ (function (onos){ 'use strict'; - // API's - var api = onos.api; - // Config variables var w = '100%', h = '80%', fade = 500, vb = '-200 0 400 400'; + // layout configuration + var pad = 10, + offy = 45, + sepYDelta = 20, + colXDelta = 16, + yTextSpc = 12, + offDesc = 8; + // State variables - var data = []; + var data = [], + yCount; // DOM elements and the like var qhdiv = d3.select('#quickhelp'), svg = qhdiv.select('svg'), - pane, - rect, - items, - keyAgg; + pane, rect, items; // General functions - function isA(a) { - return $.isArray(a) ? a : null; + function isA(a) { return $.isArray(a) ? a : null; } + function isS(s) { return typeof s === 'string'; } + + function cap(s) { + return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); }); } var keyDisp = { @@ -63,144 +69,212 @@ downArrow: 'D-arrow' }; - function cap(s) { - return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); }); - } - function mkKeyDisp(id) { var v = keyDisp[id] || id; return cap(v); } - // layout configuration - var pad = 8, - offy = 45, - dy = 10, - offDesc = 8; + function addSeparator(el, i) { + var y = sepYDelta/2 - 5; + el.append('line') + .attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y }); + } - // D3 magic - function updateKeyItems() { - var keyItems = items.selectAll('.keyItem') - .data(data); + function addContent(el, data, ri) { + var xCount = 0, + clsPfx = 'qh-r' + ri + '-c'; - var entering = keyItems.enter() - .append('g') - .attr({ - id: function (d) { return d.id; }, - class: 'keyItem' + 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: 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; }); - entering.each(function (d, i) { + // 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), - y = offy + dy * i; + sep = r.type === 'sep', + dy; - if (d.id[0] === '_') { - el.append('line') - .attr({ x1: 0, y1: y, x2: 1, y2: y}); + el.attr('transform', translate(0, yCount)); + + if (sep) { + addSeparator(el, i); + yCount += sepYDelta; } else { - el.append('text') - .text(d.key) - .attr({ - class: 'key', - x: 0, - y: y - }); - // NOTE: used for sizing column width... - keyAgg.append('text').text(d.key).attr('class', 'key'); - - el.append('text') - .text(d.desc) - .attr({ - class: 'desc', - x: offDesc, - y: y - }); + dy = addContent(el, r.data, i); + yCount += dy; } }); - var kbox = keyAgg.node().getBBox(); - items.selectAll('.desc').attr('x', kbox.width + offDesc); + // size the backing rectangle + var ibox = items.node().getBBox(), + paneW = ibox.width + pad * 2, + paneH = ibox.height + offy; - var box = items.node().getBBox(), - paneW = box.width + pad * 2, - paneH = box.height + offy; - - items.selectAll('line').attr('x2', box.width); + items.selectAll('.qhrowsep').attr('x2', ibox.width); items.attr('transform', translate(-paneW/2, -pad)); rect.attr({ width: paneW, height: paneH, transform: translate(-paneW/2-pad, 0) }); + } function translate(x, y) { return 'translate(' + x + ',' + y + ')'; } + 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 = isA(fmt), + n = a && a.length, + ns = 0, + na = 0; + + if (n) { + // it is an array which has some content + a.forEach(function (d) { + isA(d) && na++; + 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 = 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), - fmt = vmap.get(hf), vgest = bindings.viewGestures, - gkeys = gmap.keys(), - vkeys, - sep = 0; + vfmt, vkeys; // filter out help format entry + vfmt = checkFmt(vmap.get(hf)); vmap.remove(hf); - vkeys = vmap.keys(), - gkeys.sort(); - vkeys.sort(); + // if bad (or no) format, fallback to sorted keys + if (!vfmt) { + vkeys = vmap.keys(); + vfmt = vkeys.sort(); + } data = []; - gkeys.forEach(function (k) { - addItem('glob', k, gmap.get(k)); - }); - addItem('sep'); - vkeys.forEach(function (k) { - addItem('view', k, vmap.get(k)); - }); - addItem('sep'); - vgest.forEach(function (g) { - if (g.length === 2) { - addItem('gest', g[0], g[1]); - } - }); - - function addItem(type, k, d) { - var id = type + '-' + k, - a = isA(d), - desc = a && a[1]; - - if (type === 'sep') { - data.push({ - id: '_' + sep++, - type: type - }); - } else if (type === 'gest') { - data.push({ - id: id, - type: type, - key: k, - desc: d - }); - } else if (desc) { - data.push( - { - id: id, - type: type, - key: mkKeyDisp(k), - desc: desc - } - ); - } - } + addRow(mkMapRow(gmap, gfmt)); + addRow(); + addRow(isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt)); + addRow(); + addRow(mkArrRow(vgest)); } + function popBind(bindings) { pane = svg.append('g') .attr({ @@ -220,8 +294,6 @@ }); items = pane.append('g'); - keyAgg = pane.append('g').style('visibility', 'hidden'); - aggregateData(bindings); updateKeyItems();