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
59using namespace WTF;
60using namespace Unicode;
61
62using std::min;
63using std::max;
64
65namespace WebCore {
66
67typedef unsigned long long TimeStamp;
68
69static const int kMaxVisibleRows = 20;
70static const int kMaxHeight = 500;
71static const int kBorderSize = 1;
72static const TimeStamp kTypeAheadTimeoutMs = 1000;
73
74// The settings used for the drop down menu.
75// This is the delegate used if none is provided.
76static 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).
88class PopupListBox : public FramelessScrollView {
89public:
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
146private:
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
267static 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
281static 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
299PassRefPtr<PopupContainer> PopupContainer::create(PopupMenuClient* client,
300                                                  const PopupContainerSettings& settings)
301{
302    return adoptRef(new PopupContainer(client, settings));
303}
304
305PopupContainer::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
313PopupContainer::~PopupContainer()
314{
315    if (m_listBox && m_listBox->parent())
316        removeChild(m_listBox.get());
317}
318
319void 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
353void 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
382void PopupContainer::hidePopup()
383{
384    listBox()->hidePopup();
385}
386
387void 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
400bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event)
401{
402    return m_listBox->handleMouseDownEvent(
403        constructRelativeMouseEvent(event, this, m_listBox.get()));
404}
405
406bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event)
407{
408    return m_listBox->handleMouseMoveEvent(
409        constructRelativeMouseEvent(event, this, m_listBox.get()));
410}
411
412bool PopupContainer::handleMouseReleaseEvent(const PlatformMouseEvent& event)
413{
414    return m_listBox->handleMouseReleaseEvent(
415        constructRelativeMouseEvent(event, this, m_listBox.get()));
416}
417
418bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event)
419{
420    return m_listBox->handleWheelEvent(
421        constructRelativeWheelEvent(event, this, m_listBox.get()));
422}
423
424bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event)
425{
426    return m_listBox->handleKeyEvent(event);
427}
428
429void PopupContainer::hide()
430{
431    m_listBox->abandon();
432}
433
434void 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
450void 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
468bool PopupContainer::isInterestedInEventForKey(int keyCode)
469{
470    return m_listBox->isInterestedInEventForKey(keyCode);
471}
472
473void 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
498void PopupContainer::refresh()
499{
500    listBox()->updateFromElement();
501    layout();
502}
503
504int PopupContainer::selectedIndex() const
505{
506    return m_listBox->selectedIndex();
507}
508
509int PopupContainer::menuItemHeight() const
510{
511    return m_listBox->getRowHeight(0);
512}
513
514const WTF::Vector<PopupItem*>& PopupContainer:: popupData() const
515{
516    return m_listBox->items();
517}
518
519///////////////////////////////////////////////////////////////////////////////
520// PopupListBox implementation
521
522bool 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
537bool 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
564bool 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
579bool 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().
593bool 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
611static 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
620bool 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
690HostWindow* 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
698static 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
711void 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
759void 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
787static const int separatorPadding = 4;
788static const int separatorHeight = 1;
789
790void 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
866Font 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
881void 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
895int 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
912void 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
940void 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
954void PopupListBox::setOriginalIndex(int index)
955{
956    m_originalIndex = m_selectedIndex = index;
957}
958
959int PopupListBox::getRowHeight(int index)
960{
961    if (index < 0)
962        return 0;
963
964    return getRowFont(index).height();
965}
966
967IntRect 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
975void 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
985void 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
1001bool 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
1007void PopupListBox::clearSelection()
1008{
1009    if (m_selectedIndex != -1) {
1010        invalidateRow(m_selectedIndex);
1011        m_selectedIndex = -1;
1012    }
1013}
1014
1015void 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
1026void 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
1044void 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
1079void 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
1090void 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
1113void 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
1193void 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
1200bool 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
1212PopupMenu::PopupMenu(PopupMenuClient* client)
1213    : m_popupClient(client)
1214{
1215}
1216
1217PopupMenu::~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.
1226void 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
1237void PopupMenu::hide()
1238{
1239    if (p.popup)
1240        p.popup->hide();
1241}
1242
1243void PopupMenu::updateFromElement()
1244{
1245    p.popup->listBox()->updateFromElement();
1246}
1247
1248bool PopupMenu::itemWritingDirectionIsNatural()
1249{
1250    return false;
1251}
1252
1253} // namespace WebCore
1254