1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/chrome/firefox",
  6     "firebug/firebug",
  7     "firebug/dom/toggleBranch",
  8     "firebug/lib/events",
  9     "firebug/lib/dom",
 10     "firebug/lib/css",
 11     "firebug/js/stackFrame",
 12     "firebug/lib/locale",
 13     "firebug/lib/string",
 14     "firebug/dom/domPanel",     // Firebug.DOMBasePanel, Firebug.DOMPanel.DirTable
 15 ],
 16 function(Obj, Firefox, Firebug, ToggleBranch, Events, Dom, Css, StackFrame, Locale, Str) {
 17 
 18 // ********************************************************************************************* //
 19 // Watch Panel
 20 
 21 Firebug.WatchPanel = function()
 22 {
 23 }
 24 
 25 /**
 26  * Represents the Watch side panel available in the Script panel.
 27  */
 28 Firebug.WatchPanel.prototype = Obj.extend(Firebug.DOMBasePanel.prototype,
 29 /** @lends Firebug.WatchPanel */
 30 {
 31     tag: Firebug.DOMPanel.DirTable.watchTag,
 32 
 33     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 34     // extends Panel
 35 
 36     name: "watches",
 37     order: 0,
 38     parentPanel: "script",
 39     enableA11y: true,
 40     deriveA11yFrom: "console",
 41 
 42     initialize: function()
 43     {
 44         this.onMouseDown = Obj.bind(this.onMouseDown, this);
 45         this.onMouseOver = Obj.bind(this.onMouseOver, this);
 46         this.onMouseOut = Obj.bind(this.onMouseOut, this);
 47 
 48         Firebug.registerUIListener(this);
 49 
 50         Firebug.DOMBasePanel.prototype.initialize.apply(this, arguments);
 51     },
 52 
 53     destroy: function(state)
 54     {
 55         state.watches = this.watches;
 56 
 57         Firebug.unregisterUIListener(this);
 58 
 59         Firebug.DOMBasePanel.prototype.destroy.apply(this, arguments);
 60     },
 61 
 62     show: function(state)
 63     {
 64         if (state && state.watches)
 65             this.watches = state.watches;
 66     },
 67 
 68     initializeNode: function(oldPanelNode)
 69     {
 70         Events.addEventListener(this.panelNode, "mousedown", this.onMouseDown, false);
 71         Events.addEventListener(this.panelNode, "mouseover", this.onMouseOver, false);
 72         Events.addEventListener(this.panelNode, "mouseout", this.onMouseOut, false);
 73 
 74         Firebug.DOMBasePanel.prototype.initializeNode.apply(this, arguments);
 75     },
 76 
 77     destroyNode: function()
 78     {
 79         Events.removeEventListener(this.panelNode, "mousedown", this.onMouseDown, false);
 80         Events.removeEventListener(this.panelNode, "mouseover", this.onMouseOver, false);
 81         Events.removeEventListener(this.panelNode, "mouseout", this.onMouseOut, false);
 82 
 83         Firebug.DOMBasePanel.prototype.destroyNode.apply(this, arguments);
 84     },
 85 
 86     refresh: function()
 87     {
 88         this.rebuild(true);
 89     },
 90 
 91     updateSelection: function(frame)
 92     {
 93         // this method is called while the debugger has halted JS,
 94         // so failures don't show up in FBS_ERRORS
 95         try
 96         {
 97             this.doUpdateSelection(frame);
 98         }
 99         catch (exc)
100         {
101             if (FBTrace.DBG_ERRORS && FBTrace.DBG_STACK)
102                 FBTrace.sysout("updateSelection FAILS " + exc, exc);
103         }
104     },
105 
106     doUpdateSelection: function(frame)
107     {
108         if (FBTrace.DBG_STACK)
109             FBTrace.sysout("dom watch panel updateSelection frame " + frame, frame);
110 
111         Events.dispatch(this.fbListeners, "onBeforeDomUpdateSelection", [this]);
112 
113         var newFrame = frame && ("signature" in frame) &&
114             (frame.signature() != this.frameSignature);
115 
116         if (newFrame)
117         {
118             this.toggles = new ToggleBranch.ToggleBranch();
119             this.frameSignature = frame.signature();
120         }
121 
122         var scopes;
123         if (frame instanceof StackFrame.StackFrame)
124             scopes = frame.getScopes(Firebug.viewChrome);
125         else
126             scopes = [this.context.getGlobalScope()];
127 
128         if (FBTrace.DBG_STACK)
129             FBTrace.sysout("dom watch frame isStackFrame " +
130                 (frame instanceof StackFrame.StackFrame) +
131                 " updateSelection scopes " + scopes.length, scopes);
132 
133         var members = [];
134 
135         if (this.watches)
136         {
137             for (var i = 0; i < this.watches.length; ++i)
138             {
139                 var expr = this.watches[i];
140                 var value = null;
141 
142                 Firebug.CommandLine.evaluate(expr, this.context, null, this.context.getGlobalScope(),
143                     function success(result, context)
144                     {
145                         value = result;
146                     },
147                     function failed(result, context)
148                     {
149                         var exc = result;
150                         value = new FirebugReps.ErrorCopy(exc+"");
151                     }
152                 );
153 
154                 this.addMember(scopes[0], "watch", members, expr, value, 0);
155 
156                 if (FBTrace.DBG_DOM)
157                     FBTrace.sysout("watch.updateSelection " + expr + " = " + value,
158                         {expr: expr, value: value, members: members})
159             }
160         }
161 
162         if (frame && frame instanceof StackFrame.StackFrame)
163         {
164             var thisVar = frame.getThisValue();
165             if (thisVar)
166                 this.addMember(scopes[0], "user", members, "this", thisVar, 0);
167 
168             // locals, pre-expanded
169             members.push.apply(members, this.getMembers(scopes[0], 0, this.context));
170 
171             for (var i=1; i<scopes.length; i++)
172                 this.addMember(scopes[i], "scopes", members, scopes[i].toString(), scopes[i], 0);
173         }
174 
175         this.expandMembers(members, this.toggles, 0, 0, this.context);
176         this.showMembers(members, false);
177 
178         if (FBTrace.DBG_STACK)
179             FBTrace.sysout("dom watch panel updateSelection members " + members.length, members);
180     },
181 
182     rebuild: function()
183     {
184         if (FBTrace.DBG_WATCH)
185             FBTrace.sysout("Firebug.WatchPanel.rebuild", this.selection);
186 
187         this.updateSelection(this.selection);
188     },
189 
190     showEmptyMembers: function()
191     {
192         var domTable = this.tag.replace({domPanel: this, toggles: new ToggleBranch.ToggleBranch()},
193             this.panelNode);
194 
195         // The direction needs to be adjusted according to the direction
196         // of the user agent. See issue 5073.
197         // TODO: Set the direction at the <body> to allow correct formatting of all relevant parts.
198         // This requires more adjustments related for rtl user agents.
199         var mainFrame = Firefox.getElementById("fbMainFrame");
200         var cs = mainFrame.ownerDocument.defaultView.getComputedStyle(mainFrame);
201         var watchRow = domTable.getElementsByClassName("watchNewRow").item(0);
202         watchRow.style.direction = cs.direction;
203     },
204 
205     addWatch: function(expression)
206     {
207         expression = Str.trim(expression);
208 
209         if (FBTrace.DBG_WATCH)
210             FBTrace.sysout("Firebug.WatchPanel.addWatch; expression: "+expression);
211 
212         if (!this.watches)
213             this.watches = [];
214 
215         for (var i=0; i<this.watches.length; i++)
216         {
217             if (expression == this.watches[i])
218                 return;
219         }
220 
221         this.watches.splice(0, 0, expression);
222         this.rebuild(true);
223     },
224 
225     removeWatch: function(expression)
226     {
227         if (FBTrace.DBG_WATCH)
228             FBTrace.sysout("Firebug.WatchPanel.removeWatch; expression: " + expression);
229 
230         if (!this.watches)
231             return;
232 
233         var index = this.watches.indexOf(expression);
234         if (index != -1)
235             this.watches.splice(index, 1);
236     },
237 
238     editNewWatch: function(value)
239     {
240         if (FBTrace.DBG_WATCH)
241             FBTrace.sysout("Firebug.WatchPanel.editNewWatch; value: " + value);
242 
243         var watchNewRow = this.panelNode.getElementsByClassName("watchNewRow").item(0);
244         if (watchNewRow)
245             this.editProperty(watchNewRow, value);
246     },
247 
248     setWatchValue: function(row, value)
249     {
250         if (FBTrace.DBG_WATCH)
251             FBTrace.sysout("Firebug.WatchPanel.setWatchValue", {row: row, value: value});
252 
253         var rowIndex = getWatchRowIndex(row);
254         this.watches[rowIndex] = value;
255         this.rebuild(true);
256     },
257 
258     deleteWatch: function(row)
259     {
260         if (FBTrace.DBG_WATCH)
261             FBTrace.sysout("Firebug.WatchPanel.deleteWatch", row);
262 
263         var rowIndex = getWatchRowIndex(row);
264         this.watches.splice(rowIndex, 1);
265         this.rebuild(true);
266 
267         this.context.setTimeout(Obj.bindFixed(function()
268         {
269             this.showToolbox(null);
270         }, this));
271     },
272 
273     // deletes all the watches
274     deleteAllWatches: function()
275     {
276         if (FBTrace.DBG_WATCH)
277             FBTrace.sysout("Firebug.WatchPanel.deleteAllWatches");
278         this.watches = [];
279         this.rebuild(true);
280         this.context.setTimeout(Obj.bindFixed(function()
281         {
282             this.showToolbox(null);
283         }, this));
284     },
285 
286     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
287 
288     showToolbox: function(row)
289     {
290         var toolbox = this.getToolbox();
291         if (row)
292         {
293             if (Css.hasClass(row, "editing"))
294                 return;
295 
296             toolbox.watchRow = row;
297 
298             var offset = Dom.getClientOffset(row);
299             toolbox.style.top = offset.y + "px";
300             this.panelNode.appendChild(toolbox);
301         }
302         else
303         {
304             delete toolbox.watchRow;
305 
306             if (toolbox.parentNode)
307                 toolbox.parentNode.removeChild(toolbox);
308         }
309     },
310 
311     getToolbox: function()
312     {
313         if (!this.toolbox)
314         {
315             this.toolbox = Firebug.DOMBasePanel.ToolboxPlate.tag.replace(
316                 {domPanel: this}, this.document);
317         }
318 
319         return this.toolbox;
320     },
321 
322     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
323 
324     onMouseDown: function(event)
325     {
326         var watchNewRow = Dom.getAncestorByClass(event.target, "watchNewRow");
327         if (watchNewRow)
328         {
329             this.editProperty(watchNewRow);
330             Events.cancelEvent(event);
331         }
332     },
333 
334     onMouseOver: function(event)
335     {
336         var watchRow = Dom.getAncestorByClass(event.target, "watchRow");
337         if (watchRow)
338             this.showToolbox(watchRow);
339     },
340 
341     onMouseOut: function(event)
342     {
343         if (Dom.isAncestor(event.relatedTarget, this.getToolbox()))
344             return;
345 
346         var watchRow = Dom.getAncestorByClass(event.relatedTarget, "watchRow");
347         if (!watchRow)
348             this.showToolbox(null);
349     },
350 
351     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
352     // Context Menu
353 
354     /**
355      * Creates "Add Watch" menu item within DOM and Watch panel context menus.
356      */
357     onContextMenu: function(items, object, target, context, panel, popup)
358     {
359         // Ignore events from other contexts.
360         if (this.context != context)
361             return;
362 
363         if (panel.name != "dom" && panel.name != "watches")
364             return;
365 
366         var row = Dom.getAncestorByClass(target, "memberRow");
367         if (!row) 
368             return;
369 
370         var path = this.getPropertyPath(row);
371         if (!path || !path.length)
372             return;
373 
374 
375         // Ignore top level variables in the Watch panel.
376         if (panel.name == "watches" && path.length == 1)
377             return;
378 
379         items.push({
380            id: "fbAddWatch",
381            label: "AddWatch",
382            tooltiptext: "watch.tip.Add_Watch",
383            command: Obj.bindFixed(this.addWatch, this, path.join(""))
384         });
385     },
386 
387     getContextMenuItems: function(object, target)
388     {
389         var items = Firebug.DOMBasePanel.prototype.getContextMenuItems.apply(this, arguments);
390 
391         if (!this.watches || this.watches.length == 0)
392             return items;
393 
394         // find the index of "DeletePropery" in the items:
395         var deleteWatchIndex = items.map(function(item)
396         {
397             return item.id;
398         }).indexOf("DeleteProperty");
399 
400         // if DeleteWatch was found, we insert DeleteAllWatches after it
401         // otherwise, we insert the item at the beginning of the menu
402         var deleteAllWatchesIndex = (deleteWatchIndex >= 0) ? deleteWatchIndex + 1 : 0;
403 
404         if (FBTrace.DBG_WATCH)
405             FBTrace.sysout("insert DeleteAllWatches at: "+ deleteAllWatchesIndex);
406 
407         // insert DeleteAllWatches after DeleteWatch
408         items.splice(deleteAllWatchesIndex, 0, {
409             id: "fbDeleteAllWatches",
410             label: "DeleteAllWatches",
411             tooltiptext: "watch.tip.Delete_All_Watches",
412             command: Obj.bindFixed(this.deleteAllWatches, this)
413         });
414 
415         return items;
416     }
417 });
418 
419 // ********************************************************************************************* //
420 // Local Helpers
421 
422 function getWatchRowIndex(row)
423 {
424     var index = -1;
425     for (; row; row = row.previousSibling)
426     {
427         if (Css.hasClass(row, "watchRow"))
428             ++index;
429     }
430     return index;
431 }
432 
433 // ********************************************************************************************* //
434 // Registration
435 
436 Firebug.registerPanel(Firebug.WatchPanel);
437 
438 return Firebug.WatchPanel;
439 
440 // ********************************************************************************************* //
441 });
442