• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2008, 2009, 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 "PopupMenuChromium.h"
33 
34 #include "CharacterNames.h"
35 #include "ChromeClientChromium.h"
36 #include "Font.h"
37 #include "FontSelector.h"
38 #include "FrameView.h"
39 #include "Frame.h"
40 #include "FramelessScrollView.h"
41 #include "FramelessScrollViewClient.h"
42 #include "GraphicsContext.h"
43 #include "IntRect.h"
44 #include "KeyboardCodes.h"
45 #include "Page.h"
46 #include "PlatformKeyboardEvent.h"
47 #include "PlatformMouseEvent.h"
48 #include "PlatformScreen.h"
49 #include "PlatformWheelEvent.h"
50 #include "PopupMenu.h"
51 #include "RenderTheme.h"
52 #include "ScrollbarTheme.h"
53 #include "SystemTime.h"
54 
55 #include <wtf/CurrentTime.h>
56 
57 using namespace WTF;
58 using namespace Unicode;
59 
60 using std::min;
61 using std::max;
62 
63 namespace WebCore {
64 
65 typedef unsigned long long TimeStamp;
66 
67 static const int kMaxVisibleRows = 20;
68 static const int kMaxHeight = 500;
69 static const int kBorderSize = 1;
70 static const TimeStamp kTypeAheadTimeoutMs = 1000;
71 
72 // The settings used for the drop down menu.
73 // This is the delegate used if none is provided.
74 static const PopupContainerSettings dropDownSettings = {
75     true, // focusOnShow
76     true, // setTextOnIndexChange
77     true, // acceptOnAbandon
78     false // loopSelectionNavigation
79 };
80 
81 // This class uses WebCore code to paint and handle events for a drop-down list
82 // box ("combobox" on Windows).
83 class PopupListBox : public FramelessScrollView {
84 public:
create(PopupMenuClient * client,const PopupContainerSettings & settings)85     static PassRefPtr<PopupListBox> create(PopupMenuClient* client, const PopupContainerSettings& settings)
86     {
87         return adoptRef(new PopupListBox(client, settings));
88     }
89 
90     // FramelessScrollView
91     virtual void paint(GraphicsContext*, const IntRect&);
92     virtual bool handleMouseDownEvent(const PlatformMouseEvent&);
93     virtual bool handleMouseMoveEvent(const PlatformMouseEvent&);
94     virtual bool handleMouseReleaseEvent(const PlatformMouseEvent&);
95     virtual bool handleWheelEvent(const PlatformWheelEvent&);
96     virtual bool handleKeyEvent(const PlatformKeyboardEvent&);
97 
98     // ScrollView
99     virtual HostWindow* hostWindow() const;
100 
101     // PopupListBox methods
102 
103     // Shows the popup
104     void showPopup();
105 
106     // Hides the popup.  Do not call this directly: use client->hidePopup().
107     void hidePopup();
108 
109     // Updates our internal list to match the client.
110     void updateFromElement();
111 
112     // Frees any allocated resources used in a particular popup session.
113     void clear();
114 
115     // Sets the index of the option that is displayed in the <select> widget in the page
116     void setOriginalIndex(int index);
117 
118     // Gets the index of the item that the user is currently moused over or has selected with
119     // the keyboard. This is not the same as the original index, since the user has not yet
120     // accepted this input.
selectedIndex() const121     int selectedIndex() const { return m_selectedIndex; }
122 
123     // Moves selection down/up the given number of items, scrolling if necessary.
124     // Positive is down.  The resulting index will be clamped to the range
125     // [0, numItems), and non-option items will be skipped.
126     void adjustSelectedIndex(int delta);
127 
128     // Returns the number of items in the list.
numItems() const129     int numItems() const { return static_cast<int>(m_items.size()); }
130 
setBaseWidth(int width)131     void setBaseWidth(int width) { m_baseWidth = width; }
132 
133     // Computes the size of widget and children.
134     void layout();
135 
136     // Returns whether the popup wants to process events for the passed key.
137     bool isInterestedInEventForKey(int keyCode);
138 
139     // Gets the height of a row.
140     int getRowHeight(int index);
141 
items() const142     const Vector<PopupItem*>& items() const { return m_items; }
143 
144 private:
145     friend class PopupContainer;
146     friend class RefCounted<PopupListBox>;
147 
PopupListBox(PopupMenuClient * client,const PopupContainerSettings & settings)148     PopupListBox(PopupMenuClient* client, const PopupContainerSettings& settings)
149         : m_settings(settings)
150         , m_originalIndex(0)
151         , m_selectedIndex(0)
152         , m_acceptedIndexOnAbandon(-1)
153         , m_visibleRows(0)
154         , m_baseWidth(0)
155         , m_popupClient(client)
156         , m_repeatingChar(0)
157         , m_lastCharTime(0)
158     {
159         setScrollbarModes(ScrollbarAlwaysOff, ScrollbarAlwaysOff);
160     }
161 
~PopupListBox()162     ~PopupListBox()
163     {
164         clear();
165     }
166 
disconnectClient()167     void disconnectClient() { m_popupClient = 0; }
168 
169     // Closes the popup
170     void abandon();
171 
172     // Returns true if the selection can be changed to index.
173     // Disabled items, or labels cannot be selected.
174     bool isSelectableItem(int index);
175 
176     // Select an index in the list, scrolling if necessary.
177     void selectIndex(int index);
178 
179     // Accepts the selected index as the value to be displayed in the <select> widget on
180     // the web page, and closes the popup.
181     void acceptIndex(int index);
182 
183     // Clears the selection (so no row appears selected).
184     void clearSelection();
185 
186     // Scrolls to reveal the given index.
187     void scrollToRevealRow(int index);
scrollToRevealSelection()188     void scrollToRevealSelection() { scrollToRevealRow(m_selectedIndex); }
189 
190     // Invalidates the row at the given index.
191     void invalidateRow(int index);
192 
193     // Get the bounds of a row.
194     IntRect getRowBounds(int index);
195 
196     // Converts a point to an index of the row the point is over
197     int pointToRowIndex(const IntPoint&);
198 
199     // Paint an individual row
200     void paintRow(GraphicsContext*, const IntRect&, int rowIndex);
201 
202     // Test if the given point is within the bounds of the popup window.
203     bool isPointInBounds(const IntPoint&);
204 
205     // Called when the user presses a text key.  Does a prefix-search of the items.
206     void typeAheadFind(const PlatformKeyboardEvent&);
207 
208     // Returns the font to use for the given row
209     Font getRowFont(int index);
210 
211     // Moves the selection down/up one item, taking care of looping back to the
212     // first/last element if m_loopSelectionNavigation is true.
213     void selectPreviousRow();
214     void selectNextRow();
215 
216     // The settings that specify the behavior for this Popup window.
217     PopupContainerSettings m_settings;
218 
219     // This is the index of the item marked as "selected" - i.e. displayed in the widget on the
220     // page.
221     int m_originalIndex;
222 
223     // This is the index of the item that the user is hovered over or has selected using the
224     // keyboard in the list. They have not confirmed this selection by clicking or pressing
225     // enter yet however.
226     int m_selectedIndex;
227 
228     // If >= 0, this is the index we should accept if the popup is "abandoned".
229     // This is used for keyboard navigation, where we want the
230     // selection to change immediately, and is only used if the settings
231     // acceptOnAbandon field is true.
232     int m_acceptedIndexOnAbandon;
233 
234     // This is the number of rows visible in the popup. The maximum number visible at a time is
235     // defined as being kMaxVisibleRows. For a scrolled popup, this can be thought of as the
236     // page size in data units.
237     int m_visibleRows;
238 
239     // Our suggested width, not including scrollbar.
240     int m_baseWidth;
241 
242     // A list of the options contained within the <select>
243     Vector<PopupItem*> m_items;
244 
245     // The <select> PopupMenuClient that opened us.
246     PopupMenuClient* m_popupClient;
247 
248     // The scrollbar which has mouse capture.  Mouse events go straight to this
249     // if non-NULL.
250     RefPtr<Scrollbar> m_capturingScrollbar;
251 
252     // The last scrollbar that the mouse was over.  Used for mouseover highlights.
253     RefPtr<Scrollbar> m_lastScrollbarUnderMouse;
254 
255     // The string the user has typed so far into the popup. Used for typeAheadFind.
256     String m_typedString;
257 
258     // The char the user has hit repeatedly.  Used for typeAheadFind.
259     UChar m_repeatingChar;
260 
261     // The last time the user hit a key.  Used for typeAheadFind.
262     TimeStamp m_lastCharTime;
263 };
264 
constructRelativeMouseEvent(const PlatformMouseEvent & e,FramelessScrollView * parent,FramelessScrollView * child)265 static PlatformMouseEvent constructRelativeMouseEvent(const PlatformMouseEvent& e,
266                                                       FramelessScrollView* parent,
267                                                       FramelessScrollView* child)
268 {
269     IntPoint pos = parent->convertSelfToChild(child, e.pos());
270 
271     // FIXME: This is a horrible hack since PlatformMouseEvent has no setters for x/y.
272     PlatformMouseEvent relativeEvent = e;
273     IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.pos());
274     relativePos.setX(pos.x());
275     relativePos.setY(pos.y());
276     return relativeEvent;
277 }
278 
constructRelativeWheelEvent(const PlatformWheelEvent & e,FramelessScrollView * parent,FramelessScrollView * child)279 static PlatformWheelEvent constructRelativeWheelEvent(const PlatformWheelEvent& e,
280                                                       FramelessScrollView* parent,
281                                                       FramelessScrollView* child)
282 {
283     IntPoint pos = parent->convertSelfToChild(child, e.pos());
284 
285     // FIXME: This is a horrible hack since PlatformWheelEvent has no setters for x/y.
286     PlatformWheelEvent relativeEvent = e;
287     IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.pos());
288     relativePos.setX(pos.x());
289     relativePos.setY(pos.y());
290     return relativeEvent;
291 }
292 
293 ///////////////////////////////////////////////////////////////////////////////
294 // PopupContainer implementation
295 
296 // static
create(PopupMenuClient * client,const PopupContainerSettings & settings)297 PassRefPtr<PopupContainer> PopupContainer::create(PopupMenuClient* client,
298                                                   const PopupContainerSettings& settings)
299 {
300     return adoptRef(new PopupContainer(client, settings));
301 }
302 
PopupContainer(PopupMenuClient * client,const PopupContainerSettings & settings)303 PopupContainer::PopupContainer(PopupMenuClient* client,
304                                const PopupContainerSettings& settings)
305     : m_listBox(PopupListBox::create(client, settings))
306     , m_settings(settings)
307 {
308     setScrollbarModes(ScrollbarAlwaysOff, ScrollbarAlwaysOff);
309 }
310 
~PopupContainer()311 PopupContainer::~PopupContainer()
312 {
313     if (m_listBox && m_listBox->parent())
314         removeChild(m_listBox.get());
315 }
316 
showPopup(FrameView * view)317 void PopupContainer::showPopup(FrameView* view)
318 {
319     // Pre-layout, our size matches the <select> dropdown control.
320     int selectHeight = frameRect().height();
321 
322     // Lay everything out to figure out our preferred size, then tell the view's
323     // WidgetClient about it.  It should assign us a client.
324     layout();
325 
326     ChromeClientChromium* chromeClient = static_cast<ChromeClientChromium*>(
327         view->frame()->page()->chrome()->client());
328     if (chromeClient) {
329         // If the popup would extend past the bottom of the screen, open upwards
330         // instead.
331         FloatRect screen = screenAvailableRect(view);
332         IntRect widgetRect = chromeClient->windowToScreen(frameRect());
333         if (widgetRect.bottom() > static_cast<int>(screen.bottom()))
334             widgetRect.move(0, -(widgetRect.height() + selectHeight));
335 
336         chromeClient->popupOpened(this, widgetRect, m_settings.focusOnShow, false);
337     }
338 
339     if (!m_listBox->parent())
340         addChild(m_listBox.get());
341 
342     // Enable scrollbars after the listbox is inserted into the hierarchy,
343     // so it has a proper WidgetClient.
344     m_listBox->setVerticalScrollbarMode(ScrollbarAuto);
345 
346     m_listBox->scrollToRevealSelection();
347 
348     invalidate();
349 }
350 
showExternal(const IntRect & rect,FrameView * v,int index)351 void PopupContainer::showExternal(const IntRect& rect, FrameView* v, int index)
352 {
353     if (!listBox())
354         return;
355 
356     listBox()->setBaseWidth(rect.width());
357     listBox()->updateFromElement();
358 
359     if (listBox()->numItems() < 1) {
360         hidePopup();
361         return;
362     }
363 
364     // Adjust the popup position to account for scrolling.
365     IntPoint location = v->contentsToWindow(rect.location());
366     IntRect popupRect(location, rect.size());
367 
368     // Get the ChromeClient and pass it the popup menu's listbox data.
369     ChromeClientChromium* client = static_cast<ChromeClientChromium*>(
370          v->frame()->page()->chrome()->client());
371     client->popupOpened(this, popupRect, true, true);
372 }
373 
hidePopup()374 void PopupContainer::hidePopup()
375 {
376     if (client())
377         client()->popupClosed(this);
378 }
379 
layout()380 void PopupContainer::layout()
381 {
382     m_listBox->layout();
383 
384     // Place the listbox within our border.
385     m_listBox->move(kBorderSize, kBorderSize);
386 
387     // Size ourselves to contain listbox + border.
388     resize(m_listBox->width() + kBorderSize * 2, m_listBox->height() + kBorderSize * 2);
389 
390     invalidate();
391 }
392 
handleMouseDownEvent(const PlatformMouseEvent & event)393 bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event)
394 {
395     return m_listBox->handleMouseDownEvent(
396         constructRelativeMouseEvent(event, this, m_listBox.get()));
397 }
398 
handleMouseMoveEvent(const PlatformMouseEvent & event)399 bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event)
400 {
401     return m_listBox->handleMouseMoveEvent(
402         constructRelativeMouseEvent(event, this, m_listBox.get()));
403 }
404 
handleMouseReleaseEvent(const PlatformMouseEvent & event)405 bool PopupContainer::handleMouseReleaseEvent(const PlatformMouseEvent& event)
406 {
407     return m_listBox->handleMouseReleaseEvent(
408         constructRelativeMouseEvent(event, this, m_listBox.get()));
409 }
410 
handleWheelEvent(const PlatformWheelEvent & event)411 bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event)
412 {
413     return m_listBox->handleWheelEvent(
414         constructRelativeWheelEvent(event, this, m_listBox.get()));
415 }
416 
handleKeyEvent(const PlatformKeyboardEvent & event)417 bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event)
418 {
419     return m_listBox->handleKeyEvent(event);
420 }
421 
hide()422 void PopupContainer::hide()
423 {
424     m_listBox->abandon();
425 }
426 
paint(GraphicsContext * gc,const IntRect & rect)427 void PopupContainer::paint(GraphicsContext* gc, const IntRect& rect)
428 {
429     // adjust coords for scrolled frame
430     IntRect r = intersection(rect, frameRect());
431     int tx = x();
432     int ty = y();
433 
434     r.move(-tx, -ty);
435 
436     gc->translate(static_cast<float>(tx), static_cast<float>(ty));
437     m_listBox->paint(gc, r);
438     gc->translate(-static_cast<float>(tx), -static_cast<float>(ty));
439 
440     paintBorder(gc, rect);
441 }
442 
paintBorder(GraphicsContext * gc,const IntRect & rect)443 void PopupContainer::paintBorder(GraphicsContext* gc, const IntRect& rect)
444 {
445     // FIXME: Where do we get the border color from?
446     Color borderColor(127, 157, 185);
447 
448     gc->setStrokeStyle(NoStroke);
449     gc->setFillColor(borderColor);
450 
451     int tx = x();
452     int ty = y();
453 
454     // top, left, bottom, right
455     gc->drawRect(IntRect(tx, ty, width(), kBorderSize));
456     gc->drawRect(IntRect(tx, ty, kBorderSize, height()));
457     gc->drawRect(IntRect(tx, ty + height() - kBorderSize, width(), kBorderSize));
458     gc->drawRect(IntRect(tx + width() - kBorderSize, ty, kBorderSize, height()));
459 }
460 
isInterestedInEventForKey(int keyCode)461 bool PopupContainer::isInterestedInEventForKey(int keyCode)
462 {
463     return m_listBox->isInterestedInEventForKey(keyCode);
464 }
465 
show(const IntRect & r,FrameView * v,int index)466 void PopupContainer::show(const IntRect& r, FrameView* v, int index)
467 {
468     // The rect is the size of the select box. It's usually larger than we need.
469     // subtract border size so that usually the container will be displayed
470     // exactly the same width as the select box.
471     listBox()->setBaseWidth(max(r.width() - kBorderSize * 2, 0));
472 
473     listBox()->updateFromElement();
474 
475     // We set the selected item in updateFromElement(), and disregard the
476     // index passed into this function (same as Webkit's PopupMenuWin.cpp)
477     // FIXME: make sure this is correct, and add an assertion.
478     // ASSERT(popupWindow(popup)->listBox()->selectedIndex() == index);
479 
480     // Convert point to main window coords.
481     IntPoint location = v->contentsToWindow(r.location());
482 
483     // Move it below the select widget.
484     location.move(0, r.height());
485 
486     IntRect popupRect(location, r.size());
487     setFrameRect(popupRect);
488     showPopup(v);
489 }
490 
refresh()491 void PopupContainer::refresh()
492 {
493     listBox()->updateFromElement();
494     layout();
495 }
496 
selectedIndex() const497 int PopupContainer::selectedIndex() const
498 {
499     return m_listBox->selectedIndex();
500 }
501 
menuItemHeight() const502 int PopupContainer::menuItemHeight() const
503 {
504     return m_listBox->getRowHeight(0);
505 }
506 
popupData() const507 const WTF::Vector<PopupItem*>& PopupContainer:: popupData() const
508 {
509     return m_listBox->items();
510 }
511 
512 ///////////////////////////////////////////////////////////////////////////////
513 // PopupListBox implementation
514 
handleMouseDownEvent(const PlatformMouseEvent & event)515 bool PopupListBox::handleMouseDownEvent(const PlatformMouseEvent& event)
516 {
517     Scrollbar* scrollbar = scrollbarAtPoint(event.pos());
518     if (scrollbar) {
519         m_capturingScrollbar = scrollbar;
520         m_capturingScrollbar->mouseDown(event);
521         return true;
522     }
523 
524     if (!isPointInBounds(event.pos()))
525         abandon();
526 
527     return true;
528 }
529 
handleMouseMoveEvent(const PlatformMouseEvent & event)530 bool PopupListBox::handleMouseMoveEvent(const PlatformMouseEvent& event)
531 {
532     if (m_capturingScrollbar) {
533         m_capturingScrollbar->mouseMoved(event);
534         return true;
535     }
536 
537     Scrollbar* scrollbar = scrollbarAtPoint(event.pos());
538     if (m_lastScrollbarUnderMouse != scrollbar) {
539         // Send mouse exited to the old scrollbar.
540         if (m_lastScrollbarUnderMouse)
541             m_lastScrollbarUnderMouse->mouseExited();
542         m_lastScrollbarUnderMouse = scrollbar;
543     }
544 
545     if (scrollbar) {
546         scrollbar->mouseMoved(event);
547         return true;
548     }
549 
550     if (!isPointInBounds(event.pos()))
551         return false;
552 
553     selectIndex(pointToRowIndex(event.pos()));
554     return true;
555 }
556 
handleMouseReleaseEvent(const PlatformMouseEvent & event)557 bool PopupListBox::handleMouseReleaseEvent(const PlatformMouseEvent& event)
558 {
559     if (m_capturingScrollbar) {
560         m_capturingScrollbar->mouseUp();
561         m_capturingScrollbar = 0;
562         return true;
563     }
564 
565     if (!isPointInBounds(event.pos()))
566         return true;
567 
568     acceptIndex(pointToRowIndex(event.pos()));
569     return true;
570 }
571 
handleWheelEvent(const PlatformWheelEvent & event)572 bool PopupListBox::handleWheelEvent(const PlatformWheelEvent& event)
573 {
574     if (!isPointInBounds(event.pos())) {
575         abandon();
576         return true;
577     }
578 
579     // Pass it off to the scroll view.
580     // Sadly, WebCore devs don't understand the whole "const" thing.
581     wheelEvent(const_cast<PlatformWheelEvent&>(event));
582     return true;
583 }
584 
585 // Should be kept in sync with handleKeyEvent().
isInterestedInEventForKey(int keyCode)586 bool PopupListBox::isInterestedInEventForKey(int keyCode)
587 {
588     switch (keyCode) {
589     case VKEY_ESCAPE:
590     case VKEY_RETURN:
591     case VKEY_UP:
592     case VKEY_DOWN:
593     case VKEY_PRIOR:
594     case VKEY_NEXT:
595     case VKEY_HOME:
596     case VKEY_END:
597     case VKEY_TAB:
598         return true;
599     default:
600         return false;
601     }
602 }
603 
handleKeyEvent(const PlatformKeyboardEvent & event)604 bool PopupListBox::handleKeyEvent(const PlatformKeyboardEvent& event)
605 {
606     if (event.type() == PlatformKeyboardEvent::KeyUp)
607         return true;
608 
609     if (numItems() == 0 && event.windowsVirtualKeyCode() != VKEY_ESCAPE)
610         return true;
611 
612     switch (event.windowsVirtualKeyCode()) {
613     case VKEY_ESCAPE:
614         abandon();  // may delete this
615         return true;
616     case VKEY_RETURN:
617         if (m_selectedIndex == -1)  {
618             m_popupClient->hidePopup();
619             // Don't eat the enter if nothing is selected.
620             return false;
621         }
622         acceptIndex(m_selectedIndex);  // may delete this
623         return true;
624     case VKEY_UP:
625         selectPreviousRow();
626         break;
627     case VKEY_DOWN:
628         selectNextRow();
629         break;
630     case VKEY_PRIOR:
631         adjustSelectedIndex(-m_visibleRows);
632         break;
633     case VKEY_NEXT:
634         adjustSelectedIndex(m_visibleRows);
635         break;
636     case VKEY_HOME:
637         adjustSelectedIndex(-m_selectedIndex);
638         break;
639     case VKEY_END:
640         adjustSelectedIndex(m_items.size());
641         break;
642     default:
643         if (!event.ctrlKey() && !event.altKey() && !event.metaKey()
644             && isPrintableChar(event.windowsVirtualKeyCode()))
645             typeAheadFind(event);
646         break;
647     }
648 
649     if (m_originalIndex != m_selectedIndex) {
650         // Keyboard events should update the selection immediately (but we don't
651         // want to fire the onchange event until the popup is closed, to match
652         // IE).  We change the original index so we revert to that when the
653         // popup is closed.
654         if (m_settings.acceptOnAbandon)
655             m_acceptedIndexOnAbandon = m_selectedIndex;
656 
657         setOriginalIndex(m_selectedIndex);
658         if (m_settings.setTextOnIndexChange)
659             m_popupClient->setTextFromItem(m_selectedIndex);
660     } else if (!m_settings.setTextOnIndexChange &&
661                event.windowsVirtualKeyCode() == VKEY_TAB) {
662         // TAB is a special case as it should select the current item if any and
663         // advance focus.
664         if (m_selectedIndex >= 0)
665             m_popupClient->setTextFromItem(m_selectedIndex);
666         // Return false so the TAB key event is propagated to the page.
667         return false;
668     }
669 
670     return true;
671 }
672 
hostWindow() const673 HostWindow* PopupListBox::hostWindow() const
674 {
675     // Our parent is the root ScrollView, so it is the one that has a
676     // HostWindow.  FrameView::hostWindow() works similarly.
677     return parent() ? parent()->hostWindow() : 0;
678 }
679 
680 // From HTMLSelectElement.cpp
stripLeadingWhiteSpace(const String & string)681 static String stripLeadingWhiteSpace(const String& string)
682 {
683     int length = string.length();
684     int i;
685     for (i = 0; i < length; ++i)
686         if (string[i] != noBreakSpace
687             && (string[i] <= 0x7F ? !isspace(string[i]) : (direction(string[i]) != WhiteSpaceNeutral)))
688             break;
689 
690     return string.substring(i, length - i);
691 }
692 
isCharacterTypeEvent(const PlatformKeyboardEvent & event)693 static bool isCharacterTypeEvent(const PlatformKeyboardEvent& event) {
694     // Check whether the event is a character-typed event or not.
695     // In Windows, PlatformKeyboardEvent::Char (not RawKeyDown) type event
696     // is considered as character type event. In Mac OS, KeyDown (not
697     // KeyUp) is considered as character type event.
698 #if PLATFORM(WIN_OS)
699     if (event.type() == PlatformKeyboardEvent::Char)
700         return true;
701 #else
702     if (event.type() == PlatformKeyboardEvent::KeyDown)
703         return true;
704 #endif
705     return false;
706 }
707 
708 // From HTMLSelectElement.cpp, with modifications
typeAheadFind(const PlatformKeyboardEvent & event)709 void PopupListBox::typeAheadFind(const PlatformKeyboardEvent& event)
710 {
711     TimeStamp now = static_cast<TimeStamp>(currentTime() * 1000.0f);
712     TimeStamp delta = now - m_lastCharTime;
713 
714     // Reset the time when user types in a character. The time gap between
715     // last character and the current character is used to indicate whether
716     // user typed in a string or just a character as the search prefix.
717     if (isCharacterTypeEvent(event))
718         m_lastCharTime = now;
719 
720     UChar c = event.windowsVirtualKeyCode();
721 
722     String prefix;
723     int searchStartOffset = 1;
724     if (delta > kTypeAheadTimeoutMs) {
725         m_typedString = prefix = String(&c, 1);
726         m_repeatingChar = c;
727     } else {
728         m_typedString.append(c);
729 
730         if (c == m_repeatingChar)
731             // The user is likely trying to cycle through all the items starting with this character, so just search on the character
732             prefix = String(&c, 1);
733         else {
734             m_repeatingChar = 0;
735             prefix = m_typedString;
736             searchStartOffset = 0;
737         }
738     }
739 
740     int itemCount = numItems();
741     int index = (max(0, m_selectedIndex) + searchStartOffset) % itemCount;
742     for (int i = 0; i < itemCount; i++, index = (index + 1) % itemCount) {
743         if (!isSelectableItem(index))
744             continue;
745 
746         if (stripLeadingWhiteSpace(m_items[index]->label).startsWith(prefix, false)) {
747             selectIndex(index);
748             return;
749         }
750     }
751 }
752 
paint(GraphicsContext * gc,const IntRect & rect)753 void PopupListBox::paint(GraphicsContext* gc, const IntRect& rect)
754 {
755     // adjust coords for scrolled frame
756     IntRect r = intersection(rect, frameRect());
757     int tx = x() - scrollX();
758     int ty = y() - scrollY();
759 
760     r.move(-tx, -ty);
761 
762     // set clip rect to match revised damage rect
763     gc->save();
764     gc->translate(static_cast<float>(tx), static_cast<float>(ty));
765     gc->clip(r);
766 
767     // FIXME: Can we optimize scrolling to not require repainting the entire
768     // window?  Should we?
769     for (int i = 0; i < numItems(); ++i)
770         paintRow(gc, r, i);
771 
772     // Special case for an empty popup.
773     if (numItems() == 0)
774         gc->fillRect(r, Color::white);
775 
776     gc->restore();
777 
778     ScrollView::paint(gc, rect);
779 }
780 
781 static const int separatorPadding = 4;
782 static const int separatorHeight = 1;
783 
paintRow(GraphicsContext * gc,const IntRect & rect,int rowIndex)784 void PopupListBox::paintRow(GraphicsContext* gc, const IntRect& rect, int rowIndex)
785 {
786     // This code is based largely on RenderListBox::paint* methods.
787 
788     IntRect rowRect = getRowBounds(rowIndex);
789     if (!rowRect.intersects(rect))
790         return;
791 
792     PopupMenuStyle style = m_popupClient->itemStyle(rowIndex);
793 
794     // Paint background
795     Color backColor, textColor;
796     if (rowIndex == m_selectedIndex) {
797         backColor = RenderTheme::defaultTheme()->activeListBoxSelectionBackgroundColor();
798         textColor = RenderTheme::defaultTheme()->activeListBoxSelectionForegroundColor();
799     } else {
800         backColor = style.backgroundColor();
801         textColor = style.foregroundColor();
802     }
803 
804     // If we have a transparent background, make sure it has a color to blend
805     // against.
806     if (backColor.hasAlpha())
807         gc->fillRect(rowRect, Color::white);
808 
809     gc->fillRect(rowRect, backColor);
810 
811     if (m_popupClient->itemIsSeparator(rowIndex)) {
812         IntRect separatorRect(
813             rowRect.x() + separatorPadding,
814             rowRect.y() + (rowRect.height() - separatorHeight) / 2,
815             rowRect.width() - 2 * separatorPadding, separatorHeight);
816         gc->fillRect(separatorRect, textColor);
817         return;
818     }
819 
820     gc->setFillColor(textColor);
821 
822     // Bunch of shit to deal with RTL text...
823     String itemText = m_popupClient->itemText(rowIndex);
824     unsigned length = itemText.length();
825     const UChar* str = itemText.characters();
826 
827     TextRun textRun(str, length, false, 0, 0, itemText.defaultWritingDirection() == WTF::Unicode::RightToLeft);
828 
829     // FIXME: http://b/1210481 We should get the padding of individual option
830     // elements.  This probably implies changes to PopupMenuClient.
831 
832     // Draw the item text
833     if (style.isVisible()) {
834         Font itemFont = getRowFont(rowIndex);
835         int textX = max(0, m_popupClient->clientPaddingLeft() - m_popupClient->clientInsetLeft());
836         int textY = rowRect.y() + itemFont.ascent() + (rowRect.height() - itemFont.height()) / 2;
837         gc->drawBidiText(itemFont, textRun, IntPoint(textX, textY));
838     }
839 }
840 
getRowFont(int rowIndex)841 Font PopupListBox::getRowFont(int rowIndex)
842 {
843     Font itemFont = m_popupClient->menuStyle().font();
844     if (m_popupClient->itemIsLabel(rowIndex)) {
845         // Bold-ify labels (ie, an <optgroup> heading).
846         FontDescription d = itemFont.fontDescription();
847         d.setWeight(FontWeightBold);
848         Font font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
849         font.update(0);
850         return font;
851     }
852 
853     return itemFont;
854 }
855 
abandon()856 void PopupListBox::abandon()
857 {
858     RefPtr<PopupListBox> keepAlive(this);
859 
860     m_selectedIndex = m_originalIndex;
861 
862     m_popupClient->hidePopup();
863 
864     if (m_acceptedIndexOnAbandon >= 0) {
865         m_popupClient->valueChanged(m_acceptedIndexOnAbandon);
866         m_acceptedIndexOnAbandon = -1;
867     }
868 }
869 
pointToRowIndex(const IntPoint & point)870 int PopupListBox::pointToRowIndex(const IntPoint& point)
871 {
872     int y = scrollY() + point.y();
873 
874     // FIXME: binary search if perf matters.
875     for (int i = 0; i < numItems(); ++i) {
876         if (y < m_items[i]->yOffset)
877             return i-1;
878     }
879 
880     // Last item?
881     if (y < contentsHeight())
882         return m_items.size()-1;
883 
884     return -1;
885 }
886 
acceptIndex(int index)887 void PopupListBox::acceptIndex(int index)
888 {
889     if (index >= numItems())
890         return;
891 
892     if (index < 0) {
893         if (m_popupClient) {
894             // Enter pressed with no selection, just close the popup.
895             m_popupClient->hidePopup();
896         }
897         return;
898     }
899 
900     if (isSelectableItem(index)) {
901         RefPtr<PopupListBox> keepAlive(this);
902 
903         // Hide ourselves first since valueChanged may have numerous side-effects.
904         m_popupClient->hidePopup();
905 
906         // Tell the <select> PopupMenuClient what index was selected.
907         m_popupClient->valueChanged(index);
908     }
909 }
910 
selectIndex(int index)911 void PopupListBox::selectIndex(int index)
912 {
913     if (index < 0 || index >= numItems())
914         return;
915 
916     if (index != m_selectedIndex && isSelectableItem(index)) {
917         invalidateRow(m_selectedIndex);
918         m_selectedIndex = index;
919         invalidateRow(m_selectedIndex);
920 
921         scrollToRevealSelection();
922     }
923 }
924 
setOriginalIndex(int index)925 void PopupListBox::setOriginalIndex(int index)
926 {
927     m_originalIndex = m_selectedIndex = index;
928 }
929 
getRowHeight(int index)930 int PopupListBox::getRowHeight(int index)
931 {
932     if (index < 0)
933         return 0;
934 
935     return getRowFont(index).height();
936 }
937 
getRowBounds(int index)938 IntRect PopupListBox::getRowBounds(int index)
939 {
940     if (index < 0)
941         return IntRect(0, 0, visibleWidth(), getRowHeight(index));
942 
943     return IntRect(0, m_items[index]->yOffset, visibleWidth(), getRowHeight(index));
944 }
945 
invalidateRow(int index)946 void PopupListBox::invalidateRow(int index)
947 {
948     if (index < 0)
949         return;
950 
951     // Invalidate in the window contents, as FramelessScrollView::invalidateRect
952     // paints in the window coordinates.
953     invalidateRect(contentsToWindow(getRowBounds(index)));
954 }
955 
scrollToRevealRow(int index)956 void PopupListBox::scrollToRevealRow(int index)
957 {
958     if (index < 0)
959         return;
960 
961     IntRect rowRect = getRowBounds(index);
962 
963     if (rowRect.y() < scrollY()) {
964         // Row is above current scroll position, scroll up.
965         ScrollView::setScrollPosition(IntPoint(0, rowRect.y()));
966     } else if (rowRect.bottom() > scrollY() + visibleHeight()) {
967         // Row is below current scroll position, scroll down.
968         ScrollView::setScrollPosition(IntPoint(0, rowRect.bottom() - visibleHeight()));
969     }
970 }
971 
isSelectableItem(int index)972 bool PopupListBox::isSelectableItem(int index)
973 {
974     ASSERT(index >= 0 && index < numItems());
975     return m_items[index]->type == PopupItem::TypeOption && m_popupClient->itemIsEnabled(index);
976 }
977 
clearSelection()978 void PopupListBox::clearSelection()
979 {
980     if (m_selectedIndex != -1) {
981         invalidateRow(m_selectedIndex);
982         m_selectedIndex = -1;
983     }
984 }
985 
selectNextRow()986 void PopupListBox::selectNextRow()
987 {
988     if (!m_settings.loopSelectionNavigation || m_selectedIndex != numItems() - 1) {
989         adjustSelectedIndex(1);
990         return;
991     }
992 
993     // We are moving past the last item, no row should be selected.
994     clearSelection();
995 }
996 
selectPreviousRow()997 void PopupListBox::selectPreviousRow()
998 {
999     if (!m_settings.loopSelectionNavigation || m_selectedIndex > 0) {
1000         adjustSelectedIndex(-1);
1001         return;
1002     }
1003 
1004     if (m_selectedIndex == 0) {
1005         // We are moving past the first item, clear the selection.
1006         clearSelection();
1007         return;
1008     }
1009 
1010     // No row is selected, jump to the last item.
1011     selectIndex(numItems() - 1);
1012     scrollToRevealSelection();
1013 }
1014 
adjustSelectedIndex(int delta)1015 void PopupListBox::adjustSelectedIndex(int delta)
1016 {
1017     int targetIndex = m_selectedIndex + delta;
1018     targetIndex = min(max(targetIndex, 0), numItems() - 1);
1019     if (!isSelectableItem(targetIndex)) {
1020         // We didn't land on an option.  Try to find one.
1021         // We try to select the closest index to target, prioritizing any in
1022         // the range [current, target].
1023 
1024         int dir = delta > 0 ? 1 : -1;
1025         int testIndex = m_selectedIndex;
1026         int bestIndex = m_selectedIndex;
1027         bool passedTarget = false;
1028         while (testIndex >= 0 && testIndex < numItems()) {
1029             if (isSelectableItem(testIndex))
1030                 bestIndex = testIndex;
1031             if (testIndex == targetIndex)
1032                 passedTarget = true;
1033             if (passedTarget && bestIndex != m_selectedIndex)
1034                 break;
1035 
1036             testIndex += dir;
1037         }
1038 
1039         // Pick the best index, which may mean we don't change.
1040         targetIndex = bestIndex;
1041     }
1042 
1043     // Select the new index, and ensure its visible.  We do this regardless of
1044     // whether the selection changed to ensure keyboard events always bring the
1045     // selection into view.
1046     selectIndex(targetIndex);
1047     scrollToRevealSelection();
1048 }
1049 
updateFromElement()1050 void PopupListBox::updateFromElement()
1051 {
1052     clear();
1053 
1054     int size = m_popupClient->listSize();
1055     for (int i = 0; i < size; ++i) {
1056         PopupItem::Type type;
1057         if (m_popupClient->itemIsSeparator(i))
1058             type = PopupItem::TypeSeparator;
1059         else if (m_popupClient->itemIsLabel(i))
1060             type = PopupItem::TypeGroup;
1061         else
1062             type = PopupItem::TypeOption;
1063         m_items.append(new PopupItem(m_popupClient->itemText(i), type));
1064         m_items[i]->enabled = isSelectableItem(i);
1065     }
1066 
1067     m_selectedIndex = m_popupClient->selectedIndex();
1068     setOriginalIndex(m_selectedIndex);
1069 
1070     layout();
1071 }
1072 
layout()1073 void PopupListBox::layout()
1074 {
1075     // Size our child items.
1076     int baseWidth = 0;
1077     int paddingWidth = 0;
1078     int y = 0;
1079     for (int i = 0; i < numItems(); ++i) {
1080         Font itemFont = getRowFont(i);
1081 
1082         // Place the item vertically.
1083         m_items[i]->yOffset = y;
1084         y += itemFont.height();
1085 
1086         // Ensure the popup is wide enough to fit this item.
1087         String text = m_popupClient->itemText(i);
1088         if (!text.isEmpty()) {
1089             int width = itemFont.width(TextRun(text));
1090             baseWidth = max(baseWidth, width);
1091         }
1092         // FIXME: http://b/1210481 We should get the padding of individual option elements.
1093         paddingWidth = max(paddingWidth,
1094             m_popupClient->clientPaddingLeft() + m_popupClient->clientPaddingRight());
1095     }
1096 
1097     int windowHeight = 0;
1098 
1099 #if PLATFORM(DARWIN)
1100     // Set the popup's window to contain all available items on Mac only, which
1101     // uses native controls that manage their own scrolling. This allows hit
1102     // testing to work when selecting items in popups that have more menu entries
1103     // than the maximum window size.
1104     m_visibleRows = numItems();
1105 #else
1106     m_visibleRows = min(numItems(), kMaxVisibleRows);
1107 #endif
1108 
1109     for (int i = 0; i < m_visibleRows; ++i) {
1110         int rowHeight = getRowHeight(i);
1111 #if !PLATFORM(DARWIN)
1112         // Only clip the window height for non-Mac platforms.
1113         if (windowHeight + rowHeight > kMaxHeight) {
1114             m_visibleRows = i;
1115             break;
1116         }
1117 #endif
1118 
1119         windowHeight += rowHeight;
1120     }
1121 
1122     // Set our widget and scrollable contents sizes.
1123     int scrollbarWidth = 0;
1124     if (m_visibleRows < numItems())
1125         scrollbarWidth = ScrollbarTheme::nativeTheme()->scrollbarThickness();
1126 
1127     int windowWidth = baseWidth + scrollbarWidth + paddingWidth;
1128     int contentWidth = baseWidth;
1129 
1130     if (windowWidth < m_baseWidth) {
1131         windowWidth = m_baseWidth;
1132         contentWidth = m_baseWidth - scrollbarWidth - paddingWidth;
1133     } else {
1134         m_baseWidth = baseWidth;
1135     }
1136 
1137     resize(windowWidth, windowHeight);
1138     setContentsSize(IntSize(contentWidth, getRowBounds(numItems() - 1).bottom()));
1139 
1140     if (hostWindow())
1141         scrollToRevealSelection();
1142 
1143     invalidate();
1144 }
1145 
clear()1146 void PopupListBox::clear()
1147 {
1148     for (Vector<PopupItem*>::iterator it = m_items.begin(); it != m_items.end(); ++it)
1149         delete *it;
1150     m_items.clear();
1151 }
1152 
isPointInBounds(const IntPoint & point)1153 bool PopupListBox::isPointInBounds(const IntPoint& point)
1154 {
1155     return numItems() != 0 && IntRect(0, 0, width(), height()).contains(point);
1156 }
1157 
1158 ///////////////////////////////////////////////////////////////////////////////
1159 // PopupMenu implementation
1160 //
1161 // Note: you cannot add methods to this class, since it is defined above the
1162 //       portability layer. To access methods and properties on the
1163 //       popup widgets, use |popupWindow| above.
1164 
PopupMenu(PopupMenuClient * client)1165 PopupMenu::PopupMenu(PopupMenuClient* client)
1166     : m_popupClient(client)
1167 {
1168 }
1169 
~PopupMenu()1170 PopupMenu::~PopupMenu()
1171 {
1172     hide();
1173 }
1174 
1175 // The Mac Chromium implementation relies on external control (a Cocoa control)
1176 // to display, handle the input tracking and menu item selection for the popup.
1177 // Windows and Linux Chromium let our WebKit port handle the display, while
1178 // another process manages the popup window and input handling.
show(const IntRect & r,FrameView * v,int index)1179 void PopupMenu::show(const IntRect& r, FrameView* v, int index)
1180 {
1181     if (!p.popup)
1182         p.popup = PopupContainer::create(client(), dropDownSettings);
1183 #if PLATFORM(DARWIN)
1184     p.popup->showExternal(r, v, index);
1185 #else
1186     p.popup->show(r, v, index);
1187 #endif
1188 }
1189 
hide()1190 void PopupMenu::hide()
1191 {
1192     if (p.popup)
1193         p.popup->hidePopup();
1194 }
1195 
updateFromElement()1196 void PopupMenu::updateFromElement()
1197 {
1198     p.popup->listBox()->updateFromElement();
1199 }
1200 
itemWritingDirectionIsNatural()1201 bool PopupMenu::itemWritingDirectionIsNatural()
1202 {
1203     return false;
1204 }
1205 
1206 } // namespace WebCore
1207