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 55 namespace WebCore { 56 57 static const int borderSize = 1; 58 59 static 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 71 static 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 84 PassRefPtr<PopupContainer> PopupContainer::create(PopupMenuClient* client, PopupType popupType, const PopupContainerSettings& settings) 85 { 86 return adoptRef(new PopupContainer(client, popupType, settings)); 87 } 88 89 PopupContainer::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 98 PopupContainer::~PopupContainer() 99 { 100 if (m_listBox && m_listBox->parent()) 101 removeChild(m_listBox.get()); 102 } 103 104 IntRect 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 157 IntRect 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 210 void 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 233 void PopupContainer::hidePopup() 234 { 235 listBox()->hidePopup(); 236 } 237 238 void PopupContainer::notifyPopupHidden() 239 { 240 if (!m_popupOpen) 241 return; 242 m_popupOpen = false; 243 chromeClient()->popupClosed(this); 244 } 245 246 void 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 256 bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event) 257 { 258 UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture); 259 return m_listBox->handleMouseDownEvent( 260 constructRelativeMouseEvent(event, this, m_listBox.get())); 261 } 262 263 bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event) 264 { 265 UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture); 266 return m_listBox->handleMouseMoveEvent( 267 constructRelativeMouseEvent(event, this, m_listBox.get())); 268 } 269 270 bool 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 278 bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event) 279 { 280 UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture); 281 return m_listBox->handleWheelEvent( 282 constructRelativeWheelEvent(event, this, m_listBox.get())); 283 } 284 285 bool PopupContainer::handleTouchEvent(const PlatformTouchEvent&) 286 { 287 return false; 288 } 289 290 // FIXME: Refactor this code to share functionality with 291 // EventHandler::handleGestureEvent. 292 bool 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 320 bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event) 321 { 322 UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture); 323 return m_listBox->handleKeyEvent(event); 324 } 325 326 void PopupContainer::hide() 327 { 328 m_listBox->abandon(); 329 } 330 331 void 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 347 void 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 365 bool PopupContainer::isInterestedInEventForKey(int keyCode) 366 { 367 return m_listBox->isInterestedInEventForKey(keyCode); 368 } 369 370 ChromeClient* PopupContainer::chromeClient() 371 { 372 return m_frameView->frame()->page()->chrome().client(); 373 } 374 375 void 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 405 IntRect 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 427 inline bool PopupContainer::isRTL() const 428 { 429 return m_listBox->m_popupClient->menuStyle().textDirection() == RTL; 430 } 431 432 int PopupContainer::selectedIndex() const 433 { 434 return m_listBox->selectedIndex(); 435 } 436 437 int PopupContainer::menuItemHeight() const 438 { 439 return m_listBox->getRowHeight(0); 440 } 441 442 int PopupContainer::menuItemFontSize() const 443 { 444 return m_listBox->getRowFont(0).size(); 445 } 446 447 PopupMenuStyle PopupContainer::menuStyle() const 448 { 449 return m_listBox->m_popupClient->menuStyle(); 450 } 451 452 const WTF::Vector<PopupItem*>& PopupContainer:: popupData() const 453 { 454 return m_listBox->items(); 455 } 456 457 String 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