1 /* See license.txt for terms of usage */ 2 3 FBL.ns(function() { with (FBL) { 4 5 // ************************************************************************************************ 6 // Constants 7 8 const throttleTimeWindow = 200; 9 const throttleMessageLimit = 30; 10 const throttleInterval = 30; 11 const throttleFlushCount = 20; 12 13 const refreshDelay = 300; 14 15 // ************************************************************************************************ 16 17 Firebug.TabContext = function(win, browser, chrome, persistedState) 18 { 19 this.window = win; 20 this.browser = browser; 21 this.persistedState = persistedState; 22 23 browser.__defineGetter__("chrome", function() { return Firebug.chrome; }); // backward compat 24 25 this.name = normalizeURL(this.getWindowLocation().toString()); 26 27 this.windows = []; 28 this.panelMap = {}; 29 this.sidePanelNames = {}; 30 this.sourceFileMap = {}; 31 32 // New nsITraceableChannel interface (introduced in FF3.0.4) makes possible 33 // to re-implement source-cache so, it solves the double-load problem. 34 // Anyway, keep the previous cache implementation for backward compatibility 35 // (with Firefox 3.0.3 and lower) 36 if (Components.interfaces.nsITraceableChannel) 37 this.sourceCache = new Firebug.TabCache(this); 38 else 39 this.sourceCache = new Firebug.SourceCache(this); 40 41 this.global = win; // used by chromebug 42 }; 43 44 Firebug.TabContext.prototype = 45 { 46 getWindowLocation: function() 47 { 48 return safeGetWindowLocation(this.window); 49 }, 50 51 getTitle: function() 52 { 53 if (this.window && this.window.document) 54 return this.window.document.title; 55 else 56 return ""; 57 }, 58 59 getName: function() 60 { 61 if (!this.name || this.name === "about:blank") 62 { 63 var url = this.getWindowLocation().toString(); 64 if (isDataURL(url)) 65 { 66 var props = splitDataURL(url); 67 if (props.fileName) 68 this.name = "data url from "+props.fileName; 69 } 70 else 71 { 72 this.name = normalizeURL(url); 73 } 74 } 75 return this.name; 76 }, 77 78 getGlobalScope: function() 79 { 80 return this.window; 81 }, 82 83 addSourceFile: function(sourceFile) 84 { 85 this.sourceFileMap[sourceFile.href] = sourceFile; 86 sourceFile.context = this; 87 88 Firebug.onSourceFileCreated(this, sourceFile); 89 }, 90 91 removeSourceFile: function(sourceFile) 92 { 93 delete this.sourceFileMap[sourceFile.href]; 94 delete sourceFile.context; 95 96 // ?? Firebug.onSourceFileDestroyed(this, sourceFile); 97 }, 98 // *************************************************************************** 99 get chrome() // backward compat 100 { 101 return Firebug.chrome; 102 }, 103 104 reattach: function(oldChrome, newChrome) 105 { 106 for (var panelName in this.panelMap) 107 { 108 var panel = this.panelMap[panelName]; 109 panel.detach(oldChrome, newChrome); 110 panel.invalid = true;// this will cause reattach on next use 111 112 var panelNode = panel.panelNode; // delete panel content 113 if (panelNode && panelNode.parentNode) 114 panelNode.parentNode.removeChild(panelNode); 115 } 116 }, 117 118 destroy: function(state) 119 { 120 if (this.timeouts) 121 { 122 for (var timeout in this.timeouts) 123 clearTimeout(timeout); 124 } 125 126 if (this.intervals) 127 { 128 for (var timeout in this.intervals) 129 clearInterval(timeout); 130 } 131 132 if (this.throttleTimeout) 133 clearTimeout(this.throttleTimeout); 134 135 state.panelState = {}; 136 137 // Inherit panelStates that have not been restored yet 138 if (this.persistedState) 139 { 140 for (var panelName in this.persistedState.panelState) 141 state.panelState[panelName] = this.persistedState.panelState[panelName]; 142 } 143 144 for (var panelName in this.panelMap) 145 { 146 var panel = this.panelMap[panelName]; 147 148 // Create an object to persist state, re-using old one if it was never restored 149 var panelState = panelName in state.panelState ? state.panelState[panelName] : {}; 150 state.panelState[panelName] = panelState; 151 152 try 153 { 154 // Destroy the panel and allow it to persist extra info to the state object 155 panel.destroy(panelState); 156 } 157 catch(exc) 158 { 159 if (FBTrace.DBG_ERRORS) 160 FBTrace.sysout("tabContext.destroy FAILS "+exc, exc); 161 // the destroy failed, don't keep the bad state 162 delete state.panelState[panelName]; 163 } 164 165 // Remove the panel node from the DOM 166 var panelNode = panel.panelNode; // delete panel content 167 if (panelNode && panelNode.parentNode) 168 panelNode.parentNode.removeChild(panelNode); 169 } 170 171 if (FBTrace.DBG_INITIALIZE) 172 FBTrace.sysout("tabContext.destroy "+this.getName()+" set state ", state); 173 174 // Release all members just to be safe in case somebody leaks this context 175 for (var name in this) 176 delete this[name]; 177 }, 178 179 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 180 181 addPanelType: function(url, title, parentPanel) 182 { 183 url = absoluteURL(url, this.window.location.href); 184 if (!url) 185 { 186 // XXXjoe Need some kind of notification to console that URL is invalid 187 throw("addPanelType: url is invalid!"); 188 return; 189 } 190 191 if (!this.panelTypes) 192 { 193 this.panelTypes = []; 194 this.panelTypeMap = {}; 195 } 196 197 var name = createPanelName(url); 198 while (name in this.panelTypeMap) 199 name += "_"; 200 201 var panelType = createPanelType(name, url, title, parentPanel); 202 203 this.panelTypes.push(panelType); 204 this.panelTypeMap[name] = panelType; 205 206 return panelType; 207 }, 208 209 removePanelType: function(url) 210 { 211 // NYI 212 }, 213 214 getPanel: function(panelName, noCreate) 215 { 216 var panelType = Firebug.getPanelType(panelName); 217 if (!panelType && this.panelTypeMap) 218 panelType = this.panelTypeMap[panelName]; // context local panelType 219 //if (FBTrace.DBG_PANELS) /*@expore*/ 220 // FBTrace.sysout("tabContext.getPanel name="+panelName+" noCreate="+noCreate+" panelType="+(panelType?panelType.prototype.name:"null")+"\n"); /*@expore*/ 221 if (panelType) 222 return this.getPanelByType(panelType, noCreate); 223 }, 224 225 getPanelByType: function(panelType, noCreate) 226 { 227 if (!panelType || !this.panelMap) 228 return null; 229 230 var panelName = panelType.prototype.name; 231 if ( this.panelMap.hasOwnProperty(panelName) ) 232 { 233 var panel = this.panelMap[panelName]; 234 //if (FBTrace.DBG_PANELS) 235 // FBTrace.sysout("tabContext.getPanelByType panel in panelMap, .invalid="+panel.invalid+"\n"); 236 if (panel.invalid) 237 { 238 var doc = this.chrome.getPanelDocument(panelType); 239 panel.reattach(doc); 240 delete panel.invalid; 241 } 242 243 return panel; 244 } 245 else if (!noCreate) 246 { 247 //if (FBTrace.DBG_PANELS) FBTrace.sysout("tabContext.getPanelByType panel NOT in panelMap\n"); 248 var panel = new panelType(); // This is why panels are defined by prototype inheritance 249 var doc = this.chrome.getPanelDocument(panelType); 250 panel.initialize(this, doc); 251 252 return this.panelMap[panel.name] = panel; 253 } 254 }, 255 256 setPanel: function(panelName, panel) // allows a panel from one context to be used in other contexts. 257 { 258 if (panel) 259 this.panelMap[panelName] = panel; 260 else 261 delete this.panelMap[panelName]; 262 }, 263 264 invalidatePanels: function() 265 { 266 if (!this.invalidPanels) 267 this.invalidPanels = {}; 268 269 for (var i = 0; i < arguments.length; ++i) 270 { 271 var panelName = arguments[i]; 272 var panel = this.getPanel(panelName, true); 273 if (panel && !panel.noRefresh) 274 this.invalidPanels[panelName] = 1; 275 } 276 277 if (this.refreshTimeout) 278 { 279 this.clearTimeout(this.refreshTimeout); 280 delete this.refreshTimeout; 281 } 282 283 this.refreshTimeout = this.setTimeout(bindFixed(function() 284 { 285 var invalids = []; 286 287 for (var panelName in this.invalidPanels) 288 { 289 var panel = this.getPanel(panelName, true); 290 if (panel) 291 { 292 if (panel.visible && !panel.editing) 293 panel.refresh(); 294 else 295 panel.needsRefresh = true; 296 297 // If the panel is being edited, we'll keep trying to 298 // refresh it until editing is done 299 if (panel.editing) 300 invalids.push(panelName); 301 } 302 } 303 304 delete this.invalidPanels; 305 delete this.refreshTimeout; 306 307 // Keep looping until every tab is valid 308 if (invalids.length) 309 this.invalidatePanels.apply(this, invalids); 310 }, this), refreshDelay); 311 }, 312 313 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 314 315 setTimeout: function() 316 { 317 if (setTimeout == this.setTimeout) 318 throw new Error("setTimeout recursion"); 319 var timeout = setTimeout.apply(top, arguments); 320 321 if (!this.timeouts) 322 this.timeouts = {}; 323 324 this.timeouts[timeout] = 1; 325 326 return timeout; 327 }, 328 329 clearTimeout: function(timeout) 330 { 331 clearTimeout(timeout); 332 333 if (this.timeouts) 334 delete this.timeouts[timeout]; 335 }, 336 337 setInterval: function() 338 { 339 var timeout = setInterval.apply(top, arguments); 340 341 if (!this.intervals) 342 this.intervals = {}; 343 344 this.intervals[timeout] = 1; 345 346 return timeout; 347 }, 348 349 clearInterval: function(timeout) 350 { 351 clearInterval(timeout); 352 353 if (this.intervals) 354 delete this.intervals[timeout]; 355 }, 356 357 delay: function(message, object) 358 { 359 this.throttle(message, object, null, true); 360 }, 361 362 // queue the call |object.message(arg)| or just delay it if forceDelay 363 throttle: function(message, object, args, forceDelay) 364 { 365 if (!this.throttleInit) 366 { 367 this.throttleBuildup = 0; 368 this.throttleQueue = []; 369 this.throttleTimeout = 0; 370 this.lastMessageTime = 0; 371 this.throttleInit = true; 372 } 373 374 if (!forceDelay) 375 { 376 if (!Firebug.throttleMessages) 377 { 378 message.apply(object, args); 379 return false; 380 } 381 382 // Count how many messages have been logged during the throttle period 383 var logTime = new Date().getTime(); 384 if (logTime - this.lastMessageTime < throttleTimeWindow) 385 ++this.throttleBuildup; 386 else 387 this.throttleBuildup = 0; 388 389 this.lastMessageTime = logTime; 390 391 // If the throttle limit has been passed, enqueue the message to be logged later on a timer, 392 // otherwise just execute it now 393 if (!this.throttleQueue.length && this.throttleBuildup <= throttleMessageLimit) 394 { 395 message.apply(object, args); 396 return false; 397 } 398 } 399 400 this.throttleQueue.push(message, object, args); 401 402 if (this.throttleTimeout) 403 this.clearTimeout(this.throttleTimeout); 404 405 var self = this; 406 this.throttleTimeout = 407 this.setTimeout(function() { self.flushThrottleQueue(); }, throttleInterval); 408 return true; 409 }, 410 411 flushThrottleQueue: function() 412 { 413 var queue = this.throttleQueue; 414 415 if (!queue[0]) 416 FBTrace.sysout("tabContext.flushThrottleQueue no queue[0]", queue); 417 418 var max = throttleFlushCount * 3; 419 if (max > queue.length) 420 max = queue.length; 421 422 for (var i = 0; i < max; i += 3) 423 queue[i].apply(queue[i+1], queue[i+2]); 424 425 queue.splice(0, throttleFlushCount*3); 426 427 if (queue.length) 428 { 429 var self = this; 430 this.throttleTimeout = 431 this.setTimeout(function f() { self.flushThrottleQueue(); }, throttleInterval); 432 } 433 else 434 this.throttleTimeout = 0; 435 } 436 }; 437 438 // ************************************************************************************************ 439 // Local Helpers 440 441 function createPanelType(name, url, title, parentPanel) 442 { 443 var panelType = new Function(""); 444 panelType.prototype = extend(new Firebug.PluginPanel(), 445 { 446 name: name, 447 url: url, 448 title: title ? title : "...", 449 parentPanel: parentPanel 450 }); 451 452 return panelType; 453 } 454 455 function createPanelName(url) 456 { 457 return url.replace(/[:\\\/\s\.\?\=\&\~]/g, "_"); 458 } 459 460 // ************************************************************************************************ 461 462 }}); 463