ToolboxToolbar.js (18785B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 const { 7 Component, 8 createFactory, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const { div, button } = dom; 13 const MenuButton = createFactory( 14 require("resource://devtools/client/shared/components/menu/MenuButton.js") 15 ); 16 const ToolboxTabs = createFactory( 17 require("resource://devtools/client/framework/components/ToolboxTabs.js") 18 ); 19 loader.lazyGetter(this, "MeatballMenu", function () { 20 return createFactory( 21 require("resource://devtools/client/framework/components/MeatballMenu.js") 22 ); 23 }); 24 loader.lazyGetter(this, "MenuItem", function () { 25 return createFactory( 26 require("resource://devtools/client/shared/components/menu/MenuItem.js") 27 ); 28 }); 29 loader.lazyGetter(this, "MenuList", function () { 30 return createFactory( 31 require("resource://devtools/client/shared/components/menu/MenuList.js") 32 ); 33 }); 34 loader.lazyGetter(this, "LocalizationProvider", function () { 35 return createFactory( 36 require("resource://devtools/client/shared/vendor/fluent-react.js") 37 .LocalizationProvider 38 ); 39 }); 40 loader.lazyGetter(this, "DebugTargetInfo", () => 41 createFactory( 42 require("resource://devtools/client/framework/components/DebugTargetInfo.js") 43 ) 44 ); 45 loader.lazyGetter(this, "ChromeDebugToolbar", () => 46 createFactory( 47 require("resource://devtools/client/framework/components/ChromeDebugToolbar.js") 48 ) 49 ); 50 51 loader.lazyRequireGetter( 52 this, 53 "getUnicodeUrl", 54 "resource://devtools/client/shared/unicode-url.js", 55 true 56 ); 57 58 /** 59 * This is the overall component for the toolbox toolbar. It is designed to not know how 60 * the state is being managed, and attempts to be as pure as possible. The 61 * ToolboxController component controls the changing state, and passes in everything as 62 * props. 63 */ 64 class ToolboxToolbar extends Component { 65 static get propTypes() { 66 return { 67 // The currently focused item (for arrow keyboard navigation) 68 // This ID determines the tabindex being 0 or -1. 69 focusedButton: PropTypes.string, 70 // List of command button definitions. 71 toolboxButtons: PropTypes.array, 72 // The id of the currently selected tool, e.g. "inspector" 73 currentToolId: PropTypes.string, 74 // An optionally highlighted tools, e.g. "inspector" (used by ToolboxTabs 75 // component). 76 highlightedTools: PropTypes.instanceOf(Set), 77 // List of tool panel definitions (used by ToolboxTabs component). 78 panelDefinitions: PropTypes.array, 79 // List of possible docking options. 80 hostTypes: PropTypes.arrayOf( 81 PropTypes.shape({ 82 position: PropTypes.string.isRequired, 83 switchHost: PropTypes.func.isRequired, 84 }) 85 ), 86 // Current docking type. Typically one of the position values in 87 // |hostTypes| but this is not always the case (e.g. for "browsertoolbox"). 88 currentHostType: PropTypes.string, 89 // Are docking options enabled? They are not enabled in certain situations 90 // like when the toolbox is opened in a tab. 91 areDockOptionsEnabled: PropTypes.bool, 92 // Do we need to add UI for closing the toolbox? We don't when the 93 // toolbox is undocked, for example. 94 canCloseToolbox: PropTypes.bool, 95 // Is the split console currently visible? 96 isSplitConsoleActive: PropTypes.bool, 97 // Are we disabling the behavior where pop-ups are automatically closed 98 // when clicking outside them? 99 // 100 // This is a tri-state value that may be true/false or undefined where 101 // undefined means that the option is not relevant in this context 102 // (i.e. we're not in a browser toolbox). 103 disableAutohide: PropTypes.bool, 104 // Are we displaying the window always on top? 105 // 106 // This is a tri-state value that may be true/false or undefined where 107 // undefined means that the option is not relevant in this context 108 // (i.e. we're not in a local web extension toolbox). 109 alwaysOnTop: PropTypes.bool, 110 // Is the toolbox currently focused? 111 // 112 // This will only be defined when alwaysOnTop is true. 113 focusedState: PropTypes.bool, 114 // Function to turn the options panel on / off. 115 toggleOptions: PropTypes.func.isRequired, 116 // Function to turn the split console on / off. 117 toggleSplitConsole: PropTypes.func, 118 // Function to turn the disable pop-up autohide behavior on / off. 119 toggleNoAutohide: PropTypes.func, 120 // Function to turn the always on top behavior on / off. 121 toggleAlwaysOnTop: PropTypes.func, 122 // Function to completely close the toolbox. 123 closeToolbox: PropTypes.func, 124 // Keep a record of what button is focused. 125 focusButton: PropTypes.func, 126 // Hold off displaying the toolbar until enough information is ready for 127 // it to render nicely. 128 canRender: PropTypes.bool, 129 // Localization interface. 130 L10N: PropTypes.object.isRequired, 131 // The devtools toolbox 132 toolbox: PropTypes.object, 133 // Call back function to detect tabs order updated. 134 onTabsOrderUpdated: PropTypes.func.isRequired, 135 // Count of visible toolbox buttons which is used by ToolboxTabs component 136 // to recognize that the visibility of toolbox buttons were changed. 137 // Because in the component we cannot compare the visibility since the 138 // button definition instance in toolboxButtons will be unchanged. 139 visibleToolboxButtonCount: PropTypes.number, 140 // Data to show debug target info, if needed 141 debugTargetData: PropTypes.shape({ 142 runtimeInfo: PropTypes.object.isRequired, 143 descriptorType: PropTypes.string.isRequired, 144 }), 145 // The loaded Fluent localization bundles. 146 fluentBundles: PropTypes.array.isRequired, 147 }; 148 } 149 150 constructor(props) { 151 super(props); 152 153 this.hideMenu = this.hideMenu.bind(this); 154 this.createFrameList = this.createFrameList.bind(this); 155 this.highlightFrame = this.highlightFrame.bind(this); 156 } 157 158 componentDidMount() { 159 this.props.toolbox.on("panel-changed", this.hideMenu); 160 } 161 162 componentWillUnmount() { 163 this.props.toolbox.off("panel-changed", this.hideMenu); 164 } 165 166 hideMenu() { 167 if (this.refs.meatballMenuButton) { 168 this.refs.meatballMenuButton.hideMenu(); 169 } 170 171 if (this.refs.frameMenuButton) { 172 this.refs.frameMenuButton.hideMenu(); 173 } 174 } 175 176 /** 177 * A little helper function to call renderToolboxButtons for buttons at the start 178 * of the toolbox. 179 */ 180 renderToolboxButtonsStart() { 181 return this.renderToolboxButtons(true); 182 } 183 184 /** 185 * A little helper function to call renderToolboxButtons for buttons at the end 186 * of the toolbox. 187 */ 188 renderToolboxButtonsEnd() { 189 return this.renderToolboxButtons(false); 190 } 191 192 /** 193 * Render all of the tabs, this takes in a list of toolbox button states. These are plain 194 * objects that have all of the relevant information needed to render the button. 195 * See Toolbox.prototype._createButtonState in devtools/client/framework/toolbox.js for 196 * documentation on this object. 197 * 198 * @param {string} focusedButton - The id of the focused button. 199 * @param {Array} toolboxButtons - Array of objects that define the command buttons. 200 * @param {Function} focusButton - Keep a record of the currently focused button. 201 * @param {boolean} isStart - Render either the starting buttons, or ending buttons. 202 */ 203 renderToolboxButtons(isStart) { 204 const { focusedButton, toolboxButtons, focusButton } = this.props; 205 const visibleButtons = toolboxButtons.filter(command => { 206 const { isVisible, isInStartContainer } = command; 207 return isVisible && (isStart ? isInStartContainer : !isInStartContainer); 208 }); 209 210 if (visibleButtons.length === 0) { 211 return null; 212 } 213 214 // The RDM button, if present, should always go last 215 const rdmIndex = visibleButtons.findIndex( 216 button => button.id === "command-button-responsive" 217 ); 218 if (rdmIndex !== -1 && rdmIndex !== visibleButtons.length - 1) { 219 const rdm = visibleButtons.splice(rdmIndex, 1)[0]; 220 visibleButtons.push(rdm); 221 } 222 223 const renderedButtons = visibleButtons.map(command => { 224 const { 225 id, 226 description, 227 disabled, 228 onClick, 229 isChecked, 230 isToggle, 231 className: buttonClass, 232 onKeyDown, 233 } = command; 234 235 // If button is frame button, create menu button in order to 236 // use the doorhanger menu. 237 if (id === "command-button-frames") { 238 return this.renderFrameButton(command); 239 } 240 241 if (id === "command-button-errorcount") { 242 return this.renderErrorIcon(command); 243 } 244 245 return button({ 246 id, 247 title: description, 248 disabled, 249 "aria-pressed": !isToggle ? null : isChecked, 250 className: `devtools-tabbar-button command-button ${ 251 buttonClass || "" 252 } ${isChecked ? "checked" : ""}`, 253 onClick: event => { 254 onClick(event); 255 focusButton(id); 256 }, 257 onFocus: () => focusButton(id), 258 tabIndex: id === focusedButton ? "0" : "-1", 259 onKeyDown: event => { 260 onKeyDown(event); 261 }, 262 onContextMenu: event => { 263 const menu = command.getContextMenu(); 264 if (!menu) { 265 return; 266 } 267 268 event.preventDefault(); 269 event.stopPropagation(); 270 271 menu.popup(event.screenX, event.screenY, window.parent.document); 272 }, 273 }); 274 }); 275 276 // Add the appropriate separator, if needed. 277 const children = renderedButtons; 278 if (renderedButtons.length) { 279 if (isStart) { 280 children.push(this.renderSeparator()); 281 // For the end group we add a separator *before* the RDM button if it 282 // exists, but only if it is not the only button. 283 } else if (rdmIndex !== -1 && renderedButtons.length > 1) { 284 children.splice(children.length - 1, 0, this.renderSeparator()); 285 } 286 } 287 288 return div( 289 { id: `toolbox-buttons-${isStart ? "start" : "end"}` }, 290 ...children 291 ); 292 } 293 294 renderFrameButton(command) { 295 const { id, isChecked, disabled, description } = command; 296 297 const { toolbox } = this.props; 298 299 return MenuButton( 300 { 301 id, 302 disabled, 303 menuId: id + "-panel", 304 toolboxDoc: toolbox.doc, 305 className: `devtools-tabbar-button command-button ${ 306 isChecked ? "checked" : "" 307 }`, 308 ref: "frameMenuButton", 309 title: description, 310 onCloseButton: async () => { 311 // Only try to unhighlight if the inspectorFront has been created already 312 const inspectorFront = toolbox.target.getCachedFront("inspector"); 313 if (inspectorFront) { 314 const highlighter = toolbox.getHighlighter(); 315 await highlighter.unhighlight(); 316 } 317 }, 318 }, 319 this.createFrameList 320 ); 321 } 322 323 renderErrorIcon(command) { 324 let { errorCount, id } = command; 325 326 if (!errorCount) { 327 return null; 328 } 329 330 if (errorCount > 99) { 331 errorCount = "99+"; 332 } 333 334 const errorIconTooltip = this.props.toolbox.isSplitConsoleEnabled() 335 ? this.props.L10N.getStr("toolbox.errorCountButton.tooltip") 336 : this.props.L10N.getStr("toolbox.errorCountButtonConsoleTab.tooltip"); 337 338 return button( 339 { 340 id, 341 className: "devtools-tabbar-button command-button toolbox-error", 342 onClick: () => { 343 if (this.props.currentToolId !== "webconsole") { 344 this.props.toolbox.openSplitConsole(); 345 } 346 }, 347 title: 348 this.props.currentToolId !== "webconsole" ? errorIconTooltip : null, 349 }, 350 errorCount 351 ); 352 } 353 354 highlightFrame(id) { 355 const { toolbox } = this.props; 356 if (!id) { 357 return; 358 } 359 360 toolbox.onHighlightFrame(id); 361 } 362 363 createFrameList() { 364 const { toolbox } = this.props; 365 if (toolbox.frameMap.size < 1) { 366 return null; 367 } 368 369 const items = []; 370 toolbox.frameMap.forEach(frame => { 371 const label = toolbox.commands.descriptorFront.isWebExtensionDescriptor 372 ? toolbox.getExtensionPathName(frame.url) 373 : getUnicodeUrl(frame.url); 374 375 const item = MenuItem({ 376 id: frame.id.toString(), 377 key: "toolbox-frame-key-" + frame.id, 378 label, 379 checked: frame.id === toolbox.selectedFrameId, 380 onClick: () => toolbox.onIframePickerFrameSelected(frame.id), 381 }); 382 383 // Always put the top level frame at the top 384 if (frame.isTopLevel) { 385 items.unshift(item); 386 } else { 387 items.push(item); 388 } 389 }); 390 391 return MenuList( 392 { 393 id: "toolbox-frame-menu", 394 onHighlightedChildChange: this.highlightFrame, 395 }, 396 items 397 ); 398 } 399 400 /** 401 * Render a separator. 402 */ 403 renderSeparator() { 404 return div({ className: "devtools-separator" }); 405 } 406 407 /** 408 * Render the toolbox control buttons. The following props are expected: 409 * 410 * @param {string} props.focusedButton 411 * The id of the focused button. 412 * @param {string} props.currentToolId 413 * The id of the currently selected tool, e.g. "inspector". 414 * @param {object[]} props.hostTypes 415 * Array of host type objects. 416 * @param {string} props.hostTypes[].position 417 * Position name. 418 * @param {Function} props.hostTypes[].switchHost 419 * Function to switch the host. 420 * @param {string} props.currentHostType 421 * The current docking configuration. 422 * @param {boolean} props.areDockOptionsEnabled 423 * They are not enabled in certain situations like when the toolbox is 424 * in a tab. 425 * @param {boolean} props.canCloseToolbox 426 * Do we need to add UI for closing the toolbox? We don't when the 427 * toolbox is undocked, for example. 428 * @param {boolean} props.isSplitConsoleActive 429 * Is the split console currently visible? 430 * toolbox is undocked, for example. 431 * @param {boolean|undefined} props.disableAutohide 432 * Are we disabling the behavior where pop-ups are automatically 433 * closed when clicking outside them? 434 * (Only defined for the browser toolbox.) 435 * @param {Function} props.selectTool 436 * Function to select a tool based on its id. 437 * @param {Function} props.toggleOptions 438 * Function to turn the options panel on / off. 439 * @param {Function} props.toggleSplitConsole 440 * Function to turn the split console on / off. 441 * @param {Function} props.toggleNoAutohide 442 * Function to turn the disable pop-up autohide behavior on / off. 443 * @param {Function} props.toggleAlwaysOnTop 444 * Function to turn the always on top behavior on / off. 445 * @param {Function} props.closeToolbox 446 * Completely close the toolbox. 447 * @param {Function} props.focusButton 448 * Keep a record of the currently focused button. 449 * @param {object} props.L10N 450 * Localization interface. 451 * @param {object} props.toolbox 452 * The devtools toolbox. Used by the MenuButton component to display 453 * the menu popup. 454 * @param {object} refs 455 * The components refs object. Used to keep a reference to the MenuButton 456 * for the meatball menu so that we can tell it to resize its contents 457 * when they change. 458 */ 459 renderToolboxControls() { 460 const { 461 focusedButton, 462 canCloseToolbox, 463 closeToolbox, 464 focusButton, 465 L10N, 466 toolbox, 467 } = this.props; 468 469 const meatballMenuButtonId = "toolbox-meatball-menu-button"; 470 471 const meatballMenuButton = MenuButton( 472 { 473 id: meatballMenuButtonId, 474 menuId: meatballMenuButtonId + "-panel", 475 toolboxDoc: toolbox.doc, 476 onFocus: () => focusButton(meatballMenuButtonId), 477 className: "devtools-tabbar-button", 478 title: L10N.getStr("toolbox.meatballMenu.button.tooltip"), 479 tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1", 480 ref: "meatballMenuButton", 481 }, 482 MeatballMenu({ 483 ...this.props, 484 hostTypes: this.props.areDockOptionsEnabled ? this.props.hostTypes : [], 485 onResize: () => { 486 this.refs.meatballMenuButton.resizeContent(); 487 }, 488 }) 489 ); 490 491 const closeButtonId = "toolbox-close"; 492 493 const closeButton = canCloseToolbox 494 ? button({ 495 id: closeButtonId, 496 onFocus: () => focusButton(closeButtonId), 497 className: "devtools-tabbar-button", 498 title: L10N.getStr("toolbox.closebutton.tooltip"), 499 onClick: () => closeToolbox(), 500 tabIndex: focusedButton === "toolbox-close" ? "0" : "-1", 501 }) 502 : null; 503 504 return div({ id: "toolbox-controls" }, meatballMenuButton, closeButton); 505 } 506 507 /** 508 * The render function is kept fairly short for maintainability. See the individual 509 * render functions for how each of the sections is rendered. 510 */ 511 render() { 512 const { L10N, debugTargetData, toolbox, fluentBundles } = this.props; 513 const classnames = ["devtools-tabbar"]; 514 const startButtons = this.renderToolboxButtonsStart(); 515 const endButtons = this.renderToolboxButtonsEnd(); 516 517 if (!startButtons) { 518 classnames.push("devtools-tabbar-has-start"); 519 } 520 if (!endButtons) { 521 classnames.push("devtools-tabbar-has-end"); 522 } 523 524 const toolbar = this.props.canRender 525 ? div( 526 { 527 className: classnames.join(" "), 528 }, 529 startButtons, 530 ToolboxTabs(this.props), 531 endButtons, 532 this.renderToolboxControls() 533 ) 534 : div({ className: classnames.join(" ") }); 535 536 const debugTargetInfo = debugTargetData 537 ? DebugTargetInfo({ 538 alwaysOnTop: this.props.alwaysOnTop, 539 focusedState: this.props.focusedState, 540 toggleAlwaysOnTop: this.props.toggleAlwaysOnTop, 541 debugTargetData, 542 L10N, 543 toolbox, 544 }) 545 : null; 546 547 // Display the toolbar in the MBT and about:debugging MBT if we have server support for it. 548 const chromeDebugToolbar = toolbox.commands.targetCommand.descriptorFront 549 .isBrowserProcessDescriptor 550 ? ChromeDebugToolbar() 551 : null; 552 553 return LocalizationProvider( 554 { bundles: fluentBundles }, 555 div({}, chromeDebugToolbar, debugTargetInfo, toolbar) 556 ); 557 } 558 } 559 560 module.exports = ToolboxToolbar;