App.js (11692B)
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 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import React, { Component } from "devtools/client/shared/vendor/react"; 6 import { 7 button, 8 div, 9 main, 10 span, 11 } from "devtools/client/shared/vendor/react-dom-factories"; 12 import PropTypes from "devtools/client/shared/vendor/react-prop-types"; 13 import { connect } from "devtools/client/shared/vendor/react-redux"; 14 import { prefs } from "../utils/prefs"; 15 import { primaryPaneTabs } from "../constants"; 16 import actions from "../actions/index"; 17 import DebuggerImage from "./shared/DebuggerImage"; 18 19 import { 20 getSelectedLocation, 21 getPaneCollapse, 22 getActiveSearch, 23 getQuickOpenEnabled, 24 getOrientation, 25 getIsCurrentThreadPaused, 26 isMapScopesEnabled, 27 getSourceMapErrorForSourceActor, 28 } from "../selectors/index"; 29 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js"); 30 31 const SplitBox = require("resource://devtools/client/shared/components/splitter/SplitBox.js"); 32 const AppErrorBoundary = require("resource://devtools/client/shared/components/AppErrorBoundary.js"); 33 34 const horizontalLayoutBreakpoint = window.matchMedia("(min-width: 800px)"); 35 const verticalLayoutBreakpoint = window.matchMedia( 36 "(min-width: 10px) and (max-width: 799px)" 37 ); 38 39 import { ShortcutsModal } from "./ShortcutsModal"; 40 import PrimaryPanes from "./PrimaryPanes/index"; 41 import Editor from "./Editor/index"; 42 import SecondaryPanes from "./SecondaryPanes/index"; 43 import WelcomeBox from "./WelcomeBox"; 44 import EditorTabs from "./Editor/Tabs"; 45 import EditorFooter from "./Editor/Footer"; 46 import QuickOpenModal from "./QuickOpenModal"; 47 48 class App extends Component { 49 #shortcuts; 50 51 constructor(props) { 52 super(props); 53 54 // The shortcuts should be built as early as possible because they are 55 // exposed via getChildContext. 56 this.#shortcuts = new KeyShortcuts({ window }); 57 58 this.state = { 59 shortcutsModalEnabled: false, 60 startPanelSize: 0, 61 endPanelSize: 0, 62 }; 63 } 64 65 static get propTypes() { 66 return { 67 activeSearch: PropTypes.oneOf(["file", "project"]), 68 closeActiveSearch: PropTypes.func.isRequired, 69 closeQuickOpen: PropTypes.func.isRequired, 70 endPanelCollapsed: PropTypes.bool.isRequired, 71 fluentBundles: PropTypes.array.isRequired, 72 openQuickOpen: PropTypes.func.isRequired, 73 orientation: PropTypes.oneOf(["horizontal", "vertical"]).isRequired, 74 quickOpenEnabled: PropTypes.bool.isRequired, 75 showWelcomeBox: PropTypes.bool.isRequired, 76 setActiveSearch: PropTypes.func.isRequired, 77 setOrientation: PropTypes.func.isRequired, 78 setPrimaryPaneTab: PropTypes.func.isRequired, 79 startPanelCollapsed: PropTypes.bool.isRequired, 80 toolboxDoc: PropTypes.object.isRequired, 81 showOriginalVariableMappingWarning: PropTypes.bool, 82 }; 83 } 84 85 getChildContext() { 86 return { 87 fluentBundles: this.props.fluentBundles, 88 toolboxDoc: this.props.toolboxDoc, 89 shortcuts: this.#shortcuts, 90 l10n: L10N, 91 }; 92 } 93 94 componentDidMount() { 95 horizontalLayoutBreakpoint.addListener(this.onLayoutChange); 96 verticalLayoutBreakpoint.addListener(this.onLayoutChange); 97 this.setOrientation(); 98 99 this.#shortcuts.on(L10N.getStr("symbolSearch.search.key2"), e => 100 this.toggleQuickOpenModal(e, "@") 101 ); 102 103 [ 104 L10N.getStr("sources.search.key2"), 105 L10N.getStr("sources.search.alt.key"), 106 ].forEach(key => this.#shortcuts.on(key, this.toggleQuickOpenModal)); 107 108 [ 109 L10N.getStr("gotoLineModal.key3"), 110 // For consistency with sourceeditor and codemirror5 shortcuts, map 111 // sourceeditor jumpToLine command key. 112 `CmdOrCtrl+${L10N.getStr("jumpToLine.commandkey")}`, 113 ].forEach(key => this.#shortcuts.on(key, this.toggleJumpToLine)); 114 115 this.#shortcuts.on( 116 L10N.getStr("projectTextSearch.key"), 117 this.jumpToProjectSearch 118 ); 119 120 this.#shortcuts.on("Escape", this.onEscape); 121 this.#shortcuts.on("CmdOrCtrl+/", this.onCommandSlash); 122 } 123 124 componentWillUnmount() { 125 horizontalLayoutBreakpoint.removeListener(this.onLayoutChange); 126 verticalLayoutBreakpoint.removeListener(this.onLayoutChange); 127 this.#shortcuts.destroy(); 128 } 129 130 jumpToProjectSearch = e => { 131 e.preventDefault(); 132 this.props.setPrimaryPaneTab(primaryPaneTabs.PROJECT_SEARCH); 133 this.props.setActiveSearch(primaryPaneTabs.PROJECT_SEARCH); 134 }; 135 136 onEscape = e => { 137 const { 138 activeSearch, 139 closeActiveSearch, 140 closeQuickOpen, 141 quickOpenEnabled, 142 } = this.props; 143 const { shortcutsModalEnabled } = this.state; 144 145 if (activeSearch) { 146 e.preventDefault(); 147 closeActiveSearch(); 148 } 149 150 if (quickOpenEnabled) { 151 e.preventDefault(); 152 closeQuickOpen(); 153 } 154 155 if (shortcutsModalEnabled) { 156 e.preventDefault(); 157 this.toggleShortcutsModal(); 158 } 159 }; 160 161 onCommandSlash = () => { 162 this.toggleShortcutsModal(); 163 }; 164 165 isHorizontal() { 166 return this.props.orientation === "horizontal"; 167 } 168 169 toggleJumpToLine = e => { 170 this.toggleQuickOpenModal(e, ":"); 171 }; 172 173 toggleQuickOpenModal = (e, query) => { 174 const { quickOpenEnabled, openQuickOpen, closeQuickOpen } = this.props; 175 176 e.preventDefault(); 177 e.stopPropagation(); 178 179 if (quickOpenEnabled === true) { 180 closeQuickOpen(); 181 return; 182 } 183 184 if (query != null) { 185 openQuickOpen(query); 186 return; 187 } 188 openQuickOpen(); 189 }; 190 191 onLayoutChange = () => { 192 this.setOrientation(); 193 }; 194 195 setOrientation() { 196 // If the orientation does not match (if it is not visible) it will 197 // not setOrientation, or if it is the same as before, calling 198 // setOrientation will not cause a rerender. 199 if (horizontalLayoutBreakpoint.matches) { 200 this.props.setOrientation("horizontal"); 201 } else if (verticalLayoutBreakpoint.matches) { 202 this.props.setOrientation("vertical"); 203 } 204 } 205 206 closeSourceMapError = () => { 207 this.setState({ hiddenSourceMapError: this.props.sourceMapError }); 208 }; 209 210 renderEditorNotificationBar() { 211 if ( 212 this.props.sourceMapError && 213 this.state.hiddenSourceMapError != this.props.sourceMapError 214 ) { 215 return div( 216 { className: "editor-notification-footer", "aria-role": "status" }, 217 span( 218 { className: "info icon" }, 219 React.createElement(DebuggerImage, { name: "sourcemap" }) 220 ), 221 `Source Map Error: ${this.props.sourceMapError}`, 222 button({ className: "close-button", onClick: this.closeSourceMapError }) 223 ); 224 } 225 if (this.props.showOriginalVariableMappingWarning) { 226 return div( 227 { className: "editor-notification-footer", "aria-role": "status" }, 228 span( 229 { className: "info icon" }, 230 React.createElement(DebuggerImage, { name: "sourcemap" }) 231 ), 232 L10N.getFormatStr( 233 "editorNotificationFooter.noOriginalScopes", 234 L10N.getStr("scopes.showOriginalScopes") 235 ) 236 ); 237 } 238 return null; 239 } 240 241 renderEditorPane = () => { 242 const { startPanelCollapsed, endPanelCollapsed } = this.props; 243 const { endPanelSize, startPanelSize } = this.state; 244 const horizontal = this.isHorizontal(); 245 return main( 246 { 247 className: "editor-pane", 248 }, 249 div( 250 { 251 className: "editor-container", 252 }, 253 React.createElement(EditorTabs, { 254 startPanelCollapsed, 255 endPanelCollapsed, 256 horizontal, 257 }), 258 React.createElement(Editor, { 259 startPanelSize, 260 endPanelSize, 261 }), 262 this.props.showWelcomeBox 263 ? React.createElement(WelcomeBox, { 264 horizontal, 265 toggleShortcutsModal: () => this.toggleShortcutsModal(), 266 }) 267 : null, 268 this.renderEditorNotificationBar(), 269 React.createElement(EditorFooter, { 270 horizontal, 271 }) 272 ) 273 ); 274 }; 275 276 toggleShortcutsModal() { 277 this.setState(prevState => ({ 278 shortcutsModalEnabled: !prevState.shortcutsModalEnabled, 279 })); 280 } 281 282 // Important so that the tabs chevron updates appropriately when 283 // the user resizes the left or right columns 284 triggerEditorPaneResize() { 285 const editorPane = window.document.querySelector(".editor-pane"); 286 if (editorPane) { 287 editorPane.dispatchEvent(new Event("resizeend")); 288 } 289 } 290 291 renderLayout = () => { 292 const { startPanelCollapsed, endPanelCollapsed } = this.props; 293 const horizontal = this.isHorizontal(); 294 return React.createElement(SplitBox, { 295 style: { 296 width: "100vw", 297 }, 298 initialSize: prefs.endPanelSize, 299 minSize: 30, 300 maxSize: "70%", 301 splitterSize: 1, 302 vert: horizontal, 303 onResizeEnd: num => { 304 prefs.endPanelSize = num; 305 this.triggerEditorPaneResize(); 306 }, 307 startPanel: React.createElement(SplitBox, { 308 style: { 309 width: "100vw", 310 }, 311 initialSize: prefs.startPanelSize, 312 minSize: 30, 313 maxSize: "85%", 314 splitterSize: 1, 315 onResizeEnd: num => { 316 prefs.startPanelSize = num; 317 this.triggerEditorPaneResize(); 318 }, 319 startPanelCollapsed, 320 startPanel: React.createElement(PrimaryPanes, { 321 horizontal, 322 }), 323 endPanel: this.renderEditorPane(), 324 }), 325 endPanelControl: true, 326 endPanel: React.createElement(SecondaryPanes, { 327 horizontal, 328 }), 329 endPanelCollapsed, 330 }); 331 }; 332 333 render() { 334 const { quickOpenEnabled } = this.props; 335 return div( 336 { 337 className: "debugger", 338 }, 339 React.createElement( 340 AppErrorBoundary, 341 { 342 componentName: "Debugger", 343 panel: L10N.getStr("ToolboxDebugger.label"), 344 }, 345 this.renderLayout(), 346 quickOpenEnabled === true && 347 React.createElement(QuickOpenModal, { 348 shortcutsModalEnabled: this.state.shortcutsModalEnabled, 349 toggleShortcutsModal: () => this.toggleShortcutsModal(), 350 }), 351 React.createElement(ShortcutsModal, { 352 enabled: this.state.shortcutsModalEnabled, 353 handleClose: () => this.toggleShortcutsModal(), 354 }) 355 ) 356 ); 357 } 358 } 359 360 App.childContextTypes = { 361 toolboxDoc: PropTypes.object, 362 shortcuts: PropTypes.object, 363 l10n: PropTypes.object, 364 fluentBundles: PropTypes.array, 365 }; 366 367 const mapStateToProps = state => { 368 const selectedLocation = getSelectedLocation(state); 369 const mapScopeEnabled = isMapScopesEnabled(state); 370 const isPaused = getIsCurrentThreadPaused(state); 371 372 const showOriginalVariableMappingWarning = 373 isPaused && 374 selectedLocation?.source.isOriginal && 375 !selectedLocation?.source.isPrettyPrinted && 376 !mapScopeEnabled; 377 378 return { 379 showOriginalVariableMappingWarning, 380 showWelcomeBox: !selectedLocation, 381 startPanelCollapsed: getPaneCollapse(state, "start"), 382 endPanelCollapsed: getPaneCollapse(state, "end"), 383 activeSearch: getActiveSearch(state), 384 quickOpenEnabled: getQuickOpenEnabled(state), 385 orientation: getOrientation(state), 386 sourceMapError: selectedLocation?.sourceActor 387 ? getSourceMapErrorForSourceActor(state, selectedLocation.sourceActor.id) 388 : null, 389 }; 390 }; 391 392 export default connect(mapStateToProps, { 393 setActiveSearch: actions.setActiveSearch, 394 closeActiveSearch: actions.closeActiveSearch, 395 openQuickOpen: actions.openQuickOpen, 396 closeQuickOpen: actions.closeQuickOpen, 397 setOrientation: actions.setOrientation, 398 setPrimaryPaneTab: actions.setPrimaryPaneTab, 399 })(App);