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