|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 "use strict"; |
|
7 |
|
8 const {Cc, Ci, Cu} = require("chrome"); |
|
9 |
|
10 Cu.import("resource://gre/modules/Services.jsm"); |
|
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
12 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); |
|
13 |
|
14 const STACK_THICKNESS = 15; |
|
15 |
|
16 /** |
|
17 * Module containing various helper functions used throughout Tilt. |
|
18 */ |
|
19 this.TiltUtils = {}; |
|
20 module.exports = this.TiltUtils; |
|
21 |
|
22 /** |
|
23 * Various console/prompt output functions required by the engine. |
|
24 */ |
|
25 TiltUtils.Output = { |
|
26 |
|
27 /** |
|
28 * Logs a message to the console. |
|
29 * |
|
30 * @param {String} aMessage |
|
31 * the message to be logged |
|
32 */ |
|
33 log: function TUO_log(aMessage) |
|
34 { |
|
35 if (this.suppressLogs) { |
|
36 return; |
|
37 } |
|
38 // get the console service |
|
39 let consoleService = Cc["@mozilla.org/consoleservice;1"] |
|
40 .getService(Ci.nsIConsoleService); |
|
41 |
|
42 // log the message |
|
43 consoleService.logStringMessage(aMessage); |
|
44 }, |
|
45 |
|
46 /** |
|
47 * Logs an error to the console. |
|
48 * |
|
49 * @param {String} aMessage |
|
50 * the message to be logged |
|
51 * @param {Object} aProperties |
|
52 * and object containing script error initialization details |
|
53 */ |
|
54 error: function TUO_error(aMessage, aProperties) |
|
55 { |
|
56 if (this.suppressErrors) { |
|
57 return; |
|
58 } |
|
59 // make sure the properties parameter is a valid object |
|
60 aProperties = aProperties || {}; |
|
61 |
|
62 // get the console service |
|
63 let consoleService = Cc["@mozilla.org/consoleservice;1"] |
|
64 .getService(Ci.nsIConsoleService); |
|
65 |
|
66 // get the script error service |
|
67 let scriptError = Cc["@mozilla.org/scripterror;1"] |
|
68 .createInstance(Ci.nsIScriptError); |
|
69 |
|
70 // initialize a script error |
|
71 scriptError.init(aMessage, |
|
72 aProperties.sourceName || "", |
|
73 aProperties.sourceLine || "", |
|
74 aProperties.lineNumber || 0, |
|
75 aProperties.columnNumber || 0, |
|
76 aProperties.flags || 0, |
|
77 aProperties.category || ""); |
|
78 |
|
79 // log the error |
|
80 consoleService.logMessage(scriptError); |
|
81 }, |
|
82 |
|
83 /** |
|
84 * Shows a modal alert message popup. |
|
85 * |
|
86 * @param {String} aTitle |
|
87 * the title of the popup |
|
88 * @param {String} aMessage |
|
89 * the message to be logged |
|
90 */ |
|
91 alert: function TUO_alert(aTitle, aMessage) |
|
92 { |
|
93 if (this.suppressAlerts) { |
|
94 return; |
|
95 } |
|
96 if (!aMessage) { |
|
97 aMessage = aTitle; |
|
98 aTitle = ""; |
|
99 } |
|
100 |
|
101 // get the prompt service |
|
102 let prompt = Cc["@mozilla.org/embedcomp/prompt-service;1"] |
|
103 .getService(Ci.nsIPromptService); |
|
104 |
|
105 // show the alert message |
|
106 prompt.alert(null, aTitle, aMessage); |
|
107 } |
|
108 }; |
|
109 |
|
110 /** |
|
111 * Helper functions for managing preferences. |
|
112 */ |
|
113 TiltUtils.Preferences = { |
|
114 |
|
115 /** |
|
116 * Gets a custom Tilt preference. |
|
117 * If the preference does not exist, undefined is returned. If it does exist, |
|
118 * but the type is not correctly specified, null is returned. |
|
119 * |
|
120 * @param {String} aPref |
|
121 * the preference name |
|
122 * @param {String} aType |
|
123 * either "boolean", "string" or "integer" |
|
124 * |
|
125 * @return {Boolean | String | Number} the requested preference |
|
126 */ |
|
127 get: function TUP_get(aPref, aType) |
|
128 { |
|
129 if (!aPref || !aType) { |
|
130 return; |
|
131 } |
|
132 |
|
133 try { |
|
134 let prefs = this._branch; |
|
135 |
|
136 switch(aType) { |
|
137 case "boolean": |
|
138 return prefs.getBoolPref(aPref); |
|
139 case "string": |
|
140 return prefs.getCharPref(aPref); |
|
141 case "integer": |
|
142 return prefs.getIntPref(aPref); |
|
143 } |
|
144 return null; |
|
145 |
|
146 } catch(e) { |
|
147 // handle any unexpected exceptions |
|
148 TiltUtils.Output.error(e.message); |
|
149 return undefined; |
|
150 } |
|
151 }, |
|
152 |
|
153 /** |
|
154 * Sets a custom Tilt preference. |
|
155 * If the preference already exists, it is overwritten. |
|
156 * |
|
157 * @param {String} aPref |
|
158 * the preference name |
|
159 * @param {String} aType |
|
160 * either "boolean", "string" or "integer" |
|
161 * @param {String} aValue |
|
162 * a new preference value |
|
163 * |
|
164 * @return {Boolean} true if the preference was set successfully |
|
165 */ |
|
166 set: function TUP_set(aPref, aType, aValue) |
|
167 { |
|
168 if (!aPref || !aType || aValue === undefined || aValue === null) { |
|
169 return; |
|
170 } |
|
171 |
|
172 try { |
|
173 let prefs = this._branch; |
|
174 |
|
175 switch(aType) { |
|
176 case "boolean": |
|
177 return prefs.setBoolPref(aPref, aValue); |
|
178 case "string": |
|
179 return prefs.setCharPref(aPref, aValue); |
|
180 case "integer": |
|
181 return prefs.setIntPref(aPref, aValue); |
|
182 } |
|
183 } catch(e) { |
|
184 // handle any unexpected exceptions |
|
185 TiltUtils.Output.error(e.message); |
|
186 } |
|
187 return false; |
|
188 }, |
|
189 |
|
190 /** |
|
191 * Creates a custom Tilt preference. |
|
192 * If the preference already exists, it is left unchanged. |
|
193 * |
|
194 * @param {String} aPref |
|
195 * the preference name |
|
196 * @param {String} aType |
|
197 * either "boolean", "string" or "integer" |
|
198 * @param {String} aValue |
|
199 * the initial preference value |
|
200 * |
|
201 * @return {Boolean} true if the preference was initialized successfully |
|
202 */ |
|
203 create: function TUP_create(aPref, aType, aValue) |
|
204 { |
|
205 if (!aPref || !aType || aValue === undefined || aValue === null) { |
|
206 return; |
|
207 } |
|
208 |
|
209 try { |
|
210 let prefs = this._branch; |
|
211 |
|
212 if (!prefs.prefHasUserValue(aPref)) { |
|
213 switch(aType) { |
|
214 case "boolean": |
|
215 return prefs.setBoolPref(aPref, aValue); |
|
216 case "string": |
|
217 return prefs.setCharPref(aPref, aValue); |
|
218 case "integer": |
|
219 return prefs.setIntPref(aPref, aValue); |
|
220 } |
|
221 } |
|
222 } catch(e) { |
|
223 // handle any unexpected exceptions |
|
224 TiltUtils.Output.error(e.message); |
|
225 } |
|
226 return false; |
|
227 }, |
|
228 |
|
229 /** |
|
230 * The preferences branch for this extension. |
|
231 */ |
|
232 _branch: (function(aBranch) { |
|
233 return Cc["@mozilla.org/preferences-service;1"] |
|
234 .getService(Ci.nsIPrefService) |
|
235 .getBranch(aBranch); |
|
236 |
|
237 }("devtools.tilt.")) |
|
238 }; |
|
239 |
|
240 /** |
|
241 * Easy way to access the string bundle. |
|
242 */ |
|
243 TiltUtils.L10n = { |
|
244 |
|
245 /** |
|
246 * The string bundle element. |
|
247 */ |
|
248 stringBundle: null, |
|
249 |
|
250 /** |
|
251 * Returns a string in the string bundle. |
|
252 * If the string bundle is not found, null is returned. |
|
253 * |
|
254 * @param {String} aName |
|
255 * the string name in the bundle |
|
256 * |
|
257 * @return {String} the equivalent string from the bundle |
|
258 */ |
|
259 get: function TUL_get(aName) |
|
260 { |
|
261 // check to see if the parent string bundle document element is valid |
|
262 if (!this.stringBundle || !aName) { |
|
263 return null; |
|
264 } |
|
265 return this.stringBundle.GetStringFromName(aName); |
|
266 }, |
|
267 |
|
268 /** |
|
269 * Returns a formatted string using the string bundle. |
|
270 * If the string bundle is not found, null is returned. |
|
271 * |
|
272 * @param {String} aName |
|
273 * the string name in the bundle |
|
274 * @param {Array} aArgs |
|
275 * an array of arguments for the formatted string |
|
276 * |
|
277 * @return {String} the equivalent formatted string from the bundle |
|
278 */ |
|
279 format: function TUL_format(aName, aArgs) |
|
280 { |
|
281 // check to see if the parent string bundle document element is valid |
|
282 if (!this.stringBundle || !aName || !aArgs) { |
|
283 return null; |
|
284 } |
|
285 return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length); |
|
286 } |
|
287 }; |
|
288 |
|
289 /** |
|
290 * Utilities for accessing and manipulating a document. |
|
291 */ |
|
292 TiltUtils.DOM = { |
|
293 |
|
294 /** |
|
295 * Current parent node object used when creating canvas elements. |
|
296 */ |
|
297 parentNode: null, |
|
298 |
|
299 /** |
|
300 * Helper method, allowing to easily create and manage a canvas element. |
|
301 * If the width and height params are falsy, they default to the parent node |
|
302 * client width and height. |
|
303 * |
|
304 * @param {Document} aParentNode |
|
305 * the parent node used to create the canvas |
|
306 * if not specified, it will be reused from the cache |
|
307 * @param {Object} aProperties |
|
308 * optional, object containing some of the following props: |
|
309 * {Boolean} focusable |
|
310 * optional, true to make the canvas focusable |
|
311 * {Boolean} append |
|
312 * optional, true to append the canvas to the parent node |
|
313 * {Number} width |
|
314 * optional, specifies the width of the canvas |
|
315 * {Number} height |
|
316 * optional, specifies the height of the canvas |
|
317 * {String} id |
|
318 * optional, id for the created canvas element |
|
319 * |
|
320 * @return {HTMLCanvasElement} the newly created canvas element |
|
321 */ |
|
322 initCanvas: function TUD_initCanvas(aParentNode, aProperties) |
|
323 { |
|
324 // check to see if the parent node element is valid |
|
325 if (!(aParentNode = aParentNode || this.parentNode)) { |
|
326 return null; |
|
327 } |
|
328 |
|
329 // make sure the properties parameter is a valid object |
|
330 aProperties = aProperties || {}; |
|
331 |
|
332 // cache this parent node so that it can be reused |
|
333 this.parentNode = aParentNode; |
|
334 |
|
335 // create the canvas element |
|
336 let canvas = aParentNode.ownerDocument. |
|
337 createElementNS("http://www.w3.org/1999/xhtml", "canvas"); |
|
338 |
|
339 let width = aProperties.width || aParentNode.clientWidth; |
|
340 let height = aProperties.height || aParentNode.clientHeight; |
|
341 let id = aProperties.id || null; |
|
342 |
|
343 canvas.setAttribute("style", "min-width: 1px; min-height: 1px;"); |
|
344 canvas.setAttribute("width", width); |
|
345 canvas.setAttribute("height", height); |
|
346 canvas.setAttribute("id", id); |
|
347 |
|
348 // the canvas is unfocusable by default, we may require otherwise |
|
349 if (aProperties.focusable) { |
|
350 canvas.setAttribute("tabindex", "1"); |
|
351 canvas.style.outline = "none"; |
|
352 } |
|
353 |
|
354 // append the canvas element to the current parent node, if specified |
|
355 if (aProperties.append) { |
|
356 aParentNode.appendChild(canvas); |
|
357 } |
|
358 |
|
359 return canvas; |
|
360 }, |
|
361 |
|
362 /** |
|
363 * Gets the full webpage dimensions (width and height). |
|
364 * |
|
365 * @param {Window} aContentWindow |
|
366 * the content window holding the document |
|
367 * |
|
368 * @return {Object} an object containing the width and height coords |
|
369 */ |
|
370 getContentWindowDimensions: function TUD_getContentWindowDimensions( |
|
371 aContentWindow) |
|
372 { |
|
373 return { |
|
374 width: aContentWindow.innerWidth + aContentWindow.scrollMaxX, |
|
375 height: aContentWindow.innerHeight + aContentWindow.scrollMaxY |
|
376 }; |
|
377 }, |
|
378 |
|
379 /** |
|
380 * Calculates the position and depth to display a node, this can be overriden |
|
381 * to change the visualization. |
|
382 * |
|
383 * @param {Window} aContentWindow |
|
384 * the window content holding the document |
|
385 * @param {Node} aNode |
|
386 * the node to get the position for |
|
387 * @param {Object} aParentPosition |
|
388 * the position of the parent node, as returned by this |
|
389 * function |
|
390 * |
|
391 * @return {Object} an object describing the node's position in 3D space |
|
392 * containing the following properties: |
|
393 * {Number} top |
|
394 * distance along the x axis |
|
395 * {Number} left |
|
396 * distance along the y axis |
|
397 * {Number} depth |
|
398 * distance along the z axis |
|
399 * {Number} width |
|
400 * width of the node |
|
401 * {Number} height |
|
402 * height of the node |
|
403 * {Number} thickness |
|
404 * thickness of the node |
|
405 */ |
|
406 getNodePosition: function TUD_getNodePosition(aContentWindow, aNode, |
|
407 aParentPosition) { |
|
408 let lh = new LayoutHelpers(aContentWindow); |
|
409 // get the x, y, width and height coordinates of the node |
|
410 let coord = lh.getRect(aNode, aContentWindow); |
|
411 if (!coord) { |
|
412 return null; |
|
413 } |
|
414 |
|
415 coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0; |
|
416 coord.thickness = STACK_THICKNESS; |
|
417 |
|
418 return coord; |
|
419 }, |
|
420 |
|
421 /** |
|
422 * Traverses a document object model & calculates useful info for each node. |
|
423 * |
|
424 * @param {Window} aContentWindow |
|
425 * the window content holding the document |
|
426 * @param {Object} aProperties |
|
427 * optional, an object containing the following properties: |
|
428 * {Function} nodeCallback |
|
429 * a function to call instead of TiltUtils.DOM.getNodePosition |
|
430 * to get the position and depth to display nodes |
|
431 * {Object} invisibleElements |
|
432 * elements which should be ignored |
|
433 * {Number} minSize |
|
434 * the minimum dimensions needed for a node to be traversed |
|
435 * {Number} maxX |
|
436 * the maximum left position of an element |
|
437 * {Number} maxY |
|
438 * the maximum top position of an element |
|
439 * |
|
440 * @return {Array} list containing nodes positions and local names |
|
441 */ |
|
442 traverse: function TUD_traverse(aContentWindow, aProperties) |
|
443 { |
|
444 // make sure the properties parameter is a valid object |
|
445 aProperties = aProperties || {}; |
|
446 |
|
447 let aInvisibleElements = aProperties.invisibleElements || {}; |
|
448 let aMinSize = aProperties.minSize || -1; |
|
449 let aMaxX = aProperties.maxX || Number.MAX_VALUE; |
|
450 let aMaxY = aProperties.maxY || Number.MAX_VALUE; |
|
451 |
|
452 let nodeCallback = aProperties.nodeCallback || this.getNodePosition.bind(this); |
|
453 |
|
454 let nodes = aContentWindow.document.childNodes; |
|
455 let store = { info: [], nodes: [] }; |
|
456 let depth = 0; |
|
457 |
|
458 let queue = [ |
|
459 { parentPosition: null, nodes: aContentWindow.document.childNodes } |
|
460 ] |
|
461 |
|
462 while (queue.length) { |
|
463 let { nodes, parentPosition } = queue.shift(); |
|
464 |
|
465 for (let node of nodes) { |
|
466 // skip some nodes to avoid visualization meshes that are too bloated |
|
467 let name = node.localName; |
|
468 if (!name || aInvisibleElements[name]) { |
|
469 continue; |
|
470 } |
|
471 |
|
472 let coord = nodeCallback(aContentWindow, node, parentPosition); |
|
473 if (!coord) { |
|
474 continue; |
|
475 } |
|
476 |
|
477 // the maximum size slices the traversal where needed |
|
478 if (coord.left > aMaxX || coord.top > aMaxY) { |
|
479 continue; |
|
480 } |
|
481 |
|
482 // use this node only if it actually has visible dimensions |
|
483 if (coord.width > aMinSize && coord.height > aMinSize) { |
|
484 |
|
485 // save the necessary details into a list to be returned later |
|
486 store.info.push({ coord: coord, name: name }); |
|
487 store.nodes.push(node); |
|
488 } |
|
489 |
|
490 let childNodes = (name === "iframe" || name === "frame") ? node.contentDocument.childNodes : node.childNodes; |
|
491 if (childNodes.length > 0) |
|
492 queue.push({ parentPosition: coord, nodes: childNodes }); |
|
493 } |
|
494 } |
|
495 |
|
496 return store; |
|
497 } |
|
498 }; |
|
499 |
|
500 /** |
|
501 * Binds a new owner object to the child functions. |
|
502 * If the new parent is not specified, it will default to the passed scope. |
|
503 * |
|
504 * @param {Object} aScope |
|
505 * the object from which all functions will be rebound |
|
506 * @param {String} aRegex |
|
507 * a regular expression to identify certain functions |
|
508 * @param {Object} aParent |
|
509 * the new parent for the object's functions |
|
510 */ |
|
511 TiltUtils.bindObjectFunc = function TU_bindObjectFunc(aScope, aRegex, aParent) |
|
512 { |
|
513 if (!aScope) { |
|
514 return; |
|
515 } |
|
516 |
|
517 for (let i in aScope) { |
|
518 try { |
|
519 if ("function" === typeof aScope[i] && (aRegex ? i.match(aRegex) : 1)) { |
|
520 aScope[i] = aScope[i].bind(aParent || aScope); |
|
521 } |
|
522 } catch(e) { |
|
523 TiltUtils.Output.error(e); |
|
524 } |
|
525 } |
|
526 }; |
|
527 |
|
528 /** |
|
529 * Destroys an object and deletes all members. |
|
530 * |
|
531 * @param {Object} aScope |
|
532 * the object from which all children will be destroyed |
|
533 */ |
|
534 TiltUtils.destroyObject = function TU_destroyObject(aScope) |
|
535 { |
|
536 if (!aScope) { |
|
537 return; |
|
538 } |
|
539 |
|
540 // objects in Tilt usually use a function to handle internal destruction |
|
541 if ("function" === typeof aScope._finalize) { |
|
542 aScope._finalize(); |
|
543 } |
|
544 for (let i in aScope) { |
|
545 if (aScope.hasOwnProperty(i)) { |
|
546 delete aScope[i]; |
|
547 } |
|
548 } |
|
549 }; |
|
550 |
|
551 /** |
|
552 * Retrieve the unique ID of a window object. |
|
553 * |
|
554 * @param {Window} aWindow |
|
555 * the window to get the ID from |
|
556 * |
|
557 * @return {Number} the window ID |
|
558 */ |
|
559 TiltUtils.getWindowId = function TU_getWindowId(aWindow) |
|
560 { |
|
561 if (!aWindow) { |
|
562 return; |
|
563 } |
|
564 |
|
565 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
566 .getInterface(Ci.nsIDOMWindowUtils) |
|
567 .currentInnerWindowID; |
|
568 }; |
|
569 |
|
570 /** |
|
571 * Sets the markup document viewer zoom for the currently selected browser. |
|
572 * |
|
573 * @param {Window} aChromeWindow |
|
574 * the top-level browser window |
|
575 * |
|
576 * @param {Number} the zoom ammount |
|
577 */ |
|
578 TiltUtils.setDocumentZoom = function TU_setDocumentZoom(aChromeWindow, aZoom) { |
|
579 aChromeWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom; |
|
580 }; |
|
581 |
|
582 /** |
|
583 * Performs a garbage collection. |
|
584 * |
|
585 * @param {Window} aChromeWindow |
|
586 * the top-level browser window |
|
587 */ |
|
588 TiltUtils.gc = function TU_gc(aChromeWindow) |
|
589 { |
|
590 aChromeWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
591 .getInterface(Ci.nsIDOMWindowUtils) |
|
592 .garbageCollect(); |
|
593 }; |
|
594 |
|
595 /** |
|
596 * Clears the cache and sets all the variables to null. |
|
597 */ |
|
598 TiltUtils.clearCache = function TU_clearCache() |
|
599 { |
|
600 TiltUtils.DOM.parentNode = null; |
|
601 }; |
|
602 |
|
603 // bind the owner object to the necessary functions |
|
604 TiltUtils.bindObjectFunc(TiltUtils.Output); |
|
605 TiltUtils.bindObjectFunc(TiltUtils.Preferences); |
|
606 TiltUtils.bindObjectFunc(TiltUtils.L10n); |
|
607 TiltUtils.bindObjectFunc(TiltUtils.DOM); |
|
608 |
|
609 // set the necessary string bundle |
|
610 XPCOMUtils.defineLazyGetter(TiltUtils.L10n, "stringBundle", function() { |
|
611 return Services.strings.createBundle( |
|
612 "chrome://browser/locale/devtools/tilt.properties"); |
|
613 }); |