// Copyright (c) 2008, 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" #pragma warning(push, 0) #include "PopupMenu.h" #include "CharacterNames.h" #include "ChromeClientWin.h" #include "Document.h" #include "Font.h" #include "Frame.h" #include "FontSelector.h" #include "FramelessScrollView.h" #include "GraphicsContext.h" #include "IntRect.h" #include "Page.h" #include "PlatformKeyboardEvent.h" #include "PlatformMouseEvent.h" #include "PlatformScreen.h" #include "PlatformScrollbar.h" #include "PlatformWheelEvent.h" #include "SystemTime.h" #include "RenderBlock.h" #include "RenderTheme.h" #include "Widget.h" #include "WidgetClientWin.h" #pragma warning(pop) //#define LOG_ENABLE #include "LogWin.h" using namespace WTF; using namespace Unicode; using std::min; using std::max; namespace WebCore { typedef unsigned long long TimeStamp; static const int kMaxVisibleRows = 20; static const int kMaxHeight = 500; static const int kBorderSize = 1; static const TimeStamp kTypeAheadTimeoutMs = 1000; class PopupListBox; // This class holds a PopupListBox. Its sole purpose is to be able to draw // a border around its child. All its paint/event handling is just forwarded // to the child listBox (with the appropriate transforms). class PopupContainer : public FramelessScrollView { public: static HWND Create(PopupMenuClient* client); // FramelessScrollView virtual void paint(GraphicsContext* gc, const IntRect& rect); virtual void hide(); virtual bool handleMouseDownEvent(const PlatformMouseEvent& event); virtual bool handleMouseMoveEvent(const PlatformMouseEvent& event); virtual bool handleMouseReleaseEvent(const PlatformMouseEvent& event); virtual bool handleWheelEvent(const PlatformWheelEvent& event); virtual bool handleKeyEvent(const PlatformKeyboardEvent& event); // PopupContainer methods // Show the popup void showPopup(FrameView* view); // Hide the popup. Do not call this directly: use client->hidePopup(). void hidePopup(); // Compute size of widget and children. void layout(); PopupListBox* listBox() const { return m_listBox.get(); } private: PopupContainer(PopupMenuClient* client); ~PopupContainer(); // Paint the border. void paintBorder(GraphicsContext* gc, const IntRect& rect); RefPtr m_listBox; }; // This class uses WebCore code to paint and handle events for a drop-down list // box ("combobox" on Windows). class PopupListBox : public FramelessScrollView { public: // FramelessScrollView virtual void paint(GraphicsContext* gc, const IntRect& rect); virtual bool handleMouseDownEvent(const PlatformMouseEvent& event); virtual bool handleMouseMoveEvent(const PlatformMouseEvent& event); virtual bool handleMouseReleaseEvent(const PlatformMouseEvent& event); virtual bool handleWheelEvent(const PlatformWheelEvent& event); virtual bool handleKeyEvent(const PlatformKeyboardEvent& event); // PopupListBox methods // Show the popup void showPopup(); // Hide the popup. Do not call this directly: use client->hidePopup(). void hidePopup(); // Update our internal list to match the client. void updateFromElement(); // Free any allocated resources used in a particular popup session. void clear(); // Set the index of the option that is displayed in the widget. struct ListItem { ListItem(const String& label, ListItemType type) : label(label.copy()), type(type), y(0) {} String label; ListItemType type; int y; // y offset of this item, relative to the top of the popup. }; PopupListBox(PopupMenuClient* client) : m_originalIndex(0) , m_selectedIndex(0) , m_visibleRows(0) , m_popupClient(client) , m_repeatingChar(0) , m_lastCharTime(0) , m_acceptOnAbandon(false) { setScrollbarsMode(ScrollbarAlwaysOff); } ~PopupListBox() { clear(); } void disconnectClient() { m_popupClient = 0; } // Closes the popup void abandon(); // Select an index in the list, scrolling if necessary. void selectIndex(int index); // Accepts the selected index as the value to be displayed in the Vector m_items; // The dropdown control. int selectHeight = frameGeometry().height(); // Lay everything out to figure out our preferred size, then tell the view's // WidgetClient about it. It should assign us a client. layout(); WidgetClientWin* widgetClient = static_cast(view->client()); ChromeClientWin* chromeClient = static_cast(view->frame()->page()->chrome()->client()); if (widgetClient && chromeClient) { // If the popup would extend past the bottom of the screen, open upwards // instead. FloatRect screen = screenRect(view); IntRect widgetRect = chromeClient->windowToScreen(frameGeometry()); if (widgetRect.bottom() > static_cast(screen.bottom())) widgetRect.move(0, -(widgetRect.height() + selectHeight)); widgetClient->popupOpened(this, widgetRect); } // Must get called after we have a client and containingWindow. addChild(m_listBox.get()); // Enable scrollbars after the listbox is inserted into the hierarchy, so // it has a proper WidgetClient. m_listBox->setVScrollbarMode(ScrollbarAuto); m_listBox->scrollToRevealSelection(); invalidate(); } void PopupContainer::hidePopup() { invalidate(); m_listBox->disconnectClient(); removeChild(m_listBox.get()); if (client()) static_cast(client())->popupClosed(this); } void PopupContainer::layout() { m_listBox->layout(); // Place the listbox within our border. m_listBox->move(kBorderSize, kBorderSize); // Size ourselves to contain listbox + border. resize(m_listBox->width() + kBorderSize*2, m_listBox->height() + kBorderSize*2); invalidate(); } bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event) { return m_listBox->handleMouseDownEvent( constructRelativeMouseEvent(event, this, m_listBox.get())); } bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event) { return m_listBox->handleMouseMoveEvent( constructRelativeMouseEvent(event, this, m_listBox.get())); } bool PopupContainer::handleMouseReleaseEvent(const PlatformMouseEvent& event) { return m_listBox->handleMouseReleaseEvent( constructRelativeMouseEvent(event, this, m_listBox.get())); } bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event) { return m_listBox->handleWheelEvent( constructRelativeWheelEvent(event, this, m_listBox.get())); } bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event) { return m_listBox->handleKeyEvent(event); } void PopupContainer::hide() { m_listBox->abandon(); } void PopupContainer::paint(GraphicsContext* gc, const IntRect& rect) { // adjust coords for scrolled frame IntRect r = intersection(rect, frameGeometry()); int tx = x(); int ty = y(); r.move(-tx, -ty); gc->translate(static_cast(tx), static_cast(ty)); m_listBox->paint(gc, r); gc->translate(-static_cast(tx), -static_cast(ty)); paintBorder(gc, rect); } void PopupContainer::paintBorder(GraphicsContext* gc, const IntRect& rect) { // FIXME(mpcomplete): where do we get the border color from? Color borderColor(127, 157, 185); gc->setStrokeStyle(NoStroke); gc->setFillColor(borderColor); int tx = x(); int ty = y(); // top, left, bottom, right gc->drawRect(IntRect(tx, ty, width(), kBorderSize)); gc->drawRect(IntRect(tx, ty, kBorderSize, height())); gc->drawRect(IntRect(tx, ty + height() - kBorderSize, width(), kBorderSize)); gc->drawRect(IntRect(tx + width() - kBorderSize, ty, kBorderSize, height())); } /////////////////////////////////////////////////////////////////////////////// // PopupListBox implementation bool PopupListBox::handleMouseDownEvent(const PlatformMouseEvent& event) { PlatformScrollbar* scrollbar = scrollbarUnderMouse(event); if (scrollbar) { m_capturingScrollbar = scrollbar; m_capturingScrollbar->handleMousePressEvent(event); return true; } if (!isPointInBounds(event.pos())) abandon(); return true; } bool PopupListBox::handleMouseMoveEvent(const PlatformMouseEvent& event) { if (m_capturingScrollbar) { m_capturingScrollbar->handleMouseMoveEvent(event); return true; } PlatformScrollbar* scrollbar = scrollbarUnderMouse(event); if (m_lastScrollbarUnderMouse != scrollbar) { // Send mouse exited to the old scrollbar. if (m_lastScrollbarUnderMouse) m_lastScrollbarUnderMouse->handleMouseOutEvent(event); m_lastScrollbarUnderMouse = scrollbar; } if (scrollbar) { scrollbar->handleMouseMoveEvent(event); return true; } if (!isPointInBounds(event.pos())) return false; selectIndex(pointToRowIndex(event.pos())); return true; } bool PopupListBox::handleMouseReleaseEvent(const PlatformMouseEvent& event) { if (m_capturingScrollbar) { m_capturingScrollbar->handleMouseReleaseEvent(event); m_capturingScrollbar = 0; return true; } if (!isPointInBounds(event.pos())) return true; acceptIndex(pointToRowIndex(event.pos())); return true; } bool PopupListBox::handleWheelEvent(const PlatformWheelEvent& event) { if (!isPointInBounds(event.pos())) { abandon(); return true; } // Pass it off to the scroll view. // Sadly, WebCore devs don't understand the whole "const" thing. wheelEvent(const_cast(event)); return true; } bool PopupListBox::handleKeyEvent(const PlatformKeyboardEvent& event) { if (event.type() == PlatformKeyboardEvent::KeyUp) return true; if (numItems() == 0 && event.windowsVirtualKeyCode() != VK_ESCAPE) return true; int oldIndex = m_selectedIndex; switch (event.windowsVirtualKeyCode()) { case VK_ESCAPE: abandon(); // may delete this return true; case VK_RETURN: acceptIndex(m_selectedIndex); // may delete this return true; case VK_UP: adjustSelectedIndex(-1); break; case VK_DOWN: adjustSelectedIndex(1); break; case VK_PRIOR: adjustSelectedIndex(-m_visibleRows); break; case VK_NEXT: adjustSelectedIndex(m_visibleRows); break; case VK_HOME: adjustSelectedIndex(-m_selectedIndex); break; case VK_END: adjustSelectedIndex(m_items.size()); break; default: if (!event.ctrlKey() && !event.altKey() && !event.metaKey() && isPrintableChar(event.windowsVirtualKeyCode())) { 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. m_acceptOnAbandon = true; setOriginalIndex(m_selectedIndex); m_popupClient->setTextFromItem(m_selectedIndex); } return true; } // 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 ? !isspace(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; m_lastCharTime = now; UChar c = event.windowsVirtualKeyCode(); String prefix; int searchStartOffset = 1; if (delta > kTypeAheadTimeoutMs) { 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; } } int itemCount = numItems(); int index = (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).startsWith(prefix, false)) { selectIndex(index); return; } } } void PopupListBox::paint(GraphicsContext* gc, const IntRect& rect) { // adjust coords for scrolled frame IntRect r = intersection(rect, frameGeometry()); int tx = x() - contentsX(); int ty = y() - contentsY(); r.move(-tx, -ty); LOG(("PopupListBox::paint [%d,%d] [r: %d,%d,%d,%d]", tx, ty, r.x(), r.y(), r.width(), r.height())); // set clip rect to match revised damage rect gc->save(); gc->translate(static_cast(tx), static_cast(ty)); gc->clip(r); // TODO(mpcomplete): 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() == 0) gc->fillRect(r, Color::white); gc->restore(); ScrollView::paint(gc, rect); } static RenderStyle* getPopupClientStyleForRow(PopupMenuClient* client, int rowIndex) { RenderStyle* style = client->itemStyle(rowIndex); if (!style) style = client->clientStyle(); return style; } 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; RenderStyle* style = getPopupClientStyleForRow(m_popupClient, rowIndex); // Paint background Color backColor, textColor; if (rowIndex == m_selectedIndex) { backColor = theme()->activeListBoxSelectionBackgroundColor(); textColor = theme()->activeListBoxSelectionForegroundColor(); } else { backColor = m_popupClient->itemBackgroundColor(rowIndex); textColor = style->color(); } // If we have a transparent background, make sure it has a color to blend // against. if (backColor.hasAlpha()) gc->fillRect(rowRect, Color::white); gc->fillRect(rowRect, backColor); gc->setFillColor(textColor); LOG(("paintRow %d, [%d, %d, %d, %d] %x on %x", rowIndex, rowRect.x(), rowRect.y(), rowRect.width(), rowRect.height(), textColor.rgb(), backColor.rgb())); Font itemFont = getRowFont(rowIndex); gc->setFont(itemFont); // Bunch of shit to deal with RTL text... String itemText = m_popupClient->itemText(rowIndex); unsigned length = itemText.length(); const UChar* str = itemText.characters(); TextRun textRun(str, length, false, 0, 0, style->direction() == RTL, style->unicodeBidi() == Override); // Draw the item text // TODO(ojan): http://b/1210481 We should get the padding of individual option elements. rowRect.move(theme()->popupInternalPaddingLeft(style), itemFont.ascent()); if (style->direction() == RTL) { // Right-justify the text for RTL style. rowRect.move(rowRect.width() - itemFont.width(textRun) - 2 * theme()->popupInternalPaddingLeft(style), 0); } gc->drawBidiText(textRun, rowRect.location()); } Font PopupListBox::getRowFont(int rowIndex) { Font itemFont = m_popupClient->itemStyle(rowIndex)->font(); if (m_popupClient->itemIsLabel(rowIndex)) { // Bold-ify labels (ie, an heading). FontDescription d = itemFont.fontDescription(); d.setBold(true); Font font(d, itemFont.letterSpacing(), itemFont.wordSpacing()); font.update(0); return font; } return itemFont; } void PopupListBox::abandon() { RefPtr keepAlive(this); m_selectedIndex = m_originalIndex; if (m_acceptOnAbandon) m_popupClient->valueChanged(m_selectedIndex); // valueChanged may have torn down the popup! if (m_popupClient) m_popupClient->hidePopup(); } int PopupListBox::pointToRowIndex(const IntPoint& point) { int y = contentsY() + point.y(); // TODO(mpcomplete): binary search if perf matters. for (int i = 0; i < numItems(); ++i) { if (y < m_items[i]->y) return i-1; } // Last item? if (y < contentsHeight()) return m_items.size()-1; return -1; } void PopupListBox::acceptIndex(int index) { ASSERT(index >= 0 && index < numItems()); if (isSelectableItem(index)) { RefPtr keepAlive(this); // Tell the