1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/domplate",
  7     "firebug/chrome/reps",
  8     "firebug/lib/locale",
  9     "firebug/lib/events",
 10     "firebug/js/sourceLink",
 11     "firebug/js/stackFrame",
 12     "firebug/lib/css",
 13     "firebug/lib/dom",
 14     "firebug/lib/string",
 15     "firebug/lib/array",
 16     "firebug/lib/persist",
 17     "firebug/chrome/menu",
 18     "firebug/js/fbs",
 19     "firebug/editor/editor",
 20     "firebug/console/autoCompleter"
 21 ],
 22 function(Obj, Firebug, Domplate, FirebugReps, Locale, Events, SourceLink,
 23     StackFrame, Css, Dom, Str, Arr, Persist, Menu, FBS) {
 24 
 25 // ********************************************************************************************* //
 26 // Constants
 27 
 28 /* const animationDuration = 0.8; see issue 5618 */
 29 
 30 // ********************************************************************************************* //
 31 // Breakpoints
 32 
 33 Firebug.Breakpoint = Obj.extend(Firebug.Module,
 34 {
 35     dispatchName: "breakpoints",
 36 
 37     toggleBreakOnNext: function(panel)
 38     {
 39         var breakable = Firebug.chrome.getGlobalAttribute("cmd_firebug_toggleBreakOn", "breakable");
 40 
 41         if (FBTrace.DBG_BP)
 42             FBTrace.sysout("breakpoint.toggleBreakOnNext; currentBreakable "+breakable+
 43                 " in " + panel.context.getName());
 44 
 45         // Toggle button's state.
 46         breakable = (breakable == "true" ? "false" : "true");
 47         Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleBreakOn", "breakable", breakable);
 48 
 49         // Call the current panel's logic related to break-on-next.
 50         // If breakable == "true" the feature is currently disabled.
 51         var enabled = (breakable == "true" ? false : true);
 52         panel.breakOnNext(enabled);
 53 
 54         // Make sure the correct tooltip (coming from the current panel) is used.
 55         this.updateBreakOnNextTooltips(panel);
 56 
 57         // Light up the tab whenever break on next is selected
 58         this.updatePanelTab(panel, enabled);
 59 
 60         return enabled;
 61     },
 62 
 63     showPanel: function(browser, panel)
 64     {
 65         if (!panel)  // there is no selectedPanel?
 66             return;
 67 
 68         var breakButton = Firebug.chrome.$("fbBreakOnNextButton");
 69         if (panel.name)
 70             breakButton.setAttribute("panelName", panel.name);
 71 
 72         breakButton.removeAttribute("type");
 73         Dom.collapse(Firebug.chrome.$("fbBonButtons"), !panel.breakable);
 74 
 75         // The script panel can be created at this moment (the second parameter is false)
 76         // It's needed for break on next to work (do not wait till the user actuall
 77         // selectes the panel).
 78         var scriptPanel = panel.context.getPanel("script");
 79         var scriptEnabled = scriptPanel && scriptPanel.isEnabled();
 80         var tool = Firebug.connection.getTool("script");
 81         var scriptActive = tool && tool.getActive();
 82         var supported = panel.supportsBreakOnNext();
 83 
 84         // Enable by default and disable if needed.
 85         Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleBreakOn", "disabled", null);
 86 
 87         // Disable BON if script is disabled or if BON isn't supported by the current panel.
 88         if (!scriptEnabled || !scriptActive || !supported)
 89         {
 90             Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleBreakOn", "breakable", "disabled");
 91             Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleBreakOn", "disabled", "true");
 92             this.updateBreakOnNextTooltips(panel);
 93             return;
 94         }
 95 
 96         // Set the tooltips and update break-on-next button's state.
 97         var shouldBreak = panel.shouldBreakOnNext();
 98         this.updateBreakOnNextState(panel, shouldBreak);
 99         this.updateBreakOnNextTooltips(panel);
100         this.updatePanelTab(panel, shouldBreak);
101 
102         var menuItems = panel.getBreakOnMenuItems();
103         if (!menuItems || !menuItems.length)
104             return;
105 
106         breakButton.setAttribute("type", "menu-button");
107 
108         var menuPopup = Firebug.chrome.$("fbBreakOnNextOptions");
109         Dom.eraseNode(menuPopup);
110 
111         Menu.createMenuItems(menuPopup, menuItems);
112     },
113 
114     /* see issue 5618
115     toggleTabHighlighting: function(event)
116     {
117         // Don't continue if it's the wrong animation phase
118         if (Math.floor(event.elapsedTime * 10) % (animationDuration * 20) != 0)
119             return;
120 
121         Events.removeEventListener(event.target, "animationiteration",
122             Firebug.Breakpoint.toggleTabHighlighting, true);
123 
124         var panel = Firebug.currentContext.getPanel(event.target.panelType.prototype.name);
125         if (!panel)
126             return;
127 
128         if (!panel.context.delayedArmedTab)
129             return;
130 
131         panel.context.delayedArmedTab.setAttribute("breakOnNextArmed", "true");
132         delete panel.context.delayedArmedTab;
133     },
134     */
135 
136     updateBreakOnNextTooltips: function(panel)
137     {
138         var breakable = Firebug.chrome.getGlobalAttribute("cmd_firebug_toggleBreakOn", "breakable");
139 
140         // Get proper tooltip for the break-on-next button from the current panel.
141         // If breakable is set to "false" the feature is already activated (throbbing).
142         var armed = (breakable == "false");
143         var tooltip = panel.getBreakOnNextTooltip(armed);
144         if (!tooltip)
145             tooltip = "";
146 
147         // The user should know that BON is disabled if the Script panel (debugger) is disabled.
148         if (breakable == "disabled")
149             tooltip += " " + Locale.$STR("firebug.bon.scriptPanelNeeded");
150 
151         Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleBreakOn", "tooltiptext", tooltip);
152     },
153 
154     updateBreakOnNextState: function(panel, armed)
155     {
156         // If the panel should break at the next chance, set the button to not breakable,
157         // which means already active (throbbing).
158         var breakable = armed ? "false" : "true";
159         Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleBreakOn", "breakable", breakable);
160     },
161 
162     updatePanelTab: function(panel, armed)
163     {
164         if (!panel)
165             return;
166 
167         // If the script panels is disabled, BON can't be active.
168         if (!Firebug.PanelActivation.isPanelEnabled("script"))
169             armed = false;
170 
171         var panelBar = Firebug.chrome.$("fbPanelBar1");
172         var tab = panelBar.getTab(panel.name);
173         if (tab)
174             tab.setAttribute("breakOnNextArmed", armed ? "true" : "false");
175 
176         /* see issue 5618
177         {
178             if (armed)
179             {
180                 // If there is already a panel armed synchronize highlighting of the panel tabs
181                 var tabPanel = tab.parentNode;
182                 var otherTabIsArmed = false;
183                 for (var i = 0; i < tabPanel.children.length; ++i)
184                 {
185                     var panelTab = tabPanel.children[i];
186                     if (panelTab !== tab && panelTab.getAttribute("breakOnNextArmed") == "true")
187                     {
188                         panel.context.delayedArmedTab = tab;
189                         Events.addEventListener(panelTab, "animationiteration",
190                             this.toggleTabHighlighting, true);
191                         otherTabIsArmed = true;
192                         break;
193                     }
194                 }
195 
196                 if (!otherTabIsArmed)
197                     tab.setAttribute("breakOnNextArmed", "true");
198             }
199             else
200             {
201                 delete panel.context.delayedArmedTab;
202                 tab.setAttribute("breakOnNextArmed", "false");
203             }
204         }
205         */
206     },
207 
208     updatePanelTabs: function(context)
209     {
210         if (!context)
211             return;
212 
213         var panelTypes = Firebug.getMainPanelTypes(context);
214         for (var i=0; i<panelTypes.length; ++i)
215         {
216             var panelType = panelTypes[i];
217             var panel = context.getPanel(panelType.prototype.name);
218             var shouldBreak = (panel && panel.shouldBreakOnNext()) ? true : false;
219             this.updatePanelTab(panel, shouldBreak);
220         }
221     },
222 
223     // supports non-JS break on next
224     breakNow: function(panel)
225     {
226         this.updatePanelTab(panel, false);
227         Firebug.Debugger.breakNow(panel.context);  // TODO BTI
228     },
229 
230     updateOption: function(name, value)
231     {
232         if (name == "showBreakNotification")
233         {
234             var panelBar1 = Firebug.chrome.$("fbPanelBar1");
235             var doc = panelBar1.browser.contentDocument;
236             var checkboxes = doc.querySelectorAll(".doNotShowBreakNotification");
237 
238             for (var i=0; i<checkboxes.length; i++)
239                 checkboxes[i].checked = !value;
240         }
241     },
242 });
243 
244 // ************************************************************************************************
245 
246 with (Domplate) {
247 Firebug.Breakpoint.BreakpointListRep = domplate(Firebug.Rep,
248 {
249     tag:
250         DIV({role : "list"},
251             FOR("group", "$groups",
252                 DIV({"class": "breakpointBlock breakpointBlock-$group.name", role: "list",
253                         $opened: "$group.opened", _repObject: "$group", onclick: "$onClick"},
254                     H1({"class": "breakpointHeader groupHeader"},
255                         DIV({"class": "twisty", role: "presentation"}),
256                         SPAN({"class": "breakpointsHeaderLabel"}, "$group.title")
257                     ),
258                     DIV({"class": "breakpointsGroupListBox", role: "listbox"},
259                         FOR("bp", "$group.breakpoints",
260                             TAG("$bp|getBreakpointRep", {bp: "$bp"})
261                         )
262                     )
263                 )
264             )
265         ),
266 
267     getBreakpointRep: function(bp)
268     {
269         var rep = Firebug.getRep(bp, Firebug.currentContext);
270         return rep.tag;
271     },
272 
273     toggleGroup: function(node)
274     {
275         var panel = Firebug.getElementPanel(node);
276         var groupNode = Dom.getAncestorByClass(node, "breakpointBlock");
277         var group = Firebug.getRepObject(groupNode);
278 
279         Css.toggleClass(groupNode, "opened");
280         var opened = Css.hasClass(groupNode, "opened");
281         panel.groupOpened[group.name] = opened;
282 
283         if (opened)
284         {
285             var offset = Dom.getClientOffset(node);
286             var titleAtTop = offset.y < panel.panelNode.scrollTop;
287             Dom.scrollTo(groupNode, panel.panelNode, null,
288                 groupNode.offsetHeight > panel.panelNode.clientHeight || titleAtTop ? "top" : "bottom");
289         }
290     },
291 
292     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
293 
294     onClick: function(event)
295     {
296         if (!Events.isLeftClick(event))
297             return;
298 
299         var header = Dom.getAncestorByClass(event.target, "breakpointHeader");
300         if (header)
301         {
302             this.toggleGroup(event.target);
303             return;
304         }
305     }
306 });
307 
308 // ********************************************************************************************* //
309 
310 Firebug.Breakpoint.BreakpointRep = domplate(Firebug.Rep,
311 {
312     tag:
313         DIV({"class": "breakpointRow focusRow", $disabled: "$bp|isDisabled", role: "option",
314                 "aria-checked": "$bp.checked", _repObject: "$bp", onclick: "$onClick"},
315             DIV({"class": "breakpointBlockHead"},
316                 INPUT({"class": "breakpointCheckbox", type: "checkbox",
317                     _checked: "$bp.checked", tabindex : '-1'}),
318                 SPAN({"class": "breakpointName"}, "$bp.name"),
319                 TAG(FirebugReps.SourceLink.tag, {object: "$bp|getSourceLink"}),
320                 IMG({"class": "closeButton", src: "blank.gif"})
321             ),
322             DIV({"class": "breakpointCode"}, "$bp.sourceLine")
323         ),
324 
325     getSourceLink: function(bp)
326     {
327         return new SourceLink.SourceLink(bp.href, bp.lineNumber, "js");
328     },
329 
330     removeBreakpoint: function(groupName, href, lineNumber)
331     {
332         if (groupName == "breakpoints")
333             FBS.clearBreakpoint(href, lineNumber);
334         else if (groupName == "errorBreakpoints")
335             FBS.clearErrorBreakpoint(href, lineNumber);
336         else if (groupName == "monitors")
337             FBS.unmonitor(href, lineNumber);
338     },
339 
340     enableBreakpoint: function(href, lineNumber)
341     {
342         FBS.enableBreakpoint(href, lineNumber);
343     },
344 
345     disableBreakpoint: function(href, lineNumber)
346     {
347         FBS.disableBreakpoint(href, lineNumber);
348     },
349 
350     isDisabled: function(bp)
351     {
352         return !bp.checked;
353     },
354 
355     getContextMenuItems: function(breakpoint, target)
356     {
357         var head = Dom.getAncestorByClass(target, "breakpointBlock");
358         var groupName = Css.getClassValue(head, "breakpointBlock");
359 
360         var items = [{
361             label: "breakpoints.Remove_Breakpoint",
362             tooltiptext: "breakpoints.tip.Remove_Breakpoint",
363             command: Obj.bindFixed(this.removeBreakpoint, this, groupName,
364                 breakpoint.href, breakpoint.lineNumber)
365         }];
366 
367         if (groupName == "breakpoints")
368         {
369             if (breakpoint.checked)
370             {
371                 items.push({
372                     label: "breakpoints.Disable_Breakpoint",
373                     tooltiptext: "breakpoints.tip.Disable_Breakpoint",
374                     command: Obj.bindFixed(this.disableBreakpoint, this, breakpoint.href,
375                         breakpoint.lineNumber)
376                 });
377             }
378             else
379             {
380                 items.push({
381                     label: "breakpoints.Enable_Breakpoint",
382                     tooltiptext: "breakpoints.tip.Enable_Breakpoint",
383                     command: Obj.bindFixed(this.enableBreakpoint, this, breakpoint.href,
384                         breakpoint.lineNumber)
385                 });
386             }
387         }
388 
389         items.push(
390              "-"
391         );
392 
393         return items;
394     },
395 
396     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
397 
398     inspectable: false,
399 
400     supportsObject: function(object, type)
401     {
402         return (object instanceof Firebug.Debugger.Breakpoint);  // FIXME moz back end
403     },
404 
405     onClick: function(event)
406     {
407         var panel = Firebug.getElementPanel(event.target);
408 
409         if (Dom.getAncestorByClass(event.target, "breakpointCheckbox"))
410         {
411             var node = event.target.parentNode.getElementsByClassName(
412                 "objectLink-sourceLink").item(0);
413 
414             if (!node)
415                 return;
416 
417             var sourceLink = node.repObject;
418 
419             panel.noRefresh = true;
420             var checkBox = event.target;
421             var bpRow = Dom.getAncestorByClass(checkBox, "breakpointRow");
422 
423             if (checkBox.checked)
424             {
425                 this.enableBreakpoint(sourceLink.href, sourceLink.line);
426                 bpRow.setAttribute("aria-checked", "true");
427             }
428             else
429             {
430                 this.disableBreakpoint(sourceLink.href, sourceLink.line);
431                 bpRow.setAttribute("aria-checked", "false");
432             }
433             panel.noRefresh = false;
434         }
435         else if (Dom.getAncestorByClass(event.target, "closeButton"))
436         {
437             panel.noRefresh = true;
438             var sourceLink = event.target.parentNode.getElementsByClassName(
439                 "objectLink-sourceLink").item(0).repObject;
440 
441             var head = Dom.getAncestorByClass(event.target, "breakpointBlock");
442             var groupName = Css.getClassValue(head, "breakpointBlock");
443 
444             this.removeBreakpoint(groupName, sourceLink.href, sourceLink.line);
445 
446             panel.noRefresh = false;
447         }
448 
449         panel.refresh();
450     }
451 })};
452 
453 // ********************************************************************************************* //
454 
455 Firebug.Breakpoint.BreakpointsPanel = function() {}
456 
457 Firebug.Breakpoint.BreakpointsPanel.prototype = Obj.extend(Firebug.Panel,
458 {
459     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
460     // extends Panel
461 
462     name: "breakpoints",
463     parentPanel: "script",
464     order: 2,
465     enableA11y: true,
466     deriveA11yFrom: "console",
467 
468     initialize: function()
469     {
470         this.groupOpened = [];
471 
472         Firebug.Panel.initialize.apply(this, arguments);
473     },
474 
475     destroy: function(state)
476     {
477         state.groupOpened = this.groupOpened;
478 
479         Firebug.Panel.destroy.apply(this, arguments);
480     },
481 
482     show: function(state)
483     {
484         if (this.context.loaded)
485         {
486             var state;
487             Persist.restoreObjects(this, state);
488 
489             if (state)
490             {
491                 if (state.groupOpened)
492                     this.groupOpened = state.groupOpened;
493             }
494         }
495 
496         this.refresh();
497     },
498 
499     refresh: function()
500     {
501         if (this.noRefresh)
502             return;
503 
504         var extracted = this.extractBreakpoints(this.context);
505 
506         var breakpoints = extracted.breakpoints;
507         var errorBreakpoints = extracted.errorBreakpoints;
508         var monitors = extracted.monitors;
509 
510         if (FBTrace.DBG_BP)
511             FBTrace.sysout("breakpoints.refresh extracted " +
512                 breakpoints.length+errorBreakpoints.length+monitors.length,
513                 [breakpoints, errorBreakpoints, monitors]);
514 
515         function sortBreakpoints(a, b)
516         {
517             if (a.href == b.href)
518                 return a.lineNumber < b.lineNumber ? -1 : 1;
519             else
520                 return a.href < b.href ? -1 : 1;
521         }
522 
523         breakpoints.sort(sortBreakpoints);
524         errorBreakpoints.sort(sortBreakpoints);
525         monitors.sort(sortBreakpoints);
526 
527         if (FBTrace.DBG_BP)
528             FBTrace.sysout("breakpoints.refresh sorted "+breakpoints.length+
529                 errorBreakpoints.length+monitors.length, [breakpoints, errorBreakpoints, monitors]);
530 
531         var groups = [];
532 
533         if (breakpoints.length)
534             groups.push({name: "breakpoints", title: Locale.$STR("Breakpoints"),
535                 breakpoints: breakpoints});
536 
537         if (errorBreakpoints.length)
538             groups.push({name: "errorBreakpoints", title: Locale.$STR("ErrorBreakpoints"),
539                 breakpoints: errorBreakpoints});
540 
541         if (monitors.length)
542             groups.push({name: "monitors", title: Locale.$STR("LoggedFunctions"),
543                 breakpoints: monitors});
544 
545         Firebug.connection.dispatch("getBreakpoints", [this.context, groups]);
546 
547         if (groups.length != 0)
548         {
549             for (var i = 0; i < groups.length; ++i)
550             {
551                 groups[i].opened = typeof this.groupOpened[groups[i].name] != "undefined" ?
552                     this.groupOpened[groups[i].name] : true;
553             }
554 
555             Firebug.Breakpoint.BreakpointListRep.tag.replace({groups: groups}, this.panelNode);
556         }
557         else
558         {
559             FirebugReps.Warning.tag.replace({object: "NoBreakpointsWarning"}, this.panelNode);
560         }
561 
562         if (FBTrace.DBG_BP)
563         {
564             FBTrace.sysout("breakpoints.refresh "+breakpoints.length+
565                 errorBreakpoints.length+monitors.length, [breakpoints, errorBreakpoints, monitors]);
566         }
567 
568         Events.dispatch(this.fbListeners, "onBreakRowsRefreshed", [this, this.panelNode]);
569     },
570 
571     extractBreakpoints: function(context)
572     {
573         var breakpoints = [];
574         var errorBreakpoints = [];
575         var monitors = [];
576 
577         var renamer = new SourceFileRenamer(context);
578         var self = this;
579         var Breakpoint = Firebug.Debugger.Breakpoint;
580 
581         for (var url in context.sourceFileMap)
582         {
583             FBS.enumerateBreakpoints(url, {call: function(url, line, props, scripts)
584             {
585                 if (FBTrace.DBG_BP)
586                     FBTrace.sysout("breakpoints.extractBreakpoints type: "+props.type+" in url "+
587                         url+"@"+line+" context "+context.getName(), props);
588 
589                 // some url in this sourceFileMap has changed, we'll be back.
590                 if (renamer.checkForRename(url, line, props))
591                     return;
592 
593                 if (scripts)  // then this is a current (not future) breakpoint
594                 {
595                     var script = scripts[0];
596                     var analyzer = Firebug.SourceFile.getScriptAnalyzer(context, script);
597                     if (FBTrace.DBG_BP)
598                         FBTrace.sysout("breakpoints.refresh enumerateBreakpoints for script="+
599                             script.tag+(analyzer?" has analyzer":" no analyzer")+" in context "+
600                             context.getName());
601 
602                     if (analyzer)
603                         var name = analyzer.getFunctionDescription(script, context).name;
604                     else
605                         var name = StackFrame.guessFunctionName(url, 1, context);
606 
607                     var isFuture = false;
608                 }
609                 else
610                 {
611                     if (FBTrace.DBG_BP)
612                         FBTrace.sysout("breakpoints.refresh enumerateBreakpoints future for url@line="+
613                             url+"@"+line+"\n");
614 
615                     var isFuture = true;
616                 }
617 
618                 var source = context.sourceCache.getLine(url, line);
619                 breakpoints.push(new Breakpoint(name, url, line, !props.disabled, source, isFuture));
620             }});
621 
622             FBS.enumerateErrorBreakpoints(url, {call: function(url, line, props)
623             {
624                 // some url in this sourceFileMap has changed, we'll be back.
625                 if (renamer.checkForRename(url, line, props))
626                     return;
627 
628                 var name = Firebug.SourceFile.guessEnclosingFunctionName(url, line, context);
629                 var source = context.sourceCache.getLine(url, line);
630                 errorBreakpoints.push(new Breakpoint(name, url, line, true, source));
631             }});
632 
633             FBS.enumerateMonitors(url, {call: function(url, line, props)
634             {
635                 // some url in this sourceFileMap has changed, we'll be back.
636                 if (renamer.checkForRename(url, line, props))
637                     return;
638 
639                 var name = Firebug.SourceFile.guessEnclosingFunctionName(url, line, context);
640                 monitors.push(new Breakpoint(name, url, line, true, ""));
641             }});
642         }
643 
644         var result = null;
645 
646         if (renamer.needToRename(context))
647         {
648             // since we renamed some sourceFiles we need to refresh the breakpoints again.
649             result = this.extractBreakpoints(context);
650         }
651         else
652         {
653             result = {
654                 breakpoints: breakpoints,
655                 errorBreakpoints: errorBreakpoints,
656                 monitors: monitors
657             };
658         }
659 
660         // even if we did not rename, some bp may be dynamic
661         if (FBTrace.DBG_SOURCEFILES)
662             FBTrace.sysout("breakpoints.extractBreakpoints context.dynamicURLhasBP: "+
663                 context.dynamicURLhasBP, result);
664 
665         return result;
666     },
667 
668     getOptionsMenuItems: function()
669     {
670         var items = [];
671 
672         var context = this.context;
673 
674         var bpCount = 0, disabledCount = 0;
675         var checkBoxes = this.panelNode.getElementsByClassName("breakpointCheckbox");
676         for (var i=0; i<checkBoxes.length; i++)
677         {
678             ++bpCount;
679             if (!checkBoxes[i].checked)
680                 ++disabledCount;
681         }
682 
683         if (disabledCount)
684         {
685             items.push(
686                 {
687                     label: "EnableAllBreakpoints",
688                     command: Obj.bindFixed(this.enableAllBreakpoints, this, context, true),
689                     tooltiptext: "breakpoints.option.tip.Enable_All_Breakpoints"
690                 }
691             );
692         }
693         if (bpCount && disabledCount != bpCount)
694         {
695             items.push(
696                 {
697                     label: "DisableAllBreakpoints",
698                     command: Obj.bindFixed(this.enableAllBreakpoints, this, context, false),
699                     tooltiptext: "breakpoints.option.tip.Disable_All_Breakpoints"
700                 }
701             );
702         }
703 
704         items.push(
705             "-",
706             {
707                 label: "ClearAllBreakpoints",
708                 disabled: !bpCount,
709                 command: Obj.bindFixed(this.clearAllBreakpoints, this, context),
710                 tooltiptext: "breakpoints.option.tip.Clear_All_Breakpoints"
711             }
712         );
713 
714         return items;
715     },
716 
717     getContextMenuItems: function(object, target, context)
718     {
719         return this.getOptionsMenuItems();
720     },
721 
722     enableAllBreakpoints: function(context, status)
723     {
724         var checkBoxes = this.panelNode.getElementsByClassName("breakpointCheckbox");
725         for (var i=0; i<checkBoxes.length; i++)
726         {
727             var box = checkBoxes[i];
728             if (box.checked != status)
729                 this.click(box);
730         }
731     },
732 
733     clearAllBreakpoints: function(context)
734     {
735         this.noRefresh = true;
736 
737         try
738         {
739             // Remove regular JSD breakpoints
740             Firebug.Debugger.clearAllBreakpoints(context);
741         }
742         catch(exc)
743         {
744             FBTrace.sysout("breakpoint.clearAllBreakpoints FAILS "+exc, exc);
745         }
746 
747         this.noRefresh = false;
748         this.refresh();
749 
750         // Remove the rest of all the other kinds of breakpoints (after refresh).
751         // These can come from various modules and perhaps extensions, so use
752         // the appropriate remove buttons.
753         var buttons = this.panelNode.getElementsByClassName("closeButton");
754         while (buttons.length)
755             this.click(buttons[0]);
756 
757         // Breakpoint group titles must also go away.
758         this.refresh();
759     },
760 
761     click: function(node)
762     {
763         var doc = node.ownerDocument, event = doc.createEvent("MouseEvents");
764         event.initMouseEvent("click", true, true, doc.defaultView, 0, 0, 0, 0, 0,
765             false, false, false, false, 0, null);
766         return node.dispatchEvent(event);
767     }
768 });
769 
770 // ********************************************************************************************* //
771 
772 function countBreakpoints(context)
773 {
774     var count = 0;
775     for (var url in context.sourceFileMap)
776     {
777         FBS.enumerateBreakpoints(url, {call: function(url, lineNo)
778         {
779             ++count;
780         }});
781     }
782     return count;
783 }
784 
785 // ********************************************************************************************* //
786 
787 Firebug.Breakpoint.BreakpointGroup = function()
788 {
789     this.breakpoints = [];
790 }
791 
792 Firebug.Breakpoint.BreakpointGroup.prototype =
793 {
794     removeBreakpoint: function(bp)
795     {
796         Arr.remove(this.breakpoints, bp);
797     },
798 
799     enumerateBreakpoints: function(callback)
800     {
801         var breakpoints = Arr.cloneArray(this.breakpoints);
802         for (var i=0; i<breakpoints.length; i++)
803         {
804             var bp = breakpoints[i];
805             if (callback(bp))
806                 return true;
807         }
808         return false;
809     },
810 
811     findBreakpoint: function()
812     {
813         for (var i=0; i<this.breakpoints.length; i++)
814         {
815             var bp = this.breakpoints[i];
816             if (this.matchBreakpoint(bp, arguments))
817                 return bp;
818         }
819         return null;
820     },
821 
822     matchBreakpoint: function(bp, args)
823     {
824         // TODO: must be implemented in derived objects.
825         return false;
826     },
827 
828     isEmpty: function()
829     {
830         return !this.breakpoints.length;
831     }
832 };
833 
834 // ********************************************************************************************* //
835 // TODO move to mozilla back end
836 
837 function SourceFileRenamer(context)
838 {
839     this.renamedSourceFiles = [];
840     this.context = context;
841     this.bps = [];
842 }
843 
844 SourceFileRenamer.prototype.checkForRename = function(url, line, props)
845 {
846     var sourceFile = this.context.sourceFileMap[url];
847     if (sourceFile.isEval() || sourceFile.isEvent())
848     {
849         var segs = sourceFile.href.split('/');
850         if (segs.length > 2)
851         {
852             if (segs[segs.length - 2] == "seq")
853             {
854                 this.renamedSourceFiles.push(sourceFile);
855                 this.bps.push(props);
856             }
857         }
858 
859         // whether not we needed to rename, the dynamic sourceFile has a bp.
860         this.context.dynamicURLhasBP = true;
861 
862         if (FBTrace.DBG_SOURCEFILES)
863             FBTrace.sysout("breakpoints.checkForRename found bp in "+sourceFile+" renamed files:",
864                 this.renamedSourceFiles);
865     }
866     else
867     {
868         if (FBTrace.DBG_SOURCEFILES)
869             FBTrace.sysout("breakpoints.checkForRename found static bp in " + sourceFile +
870                 " bp:", props);
871     }
872 
873     return (this.renamedSourceFiles.length > 0);
874 };
875 
876 SourceFileRenamer.prototype.needToRename = function(context)
877 {
878     if (this.renamedSourceFiles.length > 0)
879         this.renameSourceFiles(context);
880 
881     if (FBTrace.DBG_SOURCEFILES)
882         FBTrace.sysout("debugger renamed " + this.renamedSourceFiles.length + " sourceFiles",
883             context.sourceFileMap);
884 
885     return this.renamedSourceFiles.length;
886 }
887 
888 SourceFileRenamer.prototype.renameSourceFiles = function(context)
889 {
890     for (var i = 0; i < this.renamedSourceFiles.length; i++)
891     {
892         var sourceFile = this.renamedSourceFiles[i];
893         var bp = this.bps[i];
894 
895         var oldURL = sourceFile.href;
896         var sameType = bp.type;
897         var sameLineNo = bp.lineNo;
898 
899         // last is sequence #, next-last is "seq", next-next-last is kind
900         var segs = oldURL.split('/');
901         var kind = segs.splice(segs.length - 3, 3)[0];
902         var callerURL = segs.join('/');
903         if (!sourceFile.source)
904         {
905             FBTrace.sysout("breakpoint.renameSourceFiles no source for " + oldURL +
906                 " callerURL " + callerURL, sourceFile)
907             continue;
908         }
909 
910         var newURL = Firebug.Debugger.getURLFromMD5(callerURL, sourceFile.source, kind);
911         sourceFile.href = newURL.href;
912 
913         FBS.removeBreakpoint(bp.type, oldURL, bp.lineNo);
914         delete context.sourceFileMap[oldURL];  // SourceFile delete
915 
916         if (FBTrace.DBG_SOURCEFILES)
917             FBTrace.sysout("breakpoints.renameSourceFiles type: "+bp.type, bp);
918 
919         Firebug.Debugger.watchSourceFile(context, sourceFile);
920         var newBP = FBS.addBreakpoint(sameType, sourceFile, sameLineNo, bp, Firebug.Debugger);
921 
922         var panel = context.getPanel("script", true);
923         if (panel)
924         {
925             panel.context.invalidatePanels("breakpoints");
926             panel.renameSourceBox(oldURL, newURL.href);
927         }
928 
929         if (context.sourceCache.isCached(oldURL))
930         {
931             var lines = context.sourceCache.load(oldURL);
932             context.sourceCache.storeSplitLines(newURL.href, lines);
933             context.sourceCache.invalidate(oldURL);
934         }
935 
936         if (FBTrace.DBG_SOURCEFILES)
937             FBTrace.sysout("SourceFileRenamer renamed " + oldURL + " to " + newURL,
938                 { newBP: newBP, oldBP: bp});
939     }
940 
941     return this.renamedSourceFiles.length;
942 }
943 
944 // ********************************************************************************************* //
945 
946 Firebug.Breakpoint.ConditionEditor = function(doc)
947 {
948     this.initialize(doc);
949 }
950 
951 with (Domplate) {
952 Firebug.Breakpoint.ConditionEditor.prototype = domplate(Firebug.JSEditor.prototype,
953 {
954     tag:
955         DIV({"class": "conditionEditor"},
956             DIV({"class": "conditionCaption"}, Locale.$STR("ConditionInput")),
957             INPUT({"class": "conditionInput completionBox", type: "text",
958                 tabindex: "-1"}),
959             INPUT({"class": "conditionInput completionInput", type: "text",
960                 "aria-label": Locale.$STR("ConditionInput"),
961                 oninput: "$onInput", onkeypress: "$onKeyPress"}
962             )
963         ),
964 
965     initialize: function(doc)
966     {
967         this.box = this.tag.replace({}, doc, this);
968         this.input = this.box.getElementsByClassName("completionInput").item(0);
969 
970         var completionBox = this.box.getElementsByClassName("completionBox").item(0);
971         var options = {
972             tabWarnings: true
973         };
974         this.setupCompleter(completionBox, options);
975     },
976 
977     show: function(sourceLine, panel, value)
978     {
979         this.target = sourceLine;
980         this.panel = panel;
981 
982         this.getAutoCompleter().reset();
983 
984         Dom.hide(this.box, true);
985         panel.selectedSourceBox.appendChild(this.box);
986 
987         this.input.value = value;
988 
989         setTimeout(Obj.bindFixed(function()
990         {
991             var offset = Dom.getClientOffset(sourceLine);
992 
993             var bottom = offset.y+sourceLine.offsetHeight;
994             var y = bottom - this.box.offsetHeight;
995             if (y < panel.selectedSourceBox.scrollTop)
996             {
997                 y = offset.y;
998                 Css.setClass(this.box, "upsideDown");
999             }
1000             else
1001             {
1002                 Css.removeClass(this.box, "upsideDown");
1003             }
1004 
1005             this.box.style.top = y + "px";
1006             Dom.hide(this.box, false);
1007 
1008             this.input.focus();
1009             this.input.select();
1010         }, this));
1011     },
1012 
1013     hide: function()
1014     {
1015         this.box.parentNode.removeChild(this.box);
1016 
1017         delete this.target;
1018         delete this.panel;
1019     },
1020 
1021     layout: function()
1022     {
1023     },
1024 
1025     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1026 
1027     endEditing: function(target, value, cancel)
1028     {
1029         if (!cancel)
1030         {
1031             var compilationUnit = this.panel.location;
1032             var lineNo = parseInt(this.target.textContent);
1033             // TODO rest is mozilla backend
1034             var sourceFile = compilationUnit.sourceFile;
1035             FBS.setBreakpointCondition(sourceFile, lineNo, value, Firebug.Debugger);
1036         }
1037     },
1038 });
1039 
1040 // ********************************************************************************************* //
1041 
1042 /**
1043  * Construct a break notification popup
1044  * @param doc the document to contain the notification
1045  * @param cause info object for the popup, with these optional fields:
1046  *   strings: title, message, attrName
1047  *   elements: target, relatedTarget: element
1048  *   objects: prevValue, newValue
1049  */
1050 Firebug.Breakpoint.BreakNotification = function(doc, cause)
1051 {
1052     this.document = doc;
1053     this.cause = cause;
1054 }
1055 
1056 Firebug.Breakpoint.BreakNotification.prototype = domplate(Firebug.Rep,
1057 /** @lends Firebug.ScriptPanel.Notification */
1058 {
1059     tag:
1060         DIV({"class": "notificationBox"},
1061             TABLE({"class": "notificationTable", onclick: "$onHide",
1062                 onmouseover: "$onMouseOver", onmouseout: "$onMouseOut"},
1063                 TBODY(
1064                     TR(
1065                         TD({"class": "imageCol"},
1066                             IMG({"class": "notificationImage",
1067                                 src: "chrome://firebug/skin/breakpoint.png"})
1068                         ),
1069                         TD({"class": "descCol"},
1070                             SPAN({"class": "notificationDesc"}, "$cause|getDescription"),
1071                             SPAN(" "),
1072                             SPAN({"class": "diff"}, "$cause|getDiff"),
1073                             SPAN({"class": "targets"}),
1074                             DIV({"class": "noNotificationDesc"})
1075                         ),
1076                         TD({"class": "buttonsCol"},
1077                             BUTTON({"class": "notificationButton copyButton",
1078                                 onclick: "$onCopyAction",
1079                                 $collapsed: "$cause|hideCopyAction"},
1080                                 Locale.$STR("Copy")
1081                             ),
1082                             BUTTON({"class": "notificationButton skipButton",
1083                                 onclick: "$onSkipAction",
1084                                 $collapsed: "$cause|hideSkipAction"},
1085                                 Locale.$STR("script.balloon.Disable")
1086                             ),
1087                             BUTTON({"class": "notificationButton okButton",
1088                                 onclick: "$onOkAction",
1089                                 $collapsed: "$cause|hideOkAction"},
1090                                 Locale.$STR("script.balloon.Continue")
1091                             )
1092                         ),
1093                         TD(
1094                             DIV({"class": "notificationClose", onclick: "$onHide"})
1095                         )
1096                     )
1097                 )
1098             )
1099         ),
1100 
1101     targets:
1102         SPAN(
1103             SPAN(" "),
1104             TAG("$cause|getTargetTag", {object: "$cause.target"}),
1105             SPAN(" "),
1106             TAG("$cause|getRelatedTargetTag", {object: "$cause.relatedNode"})
1107         ),
1108 
1109     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1110 
1111     onMouseOver: function(event)
1112     {
1113         var target = event.target;
1114         var box = Dom.getAncestorByClass(target, "notificationBox");
1115         var close = box.querySelector(".notificationClose");
1116 
1117         // The close button is "active" (red) if the mouse hovers over the notification
1118         // area except when it hovers over a button or link.
1119         var localName = target.localName ? target.localName.toLowerCase() : "";
1120         if (Css.hasClass(target, "notificationButton") || localName == "a")
1121             close.removeAttribute("active");
1122         else
1123             close.setAttribute("active", true);
1124     },
1125 
1126     onMouseOut: function(event)
1127     {
1128         var box = Dom.getAncestorByClass(event.target, "notificationBox");
1129         var close = box.querySelector(".notificationClose");
1130         close.removeAttribute("active");
1131     },
1132 
1133     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1134 
1135     onHide: function(event)
1136     {
1137         var notify = this.getNotifyObject(event.target);
1138         notify.hide();
1139     },
1140 
1141     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1142 
1143     getDescription: function(cause)
1144     {
1145         var str = cause.message + (cause.attrName ? (" '" + cause.attrName + "'") : "");
1146         if (this.getDiff(cause))
1147             str += ":";
1148 
1149         return str;
1150     },
1151 
1152     getTargetTag: function(cause)
1153     {
1154         return this.getElementTag(cause.target) || null;
1155     },
1156 
1157     getRelatedTargetTag: function(cause)
1158     {
1159         return this.getElementTag(cause.relatedNode) || null;
1160     },
1161 
1162     getElementTag: function(node)
1163     {
1164         if (node)
1165         {
1166             var rep = Firebug.getRep(node);
1167             if (rep)
1168                 return rep.shortTag || rep.tag;
1169         }
1170     },
1171 
1172     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1173     // Button Handlers
1174 
1175     hideCopyAction: function(cause)
1176     {
1177         return !cause.copyAction;
1178     },
1179 
1180     hideSkipAction: function(cause)
1181     {
1182         return !cause.skipAction;
1183     },
1184 
1185     hideOkAction: function(cause)
1186     {
1187         return !cause.okAction;
1188     },
1189 
1190     onCopyAction: function(event)
1191     {
1192         var notify = this.getNotifyObject(event.target);
1193         if (notify.cause.copyAction)
1194             notify.cause.copyAction();
1195     },
1196 
1197     onSkipAction: function(event)
1198     {
1199         var notify = this.getNotifyObject(event.target);
1200         if (notify.cause.skipAction)
1201             notify.cause.skipAction();
1202     },
1203 
1204     onOkAction: function(event)
1205     {
1206         var notify = this.getNotifyObject(event.target);
1207         if (notify.cause.okAction)
1208             notify.cause.okAction();
1209     },
1210 
1211     onCloseAction: function(event)
1212     {
1213         var notify = this.getNotifyObject(event.target);
1214         if (notify.cause.onCloseAction)
1215             notify.cause.onCloseAction();
1216         else
1217             notify.hide(event); // same as click on notify body
1218     },
1219 
1220     getNotifyObject: function(target)
1221     {
1222         var parentNode = Dom.getAncestorByClass(target, "notificationBox");
1223         return parentNode.repObject;
1224     },
1225 
1226     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1227     // Action handlers from "do not show again" description
1228 
1229     onClickLink: function(event)
1230     {
1231         this.showTabMenu(event);
1232     },
1233 
1234     disableNotifications: function(event)
1235     {
1236         Firebug.setPref(Firebug.prefDomain, "showBreakNotification", false);
1237 
1238         // Hide the notification, but default processing of this event would hide it anyway.
1239         this.onHide(event);
1240     },
1241 
1242     showTabMenu: function(event)
1243     {
1244         // Open panel's tab menu to show the "Show Break Notifications" option
1245         // to teach the user where to enable it again.
1246         var panelBar = Firebug.chrome.$("fbPanelBar1");
1247         var tab = panelBar.getTab("script");
1248         tab.tabMenu.showMenu();
1249 
1250         // Avoid default processing that hides the notification popup.
1251         Events.cancelEvent(event);
1252     },
1253 
1254     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1255     // Helpers
1256 
1257     getDiff: function(cause)
1258     {
1259         var str = "";
1260 
1261         if (cause.prevValue)
1262             str += Str.cropString(cause.prevValue, 40) + " -> ";
1263 
1264         if (cause.newValue)
1265             str += Str.cropString(cause.newValue, 40);
1266 
1267         if (!str.length)
1268             return "";
1269 
1270         if (!cause.target)
1271             return str;
1272 
1273         return str;
1274     },
1275 
1276     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1277     // Public
1278 
1279     show: function(parentNode)
1280     {
1281         if (FBTrace.DBG_BP)
1282             FBTrace.sysout("breakNotification.show; " + this.id);
1283 
1284         // Reneder the entire notification box.
1285         this.box = this.tag.append(this.cause, parentNode, this);
1286         this.box.repObject = this;
1287 
1288         // Appends the HTML targets dynamically. In case they are null, it breaks
1289         // click events.
1290         // xxxHonza: this problem would deserve clarification.
1291         if (this.cause.target || this.cause.relatedNode)
1292         {
1293             var targetsNode = this.box.querySelector(".targets");
1294             this.targets.replace(this.cause, targetsNode, this);
1295         }
1296 
1297         // Render "do not show again" text
1298         var descNode = this.box.querySelector(".noNotificationDesc");
1299         FirebugReps.Description.render(Locale.$STR("firebug.breakpoint.doNotShowBreakNotification2"),
1300             descNode, Obj.bind(this.onClickLink, this));
1301 
1302         // Tooltips
1303         if (this.cause.skipActionTooltip)
1304             this.box.querySelector(".skipButton").setAttribute("title", this.cause.skipActionTooltip);
1305         if (this.cause.okActionTooltip)
1306             this.box.querySelector(".okButton").setAttribute("title", this.cause.okActionTooltip);
1307         if (this.cause.copyActionTooltip)
1308             this.box.querySelector(".copyButton").setAttribute("title", this.cause.copyActionTooltip);
1309 
1310         // xxxHonza: disable the animation, the interval seems to be frozen during debugger break.
1311         this.box.style.top = "0";
1312         return;
1313 
1314         // Animation
1315         var self = this;
1316         var delta = Math.max(3, Math.floor(this.box.clientHeight/5));
1317         var clientHeight = this.box.clientHeight;
1318 
1319         this.box.style.top = -clientHeight + "px";
1320         var interval = setInterval(function slide(event)
1321         {
1322             var top = parseInt(self.box.style.top, 10);
1323             if (top >= 0)
1324             {
1325                 clearInterval(interval);
1326             }
1327             else
1328             {
1329                 var newTop = (top + delta) > 0 ? 0 : (top + delta);
1330                 self.box.style.top = newTop + "px";
1331             }
1332         }, 15);
1333 
1334         return this.box;
1335     },
1336 
1337     hide: function()
1338     {
1339         if (FBTrace.DBG_BP)
1340             FBTrace.sysout("breakNotification.hide;");
1341 
1342         // xxxHonza: disable the animation, the interval seems to be frozen during debugger break.
1343         if (this.box.parentNode)
1344             this.box.parentNode.removeChild(this.box);
1345         return;
1346 
1347         // Animation
1348         var self = this;
1349         var delta = Math.max(3, Math.floor(this.box.clientHeight/5));
1350         var clientHeight = this.box.clientHeight;
1351         var top = 0;
1352 
1353         var interval = setInterval(function slide(event)
1354         {
1355             top = top - delta;
1356             if (top < -clientHeight)
1357             {
1358                 clearInterval(interval);
1359 
1360                 if (self.box.parentNode)
1361                     self.box.parentNode.removeChild(self.box);
1362             }
1363             else
1364             {
1365                 self.box.style.top = top + "px";
1366             }
1367         }, 15);
1368     }
1369 })};
1370 
1371 // ********************************************************************************************* //
1372 // Registration
1373 
1374 Firebug.registerPanel(Firebug.Breakpoint.BreakpointsPanel);
1375 Firebug.registerRep(Firebug.Breakpoint.BreakpointRep);
1376 Firebug.registerModule(Firebug.Breakpoint);
1377 
1378 return Firebug.Breakpoint;
1379 
1380 // ********************************************************************************************* //
1381 });
1382