Bug 1578220 - [devtools] Allow disabling debugger statement by a toggle in the breakp...
[gecko.git] / devtools / client / debugger / src / components / SecondaryPanes / index.js
blob459c6464cea382cbc7a5695911367b0ae2f6efc2
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/>. */
5 const SplitBox = require("devtools/client/shared/components/splitter/SplitBox");
7 import React, { Component } from "react";
8 import { div, input, label, button, a } from "react-dom-factories";
9 import PropTypes from "prop-types";
10 import { connect } from "../../utils/connect";
12 import actions from "../../actions";
13 import {
14   getTopFrame,
15   getExpressions,
16   getPauseCommand,
17   isMapScopesEnabled,
18   getSelectedFrame,
19   getSelectedSource,
20   getThreads,
21   getCurrentThread,
22   getPauseReason,
23   getShouldBreakpointsPaneOpenOnPause,
24   getSkipPausing,
25   shouldLogEventBreakpoints,
26 } from "../../selectors";
28 import AccessibleImage from "../shared/AccessibleImage";
29 import { prefs } from "../../utils/prefs";
31 import Breakpoints from "./Breakpoints";
32 import Expressions from "./Expressions";
33 import Frames from "./Frames";
34 import Threads from "./Threads";
35 import Accordion from "../shared/Accordion";
36 import CommandBar from "./CommandBar";
37 import XHRBreakpoints from "./XHRBreakpoints";
38 import EventListeners from "./EventListeners";
39 import DOMMutationBreakpoints from "./DOMMutationBreakpoints";
40 import WhyPaused from "./WhyPaused";
42 import Scopes from "./Scopes";
44 const classnames = require("devtools/client/shared/classnames.js");
46 import "./SecondaryPanes.css";
48 function debugBtn(onClick, type, className, tooltip) {
49   return button(
50     {
51       onClick: onClick,
52       className: `${type} ${className}`,
53       key: type,
54       title: tooltip,
55     },
56     React.createElement(AccessibleImage, {
57       className: type,
58       title: tooltip,
59       "aria-label": tooltip,
60     })
61   );
64 const mdnLink =
65   "https://firefox-source-docs.mozilla.org/devtools-user/debugger/using_the_debugger_map_scopes_feature/";
67 class SecondaryPanes extends Component {
68   constructor(props) {
69     super(props);
71     this.state = {
72       showExpressionsInput: false,
73       showXHRInput: false,
74     };
75   }
77   static get propTypes() {
78     return {
79       evaluateExpressionsForCurrentContext: PropTypes.func.isRequired,
80       expressions: PropTypes.array.isRequired,
81       hasFrames: PropTypes.bool.isRequired,
82       horizontal: PropTypes.bool.isRequired,
83       logEventBreakpoints: PropTypes.bool.isRequired,
84       mapScopesEnabled: PropTypes.bool.isRequired,
85       pauseReason: PropTypes.string.isRequired,
86       shouldBreakpointsPaneOpenOnPause: PropTypes.bool.isRequired,
87       thread: PropTypes.string.isRequired,
88       renderWhyPauseDelay: PropTypes.number.isRequired,
89       selectedFrame: PropTypes.object,
90       skipPausing: PropTypes.bool.isRequired,
91       source: PropTypes.object,
92       toggleEventLogging: PropTypes.func.isRequired,
93       resetBreakpointsPaneState: PropTypes.func.isRequired,
94       toggleMapScopes: PropTypes.func.isRequired,
95       threads: PropTypes.array.isRequired,
96       removeAllBreakpoints: PropTypes.func.isRequired,
97       removeAllXHRBreakpoints: PropTypes.func.isRequired,
98     };
99   }
101   onExpressionAdded = () => {
102     this.setState({ showExpressionsInput: false });
103   };
105   onXHRAdded = () => {
106     this.setState({ showXHRInput: false });
107   };
109   watchExpressionHeaderButtons() {
110     const { expressions } = this.props;
111     const buttons = [];
113     if (expressions.length) {
114       buttons.push(
115         debugBtn(
116           () => {
117             this.props.evaluateExpressionsForCurrentContext();
118           },
119           "refresh",
120           "active",
121           L10N.getStr("watchExpressions.refreshButton")
122         )
123       );
124     }
125     buttons.push(
126       debugBtn(
127         () => {
128           if (!prefs.expressionsVisible) {
129             this.onWatchExpressionPaneToggle(true);
130           }
131           this.setState({ showExpressionsInput: true });
132         },
133         "plus",
134         "active",
135         L10N.getStr("expressions.placeholder")
136       )
137     );
138     return buttons;
139   }
141   xhrBreakpointsHeaderButtons() {
142     return [
143       debugBtn(
144         () => {
145           if (!prefs.xhrBreakpointsVisible) {
146             this.onXHRPaneToggle(true);
147           }
148           this.setState({ showXHRInput: true });
149         },
150         "plus",
151         "active",
152         L10N.getStr("xhrBreakpoints.label")
153       ),
155       debugBtn(
156         () => {
157           this.props.removeAllXHRBreakpoints();
158         },
159         "removeAll",
160         "active",
161         L10N.getStr("xhrBreakpoints.removeAll.tooltip")
162       ),
163     ];
164   }
166   breakpointsHeaderButtons() {
167     return [
168       debugBtn(
169         () => {
170           this.props.removeAllBreakpoints();
171         },
172         "removeAll",
173         "active",
174         L10N.getStr("breakpointMenuItem.deleteAll")
175       ),
176     ];
177   }
179   getScopeItem() {
180     return {
181       header: L10N.getStr("scopes.header"),
182       className: "scopes-pane",
183       component: React.createElement(Scopes, null),
184       opened: prefs.scopesVisible,
185       buttons: this.getScopesButtons(),
186       onToggle: opened => {
187         prefs.scopesVisible = opened;
188       },
189     };
190   }
192   getScopesButtons() {
193     const { selectedFrame, mapScopesEnabled, source } = this.props;
195     if (!selectedFrame || !source?.isOriginal || source?.isPrettyPrinted) {
196       return null;
197     }
199     return [
200       div(
201         {
202           key: "scopes-buttons",
203         },
204         label(
205           {
206             className: "map-scopes-header",
207             title: L10N.getStr("scopes.showOriginalScopesTooltip"),
208             onClick: e => e.stopPropagation(),
209           },
210           input({
211             type: "checkbox",
212             checked: mapScopesEnabled ? "checked" : "",
213             onChange: e => this.props.toggleMapScopes(),
214           }),
215           L10N.getStr("scopes.showOriginalScopes")
216         ),
217         a(
218           {
219             className: "mdn",
220             target: "_blank",
221             href: mdnLink,
222             onClick: e => e.stopPropagation(),
223             title: L10N.getStr("scopes.showOriginalScopesHelpTooltip"),
224           },
225           React.createElement(AccessibleImage, {
226             className: "shortcuts",
227           })
228         )
229       ),
230     ];
231   }
233   getEventButtons() {
234     const { logEventBreakpoints } = this.props;
235     return [
236       div(
237         {
238           key: "events-buttons",
239         },
240         label(
241           {
242             className: "events-header",
243             title: L10N.getStr("eventlisteners.log.label"),
244           },
245           input({
246             type: "checkbox",
247             checked: logEventBreakpoints ? "checked" : "",
248             onChange: e => this.props.toggleEventLogging(),
249           }),
250           L10N.getStr("eventlisteners.log")
251         )
252       ),
253     ];
254   }
256   onWatchExpressionPaneToggle(opened) {
257     prefs.expressionsVisible = opened;
258   }
260   getWatchItem() {
261     return {
262       header: L10N.getStr("watchExpressions.header"),
263       id: "watch-expressions-pane",
264       className: "watch-expressions-pane",
265       buttons: this.watchExpressionHeaderButtons(),
266       component: React.createElement(Expressions, {
267         showInput: this.state.showExpressionsInput,
268         onExpressionAdded: this.onExpressionAdded,
269       }),
270       opened: prefs.expressionsVisible,
271       onToggle: this.onWatchExpressionPaneToggle,
272     };
273   }
275   onXHRPaneToggle(opened) {
276     prefs.xhrBreakpointsVisible = opened;
277   }
279   getXHRItem() {
280     const { pauseReason } = this.props;
282     return {
283       header: L10N.getStr("xhrBreakpoints.header"),
284       id: "xhr-breakpoints-pane",
285       className: "xhr-breakpoints-pane",
286       buttons: this.xhrBreakpointsHeaderButtons(),
287       component: React.createElement(XHRBreakpoints, {
288         showInput: this.state.showXHRInput,
289         onXHRAdded: this.onXHRAdded,
290       }),
291       opened: prefs.xhrBreakpointsVisible || pauseReason === "XHR",
292       onToggle: this.onXHRPaneToggle,
293     };
294   }
296   getCallStackItem() {
297     return {
298       header: L10N.getStr("callStack.header"),
299       id: "call-stack-pane",
300       className: "call-stack-pane",
301       component: React.createElement(Frames, {
302         panel: "debugger",
303       }),
304       opened: prefs.callStackVisible,
305       onToggle: opened => {
306         prefs.callStackVisible = opened;
307       },
308     };
309   }
311   getThreadsItem() {
312     return {
313       header: L10N.getStr("threadsHeader"),
314       id: "threads-pane",
315       className: "threads-pane",
316       component: React.createElement(Threads, null),
317       opened: prefs.threadsVisible,
318       onToggle: opened => {
319         prefs.threadsVisible = opened;
320       },
321     };
322   }
324   getBreakpointsItem() {
325     const { pauseReason, shouldBreakpointsPaneOpenOnPause, thread } =
326       this.props;
328     return {
329       header: L10N.getStr("breakpoints.header"),
330       id: "breakpoints-pane",
331       className: "breakpoints-pane",
332       buttons: this.breakpointsHeaderButtons(),
333       component: React.createElement(Breakpoints),
334       opened:
335         prefs.breakpointsVisible ||
336         (pauseReason === "breakpoint" && shouldBreakpointsPaneOpenOnPause),
337       onToggle: opened => {
338         prefs.breakpointsVisible = opened;
339         //  one-shot flag used to force open the Breakpoints Pane only
340         //  when hitting a breakpoint, but not when selecting frames etc...
341         if (shouldBreakpointsPaneOpenOnPause) {
342           this.props.resetBreakpointsPaneState(thread);
343         }
344       },
345     };
346   }
348   getEventListenersItem() {
349     const { pauseReason } = this.props;
351     return {
352       header: L10N.getStr("eventListenersHeader1"),
353       id: "event-listeners-pane",
354       className: "event-listeners-pane",
355       buttons: this.getEventButtons(),
356       component: React.createElement(EventListeners, null),
357       opened: prefs.eventListenersVisible || pauseReason === "eventBreakpoint",
358       onToggle: opened => {
359         prefs.eventListenersVisible = opened;
360       },
361     };
362   }
364   getDOMMutationsItem() {
365     const { pauseReason } = this.props;
367     return {
368       header: L10N.getStr("domMutationHeader"),
369       id: "dom-mutations-pane",
370       className: "dom-mutations-pane",
371       buttons: [],
372       component: React.createElement(DOMMutationBreakpoints, null),
373       opened:
374         prefs.domMutationBreakpointsVisible ||
375         pauseReason === "mutationBreakpoint",
376       onToggle: opened => {
377         prefs.domMutationBreakpointsVisible = opened;
378       },
379     };
380   }
382   getStartItems() {
383     const items = [];
384     const { horizontal, hasFrames } = this.props;
386     if (horizontal) {
387       if (this.props.threads.length) {
388         items.push(this.getThreadsItem());
389       }
391       items.push(this.getWatchItem());
392     }
394     items.push(this.getBreakpointsItem());
396     if (hasFrames) {
397       items.push(this.getCallStackItem());
398       if (horizontal) {
399         items.push(this.getScopeItem());
400       }
401     }
403     items.push(this.getXHRItem());
405     items.push(this.getEventListenersItem());
407     items.push(this.getDOMMutationsItem());
409     return items;
410   }
412   getEndItems() {
413     if (this.props.horizontal) {
414       return [];
415     }
417     const items = [];
418     if (this.props.threads.length) {
419       items.push(this.getThreadsItem());
420     }
422     items.push(this.getWatchItem());
424     if (this.props.hasFrames) {
425       items.push(this.getScopeItem());
426     }
428     return items;
429   }
431   getItems() {
432     return [...this.getStartItems(), ...this.getEndItems()];
433   }
435   renderHorizontalLayout() {
436     const { renderWhyPauseDelay } = this.props;
437     return div(
438       null,
439       React.createElement(WhyPaused, {
440         delay: renderWhyPauseDelay,
441       }),
442       React.createElement(Accordion, {
443         items: this.getItems(),
444       })
445     );
446   }
448   renderVerticalLayout() {
449     return React.createElement(SplitBox, {
450       initialSize: "300px",
451       minSize: 10,
452       maxSize: "50%",
453       splitterSize: 1,
454       startPanel: div(
455         {
456           style: {
457             width: "inherit",
458           },
459         },
460         React.createElement(WhyPaused, {
461           delay: this.props.renderWhyPauseDelay,
462         }),
463         React.createElement(Accordion, {
464           items: this.getStartItems(),
465         })
466       ),
467       endPanel: React.createElement(Accordion, {
468         items: this.getEndItems(),
469       }),
470     });
471   }
473   render() {
474     const { skipPausing } = this.props;
475     return div(
476       {
477         className: "secondary-panes-wrapper",
478       },
479       React.createElement(CommandBar, {
480         horizontal: this.props.horizontal,
481       }),
482       React.createElement(
483         "div",
484         {
485           className: classnames(
486             "secondary-panes",
487             skipPausing && "skip-pausing"
488           ),
489         },
490         this.props.horizontal
491           ? this.renderHorizontalLayout()
492           : this.renderVerticalLayout()
493       )
494     );
495   }
498 // Checks if user is in debugging mode and adds a delay preventing
499 // excessive vertical 'jumpiness'
500 function getRenderWhyPauseDelay(state, thread) {
501   const inPauseCommand = !!getPauseCommand(state, thread);
503   if (!inPauseCommand) {
504     return 100;
505   }
507   return 0;
510 const mapStateToProps = state => {
511   const thread = getCurrentThread(state);
512   const selectedFrame = getSelectedFrame(state, thread);
513   const pauseReason = getPauseReason(state, thread);
514   const shouldBreakpointsPaneOpenOnPause = getShouldBreakpointsPaneOpenOnPause(
515     state,
516     thread
517   );
519   return {
520     expressions: getExpressions(state),
521     hasFrames: !!getTopFrame(state, thread),
522     renderWhyPauseDelay: getRenderWhyPauseDelay(state, thread),
523     selectedFrame,
524     mapScopesEnabled: isMapScopesEnabled(state),
525     threads: getThreads(state),
526     skipPausing: getSkipPausing(state),
527     logEventBreakpoints: shouldLogEventBreakpoints(state),
528     source: getSelectedSource(state),
529     pauseReason: pauseReason?.type ?? "",
530     shouldBreakpointsPaneOpenOnPause,
531     thread,
532   };
535 export default connect(mapStateToProps, {
536   evaluateExpressionsForCurrentContext:
537     actions.evaluateExpressionsForCurrentContext,
538   toggleMapScopes: actions.toggleMapScopes,
539   breakOnNext: actions.breakOnNext,
540   toggleEventLogging: actions.toggleEventLogging,
541   removeAllBreakpoints: actions.removeAllBreakpoints,
542   removeAllXHRBreakpoints: actions.removeAllXHRBreakpoints,
543   resetBreakpointsPaneState: actions.resetBreakpointsPaneState,
544 })(SecondaryPanes);