OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright (c) 2011, 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 "web/PopupListBox.h" | |
33 | |
34 #include "core/CSSValueKeywords.h" | |
35 #include "core/rendering/RenderTheme.h" | |
36 #include "platform/KeyboardCodes.h" | |
37 #include "platform/PlatformGestureEvent.h" | |
38 #include "platform/PlatformKeyboardEvent.h" | |
39 #include "platform/PlatformMouseEvent.h" | |
40 #include "platform/PlatformScreen.h" | |
41 #include "platform/PlatformTouchEvent.h" | |
42 #include "platform/PlatformWheelEvent.h" | |
43 #include "platform/PopupMenuClient.h" | |
44 #include "platform/RuntimeEnabledFeatures.h" | |
45 #include "platform/fonts/Font.h" | |
46 #include "platform/fonts/FontCache.h" | |
47 #include "platform/fonts/FontSelector.h" | |
48 #include "platform/geometry/IntRect.h" | |
49 #include "platform/graphics/GraphicsContext.h" | |
50 #include "platform/graphics/GraphicsContextStateSaver.h" | |
51 #include "platform/scroll/ScrollbarTheme.h" | |
52 #include "platform/text/StringTruncator.h" | |
53 #include "platform/text/TextRun.h" | |
54 #include "web/PopupContainer.h" | |
55 #include "web/PopupContainerClient.h" | |
56 #include "web/PopupMenuChromium.h" | |
57 #include "wtf/ASCIICType.h" | |
58 #include "wtf/CurrentTime.h" | |
59 #include <limits> | |
60 | |
61 namespace blink { | |
62 | |
63 using namespace WTF::Unicode; | |
64 | |
65 const int PopupListBox::defaultMaxHeight = 500; | |
66 static const int maxVisibleRows = 20; | |
67 static const int minEndOfLinePadding = 2; | |
68 static const TimeStamp typeAheadTimeoutMs = 1000; | |
69 | |
70 PopupListBox::PopupListBox(PopupMenuClient* client, bool deviceSupportsTouch, Po
pupContainer* container) | |
71 : m_deviceSupportsTouch(deviceSupportsTouch) | |
72 , m_originalIndex(0) | |
73 , m_selectedIndex(0) | |
74 , m_acceptedIndexOnAbandon(-1) | |
75 , m_visibleRows(0) | |
76 , m_baseWidth(0) | |
77 , m_maxHeight(defaultMaxHeight) | |
78 , m_popupClient(client) | |
79 , m_repeatingChar(0) | |
80 , m_lastCharTime(0) | |
81 , m_maxWindowWidth(std::numeric_limits<int>::max()) | |
82 , m_container(container) | |
83 { | |
84 } | |
85 | |
86 PopupListBox::~PopupListBox() | |
87 { | |
88 clear(); | |
89 | |
90 // Oilpan: the scrollbars of the ScrollView are self-sufficient, | |
91 // capable of detaching themselves from their animator on | |
92 // finalization. | |
93 #if !ENABLE(OILPAN) | |
94 setHasVerticalScrollbar(false); | |
95 #endif | |
96 } | |
97 | |
98 void PopupListBox::trace(Visitor* visitor) | |
99 { | |
100 visitor->trace(m_capturingScrollbar); | |
101 visitor->trace(m_lastScrollbarUnderMouse); | |
102 visitor->trace(m_focusedElement); | |
103 visitor->trace(m_container); | |
104 visitor->trace(m_verticalScrollbar); | |
105 Widget::trace(visitor); | |
106 } | |
107 | |
108 bool PopupListBox::handleMouseDownEvent(const PlatformMouseEvent& event) | |
109 { | |
110 Scrollbar* scrollbar = scrollbarAtWindowPoint(event.position()); | |
111 if (scrollbar) { | |
112 m_capturingScrollbar = scrollbar; | |
113 m_capturingScrollbar->mouseDown(event); | |
114 return true; | |
115 } | |
116 | |
117 if (!isPointInBounds(event.position())) | |
118 abandon(); | |
119 | |
120 return true; | |
121 } | |
122 | |
123 bool PopupListBox::handleMouseMoveEvent(const PlatformMouseEvent& event) | |
124 { | |
125 if (m_capturingScrollbar) { | |
126 m_capturingScrollbar->mouseMoved(event); | |
127 return true; | |
128 } | |
129 | |
130 Scrollbar* scrollbar = scrollbarAtWindowPoint(event.position()); | |
131 if (m_lastScrollbarUnderMouse != scrollbar) { | |
132 // Send mouse exited to the old scrollbar. | |
133 if (m_lastScrollbarUnderMouse) | |
134 m_lastScrollbarUnderMouse->mouseExited(); | |
135 m_lastScrollbarUnderMouse = scrollbar; | |
136 } | |
137 | |
138 if (scrollbar) { | |
139 scrollbar->mouseMoved(event); | |
140 return true; | |
141 } | |
142 | |
143 if (!isPointInBounds(event.position())) | |
144 return false; | |
145 | |
146 selectIndex(pointToRowIndex(event.position())); | |
147 return true; | |
148 } | |
149 | |
150 bool PopupListBox::handleMouseReleaseEvent(const PlatformMouseEvent& event) | |
151 { | |
152 if (m_capturingScrollbar) { | |
153 m_capturingScrollbar->mouseUp(event); | |
154 m_capturingScrollbar = nullptr; | |
155 return true; | |
156 } | |
157 | |
158 if (!isPointInBounds(event.position())) | |
159 return true; | |
160 | |
161 if (acceptIndex(pointToRowIndex(event.position())) && m_focusedElement) { | |
162 m_focusedElement->dispatchMouseEvent(event, EventTypeNames::mouseup); | |
163 m_focusedElement->dispatchMouseEvent(event, EventTypeNames::click); | |
164 | |
165 // Clear m_focusedElement here, because we cannot clear in hidePopup() | |
166 // which is called before dispatchMouseEvent() is called. | |
167 m_focusedElement = nullptr; | |
168 } | |
169 | |
170 return true; | |
171 } | |
172 | |
173 bool PopupListBox::handleWheelEvent(const PlatformWheelEvent& event) | |
174 { | |
175 if (!isPointInBounds(event.position())) { | |
176 abandon(); | |
177 return true; | |
178 } | |
179 | |
180 ScrollableArea::handleWheelEvent(event); | |
181 return true; | |
182 } | |
183 | |
184 // Should be kept in sync with handleKeyEvent(). | |
185 bool PopupListBox::isInterestedInEventForKey(int keyCode) | |
186 { | |
187 switch (keyCode) { | |
188 case VKEY_ESCAPE: | |
189 case VKEY_RETURN: | |
190 case VKEY_UP: | |
191 case VKEY_DOWN: | |
192 case VKEY_PRIOR: | |
193 case VKEY_NEXT: | |
194 case VKEY_HOME: | |
195 case VKEY_END: | |
196 case VKEY_TAB: | |
197 return true; | |
198 default: | |
199 return false; | |
200 } | |
201 } | |
202 | |
203 bool PopupListBox::handleTouchEvent(const PlatformTouchEvent&) | |
204 { | |
205 return false; | |
206 } | |
207 | |
208 bool PopupListBox::handleGestureEvent(const PlatformGestureEvent&) | |
209 { | |
210 return false; | |
211 } | |
212 | |
213 static bool isCharacterTypeEvent(const PlatformKeyboardEvent& event) | |
214 { | |
215 // Check whether the event is a character-typed event or not. | |
216 // We use RawKeyDown/Char/KeyUp event scheme on all platforms, | |
217 // so PlatformKeyboardEvent::Char (not RawKeyDown) type event | |
218 // is considered as character type event. | |
219 return event.type() == PlatformEvent::Char; | |
220 } | |
221 | |
222 bool PopupListBox::handleKeyEvent(const PlatformKeyboardEvent& event) | |
223 { | |
224 if (event.type() == PlatformEvent::KeyUp) | |
225 return true; | |
226 | |
227 if (!numItems() && event.windowsVirtualKeyCode() != VKEY_ESCAPE) | |
228 return true; | |
229 | |
230 switch (event.windowsVirtualKeyCode()) { | |
231 case VKEY_ESCAPE: | |
232 abandon(); // may delete this | |
233 return true; | |
234 case VKEY_RETURN: | |
235 if (m_selectedIndex == -1) { | |
236 hidePopup(); | |
237 // Don't eat the enter if nothing is selected. | |
238 return false; | |
239 } | |
240 acceptIndex(m_selectedIndex); // may delete this | |
241 return true; | |
242 case VKEY_UP: | |
243 selectPreviousRow(); | |
244 break; | |
245 case VKEY_DOWN: | |
246 selectNextRow(); | |
247 break; | |
248 case VKEY_PRIOR: | |
249 adjustSelectedIndex(-m_visibleRows); | |
250 break; | |
251 case VKEY_NEXT: | |
252 adjustSelectedIndex(m_visibleRows); | |
253 break; | |
254 case VKEY_HOME: | |
255 adjustSelectedIndex(-m_selectedIndex); | |
256 break; | |
257 case VKEY_END: | |
258 adjustSelectedIndex(m_items.size()); | |
259 break; | |
260 default: | |
261 if (!event.ctrlKey() && !event.altKey() && !event.metaKey() | |
262 && isPrintableChar(event.windowsVirtualKeyCode()) | |
263 && isCharacterTypeEvent(event)) | |
264 typeAheadFind(event); | |
265 break; | |
266 } | |
267 | |
268 if (event.altKey() && (event.keyIdentifier() == "Down" || event.keyIdentifie
r() == "Up")) { | |
269 hidePopup(); | |
270 return true; | |
271 } | |
272 | |
273 if (m_originalIndex != m_selectedIndex) { | |
274 // Keyboard events should update the selection immediately (but we don't | |
275 // want to fire the onchange event until the popup is closed, to match | |
276 // IE). We change the original index so we revert to that when the | |
277 // popup is closed. | |
278 m_acceptedIndexOnAbandon = m_selectedIndex; | |
279 | |
280 setOriginalIndex(m_selectedIndex); | |
281 m_popupClient->setTextFromItem(m_selectedIndex); | |
282 } | |
283 if (event.windowsVirtualKeyCode() == VKEY_TAB) { | |
284 // TAB is a special case as it should select the current item if any and | |
285 // advance focus. | |
286 if (m_selectedIndex >= 0) { | |
287 acceptIndex(m_selectedIndex); // May delete us. | |
288 // Return false so the TAB key event is propagated to the page. | |
289 return false; | |
290 } | |
291 // Call abandon() so we honor m_acceptedIndexOnAbandon if set. | |
292 abandon(); | |
293 // Return false so the TAB key event is propagated to the page. | |
294 return false; | |
295 } | |
296 | |
297 return true; | |
298 } | |
299 | |
300 HostWindow* PopupListBox::hostWindow() const | |
301 { | |
302 // Our parent is the root FrameView, so it is the one that has a | |
303 // HostWindow. FrameView::hostWindow() works similarly. | |
304 return parent() ? parent()->hostWindow() : 0; | |
305 } | |
306 | |
307 bool PopupListBox::shouldPlaceVerticalScrollbarOnLeft() const | |
308 { | |
309 return m_popupClient->menuStyle().textDirection() == RTL; | |
310 } | |
311 | |
312 // From HTMLSelectElement.cpp | |
313 static String stripLeadingWhiteSpace(const String& string) | |
314 { | |
315 int length = string.length(); | |
316 int i; | |
317 for (i = 0; i < length; ++i) | |
318 if (string[i] != noBreakSpace | |
319 && !isSpaceOrNewline(string[i])) | |
320 break; | |
321 | |
322 return string.substring(i, length - i); | |
323 } | |
324 | |
325 // From HTMLSelectElement.cpp, with modifications | |
326 void PopupListBox::typeAheadFind(const PlatformKeyboardEvent& event) | |
327 { | |
328 TimeStamp now = static_cast<TimeStamp>(currentTime() * 1000.0f); | |
329 TimeStamp delta = now - m_lastCharTime; | |
330 | |
331 // Reset the time when user types in a character. The time gap between | |
332 // last character and the current character is used to indicate whether | |
333 // user typed in a string or just a character as the search prefix. | |
334 m_lastCharTime = now; | |
335 | |
336 UChar c = event.windowsVirtualKeyCode(); | |
337 | |
338 String prefix; | |
339 int searchStartOffset = 1; | |
340 if (delta > typeAheadTimeoutMs) { | |
341 m_typedString = prefix = String(&c, 1); | |
342 m_repeatingChar = c; | |
343 } else { | |
344 m_typedString.append(c); | |
345 | |
346 if (c == m_repeatingChar) { | |
347 // The user is likely trying to cycle through all the items starting | |
348 // with this character, so just search on the character. | |
349 prefix = String(&c, 1); | |
350 } else { | |
351 m_repeatingChar = 0; | |
352 prefix = m_typedString; | |
353 searchStartOffset = 0; | |
354 } | |
355 } | |
356 | |
357 // Compute a case-folded copy of the prefix string before beginning the | |
358 // search for a matching element. This code uses foldCase to work around the | |
359 // fact that String::startWith does not fold non-ASCII characters. This code | |
360 // can be changed to use startWith once that is fixed. | |
361 String prefixWithCaseFolded(prefix.foldCase()); | |
362 int itemCount = numItems(); | |
363 int index = (max(0, m_selectedIndex) + searchStartOffset) % itemCount; | |
364 for (int i = 0; i < itemCount; i++, index = (index + 1) % itemCount) { | |
365 if (!isSelectableItem(index)) | |
366 continue; | |
367 | |
368 if (stripLeadingWhiteSpace(m_items[index]->label).foldCase().startsWith(
prefixWithCaseFolded)) { | |
369 selectIndex(index); | |
370 return; | |
371 } | |
372 } | |
373 } | |
374 | |
375 void PopupListBox::paint(GraphicsContext* gc, const IntRect& rect) | |
376 { | |
377 // Adjust coords for scrolled frame. | |
378 IntRect r = intersection(rect, frameRect()); | |
379 int tx = x() - scrollX() + ((shouldPlaceVerticalScrollbarOnLeft() && vertica
lScrollbar() && !verticalScrollbar()->isOverlayScrollbar()) ? verticalScrollbar(
)->width() : 0); | |
380 int ty = y() - scrollY(); | |
381 | |
382 r.move(-tx, -ty); | |
383 | |
384 // Set clip rect to match revised damage rect. | |
385 gc->save(); | |
386 gc->translate(static_cast<float>(tx), static_cast<float>(ty)); | |
387 gc->clip(r); | |
388 | |
389 // FIXME: Can we optimize scrolling to not require repainting the entire | |
390 // window? Should we? | |
391 for (int i = 0; i < numItems(); ++i) | |
392 paintRow(gc, r, i); | |
393 | |
394 // Special case for an empty popup. | |
395 if (!numItems()) | |
396 gc->fillRect(r, Color::white); | |
397 | |
398 gc->restore(); | |
399 | |
400 if (m_verticalScrollbar) { | |
401 GraphicsContextStateSaver stateSaver(*gc); | |
402 IntRect scrollbarDirtyRect = rect; | |
403 IntRect visibleAreaWithScrollbars(location(), visibleContentRect(Include
Scrollbars).size()); | |
404 scrollbarDirtyRect.intersect(visibleAreaWithScrollbars); | |
405 gc->translate(x(), y()); | |
406 scrollbarDirtyRect.moveBy(-location()); | |
407 gc->clip(IntRect(IntPoint(), visibleAreaWithScrollbars.size())); | |
408 | |
409 m_verticalScrollbar->paint(gc, scrollbarDirtyRect); | |
410 } | |
411 } | |
412 | |
413 static const int separatorPadding = 4; | |
414 static const int separatorHeight = 1; | |
415 static const int minRowHeight = 0; | |
416 static const int optionRowHeightForTouch = 28; | |
417 | |
418 void PopupListBox::paintRow(GraphicsContext* gc, const IntRect& rect, int rowInd
ex) | |
419 { | |
420 // This code is based largely on RenderListBox::paint* methods. | |
421 | |
422 IntRect rowRect = getRowBounds(rowIndex); | |
423 if (!rowRect.intersects(rect)) | |
424 return; | |
425 | |
426 PopupMenuStyle style = m_popupClient->itemStyle(rowIndex); | |
427 | |
428 // Paint background | |
429 Color backColor, textColor, labelColor; | |
430 if (rowIndex == m_selectedIndex) { | |
431 backColor = RenderTheme::theme().activeListBoxSelectionBackgroundColor()
; | |
432 textColor = RenderTheme::theme().activeListBoxSelectionForegroundColor()
; | |
433 labelColor = textColor; | |
434 } else { | |
435 backColor = style.backgroundColor(); | |
436 textColor = style.foregroundColor(); | |
437 #if OS(LINUX) || OS(ANDROID) | |
438 // On other platforms, the <option> background color is the same as the | |
439 // <select> background color. On Linux, that makes the <option> | |
440 // background color very dark, so by default, try to use a lighter | |
441 // background color for <option>s. | |
442 if (style.backgroundColorType() == PopupMenuStyle::DefaultBackgroundColo
r && RenderTheme::theme().systemColor(CSSValueButtonface) == backColor) | |
443 backColor = RenderTheme::theme().systemColor(CSSValueMenu); | |
444 #endif | |
445 | |
446 // FIXME: for now the label color is hard-coded. It should be added to | |
447 // the PopupMenuStyle. | |
448 labelColor = Color(115, 115, 115); | |
449 } | |
450 | |
451 // If we have a transparent background, make sure it has a color to blend | |
452 // against. | |
453 if (backColor.hasAlpha()) | |
454 gc->fillRect(rowRect, Color::white); | |
455 | |
456 gc->fillRect(rowRect, backColor); | |
457 | |
458 if (m_popupClient->itemIsSeparator(rowIndex)) { | |
459 IntRect separatorRect( | |
460 rowRect.x() + separatorPadding, | |
461 rowRect.y() + (rowRect.height() - separatorHeight) / 2, | |
462 rowRect.width() - 2 * separatorPadding, separatorHeight); | |
463 gc->fillRect(separatorRect, textColor); | |
464 return; | |
465 } | |
466 | |
467 if (!style.isVisible()) | |
468 return; | |
469 | |
470 gc->setFillColor(textColor); | |
471 | |
472 FontCachePurgePreventer fontCachePurgePreventer; | |
473 | |
474 Font itemFont = getRowFont(rowIndex); | |
475 // FIXME: http://crbug.com/19872 We should get the padding of individual opt
ion | |
476 // elements. This probably implies changes to PopupMenuClient. | |
477 bool rightAligned = m_popupClient->menuStyle().textDirection() == RTL; | |
478 int textX = 0; | |
479 int maxWidth = 0; | |
480 if (rightAligned) { | |
481 maxWidth = rowRect.width() - max<int>(0, m_popupClient->clientPaddingRig
ht()); | |
482 } else { | |
483 textX = max<int>(0, m_popupClient->clientPaddingLeft()); | |
484 maxWidth = rowRect.width() - textX; | |
485 } | |
486 // Prepare text to be drawn. | |
487 String itemText = m_popupClient->itemText(rowIndex); | |
488 | |
489 // Prepare the directionality to draw text. | |
490 TextRun textRun(itemText, 0, 0, TextRun::AllowTrailingExpansion, style.textD
irection(), style.hasTextDirectionOverride()); | |
491 // If the text is right-to-left, make it right-aligned by adjusting its | |
492 // beginning position. | |
493 if (rightAligned) | |
494 textX += maxWidth - itemFont.width(textRun); | |
495 | |
496 // Draw the item text. | |
497 int textY = rowRect.y() + itemFont.fontMetrics().ascent() + (rowRect.height(
) - itemFont.fontMetrics().height()) / 2; | |
498 TextRunPaintInfo textRunPaintInfo(textRun); | |
499 textRunPaintInfo.bounds = rowRect; | |
500 gc->drawBidiText(itemFont, textRunPaintInfo, IntPoint(textX, textY)); | |
501 } | |
502 | |
503 Font PopupListBox::getRowFont(int rowIndex) const | |
504 { | |
505 Font itemFont = m_popupClient->itemStyle(rowIndex).font(); | |
506 if (m_popupClient->itemIsLabel(rowIndex)) { | |
507 // Bold-ify labels (ie, an <optgroup> heading). | |
508 FontDescription d = itemFont.fontDescription(); | |
509 d.setWeight(FontWeightBold); | |
510 Font font(d); | |
511 font.update(nullptr); | |
512 return font; | |
513 } | |
514 | |
515 return itemFont; | |
516 } | |
517 | |
518 void PopupListBox::abandon() | |
519 { | |
520 RefPtrWillBeRawPtr<PopupListBox> protect(this); | |
521 | |
522 m_selectedIndex = m_originalIndex; | |
523 | |
524 hidePopup(); | |
525 | |
526 if (m_acceptedIndexOnAbandon >= 0) { | |
527 if (m_popupClient) | |
528 m_popupClient->valueChanged(m_acceptedIndexOnAbandon); | |
529 m_acceptedIndexOnAbandon = -1; | |
530 } | |
531 } | |
532 | |
533 int PopupListBox::pointToRowIndex(const IntPoint& point) | |
534 { | |
535 int y = scrollY() + point.y(); | |
536 | |
537 // FIXME: binary search if perf matters. | |
538 for (int i = 0; i < numItems(); ++i) { | |
539 if (y < m_items[i]->yOffset) | |
540 return i-1; | |
541 } | |
542 | |
543 // Last item? | |
544 if (y < contentsSize().height()) | |
545 return m_items.size()-1; | |
546 | |
547 return -1; | |
548 } | |
549 | |
550 bool PopupListBox::acceptIndex(int index) | |
551 { | |
552 // Clear m_acceptedIndexOnAbandon once user accepts the selected index. | |
553 if (m_acceptedIndexOnAbandon >= 0) | |
554 m_acceptedIndexOnAbandon = -1; | |
555 | |
556 if (index >= numItems()) | |
557 return false; | |
558 | |
559 if (index < 0) { | |
560 if (m_popupClient) { | |
561 // Enter pressed with no selection, just close the popup. | |
562 hidePopup(); | |
563 } | |
564 return false; | |
565 } | |
566 | |
567 if (isSelectableItem(index)) { | |
568 RefPtrWillBeRawPtr<PopupListBox> protect(this); | |
569 | |
570 // Hide ourselves first since valueChanged may have numerous side-effect
s. | |
571 hidePopup(); | |
572 | |
573 // Tell the <select> PopupMenuClient what index was selected. | |
574 m_popupClient->valueChanged(index); | |
575 | |
576 return true; | |
577 } | |
578 | |
579 return false; | |
580 } | |
581 | |
582 void PopupListBox::selectIndex(int index) | |
583 { | |
584 if (index < 0 || index >= numItems()) | |
585 return; | |
586 | |
587 bool isSelectable = isSelectableItem(index); | |
588 if (index != m_selectedIndex && isSelectable) { | |
589 invalidateRow(m_selectedIndex); | |
590 m_selectedIndex = index; | |
591 invalidateRow(m_selectedIndex); | |
592 | |
593 scrollToRevealSelection(); | |
594 m_popupClient->selectionChanged(m_selectedIndex); | |
595 } else if (!isSelectable) | |
596 clearSelection(); | |
597 } | |
598 | |
599 void PopupListBox::setOriginalIndex(int index) | |
600 { | |
601 m_originalIndex = m_selectedIndex = index; | |
602 } | |
603 | |
604 int PopupListBox::getRowHeight(int index) const | |
605 { | |
606 int minimumHeight = m_deviceSupportsTouch ? optionRowHeightForTouch : minRow
Height; | |
607 | |
608 if (index < 0 || m_popupClient->itemStyle(index).isDisplayNone()) | |
609 return minimumHeight; | |
610 | |
611 // Separator row height is the same size as itself. | |
612 if (m_popupClient->itemIsSeparator(index)) | |
613 return max(separatorHeight, minimumHeight); | |
614 | |
615 int fontHeight = getRowFont(index).fontMetrics().height(); | |
616 return max(fontHeight, minimumHeight); | |
617 } | |
618 | |
619 IntRect PopupListBox::getRowBounds(int index) | |
620 { | |
621 if (index < 0) | |
622 return IntRect(0, 0, visibleWidth(), getRowHeight(index)); | |
623 | |
624 return IntRect(0, m_items[index]->yOffset, visibleWidth(), getRowHeight(inde
x)); | |
625 } | |
626 | |
627 void PopupListBox::invalidateRow(int index) | |
628 { | |
629 if (index < 0) | |
630 return; | |
631 | |
632 // Invalidate in the window contents, as invalidateRect paints in the window
coordinates. | |
633 IntRect clipRect = contentsToWindow(getRowBounds(index)); | |
634 if (shouldPlaceVerticalScrollbarOnLeft() && verticalScrollbar() && !vertical
Scrollbar()->isOverlayScrollbar()) | |
635 clipRect.move(verticalScrollbar()->width(), 0); | |
636 invalidateRect(clipRect); | |
637 } | |
638 | |
639 void PopupListBox::scrollToRevealRow(int index) | |
640 { | |
641 if (index < 0) | |
642 return; | |
643 | |
644 IntRect rowRect = getRowBounds(index); | |
645 | |
646 if (rowRect.y() < scrollY()) { | |
647 // Row is above current scroll position, scroll up. | |
648 updateScrollbars(IntPoint(0, rowRect.y())); | |
649 } else if (rowRect.maxY() > scrollY() + visibleHeight()) { | |
650 // Row is below current scroll position, scroll down. | |
651 updateScrollbars(IntPoint(0, rowRect.maxY() - visibleHeight())); | |
652 } | |
653 } | |
654 | |
655 bool PopupListBox::isSelectableItem(int index) | |
656 { | |
657 ASSERT(index >= 0 && index < numItems()); | |
658 return m_items[index]->type == PopupItem::TypeOption && m_popupClient->itemI
sEnabled(index); | |
659 } | |
660 | |
661 void PopupListBox::clearSelection() | |
662 { | |
663 if (m_selectedIndex != -1) { | |
664 invalidateRow(m_selectedIndex); | |
665 m_selectedIndex = -1; | |
666 m_popupClient->selectionCleared(); | |
667 } | |
668 } | |
669 | |
670 void PopupListBox::selectNextRow() | |
671 { | |
672 adjustSelectedIndex(1); | |
673 } | |
674 | |
675 void PopupListBox::selectPreviousRow() | |
676 { | |
677 adjustSelectedIndex(-1); | |
678 } | |
679 | |
680 void PopupListBox::adjustSelectedIndex(int delta) | |
681 { | |
682 int targetIndex = m_selectedIndex + delta; | |
683 targetIndex = std::min(std::max(targetIndex, 0), numItems() - 1); | |
684 if (!isSelectableItem(targetIndex)) { | |
685 // We didn't land on an option. Try to find one. | |
686 // We try to select the closest index to target, prioritizing any in | |
687 // the range [current, target]. | |
688 | |
689 int dir = delta > 0 ? 1 : -1; | |
690 int testIndex = m_selectedIndex; | |
691 int bestIndex = m_selectedIndex; | |
692 bool passedTarget = false; | |
693 while (testIndex >= 0 && testIndex < numItems()) { | |
694 if (isSelectableItem(testIndex)) | |
695 bestIndex = testIndex; | |
696 if (testIndex == targetIndex) | |
697 passedTarget = true; | |
698 if (passedTarget && bestIndex != m_selectedIndex) | |
699 break; | |
700 | |
701 testIndex += dir; | |
702 } | |
703 | |
704 // Pick the best index, which may mean we don't change. | |
705 targetIndex = bestIndex; | |
706 } | |
707 | |
708 // Select the new index, and ensure its visible. We do this regardless of | |
709 // whether the selection changed to ensure keyboard events always bring the | |
710 // selection into view. | |
711 selectIndex(targetIndex); | |
712 scrollToRevealSelection(); | |
713 } | |
714 | |
715 void PopupListBox::hidePopup() | |
716 { | |
717 if (parent()) { | |
718 if (m_container->client()) | |
719 m_container->client()->popupClosed(m_container); | |
720 m_container->notifyPopupHidden(); | |
721 } | |
722 | |
723 if (m_popupClient) | |
724 m_popupClient->popupDidHide(); | |
725 } | |
726 | |
727 void PopupListBox::updateFromElement() | |
728 { | |
729 clear(); | |
730 | |
731 int size = m_popupClient->listSize(); | |
732 for (int i = 0; i < size; ++i) { | |
733 PopupItem::Type type; | |
734 if (m_popupClient->itemIsSeparator(i)) | |
735 type = PopupItem::TypeSeparator; | |
736 else if (m_popupClient->itemIsLabel(i)) | |
737 type = PopupItem::TypeGroup; | |
738 else | |
739 type = PopupItem::TypeOption; | |
740 m_items.append(new PopupItem(m_popupClient->itemText(i), type)); | |
741 m_items[i]->enabled = isSelectableItem(i); | |
742 PopupMenuStyle style = m_popupClient->itemStyle(i); | |
743 m_items[i]->textDirection = style.textDirection(); | |
744 m_items[i]->hasTextDirectionOverride = style.hasTextDirectionOverride(); | |
745 m_items[i]->displayNone = style.isDisplayNone(); | |
746 } | |
747 | |
748 m_selectedIndex = m_popupClient->selectedIndex(); | |
749 setOriginalIndex(m_selectedIndex); | |
750 | |
751 layout(); | |
752 } | |
753 | |
754 void PopupListBox::setMaxWidthAndLayout(int maxWidth) | |
755 { | |
756 m_maxWindowWidth = maxWidth; | |
757 layout(); | |
758 } | |
759 | |
760 int PopupListBox::getRowBaseWidth(int index) | |
761 { | |
762 Font font = getRowFont(index); | |
763 PopupMenuStyle style = m_popupClient->itemStyle(index); | |
764 String text = m_popupClient->itemText(index); | |
765 if (text.isEmpty()) | |
766 return 0; | |
767 TextRun textRun(text, 0, 0, TextRun::AllowTrailingExpansion, style.textDirec
tion(), style.hasTextDirectionOverride()); | |
768 return font.width(textRun); | |
769 } | |
770 | |
771 void PopupListBox::layout() | |
772 { | |
773 bool isRightAligned = m_popupClient->menuStyle().textDirection() == RTL; | |
774 | |
775 // Size our child items. | |
776 int baseWidth = 0; | |
777 int paddingWidth = 0; | |
778 int lineEndPaddingWidth = 0; | |
779 int y = 0; | |
780 for (int i = 0; i < numItems(); ++i) { | |
781 // Place the item vertically. | |
782 m_items[i]->yOffset = y; | |
783 if (m_popupClient->itemStyle(i).isDisplayNone()) | |
784 continue; | |
785 y += getRowHeight(i); | |
786 | |
787 // Ensure the popup is wide enough to fit this item. | |
788 baseWidth = max(baseWidth, getRowBaseWidth(i)); | |
789 // FIXME: http://b/1210481 We should get the padding of individual | |
790 // option elements. | |
791 paddingWidth = max<int>(paddingWidth, | |
792 m_popupClient->clientPaddingLeft() + m_popupClient->clientPaddingRig
ht()); | |
793 lineEndPaddingWidth = max<int>(lineEndPaddingWidth, | |
794 isRightAligned ? m_popupClient->clientPaddingLeft() : m_popupClient-
>clientPaddingRight()); | |
795 } | |
796 | |
797 // Calculate scroll bar width. | |
798 int windowHeight = 0; | |
799 m_visibleRows = std::min(numItems(), maxVisibleRows); | |
800 | |
801 for (int i = 0; i < m_visibleRows; ++i) { | |
802 int rowHeight = getRowHeight(i); | |
803 | |
804 // Only clip the window height for non-Mac platforms. | |
805 if (windowHeight + rowHeight > m_maxHeight) { | |
806 m_visibleRows = i; | |
807 break; | |
808 } | |
809 | |
810 windowHeight += rowHeight; | |
811 } | |
812 | |
813 // Set our widget and scrollable contents sizes. | |
814 int scrollbarWidth = 0; | |
815 if (m_visibleRows < numItems()) { | |
816 if (!ScrollbarTheme::theme()->usesOverlayScrollbars()) | |
817 scrollbarWidth = ScrollbarTheme::theme()->scrollbarThickness(); | |
818 | |
819 // Use minEndOfLinePadding when there is a scrollbar so that we use | |
820 // as much as (lineEndPaddingWidth - minEndOfLinePadding) padding | |
821 // space for scrollbar and allow user to use CSS padding to make the | |
822 // popup listbox align with the select element. | |
823 paddingWidth = paddingWidth - lineEndPaddingWidth + minEndOfLinePadding; | |
824 } | |
825 | |
826 int windowWidth = baseWidth + scrollbarWidth + paddingWidth; | |
827 if (windowWidth > m_maxWindowWidth) { | |
828 // windowWidth exceeds m_maxWindowWidth, so we have to clip. | |
829 windowWidth = m_maxWindowWidth; | |
830 baseWidth = windowWidth - scrollbarWidth - paddingWidth; | |
831 m_baseWidth = baseWidth; | |
832 } | |
833 int contentWidth = windowWidth - scrollbarWidth; | |
834 | |
835 if (windowWidth < m_baseWidth) { | |
836 windowWidth = m_baseWidth; | |
837 contentWidth = m_baseWidth - scrollbarWidth; | |
838 } else { | |
839 m_baseWidth = baseWidth; | |
840 } | |
841 | |
842 resize(windowWidth, windowHeight); | |
843 setContentsSize(IntSize(contentWidth, getRowBounds(numItems() - 1).maxY())); | |
844 | |
845 if (hostWindow()) | |
846 scrollToRevealSelection(); | |
847 | |
848 invalidate(); | |
849 } | |
850 | |
851 void PopupListBox::clear() | |
852 { | |
853 deleteAllValues(m_items); | |
854 m_items.clear(); | |
855 } | |
856 | |
857 bool PopupListBox::isPointInBounds(const IntPoint& point) | |
858 { | |
859 return numItems() && IntRect(0, 0, width(), height()).contains(point); | |
860 } | |
861 | |
862 int PopupListBox::popupContentHeight() const | |
863 { | |
864 return height(); | |
865 } | |
866 | |
867 void PopupListBox::invalidateRect(const IntRect& rect) | |
868 { | |
869 if (HostWindow* h = hostWindow()) | |
870 h->invalidateContentsAndRootView(rect); | |
871 } | |
872 | |
873 IntRect PopupListBox::windowClipRect() const | |
874 { | |
875 IntRect clipRect = visibleContentRect(); | |
876 if (shouldPlaceVerticalScrollbarOnLeft() && verticalScrollbar() && !vertical
Scrollbar()->isOverlayScrollbar()) | |
877 clipRect.move(verticalScrollbar()->width(), 0); | |
878 return contentsToWindow(clipRect); | |
879 } | |
880 | |
881 void PopupListBox::invalidateScrollbarRect(Scrollbar* scrollbar, const IntRect&
rect) | |
882 { | |
883 // Add in our offset within the FrameView. | |
884 IntRect dirtyRect = rect; | |
885 dirtyRect.move(scrollbar->x(), scrollbar->y()); | |
886 invalidateRect(dirtyRect); | |
887 } | |
888 | |
889 bool PopupListBox::isActive() const | |
890 { | |
891 // FIXME | |
892 return true; | |
893 } | |
894 | |
895 bool PopupListBox::scrollbarsCanBeActive() const | |
896 { | |
897 return isActive(); | |
898 } | |
899 | |
900 IntRect PopupListBox::scrollableAreaBoundingBox() const | |
901 { | |
902 return windowClipRect(); | |
903 } | |
904 | |
905 // FIXME: The following methods are based on code in FrameView, with | |
906 // simplifications for the constraints of PopupListBox (e.g. only vertical | |
907 // scrollbar, not horizontal). This functionality should be moved into | |
908 // ScrollableArea after http://crbug.com/417782 is fixed. | |
909 | |
910 void PopupListBox::setHasVerticalScrollbar(bool hasBar) | |
911 { | |
912 if (hasBar && !m_verticalScrollbar) { | |
913 m_verticalScrollbar = Scrollbar::create(this, VerticalScrollbar, Regular
Scrollbar); | |
914 m_verticalScrollbar->setParent(this); | |
915 didAddScrollbar(m_verticalScrollbar.get(), VerticalScrollbar); | |
916 m_verticalScrollbar->styleChanged(); | |
917 } else if (!hasBar && m_verticalScrollbar) { | |
918 m_verticalScrollbar->setParent(0); | |
919 willRemoveScrollbar(m_verticalScrollbar.get(), VerticalScrollbar); | |
920 m_verticalScrollbar = nullptr; | |
921 } | |
922 } | |
923 | |
924 Scrollbar* PopupListBox::scrollbarAtWindowPoint(const IntPoint& windowPoint) | |
925 { | |
926 return m_verticalScrollbar && m_verticalScrollbar->frameRect().contains( | |
927 convertFromContainingWindow(windowPoint)) ? m_verticalScrollbar.get() :
0; | |
928 } | |
929 | |
930 IntRect PopupListBox::contentsToWindow(const IntRect& contentsRect) const | |
931 { | |
932 IntRect viewRect = contentsRect; | |
933 viewRect.moveBy(-scrollPosition()); | |
934 return convertToContainingWindow(viewRect); | |
935 } | |
936 | |
937 void PopupListBox::setContentsSize(const IntSize& newSize) | |
938 { | |
939 if (contentsSize() == newSize) | |
940 return; | |
941 m_contentsSize = newSize; | |
942 updateScrollbars(scrollPosition()); | |
943 } | |
944 | |
945 void PopupListBox::setFrameRect(const IntRect& newRect) | |
946 { | |
947 IntRect oldRect = frameRect(); | |
948 if (newRect == oldRect) | |
949 return; | |
950 | |
951 Widget::setFrameRect(newRect); | |
952 updateScrollbars(scrollPosition()); | |
953 // NOTE: We do not need to call m_verticalScrollbar->frameRectsChanged as | |
954 // Scrollbar does not implement it. | |
955 } | |
956 | |
957 IntRect PopupListBox::visibleContentRect(IncludeScrollbarsInRect scrollbarInclus
ion) const | |
958 { | |
959 // NOTE: Unlike FrameView we do not need to incorporate any scaling factor, | |
960 // and there is only one scrollbar to exclude. | |
961 IntSize size = frameRect().size(); | |
962 Scrollbar* verticalBar = verticalScrollbar(); | |
963 if (scrollbarInclusion == ExcludeScrollbars && verticalBar && !verticalBar->
isOverlayScrollbar()) { | |
964 size.setWidth(std::max(0, size.width() - verticalBar->width())); | |
965 } | |
966 return IntRect(m_scrollOffset, size); | |
967 } | |
968 | |
969 void PopupListBox::updateScrollbars(IntPoint desiredOffset) | |
970 { | |
971 IntSize oldVisibleSize = visibleContentRect().size(); | |
972 adjustScrollbarExistence(); | |
973 updateScrollbarGeometry(); | |
974 IntSize newVisibleSize = visibleContentRect().size(); | |
975 | |
976 if (newVisibleSize.width() > oldVisibleSize.width()) { | |
977 if (shouldPlaceVerticalScrollbarOnLeft()) | |
978 invalidateRect(IntRect(0, 0, newVisibleSize.width() - oldVisibleSize
.width(), newVisibleSize.height())); | |
979 else | |
980 invalidateRect(IntRect(oldVisibleSize.width(), 0, newVisibleSize.wid
th() - oldVisibleSize.width(), newVisibleSize.height())); | |
981 } | |
982 | |
983 desiredOffset = desiredOffset.shrunkTo(maximumScrollPosition()); | |
984 desiredOffset = desiredOffset.expandedTo(minimumScrollPosition()); | |
985 | |
986 if (desiredOffset != scrollPosition()) | |
987 ScrollableArea::scrollToOffsetWithoutAnimation(desiredOffset); | |
988 } | |
989 | |
990 void PopupListBox::adjustScrollbarExistence() | |
991 { | |
992 bool needsVerticalScrollbar = contentsSize().height() > visibleHeight(); | |
993 if (!!m_verticalScrollbar != needsVerticalScrollbar) { | |
994 setHasVerticalScrollbar(needsVerticalScrollbar); | |
995 contentsResized(); | |
996 } | |
997 } | |
998 | |
999 void PopupListBox::updateScrollbarGeometry() | |
1000 { | |
1001 if (m_verticalScrollbar) { | |
1002 int clientHeight = visibleHeight(); | |
1003 IntRect oldRect(m_verticalScrollbar->frameRect()); | |
1004 IntRect vBarRect(shouldPlaceVerticalScrollbarOnLeft() ? 0 : (width() - m
_verticalScrollbar->width()), | |
1005 0, m_verticalScrollbar->width(), height()); | |
1006 m_verticalScrollbar->setFrameRect(vBarRect); | |
1007 if (oldRect != m_verticalScrollbar->frameRect()) | |
1008 m_verticalScrollbar->invalidate(); | |
1009 | |
1010 m_verticalScrollbar->setEnabled(contentsSize().height() > clientHeight); | |
1011 m_verticalScrollbar->setProportion(clientHeight, contentsSize().height()
); | |
1012 m_verticalScrollbar->offsetDidChange(); | |
1013 // NOTE: PopupListBox does not support suppressing scrollbars. | |
1014 } | |
1015 } | |
1016 | |
1017 IntPoint PopupListBox::convertChildToSelf(const Widget* child, const IntPoint& p
oint) const | |
1018 { | |
1019 // NOTE: m_verticalScrollbar is the only child. | |
1020 IntPoint newPoint = point; | |
1021 newPoint.moveBy(child->location()); | |
1022 return newPoint; | |
1023 } | |
1024 | |
1025 IntPoint PopupListBox::convertSelfToChild(const Widget* child, const IntPoint& p
oint) const | |
1026 { | |
1027 // NOTE: m_verticalScrollbar is the only child. | |
1028 IntPoint newPoint = point; | |
1029 newPoint.moveBy(-child->location()); | |
1030 return newPoint; | |
1031 } | |
1032 | |
1033 int PopupListBox::scrollSize(ScrollbarOrientation orientation) const | |
1034 { | |
1035 return (orientation == HorizontalScrollbar || !m_verticalScrollbar) ? | |
1036 0 : m_verticalScrollbar->totalSize() - m_verticalScrollbar->visibleSize(
); | |
1037 } | |
1038 | |
1039 void PopupListBox::setScrollOffset(const IntPoint& newOffset) | |
1040 { | |
1041 // NOTE: We do not support any "fast path" for scrolling. When the scroll | |
1042 // offset changes, we just repaint the whole popup. | |
1043 IntSize scrollDelta = newOffset - m_scrollOffset; | |
1044 if (scrollDelta == IntSize()) | |
1045 return; | |
1046 m_scrollOffset = newOffset; | |
1047 | |
1048 if (HostWindow* window = hostWindow()) { | |
1049 IntRect clipRect = windowClipRect(); | |
1050 IntRect updateRect = clipRect; | |
1051 updateRect.intersect(convertToContainingWindow(IntRect((shouldPlaceVerti
calScrollbarOnLeft() && verticalScrollbar()) ? verticalScrollbar()->width() : 0,
0, visibleWidth(), visibleHeight()))); | |
1052 window->invalidateContentsForSlowScroll(updateRect); | |
1053 } | |
1054 } | |
1055 | |
1056 IntPoint PopupListBox::maximumScrollPosition() const | |
1057 { | |
1058 IntPoint maximumOffset(contentsSize().width() - visibleWidth() - scrollOrigi
n().x(), contentsSize().height() - visibleHeight() - scrollOrigin().y()); | |
1059 maximumOffset.clampNegativeToZero(); | |
1060 return maximumOffset; | |
1061 } | |
1062 | |
1063 IntPoint PopupListBox::minimumScrollPosition() const | |
1064 { | |
1065 return IntPoint(-scrollOrigin().x(), -scrollOrigin().y()); | |
1066 } | |
1067 | |
1068 } // namespace blink | |
OLD | NEW |