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