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