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 "ChromeClientChromium.h"
36 #include "Font.h"
37 #include "FontSelector.h"
38 #include "FrameView.h"
39 #include "Frame.h"
40 #include "FramelessScrollView.h"
41 #include "FramelessScrollViewClient.h"
42 #include "GraphicsContext.h"
43 #include "IntRect.h"
44 #include "KeyboardCodes.h"
45 #include "Page.h"
46 #include "PlatformKeyboardEvent.h"
47 #include "PlatformMouseEvent.h"
48 #include "PlatformScreen.h"
49 #include "PlatformWheelEvent.h"
50 #include "PopupMenu.h"
51 #include "RenderTheme.h"
52 #include "ScrollbarTheme.h"
53 #include "SystemTime.h"
54
55 #include <wtf/CurrentTime.h>
56
57 using namespace WTF;
58 using namespace Unicode;
59
60 using std::min;
61 using std::max;
62
63 namespace WebCore {
64
65 typedef unsigned long long TimeStamp;
66
67 static const int kMaxVisibleRows = 20;
68 static const int kMaxHeight = 500;
69 static const int kBorderSize = 1;
70 static const TimeStamp kTypeAheadTimeoutMs = 1000;
71
72 // The settings used for the drop down menu.
73 // This is the delegate used if none is provided.
74 static const PopupContainerSettings dropDownSettings = {
75 true, // focusOnShow
76 true, // setTextOnIndexChange
77 true, // acceptOnAbandon
78 false // loopSelectionNavigation
79 };
80
81 // This class uses WebCore code to paint and handle events for a drop-down list
82 // box ("combobox" on Windows).
83 class PopupListBox : public FramelessScrollView {
84 public:
create(PopupMenuClient * client,const PopupContainerSettings & settings)85 static PassRefPtr<PopupListBox> create(PopupMenuClient* client, const PopupContainerSettings& settings)
86 {
87 return adoptRef(new PopupListBox(client, settings));
88 }
89
90 // FramelessScrollView
91 virtual void paint(GraphicsContext*, const IntRect&);
92 virtual bool handleMouseDownEvent(const PlatformMouseEvent&);
93 virtual bool handleMouseMoveEvent(const PlatformMouseEvent&);
94 virtual bool handleMouseReleaseEvent(const PlatformMouseEvent&);
95 virtual bool handleWheelEvent(const PlatformWheelEvent&);
96 virtual bool handleKeyEvent(const PlatformKeyboardEvent&);
97
98 // ScrollView
99 virtual HostWindow* hostWindow() const;
100
101 // PopupListBox methods
102
103 // Shows the popup
104 void showPopup();
105
106 // Hides the popup. Do not call this directly: use client->hidePopup().
107 void hidePopup();
108
109 // Updates our internal list to match the client.
110 void updateFromElement();
111
112 // Frees any allocated resources used in a particular popup session.
113 void clear();
114
115 // Sets the index of the option that is displayed in the <select> widget in the page
116 void setOriginalIndex(int index);
117
118 // Gets the index of the item that the user is currently moused over or has selected with
119 // the keyboard. This is not the same as the original index, since the user has not yet
120 // accepted this input.
selectedIndex() const121 int selectedIndex() const { return m_selectedIndex; }
122
123 // Moves selection down/up the given number of items, scrolling if necessary.
124 // Positive is down. The resulting index will be clamped to the range
125 // [0, numItems), and non-option items will be skipped.
126 void adjustSelectedIndex(int delta);
127
128 // Returns the number of items in the list.
numItems() const129 int numItems() const { return static_cast<int>(m_items.size()); }
130
setBaseWidth(int width)131 void setBaseWidth(int width) { m_baseWidth = width; }
132
133 // Computes the size of widget and children.
134 void layout();
135
136 // Returns whether the popup wants to process events for the passed key.
137 bool isInterestedInEventForKey(int keyCode);
138
139 // Gets the height of a row.
140 int getRowHeight(int index);
141
items() const142 const Vector<PopupItem*>& items() const { return m_items; }
143
144 private:
145 friend class PopupContainer;
146 friend class RefCounted<PopupListBox>;
147
PopupListBox(PopupMenuClient * client,const PopupContainerSettings & settings)148 PopupListBox(PopupMenuClient* client, const PopupContainerSettings& settings)
149 : m_settings(settings)
150 , m_originalIndex(0)
151 , m_selectedIndex(0)
152 , m_acceptedIndexOnAbandon(-1)
153 , m_visibleRows(0)
154 , m_baseWidth(0)
155 , m_popupClient(client)
156 , m_repeatingChar(0)
157 , m_lastCharTime(0)
158 {
159 setScrollbarModes(ScrollbarAlwaysOff, ScrollbarAlwaysOff);
160 }
161
~PopupListBox()162 ~PopupListBox()
163 {
164 clear();
165 }
166
disconnectClient()167 void disconnectClient() { m_popupClient = 0; }
168
169 // Closes the popup
170 void abandon();
171
172 // Returns true if the selection can be changed to index.
173 // Disabled items, or labels cannot be selected.
174 bool isSelectableItem(int index);
175
176 // Select an index in the list, scrolling if necessary.
177 void selectIndex(int index);
178
179 // Accepts the selected index as the value to be displayed in the <select> widget on
180 // the web page, and closes the popup.
181 void acceptIndex(int index);
182
183 // Clears the selection (so no row appears selected).
184 void clearSelection();
185
186 // Scrolls to reveal the given index.
187 void scrollToRevealRow(int index);
scrollToRevealSelection()188 void scrollToRevealSelection() { scrollToRevealRow(m_selectedIndex); }
189
190 // Invalidates the row at the given index.
191 void invalidateRow(int index);
192
193 // Get the bounds of a row.
194 IntRect getRowBounds(int index);
195
196 // Converts a point to an index of the row the point is over
197 int pointToRowIndex(const IntPoint&);
198
199 // Paint an individual row
200 void paintRow(GraphicsContext*, const IntRect&, int rowIndex);
201
202 // Test if the given point is within the bounds of the popup window.
203 bool isPointInBounds(const IntPoint&);
204
205 // Called when the user presses a text key. Does a prefix-search of the items.
206 void typeAheadFind(const PlatformKeyboardEvent&);
207
208 // Returns the font to use for the given row
209 Font getRowFont(int index);
210
211 // Moves the selection down/up one item, taking care of looping back to the
212 // first/last element if m_loopSelectionNavigation is true.
213 void selectPreviousRow();
214 void selectNextRow();
215
216 // The settings that specify the behavior for this Popup window.
217 PopupContainerSettings m_settings;
218
219 // This is the index of the item marked as "selected" - i.e. displayed in the widget on the
220 // page.
221 int m_originalIndex;
222
223 // This is the index of the item that the user is hovered over or has selected using the
224 // keyboard in the list. They have not confirmed this selection by clicking or pressing
225 // enter yet however.
226 int m_selectedIndex;
227
228 // If >= 0, this is the index we should accept if the popup is "abandoned".
229 // This is used for keyboard navigation, where we want the
230 // selection to change immediately, and is only used if the settings
231 // acceptOnAbandon field is true.
232 int m_acceptedIndexOnAbandon;
233
234 // This is the number of rows visible in the popup. The maximum number visible at a time is
235 // defined as being kMaxVisibleRows. For a scrolled popup, this can be thought of as the
236 // page size in data units.
237 int m_visibleRows;
238
239 // Our suggested width, not including scrollbar.
240 int m_baseWidth;
241
242 // A list of the options contained within the <select>
243 Vector<PopupItem*> m_items;
244
245 // The <select> PopupMenuClient that opened us.
246 PopupMenuClient* m_popupClient;
247
248 // The scrollbar which has mouse capture. Mouse events go straight to this
249 // if non-NULL.
250 RefPtr<Scrollbar> m_capturingScrollbar;
251
252 // The last scrollbar that the mouse was over. Used for mouseover highlights.
253 RefPtr<Scrollbar> m_lastScrollbarUnderMouse;
254
255 // The string the user has typed so far into the popup. Used for typeAheadFind.
256 String m_typedString;
257
258 // The char the user has hit repeatedly. Used for typeAheadFind.
259 UChar m_repeatingChar;
260
261 // The last time the user hit a key. Used for typeAheadFind.
262 TimeStamp m_lastCharTime;
263 };
264
constructRelativeMouseEvent(const PlatformMouseEvent & e,FramelessScrollView * parent,FramelessScrollView * child)265 static PlatformMouseEvent constructRelativeMouseEvent(const PlatformMouseEvent& e,
266 FramelessScrollView* parent,
267 FramelessScrollView* child)
268 {
269 IntPoint pos = parent->convertSelfToChild(child, e.pos());
270
271 // FIXME: This is a horrible hack since PlatformMouseEvent has no setters for x/y.
272 PlatformMouseEvent relativeEvent = e;
273 IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.pos());
274 relativePos.setX(pos.x());
275 relativePos.setY(pos.y());
276 return relativeEvent;
277 }
278
constructRelativeWheelEvent(const PlatformWheelEvent & e,FramelessScrollView * parent,FramelessScrollView * child)279 static PlatformWheelEvent constructRelativeWheelEvent(const PlatformWheelEvent& e,
280 FramelessScrollView* parent,
281 FramelessScrollView* child)
282 {
283 IntPoint pos = parent->convertSelfToChild(child, e.pos());
284
285 // FIXME: This is a horrible hack since PlatformWheelEvent has no setters for x/y.
286 PlatformWheelEvent relativeEvent = e;
287 IntPoint& relativePos = const_cast<IntPoint&>(relativeEvent.pos());
288 relativePos.setX(pos.x());
289 relativePos.setY(pos.y());
290 return relativeEvent;
291 }
292
293 ///////////////////////////////////////////////////////////////////////////////
294 // PopupContainer implementation
295
296 // static
create(PopupMenuClient * client,const PopupContainerSettings & settings)297 PassRefPtr<PopupContainer> PopupContainer::create(PopupMenuClient* client,
298 const PopupContainerSettings& settings)
299 {
300 return adoptRef(new PopupContainer(client, settings));
301 }
302
PopupContainer(PopupMenuClient * client,const PopupContainerSettings & settings)303 PopupContainer::PopupContainer(PopupMenuClient* client,
304 const PopupContainerSettings& settings)
305 : m_listBox(PopupListBox::create(client, settings))
306 , m_settings(settings)
307 {
308 setScrollbarModes(ScrollbarAlwaysOff, ScrollbarAlwaysOff);
309 }
310
~PopupContainer()311 PopupContainer::~PopupContainer()
312 {
313 if (m_listBox && m_listBox->parent())
314 removeChild(m_listBox.get());
315 }
316
showPopup(FrameView * view)317 void PopupContainer::showPopup(FrameView* view)
318 {
319 // Pre-layout, our size matches the <select> dropdown control.
320 int selectHeight = frameRect().height();
321
322 // Lay everything out to figure out our preferred size, then tell the view's
323 // WidgetClient about it. It should assign us a client.
324 layout();
325
326 ChromeClientChromium* chromeClient = static_cast<ChromeClientChromium*>(
327 view->frame()->page()->chrome()->client());
328 if (chromeClient) {
329 // If the popup would extend past the bottom of the screen, open upwards
330 // instead.
331 FloatRect screen = screenAvailableRect(view);
332 IntRect widgetRect = chromeClient->windowToScreen(frameRect());
333 if (widgetRect.bottom() > static_cast<int>(screen.bottom()))
334 widgetRect.move(0, -(widgetRect.height() + selectHeight));
335
336 chromeClient->popupOpened(this, widgetRect, m_settings.focusOnShow, false);
337 }
338
339 if (!m_listBox->parent())
340 addChild(m_listBox.get());
341
342 // Enable scrollbars after the listbox is inserted into the hierarchy,
343 // so it has a proper WidgetClient.
344 m_listBox->setVerticalScrollbarMode(ScrollbarAuto);
345
346 m_listBox->scrollToRevealSelection();
347
348 invalidate();
349 }
350
showExternal(const IntRect & rect,FrameView * v,int index)351 void PopupContainer::showExternal(const IntRect& rect, FrameView* v, int index)
352 {
353 if (!listBox())
354 return;
355
356 listBox()->setBaseWidth(rect.width());
357 listBox()->updateFromElement();
358
359 if (listBox()->numItems() < 1) {
360 hidePopup();
361 return;
362 }
363
364 // Adjust the popup position to account for scrolling.
365 IntPoint location = v->contentsToWindow(rect.location());
366 IntRect popupRect(location, rect.size());
367
368 // Get the ChromeClient and pass it the popup menu's listbox data.
369 ChromeClientChromium* client = static_cast<ChromeClientChromium*>(
370 v->frame()->page()->chrome()->client());
371 client->popupOpened(this, popupRect, true, true);
372 }
373
hidePopup()374 void PopupContainer::hidePopup()
375 {
376 if (client())
377 client()->popupClosed(this);
378 }
379
layout()380 void PopupContainer::layout()
381 {
382 m_listBox->layout();
383
384 // Place the listbox within our border.
385 m_listBox->move(kBorderSize, kBorderSize);
386
387 // Size ourselves to contain listbox + border.
388 resize(m_listBox->width() + kBorderSize * 2, m_listBox->height() + kBorderSize * 2);
389
390 invalidate();
391 }
392
handleMouseDownEvent(const PlatformMouseEvent & event)393 bool PopupContainer::handleMouseDownEvent(const PlatformMouseEvent& event)
394 {
395 return m_listBox->handleMouseDownEvent(
396 constructRelativeMouseEvent(event, this, m_listBox.get()));
397 }
398
handleMouseMoveEvent(const PlatformMouseEvent & event)399 bool PopupContainer::handleMouseMoveEvent(const PlatformMouseEvent& event)
400 {
401 return m_listBox->handleMouseMoveEvent(
402 constructRelativeMouseEvent(event, this, m_listBox.get()));
403 }
404
handleMouseReleaseEvent(const PlatformMouseEvent & event)405 bool PopupContainer::handleMouseReleaseEvent(const PlatformMouseEvent& event)
406 {
407 return m_listBox->handleMouseReleaseEvent(
408 constructRelativeMouseEvent(event, this, m_listBox.get()));
409 }
410
handleWheelEvent(const PlatformWheelEvent & event)411 bool PopupContainer::handleWheelEvent(const PlatformWheelEvent& event)
412 {
413 return m_listBox->handleWheelEvent(
414 constructRelativeWheelEvent(event, this, m_listBox.get()));
415 }
416
handleKeyEvent(const PlatformKeyboardEvent & event)417 bool PopupContainer::handleKeyEvent(const PlatformKeyboardEvent& event)
418 {
419 return m_listBox->handleKeyEvent(event);
420 }
421
hide()422 void PopupContainer::hide()
423 {
424 m_listBox->abandon();
425 }
426
paint(GraphicsContext * gc,const IntRect & rect)427 void PopupContainer::paint(GraphicsContext* gc, const IntRect& rect)
428 {
429 // adjust coords for scrolled frame
430 IntRect r = intersection(rect, frameRect());
431 int tx = x();
432 int ty = y();
433
434 r.move(-tx, -ty);
435
436 gc->translate(static_cast<float>(tx), static_cast<float>(ty));
437 m_listBox->paint(gc, r);
438 gc->translate(-static_cast<float>(tx), -static_cast<float>(ty));
439
440 paintBorder(gc, rect);
441 }
442
paintBorder(GraphicsContext * gc,const IntRect & rect)443 void PopupContainer::paintBorder(GraphicsContext* gc, const IntRect& rect)
444 {
445 // FIXME: Where do we get the border color from?
446 Color borderColor(127, 157, 185);
447
448 gc->setStrokeStyle(NoStroke);
449 gc->setFillColor(borderColor);
450
451 int tx = x();
452 int ty = y();
453
454 // top, left, bottom, right
455 gc->drawRect(IntRect(tx, ty, width(), kBorderSize));
456 gc->drawRect(IntRect(tx, ty, kBorderSize, height()));
457 gc->drawRect(IntRect(tx, ty + height() - kBorderSize, width(), kBorderSize));
458 gc->drawRect(IntRect(tx + width() - kBorderSize, ty, kBorderSize, height()));
459 }
460
isInterestedInEventForKey(int keyCode)461 bool PopupContainer::isInterestedInEventForKey(int keyCode)
462 {
463 return m_listBox->isInterestedInEventForKey(keyCode);
464 }
465
show(const IntRect & r,FrameView * v,int index)466 void PopupContainer::show(const IntRect& r, FrameView* v, int index)
467 {
468 // The rect is the size of the select box. It's usually larger than we need.
469 // subtract border size so that usually the container will be displayed
470 // exactly the same width as the select box.
471 listBox()->setBaseWidth(max(r.width() - kBorderSize * 2, 0));
472
473 listBox()->updateFromElement();
474
475 // We set the selected item in updateFromElement(), and disregard the
476 // index passed into this function (same as Webkit's PopupMenuWin.cpp)
477 // FIXME: make sure this is correct, and add an assertion.
478 // ASSERT(popupWindow(popup)->listBox()->selectedIndex() == index);
479
480 // Convert point to main window coords.
481 IntPoint location = v->contentsToWindow(r.location());
482
483 // Move it below the select widget.
484 location.move(0, r.height());
485
486 IntRect popupRect(location, r.size());
487 setFrameRect(popupRect);
488 showPopup(v);
489 }
490
refresh()491 void PopupContainer::refresh()
492 {
493 listBox()->updateFromElement();
494 layout();
495 }
496
selectedIndex() const497 int PopupContainer::selectedIndex() const
498 {
499 return m_listBox->selectedIndex();
500 }
501
menuItemHeight() const502 int PopupContainer::menuItemHeight() const
503 {
504 return m_listBox->getRowHeight(0);
505 }
506
popupData() const507 const WTF::Vector<PopupItem*>& PopupContainer:: popupData() const
508 {
509 return m_listBox->items();
510 }
511
512 ///////////////////////////////////////////////////////////////////////////////
513 // PopupListBox implementation
514
handleMouseDownEvent(const PlatformMouseEvent & event)515 bool PopupListBox::handleMouseDownEvent(const PlatformMouseEvent& event)
516 {
517 Scrollbar* scrollbar = scrollbarAtPoint(event.pos());
518 if (scrollbar) {
519 m_capturingScrollbar = scrollbar;
520 m_capturingScrollbar->mouseDown(event);
521 return true;
522 }
523
524 if (!isPointInBounds(event.pos()))
525 abandon();
526
527 return true;
528 }
529
handleMouseMoveEvent(const PlatformMouseEvent & event)530 bool PopupListBox::handleMouseMoveEvent(const PlatformMouseEvent& event)
531 {
532 if (m_capturingScrollbar) {
533 m_capturingScrollbar->mouseMoved(event);
534 return true;
535 }
536
537 Scrollbar* scrollbar = scrollbarAtPoint(event.pos());
538 if (m_lastScrollbarUnderMouse != scrollbar) {
539 // Send mouse exited to the old scrollbar.
540 if (m_lastScrollbarUnderMouse)
541 m_lastScrollbarUnderMouse->mouseExited();
542 m_lastScrollbarUnderMouse = scrollbar;
543 }
544
545 if (scrollbar) {
546 scrollbar->mouseMoved(event);
547 return true;
548 }
549
550 if (!isPointInBounds(event.pos()))
551 return false;
552
553 selectIndex(pointToRowIndex(event.pos()));
554 return true;
555 }
556
handleMouseReleaseEvent(const PlatformMouseEvent & event)557 bool PopupListBox::handleMouseReleaseEvent(const PlatformMouseEvent& event)
558 {
559 if (m_capturingScrollbar) {
560 m_capturingScrollbar->mouseUp();
561 m_capturingScrollbar = 0;
562 return true;
563 }
564
565 if (!isPointInBounds(event.pos()))
566 return true;
567
568 acceptIndex(pointToRowIndex(event.pos()));
569 return true;
570 }
571
handleWheelEvent(const PlatformWheelEvent & event)572 bool PopupListBox::handleWheelEvent(const PlatformWheelEvent& event)
573 {
574 if (!isPointInBounds(event.pos())) {
575 abandon();
576 return true;
577 }
578
579 // Pass it off to the scroll view.
580 // Sadly, WebCore devs don't understand the whole "const" thing.
581 wheelEvent(const_cast<PlatformWheelEvent&>(event));
582 return true;
583 }
584
585 // Should be kept in sync with handleKeyEvent().
isInterestedInEventForKey(int keyCode)586 bool PopupListBox::isInterestedInEventForKey(int keyCode)
587 {
588 switch (keyCode) {
589 case VKEY_ESCAPE:
590 case VKEY_RETURN:
591 case VKEY_UP:
592 case VKEY_DOWN:
593 case VKEY_PRIOR:
594 case VKEY_NEXT:
595 case VKEY_HOME:
596 case VKEY_END:
597 case VKEY_TAB:
598 return true;
599 default:
600 return false;
601 }
602 }
603
handleKeyEvent(const PlatformKeyboardEvent & event)604 bool PopupListBox::handleKeyEvent(const PlatformKeyboardEvent& event)
605 {
606 if (event.type() == PlatformKeyboardEvent::KeyUp)
607 return true;
608
609 if (numItems() == 0 && event.windowsVirtualKeyCode() != VKEY_ESCAPE)
610 return true;
611
612 switch (event.windowsVirtualKeyCode()) {
613 case VKEY_ESCAPE:
614 abandon(); // may delete this
615 return true;
616 case VKEY_RETURN:
617 if (m_selectedIndex == -1) {
618 m_popupClient->hidePopup();
619 // Don't eat the enter if nothing is selected.
620 return false;
621 }
622 acceptIndex(m_selectedIndex); // may delete this
623 return true;
624 case VKEY_UP:
625 selectPreviousRow();
626 break;
627 case VKEY_DOWN:
628 selectNextRow();
629 break;
630 case VKEY_PRIOR:
631 adjustSelectedIndex(-m_visibleRows);
632 break;
633 case VKEY_NEXT:
634 adjustSelectedIndex(m_visibleRows);
635 break;
636 case VKEY_HOME:
637 adjustSelectedIndex(-m_selectedIndex);
638 break;
639 case VKEY_END:
640 adjustSelectedIndex(m_items.size());
641 break;
642 default:
643 if (!event.ctrlKey() && !event.altKey() && !event.metaKey()
644 && isPrintableChar(event.windowsVirtualKeyCode()))
645 typeAheadFind(event);
646 break;
647 }
648
649 if (m_originalIndex != m_selectedIndex) {
650 // Keyboard events should update the selection immediately (but we don't
651 // want to fire the onchange event until the popup is closed, to match
652 // IE). We change the original index so we revert to that when the
653 // popup is closed.
654 if (m_settings.acceptOnAbandon)
655 m_acceptedIndexOnAbandon = m_selectedIndex;
656
657 setOriginalIndex(m_selectedIndex);
658 if (m_settings.setTextOnIndexChange)
659 m_popupClient->setTextFromItem(m_selectedIndex);
660 } else if (!m_settings.setTextOnIndexChange &&
661 event.windowsVirtualKeyCode() == VKEY_TAB) {
662 // TAB is a special case as it should select the current item if any and
663 // advance focus.
664 if (m_selectedIndex >= 0)
665 m_popupClient->setTextFromItem(m_selectedIndex);
666 // Return false so the TAB key event is propagated to the page.
667 return false;
668 }
669
670 return true;
671 }
672
hostWindow() const673 HostWindow* PopupListBox::hostWindow() const
674 {
675 // Our parent is the root ScrollView, so it is the one that has a
676 // HostWindow. FrameView::hostWindow() works similarly.
677 return parent() ? parent()->hostWindow() : 0;
678 }
679
680 // From HTMLSelectElement.cpp
stripLeadingWhiteSpace(const String & string)681 static String stripLeadingWhiteSpace(const String& string)
682 {
683 int length = string.length();
684 int i;
685 for (i = 0; i < length; ++i)
686 if (string[i] != noBreakSpace
687 && (string[i] <= 0x7F ? !isspace(string[i]) : (direction(string[i]) != WhiteSpaceNeutral)))
688 break;
689
690 return string.substring(i, length - i);
691 }
692
isCharacterTypeEvent(const PlatformKeyboardEvent & event)693 static bool isCharacterTypeEvent(const PlatformKeyboardEvent& event) {
694 // Check whether the event is a character-typed event or not.
695 // In Windows, PlatformKeyboardEvent::Char (not RawKeyDown) type event
696 // is considered as character type event. In Mac OS, KeyDown (not
697 // KeyUp) is considered as character type event.
698 #if PLATFORM(WIN_OS)
699 if (event.type() == PlatformKeyboardEvent::Char)
700 return true;
701 #else
702 if (event.type() == PlatformKeyboardEvent::KeyDown)
703 return true;
704 #endif
705 return false;
706 }
707
708 // From HTMLSelectElement.cpp, with modifications
typeAheadFind(const PlatformKeyboardEvent & event)709 void PopupListBox::typeAheadFind(const PlatformKeyboardEvent& event)
710 {
711 TimeStamp now = static_cast<TimeStamp>(currentTime() * 1000.0f);
712 TimeStamp delta = now - m_lastCharTime;
713
714 // Reset the time when user types in a character. The time gap between
715 // last character and the current character is used to indicate whether
716 // user typed in a string or just a character as the search prefix.
717 if (isCharacterTypeEvent(event))
718 m_lastCharTime = now;
719
720 UChar c = event.windowsVirtualKeyCode();
721
722 String prefix;
723 int searchStartOffset = 1;
724 if (delta > kTypeAheadTimeoutMs) {
725 m_typedString = prefix = String(&c, 1);
726 m_repeatingChar = c;
727 } else {
728 m_typedString.append(c);
729
730 if (c == m_repeatingChar)
731 // The user is likely trying to cycle through all the items starting with this character, so just search on the character
732 prefix = String(&c, 1);
733 else {
734 m_repeatingChar = 0;
735 prefix = m_typedString;
736 searchStartOffset = 0;
737 }
738 }
739
740 int itemCount = numItems();
741 int index = (max(0, m_selectedIndex) + searchStartOffset) % itemCount;
742 for (int i = 0; i < itemCount; i++, index = (index + 1) % itemCount) {
743 if (!isSelectableItem(index))
744 continue;
745
746 if (stripLeadingWhiteSpace(m_items[index]->label).startsWith(prefix, false)) {
747 selectIndex(index);
748 return;
749 }
750 }
751 }
752
paint(GraphicsContext * gc,const IntRect & rect)753 void PopupListBox::paint(GraphicsContext* gc, const IntRect& rect)
754 {
755 // adjust coords for scrolled frame
756 IntRect r = intersection(rect, frameRect());
757 int tx = x() - scrollX();
758 int ty = y() - scrollY();
759
760 r.move(-tx, -ty);
761
762 // set clip rect to match revised damage rect
763 gc->save();
764 gc->translate(static_cast<float>(tx), static_cast<float>(ty));
765 gc->clip(r);
766
767 // FIXME: Can we optimize scrolling to not require repainting the entire
768 // window? Should we?
769 for (int i = 0; i < numItems(); ++i)
770 paintRow(gc, r, i);
771
772 // Special case for an empty popup.
773 if (numItems() == 0)
774 gc->fillRect(r, Color::white);
775
776 gc->restore();
777
778 ScrollView::paint(gc, rect);
779 }
780
781 static const int separatorPadding = 4;
782 static const int separatorHeight = 1;
783
paintRow(GraphicsContext * gc,const IntRect & rect,int rowIndex)784 void PopupListBox::paintRow(GraphicsContext* gc, const IntRect& rect, int rowIndex)
785 {
786 // This code is based largely on RenderListBox::paint* methods.
787
788 IntRect rowRect = getRowBounds(rowIndex);
789 if (!rowRect.intersects(rect))
790 return;
791
792 PopupMenuStyle style = m_popupClient->itemStyle(rowIndex);
793
794 // Paint background
795 Color backColor, textColor;
796 if (rowIndex == m_selectedIndex) {
797 backColor = RenderTheme::defaultTheme()->activeListBoxSelectionBackgroundColor();
798 textColor = RenderTheme::defaultTheme()->activeListBoxSelectionForegroundColor();
799 } else {
800 backColor = style.backgroundColor();
801 textColor = style.foregroundColor();
802 }
803
804 // If we have a transparent background, make sure it has a color to blend
805 // against.
806 if (backColor.hasAlpha())
807 gc->fillRect(rowRect, Color::white);
808
809 gc->fillRect(rowRect, backColor);
810
811 if (m_popupClient->itemIsSeparator(rowIndex)) {
812 IntRect separatorRect(
813 rowRect.x() + separatorPadding,
814 rowRect.y() + (rowRect.height() - separatorHeight) / 2,
815 rowRect.width() - 2 * separatorPadding, separatorHeight);
816 gc->fillRect(separatorRect, textColor);
817 return;
818 }
819
820 gc->setFillColor(textColor);
821
822 // Bunch of shit to deal with RTL text...
823 String itemText = m_popupClient->itemText(rowIndex);
824 unsigned length = itemText.length();
825 const UChar* str = itemText.characters();
826
827 TextRun textRun(str, length, false, 0, 0, itemText.defaultWritingDirection() == WTF::Unicode::RightToLeft);
828
829 // FIXME: http://b/1210481 We should get the padding of individual option
830 // elements. This probably implies changes to PopupMenuClient.
831
832 // Draw the item text
833 if (style.isVisible()) {
834 Font itemFont = getRowFont(rowIndex);
835 int textX = max(0, m_popupClient->clientPaddingLeft() - m_popupClient->clientInsetLeft());
836 int textY = rowRect.y() + itemFont.ascent() + (rowRect.height() - itemFont.height()) / 2;
837 gc->drawBidiText(itemFont, textRun, IntPoint(textX, textY));
838 }
839 }
840
getRowFont(int rowIndex)841 Font PopupListBox::getRowFont(int rowIndex)
842 {
843 Font itemFont = m_popupClient->menuStyle().font();
844 if (m_popupClient->itemIsLabel(rowIndex)) {
845 // Bold-ify labels (ie, an <optgroup> heading).
846 FontDescription d = itemFont.fontDescription();
847 d.setWeight(FontWeightBold);
848 Font font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
849 font.update(0);
850 return font;
851 }
852
853 return itemFont;
854 }
855
abandon()856 void PopupListBox::abandon()
857 {
858 RefPtr<PopupListBox> keepAlive(this);
859
860 m_selectedIndex = m_originalIndex;
861
862 m_popupClient->hidePopup();
863
864 if (m_acceptedIndexOnAbandon >= 0) {
865 m_popupClient->valueChanged(m_acceptedIndexOnAbandon);
866 m_acceptedIndexOnAbandon = -1;
867 }
868 }
869
pointToRowIndex(const IntPoint & point)870 int PopupListBox::pointToRowIndex(const IntPoint& point)
871 {
872 int y = scrollY() + point.y();
873
874 // FIXME: binary search if perf matters.
875 for (int i = 0; i < numItems(); ++i) {
876 if (y < m_items[i]->yOffset)
877 return i-1;
878 }
879
880 // Last item?
881 if (y < contentsHeight())
882 return m_items.size()-1;
883
884 return -1;
885 }
886
acceptIndex(int index)887 void PopupListBox::acceptIndex(int index)
888 {
889 if (index >= numItems())
890 return;
891
892 if (index < 0) {
893 if (m_popupClient) {
894 // Enter pressed with no selection, just close the popup.
895 m_popupClient->hidePopup();
896 }
897 return;
898 }
899
900 if (isSelectableItem(index)) {
901 RefPtr<PopupListBox> keepAlive(this);
902
903 // Hide ourselves first since valueChanged may have numerous side-effects.
904 m_popupClient->hidePopup();
905
906 // Tell the <select> PopupMenuClient what index was selected.
907 m_popupClient->valueChanged(index);
908 }
909 }
910
selectIndex(int index)911 void PopupListBox::selectIndex(int index)
912 {
913 if (index < 0 || index >= numItems())
914 return;
915
916 if (index != m_selectedIndex && isSelectableItem(index)) {
917 invalidateRow(m_selectedIndex);
918 m_selectedIndex = index;
919 invalidateRow(m_selectedIndex);
920
921 scrollToRevealSelection();
922 }
923 }
924
setOriginalIndex(int index)925 void PopupListBox::setOriginalIndex(int index)
926 {
927 m_originalIndex = m_selectedIndex = index;
928 }
929
getRowHeight(int index)930 int PopupListBox::getRowHeight(int index)
931 {
932 if (index < 0)
933 return 0;
934
935 return getRowFont(index).height();
936 }
937
getRowBounds(int index)938 IntRect PopupListBox::getRowBounds(int index)
939 {
940 if (index < 0)
941 return IntRect(0, 0, visibleWidth(), getRowHeight(index));
942
943 return IntRect(0, m_items[index]->yOffset, visibleWidth(), getRowHeight(index));
944 }
945
invalidateRow(int index)946 void PopupListBox::invalidateRow(int index)
947 {
948 if (index < 0)
949 return;
950
951 // Invalidate in the window contents, as FramelessScrollView::invalidateRect
952 // paints in the window coordinates.
953 invalidateRect(contentsToWindow(getRowBounds(index)));
954 }
955
scrollToRevealRow(int index)956 void PopupListBox::scrollToRevealRow(int index)
957 {
958 if (index < 0)
959 return;
960
961 IntRect rowRect = getRowBounds(index);
962
963 if (rowRect.y() < scrollY()) {
964 // Row is above current scroll position, scroll up.
965 ScrollView::setScrollPosition(IntPoint(0, rowRect.y()));
966 } else if (rowRect.bottom() > scrollY() + visibleHeight()) {
967 // Row is below current scroll position, scroll down.
968 ScrollView::setScrollPosition(IntPoint(0, rowRect.bottom() - visibleHeight()));
969 }
970 }
971
isSelectableItem(int index)972 bool PopupListBox::isSelectableItem(int index)
973 {
974 ASSERT(index >= 0 && index < numItems());
975 return m_items[index]->type == PopupItem::TypeOption && m_popupClient->itemIsEnabled(index);
976 }
977
clearSelection()978 void PopupListBox::clearSelection()
979 {
980 if (m_selectedIndex != -1) {
981 invalidateRow(m_selectedIndex);
982 m_selectedIndex = -1;
983 }
984 }
985
selectNextRow()986 void PopupListBox::selectNextRow()
987 {
988 if (!m_settings.loopSelectionNavigation || m_selectedIndex != numItems() - 1) {
989 adjustSelectedIndex(1);
990 return;
991 }
992
993 // We are moving past the last item, no row should be selected.
994 clearSelection();
995 }
996
selectPreviousRow()997 void PopupListBox::selectPreviousRow()
998 {
999 if (!m_settings.loopSelectionNavigation || m_selectedIndex > 0) {
1000 adjustSelectedIndex(-1);
1001 return;
1002 }
1003
1004 if (m_selectedIndex == 0) {
1005 // We are moving past the first item, clear the selection.
1006 clearSelection();
1007 return;
1008 }
1009
1010 // No row is selected, jump to the last item.
1011 selectIndex(numItems() - 1);
1012 scrollToRevealSelection();
1013 }
1014
adjustSelectedIndex(int delta)1015 void PopupListBox::adjustSelectedIndex(int delta)
1016 {
1017 int targetIndex = m_selectedIndex + delta;
1018 targetIndex = min(max(targetIndex, 0), numItems() - 1);
1019 if (!isSelectableItem(targetIndex)) {
1020 // We didn't land on an option. Try to find one.
1021 // We try to select the closest index to target, prioritizing any in
1022 // the range [current, target].
1023
1024 int dir = delta > 0 ? 1 : -1;
1025 int testIndex = m_selectedIndex;
1026 int bestIndex = m_selectedIndex;
1027 bool passedTarget = false;
1028 while (testIndex >= 0 && testIndex < numItems()) {
1029 if (isSelectableItem(testIndex))
1030 bestIndex = testIndex;
1031 if (testIndex == targetIndex)
1032 passedTarget = true;
1033 if (passedTarget && bestIndex != m_selectedIndex)
1034 break;
1035
1036 testIndex += dir;
1037 }
1038
1039 // Pick the best index, which may mean we don't change.
1040 targetIndex = bestIndex;
1041 }
1042
1043 // Select the new index, and ensure its visible. We do this regardless of
1044 // whether the selection changed to ensure keyboard events always bring the
1045 // selection into view.
1046 selectIndex(targetIndex);
1047 scrollToRevealSelection();
1048 }
1049
updateFromElement()1050 void PopupListBox::updateFromElement()
1051 {
1052 clear();
1053
1054 int size = m_popupClient->listSize();
1055 for (int i = 0; i < size; ++i) {
1056 PopupItem::Type type;
1057 if (m_popupClient->itemIsSeparator(i))
1058 type = PopupItem::TypeSeparator;
1059 else if (m_popupClient->itemIsLabel(i))
1060 type = PopupItem::TypeGroup;
1061 else
1062 type = PopupItem::TypeOption;
1063 m_items.append(new PopupItem(m_popupClient->itemText(i), type));
1064 m_items[i]->enabled = isSelectableItem(i);
1065 }
1066
1067 m_selectedIndex = m_popupClient->selectedIndex();
1068 setOriginalIndex(m_selectedIndex);
1069
1070 layout();
1071 }
1072
layout()1073 void PopupListBox::layout()
1074 {
1075 // Size our child items.
1076 int baseWidth = 0;
1077 int paddingWidth = 0;
1078 int y = 0;
1079 for (int i = 0; i < numItems(); ++i) {
1080 Font itemFont = getRowFont(i);
1081
1082 // Place the item vertically.
1083 m_items[i]->yOffset = y;
1084 y += itemFont.height();
1085
1086 // Ensure the popup is wide enough to fit this item.
1087 String text = m_popupClient->itemText(i);
1088 if (!text.isEmpty()) {
1089 int width = itemFont.width(TextRun(text));
1090 baseWidth = max(baseWidth, width);
1091 }
1092 // FIXME: http://b/1210481 We should get the padding of individual option elements.
1093 paddingWidth = max(paddingWidth,
1094 m_popupClient->clientPaddingLeft() + m_popupClient->clientPaddingRight());
1095 }
1096
1097 int windowHeight = 0;
1098
1099 #if PLATFORM(DARWIN)
1100 // Set the popup's window to contain all available items on Mac only, which
1101 // uses native controls that manage their own scrolling. This allows hit
1102 // testing to work when selecting items in popups that have more menu entries
1103 // than the maximum window size.
1104 m_visibleRows = numItems();
1105 #else
1106 m_visibleRows = min(numItems(), kMaxVisibleRows);
1107 #endif
1108
1109 for (int i = 0; i < m_visibleRows; ++i) {
1110 int rowHeight = getRowHeight(i);
1111 #if !PLATFORM(DARWIN)
1112 // Only clip the window height for non-Mac platforms.
1113 if (windowHeight + rowHeight > kMaxHeight) {
1114 m_visibleRows = i;
1115 break;
1116 }
1117 #endif
1118
1119 windowHeight += rowHeight;
1120 }
1121
1122 // Set our widget and scrollable contents sizes.
1123 int scrollbarWidth = 0;
1124 if (m_visibleRows < numItems())
1125 scrollbarWidth = ScrollbarTheme::nativeTheme()->scrollbarThickness();
1126
1127 int windowWidth = baseWidth + scrollbarWidth + paddingWidth;
1128 int contentWidth = baseWidth;
1129
1130 if (windowWidth < m_baseWidth) {
1131 windowWidth = m_baseWidth;
1132 contentWidth = m_baseWidth - scrollbarWidth - paddingWidth;
1133 } else {
1134 m_baseWidth = baseWidth;
1135 }
1136
1137 resize(windowWidth, windowHeight);
1138 setContentsSize(IntSize(contentWidth, getRowBounds(numItems() - 1).bottom()));
1139
1140 if (hostWindow())
1141 scrollToRevealSelection();
1142
1143 invalidate();
1144 }
1145
clear()1146 void PopupListBox::clear()
1147 {
1148 for (Vector<PopupItem*>::iterator it = m_items.begin(); it != m_items.end(); ++it)
1149 delete *it;
1150 m_items.clear();
1151 }
1152
isPointInBounds(const IntPoint & point)1153 bool PopupListBox::isPointInBounds(const IntPoint& point)
1154 {
1155 return numItems() != 0 && IntRect(0, 0, width(), height()).contains(point);
1156 }
1157
1158 ///////////////////////////////////////////////////////////////////////////////
1159 // PopupMenu implementation
1160 //
1161 // Note: you cannot add methods to this class, since it is defined above the
1162 // portability layer. To access methods and properties on the
1163 // popup widgets, use |popupWindow| above.
1164
PopupMenu(PopupMenuClient * client)1165 PopupMenu::PopupMenu(PopupMenuClient* client)
1166 : m_popupClient(client)
1167 {
1168 }
1169
~PopupMenu()1170 PopupMenu::~PopupMenu()
1171 {
1172 hide();
1173 }
1174
1175 // The Mac Chromium implementation relies on external control (a Cocoa control)
1176 // to display, handle the input tracking and menu item selection for the popup.
1177 // Windows and Linux Chromium let our WebKit port handle the display, while
1178 // another process manages the popup window and input handling.
show(const IntRect & r,FrameView * v,int index)1179 void PopupMenu::show(const IntRect& r, FrameView* v, int index)
1180 {
1181 if (!p.popup)
1182 p.popup = PopupContainer::create(client(), dropDownSettings);
1183 #if PLATFORM(DARWIN)
1184 p.popup->showExternal(r, v, index);
1185 #else
1186 p.popup->show(r, v, index);
1187 #endif
1188 }
1189
hide()1190 void PopupMenu::hide()
1191 {
1192 if (p.popup)
1193 p.popup->hidePopup();
1194 }
1195
updateFromElement()1196 void PopupMenu::updateFromElement()
1197 {
1198 p.popup->listBox()->updateFromElement();
1199 }
1200
itemWritingDirectionIsNatural()1201 bool PopupMenu::itemWritingDirectionIsNatural()
1202 {
1203 return false;
1204 }
1205
1206 } // namespace WebCore
1207