blob: 2428c6914d99ecaa516f6f3a2bcdfe2dde735e65 [file] [log] [blame]
<!-- The <debugger-app-sk> custom element declaration.
The main application element for the Skia Debugger.
Attributes:
None.
Events:
None.
Methods:
None.
-->
<link rel=import href="/res/imp/bower_components/iron-flex-layout/iron-flex-layout-classes.html">
<link rel=import href="/res/imp/bower_components/iron-icon/iron-icon.html">
<link rel=import href="/res/imp/bower_components/iron-icons/iron-icons.html">
<link rel=import href="/res/imp/bower_components/paper-checkbox/paper-checkbox.html">
<link rel=import href="/res/imp/bower_components/paper-drawer-panel/paper-drawer-panel.html">
<link rel=import href="/res/imp/bower_components/paper-header-panel/paper-header-panel.html">
<link rel=import href="/res/imp/bower_components/paper-icon-button/paper-icon-button.html">
<link rel=import href="/res/imp/bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel=import href="/res/imp/bower_components/paper-radio-group/paper-radio-group.html">
<link rel=import href="/res/imp/bower_components/paper-radio-button/paper-radio-button.html">
<link rel=import href="/res/imp/bower_components/paper-input/paper-input.html">
<link rel=import href="/res/imp/bower_components/paper-toolbar/paper-toolbar.html">
<link rel=import href="/res/imp/bower_components/paper-tabs/paper-tabs.html">
<link rel=import href="/res/imp/bower_components/iron-pages/iron-pages.html">
<link rel=import href="/res/common/imp/canvas-layers.html">
<link rel=import href="/res/common/imp/crosshair.html">
<link rel=import href="/res/common/imp/dbg-info.html">
<link rel=import href="/res/common/imp/details-summary.html">
<link rel=import href="/res/common/imp/error-toast-sk.html">
<link rel=import href="/res/common/imp/play.html">
<link rel=import href="/res/common/imp/zoom.html">
<link rel=import href="/res/imp/instance.html">
<dom-module id="debugger-app-sk">
<style include="iron-flex iron-flex-alignment iron-flex-factors">
:root {
--paper-toolbar-background: #1f78b4;
}
paper-tab {
--paper-tab: {
font-size: 12px;
}
}
paper-tabs {
height: 24px;
}
button {
font-size: 12px;
}
:host {
font-size: 12px;
display: block;
height: 100%
}
#content {
margin: 0.5em;
}
op-sk {
display: block;
}
play-sk {
margin: 5px auto;
}
#center {
overflow: hidden;
}
#rendered {
margin: 0 10px;
overflow: auto;
}
* {
font-family: Helvetica,Arial,'Bitstream Vera Sans',sans-serif;
}
.hidden {
display: none;
}
.shortcuts {
margin-left: 3em;
margin-bottom: 2em;
}
.shortcuts td {
padding-left: 1em;
}
.shortcuts tr {
padding-bottom: 0.5em;
}
#colorBreakPoint {
margin-left: 3em;
}
#download {
margin: 0 3em;
color: #1f78b4;
}
dbg-info-sk,
#textBounds,
#clip {
margin-left: 3em;
margin-bottom: 1em;
display: block;
}
paper-radio-button {
margin-left: 3em;
margin-bottom: 1em;
padding: 0;
}
#gpuOpBounds,
paper-toggle-button {
margin: 0.5em 1em;
}
.gpuDrawBoundColor {
color: #E31A1C;
opacity: 0.75;
}
.gpuOpBoundColor {
color: #FF7F00;
opacity: 0.75;
}
.gpuTotalOpColor {
color: #6A3D9A;
opacity: 0.75;
}
paper-tabs {
--paper-tabs-selection-bar-color: #1f78b4;
margin-bottom: 5px;
}
paper-tab {
color: white;
background: gray;
}
paper-tab.iron-selected {
font-weight: bold;
background: #1f78b4;
}
paper-tab[aria-disabled=true] {
color: white;
font-style: italic;
background: lightgray;
}
header {
background: #1f78b4;
color: white;
padding: 0.5em;
margin: 0;
}
header h2,
header div,
header instance-status-sk,
{
display: inline-block;
}
header .hidden {
display: none;
}
header h2 {
margin: 0;
}
dt {
font-weight: bold;
padding: 0.5em 0;
}
dd {
font-family: monospace;
}
canvas-layers-sk {
border: solid lightgray 1px;
}
paper-icon-button.resizer
#left paper-icon-button {
margin: 0;
padding: 0;
width: 24px;
height: 24px;
}
#upload summary-sk {
font-weight: bold;
}
#upload div {
padding: 1em 2em;
}
#histogram table {
padding-left: 2em;
}
#histogram .countCol {
text-align: right;
}
#histogram td {
padding: 0.2em;
}
zoom-sk {
display: block;
margin-top: 1em;
box-shadow: 5px 5px 18px grey;
}
#img.bottom {
width: auto;
height: 60vh;
}
#img.fit {
max-width: 100%;
max-height: 60vh;
width: auto;
height: auto;
}
#img.natural {
width: auto;
height: auto;
}
#img.right{
width: 100%;
height: auto;
}
.sizeToolBar paper-icon-button {
opacity: 0.5;
}
.sizeToolBar paper-icon-button:hover {
background-color: #eee;
}
details-sk {
display: block;
}
</style>
<template>
<header class="horizontal layout center">
<h2>Skia Debugger</h2>
<div class=flex></div>
<instance-status-sk id=status class=hidden></instance-status-sk>
</header>
<div id=content>
<div class="layout horizontal center">
<form enctype="multipart/form-data" method="post" id=post_skp action="./new" name="fileinfo">
<label>SKP to upload:</label>
<input type="file" name="file" required />
<button class=action>Upload</button>
</form>
<a href="/download" id=download class=hidden>Download</a>
</div>
<div class="layout horizontal" hidden="[[ skp_not_loaded ]]">
<div id=left class$="layout vertical flex-{{ _leftRatio }}">
<div class="layout horizontal end-justified">
<paper-icon-button class=resizer on-change=_smallerLeft icon="arrow-back" title="Shrink the command panel."></paper-icon-button>
</div>
<div class="layout horizontal">
<paper-input id="fast" on-change="_fastFilter" label="Filter
(Leading ! means remove matches)" placeholder="!save restore"
class=flex></paper-input>
<button id=clear on-tap=_clearFilter>Clear</button>
</div>
<play-sk id=play></play-sk>
<commands-sk id=commands grouping=50></commands-sk>
</div>
<div id=center class="layout vertical flex-3">
<div>
<paper-icon-button class=resizer on-tap=_biggerLeft icon="arrow-forward"
title="Grow the command panel."></paper-icon-button>
</div>
<div id=rendered>
<paper-tabs selected="{{tab_selected}}">
<paper-tab>Picture</paper-tab>
<paper-tab disabled=[[_isEmpty(bitmap)]]>Image</paper-tab>
</paper-tabs>
<iron-pages selected="{{tab_selected}}">
<div>
<div class="layout horizontal sizeToolBar">
<paper-icon-button src="https://debugger-assets.skia.org/res/img/image.png" data-style="natural" title="Original size." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button src="https://debugger-assets.skia.org/res/img/both.png" data-style="fit" title="Fit to page." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button src="https://debugger-assets.skia.org/res/img/right.png" data-style="right" title="Fit to page width." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button src="https://debugger-assets.skia.org/res/img/bottom.png" data-style="bottom" title="Fit to height." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button icon="file-download" title="Save current image" on-tap="_saveImage"></paper-icon-button>
</div>
<canvas-layers-sk layers='["crosshair"]' id=layers on-tap="_crosshairClick">
<img id=img src="" class="fit">
</canvas-layers-sk>
<crosshair-sk id=crosshair target=layers name=crosshair update_on=move hidden></crosshair-sk>
</div>
<div>
<img id=standAloneImg src$="[[bitmap]]">
</div>
</iron-pages>
</div>
</div>
<div id=right class$="layout vertical flex-2">
<div class="layout horizontal">
<paper-toggle-button on-iron-change="_gpuChange" id=gpu checked="{{_gpuChecked(_cmd.mode)}}">GPU</paper-toggle-button>
<paper-checkbox disabled="{{_gpuNotChecked(_cmd.mode)}}" checked="{{_isTrue(_cmd.drawGpuOpBounds)}}" id=gpuOpBounds on-change="_gpuOpBounds">Display GPU Op Bounds</paper-checkbox>
</div>
<details-sk>
<summary-sk>
Color Mode
</summary-sk>
<paper-radio-group class="layout vertical" on-paper-radio-group-changed="_colorModeChange" id=colorMode selected="[[_cmd.colorMode]]">
<paper-radio-button name="0">Linear 32-bit (Legacy)</paper-radio-button>
<paper-radio-button name="1">sRGB 32-bit</paper-radio-button>
<paper-radio-button name="2">Linear half-precision float</paper-radio-button>
</paper-radio-group>
</details-sk>
<details-sk>
<summary-sk>
Bounds and Matrix
</summary-sk>
<paper-checkbox id=clip on-tap="_clipHandler">Show Clip</paper-checkbox>
<paper-checkbox id=textBounds on-tap="_textBoundsHandler">Show Text Bounds</paper-checkbox>
<dbg-info-sk info="{{ info }}"></dbg-info-sk>
</details-sk>
<details-sk id=gpuOpBoundsLegend class=hidden>
<summary-sk>
GPU Op Bounds Legend
</summary-sk>
<table class=shortcuts>
<tr><td class=gpuDrawBoundColor>Bounds for the current draw.</td></tr>
<tr><td class=gpuOpBoundColor>Individual bounds for other draws in the same op.</td></tr>
<tr><td class=gpuTotalOpColor>Total bounds of the current op.</td></tr>
</table>
</details-sk>
<details-sk id=histogram class="hidden">
<summary-sk>
Histogram
</summary-sk>
<table>
<template is="dom-repeat" items="{{histogram}}">
<tr><td class=countCol>{{item.count}}</td><td>{{item.name}}</td></tr>
</template>
<tr><td class=countCol>{{_filtered.commands.length}}</td><td><b>Total</b></td></tr>
</table>
</details-sk>
<dl>
<dt>Postion</dt>
<dd>({{ x }}, {{ y }})</dd>
<dt>Color</dt>
<dd>{{ rgb }}</dd>
<dd>{{ hex }}</dd>
</dl>
<div>
<paper-checkbox disabled id=colorBreakPoint on-tap="_breakPoint">Break on change.</paper-checkbox> <span id=prevColor></span>
</div>
<details-sk id=keyboardshortcuts class=hidden>
<summary-sk>
Keyboard shortcuts
</summary-sk>
<table class=shortcuts>
<tr><th>H</th><td>Left</td></tr>
<tr><th>L</th><td>Right</td></tr>
<tr><th>J</th><td>Down</td></tr>
<tr><th>K</th><td>Up</td></tr>
<tr><td colspan=2>Click the image again to turn off keyboard navigation.</td></tr>
</table>
</details-sk>
<zoom-sk source=img pixels=20 id=zoom></zoom-sk>
</div>
</div>
<error-toast-sk></error-toast-sk>
</div>
</template>
</dom-module>
<script>
(function () {
var INDENTERS = {
"Save": { icon: "icons:save", color: "#B2DF8A", count: 1 },
"SaveLayer": { icon: "icons:content-copy", color: "#FDBF6F", count: 1 },
"BeginDrawPicture": { icon: "image:image", color: "#A6CEE3", count: 1 },
};
var OUTDENTERS = ["Restore", "EndDrawPicture"];
Polymer({
is: "debugger-app-sk",
properties: {
tab_selected: {
type: Number,
value: 0,
reflectToAttribute: false,
},
bitmap: {
type: String,
value: "",
reflectToAttribute: false,
},
_leftRatio: {
type: Number,
value: 3,
reflectToAttribute: false,
},
skp_not_loaded: {
type: Boolean,
value: true,
reflectToAttribute: false,
},
histogram: {
type: Array,
value: function() { return []; },
reflectToAttribute: false,
}
},
ready: function() {
// _targetItem is the index of the op we are in the process of moving to.
// The index is the offset of the op in the this._filtered.commands array.
// That is, we've requested to move to this op, but the image might not
// be loaded yet.
this._targetItem = 0;
// The original JSON response from the server.
this._cmd = {
version: 1,
commands: [],
};
// _filtered contains the .commands that match the current filter, or all
// of the commands if no filter is active.
//
// NB: There is a distintion in the code below between an op's item vs
// index. That is, an ops index never changes, it is the index
// number that the server understands, and is the location of the op
// in this._cmd.commands.
//
// The op item changes, it is the location of the op in
// this._filtered.commands. Some functions use index, some use
// item.
this._filtered = {
version: 1,
commands: [],
};
// Load the test image served by the debugger.
this._setCacheBreakingURL();
this.$.post_skp.action= this._correctedOrigin() + "new";
this.$.commands.addEventListener('op-selected', function(e) {
// Only force reloading the image if necessary.
var item = this._findItemFromIndex(e.detail.index);
if (this._targetItem != item) {
this._targetItem = item;
this._setCacheBreakingURL();
} else {
// We know if we've gotten here that the element wasn't selected by
// a UI action, i.e. we know we're here because we are 'run'ing.
this.$.commands.scrollToTop(e.detail.index);
}
}.bind(this));
this.$.commands.addEventListener('op-toggled', function(e) {
// Toggle the op and the trigger a reload of the image.
DebugCanvas.toggleOp(e.detail.index, e.detail.checked).then(function() {
this._setCacheBreakingURL();
}.bind(this)).catch(sk.errorMessage);
}.bind(this));
this.$.commands.addEventListener('op-zoom', function(e) {
this.$.fast.value = e.detail;
this._fastFilter();
}.bind(this));
this._refreshPage();
this.$.crosshair.addEventListener('crosshair', function(e) {
this.$.zoom.x = e.detail.x;
this.$.zoom.y = e.detail.y;
}.bind(this));
this.$.play.prePlayCallback(function() {
if (!this.$.colorBreakPoint.disabled && this.$.colorBreakPoint.checked) {
DebugCanvas.setBreakpoint(this._targetItem, this.x, this.y).then(function(json) {
if (json.endOp != -1) {
this.$.prevColor.textContent = json.startColor + " -> " + json.endColor;
var item = this._findItemFromIndex(json.endOp);
this._targetItem = item;
this._setCacheBreakingURL();
} else {
sk.errorMessage("Pixel at (" + this.x + ", " + this.y + ") never changes.");
}
}.bind(this)).catch(sk.errorMessage);
return false;
}
return true;
}.bind(this));
this.$.zoom.addEventListener('zoom-point', function(e) {
this.set("rgb", e.detail.rgb);
this.set("hex", e.detail.hex);
this.set("x", e.detail.x);
this.set("y", e.detail.y);
}.bind(this));
this.$.zoom.addEventListener('zoom-loaded', function(e) {
this.$.play.movedTo(this._targetItem);
}.bind(this));
this.$.img.addEventListener('load', function() {
this.$.download.href = this._correctedOrigin() + "download";
this.$.download.classList.remove("hidden");
this.$.commands.item = this._targetItem;
}.bind(this));
this.$.play.addEventListener('moveto', function(e) {
this._targetItem = e.detail.item;
this._setCacheBreakingURL();
}.bind(this));
this.$.play.addEventListener('mode-changed-manually', function(e) {
// Clear the breakpoint info if the user presses a button.
this.$.prevColor.textContent = "";
}.bind(this));
this.$.status.classList.remove('hidden');
},
attached: function() {
document.body.addEventListener('keydown', this._keyDownHandler.bind(this), true);
},
_resizeImage: function(e) {
var ele = sk.findParent(e.target, "PAPER-ICON-BUTTON");
if (!ele) {
return
}
this.$.img.classList.remove("natural", "fit", "right", "bottom");
this.$.img.classList.add(ele.dataset.style);
window.dispatchEvent(new CustomEvent("partial-resize"));
},
_saveImage: function(e) {
// Download the current frame by making an anchor tag with a download attribute.
var a = document.createElement('a');
// download attribute becomes the name of the downloaded file
this._correctedOrigin()
var src = this.$.img.src
a.href = src;
a.download = src.substring(this._correctedOrigin().length);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
_smallerLeft: function() {
if (this._leftRatio > 1) {
this._leftRatio--;
}
},
_biggerLeft: function() {
if (this._leftRatio < 12) {
this._leftRatio++;
}
},
_colorModeChange: function(e) {
// Ignore if this._cmd hasn't been loaded yet.
if (!this._cmd) {
return
}
// Don't do anything if the radio group and the mode already agree.
if (e.target.selected == this._cmd.colorMode) {
return
}
DebugCanvas.setColorMode(e.target.selected).then(function() {
// Reloads the image and the JSON for all the commands.
this._refreshPage();
}.bind(this)).catch(function(err) {
this.$.colorMode.selected = 0;
sk.errorMessage(err);
}.bind(this));
},
_gpuChange: function(e) {
// Ignore if this._cmd hasn't been loaded yet.
if (!this._cmd) {
return
}
// Don't do anything if the toggle and the mode already agree.
if (e.target.checked == (this._cmd.mode == "gpu")) {
return
}
DebugCanvas.setGPU(e.target.checked).then(function() {
// Reloads the image and the JSON for all the commands.
this._refreshPage();
}.bind(this)).catch(function(err) {
this.$.gpu.checked = !this.$.gpu.checked;
sk.errorMessage(err);
}.bind(this));
},
_gpuChecked: function(mode) {
return mode == "gpu";
},
_gpuNotChecked: function(mode) {
return !this._gpuChecked(mode);
},
_isTrue: function(b) {
return !!b;
},
_isEmpty: function(s) {
return s.length == 0;
},
_gpuOpBounds: function(e) {
// Ignore if this._cmd hasn't been loaded yet.
if (!this._cmd) {
return
}
this.$.gpuOpBoundsLegend.classList.toggle("hidden", !this.$.gpuOpBounds.checked);
// Don't do anything if the toggle and the mode already agree.
if (this.$.gpuOpBounds.checked === this._cmd.drawGpuOpBounds) {
return
}
DebugCanvas.setGPUBounds(this.$.gpuOpBounds.checked).then(function() {
// Reloads the image and the JSON for all the commands.
this._refreshPage();
}.bind(this)).catch(function(err) {
this.$.gpuOpBounds.checked = !this.$.gpuOpBounds.checked;
sk.errorMessage(err);
}.bind(this));
},
_breakPoint: function() {
if (this.$.colorBreakPoint.checked) {
// We want to be able to jump to any op when the breakpoint
// triggers, so we remove all filtering.
this.$.fast.value = "";
this._fastFilter();
}
},
_findItemFromIndex: function(index) {
var item = 0;
for (var i = 0; i < this._filtered.commands.length ; i++) {
if (this._filtered.commands[i]._index == index) {
item = i;
break;
}
}
return item;
},
_clearFilter: function() {
this.$.fast.value = "";
this._fastFilter();
},
_fastFilter: function() {
var rawFilter = this.$.fast.value.trim().toLowerCase();
if (rawFilter.indexOf(":") > 0) {
// This is a range filter, e.g. "3:21".
this._rangeFilter(rawFilter);
} else {
// Text filter, e.g. "!save restore".
this._textFilter(rawFilter);
}
},
_rangeFilter: function(rawFilter) {
var parts = rawFilter.split(":");
if (parts.length != 2) {
sk.errorMessage("Range filters are of the form 'N:M'.");
return
}
var begin = +parts[0];
var end = +parts[1];
var filtered = {
version: 1,
commands: [],
}
this._cmd.commands.forEach(function(c, i) {
if (i >= begin && i <= end) {
filtered.commands.push(c);
}
}.bind(this));
this._setFiltered(filtered);
},
_textFilter: function(rawFilter) {
var negative = (rawFilter[0] == "!");
if (negative) {
rawFilter = rawFilter.slice(1).trim();
}
var filters = rawFilter.split(/\s+/);
var matches = function(s) {
s = s.toLowerCase();
for (var i = 0; i < filters.length; i++) {
if (negative) {
if (s.indexOf(filters[i]) >= 0) {
return false
}
} else {
if (s.indexOf(filters[i]) >= 0) {
return true
}
}
}
return negative;
};
var filtered = {
version: 1,
commands: [],
}
this._cmd.commands.forEach(function(c) {
if (matches(JSON.stringify(c.details).toLowerCase())) {
filtered.commands.push(c);
}
}.bind(this));
this._setFiltered(filtered);
},
_refreshPage: function() {
this.$.img.classList.remove("hidden");
this._setCacheBreakingURL();
DebugCanvas.getInfo().then(function(json) {
this.skp_not_loaded = false;
this.set('info', json);
}.bind(this)).catch(sk.errorMessage);
DebugCanvas.getCmd().then(function(json) {
this._cmd = this._processCommands(json);
if (this.$.fast.value) {
this._fastFilter();
} else {
var filtered = {
version: 1,
commands: this._cmd.commands.slice(),
};
this._setFiltered(filtered);
}
}.bind(this)).catch(sk.errorMessage);
},
_setFiltered: function(filtered) {
this.$.commands.cmd = filtered;
this._filtered = filtered;
this.$.play.size = filtered.commands.length;
this._targetItem = filtered.commands.length - 1;
this._setCacheBreakingURL();
},
_hasBitmap: function(s) {
return !!s;
},
_clipHandler: function() {
DebugCanvas.setClip(this.$.clip.checked).then(function() {
this._setCacheBreakingURL();
}.bind(this)).catch(sk.errorMessage);
},
_textBoundsHandler: function() {
DebugCanvas.setTextBounds(this.$.textBounds.checked).then(function() {
this._setCacheBreakingURL();
}.bind(this)).catch(sk.errorMessage);
},
_setCacheBreakingURL: function() {
// Get op index from item number using this._filtered.
if (this._filtered.commands.length == 0) {
return
}
var index = this._filtered.commands[this._targetItem]._index;
var op = this._filtered.commands[this._targetItem];
var path = "img/" + index + "?";
this.bitmap = op.details.bitmap ? (this._correctedOrigin() + op.details.bitmap.data) : "";
DebugCanvas.getInfo(index).then(function(json) {
this.set('info', json);
}.bind(this)).catch(sk.errorMessage);
this.$.img.src = this._correctedOrigin() + path + "?" + new Date().getTime();
},
_correctedOrigin: function() {
// Since we always use relative URLs, i.e. "./foo" we need to make
// sure the base URL ends with a "/".
var origin = window.location.href;
if (origin[origin.length-1] != "/") {
origin += "/";
}
return origin;
},
_crosshairClick: function(e) {
if (this.$.crosshair.update_on === "move") {
this.$.crosshair.update_on = "click";
this.$.colorBreakPoint.disabled = false;
this.$.keyboardshortcuts.classList.remove("hidden");
this.$.crosshair.hidden = false;
} else {
this.$.crosshair.update_on = "move";
this.$.colorBreakPoint.disabled = true;
this.$.keyboardshortcuts.classList.add("hidden");
this.$.crosshair.hidden = true;
}
},
_keyDownHandler: function(e) {
var c = String.fromCharCode(e.keyCode);
switch (c) {
case "J":
this.$.crosshair.y = this.$.crosshair.y+1;
break;
case "K":
this.$.crosshair.y = this.$.crosshair.y-1;
break;
case "H":
this.$.crosshair.x = this.$.crosshair.x-1;
break;
case "L":
this.$.crosshair.x = this.$.crosshair.x+1;
break;
}
if ("JKHL".indexOf(c) != -1 ) {
e.stopPropagation();
}
},
_json: function(s) {
return JSON.stringify(s, null, 2);
},
_deepCopy: function(o) {
return JSON.parse(JSON.stringify(o));
},
// _processCommands cycles through the commands and set a depth based on Save/Restore pairs.
//
// Also calculates the histogram.
_processCommands: function(cmd) {
var commands = cmd.commands;
var depth = 0;
var prefixes = [];
var counts = {}; // Tally of each command.
var matchup = []; // Match up saves and restores.
for (var i = 0; i < commands.length; i++) {
commands[i] = {
details: commands[i],
_index: i,
};
commands[i]._depth = depth;
commands[i]._prefix = this._deepCopy(prefixes);
var name = commands[i].details.command;
counts[name] = (counts[name] || 0) + 1;
if (commands[i].details.command in INDENTERS) {
depth++;
matchup.push(i);
// If this is the same type of indenting op we've already seen
// then just increment the count, otherwise add as a new
// op in prefixes.
if (depth > 1 && prefixes[prefixes.length-1].icon == INDENTERS[commands[i].details.command].icon) {
prefixes[prefixes.length-1].count++;
} else {
prefixes.push(this._deepCopy(INDENTERS[commands[i].details.command]));
}
} else if (OUTDENTERS.indexOf(commands[i].details.command) != -1) {
depth--;
// Now that we can match an OUTDENTER with an INDENTER we can set
// the _zoom property for both commands.
var begin = matchup.pop();
var range = begin + ":" + i;
commands[i]._zoom = range
commands[begin]._zoom = range;
// Only pop the op from prefixes if its count has reached 1.
if (prefixes[prefixes.length-1].count > 1) {
prefixes[prefixes.length-1].count--;
} else {
prefixes.pop();
}
commands[i]._depth = depth;
commands[i]._prefix = this._deepCopy(prefixes);
}
}
// Calculate the histogram of the ops.
// First convert the object into an Array of objects.
var histogram = [];
for (k in counts) {
histogram.push({
name: k,
count: counts[k],
});
}
// Now sort the array, descending on the count, ascending
// on the op name.
histogram.sort(function(a,b) {
if (a.count == b.count) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
} else {
return b.count - a.count;
}
});
this.histogram = histogram;
this.$.histogram.classList.remove('hidden');
return cmd;
}
});
})();
</script>