Merge inbound to m-c.
[gecko.git] / browser / components / customizableui / content / toolbar.xml
blob1e77847e8e6cb37cdbe1a057b0eefdeba27e6152
1 <?xml version="1.0"?>
2 <!-- This Source Code Form is subject to the terms of the Mozilla Public
3    - License, v. 2.0. If a copy of the MPL was not distributed with this
4    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
6 <bindings id="browserToolbarBindings"
7           xmlns="http://www.mozilla.org/xbl"
8           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
9           xmlns:xbl="http://www.mozilla.org/xbl">
11   <binding id="toolbar">
12     <resources>
13       <stylesheet src="chrome://global/skin/toolbar.css"/>
14     </resources>
15     <implementation implements="nsIAccessibleProvider">
16       <field name="overflowedDuringConstruction">null</field>
18       <property name="accessibleType" readonly="true">
19         <getter>
20           return Components.interfaces.nsIAccessibleProvider.XULToolbar;
21         </getter>
22       </property>
24       <constructor><![CDATA[
25           let scope = {};
26           Cu.import("resource:///modules/CustomizableUI.jsm", scope);
27           // Add an early overflow event listener that will mark if the
28           // toolbar overflowed during construction.
29           if (scope.CustomizableUI.isAreaOverflowable(this.id)) {
30             this.addEventListener("overflow", this);
31             this.addEventListener("underflow", this);
32           }
34           if (document.readyState == "complete") {
35             this._init();
36           } else {
37             // Need to wait until XUL overlays are loaded. See bug 554279.
38             let self = this;
39             document.addEventListener("readystatechange", function onReadyStateChange() {
40               if (document.readyState != "complete")
41                 return;
42               document.removeEventListener("readystatechange", onReadyStateChange, false);
43               self._init();
44             }, false);
45           }
46       ]]></constructor>
48       <method name="_init">
49         <body><![CDATA[
50           let scope = {};
51           Cu.import("resource:///modules/CustomizableUI.jsm", scope);
52           let CustomizableUI = scope.CustomizableUI;
54           // Searching for the toolbox palette in the toolbar binding because
55           // toolbars are constructed first.
56           let toolbox = this.toolbox;
57           if (toolbox && !toolbox.palette) {
58             for (let node of toolbox.children) {
59               if (node.localName == "toolbarpalette") {
60                 // Hold on to the palette but remove it from the document.
61                 toolbox.palette = node;
62                 toolbox.removeChild(node);
63                 break;
64               }
65             }
66           }
68           // pass the current set of children for comparison with placements:
69           let children = [node.id for (node of this.childNodes)
70                           if (node.getAttribute("skipintoolbarset") != "true" && node.id)];
71           CustomizableUI.registerToolbarNode(this, children);
72         ]]></body>
73       </method>
75       <method name="handleEvent">
76         <parameter name="aEvent"/>
77         <body><![CDATA[
78           if (aEvent.type == "overflow" && aEvent.detail > 0) {
79             if (this.overflowable && this.overflowable.initialized) {
80               this.overflowable.onOverflow(aEvent);
81             } else {
82               this.overflowedDuringConstruction = aEvent;
83             }
84           } else if (aEvent.type == "underflow" && aEvent.detail > 0) {
85             this.overflowedDuringConstruction = null;
86           }
87         ]]></body>
88       </method>
90       <method name="insertItem">
91         <parameter name="aId"/>
92         <parameter name="aBeforeElt"/>
93         <parameter name="aWrapper"/>
94         <body><![CDATA[
95           if (aWrapper) {
96             Cu.reportError("Can't insert " + aId + ": using insertItem " +
97                            "no longer supports wrapper elements.");
98             return null;
99           }
101           // Hack, the customizable UI code makes this be the last position
102           let pos = null;
103           if (aBeforeElt) {
104             let beforeInfo = CustomizableUI.getPlacementOfWidget(aBeforeElt.id);
105             if (beforeInfo.area != this.id) {
106               Cu.reportError("Can't insert " + aId + " before " +
107                              aBeforeElt.id + " which isn't in this area (" +
108                              this.id + ").");
109               return null;
110             }
111             pos = beforeInfo.position;
112           }
114           CustomizableUI.addWidgetToArea(aId, this.id, pos);
115           return this.ownerDocument.getElementById(aId);
116         ]]></body>
117       </method>
119       <property name="toolbarName"
120                 onget="return this.getAttribute('toolbarname');"
121                 onset="this.setAttribute('toolbarname', val); return val;"/>
123       <property name="customizationTarget" readonly="true">
124         <getter><![CDATA[
125           if (this._customizationTarget)
126             return this._customizationTarget;
128           let id = this.getAttribute("customizationtarget");
129           if (id)
130             this._customizationTarget = document.getElementById(id);
132           if (this._customizationTarget)
133             this._customizationTarget.insertItem = this.insertItem.bind(this);
134           else
135             this._customizationTarget = this;
137           return this._customizationTarget;
138         ]]></getter>
139       </property>
141       <property name="toolbox" readonly="true">
142         <getter><![CDATA[
143           if (this._toolbox)
144             return this._toolbox;
146           let toolboxId = this.getAttribute("toolboxid");
147           if (toolboxId) {
148             let toolbox = document.getElementById(toolboxId);
149             if (toolbox) {
150               if (toolbox.externalToolbars.indexOf(this) == -1)
151                 toolbox.externalToolbars.push(this);
153               this._toolbox = toolbox;
154             }
155           }
157           if (!this._toolbox && this.parentNode &&
158               this.parentNode.localName == "toolbox") {
159             this._toolbox = this.parentNode;
160           }
162           return this._toolbox;
163         ]]></getter>
164       </property>
166       <property name="currentSet">
167         <getter><![CDATA[
168           let currentWidgets = new Set();
169           for (let node of this.customizationTarget.children) {
170             let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
171             if (realNode.getAttribute("skipintoolbarset") != "true") {
172               currentWidgets.add(realNode.id);
173             }
174           }
175           if (this.getAttribute("overflowing") == "true") {
176             let overflowTarget = this.getAttribute("overflowtarget");
177             let overflowList = this.ownerDocument.getElementById(overflowTarget);
178             for (let node of overflowList.children) {
179               let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
180               if (realNode.getAttribute("skipintoolbarset") != "true") {
181                 currentWidgets.add(realNode.id);
182               }
183             }
184           }
185           let orderedPlacements = CustomizableUI.getWidgetIdsInArea(this.id);
186           return orderedPlacements.filter((x) => currentWidgets.has(x)).join(',');
187         ]]></getter>
188         <setter><![CDATA[
189           // Get list of new and old ids:
190           let newVal = (val || '').split(',').filter(x => x);
191           let oldIds = CustomizableUI.getWidgetIdsInArea(this.id);
193           // Get a list of items only in the new list
194           let newIds = [id for (id of newVal) if (oldIds.indexOf(id) == -1)];
195           CustomizableUI.beginBatchUpdate();
196           try {
197             for (let newId of newIds) {
198               oldIds = CustomizableUI.getWidgetIdsInArea(this.id);
199               let nextId = newId;
200               let pos;
201               do {
202                 // Get the next item
203                 nextId = newVal[newVal.indexOf(nextId) + 1];
204                 // Figure out where it is in the old list
205                 pos = oldIds.indexOf(nextId);
206                 // If it's not in the old list, repeat:
207               } while (pos == -1 && nextId);
208               if (pos == -1) {
209                 pos = null; // We didn't find anything, insert at the end
210               }
211               CustomizableUI.addWidgetToArea(newId, this.id, pos);
212             }
214             let currentIds = this.currentSet.split(',');
215             let removedIds = [id for (id of currentIds) if (newIds.indexOf(id) == -1 && newVal.indexOf(id) == -1)];
216             for (let removedId of removedIds) {
217               CustomizableUI.removeWidgetFromArea(removedId);
218             }
219           } finally {
220             CustomizableUI.endBatchUpdate();
221           }
222         ]]></setter>
223       </property>
226     </implementation>
227   </binding>
229   <binding id="toolbar-menubar-stub">
230     <implementation>
231       <property name="toolbox" readonly="true">
232         <getter><![CDATA[
233           if (this._toolbox)
234             return this._toolbox;
236           if (this.parentNode && this.parentNode.localName == "toolbox") {
237             this._toolbox = this.parentNode;
238           }
240           return this._toolbox;
241         ]]></getter>
242       </property>
243       <property name="currentSet" readonly="true">
244         <getter><![CDATA[
245           return this.getAttribute("defaultset");
246         ]]></getter>
247       </property>
248       <method name="insertItem">
249         <body><![CDATA[
250           return null;
251         ]]></body>
252       </method>
253     </implementation>
254   </binding>
256   <!-- The toolbar-menubar-autohide and toolbar-drag bindings are almost
257        verbatim copies of their toolkit counterparts - they just inherit from
258        the customizableui's toolbar binding instead of toolkit's. We're currently
259        OK with the maintainance burden of having two copies of a binding, since
260        the long term goal is to move the customization framework into toolkit. -->
262   <binding id="toolbar-menubar-autohide"
263            extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
264     <implementation>
265       <constructor>
266         this._setInactive();
267       </constructor>
268       <destructor>
269         this._setActive();
270       </destructor>
272       <field name="_inactiveTimeout">null</field>
274       <field name="_contextMenuListener"><![CDATA[({
275         toolbar: this,
276         contextMenu: null,
278         get active () !!this.contextMenu,
280         init: function (event) {
281           let node = event.target;
282           while (node != this.toolbar) {
283             if (node.localName == "menupopup")
284               return;
285             node = node.parentNode;
286           }
288           let contextMenuId = this.toolbar.getAttribute("context");
289           if (!contextMenuId)
290             return;
292           this.contextMenu = document.getElementById(contextMenuId);
293           if (!this.contextMenu)
294             return;
296           this.contextMenu.addEventListener("popupshown", this, false);
297           this.contextMenu.addEventListener("popuphiding", this, false);
298           this.toolbar.addEventListener("mousemove", this, false);
299         },
300         handleEvent: function (event) {
301           switch (event.type) {
302             case "popupshown":
303               this.toolbar.removeEventListener("mousemove", this, false);
304               break;
305             case "popuphiding":
306             case "mousemove":
307               this.toolbar._setInactiveAsync();
308               this.toolbar.removeEventListener("mousemove", this, false);
309               this.contextMenu.removeEventListener("popuphiding", this, false);
310               this.contextMenu.removeEventListener("popupshown", this, false);
311               this.contextMenu = null;
312               break;
313           }
314         }
315       })]]></field>
317       <method name="_setInactive">
318         <body><![CDATA[
319           this.setAttribute("inactive", "true");
320         ]]></body>
321       </method>
323       <method name="_setInactiveAsync">
324         <body><![CDATA[
325           this._inactiveTimeout = setTimeout(function (self) {
326             if (self.getAttribute("autohide") == "true") {
327               self._inactiveTimeout = null;
328               self._setInactive();
329             }
330           }, 0, this);
331         ]]></body>
332       </method>
334       <method name="_setActive">
335         <body><![CDATA[
336           if (this._inactiveTimeout) {
337             clearTimeout(this._inactiveTimeout);
338             this._inactiveTimeout = null;
339           }
340           this.removeAttribute("inactive");
341         ]]></body>
342       </method>
343     </implementation>
345     <handlers>
346       <handler event="DOMMenuBarActive"     action="this._setActive();"/>
347       <handler event="popupshowing"         action="this._setActive();"/>
348       <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/>
349       <handler event="DOMMenuBarInactive"><![CDATA[
350         if (!this._contextMenuListener.active)
351           this._setInactiveAsync();
352       ]]></handler>
353     </handlers>
354   </binding>
356   <binding id="toolbar-drag"
357            extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
358     <implementation>
359       <field name="_dragBindingAlive">true</field>
360       <constructor><![CDATA[
361         if (!this._draggableStarted) {
362           this._draggableStarted = true;
363           try {
364             let tmp = {};
365             Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
366             let draggableThis = new tmp.WindowDraggingElement(this);
367             draggableThis.mouseDownCheck = function(e) {
368               return this._dragBindingAlive;
369             };
370           } catch (e) {}
371         }
372       ]]></constructor>
373     </implementation>
374   </binding>
377 <!-- This is a peculiar binding. It is here to deal with overlayed/inserted add-on content,
378       and immediately direct such content elsewhere. -->
379   <binding id="addonbar-delegating">
380     <implementation>
381       <constructor><![CDATA[
382           // Reading these immediately so nobody messes with them anymore:
383           this._delegatingToolbar = this.getAttribute("toolbar-delegate");
384           this._wasCollapsed = this.getAttribute("collapsed");
385           // Leaving those in here to unbreak some code:
386           if (document.readyState == "complete") {
387             this._init();
388           } else {
389             // Need to wait until XUL overlays are loaded. See bug 554279.
390             let self = this;
391             document.addEventListener("readystatechange", function onReadyStateChange() {
392               if (document.readyState != "complete")
393                 return;
394               document.removeEventListener("readystatechange", onReadyStateChange, false);
395               self._init();
396             }, false);
397           }
398       ]]></constructor>
400       <method name="_init">
401         <body><![CDATA[
402           // Searching for the toolbox palette in the toolbar binding because
403           // toolbars are constructed first.
404           let toolbox = this.toolbox;
405           if (toolbox && !toolbox.palette) {
406             for (let node of toolbox.children) {
407               if (node.localName == "toolbarpalette") {
408                 // Hold on to the palette but remove it from the document.
409                 toolbox.palette = node;
410                 toolbox.removeChild(node);
411               }
412             }
413           }
415           // pass the current set of children for comparison with placements:
416           let children = [];
417           for (let node of this.childNodes) {
418             if (node.getAttribute("skipintoolbarset") != "true" && node.id) {
419               // Force everything to be removable so that buildArea can chuck stuff
420               // out if the user has customized things / we've been here before:
421               if (!this._whiteListed.has(node.id)) {
422                 node.setAttribute("removable", "true");
423               }
424               children.push(node);
425             }
426           }
427           CustomizableUI.registerToolbarNode(this, children);
428           let existingMigratedItems = (this.getAttribute("migratedset") || "").split(',');
429           for (let migratedItem of existingMigratedItems.filter((x) => !!x)) {
430             this._currentSetMigrated.add(migratedItem);
431           }
432           this.evictNodes();
433           // We can't easily use |this| or strong bindings for the observer fn here
434           // because that creates leaky circular references when the node goes away,
435           // and XBL destructors are unreliable.
436           let mutationObserver = new MutationObserver(function(mutations) {
437             if (!mutations.length) {
438               return;
439             }
440             let toolbar = mutations[0].target;
441             // Can't use our own attribute because we might not have one if we're set to
442             // collapsed
443             let areCustomizing = toolbar.ownerDocument.documentElement.getAttribute("customizing");
444             if (!toolbar._isModifying && !areCustomizing) {
445               toolbar.evictNodes();
446             }
447           });
448           mutationObserver.observe(this, {childList: true});
449         ]]></body>
450       </method>
451       <method name="evictNodes">
452         <body><![CDATA[
453           this._isModifying = true;
454           let i = this.childNodes.length;
455           while (i--) {
456             let node = this.childNodes[i];
457             if (this.childNodes[i].id) {
458               this.evictNode(this.childNodes[i]);
459             } else {
460               node.remove();
461             }
462           }
463           this._isModifying = false;
464           this._updateMigratedSet();
465         ]]></body>
466       </method>
467       <method name="evictNode">
468         <parameter name="aNode"/>
469         <body>
470         <![CDATA[
471           if (this._whiteListed.has(aNode.id) || CustomizableUI.isSpecialWidget(aNode.id)) {
472             return;
473           }
474           const kItemMaxWidth = 100;
475           let oldParent = aNode.parentNode;
476           aNode.setAttribute("removable", "true");
477           this._currentSetMigrated.add(aNode.id);
479           let movedOut = false;
480           if (!this._wasCollapsed) {
481             try {
482               let nodeWidth = aNode.getBoundingClientRect().width;
483               if (nodeWidth == 0 || nodeWidth > kItemMaxWidth) {
484                 throw new Error(aNode.id + " is too big (" + nodeWidth +
485                                 "px wide), moving to the palette");
486               }
487               CustomizableUI.addWidgetToArea(aNode.id, this._delegatingToolbar);
488               movedOut = true;
489             } catch (ex) {
490               // This will throw if the node is too big, or can't be moved there for
491               // some reason. Report this:
492               Cu.reportError(ex);
493             }
494           }
496           /* We won't have moved the widget if either the add-on bar was collapsed,
497            * or if it was too wide to be inserted into the navbar. */
498           if (!movedOut) {
499             try {
500               CustomizableUI.removeWidgetFromArea(aNode.id);
501             } catch (ex) {
502               Cu.reportError(ex);
503               aNode.remove();
504             }
505           }
507           // Surprise: addWidgetToArea(palette) will get you nothing if the palette
508           // is not constructed yet. Fix:
509           if (aNode.parentNode == oldParent) {
510             let palette = this.toolbox.palette;
511             if (palette && oldParent != palette) {
512               palette.appendChild(aNode);
513             }
514           }
515         ]]></body>
516       </method>
517       <method name="insertItem">
518         <parameter name="aId"/>
519         <parameter name="aBeforeElt"/>
520         <parameter name="aWrapper"/>
521         <body><![CDATA[
522           if (aWrapper) {
523             Cu.reportError("Can't insert " + aId + ": using insertItem " +
524                            "no longer supports wrapper elements.");
525             return null;
526           }
528           let widget = CustomizableUI.getWidget(aId);
529           widget = widget && widget.forWindow(window);
530           let node = widget && widget.node;
531           if (!node) {
532             return null;
533           }
535           this._isModifying = true;
536           // Temporarily add it here so it can have a width, then ditch it:
537           this.appendChild(node);
538           this.evictNode(node);
539           this._isModifying = false;
540           this._updateMigratedSet();
541           // We will now have moved stuff around; kick off an aftercustomization event
542           // so add-ons know we've just moved their stuff:
543           if (window.gCustomizeMode) {
544             window.gCustomizeMode.dispatchToolboxEvent("aftercustomization");
545           }
546           return node;
547         ]]></body>
548       </method>
549       <method name="getMigratedItems">
550         <body><![CDATA[
551           return [... this._currentSetMigrated];
552         ]]></body>
553       </method>
554       <method name="_updateMigratedSet">
555         <body><![CDATA[
556           let newMigratedItems = this.getMigratedItems().join(',');
557           if (this.getAttribute("migratedset") != newMigratedItems) {
558             this.setAttribute("migratedset", newMigratedItems);
559             this.ownerDocument.persist(this.id, "migratedset");
560           }
561         ]]></body>
562       </method>
563       <property name="customizationTarget" readonly="true">
564         <getter><![CDATA[
565           return this;
566         ]]></getter>
567       </property>
568       <property name="currentSet">
569         <getter><![CDATA[
570           return [node.id for (node of this.children)].join(',');
571         ]]></getter>
572         <setter><![CDATA[
573           let v = val.split(',');
574           let newButtons = v.filter(x => x && (!this._whiteListed.has(x) &&
575                                                !CustomizableUI.isSpecialWidget(x) &&
576                                                !this._currentSetMigrated.has(x)));
577           for (let newButton of newButtons) {
578             this._currentSetMigrated.add(newButton);
579             this.insertItem(newButton);
580           }
581           this._updateMigratedSet();
582         ]]></setter>
583       </property>
584       <property name="toolbox" readonly="true">
585         <getter><![CDATA[
586           if (!this._toolbox && this.parentNode &&
587               this.parentNode.localName == "toolbox") {
588             this._toolbox = this.parentNode;
589           }
591           return this._toolbox;
592         ]]></getter>
593       </property>
594       <field name="_whiteListed" readonly="true">new Set(["addonbar-closebutton", "status-bar"])</field>
595       <field name="_isModifying">false</field>
596       <field name="_currentSetMigrated">new Set()</field>
597     </implementation>
598   </binding>
599 </bindings>