1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.HierarchicalPathNavigationItem = function(identifier, components) {
27    WebInspector.NavigationItem.call(this, identifier);
28
29    this.components = components;
30};
31
32WebInspector.HierarchicalPathNavigationItem.StyleClassName = "hierarchical-path";
33WebInspector.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName = "always-show-last-path-component-separator";
34
35WebInspector.HierarchicalPathNavigationItem.Event = {
36    PathComponentWasSelected: "hierarchical-path-navigation-item-path-component-was-selected"
37};
38
39WebInspector.HierarchicalPathNavigationItem.prototype = {
40    constructor: WebInspector.HierarchicalPathNavigationItem,
41
42    // Public
43
44    get components()
45    {
46        return this._components;
47    },
48
49    set components(newComponents)
50    {
51        if (!newComponents)
52            newComponents = [];
53
54        for (var i = 0; this._components && i < this._components.length; ++i)
55            this._components[i].removeEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._siblingPathComponentWasSelected, this);
56
57        // Make a shallow copy of the newComponents array using slice.
58        this._components = newComponents.slice(0);
59
60        for (var i = 0; i < this._components.length; ++i)
61            this._components[i].addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._siblingPathComponentWasSelected, this);
62
63        this.element.removeChildren();
64        delete this._collapsedComponent;
65
66        for (var i = 0; i < newComponents.length; ++i)
67            this.element.appendChild(newComponents[i].element);
68
69        // Update layout for the so other items can adjust to the extra space (or lack thereof) too.
70        if (this.parentNavigationBar)
71            this.parentNavigationBar.updateLayoutSoon();
72    },
73
74    get lastComponent()
75    {
76        return this._components.lastValue || null;
77    },
78
79    get alwaysShowLastPathComponentSeparator()
80    {
81        return this.element.classList.contains(WebInspector.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName);
82    },
83
84    set alwaysShowLastPathComponentSeparator(flag)
85    {
86        if (flag)
87            this.element.classList.add(WebInspector.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName);
88        else
89            this.element.classList.remove(WebInspector.HierarchicalPathNavigationItem.AlwaysShowLastPathComponentSeparatorStyleClassName);
90    },
91
92    updateLayout: function(expandOnly)
93    {
94        var navigationBar = this.parentNavigationBar;
95        if (!navigationBar)
96            return;
97
98        if (this._collapsedComponent) {
99            this.element.removeChild(this._collapsedComponent.element);
100            delete this._collapsedComponent;
101        }
102
103        // Expand our components to full width to test if the items can fit at full width.
104        for (var i = 0; i < this._components.length; ++i) {
105            this._components[i].hidden = false;
106            this._components[i].forcedWidth = null;
107        }
108
109        if (expandOnly)
110            return;
111
112        if (navigationBar.sizesToFit)
113            return;
114
115        // Iterate over all the other navigation items in the bar and calculate their width.
116        var totalOtherItemsWidth = 0;
117        for (var i = 0; i < navigationBar.navigationItems.length; ++i) {
118            // Skip ourself.
119            if (navigationBar.navigationItems[i] === this)
120                continue;
121
122            // Skip flexible space items since they can take up no space at the minimum width.
123            if (navigationBar.navigationItems[i] instanceof WebInspector.FlexibleSpaceNavigationItem)
124                continue;
125
126            totalOtherItemsWidth += navigationBar.navigationItems[i].element.offsetWidth;
127        }
128
129        // Calculate the width for all the components.
130        var thisItemWidth = 0;
131        var componentWidths = [];
132        for (var i = 0; i < this._components.length; ++i) {
133            var componentWidth = this._components[i].element.offsetWidth;
134            componentWidths.push(componentWidth);
135            thisItemWidth += componentWidth;
136        }
137
138        // If all our components fit with the other navigation items in the width of the bar,
139        // then we don't need to collapse any components.
140        var barWidth = navigationBar.element.offsetWidth;
141        if (totalOtherItemsWidth + thisItemWidth <= barWidth)
142            return;
143
144        // Calculate the width we need to remove from our components, then iterate over them
145        // and force their width to be smaller.
146        var widthToRemove = totalOtherItemsWidth + thisItemWidth - barWidth;
147        for (var i = 0; i < this._components.length; ++i) {
148            var componentWidth = componentWidths[i];
149
150            // Try to take the whole width we need to remove from each component.
151            var forcedWidth = componentWidth - widthToRemove;
152            this._components[i].forcedWidth = forcedWidth;
153
154            // Since components have a minimum width, we need to see how much was actually
155            // removed and subtract that from what remans to be removed.
156            componentWidths[i] = Math.max(this._components[i].minimumWidth, forcedWidth);
157            widthToRemove -= (componentWidth - componentWidths[i]);
158
159            // If there is nothing else to remove, then we can stop.
160            if (widthToRemove <= 0)
161                break;
162        }
163
164        // If there is nothing else to remove, then we can stop.
165        if (widthToRemove <= 0)
166            return;
167
168        // If there are 3 or fewer components, then we can stop. Collapsing the middle of 3 components
169        // does not save more than a few pixels over just the icon, so it isn't worth it unless there
170        // are 4 or more components.
171        if (this._components.length <= 3)
172            return;
173
174        // We want to collapse the middle components, so find the nearest middle index.
175        var middle = this._components.length >> 1;
176        var distance = -1;
177        var i = middle;
178
179        // Create a component that will represent the hidden components with a ellipse as the display name.
180        this._collapsedComponent = new WebInspector.HierarchicalPathComponent("\u2026", []);
181        this._collapsedComponent.collapsed = true;
182
183        // Insert it in the middle, it doesn't matter exactly where since the elements around it will be hidden soon.
184        this.element.insertBefore(this._collapsedComponent.element, this._components[middle].element);
185
186        // Add the width of the collapsed component to the width we need to remove.
187        widthToRemove += this._collapsedComponent.minimumWidth;
188
189        var hiddenDisplayNames = [];
190
191        // Loop through the components starting at the middle and fanning out in each direction.
192        while (i >= 0 && i <= this._components.length - 1) {
193            // Only hide components in the middle and never the ends.
194            if (i > 0 && i < this._components.length - 1) {
195                var component = this._components[i];
196                component.hidden = true;
197
198                // Remember the displayName so it can be put in the tool tip of the collapsed component.
199                if (distance > 0)
200                    hiddenDisplayNames.unshift(component.displayName);
201                else
202                    hiddenDisplayNames.push(component.displayName);
203
204                // Fully subtract the hidden component's width.
205                widthToRemove -= componentWidths[i];
206
207                // If there is nothing else to remove, then we can stop.
208                if (widthToRemove <= 0)
209                    break;
210            }
211
212            // Calculate the next index.
213            i = middle + distance;
214
215            // Increment the distance when it is in the positive direction.
216            if (distance > 0)
217                ++distance;
218
219            // Flip the direction of the distance.
220            distance *= -1;
221        }
222
223        // Set the tool tip of the collapsed component.
224        this._collapsedComponent.element.title = hiddenDisplayNames.join("\n");
225    },
226
227    // Private
228
229    _additionalClassNames: [WebInspector.HierarchicalPathNavigationItem.StyleClassName],
230
231    _siblingPathComponentWasSelected: function(event)
232    {
233        this.dispatchEventToListeners(WebInspector.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, event.data);
234    }
235};
236
237WebInspector.HierarchicalPathNavigationItem.prototype.__proto__ = WebInspector.NavigationItem.prototype;
238