/* * Copyright (C) 2006, 2007 Apple Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include "config.h" #include "PopupMenu.h" #include "Document.h" #include "FloatRect.h" #include "FontData.h" #include "Frame.h" #include "FrameView.h" #include "GraphicsContext.h" #include "HTMLNames.h" #include "Page.h" #include "PlatformMouseEvent.h" #include "PlatformScreen.h" #include "PlatformScrollBar.h" #include "RenderTheme.h" #include "RenderView.h" #include "TextStyle.h" #include #include using std::min; namespace WebCore { using namespace HTMLNames; // Default Window animation duration in milliseconds static const int defaultAnimationDuration = 200; // Maximum height of a popup window static const int maxPopupHeight = 320; static const int popupWindowAlphaPercent = 95; const int optionSpacingMiddle = 1; const int popupWindowBorderWidth = 1; static LPCTSTR kPopupWindowClassName = _T("PopupWindowClass"); static ATOM registerPopup(); static LRESULT CALLBACK PopupWndProc(HWND, UINT, WPARAM, LPARAM); PopupMenu::PopupMenu(PopupMenuClient* client) : m_popupClient(client) , m_scrollBar(0) , m_popup(0) , m_DC(0) , m_bmp(0) , m_wasClicked(false) , m_windowRect(IntRect()) , m_itemHeight(0) , m_scrollOffset(0) , m_wheelDelta(0) , m_focusedIndex(0) , m_scrollbarCapturingMouse(false) { } PopupMenu::~PopupMenu() { if (m_bmp) ::DeleteObject(m_bmp); if (m_DC) ::DeleteObject(m_DC); if (m_popup) ::DestroyWindow(m_popup); } void PopupMenu::show(const IntRect& r, FrameView* v, int index) { calculatePositionAndSize(r, v); if (m_windowRect.isEmpty()) return; if (!m_popup) { registerPopup(); DWORD exStyle = WS_EX_LAYERED | WS_EX_LTRREADING; // Even though we already know our size and location at this point, we pass (0,0,0,0) as our size/location here. // We need to wait until after the call to ::SetWindowLongPtr to set our size so that in our WM_SIZE handler we can get access to the PopupMenu object m_popup = ::CreateWindowEx(exStyle, kPopupWindowClassName, _T("PopupMenu"), WS_POPUP | WS_BORDER, 0, 0, 0, 0, v->containingWindow(), 0, 0, 0); if (!m_popup) return; ::SetWindowLongPtr(m_popup, 0, (LONG_PTR)this); ::SetLayeredWindowAttributes(m_popup, 0, (255 * popupWindowAlphaPercent) / 100, LWA_ALPHA); } if (!m_scrollBar) if (visibleItems() < client()->listSize()) { // We need a scroll bar m_scrollBar = new PlatformScrollbar(this, VerticalScrollbar, SmallScrollbar); m_scrollBar->setContainingWindow(m_popup); } ::SetWindowPos(m_popup, HWND_TOP, m_windowRect.x(), m_windowRect.y(), m_windowRect.width(), m_windowRect.height(), 0); // Determine whether we should animate our popups // Note: Must use 'BOOL' and 'FALSE' instead of 'bool' and 'false' to avoid stack corruption with SystemParametersInfo BOOL shouldAnimate = FALSE; #ifdef CAN_ANIMATE_TRANSPARENT_WINDOWS_SMOOTHLY ::SystemParametersInfo(SPI_GETCOMBOBOXANIMATION, 0, &shouldAnimate, 0); #endif if (shouldAnimate) { RECT viewRect = {0}; ::GetWindowRect(v->containingWindow(), &viewRect); if (!::IsRectEmpty(&viewRect)) { // Popups should slide into view away from the text // Note: We can't add paddingRight() because that value includes the width // of the dropdown button, so we must use our own endOfLinePadding constant. popupWidth += endOfLinePadding + client()->clientPaddingLeft(); // Leave room for the border popupWidth += 2 * popupWindowBorderWidth; popupHeight += 2 * popupWindowBorderWidth; // The popup should be at least as wide as the control on the page popupWidth = max(rScreenCoords.width(), popupWidth); // Always left-align items in the popup. This matches popup menus on the mac. int popupX = rScreenCoords.x(); IntRect popupRect(popupX, rScreenCoords.bottom(), popupWidth, popupHeight); // The popup needs to stay within the bounds of the screen and not overlap any toolbars FloatRect screen = screenAvailableRect(v); // Check that we don't go off the screen vertically if (popupRect.bottom() > screen.height()) { // The popup will go off the screen, so try placing it above the client if (rScreenCoords.y() - popupRect.height() < 0) { // The popup won't fit above, either, so place it whereever's bigger and resize it to fit if ((rScreenCoords.y() + rScreenCoords.height() / 2) < (screen.height() / 2)) { // Below is bigger popupRect.setHeight(screen.height() - popupRect.y()); } else { // Above is bigger popupRect.setY(0); popupRect.setHeight(rScreenCoords.y()); } } else { // The popup fits above, so reposition it popupRect.setY(rScreenCoords.y() - popupRect.height()); } } // Check that we don't go off the screen horizontally if (popupRect.x() < screen.x()) { popupRect.setWidth(popupRect.width() - (screen.x() - popupRect.x())); popupRect.setX(screen.x()); } m_windowRect = popupRect; return; } bool PopupMenu::setFocusedIndex(int i, bool hotTracking) { if (i < 0 || i >= client()->listSize() || i == focusedIndex()) return false; if (!client()->itemIsEnabled(i)) return false; invalidateItem(focusedIndex()); invalidateItem(i); m_focusedIndex = i; if (!hotTracking) client()->setTextFromItem(i); if (!scrollToRevealSelection()) ::UpdateWindow(m_popup); return true; } int PopupMenu::visibleItems() const { return m_windowRect.height() / m_itemHeight; } int PopupMenu::listIndexAtPoint(const IntPoint& point) const { return m_scrollOffset + point.y() / m_itemHeight; } int PopupMenu::focusedIndex() const { return m_focusedIndex; } void PopupMenu::focusFirst() { if (!client()) return; int size = client()->listSize(); for (int i = 0; i < size; ++i) if (client()->itemIsEnabled(i)) { setFocusedIndex(i); break; } } void PopupMenu::focusLast() { if (!client()) return; int size = client()->listSize(); for (int i = size - 1; i > 0; --i) if (client()->itemIsEnabled(i)) { setFocusedIndex(i); break; } } bool PopupMenu::down(unsigned lines) { if (!client()) return false; int size = client()->listSize(); int lastSelectableIndex, selectedListIndex; lastSelectableIndex = selectedListIndex = focusedIndex(); for (int i = selectedListIndex + 1; i >= 0 && i < size; ++i) if (client()->itemIsEnabled(i)) { lastSelectableIndex = i; if (i >= selectedListIndex + (int)lines) break; } return setFocusedIndex(lastSelectableIndex); } bool PopupMenu::up(unsigned lines) { if (!client()) return false; int size = client()->listSize(); int lastSelectableIndex, selectedListIndex; lastSelectableIndex = selectedListIndex = focusedIndex(); for (int i = selectedListIndex - 1; i >= 0 && i < size; --i) if (client()->itemIsEnabled(i)) { lastSelectableIndex = i; if (i <= selectedListIndex - (int)lines) break; } return setFocusedIndex(lastSelectableIndex); } void PopupMenu::invalidateItem(int index) { if (!m_popup) return; IntRect damageRect(clientRect()); damageRect.setY(m_itemHeight * (index - m_scrollOffset)); damageRect.setHeight(m_itemHeight); if (m_scrollBar) damageRect.setWidth(damageRect.width() - m_scrollBar->frameGeometry().width()); RECT r = damageRect; ::InvalidateRect(m_popup, &r, TRUE); } IntRect PopupMenu::clientRect() const { IntRect clientRect = m_windowRect; clientRect.inflate(-popupWindowBorderWidth); clientRect.setLocation(IntPoint(0, 0)); return clientRect; } void PopupMenu::incrementWheelDelta(int delta) { m_wheelDelta += delta; } void PopupMenu::reduceWheelDelta(int delta) { ASSERT(delta >= 0); ASSERT(delta <= abs(m_wheelDelta)); if (m_wheelDelta > 0) m_wheelDelta -= delta; else if (m_wheelDelta < 0) m_wheelDelta += delta; else return; } bool PopupMenu::scrollToRevealSelection() { if (!m_scrollBar) return false; int index = focusedIndex(); if (index < m_scrollOffset) { m_scrollBar->setValue(index); return true; } if (index >= m_scrollOffset + visibleItems()) { m_scrollBar->setValue(index - visibleItems() + 1); return true; } return false; } void PopupMenu::updateFromElement() { if (!m_popup) return; ::InvalidateRect(m_popup, 0, TRUE); if (!scrollToRevealSelection()) ::UpdateWindow(m_popup); } bool PopupMenu::itemWritingDirectionIsNatural() { return false; } const int separatorPadding = 4; const int separatorHeight = 1; void PopupMenu::paint(const IntRect& damageRect, HDC hdc) { if (!m_popup) return; if (!m_DC) { m_DC = ::CreateCompatibleDC(::GetDC(m_popup)); if (!m_DC) return; } if (!m_bmp) { BITMAPINFO bitmapInfo; bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bitmapInfo.bmiHeader.biWidth = clientRect().width(); bitmapInfo.bmiHeader.biHeight = -clientRect().height(); bitmapInfo.bmiHeader.biPlanes = 1; bitmapInfo.bmiHeader.biBitCount = 32; bitmapInfo.bmiHeader.biCompression = BI_RGB; bitmapInfo.bmiHeader.biSizeImage = 0; bitmapInfo.bmiHeader.biXPelsPerMeter = 0; bitmapInfo.bmiHeader.biYPelsPerMeter = 0; bitmapInfo.bmiHeader.biClrUsed = 0; bitmapInfo.bmiHeader.biClrImportant = 0; void* pixels = 0; m_bmp = ::CreateDIBSection(m_DC, &bitmapInfo, DIB_RGB_COLORS, &pixels, 0, 0); if (!m_bmp) return; ::SelectObject(m_DC, m_bmp); } GraphicsContext context(m_DC); int itemCount = client()->listSize(); // listRect is the damageRect translated into the coordinates of the entire menu list (which is itemCount * m_itemHeight pixels tall) IntRect listRect = damageRect; listRect.move(IntSize(0, m_scrollOffset * m_itemHeight)); RenderStyle* clientStyle = client()->clientStyle(); for (int y = listRect.y(); y < listRect.bottom(); y += m_itemHeight) { int index = y / m_itemHeight; Color optionBackgroundColor, optionTextColor; RenderStyle* itemStyle = client()->itemStyle(index); if (index == focusedIndex()) { optionBackgroundColor = theme()->activeListBoxSelectionBackgroundColor(); optionTextColor = theme()->activeListBoxSelectionForegroundColor(); } else { optionBackgroundColor = client()->itemBackgroundColor(index); optionTextColor = itemStyle->color(); } // itemRect is in client coordinates IntRect itemRect(0, (index - m_scrollOffset) * m_itemHeight, damageRect.width(), m_itemHeight); // Draw the background for this menu item if (itemStyle->visibility() != HIDDEN) context.fillRect(itemRect, optionBackgroundColor); if (client()->itemIsSeparator(index)) { IntRect separatorRect(itemRect.x() + separatorPadding, itemRect.y() + (itemRect.height() - separatorHeight) / 2, itemRect.width() - 2 * separatorPadding, separatorHeight); context.fillRect(separatorRect, optionTextColor); continue; } String itemText = client()->itemText(index); unsigned length = itemText.length(); const UChar* string = itemText.characters(); TextStyle textStyle(0, 0, 0, clientStyle->direction() == RTL, clientStyle->unicodeBidi() == Override); TextRun textRun(string, length); context.setFillColor(optionTextColor); Font itemFont = client()->clientStyle()->font(); if (client()->itemIsLabel(index)) { FontDescription d = itemFont.fontDescription(); d.setBold(true); itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing()); itemFont.update(); } context.setFont(itemFont); // Draw the item text if (itemStyle->visibility() != HIDDEN) { int textX = client()->clientPaddingLeft(); int textY = itemRect.y() + itemFont.ascent() + (itemRect.height() - itemFont.height()) / 2; context.drawBidiText(textRun, IntPoint(textX, textY), textStyle); } } if (m_scrollBar) m_scrollBar->paint(&context, damageRect); if (!hdc) hdc = ::GetDC(m_popup); ::BitBlt(hdc, damageRect.x(), damageRect.y(), damageRect.width(), damageRect.height(), m_DC, damageRect.x(), damageRect.y(), SRCCOPY); } void PopupMenu::valueChanged(Scrollbar* scrollBar) { ASSERT(m_scrollBar); if (!m_popup) return; int offset = scrollBar->value(); if (m_scrollOffset == offset) return; int scrolledLines = m_scrollOffset - offset; m_scrollOffset = offset; UINT flags = SW_INVALIDATE; #ifdef CAN_SET_SMOOTH_SCROLLING_DURATION BOOL shouldSmoothScroll = FALSE; ::SystemParametersInfo(SPI_GETLISTBOXSMOOTHSCROLLING, 0, &shouldSmoothScroll, 0); if (shouldSmoothScroll) flags |= MAKEWORD(SW_SMOOTHSCROLL, smoothScrollAnimationDuration); #endif IntRect listRect = clientRect(); if (m_scrollBar) listRect.setWidth(listRect.width() - m_scrollBar->frameGeometry().width()); RECT r = listRect; ::ScrollWindowEx(m_popup, 0, scrolledLines * m_itemHeight, &r, 0, 0, 0, flags); if (m_scrollBar) { r = m_scrollBar->frameGeometry(); ::InvalidateRect(m_popup, &r, TRUE); } ::UpdateWindow(m_popup); } IntRect PopupMenu::windowClipRect() const { return m_windowRect; } static ATOM registerPopup() { static bool haveRegisteredWindowClass = false; if (haveRegisteredWindowClass) return true; WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = 0; wcex.lpfnWndProc = PopupWndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = sizeof(PopupMenu*); // For the PopupMenu pointer wcex.hInstance = Page::instanceHandle(); wcex.hIcon = 0; wcex.hCursor = LoadCursor(0, IDC_ARROW); wcex.hbrBackground = 0; wcex.lpszMenuName = 0; wcex.lpszClassName = kPopupWindowClassName; wcex.hIconSm = 0; haveRegisteredWindowClass = true; return ::RegisterClassEx(&wcex); } const int smoothScrollAnimationDuration = 5000; static LRESULT CALLBACK PopupWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT lResult = 0; LONG_PTR longPtr = GetWindowLongPtr(hWnd, 0); PopupMenu* popup = reinterpret_cast(longPtr); switch (message) { case WM_SIZE: if (popup && popup->scrollBar()) { IntSize size(LOWORD(lParam), HIWORD(lParam)); popup->scrollBar()->setRect(IntRect(size.width() - popup->scrollBar()->width(), 0, popup->scrollBar()->width(), size.height())); int visibleItems = popup->visibleItems(); popup->scrollBar()->setEnabled(visibleItems < popup->client()->listSize()); popup->scrollBar()->setSteps(1, max(1, visibleItems - 1)); popup->scrollBar()->setProportion(visibleItems, popup->client()->listSize()); } break; case WM_ACTIVATE: if (popup && popup->client() && wParam == WA_INACTIVE) popup->client()->hidePopup(); break; case WM_KILLFOCUS: if (popup && popup->client() && (HWND)wParam != popup->popupHandle()) // Focus is going elsewhere, so hide popup->client()->hidePopup(); break; case WM_KEYDOWN: if (popup && popup->client()) { lResult = 0; switch (LOWORD(wParam)) { case VK_DOWN: case VK_RIGHT: popup->down(); break; case VK_UP: case VK_LEFT: popup->up(); break; case VK_HOME: popup->focusFirst(); break; case VK_END: popup->focusLast(); break; case VK_PRIOR: if (popup->focusedIndex() != popup->scrollOffset()) { // Set the selection to the first visible item int firstVisibleItem = popup->scrollOffset(); popup->up(popup->focusedIndex() - firstVisibleItem); } else // The first visible item is selected, so move the selection back one page popup->up(popup->visibleItems()); break; case VK_NEXT: if (popup) { int lastVisibleItem = popup->scrollOffset() + popup->visibleItems() - 1; if (popup->focusedIndex() != lastVisibleItem) { // Set the selection to the last visible item popup->down(lastVisibleItem - popup->focusedIndex()); } else // The last visible item is selected, so move the selection forward one page popup->down(popup->visibleItems()); } break; case VK_TAB: ::SendMessage(popup->client()->clientDocument()->view()->containingWindow(), message, wParam, lParam); popup->client()->hidePopup(); break; default: if (isprint(::MapVirtualKey(LOWORD(wParam), 2))) // Send the keydown to the WebView so it can be used for type-ahead find ::SendMessage(popup->client()->clientDocument()->view()->containingWindow(), message, wParam, lParam); else lResult = 1; break; } } break; case WM_CHAR: if (popup && popup->client()) { lResult = 0; int index; switch (wParam) { case 0x0D: // Enter/Return popup->client()->hidePopup(); index = popup->focusedIndex(); ASSERT(index >= 0); popup->client()->valueChanged(index); break; case 0x1B: // Escape popup->client()->hidePopup(); break; case 0x09: // TAB case 0x08: // Backspace case 0x0A: // Linefeed default: // Character lResult = 1; break; } } break; case WM_MOUSEMOVE: if (popup) { IntPoint mousePoint(MAKEPOINTS(lParam)); if (popup->scrollBar()) { IntRect scrollBarRect = popup->scrollBar()->frameGeometry(); if (popup->scrollbarCapturingMouse() || scrollBarRect.contains(mousePoint)) { // Put the point into coordinates relative to the scroll bar mousePoint.move(-scrollBarRect.x(), -scrollBarRect.y()); PlatformMouseEvent event(hWnd, message, wParam, MAKELPARAM(mousePoint.x(), mousePoint.y())); popup->scrollBar()->handleMouseMoveEvent(event); break; } } BOOL shouldHotTrack = FALSE; ::SystemParametersInfo(SPI_GETHOTTRACKING, 0, &shouldHotTrack, 0); if (shouldHotTrack || wParam & MK_LBUTTON) popup->setFocusedIndex(popup->listIndexAtPoint(mousePoint), true); // Release capture if the left button isn't down, and the mousePoint is outside the popup window. // This way, the WebView will get future mouse events in the rest of the window. RECT bounds; GetClientRect(popup->popupHandle(), &bounds); if (!(wParam & MK_LBUTTON) && !::PtInRect(&bounds, mousePoint)) { ::ReleaseCapture(); break; } } break; case WM_LBUTTONDOWN: if (popup) { ::SetCapture(popup->popupHandle()); IntPoint mousePoint(MAKEPOINTS(lParam)); if (popup->scrollBar()) { IntRect scrollBarRect = popup->scrollBar()->frameGeometry(); if (scrollBarRect.contains(mousePoint)) { // Put the point into coordinates relative to the scroll bar mousePoint.move(-scrollBarRect.x(), -scrollBarRect.y()); PlatformMouseEvent event(hWnd, message, wParam, MAKELPARAM(mousePoint.x(), mousePoint.y())); popup->scrollBar()->handleMousePressEvent(event); popup->setScrollbarCapturingMouse(true); break; } } popup->setFocusedIndex(popup->listIndexAtPoint(mousePoint), true); } break; case WM_LBUTTONUP: if (popup) { IntPoint mousePoint(MAKEPOINTS(lParam)); if (popup->scrollBar()) { ::ReleaseCapture(); IntRect scrollBarRect = popup->scrollBar()->frameGeometry(); if (popup->scrollbarCapturingMouse() || scrollBarRect.contains(mousePoint)) { popup->setScrollbarCapturingMouse(false); // Put the point into coordinates relative to the scroll bar mousePoint.move(-scrollBarRect.x(), -scrollBarRect.y()); PlatformMouseEvent event(hWnd, message, wParam, MAKELPARAM(mousePoint.x(), mousePoint.y())); popup->scrollBar()->handleMouseReleaseEvent(event); // FIXME: This is a hack to work around PlatformScrollbar not invalidating correctly when it doesn't have a parent widget RECT r = scrollBarRect; ::InvalidateRect(popup->popupHandle(), &r, TRUE); break; } } // Only release capture and hide the popup if the mouse is inside the popup window. RECT bounds; GetClientRect(popup->popupHandle(), &bounds); if (popup->client() && ::PtInRect(&bounds, mousePoint)) { ::ReleaseCapture(); popup->client()->hidePopup(); int index = popup->focusedIndex(); if (index >= 0) popup->client()->valueChanged(index); } } break; case WM_MOUSEWHEEL: if (popup && popup->scrollBar()) { int i = 0; for (popup->incrementWheelDelta(GET_WHEEL_DELTA_WPARAM(wParam)); abs(popup->wheelDelta()) >= WHEEL_DELTA; popup->reduceWheelDelta(WHEEL_DELTA)) if (popup->wheelDelta() > 0) ++i; else --i; popup->scrollBar()->scroll(i > 0 ? ScrollUp : ScrollDown, ScrollByLine, abs(i)); } break; case WM_PAINT: if (popup) { PAINTSTRUCT paintInfo; ::BeginPaint(popup->popupHandle(), &paintInfo); popup->paint(paintInfo.rcPaint, paintInfo.hdc); ::EndPaint(popup->popupHandle(), &paintInfo); lResult = 0; } break; case WM_PRINTCLIENT: if (popup) popup->paint(popup->clientRect(), (HDC)wParam); break; default: lResult = DefWindowProc(hWnd, message, wParam, lParam); } return lResult; } }