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