paused-debugger.js (6900B)
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 "use strict"; 6 7 const { 8 CanvasFrameAnonymousContentHelper, 9 } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); 10 11 loader.lazyGetter(this, "PausedReasonsBundle", () => { 12 return new Localization( 13 ["devtools/shared/debugger-paused-reasons.ftl"], 14 true 15 ); 16 }); 17 18 /** 19 * The PausedDebuggerOverlay is a class that displays a semi-transparent mask on top of 20 * the whole page and a toolbar at the top of the page. 21 * This is used to signal to users that script execution is current paused. 22 * The toolbar is used to display the reason for the pause in script execution as well as 23 * buttons to resume or step through the program. 24 */ 25 class PausedDebuggerOverlay { 26 constructor(highlighterEnv, options = {}) { 27 this.env = highlighterEnv; 28 this.resume = options.resume; 29 this.stepOver = options.stepOver; 30 31 this.lastTarget = null; 32 33 this.markup = new CanvasFrameAnonymousContentHelper( 34 highlighterEnv, 35 this._buildMarkup.bind(this), 36 { 37 contentRootHostClassName: "devtools-highlighter-paused-debugger", 38 waitForDocumentToLoad: false, 39 } 40 ); 41 this.isReady = this.markup.initialize(); 42 } 43 44 _buildMarkup() { 45 const container = this.markup.createNode({ 46 attributes: { class: "highlighter-container" }, 47 }); 48 49 // Wrapper element. 50 const wrapper = this.markup.createNode({ 51 parent: container, 52 attributes: { 53 id: "paused-dbg-root", 54 class: "paused-dbg-root", 55 hidden: "true", 56 overlay: "true", 57 }, 58 }); 59 60 const toolbar = this.markup.createNode({ 61 parent: wrapper, 62 attributes: { 63 id: "paused-dbg-toolbar", 64 class: "paused-dbg-toolbar", 65 }, 66 }); 67 68 this.markup.createNode({ 69 nodeType: "span", 70 parent: toolbar, 71 attributes: { 72 id: "paused-dbg-reason", 73 class: "paused-dbg-reason", 74 }, 75 text: PausedReasonsBundle.formatValueSync("whypaused-other"), 76 }); 77 78 this.markup.createNode({ 79 parent: toolbar, 80 attributes: { 81 id: "paused-dbg-divider", 82 class: "paused-dbg-divider", 83 }, 84 }); 85 86 const stepWrapper = this.markup.createNode({ 87 parent: toolbar, 88 attributes: { 89 id: "paused-dbg-step-button-wrapper", 90 class: "paused-dbg-step-button-wrapper", 91 }, 92 }); 93 94 this.markup.createNode({ 95 nodeType: "button", 96 parent: stepWrapper, 97 attributes: { 98 id: "paused-dbg-step-button", 99 class: "paused-dbg-step-button", 100 }, 101 }); 102 103 const resumeWrapper = this.markup.createNode({ 104 parent: toolbar, 105 attributes: { 106 id: "paused-dbg-resume-button-wrapper", 107 class: "paused-dbg-resume-button-wrapper", 108 }, 109 }); 110 111 this.markup.createNode({ 112 nodeType: "button", 113 parent: resumeWrapper, 114 attributes: { 115 id: "paused-dbg-resume-button", 116 class: "paused-dbg-resume-button", 117 }, 118 }); 119 120 return container; 121 } 122 123 destroy() { 124 this.hide(); 125 this.markup.destroy(); 126 this.env = null; 127 this.lastTarget = null; 128 } 129 130 onClick(target) { 131 const { id } = target; 132 if (!id) { 133 return; 134 } 135 136 if (id.includes("paused-dbg-step-button")) { 137 this.stepOver(); 138 } else if (id.includes("paused-dbg-resume-button")) { 139 this.resume(); 140 } 141 } 142 143 onMouseMove(target) { 144 // Not an element we care about 145 if (!target || !target.id) { 146 return; 147 } 148 149 // If the user didn't change targets, do nothing 150 if (this.lastTarget && this.lastTarget.id === target.id) { 151 return; 152 } 153 154 if ( 155 target.id.includes("step-button") || 156 target.id.includes("resume-button") 157 ) { 158 // The hover should be applied to the wrapper (icon's parent node) 159 const newTarget = target.parentNode.id.includes("wrapper") 160 ? target.parentNode 161 : target; 162 163 // Remove the hover class if the user has changed buttons 164 if (this.lastTarget && this.lastTarget != newTarget) { 165 this.lastTarget.classList.remove("hover"); 166 } 167 newTarget.classList.add("hover"); 168 this.lastTarget = newTarget; 169 } else if (this.lastTarget) { 170 // Remove the hover class if the user isn't on a button 171 this.lastTarget.classList.remove("hover"); 172 } 173 } 174 175 handleEvent(e) { 176 switch (e.type) { 177 case "mousedown": 178 this.onClick(e.target); 179 break; 180 case "DOMMouseScroll": 181 // Prevent scrolling. That's because we only took a screenshot of the viewport, so 182 // scrolling out of the viewport wouldn't draw the expected things. In the future 183 // we can take the screenshot again on scroll, but for now it doesn't seem 184 // important. 185 e.preventDefault(); 186 break; 187 188 case "mousemove": 189 this.onMouseMove(e.target); 190 break; 191 } 192 } 193 194 getElement(id) { 195 return this.markup.getElement(id); 196 } 197 198 show(reason) { 199 if (this.env.isXUL || !reason) { 200 return false; 201 } 202 203 // Only track mouse movement when the the overlay is shown 204 // Prevents mouse tracking when the user isn't paused 205 const { pageListenerTarget } = this.env; 206 pageListenerTarget.addEventListener("mousemove", this); 207 208 // Show the highlighter's root element. 209 const root = this.getElement("paused-dbg-root"); 210 root.removeAttribute("hidden"); 211 root.setAttribute("overlay", "true"); 212 213 // Set the text to appear in the toolbar. 214 const toolbar = this.getElement("paused-dbg-toolbar"); 215 toolbar.removeAttribute("hidden"); 216 217 // When the debugger pauses execution in a page, events will not be delivered 218 // to any handlers added to elements on that page. So here we use the 219 // document's setSuppressedEventListener interface to still be able to act on mouse 220 // events (they'll be handled by the `handleEvent` method) 221 this.env.window.document.setSuppressedEventListener(this); 222 // Ensure layout is initialized so that we show the highlighter no matter what, 223 // even if the page is not done loading, see bug 1580394. 224 this.env.window.document.documentElement?.getBoundingClientRect(); 225 return true; 226 } 227 228 hide() { 229 if (this.env.isXUL) { 230 return; 231 } 232 233 const { pageListenerTarget } = this.env; 234 pageListenerTarget.removeEventListener("mousemove", this); 235 236 // Hide the overlay. 237 this.getElement("paused-dbg-root").setAttribute("hidden", "true"); 238 // Remove the hover state 239 this.getElement("paused-dbg-step-button-wrapper").classList?.remove( 240 "hover" 241 ); 242 this.getElement("paused-dbg-resume-button-wrapper").classList?.remove( 243 "hover" 244 ); 245 } 246 } 247 exports.PausedDebuggerOverlay = PausedDebuggerOverlay;