[tools] Improve SystemAnalyzer tooltip
- Debounce creating tooltips to declutter the UI - CTRL-mouse move causes immediate tooltips - Use icons and help text on tooltip buttons - Recreate tooltip target nodes in timeline views to avoid moving the existing tooltip if the update is debounced Change-Id: I65a885827ebfeafc09c1c08e2cfe9c2dd448edca Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4012720 Commit-Queue: Camillo Bruni <cbruni@chromium.org> Reviewed-by: Marja Hölttä <marja@chromium.org> Cr-Commit-Position: refs/heads/main@{#84120}
This commit is contained in:
parent
5c4fc2b75c
commit
8b1cfdf682
@ -181,11 +181,14 @@ found in the LICENSE file. -->
|
||||
|
||||
<h3>Keyboard Shortcuts for Navigation</h3>
|
||||
<dl>
|
||||
<dt><kbd>CTRL</kbd> + <kbd>Mouse Move</kbd></dt>
|
||||
<dd>Show tooltips immediately</dd>
|
||||
|
||||
<dt><kbd>A</kbd></dt>
|
||||
<dd>Scroll left</dd>
|
||||
|
||||
<dt><kbd>D</kbd></dt>
|
||||
<dd>Sroll right</dd>
|
||||
<dd>Scroll right</dd>
|
||||
|
||||
<dt><kbd>SHIFT</kbd> + <kbd>Arrow Up</kbd></dt>
|
||||
<dd>Follow Map transition forward (first child)</dd>
|
||||
|
@ -328,12 +328,11 @@ class App {
|
||||
throw new Error(
|
||||
`Unknown tooltip content type: ${content.constructor?.name}`);
|
||||
}
|
||||
this.setToolTip(content, event.positionOrTargetNode);
|
||||
}
|
||||
|
||||
setToolTip(content, positionOrTargetNode) {
|
||||
this._view.toolTip.positionOrTargetNode = positionOrTargetNode;
|
||||
this._view.toolTip.content = content;
|
||||
this._view.toolTip.data = {
|
||||
content: content,
|
||||
positionOrTargetNode: event.positionOrTargetNode,
|
||||
immediate: event.immediate,
|
||||
};
|
||||
}
|
||||
|
||||
restartApp() {
|
||||
|
@ -76,13 +76,14 @@ export class ToolTipEvent extends AppEvent {
|
||||
return 'showtooltip';
|
||||
}
|
||||
|
||||
constructor(content, positionOrTargetNode) {
|
||||
constructor(content, positionOrTargetNode, immediate) {
|
||||
super(ToolTipEvent.name);
|
||||
if (!positionOrTargetNode) {
|
||||
throw Error('Either provide a valid position or targetNode');
|
||||
}
|
||||
this._content = content;
|
||||
this._positionOrTargetNode = positionOrTargetNode;
|
||||
this._immediate = immediate;
|
||||
}
|
||||
|
||||
get content() {
|
||||
@ -92,4 +93,8 @@ export class ToolTipEvent extends AppEvent {
|
||||
get positionOrTargetNode() {
|
||||
return this._positionOrTargetNode;
|
||||
}
|
||||
|
||||
get immediate() {
|
||||
return this._immediate;
|
||||
}
|
||||
}
|
||||
|
@ -322,5 +322,27 @@ export function gradientStopsFromGroups(
|
||||
return stops;
|
||||
}
|
||||
|
||||
export class Debouncer {
|
||||
constructor(callback, timeout = 250) {
|
||||
this._callback = callback;
|
||||
this._timeout = timeout;
|
||||
this._timeoutId = 0;
|
||||
}
|
||||
|
||||
callNow(...args) {
|
||||
this.clear();
|
||||
return this._callback(...args);
|
||||
}
|
||||
|
||||
call(...args) {
|
||||
this.clear() this._timeoutId =
|
||||
window.setTimeout(this._callback, this._timeout, ...args)
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
export * from '../helper.mjs';
|
||||
export * from '../../js/web-api-helper.mjs'
|
||||
|
@ -127,7 +127,7 @@ DOM.defineCustomElement('view/list-panel',
|
||||
|
||||
_logEntryMouseOverHandler(e) {
|
||||
const group = e.currentTarget.group;
|
||||
this.dispatchEvent(new ToolTipEvent(group.key, e.currentTarget));
|
||||
this.dispatchEvent(new ToolTipEvent(group.key, e.currentTarget, e.ctrlKey));
|
||||
}
|
||||
|
||||
_handleDetailsClick(event) {
|
||||
|
@ -147,8 +147,8 @@ DOM.defineCustomElement('./view/map-panel/map-transitions',
|
||||
}
|
||||
|
||||
_handleMouseoverMap(event) {
|
||||
this.dispatchEvent(
|
||||
new ToolTipEvent(event.currentTarget.map, event.currentTarget));
|
||||
this.dispatchEvent(new ToolTipEvent(
|
||||
event.currentTarget.map, event.currentTarget, event.ctrlKey));
|
||||
}
|
||||
|
||||
_handleToggleSubtree(event) {
|
||||
|
@ -295,7 +295,7 @@ DOM.defineCustomElement('view/profiler-panel',
|
||||
const profileNode = e.target.data;
|
||||
if (!profileNode) return;
|
||||
const logEntry = profileNode.codeEntry.logEntry;
|
||||
this.dispatchEvent(new ToolTipEvent(logEntry, e.target));
|
||||
this.dispatchEvent(new ToolTipEvent(logEntry, e.target, e.ctrlKey));
|
||||
}
|
||||
|
||||
_handleFlameChartClick(e) {
|
||||
|
@ -111,16 +111,20 @@ DOM.defineCustomElement('view/property-link-table',
|
||||
if (this._object === undefined) return;
|
||||
if (!this._instanceLinkButtons) return;
|
||||
const footer = DOM.div('footer');
|
||||
let showButton = footer.appendChild(DOM.button('Show', this._showHandler));
|
||||
let showButton =
|
||||
footer.appendChild(DOM.button('🔍 Details', this._showHandler));
|
||||
showButton.data = this._object;
|
||||
showButton.title = `Show details for ${this._object}`
|
||||
if (this._object.sourcePosition) {
|
||||
let showSourcePositionButton = footer.appendChild(
|
||||
DOM.button('Source Position', this._showSourcePositionHandler));
|
||||
DOM.button('📍 Source Position', this._showSourcePositionHandler));
|
||||
showSourcePositionButton.data = this._object;
|
||||
showSourcePositionButton.title = 'Open the source position';
|
||||
}
|
||||
let showRelatedButton = footer.appendChild(
|
||||
DOM.button('Show Related', this._showRelatedHandler));
|
||||
DOM.button('🕸 Related', this._showRelatedHandler));
|
||||
showRelatedButton.data = this._object;
|
||||
showRelatedButton.title = 'Show all related events in all panels';
|
||||
this._fragment.appendChild(footer);
|
||||
}
|
||||
|
||||
|
@ -198,7 +198,7 @@ DOM.defineCustomElement('view/script-panel',
|
||||
break;
|
||||
}
|
||||
toolTipContent.appendChild(sourceMapContent);
|
||||
this.dispatchEvent(new ToolTipEvent(toolTipContent, e.target));
|
||||
this.dispatchEvent(new ToolTipEvent(toolTipContent, e.target, e.ctrlKey));
|
||||
}
|
||||
|
||||
handleShowToolTipEntries(event) {
|
||||
@ -233,8 +233,8 @@ class ToolTipTableBuilder {
|
||||
tr.appendChild(DOM.td(name));
|
||||
tr.appendChild(DOM.td(subtypeName));
|
||||
tr.appendChild(DOM.td(entries.length));
|
||||
const button =
|
||||
DOM.button('Show', this._scriptPanel.showToolTipEntriesHandler);
|
||||
const button = DOM.button('🔎', this._scriptPanel.showToolTipEntriesHandler);
|
||||
button.title = `Show all ${entries.length} ${name || subtypeName} entries.`
|
||||
button.data = entries;
|
||||
tr.appendChild(DOM.td(button));
|
||||
this.tableNode.appendChild(tr);
|
||||
|
@ -110,7 +110,8 @@ DOM.defineCustomElement('view/timeline/timeline-overview',
|
||||
if (!toolTipContent) {
|
||||
toolTipContent = `Time ${formatDurationMicros(timeMicros)}`;
|
||||
}
|
||||
this.dispatchEvent(new ToolTipEvent(toolTipContent, this._indicatorNode));
|
||||
this.dispatchEvent(
|
||||
new ToolTipEvent(toolTipContent, this._indicatorNode, e.ctrlKey));
|
||||
}
|
||||
|
||||
_findLogEntryAtTime(time, maxTimeDistance) {
|
||||
|
@ -30,7 +30,7 @@ export class TimelineTrackBase extends V8CustomElement {
|
||||
this.timelineChunks = this.$('#timelineChunks');
|
||||
this.timelineSamples = this.$('#timelineSamples');
|
||||
this.timelineNode = this.$('#timeline');
|
||||
this.toolTipTargetNode = this.$('#toolTipTarget');
|
||||
this._toolTipTargetNode = undefined;
|
||||
this.hitPanelNode = this.$('#hitPanel');
|
||||
this.timelineAnnotationsNode = this.$('#timelineAnnotations');
|
||||
this.timelineMarkersNode = this.$('#timelineMarkers');
|
||||
@ -356,8 +356,8 @@ export class TimelineTrackBase extends V8CustomElement {
|
||||
|
||||
_updateToolTip(event) {
|
||||
if (!this._focusedEntry) return false;
|
||||
this.dispatchEvent(
|
||||
new ToolTipEvent(this._focusedEntry, this.toolTipTargetNode));
|
||||
this.dispatchEvent(new ToolTipEvent(
|
||||
this._focusedEntry, this._toolTipTargetNode, event.ctrlKey));
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
@ -419,13 +419,27 @@ export class TimelineTrackBase extends V8CustomElement {
|
||||
(kTimelineHeight - event.layerY) / chunk.height * (chunk.size() - 1));
|
||||
if (relativeIndex > chunk.size()) return false;
|
||||
const logEntry = chunk.at(relativeIndex);
|
||||
const style = this.toolTipTargetNode.style;
|
||||
const node = this.getToolTipTargetNode(logEntry);
|
||||
if (!node) return logEntry;
|
||||
const style = node.style;
|
||||
style.left = `${chunk.index * kChunkWidth}px`;
|
||||
style.top = `${kTimelineHeight - chunk.height}px`;
|
||||
style.height = `${chunk.height}px`;
|
||||
style.width = `${kChunkVisualWidth}px`;
|
||||
return logEntry;
|
||||
}
|
||||
|
||||
getToolTipTargetNode(logEntry) {
|
||||
let node = this._toolTipTargetNode;
|
||||
if (node) {
|
||||
if (node.logEntry === logEntry) return undefined;
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
node = this._toolTipTargetNode = DOM.div('toolTipTarget');
|
||||
node.logEntry = logEntry;
|
||||
this.$('#cropper').appendChild(node);
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
class SelectionHandler {
|
||||
|
@ -61,7 +61,9 @@ export class TimelineTrackStackedBase extends TimelineTrackBase {
|
||||
const item = this._getDrawableItemForEvent(event);
|
||||
const logEntry = this._drawableItemToLogEntry(item);
|
||||
if (item === undefined) return undefined;
|
||||
const style = this.toolTipTargetNode.style;
|
||||
const node = this.getToolTipTargetNode(logEntry);
|
||||
if (!node) return logEntry;
|
||||
const style = node.style;
|
||||
style.left = `${event.layerX}px`;
|
||||
style.top = `${(item.depth + 1) * kItemHeight}px`;
|
||||
style.height = `${kItemHeight}px`
|
||||
|
@ -40,7 +40,7 @@ found in the LICENSE file. -->
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#toolTipTarget {
|
||||
.toolTipTarget {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@ -237,7 +237,6 @@ found in the LICENSE file. -->
|
||||
</svg>
|
||||
<svg id="timelineAnnotations" xmlns="http://www.w3.org/2000/svg" class="dataSized noPointerEvents"></svg>
|
||||
<svg id="timelineMarkers" xmlns="http://www.w3.org/2000/svg" class="dataSized noPointerEvents"></svg>
|
||||
<div id="toolTipTarget"></div>
|
||||
</div>
|
||||
<!-- Use a div element covering all complex items to prevent slow hit test-->
|
||||
<div id="hitPanel" class="dataSized"></div>
|
||||
|
@ -2,137 +2,146 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import {DOM, V8CustomElement} from './helper.mjs';
|
||||
import {Debouncer, DOM, V8CustomElement} from './helper.mjs';
|
||||
|
||||
DOM.defineCustomElement(
|
||||
'view/tool-tip', (templateText) => class Tooltip extends V8CustomElement {
|
||||
_targetNode;
|
||||
_content;
|
||||
_isHidden = true;
|
||||
DOM.defineCustomElement('view/tool-tip',
|
||||
(templateText) =>
|
||||
class Tooltip extends V8CustomElement {
|
||||
_targetNode;
|
||||
_content;
|
||||
_isHidden = true;
|
||||
_debouncedSetData = new Debouncer((...args) => this._setData(...args), 500)
|
||||
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this._intersectionObserver = new IntersectionObserver((entries) => {
|
||||
if (entries[0].intersectionRatio <= 0) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
this.requestUpdate(true);
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', (event) => {
|
||||
// Only hide the tooltip if we click anywhere outside of it.
|
||||
let target = event.target;
|
||||
while (target) {
|
||||
if (target == this) return;
|
||||
target = target.parentNode;
|
||||
}
|
||||
this.hide()
|
||||
});
|
||||
}
|
||||
|
||||
_update() {
|
||||
if (!this._targetNode || this._isHidden) return;
|
||||
const rect = this._targetNode.getBoundingClientRect();
|
||||
rect.x += rect.width / 2;
|
||||
let atRight = this._useRight(rect.x);
|
||||
let atBottom = this._useBottom(rect.y);
|
||||
if (atBottom) rect.y += rect.height;
|
||||
this._setPosition(rect, atRight, atBottom);
|
||||
this.requestUpdate(true);
|
||||
}
|
||||
|
||||
set positionOrTargetNode(positionOrTargetNode) {
|
||||
if (positionOrTargetNode.nodeType === undefined) {
|
||||
this.position = positionOrTargetNode;
|
||||
} else {
|
||||
this.targetNode = positionOrTargetNode;
|
||||
}
|
||||
}
|
||||
|
||||
set targetNode(targetNode) {
|
||||
this._intersectionObserver.disconnect();
|
||||
this._targetNode = targetNode;
|
||||
if (targetNode === undefined) return;
|
||||
if (!(targetNode instanceof SVGElement)) {
|
||||
this._intersectionObserver.observe(targetNode);
|
||||
}
|
||||
this.requestUpdate(true);
|
||||
}
|
||||
|
||||
set position(position) {
|
||||
this._targetNode = undefined;
|
||||
this._setPosition(
|
||||
position, this._useRight(position.x), this._useBottom(position.y));
|
||||
}
|
||||
|
||||
_setPosition(viewportPosition, atRight, atBottom) {
|
||||
const horizontalMode = atRight ? 'right' : 'left';
|
||||
const verticalMode = atBottom ? 'bottom' : 'top';
|
||||
this.bodyNode.className = horizontalMode + ' ' + verticalMode;
|
||||
const pageX = viewportPosition.x + window.scrollX;
|
||||
this.style.left = `${pageX}px`;
|
||||
const pageY = viewportPosition.y + window.scrollY;
|
||||
this.style.top = `${pageY}px`;
|
||||
}
|
||||
|
||||
_useBottom(viewportY) {
|
||||
return viewportY <= 400;
|
||||
}
|
||||
|
||||
_useRight(viewportX) {
|
||||
return viewportX < document.documentElement.clientWidth / 2;
|
||||
}
|
||||
|
||||
set content(content) {
|
||||
if (!content) return this.hide();
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this._intersectionObserver = new IntersectionObserver((entries) => {
|
||||
if (entries[0].intersectionRatio <= 0) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
if (this._content === content) return;
|
||||
this._content = content;
|
||||
|
||||
if (typeof content === 'string') {
|
||||
this.contentNode.innerHTML = content;
|
||||
this.contentNode.className = 'textContent';
|
||||
} else if (content?.nodeType && content?.nodeName) {
|
||||
this._setContentNode(content);
|
||||
} else {
|
||||
if (this.contentNode.firstChild?.localName == 'property-link-table') {
|
||||
this.contentNode.firstChild.propertyDict = content;
|
||||
} else {
|
||||
const node = DOM.element('property-link-table');
|
||||
node.instanceLinkButtons = true;
|
||||
node.propertyDict = content;
|
||||
this._setContentNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setContentNode(content) {
|
||||
const newContent = DOM.div();
|
||||
newContent.appendChild(content);
|
||||
this.contentNode.replaceWith(newContent);
|
||||
newContent.id = 'content';
|
||||
}
|
||||
|
||||
hide() {
|
||||
this._content = undefined;
|
||||
if (this._isHidden) return;
|
||||
this._isHidden = true;
|
||||
this.bodyNode.style.display = 'none';
|
||||
this.targetNode = undefined;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this._isHidden) return;
|
||||
this.bodyNode.style.display = 'block';
|
||||
this._isHidden = false;
|
||||
}
|
||||
|
||||
get bodyNode() {
|
||||
return this.$('#body');
|
||||
}
|
||||
|
||||
get contentNode() {
|
||||
return this.$('#content');
|
||||
this.requestUpdate(true);
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', (event) => {
|
||||
// Only hide the tooltip if we click anywhere outside of it.
|
||||
let target = event.target;
|
||||
while (target) {
|
||||
if (target == this) return;
|
||||
target = target.parentNode;
|
||||
}
|
||||
this.hide()
|
||||
});
|
||||
}
|
||||
|
||||
_update() {
|
||||
if (!this._targetNode || this._isHidden) return;
|
||||
if (!this._targetNode.parentNode) return;
|
||||
const rect = this._targetNode.getBoundingClientRect();
|
||||
rect.x += rect.width / 2;
|
||||
let atRight = this._useRight(rect.x);
|
||||
let atBottom = this._useBottom(rect.y);
|
||||
if (atBottom) rect.y += rect.height;
|
||||
this._setPosition(rect, atRight, atBottom);
|
||||
this.requestUpdate(true);
|
||||
}
|
||||
|
||||
set data({content, positionOrTargetNode, immediate}) {
|
||||
if (immediate) {
|
||||
this._debouncedSetData.callNow(content, positionOrTargetNode)
|
||||
} else {
|
||||
this._debouncedSetData.call(content, positionOrTargetNode)
|
||||
}
|
||||
}
|
||||
|
||||
_setData(content, positionOrTargetNode) {
|
||||
if (positionOrTargetNode.nodeType === undefined) {
|
||||
this._targetNode = undefined;
|
||||
const position = positionOrTargetNode;
|
||||
this._setPosition(
|
||||
position, this._useRight(position.x), this._useBottom(position.y));
|
||||
} else {
|
||||
this._setTargetNode(positionOrTargetNode);
|
||||
}
|
||||
this._setContent(content);
|
||||
}
|
||||
|
||||
_setTargetNode(targetNode) {
|
||||
this._intersectionObserver.disconnect();
|
||||
this._targetNode = targetNode;
|
||||
if (targetNode === undefined) return;
|
||||
if (!(targetNode instanceof SVGElement)) {
|
||||
this._intersectionObserver.observe(targetNode);
|
||||
}
|
||||
this.requestUpdate(true);
|
||||
}
|
||||
|
||||
_setPosition(viewportPosition, atRight, atBottom) {
|
||||
const horizontalMode = atRight ? 'right' : 'left';
|
||||
const verticalMode = atBottom ? 'bottom' : 'top';
|
||||
this.bodyNode.className = horizontalMode + ' ' + verticalMode;
|
||||
const pageX = viewportPosition.x + window.scrollX;
|
||||
this.style.left = `${pageX}px`;
|
||||
const pageY = viewportPosition.y + window.scrollY;
|
||||
this.style.top = `${pageY}px`;
|
||||
}
|
||||
|
||||
_useBottom(viewportY) {
|
||||
return viewportY <= 400;
|
||||
}
|
||||
|
||||
_useRight(viewportX) {
|
||||
return viewportX < document.documentElement.clientWidth / 2;
|
||||
}
|
||||
|
||||
_setContent(content) {
|
||||
if (!content) return this.hide();
|
||||
this.show();
|
||||
if (this._content === content) return;
|
||||
this._content = content;
|
||||
|
||||
if (typeof content === 'string') {
|
||||
this.contentNode.innerHTML = content;
|
||||
this.contentNode.className = 'textContent';
|
||||
} else if (content?.nodeType && content?.nodeName) {
|
||||
this._setContentNode(content);
|
||||
} else {
|
||||
if (this.contentNode.firstChild?.localName == 'property-link-table') {
|
||||
this.contentNode.firstChild.propertyDict = content;
|
||||
} else {
|
||||
const node = DOM.element('property-link-table');
|
||||
node.instanceLinkButtons = true;
|
||||
node.propertyDict = content;
|
||||
this._setContentNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setContentNode(content) {
|
||||
const newContent = DOM.div();
|
||||
newContent.appendChild(content);
|
||||
this.contentNode.replaceWith(newContent);
|
||||
newContent.id = 'content';
|
||||
}
|
||||
|
||||
hide() {
|
||||
this._content = undefined;
|
||||
if (this._isHidden) return;
|
||||
this._isHidden = true;
|
||||
this.bodyNode.style.display = 'none';
|
||||
this.targetNode = undefined;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this._isHidden) return;
|
||||
this.bodyNode.style.display = 'block';
|
||||
this._isHidden = false;
|
||||
}
|
||||
|
||||
get bodyNode() {
|
||||
return this.$('#body');
|
||||
}
|
||||
|
||||
get contentNode() {
|
||||
return this.$('#content');
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user