/* * Copyright (c) 2011, Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "PopupListBox.h" #include "CSSValueKeywords.h" #include "PopupContainer.h" #include "PopupMenuChromium.h" #include "RuntimeEnabledFeatures.h" #include "core/rendering/RenderTheme.h" #include "platform/KeyboardCodes.h" #include "platform/PlatformGestureEvent.h" #include "platform/PlatformKeyboardEvent.h" #include "platform/PlatformMouseEvent.h" #include "platform/PlatformScreen.h" #include "platform/PlatformTouchEvent.h" #include "platform/PlatformWheelEvent.h" #include "platform/PopupMenuClient.h" #include "platform/fonts/Font.h" #include "platform/fonts/FontCache.h" #include "platform/fonts/FontSelector.h" #include "platform/geometry/IntRect.h" #include "platform/graphics/GraphicsContext.h" #include "platform/scroll/FramelessScrollViewClient.h" #include "platform/scroll/ScrollbarTheme.h" #include "platform/text/StringTruncator.h" #include "platform/text/TextRun.h" #include "wtf/ASCIICType.h" #include "wtf/CurrentTime.h" #include namespace WebCore { using namespace WTF::Unicode; static const int labelToIconPadding = 5; // Padding height put at the top and bottom of each line. static const int autofillLinePaddingHeight = 3; const int PopupListBox::defaultMaxHeight = 500; static const int maxVisibleRows = 20; static const int minEndOfLinePadding = 2; static const int textToLabelPadding = 10; static const TimeStamp typeAheadTimeoutMs = 1000; PopupListBox::PopupListBox(PopupMenuClient* client, const PopupContainerSettings& settings) : m_settings(settings) , m_originalIndex(0) , m_selectedIndex(0) , m_acceptedIndexOnAbandon(-1) , m_visibleRows(0) , m_baseWidth(0) , m_maxHeight(defaultMaxHeight) , m_popupClient(client) , m_repeatingChar(0) , m_lastCharTime(0) , m_maxWindowWidth(std::numeric_limits::max()) { setScrollbarModes(ScrollbarAlwaysOff, ScrollbarAlwaysOff); } bool PopupListBox::handleMouseDownEvent(const PlatformMouseEvent& event) { Scrollbar* scrollbar = scrollbarAtPoint(event.position()); if (scrollbar) { m_capturingScrollbar = scrollbar; m_capturingScrollbar->mouseDown(event); return true; } if (!isPointInBounds(event.position())) abandon(); return true; } bool PopupListBox::handleMouseMoveEvent(const PlatformMouseEvent& event) { if (m_capturingScrollbar) { m_capturingScrollbar->mouseMoved(event); return true; } Scrollbar* scrollbar = scrollbarAtPoint(event.position()); if (m_lastScrollbarUnderMouse != scrollbar) { // Send mouse exited to the old scrollbar. if (m_lastScrollbarUnderMouse) m_lastScrollbarUnderMouse->mouseExited(); m_lastScrollbarUnderMouse = scrollbar; } if (scrollbar) { scrollbar->mouseMoved(event); return true; } if (!isPointInBounds(event.position())) return false; selectIndex(pointToRowIndex(event.position())); return true; } bool PopupListBox::handleMouseReleaseEvent(const PlatformMouseEvent& event) { if (m_capturingScrollbar) { m_capturingScrollbar->mouseUp(event); m_capturingScrollbar = 0; return true; } if (!isPointInBounds(event.position())) return true; // Need to check before calling acceptIndex(), because m_popupClient might // be removed in acceptIndex() calling because of event handler. bool isSelectPopup = m_popupClient->menuStyle().menuType() == PopupMenuStyle::SelectPopup; if (acceptIndex(pointToRowIndex(event.position())) && m_focusedElement && isSelectPopup) { m_focusedElement->dispatchMouseEvent(event, EventTypeNames::mouseup); m_focusedElement->dispatchMouseEvent(event, EventTypeNames::click); // Clear m_focusedElement here, because we cannot clear in hidePopup() // which is called before dispatchMouseEvent() is called. m_focusedElement = 0; } return true; } bool PopupListBox::handleWheelEvent(const PlatformWheelEvent& event) { if (!isPointInBounds(event.position())) { abandon(); return true; } ScrollableArea::handleWheelEvent(event); return true; } // Should be kept in sync with handleKeyEvent(). bool PopupListBox::isInterestedInEventForKey(int keyCode) { switch (keyCode) { case VKEY_ESCAPE: case VKEY_RETURN: case VKEY_UP: case VKEY_DOWN: case VKEY_PRIOR: case VKEY_NEXT: case VKEY_HOME: case VKEY_END: case VKEY_TAB: return true; default: return false; } } bool PopupListBox::handleTouchEvent(const PlatformTouchEvent&) { return false; } bool PopupListBox::handleGestureEvent(const PlatformGestureEvent&) { return false; } static bool isCharacterTypeEvent(const PlatformKeyboardEvent& event) { // Check whether the event is a character-typed event or not. // We use RawKeyDown/Char/KeyUp event scheme on all platforms, // so PlatformKeyboardEvent::Char (not RawKeyDown) type event // is considered as character type event. return event.type() == PlatformEvent::Char; } bool PopupListBox::handleKeyEvent(const PlatformKeyboardEvent& event) { if (event.type() == PlatformEvent::KeyUp) return true; if (!numItems() && event.windowsVirtualKeyCode() != VKEY_ESCAPE) return true; switch (event.windowsVirtualKeyCode()) { case VKEY_ESCAPE: abandon(); // may delete this return true; case VKEY_RETURN: if (m_selectedIndex == -1) { hidePopup(); // Don't eat the enter if nothing is selected. return false; } acceptIndex(m_selectedIndex); // may delete this return true; case VKEY_UP: case VKEY_DOWN: // We have to forward only shift + up combination to focused node when // autofill popup. Because all characters from the cursor to the start // of the text area should selected when you press shift + up arrow. // shift + down should be the similar way to shift + up. if (event.modifiers() && m_popupClient->menuStyle().menuType() == PopupMenuStyle::AutofillPopup) m_focusedElement->dispatchKeyEvent(event); else if (event.windowsVirtualKeyCode() == VKEY_UP) selectPreviousRow(); else selectNextRow(); break; case VKEY_PRIOR: adjustSelectedIndex(-m_visibleRows); break; case VKEY_NEXT: adjustSelectedIndex(m_visibleRows); break; case VKEY_HOME: adjustSelectedIndex(-m_selectedIndex); break; case VKEY_END: adjustSelectedIndex(m_items.size()); break; default: if (!event.ctrlKey() && !event.altKey() && !event.metaKey() && isPrintableChar(event.windowsVirtualKeyCode()) && isCharacterTypeEvent(event)) typeAheadFind(event); break; } if (m_originalIndex != m_selectedIndex) { // Keyboard events should update the selection immediately (but we don't // want to fire the onchange event until the popup is closed, to match // IE). We change the original index so we revert to that when the // popup is closed. if (m_settings.acceptOnAbandon) m_acceptedIndexOnAbandon = m_selectedIndex; setOriginalIndex(m_selectedIndex); if (m_settings.setTextOnIndexChange) m_popupClient->setTextFromItem(m_selectedIndex); } if (event.windowsVirtualKeyCode() == VKEY_TAB) { // TAB is a special case as it should select the current item if any and // advance focus. if (m_selectedIndex >= 0) { acceptIndex(m_selectedIndex); // May delete us. // Return false so the TAB key event is propagated to the page. return false; } // Call abandon() so we honor m_acceptedIndexOnAbandon if set. abandon(); // Return false so the TAB key event is propagated to the page. return false; } return true; } HostWindow* PopupListBox::hostWindow() const { // Our parent is the root ScrollView, so it is the one that has a // HostWindow. FrameView::hostWindow() works similarly. return parent() ? parent()->hostWindow() : 0; } bool PopupListBox::shouldPlaceVerticalScrollbarOnLeft() const { return m_popupClient->menuStyle().textDirection() == RTL; } // From HTMLSelectElement.cpp static String stripLeadingWhiteSpace(const String& string) { int length = string.length(); int i; for (i = 0; i < length; ++i) if (string[i] != noBreakSpace && (string[i] <= 0x7F ? !isASCIISpace(string[i]) : (direction(string[i]) != WhiteSpaceNeutral))) break; return string.substring(i, length - i); } // From HTMLSelectElement.cpp, with modifications void PopupListBox::typeAheadFind(const PlatformKeyboardEvent& event) { TimeStamp now = static_cast(currentTime() * 1000.0f); TimeStamp delta = now - m_lastCharTime; // Reset the time when user types in a character. The time gap between // last character and the current character is used to indicate whether // user typed in a string or just a character as the search prefix. m_lastCharTime = now; UChar c = event.windowsVirtualKeyCode(); String prefix; int searchStartOffset = 1; if (delta > typeAheadTimeoutMs) { m_typedString = prefix = String(&c, 1); m_repeatingChar = c; } else { m_typedString.append(c); if (c == m_repeatingChar) { // The user is likely trying to cycle through all the items starting // with this character, so just search on the character. prefix = String(&c, 1); } else { m_repeatingChar = 0; prefix = m_typedString; searchStartOffset = 0; } } // Compute a case-folded copy of the prefix string before beginning the // search for a matching element. This code uses foldCase to work around the // fact that String::startWith does not fold non-ASCII characters. This code // can be changed to use startWith once that is fixed. String prefixWithCaseFolded(prefix.foldCase()); int itemCount = numItems(); int index = (max(0, m_selectedIndex) + searchStartOffset) % itemCount; for (int i = 0; i < itemCount; i++, index = (index + 1) % itemCount) { if (!isSelectableItem(index)) continue; if (stripLeadingWhiteSpace(m_items[index]->label).foldCase().startsWith(prefixWithCaseFolded)) { selectIndex(index); return; } } } void PopupListBox::paint(GraphicsContext* gc, const IntRect& rect) { // Adjust coords for scrolled frame. IntRect r = intersection(rect, frameRect()); int tx = x() - scrollX() + ((shouldPlaceVerticalScrollbarOnLeft() && verticalScrollbar()) ? verticalScrollbar()->width() : 0); int ty = y() - scrollY(); r.move(-tx, -ty); // Set clip rect to match revised damage rect. gc->save(); gc->translate(static_cast(tx), static_cast(ty)); gc->clip(r); // FIXME: Can we optimize scrolling to not require repainting the entire // window? Should we? for (int i = 0; i < numItems(); ++i) paintRow(gc, r, i); // Special case for an empty popup. if (!numItems()) gc->fillRect(r, Color::white); gc->restore(); ScrollView::paint(gc, rect); } static const int separatorPadding = 4; static const int separatorHeight = 1; void PopupListBox::paintRow(GraphicsContext* gc, const IntRect& rect, int rowIndex) { // This code is based largely on RenderListBox::paint* methods. IntRect rowRect = getRowBounds(rowIndex); if (!rowRect.intersects(rect)) return; PopupMenuStyle style = m_popupClient->itemStyle(rowIndex); // Paint background Color backColor, textColor, labelColor; if (rowIndex == m_selectedIndex) { backColor = RenderTheme::theme().activeListBoxSelectionBackgroundColor(); textColor = RenderTheme::theme().activeListBoxSelectionForegroundColor(); labelColor = textColor; } else { backColor = style.backgroundColor(); textColor = style.foregroundColor(); #if OS(LINUX) || OS(ANDROID) // On other platforms, the