1/*
2 * Copyright (c) 2011, Google 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 are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31#include "config.h"
32#include "PopupContainer.h"
33
34#include "PopupListBox.h"
35#include "core/dom/Document.h"
36#include "core/dom/UserGestureIndicator.h"
37#include "core/page/Chrome.h"
38#include "core/page/ChromeClient.h"
39#include "core/page/Frame.h"
40#include "core/page/FrameView.h"
41#include "core/page/Page.h"
42#include "core/platform/PlatformGestureEvent.h"
43#include "core/platform/PlatformKeyboardEvent.h"
44#include "core/platform/PlatformMouseEvent.h"
45#include "core/platform/PlatformScreen.h"
46#include "core/platform/PlatformTouchEvent.h"
47#include "core/platform/PlatformWheelEvent.h"
48#include "core/platform/PopupMenuClient.h"
49#include "core/platform/chromium/FramelessScrollView.h"
50#include "core/platform/chromium/FramelessScrollViewClient.h"
51#include "core/platform/graphics/GraphicsContext.h"
52#include "core/platform/graphics/IntRect.h"
53#include <limits>
54
55namespace WebCore {
56
57static const int borderSize = 1;
58
59static PlatformMouseEvent constructRelativeMouseEvent(const PlatformMouseEvent& e, FramelessScrollView* parent, FramelessScrollView* child)
60{
61    IntPoint pos = parent->convertSelfToChild(child, e.position());
62
63    // FIXME: This is a horrible hack since PlatformMouseEvent has no setters for x/y.
64    PlatformMouseEvent relativeEvent = e;
65    IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.position());
66    relativePos.setX(pos.x());
67    relativePos.setY(pos.y());
68    return relativeEvent;
69}
70
71static PlatformWheelEvent constructRelativeWheelEvent(const PlatformWheelEvent& e, FramelessScrollView* parent, FramelessScrollView* child)
72{
73    IntPoint pos = parent->convertSelfToChild(child, e.position());
74
75    // FIXME: This is a horrible hack since PlatformWheelEvent has no setters for x/y.
76    PlatformWheelEvent relativeEvent = e;
77    IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.position());
78    relativePos.setX(pos.x());
79    relativePos.setY(pos.y());
80    return relativeEvent;
81}
82
83// static
84PassRefPtr<PopupContainer> PopupContainer::create(PopupMenuClient* client, PopupType popupType, const PopupContainerSettings& settings)
85{
86    return adoptRef(new PopupContainer(client, popupType, settings));
87}
88
89PopupContainer::PopupContainer(PopupMenuClient* client, PopupType popupType, const PopupContainerSettings& settings)
90    : m_listBox(PopupListBox::create(client, settings))
91    , m_settings(settings)
92    , m_popupType(popupType)
93    , m_popupOpen(false)
94{
95    setScrollbarModes(ScrollbarAlwaysOff, ScrollbarAlwaysOff);
96}
97
98PopupContainer::~PopupContainer()
99{
100    if (m_listBox && m_listBox->parent())
101        removeChild(m_listBox.get());
102}
103
104IntRect PopupContainer::layoutAndCalculateWidgetRectInternal(IntRect widgetRectInScreen, int targetControlHeight, const FloatRect& windowRect, const FloatRect& screen, bool isRTL, const int rtlOffset, const int verticalOffset, const IntSize& transformOffset, PopupContent* listBox, bool& needToResizeView)
105{
106    ASSERT(listBox);
107    if (windowRect.x() >= screen.x() && windowRect.maxX() <= screen.maxX() && (widgetRectInScreen.x() < screen.x() || widgetRectInScreen.maxX() > screen.maxX())) {
108        // First, inverse the popup alignment if it does not fit the screen -
109        // this might fix things (or make them better).
110        IntRect inverseWidgetRectInScreen = widgetRectInScreen;
111        inverseWidgetRectInScreen.setX(inverseWidgetRectInScreen.x() + (isRTL ? -rtlOffset : rtlOffset));
112        inverseWidgetRectInScreen.setY(inverseWidgetRectInScreen.y() + (isRTL ? -verticalOffset : verticalOffset));
113        IntRect enclosingScreen = enclosingIntRect(screen);
114        unsigned originalCutoff = std::max(enclosingScreen.x() - widgetRectInScreen.x(), 0) + std::max(widgetRectInScreen.maxX() - enclosingScreen.maxX(), 0);
115        unsigned inverseCutoff = std::max(enclosingScreen.x() - inverseWidgetRectInScreen.x(), 0) + std::max(inverseWidgetRectInScreen.maxX() - enclosingScreen.maxX(), 0);
116
117        // Accept the inverse popup alignment if the trimmed content gets
118        // shorter than that in the original alignment case.
119        if (inverseCutoff < originalCutoff)
120            widgetRectInScreen = inverseWidgetRectInScreen;
121
122        if (widgetRectInScreen.x() < screen.x()) {
123            widgetRectInScreen.setWidth(widgetRectInScreen.maxX() - screen.x());
124            widgetRectInScreen.setX(screen.x());
125            listBox->setMaxWidthAndLayout(std::max(widgetRectInScreen.width() - borderSize * 2, 0));
126        } else if (widgetRectInScreen.maxX() > screen.maxX()) {
127            widgetRectInScreen.setWidth(screen.maxX() - widgetRectInScreen.x());
128            listBox->setMaxWidthAndLayout(std::max(widgetRectInScreen.width() - borderSize * 2, 0));
129        }
130    }
131
132    // Calculate Y axis size.
133    if (widgetRectInScreen.maxY() > static_cast<int>(screen.maxY())) {
134        if (widgetRectInScreen.y() - widgetRectInScreen.height() - targetControlHeight - transformOffset.height() > 0) {
135            // There is enough room to open upwards.
136            widgetRectInScreen.move(-transformOffset.width(), -(widgetRectInScreen.height() + targetControlHeight + transformOffset.height()));
137        } else {
138            // Figure whether upwards or downwards has more room and set the
139            // maximum number of items.
140            int spaceAbove = widgetRectInScreen.y() - targetControlHeight + transformOffset.height();
141            int spaceBelow = screen.maxY() - widgetRectInScreen.y();
142            if (spaceAbove > spaceBelow)
143                listBox->setMaxHeight(spaceAbove);
144            else
145                listBox->setMaxHeight(spaceBelow);
146            listBox->layout();
147            needToResizeView = true;
148            widgetRectInScreen.setHeight(listBox->popupContentHeight() + borderSize * 2);
149            // Move WebWidget upwards if necessary.
150            if (spaceAbove > spaceBelow)
151                widgetRectInScreen.move(-transformOffset.width(), -(widgetRectInScreen.height() + targetControlHeight + transformOffset.height()));
152        }
153    }
154    return widgetRectInScreen;
155}
156
157IntRect PopupContainer::layoutAndCalculateWidgetRect(int targetControlHeight, const IntSize& transformOffset, const IntPoint& popupInitialCoordinate)
158{
159    // Reset the max width and height to their default values, they will be
160    // recomputed below if necessary.
161    m_listBox->setMaxHeight(PopupListBox::defaultMaxHeight);
162    m_listBox->setMaxWidth(std::numeric_limits<int>::max());
163
164    // Lay everything out to figure out our preferred size, then tell the view's
165    // WidgetClient about it. It should assign us a client.
166    m_listBox->layout();
167    fitToListBox();
168    bool isRTL = this->isRTL();
169
170    // Compute the starting x-axis for a normal RTL or right-aligned LTR
171    // dropdown. For those, the right edge of dropdown box should be aligned
172    // with the right edge of <select>/<input> element box, and the dropdown box
173    // should be expanded to the left if more space is needed.
174    // m_originalFrameRect.width() is the width of the target <select>/<input>
175    // element.
176    int rtlOffset = m_controlPosition.p2().x() - m_controlPosition.p1().x() - (m_listBox->width() + borderSize * 2);
177    int rightOffset = isRTL ? rtlOffset : 0;
178
179    // Compute the y-axis offset between the bottom left and bottom right
180    // points. If the <select>/<input> is transformed, they are not the same.
181    int verticalOffset = - m_controlPosition.p4().y() + m_controlPosition.p3().y();
182    int verticalForRTLOffset = isRTL ? verticalOffset : 0;
183
184    // Assume m_listBox size is already calculated.
185    IntSize targetSize(m_listBox->width() + borderSize * 2, m_listBox->height() + borderSize * 2);
186
187    IntRect widgetRectInScreen;
188    if (ChromeClient* client = chromeClient()) {
189        // If the popup would extend past the bottom of the screen, open upwards
190        // instead.
191        FloatRect screen = screenAvailableRect(m_frameView.get());
192        // Use popupInitialCoordinate.x() + rightOffset because RTL position
193        // needs to be considered.
194        widgetRectInScreen = client->rootViewToScreen(IntRect(popupInitialCoordinate.x() + rightOffset, popupInitialCoordinate.y() + verticalForRTLOffset, targetSize.width(), targetSize.height()));
195
196        // If we have multiple screens and the browser rect is in one screen, we
197        // have to clip the window width to the screen width.
198        // When clipping, we also need to set a maximum width for the list box.
199        FloatRect windowRect = client->windowRect();
200
201        bool needToResizeView = false;
202        widgetRectInScreen = layoutAndCalculateWidgetRectInternal(widgetRectInScreen, targetControlHeight, windowRect, screen, isRTL, rtlOffset, verticalOffset, transformOffset, m_listBox.get(), needToResizeView);
203        if (needToResizeView)
204            fitToListBox();
205    }
206
207    return widgetRectInScreen;
208}
209
210void PopupContainer::showPopup(FrameView* view)
211{
212    m_frameView = view;
213    listBox()->m_focusedElement = m_frameView->frame()->document()->focusedElement();
214
215    if (ChromeClient* client = chromeClient()) {
216        IntSize transformOffset(m_controlPosition.p4().x() - m_controlPosition.p1().x(), m_controlPosition.p4().y() - m_controlPosition.p1().y() - m_controlSize.height());
217        client->popupOpened(this, layoutAndCalculateWidgetRect(m_controlSize.height(), transformOffset, roundedIntPoint(m_controlPosition.p4())), false);
218        m_popupOpen = true;
219    }
220
221    if (!m_listBox->parent())
222        addChild(m_listBox.get());
223
224    // Enable scrollbars after the listbox is inserted into the hierarchy,
225    // so it has a proper WidgetClient.
226    m_listBox->setVerticalScrollbarMode(ScrollbarAuto);
227
228    m_listBox->scrollToRevealSelection();
229
230    invalidate();
231}
232
233void PopupContainer::hidePopup()
234{
235    listBox()->hidePopup();
236}
237
238void PopupContainer::notifyPopupHidden()
239{
240    if (!m_popupOpen)
241        return;
242    m_popupOpen = false;
243    chromeClient()->popupClosed(this);
244}
245
246void PopupContainer::fitToListBox()
247{
248    // Place the listbox within our border.
249    m_listBox->move(borderSize, borderSize);
250
251    // Size ourselves to contain listbox + border.
252    resize(m_listBox->width() + borderSize * 2, m_listBox->height() + borderSize * 2);
253    invalidate();
254}
255
256bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event)
257{
258    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
259    return m_listBox->handleMouseDownEvent(
260        constructRelativeMouseEvent(event, this, m_listBox.get()));
261}
262
263bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event)
264{
265    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
266    return m_listBox->handleMouseMoveEvent(
267        constructRelativeMouseEvent(event, this, m_listBox.get()));
268}
269
270bool PopupContainer::handleMouseReleaseEvent(const PlatformMouseEvent& event)
271{
272    RefPtr<PopupContainer> protect(this);
273    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
274    return m_listBox->handleMouseReleaseEvent(
275        constructRelativeMouseEvent(event, this, m_listBox.get()));
276}
277
278bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event)
279{
280    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
281    return m_listBox->handleWheelEvent(
282        constructRelativeWheelEvent(event, this, m_listBox.get()));
283}
284
285bool PopupContainer::handleTouchEvent(const PlatformTouchEvent&)
286{
287    return false;
288}
289
290// FIXME: Refactor this code to share functionality with
291// EventHandler::handleGestureEvent.
292bool PopupContainer::handleGestureEvent(const PlatformGestureEvent& gestureEvent)
293{
294    switch (gestureEvent.type()) {
295    case PlatformEvent::GestureTap: {
296        PlatformMouseEvent fakeMouseMove(gestureEvent.position(), gestureEvent.globalPosition(), NoButton, PlatformEvent::MouseMoved, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
297        PlatformMouseEvent fakeMouseDown(gestureEvent.position(), gestureEvent.globalPosition(), LeftButton, PlatformEvent::MousePressed, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
298        PlatformMouseEvent fakeMouseUp(gestureEvent.position(), gestureEvent.globalPosition(), LeftButton, PlatformEvent::MouseReleased, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
299        // handleMouseMoveEvent(fakeMouseMove);
300        handleMouseDownEvent(fakeMouseDown);
301        handleMouseReleaseEvent(fakeMouseUp);
302        return true;
303    }
304    case PlatformEvent::GestureScrollUpdate:
305    case PlatformEvent::GestureScrollUpdateWithoutPropagation: {
306        PlatformWheelEvent syntheticWheelEvent(gestureEvent.position(), gestureEvent.globalPosition(), gestureEvent.deltaX(), gestureEvent.deltaY(), gestureEvent.deltaX() / 120.0f, gestureEvent.deltaY() / 120.0f, ScrollByPixelWheelEvent, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey());
307        handleWheelEvent(syntheticWheelEvent);
308        return true;
309    }
310    case PlatformEvent::GestureScrollBegin:
311    case PlatformEvent::GestureScrollEnd:
312    case PlatformEvent::GestureTapDown:
313        break;
314    default:
315        ASSERT_NOT_REACHED();
316    }
317    return false;
318}
319
320bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event)
321{
322    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
323    return m_listBox->handleKeyEvent(event);
324}
325
326void PopupContainer::hide()
327{
328    m_listBox->abandon();
329}
330
331void PopupContainer::paint(GraphicsContext* gc, const IntRect& rect)
332{
333    // Adjust coords for scrolled frame.
334    IntRect r = intersection(rect, frameRect());
335    int tx = x();
336    int ty = y();
337
338    r.move(-tx, -ty);
339
340    gc->translate(static_cast<float>(tx), static_cast<float>(ty));
341    m_listBox->paint(gc, r);
342    gc->translate(-static_cast<float>(tx), -static_cast<float>(ty));
343
344    paintBorder(gc, rect);
345}
346
347void PopupContainer::paintBorder(GraphicsContext* gc, const IntRect& rect)
348{
349    // FIXME: Where do we get the border color from?
350    Color borderColor(127, 157, 185);
351
352    gc->setStrokeStyle(NoStroke);
353    gc->setFillColor(borderColor);
354
355    int tx = x();
356    int ty = y();
357
358    // top, left, bottom, right
359    gc->drawRect(IntRect(tx, ty, width(), borderSize));
360    gc->drawRect(IntRect(tx, ty, borderSize, height()));
361    gc->drawRect(IntRect(tx, ty + height() - borderSize, width(), borderSize));
362    gc->drawRect(IntRect(tx + width() - borderSize, ty, borderSize, height()));
363}
364
365bool PopupContainer::isInterestedInEventForKey(int keyCode)
366{
367    return m_listBox->isInterestedInEventForKey(keyCode);
368}
369
370ChromeClient* PopupContainer::chromeClient()
371{
372    return m_frameView->frame()->page()->chrome().client();
373}
374
375void PopupContainer::showInRect(const FloatQuad& controlPosition, const IntSize& controlSize, FrameView* v, int index)
376{
377    // The controlSize is the size of the select box. It's usually larger than
378    // we need. Subtract border size so that usually the container will be
379    // displayed exactly the same width as the select box.
380    listBox()->setBaseWidth(max(controlSize.width() - borderSize * 2, 0));
381
382    listBox()->updateFromElement();
383
384    // We set the selected item in updateFromElement(), and disregard the
385    // index passed into this function (same as Webkit's PopupMenuWin.cpp)
386    // FIXME: make sure this is correct, and add an assertion.
387    // ASSERT(popupWindow(popup)->listBox()->selectedIndex() == index);
388
389    // Save and convert the controlPosition to main window coords. Each point is converted separately
390    // to window coordinates because the control could be in a transformed webview and then each point
391    // would be transformed by a different delta.
392    m_controlPosition.setP1(v->contentsToWindow(IntPoint(controlPosition.p1().x(), controlPosition.p1().y())));
393    m_controlPosition.setP2(v->contentsToWindow(IntPoint(controlPosition.p2().x(), controlPosition.p2().y())));
394    m_controlPosition.setP3(v->contentsToWindow(IntPoint(controlPosition.p3().x(), controlPosition.p3().y())));
395    m_controlPosition.setP4(v->contentsToWindow(IntPoint(controlPosition.p4().x(), controlPosition.p4().y())));
396
397    m_controlSize = controlSize;
398
399    // Position at (0, 0) since the frameRect().location() is relative to the
400    // parent WebWidget.
401    setFrameRect(IntRect(IntPoint(), controlSize));
402    showPopup(v);
403}
404
405IntRect PopupContainer::refresh(const IntRect& targetControlRect)
406{
407    listBox()->setBaseWidth(max(m_controlSize.width() - borderSize * 2, 0));
408    listBox()->updateFromElement();
409
410    IntPoint locationInWindow = m_frameView->contentsToWindow(targetControlRect.location());
411
412    // Move it below the select widget.
413    locationInWindow.move(0, targetControlRect.height());
414
415    IntRect widgetRectInScreen = layoutAndCalculateWidgetRect(targetControlRect.height(), IntSize(), locationInWindow);
416
417    // Reset the size (which can be set to the PopupListBox size in
418    // layoutAndGetRTLOffset(), exceeding the available widget rectangle.)
419    if (size() != widgetRectInScreen.size())
420        resize(widgetRectInScreen.size());
421
422    invalidate();
423
424    return widgetRectInScreen;
425}
426
427inline bool PopupContainer::isRTL() const
428{
429    return m_listBox->m_popupClient->menuStyle().textDirection() == RTL;
430}
431
432int PopupContainer::selectedIndex() const
433{
434    return m_listBox->selectedIndex();
435}
436
437int PopupContainer::menuItemHeight() const
438{
439    return m_listBox->getRowHeight(0);
440}
441
442int PopupContainer::menuItemFontSize() const
443{
444    return m_listBox->getRowFont(0).size();
445}
446
447PopupMenuStyle PopupContainer::menuStyle() const
448{
449    return m_listBox->m_popupClient->menuStyle();
450}
451
452const WTF::Vector<PopupItem*>& PopupContainer:: popupData() const
453{
454    return m_listBox->items();
455}
456
457String PopupContainer::getSelectedItemToolTip()
458{
459    // We cannot use m_popupClient->selectedIndex() to choose tooltip message,
460    // because the selectedIndex() might return final selected index, not
461    // hovering selection.
462    return listBox()->m_popupClient->itemToolTip(listBox()->m_selectedIndex);
463}
464
465} // namespace WebCore
466