Bug 1572651 - (Part 1) Add highlighter renderer base class r=jdescottes,pbro,bgrins

**Update October 8**
 To use the new box model highlighter, flip this pref to true:
`devtools.inspector.use-new-box-model-highlighter`

---

Adding Julian as reviewer to check the sanity of the communication system and Patrick for the overall highlighter behavior.

---

This patch adds a base class for the renderer part of highlighters which is set up on the parent process in the browser window.
This is used by the `BoxModelHighlighterRender` introduced by D47092

`HighlighterRenderer.init()` will create an HTML iframe and inject it to the appropriate position in the browser window in order to serve as a rendering surface for highlighters of the inspected page content (content toolbox) or the browser UI (browser toolbox). A host iframe is used until [Bug 1492582](https://bugzilla.mozilla.org/show_bug.cgi?id=1492582) is fixed because the browser window is XUL and does not support the anonymous canvas frame used by existing highlighters.

The primary use case of `HighlighterRenderer` is as a base class for renderers which live in a separate process than the observers. This happens with highlighters for the content toolbox. Therefore, it provides methods to setup a communication system (via MessageManager for now) whereby the observer can send messages to the renderer and vice-versa: `setMessageManager()`, `postMessage()` and `onMessage()`.

I used the existing code from [`AccessibilityParent`](https://searchfox.org/mozilla-central/source/devtools/server/actors/accessibility/accessibility-parent.js) as a reference for this.

Classes that extend HighlighterRenderer must implement:
 - a typeName representing the highlighter type; used to differentiate between other types of messages when the Message Manager is used;
 - a _buildMarkup() method to generate the highlighter markup;
 - a `render()` method to update the highlighter markup when given new information about the observed node.

NOTE: A temporary pink outline is set on the highlighter surface as a quick visual check to show its extent depending on context: browser toolbox or content toolbox. This will be removed before landing.

Differential Revision: https://phabricator.services.mozilla.com/D47091

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Razvan Caliman 2019-10-10 14:13:23 +00:00
parent ba43684bbe
commit c34e4a24a6
3 changed files with 224 additions and 0 deletions

View File

@ -1347,3 +1347,24 @@ toolbarpaletteitem > toolbaritem {
toolbar[keyNav=true]:not([collapsed=true]):not([customizing=true]) toolbartabstop {
-moz-user-focus: normal;
}
/* Frame used for rendering the DevTools inspector highlighters */
iframe.devtools-highlighter-renderer {
border: none;
pointer-events: none;
}
/* Highlighter for the Browser Toolbox */
:root > iframe.devtools-highlighter-renderer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
/* Highlighter for web content */
.browserStack > iframe.devtools-highlighter-renderer {
-moz-box-flex: 1;
}

View File

@ -0,0 +1,202 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Services = require("Services");
const {
HighlighterEnvironment,
} = require("devtools/server/actors/highlighters");
const {
CanvasFrameAnonymousContentHelper,
} = require("devtools/server/actors/highlighters/utils/markup");
/**
* HighlighterRenderer is the base class that implements the rendering surface for a
* highlighter in the parent process.
*
* It injects an iframe in the browser window which hosts the anonymous canvas
* frame where the highlighter's markup is generated and manipulated.
*
* This is the renderer part of a highlighter. It has a counterpart: the observer part of
* a highlighter which lives in the content process so it can observe changes to a node's
* position and attributes over time. The observer communicates any changes either through
* messages (via message manager) or directly to the renderer which updates the
* highlighter markup.
*
* NOTE: When the highlighter is used in the context of the browser toolbox, for example,
* when inspecting the browser UI, both observer and renderer live in the parent process
* and communication is done by direct reference, not using messages.
*
* Classes that extend HighlighterRenderer must implement:
* - a `typeName` string to identify the highighter type
* - a `_buildMarkup()` method to generate the highlighter markup;
* - a `render()` method to update the highlighter markup when given new information
* about the observed node.
*/
class HighlighterRenderer {
constructor() {
// The highlighter's type name. To be implemented by sub classes.
this.typeName = "";
this.onMessage = this.onMessage.bind(this);
}
/**
* Create an HTML iframe in order to use the anonymous canvas frame within it
* for hosting and manipulating the highlighter's markup.
*
* @param {Boolean} isBrowserToolboxHighlighter
* Whether the highlighter is used in the context of the
* browser toolbox (as opposed to the content toolbox).
* When set to true, this will influence where the
* iframe is appended so that it overlaps the browser UI
* (as opposed to overlapping just the page content).
*/
init(isBrowserToolboxHighlighter) {
// Get a reference to the parent process window.
this.win = Services.wm.getMostRecentBrowserWindow();
const { gBrowser } = this.win;
// Get a reference to the selected <browser> element.
const browser = gBrowser.selectedBrowser;
const browserContainer = gBrowser.getBrowserContainer(browser);
// The parent node of the iframe depends on the highlighter context:
// - browser toolbox: top-level browser window
// - content toolbox: host node of the <browser> element for the selected tab
const parent = isBrowserToolboxHighlighter
? this.win.document.documentElement
: browserContainer.querySelector(".browserStack");
// Grab the host iframe if it was previously created by another highlighter.
const iframe = parent.querySelector(
`:scope > .devtools-highlighter-renderer`
);
if (iframe) {
this.iframe = iframe;
this.setupMarkup();
} else {
this.iframe = this.win.document.createElement("iframe");
this.iframe.classList.add("devtools-highlighter-renderer");
if (isBrowserToolboxHighlighter) {
parent.append(this.iframe);
} else {
// Ensure highlighters are drawn underneath alerts and dialog boxes.
parent.querySelector("browser").after(this.iframe);
}
this.iframe.contentWindow.addEventListener(
"DOMContentLoaded",
this.setupMarkup.bind(this)
);
}
}
/**
* Generate the highlighter markup and insert it into the anoymous canvas frame.
*/
setupMarkup() {
if (!this.iframe || !this.iframe.contentWindow) {
throw Error(
"The highlighter renderer's host iframe is missing or not yet ready"
);
}
this.highlighterEnv = new HighlighterEnvironment();
this.highlighterEnv.initFromWindow(this.iframe.contentWindow);
this.markup = new CanvasFrameAnonymousContentHelper(
this.highlighterEnv,
this._buildMarkup.bind(this)
);
}
/**
* Set up message manager listener to listen for messages
* coming from the from the child process.
*
* @param {Object} mm
* Message manager that corresponds to the current content tab.
* @param {String} prefix
* Cross-process connection prefix.
*/
setMessageManager(mm, prefix) {
if (this.messageManager === mm) {
return;
}
// Message name used to distinguish between messages over the message manager.
this._msgName = `debug:${prefix}${this.typeName}`;
if (this.messageManager) {
// If the browser was swapped we need to reset the message manager.
const oldMM = this.messageManager;
oldMM.removeMessageListener(this._msgName, this.onMessage);
}
this.messageManager = mm;
if (mm) {
mm.addMessageListener(this._msgName, this.onMessage);
}
}
postMessage(topic, data = {}) {
this.messageManager.sendAsyncMessage(`${this._msgName}:event`, {
topic,
data,
});
}
/**
* Handler for messages coming from the content process.
*
* @param {Object} msg
* Data payload associated with the message.
*/
onMessage(msg) {
const { topic, data } = msg.json;
switch (topic) {
case "render":
this.render(data);
break;
case "destroy":
this.destroy();
break;
}
}
render() {
// When called, sub classes should update the highlighter.
// To be implemented by sub classes.
throw new Error(
"Highlighter renderer class had to implement render method"
);
}
destroy() {
if (this.highlighterEnv) {
this.highlighterEnv.destroy();
this.highlighterEnv = null;
}
if (this.markup) {
this.markup.destroy();
this.markup = null;
}
if (this.iframe) {
this.iframe.remove();
this.iframe = null;
}
this.win = null;
this.setMessageManager(null);
}
}
exports.HighlighterRenderer = HighlighterRenderer;

View File

@ -18,6 +18,7 @@ DevToolsModules(
'flexbox.js',
'fonts.js',
'geometry-editor.js',
'highlighter-renderer.js',
'measuring-tool.js',
'paused-debugger.js',
'rulers.js',