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