Source code

Revision control

Copy as Markdown

Other Tools

/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
loader.lazyRequireGetter(
this,
"ResponsiveUI",
"resource://devtools/client/responsive/ui.js"
);
loader.lazyRequireGetter(
this,
"startup",
"resource://devtools/client/responsive/utils/window.js",
true
);
loader.lazyRequireGetter(
this,
"showNotification",
"resource://devtools/client/responsive/utils/notification.js",
true
);
loader.lazyRequireGetter(
this,
"l10n",
"resource://devtools/client/responsive/utils/l10n.js"
);
loader.lazyRequireGetter(
this,
"PriorityLevels",
"resource://devtools/client/shared/components/NotificationBox.js",
true
);
loader.lazyRequireGetter(
this,
"gDevTools",
"resource://devtools/client/framework/devtools.js",
true
);
loader.lazyRequireGetter(
this,
"gDevToolsBrowser",
"resource://devtools/client/framework/devtools-browser.js",
true
);
loader.lazyRequireGetter(
this,
"Telemetry",
"resource://devtools/client/shared/telemetry.js"
);
/**
* ResponsiveUIManager is the external API for the browser UI, etc. to use when
* opening and closing the responsive UI.
*/
class ResponsiveUIManager {
constructor() {
this.activeTabs = new Map();
this.handleMenuCheck = this.handleMenuCheck.bind(this);
EventEmitter.decorate(this);
}
get telemetry() {
if (!this._telemetry) {
this._telemetry = new Telemetry();
}
return this._telemetry;
}
/**
* Toggle the responsive UI for a tab.
*
* @param window
* The main browser chrome window.
* @param tab
* The browser tab.
* @param options
* Other options associated with toggling. Currently includes:
* - `trigger`: String denoting the UI entry point, such as:
* - `toolbox`: Toolbox Button
* - `menu`: Browser Tools menu item
* - `shortcut`: Keyboard shortcut
* @return Promise
* Resolved when the toggling has completed. If the UI has opened,
* it is resolved to the ResponsiveUI instance for this tab. If the
* the UI has closed, there is no resolution value.
*/
toggle(window, tab, options = {}) {
const completed = this._toggleForTab(window, tab, options);
completed.catch(console.error);
return completed;
}
_toggleForTab(window, tab, options) {
if (this.isActiveForTab(tab)) {
return this.closeIfNeeded(window, tab, options);
}
return this.openIfNeeded(window, tab, options);
}
/**
* Opens the responsive UI, if not already open.
*
* @param window
* The main browser chrome window.
* @param tab
* The browser tab.
* @param options
* Other options associated with opening. Currently includes:
* - `trigger`: String denoting the UI entry point, such as:
* - `toolbox`: Toolbox Button
* - `menu`: Browser Tools menu item
* - `shortcut`: Keyboard shortcut
* @return Promise
* Resolved to the ResponsiveUI instance for this tab when opening is
* complete.
*/
async openIfNeeded(window, tab, options = {}) {
if (!this.isActiveForTab(tab)) {
this.initMenuCheckListenerFor(window);
const ui = new ResponsiveUI(this, window, tab);
this.activeTabs.set(tab, ui);
// Explicitly not await on telemetry to avoid delaying RDM opening
this.recordTelemetryOpen(window, tab, options);
await gDevToolsBrowser.loadBrowserStyleSheet(window);
await this.setMenuCheckFor(tab, window);
await ui.initialize();
this.emit("on", { tab });
}
return this.getResponsiveUIForTab(tab);
}
/**
* Record all telemetry probes related to RDM opening.
*/
recordTelemetryOpen(window, tab, options) {
// Track whether a toolbox was opened before RDM was opened.
const toolbox = gDevTools.getToolboxForTab(tab);
const hostType = toolbox ? toolbox.hostType : "none";
const hasToolbox = !!toolbox;
if (hasToolbox) {
Glean.devtoolsResponsive.toolboxOpenedFirst.add(1);
}
this.telemetry.recordEvent("activate", "responsive_design", null, {
host: hostType,
width: Math.ceil(window.outerWidth / 50) * 50,
});
// Track opens keyed by the UI entry point used.
let { trigger } = options;
if (!trigger) {
trigger = "unknown";
}
Glean.devtoolsResponsive.openTrigger[trigger].add(1);
}
/**
* Closes the responsive UI, if not already closed.
*
* @param window
* The main browser chrome window.
* @param tab
* The browser tab.
* @param options
* Other options associated with closing. Currently includes:
* - `trigger`: String denoting the UI entry point, such as:
* - `toolbox`: Toolbox Button
* - `menu`: Browser Tools menu item
* - `shortcut`: Keyboard shortcut
* - `reason`: String detailing the specific cause for closing
* @return Promise
* Resolved (with no value) when closing is complete.
*/
async closeIfNeeded(window, tab, options = {}) {
if (this.isActiveForTab(tab)) {
const ui = this.activeTabs.get(tab);
const destroyed = await ui.destroy(options);
if (!destroyed) {
// Already in the process of destroying, abort.
return;
}
this.activeTabs.delete(tab);
if (!this.isActiveForWindow(window)) {
this.removeMenuCheckListenerFor(window);
}
this.emit("off", { tab });
await this.setMenuCheckFor(tab, window);
// Explicitly not await on telemetry to avoid delaying RDM closing
this.recordTelemetryClose(window, tab);
}
}
recordTelemetryClose(window, tab) {
const toolbox = gDevTools.getToolboxForTab(tab);
const hostType = toolbox ? toolbox.hostType : "none";
this.telemetry.recordEvent("deactivate", "responsive_design", null, {
host: hostType,
width: Math.ceil(window.outerWidth / 50) * 50,
});
}
/**
* Returns true if responsive UI is active for a given tab.
*
* @param tab
* The browser tab.
* @return boolean
*/
isActiveForTab(tab) {
return this.activeTabs.has(tab);
}
/**
* Returns true if responsive UI is active in any tab in the given window.
*
* @param window
* The main browser chrome window.
* @return boolean
*/
isActiveForWindow(window) {
return [...this.activeTabs.keys()].some(t => t.ownerGlobal === window);
}
/**
* Return the responsive UI controller for a tab.
*
* @param tab
* The browser tab.
* @return ResponsiveUI
* The UI instance for this tab.
*/
getResponsiveUIForTab(tab) {
return this.activeTabs.get(tab);
}
handleMenuCheck({ target }) {
this.setMenuCheckFor(target);
}
initMenuCheckListenerFor(window) {
const { tabContainer } = window.gBrowser;
tabContainer.addEventListener("TabSelect", this.handleMenuCheck);
}
removeMenuCheckListenerFor(window) {
if (window?.gBrowser?.tabContainer) {
const { tabContainer } = window.gBrowser;
tabContainer.removeEventListener("TabSelect", this.handleMenuCheck);
}
}
async setMenuCheckFor(tab, window = tab.ownerGlobal) {
await startup(window);
const menu = window.document.getElementById("menu_responsiveUI");
if (menu) {
menu.setAttribute("checked", this.isActiveForTab(tab));
}
}
showRemoteOnlyNotification(window, tab, { trigger } = {}) {
return showNotification(window, tab, {
toolboxButton: trigger == "toolbox",
msg: l10n.getStr("responsive.remoteOnly"),
priority: PriorityLevels.PRIORITY_CRITICAL_MEDIUM,
});
}
}
module.exports = new ResponsiveUIManager();