|
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 this.EXPORTED_SYMBOLS = [ "DeveloperToolbar", "CommandUtils" ]; |
|
8 |
|
9 const NS_XHTML = "http://www.w3.org/1999/xhtml"; |
|
10 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
11 const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
12 |
|
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
14 Cu.import("resource://gre/modules/Services.jsm"); |
|
15 |
|
16 const { require, TargetFactory } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; |
|
17 |
|
18 const Node = Ci.nsIDOMNode; |
|
19 |
|
20 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
21 "resource://gre/modules/devtools/Console.jsm"); |
|
22 |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
24 "resource://gre/modules/PluralForm.jsm"); |
|
25 |
|
26 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", |
|
27 "resource://gre/modules/devtools/event-emitter.js"); |
|
28 |
|
29 XPCOMUtils.defineLazyGetter(this, "prefBranch", function() { |
|
30 let prefService = Cc["@mozilla.org/preferences-service;1"] |
|
31 .getService(Ci.nsIPrefService); |
|
32 return prefService.getBranch(null) |
|
33 .QueryInterface(Ci.nsIPrefBranch2); |
|
34 }); |
|
35 |
|
36 XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () { |
|
37 return Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); |
|
38 }); |
|
39 |
|
40 const Telemetry = require("devtools/shared/telemetry"); |
|
41 |
|
42 // This lazy getter is needed to prevent a require loop |
|
43 XPCOMUtils.defineLazyGetter(this, "gcli", () => { |
|
44 let gcli = require("gcli/index"); |
|
45 require("devtools/commandline/commands-index"); |
|
46 gcli.load(); |
|
47 return gcli; |
|
48 }); |
|
49 |
|
50 Object.defineProperty(this, "ConsoleServiceListener", { |
|
51 get: function() { |
|
52 return require("devtools/toolkit/webconsole/utils").ConsoleServiceListener; |
|
53 }, |
|
54 configurable: true, |
|
55 enumerable: true |
|
56 }); |
|
57 |
|
58 const promise = Cu.import('resource://gre/modules/Promise.jsm', {}).Promise; |
|
59 |
|
60 /** |
|
61 * A collection of utilities to help working with commands |
|
62 */ |
|
63 let CommandUtils = { |
|
64 /** |
|
65 * Utility to ensure that things are loaded in the correct order |
|
66 */ |
|
67 createRequisition: function(environment) { |
|
68 let temp = gcli.createDisplay; // Ensure GCLI is loaded |
|
69 let Requisition = require("gcli/cli").Requisition |
|
70 return new Requisition({ environment: environment }); |
|
71 }, |
|
72 |
|
73 /** |
|
74 * Read a toolbarSpec from preferences |
|
75 * @param pref The name of the preference to read |
|
76 */ |
|
77 getCommandbarSpec: function(pref) { |
|
78 let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data; |
|
79 return JSON.parse(value); |
|
80 }, |
|
81 |
|
82 /** |
|
83 * A toolbarSpec is an array of buttonSpecs. A buttonSpec is an array of |
|
84 * strings each of which is a GCLI command (including args if needed). |
|
85 * |
|
86 * Warning: this method uses the unload event of the window that owns the |
|
87 * buttons that are of type checkbox. this means that we don't properly |
|
88 * unregister event handlers until the window is destroyed. |
|
89 */ |
|
90 createButtons: function(toolbarSpec, target, document, requisition) { |
|
91 let reply = []; |
|
92 |
|
93 toolbarSpec.forEach(function(buttonSpec) { |
|
94 let button = document.createElement("toolbarbutton"); |
|
95 reply.push(button); |
|
96 |
|
97 if (typeof buttonSpec == "string") { |
|
98 buttonSpec = { typed: buttonSpec }; |
|
99 } |
|
100 // Ask GCLI to parse the typed string (doesn't execute it) |
|
101 requisition.update(buttonSpec.typed); |
|
102 |
|
103 // Ignore invalid commands |
|
104 let command = requisition.commandAssignment.value; |
|
105 if (command == null) { |
|
106 // TODO: Have a broken icon |
|
107 // button.icon = 'Broken'; |
|
108 button.setAttribute("label", "X"); |
|
109 button.setAttribute("tooltip", "Unknown command: " + buttonSpec.typed); |
|
110 button.setAttribute("disabled", "true"); |
|
111 } |
|
112 else { |
|
113 if (command.buttonId != null) { |
|
114 button.id = command.buttonId; |
|
115 } |
|
116 if (command.buttonClass != null) { |
|
117 button.className = command.buttonClass; |
|
118 } |
|
119 if (command.tooltipText != null) { |
|
120 button.setAttribute("tooltiptext", command.tooltipText); |
|
121 } |
|
122 else if (command.description != null) { |
|
123 button.setAttribute("tooltiptext", command.description); |
|
124 } |
|
125 |
|
126 button.addEventListener("click", function() { |
|
127 requisition.update(buttonSpec.typed); |
|
128 //if (requisition.getStatus() == Status.VALID) { |
|
129 requisition.exec(); |
|
130 /* |
|
131 } |
|
132 else { |
|
133 console.error('incomplete commands not yet supported'); |
|
134 } |
|
135 */ |
|
136 }, false); |
|
137 |
|
138 // Allow the command button to be toggleable |
|
139 if (command.state) { |
|
140 button.setAttribute("autocheck", false); |
|
141 let onChange = function(event, eventTab) { |
|
142 if (eventTab == target.tab) { |
|
143 if (command.state.isChecked(target)) { |
|
144 button.setAttribute("checked", true); |
|
145 } |
|
146 else if (button.hasAttribute("checked")) { |
|
147 button.removeAttribute("checked"); |
|
148 } |
|
149 } |
|
150 }; |
|
151 command.state.onChange(target, onChange); |
|
152 onChange(null, target.tab); |
|
153 document.defaultView.addEventListener("unload", function() { |
|
154 command.state.offChange(target, onChange); |
|
155 }, false); |
|
156 } |
|
157 } |
|
158 }); |
|
159 |
|
160 requisition.update(''); |
|
161 |
|
162 return reply; |
|
163 }, |
|
164 |
|
165 /** |
|
166 * A helper function to create the environment object that is passed to |
|
167 * GCLI commands. |
|
168 * @param targetContainer An object containing a 'target' property which |
|
169 * reflects the current debug target |
|
170 */ |
|
171 createEnvironment: function(container, targetProperty='target') { |
|
172 if (container[targetProperty].supports == null) { |
|
173 throw new Error('Missing target'); |
|
174 } |
|
175 |
|
176 return { |
|
177 get target() { |
|
178 if (container[targetProperty].supports == null) { |
|
179 throw new Error('Removed target'); |
|
180 } |
|
181 |
|
182 return container[targetProperty]; |
|
183 }, |
|
184 |
|
185 get chromeWindow() { |
|
186 return this.target.tab.ownerDocument.defaultView; |
|
187 }, |
|
188 |
|
189 get chromeDocument() { |
|
190 return this.chromeWindow.document; |
|
191 }, |
|
192 |
|
193 get window() { |
|
194 return this.chromeWindow.getBrowser().selectedTab.linkedBrowser.contentWindow; |
|
195 }, |
|
196 |
|
197 get document() { |
|
198 return this.window.document; |
|
199 } |
|
200 }; |
|
201 }, |
|
202 }; |
|
203 |
|
204 this.CommandUtils = CommandUtils; |
|
205 |
|
206 /** |
|
207 * Due to a number of panel bugs we need a way to check if we are running on |
|
208 * Linux. See the comments for TooltipPanel and OutputPanel for further details. |
|
209 * |
|
210 * When bug 780102 is fixed all isLinux checks can be removed and we can revert |
|
211 * to using panels. |
|
212 */ |
|
213 XPCOMUtils.defineLazyGetter(this, "isLinux", function() { |
|
214 return OS == "Linux"; |
|
215 }); |
|
216 |
|
217 XPCOMUtils.defineLazyGetter(this, "OS", function() { |
|
218 let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; |
|
219 return os; |
|
220 }); |
|
221 |
|
222 /** |
|
223 * A component to manage the global developer toolbar, which contains a GCLI |
|
224 * and buttons for various developer tools. |
|
225 * @param aChromeWindow The browser window to which this toolbar is attached |
|
226 * @param aToolbarElement See browser.xul:<toolbar id="developer-toolbar"> |
|
227 */ |
|
228 this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement) |
|
229 { |
|
230 this._chromeWindow = aChromeWindow; |
|
231 |
|
232 this._element = aToolbarElement; |
|
233 this._element.hidden = true; |
|
234 this._doc = this._element.ownerDocument; |
|
235 |
|
236 this._telemetry = new Telemetry(); |
|
237 this._errorsCount = {}; |
|
238 this._warningsCount = {}; |
|
239 this._errorListeners = {}; |
|
240 this._errorCounterButton = this._doc |
|
241 .getElementById("developer-toolbar-toolbox-button"); |
|
242 this._errorCounterButton._defaultTooltipText = |
|
243 this._errorCounterButton.getAttribute("tooltiptext"); |
|
244 |
|
245 EventEmitter.decorate(this); |
|
246 } |
|
247 |
|
248 /** |
|
249 * Inspector notifications dispatched through the nsIObserverService |
|
250 */ |
|
251 const NOTIFICATIONS = { |
|
252 /** DeveloperToolbar.show() has been called, and we're working on it */ |
|
253 LOAD: "developer-toolbar-load", |
|
254 |
|
255 /** DeveloperToolbar.show() has completed */ |
|
256 SHOW: "developer-toolbar-show", |
|
257 |
|
258 /** DeveloperToolbar.hide() has been called */ |
|
259 HIDE: "developer-toolbar-hide" |
|
260 }; |
|
261 |
|
262 /** |
|
263 * Attach notification constants to the object prototype so tests etc can |
|
264 * use them without needing to import anything |
|
265 */ |
|
266 DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS; |
|
267 |
|
268 Object.defineProperty(DeveloperToolbar.prototype, "target", { |
|
269 get: function() { |
|
270 return TargetFactory.forTab(this._chromeWindow.getBrowser().selectedTab); |
|
271 }, |
|
272 enumerable: true |
|
273 }); |
|
274 |
|
275 /** |
|
276 * Is the toolbar open? |
|
277 */ |
|
278 Object.defineProperty(DeveloperToolbar.prototype, 'visible', { |
|
279 get: function DT_visible() { |
|
280 return !this._element.hidden; |
|
281 }, |
|
282 enumerable: true |
|
283 }); |
|
284 |
|
285 let _gSequenceId = 0; |
|
286 |
|
287 /** |
|
288 * Getter for a unique ID. |
|
289 */ |
|
290 Object.defineProperty(DeveloperToolbar.prototype, 'sequenceId', { |
|
291 get: function DT_visible() { |
|
292 return _gSequenceId++; |
|
293 }, |
|
294 enumerable: true |
|
295 }); |
|
296 |
|
297 /** |
|
298 * Called from browser.xul in response to menu-click or keyboard shortcut to |
|
299 * toggle the toolbar |
|
300 */ |
|
301 DeveloperToolbar.prototype.toggle = function() { |
|
302 if (this.visible) { |
|
303 return this.hide(); |
|
304 } else { |
|
305 return this.show(true); |
|
306 } |
|
307 }; |
|
308 |
|
309 /** |
|
310 * Called from browser.xul in response to menu-click or keyboard shortcut to |
|
311 * toggle the toolbar |
|
312 */ |
|
313 DeveloperToolbar.prototype.focus = function() { |
|
314 if (this.visible) { |
|
315 this._input.focus(); |
|
316 return promise.resolve(); |
|
317 } else { |
|
318 return this.show(true); |
|
319 } |
|
320 }; |
|
321 |
|
322 /** |
|
323 * Called from browser.xul in response to menu-click or keyboard shortcut to |
|
324 * toggle the toolbar |
|
325 */ |
|
326 DeveloperToolbar.prototype.focusToggle = function() { |
|
327 if (this.visible) { |
|
328 // If we have focus then the active element is the HTML input contained |
|
329 // inside the xul input element |
|
330 let active = this._chromeWindow.document.activeElement; |
|
331 let position = this._input.compareDocumentPosition(active); |
|
332 if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) { |
|
333 this.hide(); |
|
334 } |
|
335 else { |
|
336 this._input.focus(); |
|
337 } |
|
338 } else { |
|
339 this.show(true); |
|
340 } |
|
341 }; |
|
342 |
|
343 /** |
|
344 * Even if the user has not clicked on 'Got it' in the intro, we only show it |
|
345 * once per session. |
|
346 * Warning this is slightly messed up because this.DeveloperToolbar is not the |
|
347 * same as this.DeveloperToolbar when in browser.js context. |
|
348 */ |
|
349 DeveloperToolbar.introShownThisSession = false; |
|
350 |
|
351 /** |
|
352 * Show the developer toolbar |
|
353 */ |
|
354 DeveloperToolbar.prototype.show = function(focus) { |
|
355 if (this._showPromise != null) { |
|
356 return this._showPromise; |
|
357 } |
|
358 |
|
359 // hide() is async, so ensure we don't need to wait for hide() to finish |
|
360 var waitPromise = this._hidePromise || promise.resolve(); |
|
361 |
|
362 this._showPromise = waitPromise.then(() => { |
|
363 Services.prefs.setBoolPref("devtools.toolbar.visible", true); |
|
364 |
|
365 this._telemetry.toolOpened("developertoolbar"); |
|
366 |
|
367 this._notify(NOTIFICATIONS.LOAD); |
|
368 |
|
369 this._input = this._doc.querySelector(".gclitoolbar-input-node"); |
|
370 |
|
371 // Initializing GCLI can only be done when we've got content windows to |
|
372 // write to, so this needs to be done asynchronously. |
|
373 let panelPromises = [ |
|
374 TooltipPanel.create(this), |
|
375 OutputPanel.create(this) |
|
376 ]; |
|
377 return promise.all(panelPromises).then(panels => { |
|
378 [ this.tooltipPanel, this.outputPanel ] = panels; |
|
379 |
|
380 this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true"); |
|
381 |
|
382 this.display = gcli.createDisplay({ |
|
383 contentDocument: this._chromeWindow.getBrowser().contentDocument, |
|
384 chromeDocument: this._doc, |
|
385 chromeWindow: this._chromeWindow, |
|
386 hintElement: this.tooltipPanel.hintElement, |
|
387 inputElement: this._input, |
|
388 completeElement: this._doc.querySelector(".gclitoolbar-complete-node"), |
|
389 backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"), |
|
390 outputDocument: this.outputPanel.document, |
|
391 environment: CommandUtils.createEnvironment(this, "target"), |
|
392 tooltipClass: "gcliterm-tooltip", |
|
393 eval: null, |
|
394 scratchpad: null |
|
395 }); |
|
396 |
|
397 this.display.focusManager.addMonitoredElement(this.outputPanel._frame); |
|
398 this.display.focusManager.addMonitoredElement(this._element); |
|
399 |
|
400 this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged, |
|
401 this.outputPanel); |
|
402 this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged, |
|
403 this.tooltipPanel); |
|
404 this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel); |
|
405 |
|
406 let tabbrowser = this._chromeWindow.getBrowser(); |
|
407 tabbrowser.tabContainer.addEventListener("TabSelect", this, false); |
|
408 tabbrowser.tabContainer.addEventListener("TabClose", this, false); |
|
409 tabbrowser.addEventListener("load", this, true); |
|
410 tabbrowser.addEventListener("beforeunload", this, true); |
|
411 |
|
412 this._initErrorsCount(tabbrowser.selectedTab); |
|
413 this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this); |
|
414 this._devtoolsLoaded = this._devtoolsLoaded.bind(this); |
|
415 Services.obs.addObserver(this._devtoolsUnloaded, "devtools-unloaded", false); |
|
416 Services.obs.addObserver(this._devtoolsLoaded, "devtools-loaded", false); |
|
417 |
|
418 this._element.hidden = false; |
|
419 |
|
420 if (focus) { |
|
421 this._input.focus(); |
|
422 } |
|
423 |
|
424 this._notify(NOTIFICATIONS.SHOW); |
|
425 |
|
426 if (!DeveloperToolbar.introShownThisSession) { |
|
427 this.display.maybeShowIntro(); |
|
428 DeveloperToolbar.introShownThisSession = true; |
|
429 } |
|
430 |
|
431 this._showPromise = null; |
|
432 }); |
|
433 }); |
|
434 |
|
435 return this._showPromise; |
|
436 }; |
|
437 |
|
438 /** |
|
439 * Hide the developer toolbar. |
|
440 */ |
|
441 DeveloperToolbar.prototype.hide = function() { |
|
442 // If we're already in the process of hiding, just use the other promise |
|
443 if (this._hidePromise != null) { |
|
444 return this._hidePromise; |
|
445 } |
|
446 |
|
447 // show() is async, so ensure we don't need to wait for show() to finish |
|
448 var waitPromise = this._showPromise || promise.resolve(); |
|
449 |
|
450 this._hidePromise = waitPromise.then(() => { |
|
451 this._element.hidden = true; |
|
452 |
|
453 Services.prefs.setBoolPref("devtools.toolbar.visible", false); |
|
454 |
|
455 this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "false"); |
|
456 this.destroy(); |
|
457 |
|
458 this._telemetry.toolClosed("developertoolbar"); |
|
459 this._notify(NOTIFICATIONS.HIDE); |
|
460 |
|
461 this._hidePromise = null; |
|
462 }); |
|
463 |
|
464 return this._hidePromise; |
|
465 }; |
|
466 |
|
467 /** |
|
468 * The devtools-unloaded event handler. |
|
469 * @private |
|
470 */ |
|
471 DeveloperToolbar.prototype._devtoolsUnloaded = function() { |
|
472 let tabbrowser = this._chromeWindow.getBrowser(); |
|
473 Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this); |
|
474 }; |
|
475 |
|
476 /** |
|
477 * The devtools-loaded event handler. |
|
478 * @private |
|
479 */ |
|
480 DeveloperToolbar.prototype._devtoolsLoaded = function() { |
|
481 let tabbrowser = this._chromeWindow.getBrowser(); |
|
482 this._initErrorsCount(tabbrowser.selectedTab); |
|
483 }; |
|
484 |
|
485 /** |
|
486 * Initialize the listeners needed for tracking the number of errors for a given |
|
487 * tab. |
|
488 * |
|
489 * @private |
|
490 * @param nsIDOMNode tab the xul:tab for which you want to track the number of |
|
491 * errors. |
|
492 */ |
|
493 DeveloperToolbar.prototype._initErrorsCount = function(tab) { |
|
494 let tabId = tab.linkedPanel; |
|
495 if (tabId in this._errorsCount) { |
|
496 this._updateErrorsCount(); |
|
497 return; |
|
498 } |
|
499 |
|
500 let window = tab.linkedBrowser.contentWindow; |
|
501 let listener = new ConsoleServiceListener(window, { |
|
502 onConsoleServiceMessage: this._onPageError.bind(this, tabId), |
|
503 }); |
|
504 listener.init(); |
|
505 |
|
506 this._errorListeners[tabId] = listener; |
|
507 this._errorsCount[tabId] = 0; |
|
508 this._warningsCount[tabId] = 0; |
|
509 |
|
510 let messages = listener.getCachedMessages(); |
|
511 messages.forEach(this._onPageError.bind(this, tabId)); |
|
512 |
|
513 this._updateErrorsCount(); |
|
514 }; |
|
515 |
|
516 /** |
|
517 * Stop the listeners needed for tracking the number of errors for a given |
|
518 * tab. |
|
519 * |
|
520 * @private |
|
521 * @param nsIDOMNode tab the xul:tab for which you want to stop tracking the |
|
522 * number of errors. |
|
523 */ |
|
524 DeveloperToolbar.prototype._stopErrorsCount = function(tab) { |
|
525 let tabId = tab.linkedPanel; |
|
526 if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) { |
|
527 this._updateErrorsCount(); |
|
528 return; |
|
529 } |
|
530 |
|
531 this._errorListeners[tabId].destroy(); |
|
532 delete this._errorListeners[tabId]; |
|
533 delete this._errorsCount[tabId]; |
|
534 delete this._warningsCount[tabId]; |
|
535 |
|
536 this._updateErrorsCount(); |
|
537 }; |
|
538 |
|
539 /** |
|
540 * Hide the developer toolbar |
|
541 */ |
|
542 DeveloperToolbar.prototype.destroy = function() { |
|
543 if (this._input == null) { |
|
544 return; // Already destroyed |
|
545 } |
|
546 |
|
547 let tabbrowser = this._chromeWindow.getBrowser(); |
|
548 tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); |
|
549 tabbrowser.tabContainer.removeEventListener("TabClose", this, false); |
|
550 tabbrowser.removeEventListener("load", this, true); |
|
551 tabbrowser.removeEventListener("beforeunload", this, true); |
|
552 |
|
553 Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded"); |
|
554 Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded"); |
|
555 Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this); |
|
556 |
|
557 this.display.focusManager.removeMonitoredElement(this.outputPanel._frame); |
|
558 this.display.focusManager.removeMonitoredElement(this._element); |
|
559 |
|
560 this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel); |
|
561 this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel); |
|
562 this.display.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel); |
|
563 this.display.destroy(); |
|
564 this.outputPanel.destroy(); |
|
565 this.tooltipPanel.destroy(); |
|
566 delete this._input; |
|
567 |
|
568 // We could "delete this.display" etc if we have hard-to-track-down memory |
|
569 // leaks as a belt-and-braces approach, however this prevents our DOM node |
|
570 // hunter from looking in all the nooks and crannies, so it's better if we |
|
571 // can be leak-free without |
|
572 /* |
|
573 delete this.display; |
|
574 delete this.outputPanel; |
|
575 delete this.tooltipPanel; |
|
576 */ |
|
577 }; |
|
578 |
|
579 /** |
|
580 * Utility for sending notifications |
|
581 * @param topic a NOTIFICATION constant |
|
582 */ |
|
583 DeveloperToolbar.prototype._notify = function(topic) { |
|
584 let data = { toolbar: this }; |
|
585 data.wrappedJSObject = data; |
|
586 Services.obs.notifyObservers(data, topic, null); |
|
587 }; |
|
588 |
|
589 /** |
|
590 * Update various parts of the UI when the current tab changes |
|
591 */ |
|
592 DeveloperToolbar.prototype.handleEvent = function(ev) { |
|
593 if (ev.type == "TabSelect" || ev.type == "load") { |
|
594 if (this.visible) { |
|
595 this.display.reattach({ |
|
596 contentDocument: this._chromeWindow.getBrowser().contentDocument |
|
597 }); |
|
598 |
|
599 if (ev.type == "TabSelect") { |
|
600 this._initErrorsCount(ev.target); |
|
601 } |
|
602 } |
|
603 } |
|
604 else if (ev.type == "TabClose") { |
|
605 this._stopErrorsCount(ev.target); |
|
606 } |
|
607 else if (ev.type == "beforeunload") { |
|
608 this._onPageBeforeUnload(ev); |
|
609 } |
|
610 }; |
|
611 |
|
612 /** |
|
613 * Count a page error received for the currently selected tab. This |
|
614 * method counts the JavaScript exceptions received and CSS errors/warnings. |
|
615 * |
|
616 * @private |
|
617 * @param string tabId the ID of the tab from where the page error comes. |
|
618 * @param object pageError the page error object received from the |
|
619 * PageErrorListener. |
|
620 */ |
|
621 DeveloperToolbar.prototype._onPageError = function(tabId, pageError) { |
|
622 if (pageError.category == "CSS Parser" || |
|
623 pageError.category == "CSS Loader") { |
|
624 return; |
|
625 } |
|
626 if ((pageError.flags & pageError.warningFlag) || |
|
627 (pageError.flags & pageError.strictFlag)) { |
|
628 this._warningsCount[tabId]++; |
|
629 } else { |
|
630 this._errorsCount[tabId]++; |
|
631 } |
|
632 this._updateErrorsCount(tabId); |
|
633 }; |
|
634 |
|
635 /** |
|
636 * The |beforeunload| event handler. This function resets the errors count when |
|
637 * a different page starts loading. |
|
638 * |
|
639 * @private |
|
640 * @param nsIDOMEvent ev the beforeunload DOM event. |
|
641 */ |
|
642 DeveloperToolbar.prototype._onPageBeforeUnload = function(ev) { |
|
643 let window = ev.target.defaultView; |
|
644 if (window.top !== window) { |
|
645 return; |
|
646 } |
|
647 |
|
648 let tabs = this._chromeWindow.getBrowser().tabs; |
|
649 Array.prototype.some.call(tabs, function(tab) { |
|
650 if (tab.linkedBrowser.contentWindow === window) { |
|
651 let tabId = tab.linkedPanel; |
|
652 if (tabId in this._errorsCount || tabId in this._warningsCount) { |
|
653 this._errorsCount[tabId] = 0; |
|
654 this._warningsCount[tabId] = 0; |
|
655 this._updateErrorsCount(tabId); |
|
656 } |
|
657 return true; |
|
658 } |
|
659 return false; |
|
660 }, this); |
|
661 }; |
|
662 |
|
663 /** |
|
664 * Update the page errors count displayed in the Web Console button for the |
|
665 * currently selected tab. |
|
666 * |
|
667 * @private |
|
668 * @param string [changedTabId] Optional. The tab ID that had its page errors |
|
669 * count changed. If this is provided and it doesn't match the currently |
|
670 * selected tab, then the button is not updated. |
|
671 */ |
|
672 DeveloperToolbar.prototype._updateErrorsCount = function(changedTabId) { |
|
673 let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel; |
|
674 if (changedTabId && tabId != changedTabId) { |
|
675 return; |
|
676 } |
|
677 |
|
678 let errors = this._errorsCount[tabId]; |
|
679 let warnings = this._warningsCount[tabId]; |
|
680 let btn = this._errorCounterButton; |
|
681 if (errors) { |
|
682 let errorsText = toolboxStrings |
|
683 .GetStringFromName("toolboxToggleButton.errors"); |
|
684 errorsText = PluralForm.get(errors, errorsText).replace("#1", errors); |
|
685 |
|
686 let warningsText = toolboxStrings |
|
687 .GetStringFromName("toolboxToggleButton.warnings"); |
|
688 warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings); |
|
689 |
|
690 let tooltiptext = toolboxStrings |
|
691 .formatStringFromName("toolboxToggleButton.tooltip", |
|
692 [errorsText, warningsText], 2); |
|
693 |
|
694 btn.setAttribute("error-count", errors); |
|
695 btn.setAttribute("tooltiptext", tooltiptext); |
|
696 } else { |
|
697 btn.removeAttribute("error-count"); |
|
698 btn.setAttribute("tooltiptext", btn._defaultTooltipText); |
|
699 } |
|
700 |
|
701 this.emit("errors-counter-updated"); |
|
702 }; |
|
703 |
|
704 /** |
|
705 * Reset the errors counter for the given tab. |
|
706 * |
|
707 * @param nsIDOMElement tab The xul:tab for which you want to reset the page |
|
708 * errors counters. |
|
709 */ |
|
710 DeveloperToolbar.prototype.resetErrorsCount = function(tab) { |
|
711 let tabId = tab.linkedPanel; |
|
712 if (tabId in this._errorsCount || tabId in this._warningsCount) { |
|
713 this._errorsCount[tabId] = 0; |
|
714 this._warningsCount[tabId] = 0; |
|
715 this._updateErrorsCount(tabId); |
|
716 } |
|
717 }; |
|
718 |
|
719 /** |
|
720 * Creating a OutputPanel is asynchronous |
|
721 */ |
|
722 function OutputPanel() { |
|
723 throw new Error('Use OutputPanel.create()'); |
|
724 } |
|
725 |
|
726 /** |
|
727 * Panel to handle command line output. |
|
728 * |
|
729 * There is a tooltip bug on Windows and OSX that prevents tooltips from being |
|
730 * positioned properly (bug 786975). There is a Gnome panel bug on Linux that |
|
731 * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848). |
|
732 * We now use a tooltip on Linux and a panel on OSX & Windows. |
|
733 * |
|
734 * If a panel has no content and no height it is not shown when openPopup is |
|
735 * called on Windows and OSX (bug 692348) ... this prevents the panel from |
|
736 * appearing the first time it is shown. Setting the panel's height to 1px |
|
737 * before calling openPopup works around this issue as we resize it ourselves |
|
738 * anyway. |
|
739 * |
|
740 * @param devtoolbar The parent DeveloperToolbar object |
|
741 */ |
|
742 OutputPanel.create = function(devtoolbar) { |
|
743 var outputPanel = Object.create(OutputPanel.prototype); |
|
744 return outputPanel._init(devtoolbar); |
|
745 }; |
|
746 |
|
747 /** |
|
748 * @private See OutputPanel.create |
|
749 */ |
|
750 OutputPanel.prototype._init = function(devtoolbar) { |
|
751 this._devtoolbar = devtoolbar; |
|
752 this._input = this._devtoolbar._input; |
|
753 this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar"); |
|
754 |
|
755 /* |
|
756 <tooltip|panel id="gcli-output" |
|
757 noautofocus="true" |
|
758 noautohide="true" |
|
759 class="gcli-panel"> |
|
760 <html:iframe xmlns:html="http://www.w3.org/1999/xhtml" |
|
761 id="gcli-output-frame" |
|
762 src="chrome://browser/content/devtools/commandlineoutput.xhtml" |
|
763 sandbox="allow-same-origin"/> |
|
764 </tooltip|panel> |
|
765 */ |
|
766 |
|
767 // TODO: Switch back from tooltip to panel when metacity focus issue is fixed: |
|
768 // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 |
|
769 this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel"); |
|
770 |
|
771 this._panel.id = "gcli-output"; |
|
772 this._panel.classList.add("gcli-panel"); |
|
773 |
|
774 if (isLinux) { |
|
775 this.canHide = false; |
|
776 this._onpopuphiding = this._onpopuphiding.bind(this); |
|
777 this._panel.addEventListener("popuphiding", this._onpopuphiding, true); |
|
778 } else { |
|
779 this._panel.setAttribute("noautofocus", "true"); |
|
780 this._panel.setAttribute("noautohide", "true"); |
|
781 |
|
782 // Bug 692348: On Windows and OSX if a panel has no content and no height |
|
783 // openPopup fails to display it. Setting the height to 1px alows the panel |
|
784 // to be displayed before has content or a real height i.e. the first time |
|
785 // it is displayed. |
|
786 this._panel.setAttribute("height", "1px"); |
|
787 } |
|
788 |
|
789 this._toolbar.parentElement.insertBefore(this._panel, this._toolbar); |
|
790 |
|
791 this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe"); |
|
792 this._frame.id = "gcli-output-frame"; |
|
793 this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml"); |
|
794 this._frame.setAttribute("sandbox", "allow-same-origin"); |
|
795 this._panel.appendChild(this._frame); |
|
796 |
|
797 this.displayedOutput = undefined; |
|
798 |
|
799 this._update = this._update.bind(this); |
|
800 |
|
801 // Wire up the element from the iframe, and resolve the promise |
|
802 let deferred = promise.defer(); |
|
803 let onload = () => { |
|
804 this._frame.removeEventListener("load", onload, true); |
|
805 |
|
806 this.document = this._frame.contentDocument; |
|
807 |
|
808 this._div = this.document.getElementById("gcli-output-root"); |
|
809 this._div.classList.add('gcli-row-out'); |
|
810 this._div.setAttribute('aria-live', 'assertive'); |
|
811 |
|
812 let styles = this._toolbar.ownerDocument.defaultView |
|
813 .getComputedStyle(this._toolbar); |
|
814 this._div.setAttribute("dir", styles.direction); |
|
815 |
|
816 deferred.resolve(this); |
|
817 }; |
|
818 this._frame.addEventListener("load", onload, true); |
|
819 |
|
820 return deferred.promise; |
|
821 } |
|
822 |
|
823 /** |
|
824 * Prevent the popup from hiding if it is not permitted via this.canHide. |
|
825 */ |
|
826 OutputPanel.prototype._onpopuphiding = function(ev) { |
|
827 // TODO: When we switch back from tooltip to panel we can remove this hack: |
|
828 // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 |
|
829 if (isLinux && !this.canHide) { |
|
830 ev.preventDefault(); |
|
831 } |
|
832 }; |
|
833 |
|
834 /** |
|
835 * Display the OutputPanel. |
|
836 */ |
|
837 OutputPanel.prototype.show = function() { |
|
838 if (isLinux) { |
|
839 this.canHide = false; |
|
840 } |
|
841 |
|
842 // We need to reset the iframe size in order for future size calculations to |
|
843 // be correct |
|
844 this._frame.style.minHeight = this._frame.style.maxHeight = 0; |
|
845 this._frame.style.minWidth = 0; |
|
846 |
|
847 this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null); |
|
848 this._resize(); |
|
849 |
|
850 this._input.focus(); |
|
851 }; |
|
852 |
|
853 /** |
|
854 * Internal helper to set the height of the output panel to fit the available |
|
855 * content; |
|
856 */ |
|
857 OutputPanel.prototype._resize = function() { |
|
858 if (this._panel == null || this.document == null || !this._panel.state == "closed") { |
|
859 return |
|
860 } |
|
861 |
|
862 // Set max panel width to match any content with a max of the width of the |
|
863 // browser window. |
|
864 let maxWidth = this._panel.ownerDocument.documentElement.clientWidth; |
|
865 |
|
866 // Adjust max width according to OS. |
|
867 // We'd like to put this in CSS but we can't: |
|
868 // body { width: calc(min(-5px, max-content)); } |
|
869 // #_panel { max-width: -5px; } |
|
870 switch(OS) { |
|
871 case "Linux": |
|
872 maxWidth -= 5; |
|
873 break; |
|
874 case "Darwin": |
|
875 maxWidth -= 25; |
|
876 break; |
|
877 case "WINNT": |
|
878 maxWidth -= 5; |
|
879 break; |
|
880 } |
|
881 |
|
882 this.document.body.style.width = "-moz-max-content"; |
|
883 let style = this._frame.contentWindow.getComputedStyle(this.document.body); |
|
884 let frameWidth = parseInt(style.width, 10); |
|
885 let width = Math.min(maxWidth, frameWidth); |
|
886 this.document.body.style.width = width + "px"; |
|
887 |
|
888 // Set the width of the iframe. |
|
889 this._frame.style.minWidth = width + "px"; |
|
890 this._panel.style.maxWidth = maxWidth + "px"; |
|
891 |
|
892 // browserAdjustment is used to correct the panel height according to the |
|
893 // browsers borders etc. |
|
894 const browserAdjustment = 15; |
|
895 |
|
896 // Set max panel height to match any content with a max of the height of the |
|
897 // browser window. |
|
898 let maxHeight = |
|
899 this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment; |
|
900 let height = Math.min(maxHeight, this.document.documentElement.scrollHeight); |
|
901 |
|
902 // Set the height of the iframe. Setting iframe.height does not work. |
|
903 this._frame.style.minHeight = this._frame.style.maxHeight = height + "px"; |
|
904 |
|
905 // Set the height and width of the panel to match the iframe. |
|
906 this._panel.sizeTo(width, height); |
|
907 |
|
908 // Move the panel to the correct position in the case that it has been |
|
909 // positioned incorrectly. |
|
910 let screenX = this._input.boxObject.screenX; |
|
911 let screenY = this._toolbar.boxObject.screenY; |
|
912 this._panel.moveTo(screenX, screenY - height); |
|
913 }; |
|
914 |
|
915 /** |
|
916 * Called by GCLI when a command is executed. |
|
917 */ |
|
918 OutputPanel.prototype._outputChanged = function(ev) { |
|
919 if (ev.output.hidden) { |
|
920 return; |
|
921 } |
|
922 |
|
923 this.remove(); |
|
924 |
|
925 this.displayedOutput = ev.output; |
|
926 |
|
927 if (this.displayedOutput.completed) { |
|
928 this._update(); |
|
929 } |
|
930 else { |
|
931 this.displayedOutput.promise.then(this._update, this._update) |
|
932 .then(null, console.error); |
|
933 } |
|
934 }; |
|
935 |
|
936 /** |
|
937 * Called when displayed Output says it's changed or from outputChanged, which |
|
938 * happens when there is a new displayed Output. |
|
939 */ |
|
940 OutputPanel.prototype._update = function() { |
|
941 // destroy has been called, bail out |
|
942 if (this._div == null) { |
|
943 return; |
|
944 } |
|
945 |
|
946 // Empty this._div |
|
947 while (this._div.hasChildNodes()) { |
|
948 this._div.removeChild(this._div.firstChild); |
|
949 } |
|
950 |
|
951 if (this.displayedOutput.data != null) { |
|
952 let context = this._devtoolbar.display.requisition.conversionContext; |
|
953 this.displayedOutput.convert('dom', context).then((node) => { |
|
954 while (this._div.hasChildNodes()) { |
|
955 this._div.removeChild(this._div.firstChild); |
|
956 } |
|
957 |
|
958 var links = node.ownerDocument.querySelectorAll('*[href]'); |
|
959 for (var i = 0; i < links.length; i++) { |
|
960 links[i].setAttribute('target', '_blank'); |
|
961 } |
|
962 |
|
963 this._div.appendChild(node); |
|
964 this.show(); |
|
965 }); |
|
966 } |
|
967 }; |
|
968 |
|
969 /** |
|
970 * Detach listeners from the currently displayed Output. |
|
971 */ |
|
972 OutputPanel.prototype.remove = function() { |
|
973 if (isLinux) { |
|
974 this.canHide = true; |
|
975 } |
|
976 |
|
977 if (this._panel && this._panel.hidePopup) { |
|
978 this._panel.hidePopup(); |
|
979 } |
|
980 |
|
981 if (this.displayedOutput) { |
|
982 delete this.displayedOutput; |
|
983 } |
|
984 }; |
|
985 |
|
986 /** |
|
987 * Detach listeners from the currently displayed Output. |
|
988 */ |
|
989 OutputPanel.prototype.destroy = function() { |
|
990 this.remove(); |
|
991 |
|
992 this._panel.removeEventListener("popuphiding", this._onpopuphiding, true); |
|
993 |
|
994 this._panel.removeChild(this._frame); |
|
995 this._toolbar.parentElement.removeChild(this._panel); |
|
996 |
|
997 delete this._devtoolbar; |
|
998 delete this._input; |
|
999 delete this._toolbar; |
|
1000 delete this._onpopuphiding; |
|
1001 delete this._panel; |
|
1002 delete this._frame; |
|
1003 delete this._content; |
|
1004 delete this._div; |
|
1005 delete this.document; |
|
1006 }; |
|
1007 |
|
1008 /** |
|
1009 * Called by GCLI to indicate that we should show or hide one either the |
|
1010 * tooltip panel or the output panel. |
|
1011 */ |
|
1012 OutputPanel.prototype._visibilityChanged = function(ev) { |
|
1013 if (ev.outputVisible === true) { |
|
1014 // this.show is called by _outputChanged |
|
1015 } else { |
|
1016 if (isLinux) { |
|
1017 this.canHide = true; |
|
1018 } |
|
1019 this._panel.hidePopup(); |
|
1020 } |
|
1021 }; |
|
1022 |
|
1023 /** |
|
1024 * Creating a TooltipPanel is asynchronous |
|
1025 */ |
|
1026 function TooltipPanel() { |
|
1027 throw new Error('Use TooltipPanel.create()'); |
|
1028 } |
|
1029 |
|
1030 /** |
|
1031 * Panel to handle tooltips. |
|
1032 * |
|
1033 * There is a tooltip bug on Windows and OSX that prevents tooltips from being |
|
1034 * positioned properly (bug 786975). There is a Gnome panel bug on Linux that |
|
1035 * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848). |
|
1036 * We now use a tooltip on Linux and a panel on OSX & Windows. |
|
1037 * |
|
1038 * If a panel has no content and no height it is not shown when openPopup is |
|
1039 * called on Windows and OSX (bug 692348) ... this prevents the panel from |
|
1040 * appearing the first time it is shown. Setting the panel's height to 1px |
|
1041 * before calling openPopup works around this issue as we resize it ourselves |
|
1042 * anyway. |
|
1043 * |
|
1044 * @param devtoolbar The parent DeveloperToolbar object |
|
1045 */ |
|
1046 TooltipPanel.create = function(devtoolbar) { |
|
1047 var tooltipPanel = Object.create(TooltipPanel.prototype); |
|
1048 return tooltipPanel._init(devtoolbar); |
|
1049 }; |
|
1050 |
|
1051 /** |
|
1052 * @private See TooltipPanel.create |
|
1053 */ |
|
1054 TooltipPanel.prototype._init = function(devtoolbar) { |
|
1055 let deferred = promise.defer(); |
|
1056 |
|
1057 let chromeDocument = devtoolbar._doc; |
|
1058 this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node"); |
|
1059 this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar"); |
|
1060 this._dimensions = { start: 0, end: 0 }; |
|
1061 |
|
1062 /* |
|
1063 <tooltip|panel id="gcli-tooltip" |
|
1064 type="arrow" |
|
1065 noautofocus="true" |
|
1066 noautohide="true" |
|
1067 class="gcli-panel"> |
|
1068 <html:iframe xmlns:html="http://www.w3.org/1999/xhtml" |
|
1069 id="gcli-tooltip-frame" |
|
1070 src="chrome://browser/content/devtools/commandlinetooltip.xhtml" |
|
1071 flex="1" |
|
1072 sandbox="allow-same-origin"/> |
|
1073 </tooltip|panel> |
|
1074 */ |
|
1075 |
|
1076 // TODO: Switch back from tooltip to panel when metacity focus issue is fixed: |
|
1077 // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 |
|
1078 this._panel = devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel"); |
|
1079 |
|
1080 this._panel.id = "gcli-tooltip"; |
|
1081 this._panel.classList.add("gcli-panel"); |
|
1082 |
|
1083 if (isLinux) { |
|
1084 this.canHide = false; |
|
1085 this._onpopuphiding = this._onpopuphiding.bind(this); |
|
1086 this._panel.addEventListener("popuphiding", this._onpopuphiding, true); |
|
1087 } else { |
|
1088 this._panel.setAttribute("noautofocus", "true"); |
|
1089 this._panel.setAttribute("noautohide", "true"); |
|
1090 |
|
1091 // Bug 692348: On Windows and OSX if a panel has no content and no height |
|
1092 // openPopup fails to display it. Setting the height to 1px alows the panel |
|
1093 // to be displayed before has content or a real height i.e. the first time |
|
1094 // it is displayed. |
|
1095 this._panel.setAttribute("height", "1px"); |
|
1096 } |
|
1097 |
|
1098 this._toolbar.parentElement.insertBefore(this._panel, this._toolbar); |
|
1099 |
|
1100 this._frame = devtoolbar._doc.createElementNS(NS_XHTML, "iframe"); |
|
1101 this._frame.id = "gcli-tooltip-frame"; |
|
1102 this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlinetooltip.xhtml"); |
|
1103 this._frame.setAttribute("flex", "1"); |
|
1104 this._frame.setAttribute("sandbox", "allow-same-origin"); |
|
1105 this._panel.appendChild(this._frame); |
|
1106 |
|
1107 /** |
|
1108 * Wire up the element from the iframe, and resolve the promise. |
|
1109 */ |
|
1110 let onload = () => { |
|
1111 this._frame.removeEventListener("load", onload, true); |
|
1112 |
|
1113 this.document = this._frame.contentDocument; |
|
1114 this.hintElement = this.document.getElementById("gcli-tooltip-root"); |
|
1115 this._connector = this.document.getElementById("gcli-tooltip-connector"); |
|
1116 |
|
1117 let styles = this._toolbar.ownerDocument.defaultView |
|
1118 .getComputedStyle(this._toolbar); |
|
1119 this.hintElement.setAttribute("dir", styles.direction); |
|
1120 |
|
1121 deferred.resolve(this); |
|
1122 }; |
|
1123 this._frame.addEventListener("load", onload, true); |
|
1124 |
|
1125 return deferred.promise; |
|
1126 } |
|
1127 |
|
1128 /** |
|
1129 * Prevent the popup from hiding if it is not permitted via this.canHide. |
|
1130 */ |
|
1131 TooltipPanel.prototype._onpopuphiding = function(ev) { |
|
1132 // TODO: When we switch back from tooltip to panel we can remove this hack: |
|
1133 // https://bugzilla.mozilla.org/show_bug.cgi?id=780102 |
|
1134 if (isLinux && !this.canHide) { |
|
1135 ev.preventDefault(); |
|
1136 } |
|
1137 }; |
|
1138 |
|
1139 /** |
|
1140 * Display the TooltipPanel. |
|
1141 */ |
|
1142 TooltipPanel.prototype.show = function(dimensions) { |
|
1143 if (!dimensions) { |
|
1144 dimensions = { start: 0, end: 0 }; |
|
1145 } |
|
1146 this._dimensions = dimensions; |
|
1147 |
|
1148 // This is nasty, but displaying the panel causes it to re-flow, which can |
|
1149 // change the size it should be, so we need to resize the iframe after the |
|
1150 // panel has displayed |
|
1151 this._panel.ownerDocument.defaultView.setTimeout(() => { |
|
1152 this._resize(); |
|
1153 }, 0); |
|
1154 |
|
1155 if (isLinux) { |
|
1156 this.canHide = false; |
|
1157 } |
|
1158 |
|
1159 this._resize(); |
|
1160 this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0, |
|
1161 false, false, null); |
|
1162 this._input.focus(); |
|
1163 }; |
|
1164 |
|
1165 /** |
|
1166 * One option is to spend lots of time taking an average width of characters |
|
1167 * in the current font, dynamically, and weighting for the frequency of use of |
|
1168 * various characters, or even to render the given string off screen, and then |
|
1169 * measure the width. |
|
1170 * Or we could do this... |
|
1171 */ |
|
1172 const AVE_CHAR_WIDTH = 4.5; |
|
1173 |
|
1174 /** |
|
1175 * Display the TooltipPanel. |
|
1176 */ |
|
1177 TooltipPanel.prototype._resize = function() { |
|
1178 if (this._panel == null || this.document == null || !this._panel.state == "closed") { |
|
1179 return |
|
1180 } |
|
1181 |
|
1182 let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH); |
|
1183 this._panel.style.marginLeft = offset + "px"; |
|
1184 |
|
1185 /* |
|
1186 // Bug 744906: UX review - Not sure if we want this code to fatten connector |
|
1187 // with param width |
|
1188 let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH); |
|
1189 width = Math.min(width, 100); |
|
1190 width = Math.max(width, 10); |
|
1191 this._connector.style.width = width + "px"; |
|
1192 */ |
|
1193 |
|
1194 this._frame.height = this.document.body.scrollHeight; |
|
1195 }; |
|
1196 |
|
1197 /** |
|
1198 * Hide the TooltipPanel. |
|
1199 */ |
|
1200 TooltipPanel.prototype.remove = function() { |
|
1201 if (isLinux) { |
|
1202 this.canHide = true; |
|
1203 } |
|
1204 if (this._panel && this._panel.hidePopup) { |
|
1205 this._panel.hidePopup(); |
|
1206 } |
|
1207 }; |
|
1208 |
|
1209 /** |
|
1210 * Hide the TooltipPanel. |
|
1211 */ |
|
1212 TooltipPanel.prototype.destroy = function() { |
|
1213 this.remove(); |
|
1214 |
|
1215 this._panel.removeEventListener("popuphiding", this._onpopuphiding, true); |
|
1216 |
|
1217 this._panel.removeChild(this._frame); |
|
1218 this._toolbar.parentElement.removeChild(this._panel); |
|
1219 |
|
1220 delete this._connector; |
|
1221 delete this._dimensions; |
|
1222 delete this._input; |
|
1223 delete this._onpopuphiding; |
|
1224 delete this._panel; |
|
1225 delete this._frame; |
|
1226 delete this._toolbar; |
|
1227 delete this._content; |
|
1228 delete this.document; |
|
1229 delete this.hintElement; |
|
1230 }; |
|
1231 |
|
1232 /** |
|
1233 * Called by GCLI to indicate that we should show or hide one either the |
|
1234 * tooltip panel or the output panel. |
|
1235 */ |
|
1236 TooltipPanel.prototype._visibilityChanged = function(ev) { |
|
1237 if (ev.tooltipVisible === true) { |
|
1238 this.show(ev.dimensions); |
|
1239 } else { |
|
1240 if (isLinux) { |
|
1241 this.canHide = true; |
|
1242 } |
|
1243 this._panel.hidePopup(); |
|
1244 } |
|
1245 }; |