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