1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/lib/object", 5 "arch/compilationunit", 6 "firebug/lib/events", 7 "firebug/lib/url", 8 "firebug/chrome/window", 9 "firebug/lib/css", 10 "firebug/chrome/plugin", 11 ], 12 function(Obj, CompilationUnit, Events, Url, Win, Css) { 13 14 // ********************************************************************************************* // 15 // Constants 16 17 const throttleTimeWindow = 200; 18 const throttleMessageLimit = 30; 19 const throttleInterval = 30; 20 const throttleFlushCount = 20; 21 const refreshDelay = 300; 22 23 // ********************************************************************************************* // 24 25 Firebug.TabContext = function(win, browser, chrome, persistedState) 26 { 27 this.window = win; 28 this.browser = browser; 29 this.persistedState = persistedState; 30 31 this.name = Url.normalizeURL(this.getWindowLocation().toString()); 32 33 this.windows = []; 34 this.panelMap = {}; 35 this.sidePanelNames = {}; 36 37 this.compilationUnits = {}; 38 this.sourceFileByTag = {}; // mozilla only 39 40 // New nsITraceableChannel interface (introduced in FF3.0.4) makes possible 41 // to re-implement source-cache so that it solves the double-load problem. 42 // Anyway, keep the previous cache implementation for backward compatibility 43 // (with Firefox 3.0.3 and lower) 44 if (Components.interfaces.nsITraceableChannel) 45 this.sourceCache = new Firebug.TabCache(this); 46 else 47 this.sourceCache = new Firebug.SourceCache(this); 48 49 this.global = win; // used by chromebug 50 51 // -- Back end support -- 52 this.sourceFileMap = {}; // backend 53 }; 54 55 Firebug.TabContext.prototype = 56 { 57 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 58 // Browser Tools Interface BrowserContext 59 60 getCompilationUnit: function(url) 61 { 62 return this.compilationUnits[url]; 63 }, 64 65 getAllCompilationUnits: function() 66 { 67 return Firebug.SourceFile.mapAsArray(this.compilationUnits); 68 }, 69 70 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 71 72 getWindowLocation: function() 73 { 74 return Win.safeGetWindowLocation(this.window); 75 }, 76 77 getTitle: function() 78 { 79 if (this.window && this.window.document) 80 return this.window.document.title; 81 else 82 return ""; 83 }, 84 85 getName: function() 86 { 87 if (!this.name || this.name === "about:blank") 88 { 89 var url = this.getWindowLocation().toString(); 90 if (Url.isDataURL(url)) 91 { 92 var props = Url.splitDataURL(url); 93 if (props.fileName) 94 this.name = "data url from "+props.fileName; 95 } 96 else 97 { 98 this.name = Url.normalizeURL(url); 99 if (this.name === "about:blank" && this.window.frameElement) 100 this.name += " in "+Css.getElementCSSSelector(this.window.frameElement); 101 } 102 } 103 return this.name; 104 }, 105 106 getGlobalScope: function() 107 { 108 return this.window; 109 }, 110 111 addSourceFile: function(sourceFile) 112 { 113 if (!this.sourceFileMap) 114 { 115 FBTrace.sysout("tabContext.addSourceFile; ERROR no source map!"); 116 return; 117 } 118 119 this.sourceFileMap[sourceFile.href] = sourceFile; 120 sourceFile.context = this; 121 122 this.addTags(sourceFile); 123 124 var kind = CompilationUnit.SCRIPT_TAG; 125 if (sourceFile.compilation_unit_type == "event") 126 kind = CompilationUnit.BROWSER_GENERATED; 127 128 if (sourceFile.compilation_unit_type == "eval") 129 kind = CompilationUnit.EVAL; 130 131 var url = sourceFile.href; 132 if (FBTrace.DBG_COMPILATION_UNITS) 133 FBTrace.sysout("onCompilationUnit " + url, [this, url, kind] ); 134 135 Firebug.connection.dispatch("onCompilationUnit", [this, url, kind]); 136 137 // HACKs 138 var compilationUnit = this.getCompilationUnit(url); 139 if (!compilationUnit) 140 { 141 if (FBTrace.DBG_COMPILATION_UNITS || FBTrace.DBG_ERRORS) 142 FBTrace.sysout("tabContext.addSourceFile; ERROR Unknown URL: " + url, 143 this.compilationUnits); 144 return; 145 } 146 147 compilationUnit.sourceFile = sourceFile; 148 149 compilationUnit.getSourceLines(-1, -1, function onLines(compilationUnit, 150 firstLineNumber, lastLineNumber, lines) 151 { 152 Firebug.connection.dispatch("onSourceLines", arguments); 153 154 if (FBTrace.DBG_COMPILATION_UNITS) 155 FBTrace.sysout("onSourceLines "+compilationUnit.getURL() + " " + lines.length + 156 " lines", compilationUnit); 157 }); 158 }, 159 160 removeSourceFile: function(sourceFile) 161 { 162 if (FBTrace.DBG_SOURCEFILES) 163 FBTrace.sysout("tabContext.removeSourceFile " + sourceFile.href + " in context " + 164 sourceFile.context.getName()); 165 166 delete this.sourceFileMap[sourceFile.href]; 167 delete sourceFile.context; 168 169 // ?? Firebug.onSourceFileDestroyed(this, sourceFile); 170 }, 171 172 addTags: function(sourceFile) 173 { 174 if (sourceFile.outerScript) 175 this.sourceFileByTag[sourceFile.outerScript.tag] = sourceFile; 176 177 for (var innerTag in sourceFile.innerScripts) 178 this.sourceFileByTag[innerTag] = sourceFile; 179 }, 180 181 getSourceFileByTag: function(tag) 182 { 183 return this.sourceFileByTag[tag]; 184 }, 185 186 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 187 188 // backward compat 189 get chrome() 190 { 191 return Firebug.chrome; 192 }, 193 194 destroy: function(state) 195 { 196 // All existing timeouts need to be cleared 197 if (this.timeouts) 198 { 199 for (var timeout in this.timeouts) 200 clearTimeout(timeout); 201 } 202 203 // Also all waiting intervals must be cleared. 204 if (this.intervals) 205 { 206 for (var timeout in this.intervals) 207 clearInterval(timeout); 208 } 209 210 if (this.throttleTimeout) 211 clearTimeout(this.throttleTimeout); 212 213 // All existing DOM listeners need to be cleared. Note that context is destroyed 214 // when the top level window is unloaded. However, some listeners can be registered 215 // to iframes (documents), which can be already unloaded at this point. 216 // Removing listeners from such 'unloaded' documents (or window) can throw 217 // "TypeError: can't access dead object" 218 // We should avoid these exceptions (even if they are not representing memory leaks) 219 this.unregisterAllListeners(); 220 221 state.panelState = {}; 222 223 // Inherit panelStates that have not been restored yet 224 if (this.persistedState) 225 { 226 for (var panelName in this.persistedState.panelState) 227 state.panelState[panelName] = this.persistedState.panelState[panelName]; 228 } 229 230 // Destroy all panels in this context. 231 for (var panelName in this.panelMap) 232 { 233 var panelType = Firebug.getPanelType(panelName); 234 this.destroyPanel(panelType, state); 235 } 236 237 if (FBTrace.DBG_INITIALIZE) 238 FBTrace.sysout("tabContext.destroy " + this.getName() + " set state ", state); 239 }, 240 241 getPanel: function(panelName, noCreate) 242 { 243 // Get "global" panelType, registered using Firebug.registerPanel 244 var panelType = Firebug.getPanelType(panelName); 245 246 // The panelType can be "local", available only within the context. 247 if (!panelType && this.panelTypeMap && this.panelTypeMap.hasOwnProperty(panelName)) 248 panelType = this.panelTypeMap[panelName]; 249 250 if (!panelType) 251 return null; 252 253 if (!panelType.prototype) 254 { 255 FBTrace.sysout("tabContext.getPanel no prototype "+panelType, panelType); 256 return; 257 } 258 259 // Create instance of the panelType only if it's enabled. 260 var enabled = panelType.prototype.isEnabled ? panelType.prototype.isEnabled() : true; 261 if (enabled) 262 return this.getPanelByType(panelType, noCreate); 263 264 return null; 265 }, 266 267 getPanelByType: function(panelType, noCreate) 268 { 269 if (!panelType || !this.panelMap) 270 return null; 271 272 var panelName = panelType.prototype.name; 273 if ( this.panelMap.hasOwnProperty(panelName) ) 274 return this.panelMap[panelName]; 275 else if (!noCreate) 276 return this.createPanel(panelType); 277 }, 278 279 eachPanelInContext: function(callback) 280 { 281 for (var panelName in this.panelMap) 282 { 283 if (this.panelMap.hasOwnProperty(panelName)) 284 { 285 var panel = this.panelMap[panelName]; 286 var rc = callback(panel); 287 if (rc) 288 return rc; 289 } 290 } 291 }, 292 293 createPanel: function(panelType) 294 { 295 // Instantiate a panel object. This is why panels are defined by prototype inheritance 296 var panel = new panelType(); 297 this.panelMap[panel.name] = panel; 298 299 if (FBTrace.DBG_PANELS) 300 FBTrace.sysout("tabContext.createPanel; Panel created: " + panel.name, panel); 301 302 Events.dispatch(Firebug.modules, "onCreatePanel", [this, panel, panelType]); 303 304 // Initialize panel and associate with a document. 305 if (panel.parentPanel) 306 { 307 // then this new panel is a side panel 308 panel.mainPanel = this.panelMap[panel.parentPanel]; 309 if (panel.mainPanel) 310 { 311 // then our panel map is consistent 312 // wire the side panel to get UI events from the main panel 313 panel.mainPanel.addListener(panel); 314 } 315 else 316 { 317 // then our panel map is broken, maybe by an extension failure. 318 if (FBTrace.DBG_ERRORS) 319 FBTrace.sysout("tabContext.createPanel panel.mainPanel missing " + 320 panel.name + " from " + panel.parentPanel.name); 321 } 322 } 323 324 var doc = this.chrome.getPanelDocument(panelType); 325 panel.initialize(this, doc); 326 327 return panel; 328 }, 329 330 destroyPanel: function(panelType, state) 331 { 332 var panelName = panelType.prototype.name; 333 var panel = this.panelMap[panelName]; 334 if (!panel) 335 return; 336 337 // Create an object to persist state, re-using old one if it was never restored 338 var panelState = panelName in state.panelState ? state.panelState[panelName] : {}; 339 state.panelState[panelName] = panelState; 340 341 try 342 { 343 // Destroy the panel and allow it to persist extra info to the state object 344 var dontRemove = panel.destroy(panelState); 345 delete this.panelMap[panelName]; 346 347 if (dontRemove) 348 return; 349 } 350 catch (exc) 351 { 352 if (FBTrace.DBG_ERRORS) 353 FBTrace.sysout("tabContext.destroy FAILS (" + panelName + ") " + exc, exc); 354 355 // the destroy failed, don't keep the bad state 356 delete state.panelState[panelName]; 357 } 358 359 // Remove the panel node from the DOM and so delete its content. 360 var panelNode = panel.panelNode; 361 if (panelNode && panelNode.parentNode) 362 panelNode.parentNode.removeChild(panelNode); 363 }, 364 365 removePanel: function(panelType, state) 366 { 367 var panelName = panelType.prototype.name; 368 if (!this.panelMap.hasOwnProperty(panelName)) 369 return null; 370 371 state.panelState = {}; 372 373 this.destroyPanel(panelType, state); 374 }, 375 376 // allows a panel from one context to be used in other contexts. 377 setPanel: function(panelName, panel) 378 { 379 if (panel) 380 this.panelMap[panelName] = panel; 381 else 382 delete this.panelMap[panelName]; 383 }, 384 385 invalidatePanels: function() 386 { 387 if (!this.invalidPanels) 388 this.invalidPanels = {}; 389 390 for (var i = 0; i < arguments.length; ++i) 391 { 392 var panelName = arguments[i]; 393 var panel = this.getPanel(panelName, true); 394 if (panel && !panel.noRefresh) 395 this.invalidPanels[panelName] = 1; 396 } 397 398 if (this.refreshTimeout) 399 { 400 this.clearTimeout(this.refreshTimeout); 401 delete this.refreshTimeout; 402 } 403 404 this.refreshTimeout = this.setTimeout(Obj.bindFixed(function() 405 { 406 var invalids = []; 407 408 for (var panelName in this.invalidPanels) 409 { 410 var panel = this.getPanel(panelName, true); 411 if (panel) 412 { 413 if (panel.visible && !panel.editing) 414 panel.refresh(); 415 else 416 panel.needsRefresh = true; 417 418 // If the panel is being edited, we'll keep trying to 419 // refresh it until editing is done 420 if (panel.editing) 421 invalids.push(panelName); 422 } 423 } 424 425 delete this.invalidPanels; 426 delete this.refreshTimeout; 427 428 // Keep looping until every tab is valid 429 if (invalids.length) 430 this.invalidatePanels.apply(this, invalids); 431 432 }, this), refreshDelay); 433 }, 434 435 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 436 // Timeouts and Intervals 437 438 setTimeout: function(fn, delay) 439 { 440 if (setTimeout == this.setTimeout) 441 throw new Error("setTimeout recursion"); 442 443 // we're using a sandboxed setTimeout function 444 var timeout = setTimeout(fn, delay); 445 446 if (!this.timeouts) 447 this.timeouts = {}; 448 449 this.timeouts[timeout] = 1; 450 451 return timeout; 452 }, 453 454 clearTimeout: function(timeout) 455 { 456 // we're using a sandboxed clearTimeout function 457 clearTimeout(timeout); 458 459 if (this.timeouts) 460 delete this.timeouts[timeout]; 461 }, 462 463 setInterval: function(fn, delay) 464 { 465 // we're using a sandboxed setInterval function 466 var timeout = setInterval(fn, delay); 467 468 if (!this.intervals) 469 this.intervals = {}; 470 471 this.intervals[timeout] = 1; 472 473 return timeout; 474 }, 475 476 clearInterval: function(timeout) 477 { 478 // we're using a sandboxed clearInterval function 479 clearInterval(timeout); 480 481 if (this.intervals) 482 delete this.intervals[timeout]; 483 }, 484 485 delay: function(message, object) 486 { 487 this.throttle(message, object, null, true); 488 }, 489 490 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 491 492 // queue the call |object.message(arg)| or just delay it if forceDelay 493 throttle: function(message, object, args, forceDelay) 494 { 495 if (!this.throttleInit) 496 { 497 this.throttleBuildup = 0; 498 this.throttleQueue = []; 499 this.throttleTimeout = 0; 500 this.lastMessageTime = 0; 501 this.throttleInit = true; 502 } 503 504 if (!forceDelay) 505 { 506 if (!Firebug.throttleMessages) 507 { 508 message.apply(object, args); 509 return false; 510 } 511 512 // Count how many messages have been logged during the throttle period 513 var logTime = new Date().getTime(); 514 if (logTime - this.lastMessageTime < throttleTimeWindow) 515 ++this.throttleBuildup; 516 else 517 this.throttleBuildup = 0; 518 519 this.lastMessageTime = logTime; 520 521 // If the throttle limit has been passed, enqueue the message to be 522 // logged later on a timer, otherwise just execute it now 523 if (!this.throttleQueue.length && this.throttleBuildup <= throttleMessageLimit) 524 { 525 message.apply(object, args); 526 return false; 527 } 528 } 529 530 this.throttleQueue.push(message, object, args); 531 532 if (this.throttleTimeout) 533 this.clearTimeout(this.throttleTimeout); 534 535 var self = this; 536 this.throttleTimeout = 537 this.setTimeout(function() { self.flushThrottleQueue(); }, throttleInterval); 538 539 return true; 540 }, 541 542 flushThrottleQueue: function() 543 { 544 var queue = this.throttleQueue; 545 546 if (!queue[0]) 547 FBTrace.sysout("tabContext.flushThrottleQueue no queue[0]", queue); 548 549 var max = throttleFlushCount * 3; 550 if (max > queue.length) 551 max = queue.length; 552 553 for (var i = 0; i < max; i += 3) 554 queue[i].apply(queue[i+1], queue[i+2]); 555 556 queue.splice(0, throttleFlushCount*3); 557 558 if (queue.length) 559 { 560 var self = this; 561 this.throttleTimeout = 562 this.setTimeout(function f() { self.flushThrottleQueue(); }, throttleInterval); 563 } 564 else 565 { 566 this.throttleTimeout = 0; 567 } 568 }, 569 570 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 571 // Event Listeners 572 573 addEventListener: function(parent, eventId, listener, capturing) 574 { 575 if (!this.listeners) 576 this.listeners = []; 577 578 for (var i=0; i<this.listeners.length; i++) 579 { 580 var l = this.listeners[i]; 581 if (l.parent == parent && l.eventId == eventId && l.listener == listener && 582 l.capturing == capturing) 583 { 584 // Listener already registered! 585 return; 586 } 587 } 588 589 parent.addEventListener(eventId, listener, capturing); 590 591 this.listeners.push({ 592 parent: parent, 593 eventId: eventId, 594 listener: listener, 595 capturing: capturing, 596 }); 597 }, 598 599 removeEventListener: function(parent, eventId, listener, capturing) 600 { 601 parent.removeEventListener(eventId, listener, capturing); 602 603 if (!this.listeners) 604 this.listeners = []; 605 606 for (var i=0; i<this.listeners.length; i++) 607 { 608 var l = this.listeners[i]; 609 if (l.parent == parent && l.eventId == eventId && l.listener == listener && 610 l.capturing == capturing) 611 { 612 this.listeners.splice(i, 1); 613 break; 614 } 615 } 616 }, 617 618 /** 619 * Executed by the framework when the context is about to be destroyed. 620 */ 621 unregisterAllListeners: function() 622 { 623 if (!this.listeners) 624 return; 625 626 for (var i=0; i<this.listeners.length; i++) 627 { 628 var l = this.listeners[i]; 629 630 try 631 { 632 l.parent.removeEventListener(l.eventId, l.listener, l.capturing); 633 } 634 catch (e) 635 { 636 if (FBTrace.DBG_ERRORS) 637 { 638 FBTrace.sysout("tabContext.unregisterAllListeners; (" + l.eventId + 639 ") " + e, e); 640 } 641 } 642 } 643 644 this.listeners = null; 645 } 646 }; 647 648 // ********************************************************************************************* // 649 // Registration 650 651 return Firebug.TabContext; 652 653 // ********************************************************************************************* // 654 }); 655