1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 // ************************************************************************************************
  6 // Constants
  7 
  8 // ************************************************************************************************
  9 
 10 Firebug.Breakpoint = extend(Firebug.Module,
 11 {
 12     dispatchName: "breakpoints",
 13 
 14     toggleBreakOnNext: function(panel)
 15     {
 16         var breakable = Firebug.chrome.getGlobalAttribute("cmd_breakOnNext", "breakable");
 17 
 18         if (FBTrace.DBG_BP)
 19             FBTrace.sysout("breakpoint.toggleBreakOnNext; currentBreakable "+breakable+
 20                 " in " + panel.context.getName());
 21 
 22         // Toggle button's state.
 23         breakable = (breakable == "true" ? "false" : "true");
 24         Firebug.chrome.setGlobalAttribute("cmd_breakOnNext", "breakable", breakable);
 25 
 26         // Call the current panel's logic related to break-on-next.
 27         // If breakable == "true" the feature is currently disabled.
 28         var enabled = (breakable == "true" ? false : true);
 29         panel.breakOnNext(enabled);
 30 
 31         // Make sure the correct tooltip (coming from the current panel) is used.
 32         this.updateBreakOnNextTooltips(panel);
 33 
 34         // Light up the tab whenever break on next is selected
 35         this.updatePanelTab(panel, enabled);
 36 
 37         return enabled;
 38     },
 39 
 40     showPanel: function(browser, panel)
 41     {
 42         if(!panel)  // there is no selectedPanel?
 43             return;
 44 
 45         var breakButton = Firebug.chrome.$("fbBreakOnNextButton");
 46         if (panel.name)
 47             breakButton.setAttribute("panelName", panel.name);
 48 
 49         breakButton.removeAttribute("type");
 50 
 51         // Disable break-on-next if it isn't supported by the current panel.
 52         if (!panel.breakable)
 53         {
 54             Firebug.chrome.setGlobalAttribute("cmd_breakOnNext", "breakable", "disabled");
 55             Firebug.chrome.setGlobalAttribute("cmd_breakOnNext", "tooltiptext", "");
 56             return;
 57         }
 58 
 59         // Set the tooltips and update break-on-next button's state.
 60         var shouldBreak = panel.shouldBreakOnNext();
 61         this.updateBreakOnNextState(panel, shouldBreak);
 62         this.updateBreakOnNextTooltips(panel);
 63         this.updatePanelTab(panel, shouldBreak);
 64 
 65         var menuItems = panel.getBreakOnMenuItems();
 66         if (!menuItems || !menuItems.length)
 67             return;
 68 
 69         breakButton.setAttribute("type", "menu-button");
 70 
 71         var menuPopup = Firebug.chrome.$("fbBreakOnNextOptions");
 72         eraseNode(menuPopup);
 73 
 74         for (var i=0; i<menuItems.length; ++i)
 75             FBL.createMenuItem(menuPopup, menuItems[i]);
 76     },
 77 
 78     updateBreakOnNextTooltips: function(panel)
 79     {
 80         var breakable = Firebug.chrome.getGlobalAttribute("cmd_breakOnNext", "breakable");
 81 
 82         // Get proper tooltip for the break-on-next button from the current panel.
 83         // If breakable is set to "false" the feature is already activated (throbbing).
 84         var armed = (breakable == "false");
 85         var tooltip = panel.getBreakOnNextTooltip(armed);
 86         if (!tooltip)
 87             tooltip = "";
 88 
 89         Firebug.chrome.setGlobalAttribute("cmd_breakOnNext", "tooltiptext", tooltip);
 90     },
 91 
 92     updateBreakOnNextState: function(panel, armed)
 93     {
 94         // If the panel should break at the next chance, set the button to not breakable,
 95         // which means already active (throbbing).
 96         var breakable = armed ? "false" : "true";
 97         Firebug.chrome.setGlobalAttribute("cmd_breakOnNext", "breakable", breakable);
 98     },
 99 
100     updatePanelTab: function(panel, armed)
101     {
102         if (!panel)
103             return;
104 
105         var panelBar = Firebug.chrome.$("fbPanelBar1");
106         var tab = panelBar.getTab(panel.name);
107         if (tab)
108             tab.setAttribute("breakOnNextArmed", armed ? "true" : "false");
109     },
110 
111     breakNow: function(panel)
112     {
113         this.updatePanelTab(panel, false);
114         Firebug.Debugger.breakNow();
115     }
116 });
117 
118 // ************************************************************************************************
119 
120 Firebug.Breakpoint.BreakpointListRep = domplate(Firebug.Rep,
121 {
122     tag:
123         DIV({onclick: "$onClick", role : "list"},
124             FOR("group", "$groups",
125                 DIV({"class": "breakpointBlock breakpointBlock-$group.name", role: "listitem"},
126                     H1({"class": "breakpointHeader groupHeader"},
127                         "$group.title"
128                     ),
129                     DIV({"class": "breakpointsGroupListBox", role: "listbox"},
130                         FOR("bp", "$group.breakpoints",
131                             TAG("$bp|getBreakpointRep", {bp: "$bp"})
132                         )
133                     )
134                 )
135             )
136         ),
137 
138     getBreakpointRep: function(bp)
139     {
140         var rep = Firebug.getRep(bp);
141         return rep.tag;
142     },
143 
144     onClick: function(event)
145     {
146         var panel = Firebug.getElementPanel(event.target);
147 
148         if (getAncestorByClass(event.target, "breakpointCheckbox"))
149         {
150             var node = event.target.parentNode.getElementsByClassName("objectLink-sourceLink").item(0);
151             if (!node)
152                 return;
153 
154             var sourceLink = node.repObject;
155 
156             panel.noRefresh = true;
157             if (event.target.checked)
158                 fbs.enableBreakpoint(sourceLink.href, sourceLink.line);
159             else
160                 fbs.disableBreakpoint(sourceLink.href, sourceLink.line);
161             panel.noRefresh = false;
162         }
163         else if (getAncestorByClass(event.target, "closeButton"))
164         {
165             var sourceLink =
166                 event.target.parentNode.getElementsByClassName("objectLink-sourceLink").item(0).repObject;
167 
168             panel.noRefresh = true;
169 
170             var head = getAncestorByClass(event.target, "breakpointBlock");
171             var groupName = getClassValue(head, "breakpointBlock");
172             if (groupName == "breakpoints")
173                 fbs.clearBreakpoint(sourceLink.href, sourceLink.line);
174             else if (groupName == "errorBreakpoints")
175                 fbs.clearErrorBreakpoint(sourceLink.href, sourceLink.line);
176             else if (groupName == "monitors")
177             {
178                 fbs.unmonitor(sourceLink.href, sourceLink.line)
179             }
180 
181             var row = getAncestorByClass(event.target, "breakpointRow");
182             panel.removeRow(row);
183 
184             panel.noRefresh = false;
185         }
186     }
187 });
188 
189 // ************************************************************************************************
190 
191 Firebug.Breakpoint.BreakpointRep = domplate(Firebug.Rep,
192 {
193     tag:
194         DIV({"class": "breakpointRow focusRow", role: "option", "aria-checked": "$bp.checked"},
195             DIV({"class": "breakpointBlockHead"},
196                 INPUT({"class": "breakpointCheckbox", type: "checkbox",
197                     _checked: "$bp.checked", tabindex : '-1'}),
198                 SPAN({"class": "breakpointName"}, "$bp.name"),
199                 TAG(FirebugReps.SourceLink.tag, {object: "$bp|getSourceLink"}),
200                 IMG({"class": "closeButton", src: "blank.gif"})
201             ),
202             DIV({"class": "breakpointCode"}, "$bp.sourceLine")
203         ),
204 
205     getSourceLink: function(bp)
206     {
207         return new SourceLink(bp.href, bp.lineNumber, "js");
208     },
209 
210     supportsObject: function(bp)
211     {
212         return (bp instanceof Firebug.Debugger.Breakpoint);
213     }
214 });
215 
216 // ************************************************************************************************
217 
218 Firebug.Breakpoint.BreakpointsPanel = function() {}
219 
220 Firebug.Breakpoint.BreakpointsPanel.prototype = extend(Firebug.Panel,
221 {
222     name: "breakpoints",
223     parentPanel: "script",
224     order: 2,
225 
226     initialize: function()
227     {
228         Firebug.Panel.initialize.apply(this, arguments);
229     },
230 
231     destroy: function(state)
232     {
233         Firebug.Panel.destroy.apply(this, arguments);
234     },
235 
236     initializeNode : function(oldPanelNode)
237     {
238         dispatch([Firebug.A11yModel], 'onInitializeNode', [this, 'console']);
239     },
240 
241     destroyNode : function()
242     {
243         dispatch([Firebug.A11yModel], 'onDestroyNode', [this, 'console']);
244     },
245 
246     show: function(state)
247     {
248         this.refresh();
249     },
250 
251     refresh: function()
252     {
253         if (this.noRefresh)
254             return;
255 
256         if (!Firebug.Debugger.isAlwaysEnabled(this.context))
257             this.updateScriptFiles(this.context);
258 
259         var extracted = this.extractBreakpoints(this.context, breakpoints, errorBreakpoints, monitors);
260 
261         var breakpoints = extracted.breakpoints;
262         var errorBreakpoints = extracted.errorBreakpoints;
263         var monitors = extracted.monitors;
264 
265         if (FBTrace.DBG_BP)
266             FBTrace.sysout("debugger.breakpoints.refresh extracted " +
267                 breakpoints.length+errorBreakpoints.length+monitors.length,
268                 [breakpoints, errorBreakpoints, monitors]);
269 
270         function sortBreakpoints(a, b)
271         {
272             if (a.href == b.href)
273                 return a.lineNumber < b.lineNumber ? -1 : 1;
274             else
275                 return a.href < b.href ? -1 : 1;
276         }
277 
278         breakpoints.sort(sortBreakpoints);
279         errorBreakpoints.sort(sortBreakpoints);
280         monitors.sort(sortBreakpoints);
281 
282         if (FBTrace.DBG_BP)
283             FBTrace.sysout("debugger.breakpoints.refresh sorted "+breakpoints.length+
284                 errorBreakpoints.length+monitors.length, [breakpoints, errorBreakpoints, monitors]);
285 
286         var groups = [];
287 
288         if (breakpoints.length)
289             groups.push({name: "breakpoints", title: $STR("Breakpoints"),
290                 breakpoints: breakpoints});
291         if (errorBreakpoints.length)
292             groups.push({name: "errorBreakpoints", title: $STR("ErrorBreakpoints"),
293                 breakpoints: errorBreakpoints});
294         if (monitors.length)
295             groups.push({name: "monitors", title: $STR("LoggedFunctions"),
296                 breakpoints: monitors});
297 
298         dispatch(Firebug.Debugger.fbListeners, "getBreakpoints", [this.context, groups]);
299 
300         if (groups.length)
301             Firebug.Breakpoint.BreakpointListRep.tag.replace({groups: groups}, this.panelNode);
302         else
303             FirebugReps.Warning.tag.replace({object: "NoBreakpointsWarning"}, this.panelNode);
304 
305         if (FBTrace.DBG_BP)
306             FBTrace.sysout("debugger.breakpoints.refresh "+breakpoints.length+
307                 errorBreakpoints.length+monitors.length, [breakpoints, errorBreakpoints, monitors]);
308 
309         dispatch([Firebug.A11yModel], 'onBreakRowsRefreshed', [this, this.panelNode]);
310     },
311 
312     extractBreakpoints: function(context, breakpoints, errorBreakpoints, monitors)
313     {
314         var breakpoints = [];
315         var errorBreakpoints = [];
316         var monitors = [];
317 
318         var renamer = new SourceFileRenamer(context);
319         var self = this;
320         var Breakpoint = Firebug.Debugger.Breakpoint;
321 
322         for (var url in context.sourceFileMap)
323         {
324             fbs.enumerateBreakpoints(url, {call: function(url, line, props, script)
325             {
326                 if (FBTrace.DBG_BP) FBTrace.sysout("debugger.extractBreakpoints type: "+props.type, props);
327                 if (renamer.checkForRename(url, line, props)) // some url in this sourceFileMap has changed, we'll be back.
328                     return;
329 
330                 if (script)  // then this is a current (not future) breakpoint
331                 {
332                     var analyzer = Firebug.SourceFile.getScriptAnalyzer(context, script);
333                     if (FBTrace.DBG_BP) FBTrace.sysout("debugger.refresh enumerateBreakpoints for script="+script.tag+(analyzer?" has analyzer":" no analyzer")+"\n");
334                     if (analyzer)
335                         var name = analyzer.getFunctionDescription(script, context).name;
336                     else
337                         var name = FBL.guessFunctionName(url, 1, context);
338                     var isFuture = false;
339                 }
340                 else
341                 {
342                     if (FBTrace.DBG_BP) FBTrace.sysout("debugger.refresh enumerateBreakpoints future for url@line="+url+"@"+line+"\n");
343                     var isFuture = true;
344                 }
345 
346                 var source = context.sourceCache.getLine(url, line);
347                 breakpoints.push(new Breakpoint(name, url, line, !props.disabled, source, isFuture));
348             }});
349 
350             fbs.enumerateErrorBreakpoints(url, {call: function(url, line, props)
351             {
352                 if (renamer.checkForRename(url, line, props)) // some url in this sourceFileMap has changed, we'll be back.
353                     return;
354 
355                 var name = Firebug.SourceFile.guessEnclosingFunctionName(url, line, context);
356                 var source = context.sourceCache.getLine(url, line);
357                 errorBreakpoints.push(new Breakpoint(name, url, line, true, source));
358             }});
359 
360             fbs.enumerateMonitors(url, {call: function(url, line, props)
361             {
362                 if (renamer.checkForRename(url, line, props)) // some url in this sourceFileMap has changed, we'll be back.
363                     return;
364 
365                 var name = Firebug.SourceFile.guessEnclosingFunctionName(url, line, context);
366                 monitors.push(new Breakpoint(name, url, line, true, ""));
367             }});
368         }
369 
370         var result = null;
371 
372         if (renamer.needToRename(context))
373             result = this.extractBreakpoints(context); // since we renamed some sourceFiles we need to refresh the breakpoints again.
374         else
375             result = { breakpoints: breakpoints, errorBreakpoints: errorBreakpoints, monitors: monitors };
376 
377         // even if we did not rename, some bp may be dynamic
378         if (FBTrace.DBG_SOURCEFILES)
379             FBTrace.sysout("debugger.extractBreakpoints context.dynamicURLhasBP: "+context.dynamicURLhasBP, result);
380 
381         return result;
382     },
383 
384     getOptionsMenuItems: function()
385     {
386         var items = [];
387 
388         var context = this.context;
389         if (!Firebug.Debugger.isAlwaysEnabled(context))
390             this.updateScriptFiles(context);
391 
392         var bpCount = 0, disabledCount = 0;
393         var checkBoxes = this.panelNode.getElementsByClassName("breakpointCheckbox");
394         for (var i=0; i<checkBoxes.length; i++)
395         {
396             ++bpCount;
397             if (!checkBoxes[i].checked)
398                 ++disabledCount;
399         }
400 
401         if (disabledCount)
402         {
403             items.push(
404                 {label: "EnableAllBreakpoints",
405                     command: bindFixed(this.enableAllBreakpoints, this, context, true) }
406             );
407         }
408         if (bpCount && disabledCount != bpCount)
409         {
410             items.push(
411                 {label: "DisableAllBreakpoints",
412                     command: bindFixed(this.enableAllBreakpoints, this, context, false) }
413             );
414         }
415 
416         items.push(
417             "-",
418             {label: "ClearAllBreakpoints", disabled: !bpCount,
419                 command: bindFixed(this.clearAllBreakpoints, this, context) }
420         );
421 
422         return items;
423     },
424 
425     enableAllBreakpoints: function(context, status)
426     {
427         var checkBoxes = this.panelNode.getElementsByClassName("breakpointCheckbox");
428         for (var i=0; i<checkBoxes.length; i++)
429         {
430             var box = checkBoxes[i];
431             if (box.checked != status)
432                 this.click(box);
433         }
434     },
435 
436     clearAllBreakpoints: function(context)
437     {
438         this.noRefresh = true;
439 
440         var buttons = this.panelNode.getElementsByClassName("closeButton");
441         for (var i=0; i<buttons.length; i++)
442             this.click(buttons[i]);
443 
444         this.noRefresh = false;
445         this.refresh();
446     },
447 
448     click: function(node)
449     {
450         var doc = node.ownerDocument, event = doc.createEvent("MouseEvents");
451         event.initMouseEvent("click", true, true, doc.defaultView, 0, 0, 0, 0, 0,
452             false, false, false, false, 0, null);
453         return node.dispatchEvent(event);
454     },
455 
456     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
457 
458     removeRow: function(row)
459     {
460         row.parentNode.removeChild(row);
461 
462         var bpCount = countBreakpoints(this.context);
463         if (!bpCount)
464             this.refresh();
465     },
466 });
467 
468 // ************************************************************************************************
469 
470 function countBreakpoints(context)
471 {
472     var count = 0;
473     for (var url in context.sourceFileMap)
474     {
475         fbs.enumerateBreakpoints(url, {call: function(url, lineNo)
476         {
477             ++count;
478         }});
479     }
480     return count;
481 }
482 
483 // ************************************************************************************************
484 
485 Firebug.Breakpoint.BreakpointGroup = function()
486 {
487     this.breakpoints = [];
488 }
489 
490 Firebug.Breakpoint.BreakpointGroup.prototype =
491 {
492     removeBreakpoint: function(bp)
493     {
494         remove(this.breakpoints, bp);
495     },
496 
497     enumerateBreakpoints: function(callback)
498     {
499         var breakpoints = cloneArray(this.breakpoints);
500         for (var i=0; i<breakpoints.length; i++)
501         {
502             var bp = breakpoints[i];
503             if (callback(bp))
504                 return true;
505         }
506         return false;
507     },
508 
509     findBreakpoint: function()
510     {
511         for (var i=0; i<this.breakpoints.length; i++)
512         {
513             var bp = this.breakpoints[i];
514             if (this.matchBreakpoint(bp, arguments))
515                 return bp;
516         }
517         return null;
518     },
519 
520     matchBreakpoint: function(bp, args)
521     {
522         // TODO: must be implemented in derived objects.
523         return false;
524     },
525 
526     isEmpty: function()
527     {
528         return !this.breakpoints.length;
529     }
530 };
531 
532 // ************************************************************************************************
533 
534 function SourceFileRenamer(context)
535 {
536     this.renamedSourceFiles = [];
537     this.context = context;
538     this.bps = [];
539 }
540 
541 SourceFileRenamer.prototype.checkForRename = function(url, line, props)
542 {
543     var sourceFile = this.context.sourceFileMap[url];
544     if (sourceFile.isEval() || sourceFile.isEvent())
545     {
546         var segs = sourceFile.href.split('/');
547         if (segs.length > 2)
548         {
549             if (segs[segs.length - 2] == "seq")
550             {
551                 this.renamedSourceFiles.push(sourceFile);
552                 this.bps.push(props);
553             }
554         }
555         this.context.dynamicURLhasBP = true;  // whether not we needed to rename, the dynamic sourceFile has a bp.
556         if (FBTrace.DBG_SOURCEFILES)
557             FBTrace.sysout("debugger.checkForRename found bp in "+sourceFile+" renamed files:", this.renamedSourceFiles);
558     }
559     else
560     {
561         if (FBTrace.DBG_SOURCEFILES)
562             FBTrace.sysout("debugger.checkForRename found static bp in "+sourceFile+" bp:", props);
563     }
564 
565     return (this.renamedSourceFiles.length > 0);
566 };
567 
568 SourceFileRenamer.prototype.needToRename = function(context)
569 {
570     if (this.renamedSourceFiles.length > 0)
571         this.renameSourceFiles(context);
572 
573     if (FBTrace.DBG_SOURCEFILES)
574         FBTrace.sysout("debugger renamed " + this.renamedSourceFiles.length + " sourceFiles", context.sourceFileMap);
575 
576     return this.renamedSourceFiles.length;
577 }
578 
579 SourceFileRenamer.prototype.renameSourceFiles = function(context)
580 {
581     for (var i = 0; i < this.renamedSourceFiles.length; i++)
582     {
583         var sourceFile = this.renamedSourceFiles[i];
584         var bp = this.bps[i];
585         FBTrace.sysout("debugger.renameSourceFiles type: "+bp.type, bp);
586         var oldURL = sourceFile.href;
587         var sameType = bp.type;
588         var sameLineNo = bp.lineNo;
589         var sameDebuggr = bp.debugger;
590 
591         var segs = oldURL.split('/');  // last is sequence #, next-last is "seq", next-next-last is kind
592         var kind = segs.splice(segs.length - 3, 3)[0];
593         var callerURL = segs.join('/');
594         var newURL = Firebug.Debugger.getURLFromMD5(callerURL, sourceFile.source, kind);
595         sourceFile.href = newURL.href;
596 
597         fbs.removeBreakpoint(bp.type, oldURL, bp.lineNo);
598         delete context.sourceFileMap[oldURL];  // SourceFile delete
599 
600         Firebug.Debugger.watchSourceFile(context, sourceFile);
601         var newBP = fbs.addBreakpoint(sameType, sourceFile, sameLineNo, bp, sameDebuggr);
602 
603         var panel = context.getPanel("script", true);
604         if (panel)
605         {
606             panel.context.invalidatePanels("breakpoints");
607             panel.renameSourceBox(oldURL, newURL.href);
608         }
609         if (context.sourceCache.isCached(oldURL))
610         {
611             var lines = context.sourceCache.load(oldURL);
612             context.sourceCache.storeSplitLines(newURL.href, lines);
613             context.sourceCache.invalidate(oldURL);
614         }
615 
616         if (FBTrace.DBG_SOURCEFILES)
617             FBTrace.sysout("SourceFileRenamer renamed "+oldURL +" to "+newURL, { newBP: newBP, oldBP: bp});
618     }
619     return this.renamedSourceFiles.length;
620 }
621 
622 // ************************************************************************************************
623 
624 Firebug.Breakpoint.ConditionEditor = function(doc)
625 {
626     this.initialize(doc);
627 }
628 
629 Firebug.Breakpoint.ConditionEditor.prototype = domplate(Firebug.InlineEditor.prototype,
630 {
631     tag:
632         DIV({"class": "conditionEditor"},
633             DIV({"class": "conditionEditorTop1"},
634                 DIV({"class": "conditionEditorTop2"})
635             ),
636             DIV({"class": "conditionEditorInner1"},
637                 DIV({"class": "conditionEditorInner2"},
638                     DIV({"class": "conditionEditorInner"},
639                         DIV({"class": "conditionCaption"}, $STR("ConditionInput")),
640                         INPUT({"class": "conditionInput", type: "text",
641                             "aria-label": $STR("ConditionInput")}
642                         )
643                     )
644                 )
645             ),
646             DIV({"class": "conditionEditorBottom1"},
647                 DIV({"class": "conditionEditorBottom2"})
648             )
649         ),
650 
651     initialize: function(doc)
652     {
653         this.box = this.tag.replace({}, doc, this);
654 
655         // XXXjjb we need childNode[1] always
656         this.input = this.box.childNodes[1].firstChild.firstChild.lastChild;
657         Firebug.InlineEditor.prototype.initialize.apply(this, arguments);
658     },
659 
660     show: function(sourceLine, panel, value)
661     {
662         this.target = sourceLine;
663         this.panel = panel;
664 
665         if (this.getAutoCompleter)
666             this.getAutoCompleter().reset();
667 
668         hide(this.box, true);
669         panel.selectedSourceBox.appendChild(this.box);
670 
671         if (this.input)
672             this.input.value = value;
673 
674         setTimeout(bindFixed(function()
675         {
676             var offset = getClientOffset(sourceLine);
677 
678             var bottom = offset.y+sourceLine.offsetHeight;
679             var y = bottom - this.box.offsetHeight;
680             if (y < panel.selectedSourceBox.scrollTop)
681             {
682                 y = offset.y;
683                 setClass(this.box, "upsideDown");
684             }
685             else
686                 removeClass(this.box, "upsideDown");
687 
688             this.box.style.top = y + "px";
689             hide(this.box, false);
690 
691             if (this.input)
692             {
693                 this.input.focus();
694                 this.input.select();
695             }
696         }, this));
697     },
698 
699     hide: function()
700     {
701         this.box.parentNode.removeChild(this.box);
702 
703         delete this.target;
704         delete this.panel;
705     },
706 
707     layout: function()
708     {
709     },
710 
711     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
712 
713     endEditing: function(target, value, cancel)
714     {
715         if (!cancel)
716         {
717             var sourceFile = this.panel.location;
718             var lineNo = parseInt(this.target.textContent);
719 
720             fbs.setBreakpointCondition(sourceFile, lineNo, value, Firebug.Debugger);
721         }
722     }
723 });
724 
725 // ************************************************************************************************
726 /*
727  * Construct a break notification popup
728  * @param doc the document to contain the popup
729  * @param cause info object for the popup, with these optional fields:
730  *   strings: title, message, attrName
731  *   elements: target, relatedTarget: element
732  *   objects: prevValue, newValue
733  */
734 Firebug.Breakpoint.BreakNotification = function(doc, cause)
735 {
736     this.initialize(doc, cause);
737 }
738 
739 Firebug.Breakpoint.BreakNotification.prototype = domplate(Firebug.InlineEditor.prototype,
740 {
741     tag:
742         DIV({"class": "conditionEditor breakNotification", onclick: "$hide"},
743             DIV({"class": "notationEditorTop1"},
744                 DIV({"class": "notationEditorTop2"})
745             ),
746             DIV({"class": "notationEditorInner1"},
747                 DIV({"class": "notationEditorInner2"},
748                     DIV({"class": "conditionEditorInner"},
749                         DIV({"class": "notationCaption"},
750                             SPAN({"class": "notationTitle"}, "$cause.title"),
751                             BUTTON({"class": "notationButton", onclick: "$onCopyAction",
752                                 $collapsed: "$cause|hideCopyAction"},
753                                 $STR("Copy")
754                             )
755                         ),
756                         DIV({"class": "notationCaption"},
757                             SPAN({"class": "notationTitle"}, "$cause|getTitle"),
758                             SPAN(" "),
759                             SPAN({"class": "notationTitle diff"}, "$cause|getDiff"),
760                             SPAN(" "),
761                             TAG("$cause|getTargetTag", {object: "$cause.target"}),
762                             SPAN(" "),
763                             TAG("$cause|getRelatedTargetTag", {object: "$cause.relatedNode"})
764                         )
765                     )
766                 )
767             ),
768             DIV({"class": "notationEditorBottom1"},
769                 DIV({"class": "notationEditorBottom2"})
770             )
771         ),
772 
773     getTargetTag: function(cause)
774     {
775         return cause.target ? FirebugReps.Element.shortTag : null;
776     },
777 
778     getRelatedTargetTag: function(cause)
779     {
780         return cause.relatedTarget ? FirebugReps.Element.shortTag : null;
781     },
782 
783     getDiff: function(cause)
784     {
785         var str = "";
786         if (cause.prevValue)
787             str += cropString(cause.prevValue, 40) + " -> ";
788         if (cause.newValue)
789             str += cropString(cause.newValue, 40);
790 
791         if (!str.length)
792             return "";
793 
794         if (!cause.target)
795             return str;
796 
797         return str;
798     },
799 
800     getTitle: function(cause)
801     {
802         var str = cause.message + (cause.attrName ? (" '"+cause.attrName+"'") : "");
803         if (this.getDiff(cause))
804             str += ":";
805         return str;
806     },
807 
808     initialize: function(doc, cause)
809     {
810         this.cause = cause;
811         this.box = this.tag.replace({cause: cause}, doc, this);
812     },
813 
814     show: function(sourceLine, panel, value)
815     {
816         this.target = sourceLine;
817         this.panel = panel;
818 
819         hide(this.box, true);
820         panel.selectedSourceBox.appendChild(this.box);
821 
822         setTimeout(bindFixed(function()
823         {
824             var offset = getClientOffset(sourceLine);
825 
826             var bottom = offset.y+sourceLine.offsetHeight;
827             var y = bottom - this.box.offsetHeight;
828             if (y < panel.selectedSourceBox.scrollTop)
829             {
830                 y = offset.y;
831                 setClass(this.box, "upsideDown");
832             }
833             else
834                 removeClass(this.box, "upsideDown");
835 
836             this.box.style.top = y + "px";
837             hide(this.box, false);
838         }, this));
839     },
840 
841     hide: function(event) // the argument event does not come thru??
842     {
843         if (this.panel)
844         {
845             var guts = this.box.getElementsByClassName("conditionEditorInner").item(0);
846             collapse(guts, true);  // as the box shrinks you don't want text to spill
847 
848             var msg = this.cause.message;
849             if (msg)
850             {
851                 var self = this;
852                 var delta = Math.max(20,Math.floor(self.box.clientWidth/20));
853                 var interval = setInterval(function slide(event)
854                 {
855                     if (self.box.clientWidth < delta)
856                     {
857                         clearNode(guts);
858 
859                         clearInterval(interval);
860                         self.box.parentNode.removeChild(self.box);
861                         self.target.setAttribute('title', msg);
862                         setClass(self.target, "noteInToolTip");
863                         delete self.target;
864                         delete self.panel;
865                     }
866                     else
867                         self.box.style.width = (self.box.clientWidth - delta)+"px";
868                 }, 15);
869             }
870             else
871             {
872                 delete this.target;
873                 delete this.panel;
874             }
875         }
876         // else we already called hide
877     },
878 
879     hideCopyAction: function(cause)
880     {
881         return !cause.copyAction;
882     },
883 
884     onCopyAction: function(event)
885     {
886         if (this.cause.copyAction)
887             this.cause.copyAction();
888     }
889 });
890 
891 // ************************************************************************************************
892 // Registration
893 
894 Firebug.registerPanel(Firebug.Breakpoint.BreakpointsPanel);
895 Firebug.registerRep(Firebug.Breakpoint.BreakpointRep);
896 Firebug.registerModule(Firebug.Breakpoint);
897 
898 // ************************************************************************************************
899 }});
900