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.HoverMenu = function(delegate)
27{
28    WebInspector.Object.call(this);
29
30    this.delegate = delegate;
31
32    this._element = document.createElement("div");
33    this._element.className = WebInspector.HoverMenu.StyleClassName;
34    this._element.addEventListener("transitionend", this, true);
35
36    this._outlineElement = this._element.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg"));
37
38    this._button = this._element.appendChild(document.createElement("img"));
39    this._button.addEventListener("click", this);
40}
41
42WebInspector.HoverMenu.StyleClassName = "hover-menu";
43WebInspector.HoverMenu.VisibleClassName = "visible";
44
45WebInspector.HoverMenu.prototype = {
46    constructor: WebInspector.HoverMenu,
47    __proto__: WebInspector.Object.prototype,
48
49    // Public
50
51    get element()
52    {
53        return this._element;
54    },
55
56    present: function(rects)
57    {
58        this._outlineElement.textContent = "";
59
60        document.body.appendChild(this._element);
61        this._drawOutline(rects);
62        this._element.classList.add(WebInspector.HoverMenu.VisibleClassName);
63
64        window.addEventListener("scroll", this, true);
65    },
66
67    dismiss: function(discrete)
68    {
69        if (this._element.parentNode !== document.body)
70            return;
71
72        if (discrete)
73            this._element.remove();
74
75        this._element.classList.remove(WebInspector.HoverMenu.VisibleClassName);
76
77        window.removeEventListener("scroll", this, true);
78    },
79
80    // Protected
81
82    handleEvent: function(event)
83    {
84        switch (event.type) {
85        case "scroll":
86            if (!this._element.contains(event.target))
87                this.dismiss(true);
88            break;
89        case "click":
90            this._handleClickEvent(event);
91            break;
92        case "transitionend":
93            if (!this._element.classList.contains(WebInspector.HoverMenu.VisibleClassName))
94                this._element.remove();
95            break;
96        }
97    },
98
99    // Private
100
101    _handleClickEvent: function(event)
102    {
103        if (this.delegate && typeof this.delegate.hoverMenuButtonWasPressed === "function")
104            this.delegate.hoverMenuButtonWasPressed(this);
105    },
106
107    _drawOutline: function(rects)
108    {
109        var buttonWidth = this._button.width;
110        var buttonHeight = this._button.height;
111
112        // Add room for the button on the last line.
113        var lastRect = rects.pop();
114        lastRect.size.width += buttonWidth;
115        rects.push(lastRect);
116
117        if (rects.length === 1)
118            this._drawSingleLine(rects[0]);
119        else if (rects.length === 2 && rects[0].minX() >= rects[1].maxX())
120            this._drawTwoNonOverlappingLines(rects);
121        else
122            this._drawOverlappingLines(rects);
123
124        var bounds = WebInspector.Rect.unionOfRects(rects).pad(3); // padding + 1/2 stroke-width
125
126        var style = this._element.style;
127        style.left = bounds.minX() + "px";
128        style.top = bounds.minY() + "px";
129        style.width = bounds.size.width + "px";
130        style.height = bounds.size.height + "px";
131
132        this._outlineElement.style.width = bounds.size.width + "px";
133        this._outlineElement.style.height = bounds.size.height + "px";
134
135        this._button.style.left = (lastRect.maxX() - bounds.minX() - buttonWidth) + "px";
136        this._button.style.top = (lastRect.maxY() - bounds.minY() - buttonHeight) + "px";
137    },
138
139    _addRect: function(rect)
140    {
141        const r = 4;
142
143        var svgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
144        svgRect.setAttribute("x", 1);
145        svgRect.setAttribute("y", 1);
146        svgRect.setAttribute("width", rect.size.width);
147        svgRect.setAttribute("height", rect.size.height);
148        svgRect.setAttribute("rx", r);
149        svgRect.setAttribute("ry", r);
150        return this._outlineElement.appendChild(svgRect);
151    },
152
153    _addPath: function(commands, tx, ty)
154    {
155        var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
156        path.setAttribute("d", commands.join(" "));
157        path.setAttribute("transform", "translate(" + (tx + 1) + "," + (ty + 1) + ")");
158        return this._outlineElement.appendChild(path);
159    },
160
161    _drawSingleLine: function(rect)
162    {
163        this._addRect(rect.pad(2));
164    },
165
166    _drawTwoNonOverlappingLines: function(rects)
167    {
168        const r = 4;
169
170        var firstRect = rects[0].pad(2);
171        var secondRect = rects[1].pad(2);
172
173        var tx = -secondRect.minX();
174        var ty = -firstRect.minY();
175
176        var rect = firstRect;
177        this._addPath([
178            "M", rect.maxX(), rect.minY(),
179            "H", rect.minX() + r,
180            "q", -r, 0, -r, r,
181            "V", rect.maxY() - r,
182            "q", 0, r, r, r,
183            "H", rect.maxX()
184        ], tx, ty);
185
186        rect = secondRect;
187        this._addPath([
188            "M", rect.minX(), rect.minY(),
189            "H", rect.maxX() - r,
190            "q", r, 0, r, r,
191            "V", rect.maxY() - r,
192            "q", 0, r, -r, r,
193            "H", rect.minX()
194        ], tx, ty);
195    },
196
197    _drawOverlappingLines: function(rects)
198    {
199        const PADDING = 2;
200        const r = 4;
201
202        var minX = Number.MAX_VALUE;
203        var maxX = -Number.MAX_VALUE;
204        for (var rect of rects) {
205            var minX = Math.min(rect.minX(), minX);
206            var maxX = Math.max(rect.maxX(), maxX);
207        }
208
209        minX -= PADDING;
210        maxX += PADDING;
211
212        var minY = rects[0].minY() - PADDING;
213        var maxY = rects.lastValue.maxY() + PADDING;
214        var firstLineMinX = rects[0].minX() - PADDING;
215        var lastLineMaxX = rects.lastValue.maxX() + PADDING;
216
217        if (firstLineMinX === minX && lastLineMaxX === maxX)
218            return this._addRect(new WebInspector.Rect(minX, minY, maxX - minX, maxY - minY));
219
220        var lastLineMinY = rects.lastValue.minY() + PADDING;
221        if (rects[0].minX() === minX + PADDING)
222            return this._addPath([
223                "M", minX + r, minY,
224                "H", maxX - r,
225                "q", r, 0, r, r,
226                "V", lastLineMinY - r,
227                "q", 0, r, -r, r,
228                "H", lastLineMaxX + r,
229                "q", -r, 0, -r, r,
230                "V", maxY - r,
231                "q", 0, r, -r, r,
232                "H", minX + r,
233                "q", -r, 0, -r, -r,
234                "V", minY + r,
235                "q", 0, -r, r, -r
236            ], -minX, -minY);
237
238        var firstLineMaxY = rects[0].maxY() - PADDING;
239        if (rects.lastValue.maxX() === maxX - PADDING)
240            return this._addPath([
241                "M", firstLineMinX + r, minY,
242                "H", maxX - r,
243                "q", r, 0, r, r,
244                "V", maxY - r,
245                "q", 0, r, -r, r,
246                "H", minX + r,
247                "q", -r, 0, -r, -r,
248                "V", firstLineMaxY + r,
249                "q", 0, -r, r, -r,
250                "H", firstLineMinX - r,
251                "q", r, 0, r, -r,
252                "V", minY + r,
253                "q", 0, -r, r, -r
254            ], -minX, -minY);
255
256        return this._addPath([
257            "M", firstLineMinX + r, minY,
258            "H", maxX - r,
259            "q", r, 0, r, r,
260            "V", lastLineMinY - r,
261            "q", 0, r, -r, r,
262            "H", lastLineMaxX + r,
263            "q", -r, 0, -r, r,
264            "V", maxY - r,
265            "q", 0, r, -r, r,
266            "H", minX + r,
267            "q", -r, 0, -r, -r,
268            "V", firstLineMaxY + r,
269            "q", 0, -r, r, -r,
270            "H", firstLineMinX - r,
271            "q", r, 0, r, -r,
272            "V", minY + r,
273            "q", 0, -r, r, -r
274        ], -minX, -minY);
275    }
276}
277