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 "web/PopupContainer.h"
33
34#include "core/dom/Document.h"
35#include "core/frame/FrameView.h"
36#include "core/frame/LocalFrame.h"
37#include "core/page/Chrome.h"
38#include "core/page/ChromeClient.h"
39#include "core/page/Page.h"
40#include "platform/PlatformGestureEvent.h"
41#include "platform/PlatformKeyboardEvent.h"
42#include "platform/PlatformMouseEvent.h"
43#include "platform/PlatformScreen.h"
44#include "platform/PlatformTouchEvent.h"
45#include "platform/PlatformWheelEvent.h"
46#include "platform/PopupMenuClient.h"
47#include "platform/UserGestureIndicator.h"
48#include "platform/geometry/IntRect.h"
49#include "platform/graphics/GraphicsContext.h"
50#include "platform/scroll/FramelessScrollViewClient.h"
51#include "public/web/WebPopupMenuInfo.h"
52#include "public/web/WebPopupType.h"
53#include "public/web/WebViewClient.h"
54#include "web/WebPopupMenuImpl.h"
55#include "web/WebViewImpl.h"
56#include <limits>
57
58namespace blink {
59
60using namespace WebCore;
61
62static const int borderSize = 1;
63
64static PlatformMouseEvent constructRelativeMouseEvent(const PlatformMouseEvent& e, FramelessScrollView* parent, FramelessScrollView* child)
65{
66    IntPoint pos = parent->convertSelfToChild(child, e.position());
67
68    // FIXME: This is a horrible hack since PlatformMouseEvent has no setters for x/y.
69    PlatformMouseEvent relativeEvent = e;
70    IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.position());
71    relativePos.setX(pos.x());
72    relativePos.setY(pos.y());
73    return relativeEvent;
74}
75
76static PlatformWheelEvent constructRelativeWheelEvent(const PlatformWheelEvent& e, FramelessScrollView* parent, FramelessScrollView* child)
77{
78    IntPoint pos = parent->convertSelfToChild(child, e.position());
79
80    // FIXME: This is a horrible hack since PlatformWheelEvent has no setters for x/y.
81    PlatformWheelEvent relativeEvent = e;
82    IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.position());
83    relativePos.setX(pos.x());
84    relativePos.setY(pos.y());
85    return relativeEvent;
86}
87
88// static
89PassRefPtr<PopupContainer> PopupContainer::create(PopupMenuClient* client, bool deviceSupportsTouch)
90{
91    return adoptRef(new PopupContainer(client, deviceSupportsTouch));
92}
93
94PopupContainer::PopupContainer(PopupMenuClient* client, bool deviceSupportsTouch)
95    : m_listBox(PopupListBox::create(client, deviceSupportsTouch))
96    , m_popupOpen(false)
97{
98    setScrollbarModes(ScrollbarAlwaysOff, ScrollbarAlwaysOff);
99}
100
101PopupContainer::~PopupContainer()
102{
103    if (m_listBox && m_listBox->parent())
104        removeChild(m_listBox.get());
105}
106
107IntRect 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)
108{
109    ASSERT(listBox);
110    if (windowRect.x() >= screen.x() && windowRect.maxX() <= screen.maxX() && (widgetRectInScreen.x() < screen.x() || widgetRectInScreen.maxX() > screen.maxX())) {
111        // First, inverse the popup alignment if it does not fit the screen -
112        // this might fix things (or make them better).
113        IntRect inverseWidgetRectInScreen = widgetRectInScreen;
114        inverseWidgetRectInScreen.setX(inverseWidgetRectInScreen.x() + (isRTL ? -rtlOffset : rtlOffset));
115        inverseWidgetRectInScreen.setY(inverseWidgetRectInScreen.y() + (isRTL ? -verticalOffset : verticalOffset));
116        IntRect enclosingScreen = enclosingIntRect(screen);
117        unsigned originalCutoff = std::max(enclosingScreen.x() - widgetRectInScreen.x(), 0) + std::max(widgetRectInScreen.maxX() - enclosingScreen.maxX(), 0);
118        unsigned inverseCutoff = std::max(enclosingScreen.x() - inverseWidgetRectInScreen.x(), 0) + std::max(inverseWidgetRectInScreen.maxX() - enclosingScreen.maxX(), 0);
119
120        // Accept the inverse popup alignment if the trimmed content gets
121        // shorter than that in the original alignment case.
122        if (inverseCutoff < originalCutoff)
123            widgetRectInScreen = inverseWidgetRectInScreen;
124
125        if (widgetRectInScreen.x() < screen.x()) {
126            widgetRectInScreen.setWidth(widgetRectInScreen.maxX() - screen.x());
127            widgetRectInScreen.setX(screen.x());
128            listBox->setMaxWidthAndLayout(std::max(widgetRectInScreen.width() - borderSize * 2, 0));
129        } else if (widgetRectInScreen.maxX() > screen.maxX()) {
130            widgetRectInScreen.setWidth(screen.maxX() - widgetRectInScreen.x());
131            listBox->setMaxWidthAndLayout(std::max(widgetRectInScreen.width() - borderSize * 2, 0));
132        }
133    }
134
135    // Calculate Y axis size.
136    if (widgetRectInScreen.maxY() > static_cast<int>(screen.maxY())) {
137        if (widgetRectInScreen.y() - widgetRectInScreen.height() - targetControlHeight - transformOffset.height() > 0) {
138            // There is enough room to open upwards.
139            widgetRectInScreen.move(-transformOffset.width(), -(widgetRectInScreen.height() + targetControlHeight + transformOffset.height()));
140        } else {
141            // Figure whether upwards or downwards has more room and set the
142            // maximum number of items.
143            int spaceAbove = widgetRectInScreen.y() - targetControlHeight + transformOffset.height();
144            int spaceBelow = screen.maxY() - widgetRectInScreen.y();
145            if (spaceAbove > spaceBelow)
146                listBox->setMaxHeight(spaceAbove);
147            else
148                listBox->setMaxHeight(spaceBelow);
149            listBox->layout();
150            needToResizeView = true;
151            widgetRectInScreen.setHeight(listBox->popupContentHeight() + borderSize * 2);
152            // Move WebWidget upwards if necessary.
153            if (spaceAbove > spaceBelow)
154                widgetRectInScreen.move(-transformOffset.width(), -(widgetRectInScreen.height() + targetControlHeight + transformOffset.height()));
155        }
156    }
157    return widgetRectInScreen;
158}
159
160IntRect PopupContainer::layoutAndCalculateWidgetRect(int targetControlHeight, const IntSize& transformOffset, const IntPoint& popupInitialCoordinate)
161{
162    // Reset the max width and height to their default values, they will be
163    // recomputed below if necessary.
164    m_listBox->setMaxHeight(PopupListBox::defaultMaxHeight);
165    m_listBox->setMaxWidth(std::numeric_limits<int>::max());
166
167    // Lay everything out to figure out our preferred size, then tell the view's
168    // WidgetClient about it. It should assign us a client.
169    m_listBox->layout();
170    fitToListBox();
171    bool isRTL = this->isRTL();
172
173    // Compute the starting x-axis for a normal RTL or right-aligned LTR
174    // dropdown. For those, the right edge of dropdown box should be aligned
175    // with the right edge of <select>/<input> element box, and the dropdown box
176    // should be expanded to the left if more space is needed.
177    // m_originalFrameRect.width() is the width of the target <select>/<input>
178    // element.
179    int rtlOffset = m_controlPosition.p2().x() - m_controlPosition.p1().x() - (m_listBox->width() + borderSize * 2);
180    int rightOffset = isRTL ? rtlOffset : 0;
181
182    // Compute the y-axis offset between the bottom left and bottom right
183    // points. If the <select>/<input> is transformed, they are not the same.
184    int verticalOffset = - m_controlPosition.p4().y() + m_controlPosition.p3().y();
185    int verticalForRTLOffset = isRTL ? verticalOffset : 0;
186
187    // Assume m_listBox size is already calculated.
188    IntSize targetSize(m_listBox->width() + borderSize * 2, m_listBox->height() + borderSize * 2);
189
190    IntRect widgetRectInScreen;
191    // If the popup would extend past the bottom of the screen, open upwards
192    // instead.
193    FloatRect screen = screenAvailableRect(m_frameView.get());
194    // Use popupInitialCoordinate.x() + rightOffset because RTL position
195    // needs to be considered.
196    float pageScaleFactor = m_frameView->frame().page()->pageScaleFactor();
197    int popupX = round((popupInitialCoordinate.x() + rightOffset) * pageScaleFactor);
198    int popupY = round((popupInitialCoordinate.y() + verticalForRTLOffset) * pageScaleFactor);
199    widgetRectInScreen = chromeClient().rootViewToScreen(IntRect(popupX, popupY, targetSize.width(), targetSize.height()));
200
201    // If we have multiple screens and the browser rect is in one screen, we
202    // have to clip the window width to the screen width.
203    // When clipping, we also need to set a maximum width for the list box.
204    FloatRect windowRect = chromeClient().windowRect();
205
206    bool needToResizeView = false;
207    widgetRectInScreen = layoutAndCalculateWidgetRectInternal(widgetRectInScreen, targetControlHeight, windowRect, screen, isRTL, rtlOffset, verticalOffset, transformOffset, m_listBox.get(), needToResizeView);
208    if (needToResizeView)
209        fitToListBox();
210
211    return widgetRectInScreen;
212}
213
214void PopupContainer::showPopup(FrameView* view)
215{
216    m_frameView = view;
217    listBox()->m_focusedElement = m_frameView->frame().document()->focusedElement();
218
219    IntSize transformOffset(m_controlPosition.p4().x() - m_controlPosition.p1().x(), m_controlPosition.p4().y() - m_controlPosition.p1().y() - m_controlSize.height());
220    popupOpened(layoutAndCalculateWidgetRect(m_controlSize.height(), transformOffset, roundedIntPoint(m_controlPosition.p4())));
221    m_popupOpen = true;
222
223    if (!m_listBox->parent())
224        addChild(m_listBox.get());
225
226    // Enable scrollbars after the listbox is inserted into the hierarchy,
227    // so it has a proper WidgetClient.
228    m_listBox->setVerticalScrollbarMode(ScrollbarAuto);
229
230    m_listBox->scrollToRevealSelection();
231
232    invalidate();
233}
234
235void PopupContainer::hidePopup()
236{
237    listBox()->abandon();
238}
239
240void PopupContainer::notifyPopupHidden()
241{
242    if (!m_popupOpen)
243        return;
244    m_popupOpen = false;
245    WebViewImpl::fromPage(m_frameView->frame().page())->popupClosed(this);
246}
247
248void PopupContainer::fitToListBox()
249{
250    // Place the listbox within our border.
251    m_listBox->move(borderSize, borderSize);
252
253    // Size ourselves to contain listbox + border.
254    resize(m_listBox->width() + borderSize * 2, m_listBox->height() + borderSize * 2);
255    invalidate();
256}
257
258bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event)
259{
260    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
261    return m_listBox->handleMouseDownEvent(
262        constructRelativeMouseEvent(event, this, m_listBox.get()));
263}
264
265bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event)
266{
267    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
268    return m_listBox->handleMouseMoveEvent(
269        constructRelativeMouseEvent(event, this, m_listBox.get()));
270}
271
272bool PopupContainer::handleMouseReleaseEvent(const PlatformMouseEvent& event)
273{
274    RefPtr<PopupContainer> protect(this);
275    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
276    return m_listBox->handleMouseReleaseEvent(
277        constructRelativeMouseEvent(event, this, m_listBox.get()));
278}
279
280bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event)
281{
282    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
283    return m_listBox->handleWheelEvent(
284        constructRelativeWheelEvent(event, this, m_listBox.get()));
285}
286
287bool PopupContainer::handleTouchEvent(const PlatformTouchEvent&)
288{
289    return false;
290}
291
292// FIXME: Refactor this code to share functionality with
293// EventHandler::handleGestureEvent.
294bool PopupContainer::handleGestureEvent(const PlatformGestureEvent& gestureEvent)
295{
296    switch (gestureEvent.type()) {
297    case PlatformEvent::GestureTap: {
298        PlatformMouseEvent fakeMouseMove(gestureEvent.position(), gestureEvent.globalPosition(), NoButton, PlatformEvent::MouseMoved, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
299        PlatformMouseEvent fakeMouseDown(gestureEvent.position(), gestureEvent.globalPosition(), LeftButton, PlatformEvent::MousePressed, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
300        PlatformMouseEvent fakeMouseUp(gestureEvent.position(), gestureEvent.globalPosition(), LeftButton, PlatformEvent::MouseReleased, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
301        // handleMouseMoveEvent(fakeMouseMove);
302        handleMouseDownEvent(fakeMouseDown);
303        handleMouseReleaseEvent(fakeMouseUp);
304        return true;
305    }
306    case PlatformEvent::GestureScrollUpdate:
307    case PlatformEvent::GestureScrollUpdateWithoutPropagation: {
308        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());
309        handleWheelEvent(syntheticWheelEvent);
310        return true;
311    }
312    case PlatformEvent::GestureScrollBegin:
313    case PlatformEvent::GestureScrollEnd:
314    case PlatformEvent::GestureTapDown:
315    case PlatformEvent::GestureShowPress:
316        break;
317    default:
318        ASSERT_NOT_REACHED();
319    }
320    return false;
321}
322
323bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event)
324{
325    UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
326    return m_listBox->handleKeyEvent(event);
327}
328
329void PopupContainer::hide()
330{
331    m_listBox->abandon();
332}
333
334void PopupContainer::paint(GraphicsContext* gc, const IntRect& rect)
335{
336    // Adjust coords for scrolled frame.
337    IntRect r = intersection(rect, frameRect());
338    int tx = x();
339    int ty = y();
340
341    r.move(-tx, -ty);
342
343    gc->translate(static_cast<float>(tx), static_cast<float>(ty));
344    m_listBox->paint(gc, r);
345    gc->translate(-static_cast<float>(tx), -static_cast<float>(ty));
346
347    paintBorder(gc, rect);
348}
349
350void PopupContainer::paintBorder(GraphicsContext* gc, const IntRect& rect)
351{
352    // FIXME: Where do we get the border color from?
353    Color borderColor(127, 157, 185);
354
355    gc->setStrokeStyle(NoStroke);
356    gc->setFillColor(borderColor);
357
358    int tx = x();
359    int ty = y();
360
361    // top, left, bottom, right
362    gc->drawRect(IntRect(tx, ty, width(), borderSize));
363    gc->drawRect(IntRect(tx, ty, borderSize, height()));
364    gc->drawRect(IntRect(tx, ty + height() - borderSize, width(), borderSize));
365    gc->drawRect(IntRect(tx + width() - borderSize, ty, borderSize, height()));
366}
367
368bool PopupContainer::isInterestedInEventForKey(int keyCode)
369{
370    return m_listBox->isInterestedInEventForKey(keyCode);
371}
372
373ChromeClient& PopupContainer::chromeClient()
374{
375    return m_frameView->frame().page()->chrome().client();
376}
377
378void PopupContainer::showInRect(const FloatQuad& controlPosition, const IntSize& controlSize, FrameView* v, int index)
379{
380    // The controlSize is the size of the select box. It's usually larger than
381    // we need. Subtract border size so that usually the container will be
382    // displayed exactly the same width as the select box.
383    listBox()->setBaseWidth(max(controlSize.width() - borderSize * 2, 0));
384
385    listBox()->updateFromElement();
386
387    // We set the selected item in updateFromElement(), and disregard the
388    // index passed into this function (same as Webkit's PopupMenuWin.cpp)
389    // FIXME: make sure this is correct, and add an assertion.
390    // ASSERT(popupWindow(popup)->listBox()->selectedIndex() == index);
391
392    // Save and convert the controlPosition to main window coords. Each point is converted separately
393    // to window coordinates because the control could be in a transformed webview and then each point
394    // would be transformed by a different delta.
395    m_controlPosition.setP1(v->contentsToWindow(IntPoint(controlPosition.p1().x(), controlPosition.p1().y())));
396    m_controlPosition.setP2(v->contentsToWindow(IntPoint(controlPosition.p2().x(), controlPosition.p2().y())));
397    m_controlPosition.setP3(v->contentsToWindow(IntPoint(controlPosition.p3().x(), controlPosition.p3().y())));
398    m_controlPosition.setP4(v->contentsToWindow(IntPoint(controlPosition.p4().x(), controlPosition.p4().y())));
399
400    m_controlSize = controlSize;
401
402    // Position at (0, 0) since the frameRect().location() is relative to the
403    // parent WebWidget.
404    setFrameRect(IntRect(IntPoint(), controlSize));
405    showPopup(v);
406}
407
408IntRect PopupContainer::refresh(const IntRect& targetControlRect)
409{
410    listBox()->setBaseWidth(max(m_controlSize.width() - borderSize * 2, 0));
411    listBox()->updateFromElement();
412
413    IntPoint locationInWindow = m_frameView->contentsToWindow(targetControlRect.location());
414
415    // Move it below the select widget.
416    locationInWindow.move(0, targetControlRect.height());
417
418    IntRect widgetRectInScreen = layoutAndCalculateWidgetRect(targetControlRect.height(), IntSize(), locationInWindow);
419
420    // Reset the size (which can be set to the PopupListBox size in
421    // layoutAndGetRTLOffset(), exceeding the available widget rectangle.)
422    if (size() != widgetRectInScreen.size())
423        resize(widgetRectInScreen.size());
424
425    invalidate();
426
427    return widgetRectInScreen;
428}
429
430inline bool PopupContainer::isRTL() const
431{
432    return m_listBox->m_popupClient->menuStyle().textDirection() == RTL;
433}
434
435int PopupContainer::selectedIndex() const
436{
437    return m_listBox->selectedIndex();
438}
439
440int PopupContainer::menuItemHeight() const
441{
442    return m_listBox->getRowHeight(0);
443}
444
445int PopupContainer::menuItemFontSize() const
446{
447    return m_listBox->getRowFont(0).fontDescription().computedSize();
448}
449
450PopupMenuStyle PopupContainer::menuStyle() const
451{
452    return m_listBox->m_popupClient->menuStyle();
453}
454
455const WTF::Vector<PopupItem*>& PopupContainer:: popupData() const
456{
457    return m_listBox->items();
458}
459
460String PopupContainer::getSelectedItemToolTip()
461{
462    // We cannot use m_popupClient->selectedIndex() to choose tooltip message,
463    // because the selectedIndex() might return final selected index, not
464    // hovering selection.
465    return listBox()->m_popupClient->itemToolTip(listBox()->m_selectedIndex);
466}
467
468void PopupContainer::popupOpened(const IntRect& bounds)
469{
470    WebViewImpl* webView = WebViewImpl::fromPage(m_frameView->frame().page());
471    if (!webView->client())
472        return;
473
474    WebWidget* webwidget = webView->client()->createPopupMenu(WebPopupTypeSelect);
475    if (!webwidget)
476        return;
477    // We only notify when the WebView has to handle the popup, as when
478    // the popup is handled externally, the fact that a popup is showing is
479    // transparent to the WebView.
480    webView->popupOpened(this);
481    toWebPopupMenuImpl(webwidget)->initialize(this, bounds);
482}
483
484void PopupContainer::getPopupMenuInfo(WebPopupMenuInfo* info)
485{
486    const Vector<PopupItem*>& inputItems = popupData();
487
488    WebVector<WebMenuItemInfo> outputItems(inputItems.size());
489
490    for (size_t i = 0; i < inputItems.size(); ++i) {
491        const PopupItem& inputItem = *inputItems[i];
492        WebMenuItemInfo& outputItem = outputItems[i];
493
494        outputItem.label = inputItem.label;
495        outputItem.enabled = inputItem.enabled;
496        if (inputItem.textDirection == WebCore::RTL)
497            outputItem.textDirection = WebTextDirectionRightToLeft;
498        else
499            outputItem.textDirection = WebTextDirectionLeftToRight;
500        outputItem.hasTextDirectionOverride = inputItem.hasTextDirectionOverride;
501
502        switch (inputItem.type) {
503        case PopupItem::TypeOption:
504            outputItem.type = WebMenuItemInfo::Option;
505            break;
506        case PopupItem::TypeGroup:
507            outputItem.type = WebMenuItemInfo::Group;
508            break;
509        case PopupItem::TypeSeparator:
510            outputItem.type = WebMenuItemInfo::Separator;
511            break;
512        }
513    }
514
515    info->itemHeight = menuItemHeight();
516    info->itemFontSize = menuItemFontSize();
517    info->selectedIndex = selectedIndex();
518    info->items.swap(outputItems);
519    info->rightAligned = menuStyle().textDirection() == RTL;
520}
521
522} // namespace blink
523